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

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

実行順序

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

実行セマンティクス

Guard(事前条件)

専用の guard 文はありません。If と then:[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 の競合は ResourceUpdate と ResourcePatch の version で絞ります。version(値式、Int)を渡すと、対象の現在の sys.version と 一致する場合のみ更新し、不一致の場合は バージョン競合エラーで abort されます(Try/catch で局所的に処理できます)。省略すると検査なしの last-write-wins になります。通常は ResourceRead や ResourceFind で先に読み込み、その sys.version を渡します(クックブックの楽観的ロック CAS を参照)。

origin 基準の書き込み

書き込みは常に origin(draft)に反映され、delivery(CDA/ACDA)への公開は publish で制御します(ResourceCreate/ResourceUpdate/ResourcePatch の publish、または 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 を 保存(作成/更新)する時点で検査されます。違反すると保存が拒否されます(ランタイムではなく作成時の失敗です)。どの違反がどのコードで拒否されるかは エラー で扱います。

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

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

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

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

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

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

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

時間予算(ランタイム)

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

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

プランごとの個数上限

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

PlanScript の個数
Free10
Basic30
Pro100
Enterprise無制限

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

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

セキュリティモデル

secret ヘッダー

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

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

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

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

  • 実行アイデンティティ: 実行中のすべてのリソース操作は、/execute を呼び出したユーザーのアイデンティティで実行されます。作成または更新されるリソースの createdBy/updatedBy は呼び出し元となり、createdBy: ":self" スコープも呼び出し元を基準に解決されます。例外は匿名呼び出しです。 /execute/anonymous で入ってきた実行には呼び出し元がいないため、どちらも 作成者を基準に解決されます(匿名呼び出し)。
  • 認可の境界は二つであり、ランタイムでは statement ごとにリソース権限を再検査しません。
    1. 作成時(保存): Script を保存する際に、作成者がそれらの statement が使う リソースとアクションの権限を実際に持っているかを検査します。一つでも欠けていれば保存が拒否されます。つまり、権限のない操作を含む Script はそもそも保存されません。リソースを選ぶ文は、leaf であれブロックを所有する ResourceForEach であれ、すべてこの検査を受けます。すでに保存してある定義も更新するときに再び検査されるため、権限が回収された後はその定義を直して保存できません。
      • 会員ディレクトリ(ServiceUser)は権限マップではなく設定の軸で検査します。 読み取りの 3 文に resource: "ServiceUser" を書くには、作成者の SpaceRole settings に SETTING_SERVICE_LOGIN(または SETTING_ALL)が必要です(SpaceRole の settings)。会員ディレクトリが、他のすべての経路でも Space 設定が管掌するリソースだからです。
      • 会員を変更する文は、どの役割でも保存できません。 Script から会員を作る・直す・消す道がそもそもないため、権限不足(403)ではなく書き方を誤った文(400)として拒否されます。権限を足せば閉じられる隙間ではないという意味です。
    2. 呼び出し時(/execute): 呼び出し元の Script Execute 権限のみを検査します。権限がなければ 403 です。通過すると、statement ごとのリソース権限はランタイムで再確認せずに実行します。プログラミングにおける関数の実行権限と同じ方式です。関数を実行する権限さえあれば、その中の個々の操作の権限は再び問われません。匿名呼び出しの経路にはこの検査がありません。 検査する呼び出し元がいないからであり、そのためその経路を開くことは、Script 1 つを認証なしで公開することと同じです。
  • 直接呼び出しのブロック(directCallEnabled): Script の directCallEnabled が false の場合、/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)を有効にしたなら、where に createdBy: ":self" がなく、受け取ったリクエストを検証する文(Signature など)を先頭に置いています。
  • 外部呼び出し(Http・EmailSend)の数と全 statement の数はプランの上限以内で、SetVar は 10 個以下、Cache は 5 個以下です。
  • Cache を使ったなら、key をリテラルで書き、Loop や ResourceForEach の中には置いていません。
  • ResourceForEach で大きな集合を巡回する場合は、limit を宣言するか、完走可能なサイズかを確認しました。
  • 繰り返し(Loop・ResourceForEach)を含めたなら、その繰り返しが掛け算として 時間予算 に入ることを確認しました(外部呼び出しがなければ 30 秒の基本予算が限度です)。
  • secret 値は Http.headers の secret:true でのみ入れています(Signature.secret は暗号化して保存されないため、その Script を読める役割を確認しました)。
  • 署名検証に使うメッセージは /payload ではなく { /rawPayload } で取っています。
  • 会員(ServiceUser)を読む文があれば、作成者に SETTING_SERVICE_LOGIN があり、そのリソースを変更する文は入れていません。
  • 取り消せない操作(外部呼び出し)はできるだけ後ろに置いています。
  • update/patch の競合が心配なら ResourceUpdate や ResourcePatch の version を使います。
  • 結果を返すには Return.value を明示しています。

エラー

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

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