実行セマンティクス、制約、セキュリティ

Scriptランタイムでどのように動作するか(順序、トランザクション、エラー、ロック)、保存時にどのような静的制約を受けるか、そして セキュリティモデルを整理します。文法は Statement カタログ値式、実践的な組み合わせは クックブック を参照してください。

実行順序

  • statements上から下へ順次実行されます。Return に到達すると、その時点で終了します。
  • 実行は、呼び出しリクエストを処理する経路でインラインに行われます。 呼び出しのレスポンスがそのまま実行結果であり(レスポンスの形は Script 概要のリクエストとレスポンス を参照)、1 回の実行に与えられる時間は下の 時間予算 で扱います。

実行セマンティクス

Guard(事前条件)

専用の guard 文はありません。Ifthen:[Return] で表現します。条件に違反した場合は結果を返し、以降の statement は実行しません(guard のない Script も当然可能です)。

{ "type": "If", "condition": { "<": [ "{ /wallet/fields/balance/en-US }", "{ /payload/fields/cost }" ] },
  "then": [ { "type": "Return", "value": { "ok": false, "reason": "insufficient credit" }, "statusCode": 402 } ] }

トランザクションなしと best-effort 補償

Scriptトランザクションではありません。 失敗した場合、エンジンはそれまでに行った操作の補償(compensation)を試み、エラーの原因を返しますが、次の制限があります(設計上許容しています)。

  • 削除の取り消しは新しい sys.id を作るため、これを指していた参照が壊れます。
  • 外部効果Http)は不可逆です(すでに送信された呼び出しと課金は取り消せません)。
  • 補償がまったく実行されず、未補償の状態が残ることがあります。

真の原子性が必要な場合は、ユーザーが Script で直接補償するか、取り消せない操作(外部呼び出しなど)を最後に配置します。「連鎖はできるがロールバックはできず、しかも安全に見える」順序が最も危険です。

楽観的ロック

update/patch の競合ResourceUpdateResourcePatchversion で絞ります。version(値式、Int)を渡すと、対象の現在の sys.version一致する場合のみ更新し、不一致の場合は バージョン競合エラーで abort されます(Try/catch で局所的に処理できます)。省略すると検査なしの last-write-wins になります。通常は ResourceReadResourceFind で先に読み込み、その sys.version を渡します(クックブックの楽観的ロック CAS を参照)。

origin 基準の書き込み

書き込みは常に origin(draft)に反映され、delivery(CDA/ACDA)への公開は publish で制御します(ResourceCreate/ResourceUpdate/ResourcePatchpublish、または ResourcePublish/ResourceUnpublish)。

何が失敗か

  • 本当の失敗は statement のランタイムエラーです。Http の最終 status が 400 以上(4xx·5xx。ignoreStatusCode: true なら失敗ではない)かタイムアウト、応答本文が 10MiB を超える、リソース操作の失敗(対象なし、バージョン競合、未対応の演算など)。このような失敗は エンジンが abort して補償し、Try/catch/finally で局所的に処理できます。
  • Return はエラーではなく正常な早期終了です。catch の対象ではありません(ユーザーによる throw の概念はありません)。
  • catch の中では /error{ message } を参照します。どの文で失敗したかは含まれません。

サーバー側集計は件数のみ

件数は ResourceCount がサーバー側で数えます。項目を読み取ってこないため、処理項目数の上限を受けません。

sum と group-by には専用のサーバー操作がありません。そうした集計は ResourceForEach で巡回しながら SetVar と JsonLogic で自分で計算する必要があり、そのため 処理項目数の上限に縛られます(数百万件の集計には不向きです)。件数だけが必要なら、巡回せずに ResourceCount を使います。

待機と遅延なし

Script には Delay 文はありません。Script一度実行されて終わりであり、外部 job が終わるまで内部で待機したりポーリングしたりしません。

静的制約(保存時の検証)

以下は Script保存(作成/更新)する時点で検査されます。違反すると保存が拒否されます(ランタイムではなく作成時の失敗です)。どの違反がどのコードで拒否されるかは エラー で扱います。

制約
定義ごとの 外部呼び出しの最大数HttpEmailSendプランごと(料金プラン 参照)
ResourceForEach総処理項目数の最大(宣言した limit がなければこの値まで巡回、マッチが残ったまま達すると失敗)10,000
定義ごとの SetVar の最大数(ネスト含む)10
定義ごとの Cache の最大数(ネスト含む、演算に関係なく合算)5。超えると保存を拒否
LoopResourceForEach のブロック内の Cache保存を拒否
Cache.keyリテラルのみ、最大 128 文字。値式であれば保存を拒否
Cache.ttl1 から 30 秒のあいだ、省略時は 5 秒。外れると保存を拒否
定義ごとの 全 statement の最大数(ネスト含む)プランごと(料金プラン 参照)
Http.retry の上限2
Regex.pattern の長さ128 文字
ServiceUser変更する文保存を拒否。読み取りの 3 文だけがこのリソースを受け取ります
anonymousCallEnabledtrue の場合の wherecreatedBy: ":self"保存を拒否

上の表の固定上限はプラットフォームが定めた値なので、プランに関係なく同じです。一方、定義ごとの全 statement 数と外部呼び出し数はプランごとの上限です。この二つは妥当性エラーではなくプランの上限なので、超えると保存・更新がプラン上限超過で拒否され(同じ定義でも上位プランでは許可)、アップグレードで解除されます。プランごとの数値は 料金プラン にあります。

Media ファイルのインジェストは、HttpEmailSend のような外部呼び出しとは異なり、定義ごとの外部呼び出しの上限には含まれません。

ResourceForEach は子を所有する複合 statement なので、それ自体は外部呼び出しの数には数えません。 onEach 内の外部呼び出し文(HttpEmailSend)が数えられます(静的には 1 として数えますが、巡回して項目ごとに実際に実行されます)。onEach には外部呼び出しや Media ファイルのインジェストを含められ、これは Loop の body も同じです。反復が実際に何回まわるかはこのカウントには入らず、代わりに下の 時間予算 で掛け算として入ります。

値の長さの上限(ランタイム)

署名とテキスト処理の文、そして Cache は、扱う値の大きさに上限があります。式の長さではなく、その式が resolve された値の長さであり({ /rawPayload } の 16 文字が数十 KB を指します)、そのため保存の時点ではなく実行中に検査されます。

対象上限超えると
Signaturevalue65,536 文字その文が失敗(status 422)
Hashvalue128 文字その文が失敗(status 422)
Regexvalue10,240 文字(10KiB)その文が失敗(status 400)
Cachevalue10,240 バイト(10KiB)その文が失敗(status 422)
  • 4 つとも他のランタイム失敗と同じなので、Try/catch で局所的に処理できます。
  • Signature の上限は、実際のプロバイダーが送ってくる本文の大きさに合わせたものです(決済イベントは数 KB、注文の Webhook は数十 KB に達します)。Hash はフィールドをいくつかつなげる箇所なので、はるかに狭いです。
  • Regex.pattern の 128 文字は、上の 静的制約 にある保存時の検査です。この長さは暴走を防ぐ仕掛けではありません((a+)+$ は 6 文字でも危険です)。暴走を防ぐのは、パターンをリテラルとしてのみ書かせる規則と下の時間予算であり、長さが約束するのは人が読んで検討できる大きさだけです。

時間予算(ランタイム)

1 回の実行に与えられる時間は、1 つの式で決まります: min(30秒 + 各文が宣言した時間の合計, 180秒)

  • 予算はその Script から計算します。 基本予算に、定義が宣言した時間だけを加えます。宣言された時間は HttpEmailSendtimeoutMs の一つだけです。Http は再試行のたびに自分の timeoutMs を再び使うため timeoutMs × (1 + retry) で数え、EmailSend は再試行しないため 1 回として数えます。timeoutMs を書かなければデフォルト値(Http は 30 秒、EmailSend は 10 秒)で数えます。
  • 宣言された時間がない作業は 30 秒の基本予算から出ます。 リソースの読み取りと書き込み、Media ファイルのインジェスト、繰り返しが中で行うことがここに入ります。そのため基本予算は形式的な値ではなく、実際の取り分です。
  • 合わせ方は文の構造に従います。 順に並べた文は足し、If は二つの分岐のうち大きいほうParallel はブランチのうち最も大きいものを取ります。Loop は body に繰り返しの回数(maxIterations、宣言がなければ 10,000)を、ResourceForEachonEach に処理項目数(limit、宣言がなければ 10,000)を掛けます。
  • 外部呼び出しのない繰り返しは宣言時間が 0 です。 そのため 30 秒の基本予算が実質の限度になり、繰り返しを含む Script が実際に引っかかる地点もここです。
  • 上限の 180 秒は保存を止めるのではなく、切り詰めます。 計算の結果が上限を超えてもその Script は保存されて実行され、180 秒に達したらそこで中断されます。

プランごとの個数上限

ScriptOrganization ごとの個数がプランごとに制限されます。

PlanScript の個数
Free10
Basic30
Pro100
Enterprise無制限

これとは別に、1 つの Script 定義が含められる statement 数と外部呼び出し(HttpEmailSend)数もプランごとに制限されます。定義を保存・更新するときにそのプランの上限を超えると拒否され、具体的な数値は 料金プラン を参照してください。

上限に達すると新しい Script の作成が拒否されます。

セキュリティモデル

secret ヘッダー

Http.headerssecret:true の項目は CMA(管理者)専用であり、エンドユーザー(ServiceUser)には公開されず、送信の直前にのみ復号されます。LLM API キーのような秘密をここに置きます(App Bundle にパッキングされる際も secret 値はマスキングされ、元の Space の外には出ません)。

Signaturesecret は、Space の中ではこの扱いを受けません。 暗号化されず、定義に書いたまま保存されるため、その Script を読める役割には値が見えます。会員(ServiceUser)は Script の定義を読めません(照会とオーサリングは CMA 専用であり、ACMA には Script API がありません)。検証用の鍵を置く Script は、それを読める役割を狭くしておくほうが安全です。

Space の外へ出るときは違います。その ScriptApp Bundle にパッキングされる際、Signaturesecretマスキングされ、元の Space の外には出ません。 Http.headerssecret フラグの付いた項目と Authorization ヘッダーを隠す一方、Signaturesecret はフィールドそれ自体が署名鍵なので、条件なしで隠されます。IfLoopTry の中にネストした Signature も一緒に隠されます。

実行アイデンティティと認可

  • 実行アイデンティティ: 実行中のすべてのリソース操作は、/execute を呼び出したユーザーのアイデンティティで実行されます。作成または更新されるリソースの createdBy/updatedBy は呼び出し元となり、createdBy: ":self" スコープも呼び出し元を基準に解決されます。例外は匿名呼び出しです。 /execute/anonymous で入ってきた実行には呼び出し元がいないため、どちらも 作成者を基準に解決されます(匿名呼び出し)。
  • 認可の境界は二つであり、ランタイムでは statement ごとにリソース権限を再検査しません。
    1. 作成時(保存): Script を保存する際に、作成者がそれらの statement が使う リソースとアクションの権限を実際に持っているかを検査します。一つでも欠けていれば保存が拒否されます。つまり、権限のない操作を含む Script はそもそも保存されません。リソースを選ぶ文は、leaf であれブロックを所有する ResourceForEach であれ、すべてこの検査を受けます。すでに保存してある定義も更新するときに再び検査されるため、権限が回収された後はその定義を直して保存できません。
      • 会員ディレクトリ(ServiceUser)は権限マップではなく設定の軸で検査します。 読み取りの 3 文に resource: "ServiceUser" を書くには、作成者の SpaceRole settingsSETTING_SERVICE_LOGIN(または SETTING_ALL)が必要です(SpaceRole の settings)。会員ディレクトリが、他のすべての経路でも Space 設定が管掌するリソースだからです。
      • 会員を変更する文は、どの役割でも保存できません。 Script から会員を作る・直す・消す道がそもそもないため、権限不足(403)ではなく書き方を誤った文(400)として拒否されます。権限を足せば閉じられる隙間ではないという意味です。
    2. 呼び出し時(/execute: 呼び出し元の Script Execute 権限のみを検査します。権限がなければ 403 です。通過すると、statement ごとのリソース権限はランタイムで再確認せずに実行します。プログラミングにおける関数の実行権限と同じ方式です。関数を実行する権限さえあれば、その中の個々の操作の権限は再び問われません。匿名呼び出しの経路にはこの検査がありません。 検査する呼び出し元がいないからであり、そのためその経路を開くことは、Script 1 つを認証なしで公開することと同じです。
  • 直接呼び出しのブロック(directCallEnabled: ScriptdirectCallEnabledfalse の場合、/execute への直接呼び出し自体が拒否されます。このゲートは Execute 権限の検査を通過したあとにかかるため、Execute 権限があってもブロックされます。権限のない呼び出し元は、このゲートに達する前に 403 を受け取ります。このゲートはそのエンドポイントにのみあるので、Webhook の連携アクション(script)と Scheduler はそのまま実行します。デフォルト値は true(直接呼び出し許可)です。
  • 匿名呼び出し(anonymousCallEnabled: デフォルト値は false です。true にすると、その Script だけが認証のない専用経路(/execute/anonymous)でも実行され、そのときの 実行アイデンティティは呼び出し元ではなく作成者です。上の二つの境界のうち呼び出し時の検査(Execute 権限)がその経路にはないため、実質的な認証は Script が自分で行います(受け取ったリクエストの署名検証)。有効にする条件と保存の規則は 匿名呼び出し で扱います。
  • 所有権スコープ: where フィルターの createdBy: ":self" は「現在の呼び出し元が作成したものだけ」を意味します(例: 自分のウォレットのみ照会)。匿名呼び出しを許可した Script では、このフィルターを使えません。呼び出し元がなく作成者に解決されるため、所有権スコープという本来の意味が成り立たないからです。
  • 委任された権限(作成時の注意): 上記の二つの境界を合わせると、Script の実行は 作成者の権限を委任されて行われるのと同じです。呼び出し元は Execute 一つだけあればよく、Script 内の statement は作成者が保存時に認可された範囲でそのまま実行されます。したがって、呼び出し元が自分自身ではできないリソース操作も Script を通じて起こりえます。作成者に付与された権限がそのままその Script の影響範囲となるため、Script に含める動作は慎重に決めます。

要約チェックリスト

保存前に次を確認します。

  • 匿名呼び出し(anonymousCallEnabled)を有効にしたなら、wherecreatedBy: ":self" がなく、受け取ったリクエストを検証する文(Signature など)を先頭に置いています。
  • 外部呼び出し(HttpEmailSend)の数と全 statement の数はプランの上限以内で、SetVar は 10 個以下、Cache は 5 個以下です。
  • Cache を使ったなら、key をリテラルで書き、LoopResourceForEach の中には置いていません。
  • ResourceForEach で大きな集合を巡回する場合は、limit を宣言するか、完走可能なサイズかを確認しました。
  • 繰り返し(LoopResourceForEach)を含めたなら、その繰り返しが掛け算として 時間予算 に入ることを確認しました(外部呼び出しがなければ 30 秒の基本予算が限度です)。
  • secret 値は Http.headerssecret:true でのみ入れています(Signature.secret は暗号化して保存されないため、その Script を読める役割を確認しました)。
  • 署名検証に使うメッセージは /payload ではなく { /rawPayload } で取っています。
  • 会員(ServiceUser)を読む文があれば、作成者に SETTING_SERVICE_LOGIN があり、そのリソースを変更する文は入れていません。
  • 取り消せない操作(外部呼び出し)はできるだけ後ろに置いています。
  • update/patch の競合が心配なら ResourceUpdateResourcePatchversion を使います。
  • 結果を返すには Return.value を明示しています。

エラー

定義の形が静的制約に反して保存が拒否されるときに出るコードです。値式の規則に反したコードは 値式のエラー に、呼び出し・削除のときに出るコードは エンドポイントのエラー にあります。すべてのリソースに共通するコードは 共通エラー を参照してください。

コード条件
WGL400066一つの定義に含めた Cache 文が 5 個を超えています。
WGL400068Cache 文を LoopResourceForEach のブロック内に置きました。
WGL400067Cache 文の key に、リテラルではなく { /pointer } 参照を書きました。
WGL400065Cache 文の ttl が許容範囲を外れています。
WGL400063Cache 文で action に該当しないフィールドを書きました(GetttlSetdefaultValue)。
WGL400060書き込み系の文(ResourceCreateResourceUpdateResourcePatchResourceDelete と公開・アーカイブの文)の resource"ServiceUser" を書きました。
WGL400061匿名呼び出しを許可した ScriptanonymousCallEnabled)で、読み取り文の wherecreatedBy: ":self" を書きました。
WGL400023一つの定義に含めた SetVar 文が 10 個を超えています。
WGL400026Http 文の retry が上限の 2 を超えています。
WGL400036ResourceForEach が処理できる項目数の上限を超えています。
WGL429005一つの定義に含めた statement の総数が料金プランの上限を超えています。
WGL429006一つの定義に含めた外部呼び出し(HttpEmailSend)の数が料金プランの上限を超えています。
WGL403015定義に含めた文が扱うリソースとアクションの権限を、作成者が持っていません。権限があっても、その許可に contentTypecreatedBytag フィルターが付いていれば拒否されます。 条件のない許可でなければなりません。唯一の例外は ContentCreate で、このときは contentType の範囲を定めた許可も認められ、文に書いた contentType と照合します(MediaCreate にこの例外はありません)。読み取り文の resource"ServiceUser" を書いたのに、作成者に SETTING_SERVICE_LOGIN がない場合もこのコードです。