実行セマンティクス、制約、セキュリティ
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.ttl | 1 から 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 を指します)、そのため保存の時点ではなく実行中に検査されます。
- 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 ごとの個数がプランごとに制限されます。
| Plan | Script の個数 |
|---|---|
| Free | 10 |
| Basic | 30 |
| Pro | 100 |
| 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 ごとにリソース権限を再検査しません。
- 作成時(保存): Script を保存する際に、作成者がそれらの statement が使う リソースとアクションの権限を実際に持っているかを検査します。一つでも欠けていれば保存が拒否されます。つまり、権限のない操作を含む Script はそもそも保存されません。リソースを選ぶ文は、leaf であれブロックを所有する
ResourceForEachであれ、すべてこの検査を受けます。すでに保存してある定義も更新するときに再び検査されるため、権限が回収された後はその定義を直して保存できません。- 会員ディレクトリ(ServiceUser)は権限マップではなく設定の軸で検査します。 読み取りの 3 文に
resource: "ServiceUser"を書くには、作成者の SpaceRolesettingsにSETTING_SERVICE_LOGIN(またはSETTING_ALL)が必要です(SpaceRole の settings)。会員ディレクトリが、他のすべての経路でも Space 設定が管掌するリソースだからです。 - 会員を変更する文は、どの役割でも保存できません。 Script から会員を作る・直す・消す道がそもそもないため、権限不足(
403)ではなく書き方を誤った文(400)として拒否されます。権限を足せば閉じられる隙間ではないという意味です。
- 会員ディレクトリ(ServiceUser)は権限マップではなく設定の軸で検査します。 読み取りの 3 文に
- 呼び出し時(
/execute): 呼び出し元の Script Execute 権限のみを検査します。権限がなければ403です。通過すると、statement ごとのリソース権限はランタイムで再確認せずに実行します。プログラミングにおける関数の実行権限と同じ方式です。関数を実行する権限さえあれば、その中の個々の操作の権限は再び問われません。匿名呼び出しの経路にはこの検査がありません。 検査する呼び出し元がいないからであり、そのためその経路を開くことは、Script 1 つを認証なしで公開することと同じです。
- 作成時(保存): Script を保存する際に、作成者がそれらの statement が使う リソースとアクションの権限を実際に持っているかを検査します。一つでも欠けていれば保存が拒否されます。つまり、権限のない操作を含む Script はそもそも保存されません。リソースを選ぶ文は、leaf であれブロックを所有する
- 直接呼び出しのブロック(
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 個を超えています。 |
WGL400068 | Cache 文を Loop や ResourceForEach のブロック内に置きました。 |
WGL400067 | Cache 文の key に、リテラルではなく { /pointer } 参照を書きました。 |
WGL400065 | Cache 文の ttl が許容範囲を外れています。 |
WGL400063 | Cache 文で action に該当しないフィールドを書きました(Get に ttl、Set に defaultValue)。 |
WGL400060 | 書き込み系の文(ResourceCreate・ResourceUpdate・ResourcePatch・ResourceDelete と公開・アーカイブの文)の resource に "ServiceUser" を書きました。 |
WGL400061 | 匿名呼び出しを許可した Script(anonymousCallEnabled)で、読み取り文の where に createdBy: ":self" を書きました。 |
WGL400023 | 一つの定義に含めた SetVar 文が 10 個を超えています。 |
WGL400026 | Http 文の retry が上限の 2 を超えています。 |
WGL400036 | ResourceForEach が処理できる項目数の上限を超えています。 |
WGL429005 | 一つの定義に含めた statement の総数が料金プランの上限を超えています。 |
WGL429006 | 一つの定義に含めた外部呼び出し(Http・EmailSend)の数が料金プランの上限を超えています。 |
WGL403015 | 定義に含めた文が扱うリソースとアクションの権限を、作成者が持っていません。権限があっても、その許可に contentType・createdBy・tag フィルターが付いていれば拒否されます。 条件のない許可でなければなりません。唯一の例外は Content の Create で、このときは contentType の範囲を定めた許可も認められ、文に書いた contentType と照合します(Media の Create にこの例外はありません)。読み取り文の resource に "ServiceUser" を書いたのに、作成者に SETTING_SERVICE_LOGIN がない場合もこのコードです。 |
関連ドキュメント
- 値式: 値と条件のルール。
- Statement カタログ: 各文のフィールドと結果。
- クックブック: 完結した例のまとめ。
- Script リソースとエンドポイント:
Scriptリソース構造と/executeなどの HTTP エンドポイント。 - Script 概要: 最上位の構造と、1 回の実行に与えられる時間。
