コントロールプレーンには secrets broker が含まれます。これは任意のSecretsバックエンドで、値が漏えいし得る場所に値を置くことなく、ワーカー群全体の secret. 参照を解決します。これはエンジンの境界モデル(シークレットは値ではなく 境界 である)をチーム環境に拡張します。単一の browserflow run で成り立つのと同じ不変条件が、すべてのスケジュール実行、すべてのフロント、すべての環境にわたって成り立ちます。 ブローカーは コントロールプレーン の一部であり、採用してもFlowの実行方法は決して変わりません(原則 T6)。追加されるのは調整であって、capabilityではありません。

境界の不変条件をワーカー群全体へ

browser-flowにおけるシークレットは、値が最後の可能な瞬間にだけ越える境界です。secret: true とマークされた入力スロットは次の性質を持ちます。
  • FlowSpec、Journal、ログ行、公開済みPackのいずれにも現れない。
  • secrets capabilityを持つToolが、注入された Secrets providerを通じて解決する瞬間まで、参照としてだけ運ばれる。
  • そうでなければ漏えいし得るすべての場所でマスクされる。
ブローカーは、この3つの性質をワーカー群の規模で維持します。プレーンはデプロイレコード、スケジュール、Runメタデータ、Journalを保存しますが、そのどれにも シークレット値を保存することはありません。設計にあるとおり、プレーンはシークレットを FlowSpec、Journal、ログのいずれにも保存することなく 仲介します(境界の不変条件)。これは無効化できるポリシーではありません。単一ワーカー上で静的な taint rule とeffectのリダクションがそうであるのとまったく同じく、アーキテクチャから導かれるものです。
概念モデル、つまりシークレットが参照である理由、静的に強制されるtaint rule、そして漏えいさせずにeffectを記録する方法は、シークレットと境界 にあります。このページは運用上の見方、つまりチーム全体のバックエンドがそれらの参照をどのように解決するかを説明します。

参照が値になるまで

このドキュメント全体で使う実行例は daily-price-watch です。ブラウザーTool(search-price@1.2.0)が価格を調べ、transform 手順が結果を整形し、条件付きの http.post が、URLをシークレットとして持つwebhookへ通知します。以下は、そのシークレットを消費するnotify手順です。
flow.step("notify", {
  use: "http.post",
  when: "${{ steps.transform.output.changed }}",
  with: {
    // `secret.` 参照。不透明で、丸ごと束縛され、変換されない
    url: "${{ secret.WEBHOOK_URL }}",
    body: "${{ steps.transform.output }}",
  },
});
steps:
  - id: notify
    use: http.post
    when: ${{ steps.transform.output.changed }}
    with:
      url: ${{ secret.WEBHOOK_URL }}
      body: ${{ steps.transform.output }}
作成者が参照するのは secret.WEBHOOK_URL だけです。http.postsecrets capabilityを宣言する唯一の手順なので、値が到達できる手順もこれだけです。その参照の道のりは、ローカルでもプレーン管理ワーカー上でも同じです。変わるのは resolve(ref) に応答するバックエンドだけです。
1

作成者がスロットを参照する

Flowのテキストには ${{ secret.WEBHOOK_URL }} が含まれます。コンパイラのシングルパスtaint checkは、その参照が secrets 対応Toolに丸ごと、かつ変換なしで束縛されていることを確認し、そうでなければ secret_leak 診断を出します。コンパイル済みFlowSpecが運ぶのは 参照 であり、値ではありません。
2

Runがワーカーへ送出される

コントロールプレーンはRunをスケジュールし、ワーカーへ送出します。実行はいつもと同じ Rider で、secrets providerがプレーンのブローカーを指す RunDeps で構成されます。ワーカーが受け取るFlowSpecには、まだ参照だけが含まれています。
3

providerがワーカー上で実行時に解決する

notify 手順が実行されると、Riderは注入された Secrets providerの resolve(ref)WEBHOOK_URL に対して呼び出します。ブローカーはバックエンドからlive値を取得し、その手順に、そしてその手順だけに渡します。解決は ワーカー上で実行時に 行われ、送出前にプレーン上で行われることはありません。
4

effectはリダクトされて記録される

http.post は、Tool本体の内部、つまりeffectを記録する層で、解決済みURLを組み立てます。recorderはどのバイトがシークレット由来かを知っており、値ではなく安定したプレースホルダー({secret:<slot>})としてJournalに記録します。プレーンへ戻るJournalが運ぶのは、リダクト済みの形だけです。
Secrets providerは、store、clock、random sourceと並ぶ、エンジンの注入依存関係の1スロットです。
export interface RunDeps {
  // ...
  secrets: Secrets; // resolve(ref): string — `secrets`対応Tool専用
  // ...
}
解決は注入されたcapabilityの1つにすぎないため、バックエンドを差し替えてもFlowSpecやFlow作成者のコードには一切触れません。

差し替え可能なバックエンド

ブローカーはバックエンドインターフェイスであり、単一のstoreではありません。仕様どおり、バックエンドは差し替え可能です。
Backend内容
プレーン暗号化storeコントロールプレーン自身のエンベロープ暗号化されたシークレットstore。デフォルトであり、ホスト型サービスが現在実行しているものです(下記参照)。
HashiCorp Vaultプレーンは、値を自身で保持するのではなく、Vault経由で読み取りを仲介します。
クラウドシークレットマネージャークラウドプロバイダーのマネージドシークレットサービス。同じ方法で仲介されます。
どのバックエンドが resolve(ref) に応答する場合でも、ワーカーから見える契約は同一です。参照が入り、live値が正確に1つの手順へ戻ります。シークレットは、RegistryやRunと同じくworkspaceごとに名前空間化されます(tenancy)。
ホスト型サービス(@jinbatrail/service)は現在、下記のエンベロープ暗号化方式により、プレーン暗号化store の行を実装しています。Vaultまたはcloud-secret-managerバックエンドは同じ Secrets インターフェイスの有効な実装ですが、本稿執筆時点ではホスト型サービスには接続されていません。

保存時のエンベロープ暗号化

ホスト型サービスは、プロビジョニングされたシークレット(例: オーサリング中に取得されたセッショントークン)を、自身の secrets テーブルに (org_id, slot) をキーとして永続化します。バイト列が 平文で保存されることはありません。古典的なエンベロープ暗号化により、データstoreが保持するのは暗号文だけになり、長期鍵素材は小さく差し替え可能な Kms 境界の背後に置かれます。
1

シークレットごとに新しい32-byte data key

encryptSecret(plaintext, kms) の各呼び出しはランダムなAES-256 data keyを生成します。このキーはシークレット間で決して再利用されません。
2

AES-256-GCMがplaintextをsealする

plaintextは、そのdata keyのもとで、ランダムな12-byte IVと16-byte auth tagを使ってsealされます。これは認証付き暗号化なので、改ざんは復号時に検出され、黙って受け入れられることはありません。
3

data key自体はKmsによってwrapされる

kms.wrapDataKey(dataKey) は、Kms 実装だけが保持するmaster keyのもとでdata keyをsealします。平文のdata keyがディスクに触れることはありません。永続化されるのはwrap済みblobだけで、ciphertextと一緒に1つの不透明なbase64文字列(len(wrappedDK) ‖ wrappedDK ‖ iv ‖ tag ‖ ciphertext)としてpackされます。
devとprodの間で変わるのは Kms 実装だけです。ディスク上のenvelope形式はどちらでも同一です。
環境Kms 実装Master keyの所在
Dev / self-hostcreateMasterKeyKms(JINBA_MASTER_KEY)。環境から一度だけ読み取られる32-byte hex keyのもとでAES-256-GCMを使います。存在しない場合は、大きな警告とともにランダムな一時キーが生成されます(そのプロセスの寿命の間だけencrypted-at-rest)。サービスプロセスのメモリ
TestscreateFakeKms()。テストスイートがbyte-stableになるよう、固定された公開既知のキーを使います。N/A。テスト外では決して使用しない
Production (AWS)createKmsEnvelopeKms({ keyArn, encryptedMasterKey })。起動時に、AWS KMS CMKに対する認可済みの Decrypt 呼び出しを1回行い、32-byte master keyをプロセスメモリへ復元します。その後のシークレットごとのwrap/unwrapはすべてローカルのAES-256-GCMです(シークレットごとのKMS往復はありません)。起動時の1回の Decrypt まではAWS KMS、その後はプロセスメモリ
kms_key_id(dev master keyのhash、またはprodのCMK ARN)は、rotationや監査の追跡可能性のためだけに、各シークレットのciphertextと一緒に保存されます。それ自体がシークレットになることはありません。
CMK rotationは新しい encryptedMasterKey を生成して再デプロイするだけで済むため、既存のenvelopeは、それらをsealしたARNから構築された Kms に到達できる限り読み取り可能なままです。rotation時に強制的な再暗号化sweepはありません。

test/live境界

APIキーの modelive または test)は、RBACとスコープの上に置かれる、2つ目の独立したゲートです。これはREST実行ゲートウェイ(POST /v1/run/:flow)とMCP(POST /mcp)で同じように強制されます。
// シークレット入力を持つオートメーションをtest-modeキーで呼び出す
// → 403
{
  "error": "test_key_forbidden",
  "message": "test-mode API keys cannot run automations that use secrets; use a live key"
}
test キーは、シークレット型入力を まったく 宣言しない任意のオートメーションを実行できます。これは意図された「実験しても安全」な領域です。ただし、そのオートメーションの inputssecret: true とマークされたスロットが含まれた瞬間、即座に拒否されます。これにより、漏えいしたtestキー(スクリプト、デモ、第三者へ渡されたキー)が、liveキーと同じ本番認証情報への到達範囲を持ってしまう失敗モードを閉じます。部分的なアクセスはありません。オートメーションのどこかでシークレットを使っているなら、シークレットを持つ手順だけでなく、オートメーション全体がtestキーには立ち入り禁止です。

ローテーションは透過的

シークレットはFlowSpecにもJournalにも埋め込まれないため、値が変わっても再構築するものはありません。設計にあるとおり、シークレットはすべてのRunで再解決され、決して埋め込まれません。そのためローテーションは透過的です。
バックエンドで WEBHOOK_URL をローテーションすると、daily-price-watch次の Runは新しい値を自動的に解決します。再デプロイも、再コンパイルも、新しいdigestも不要です。FlowSpecが持っていたのは常に参照だけです。
これは、エンジンが resumereplay の間にシークレットを扱う方法とも一貫しています。シークレットは、Journalから読み戻されるのではなく liveで再解決される 唯一のものです。再開されたRunは再解決します。hermeticな replay は、記録済みeffectがシークレットを持たないため、シークレットをまったく必要としません。writeを再発火する --rerun だけが、ブローカーに新しい値を要求します。
hermeticな browserflow replay はシークレットを 必要とせず、外部呼び出しも行いません。Journalが保持するのはリダクト済みのrequest shapeと記録済みresponseであり、値ではありません。effectをliveで再実行する replay --rerun だけが、実行時にシークレットを再解決します。境界はすべてのmodeで保たれます。

RBACとrun inspector

シークレットは アクセス制御 と1つの決定的な形で交差します。roleに関係なく、シークレット値がrun inspectorによって公開されることは決してありません。 どのroleでも、adminであっても、そのRun自身の作成者であっても、inspectorを通じて解決済みの値を読むことはできません。手順のinputはシークレットがマスクされた状態で表示されます。http.post は、その url fieldを、Journalが記録した {secret:<slot>} プレースホルダーとして表示します。これは browserflow inspect が出力するものと同じデータです。 RBACが 実際に 制御するのは周辺の操作です。ホスト型サービスでは、owner だけがオーサリングを進め(これにより secret.provisioned 経由でシークレットがprovisionされます)、シークレットを持つオートメーションをliveで実行できるAPIキーを発行または取り消し、スケジュールを管理できます。member はRunとオートメーションを読めますが、それらには一切触れられません。secret.provisioned 監査エントリは、実行者、時刻、slot nameを記録します。値は決して記録しません。線引きは明確です。roleが制限するのは 操作へのアクセス であり、値の可視性 ではありません。owner/memberの完全な表と監査アクション一覧については、アクセス制御 を参照してください。
解決は、注入された Secrets providerを通じて、ワーカー上で実行時に行われます。送出前にプレーン上で行われるわけではありません。プレーンはデプロイレコード、スケジュール、Journalを保存しますが、境界の不変条件により、そのどれにもシークレット値を保存しません。バックエンドがプレーン自身の暗号化storeである場合、値はそこで暗号化された状態で存在し、解決する手順へ仲介されます。バックエンドがVaultまたはクラウドシークレットマネージャーである場合、プレーンは値自体を保持せずに読み取りを仲介します。
コンパイルできません。taint ruleは、変換された secret. 参照、secrets ではないToolへ渡された参照、または output で使われた参照を拒否します。各違反は secret_leak 診断であり、コンパイル時に証明されます。そのため、漏えいするFlowがそもそもワーカーへ到達することはありません。taint rule を参照してください。
外側の composite Tool上の secret: true inputは、内側のFlowの対応するsecret inputへ 参照 として渡され、最も内側の secrets 対応Toolでのみ解決されます。ローカルとまったく同じように、プレーン管理ワーカー上でも、境界はどのnesting depthでも保たれます。

シークレットと境界

概念モデル: 参照、静的なtaint rule、effectのリダクション。

アクセス制御

owner/member RBAC、キーごとのスコープ、監査ログ、orgごとのtenancy。

実行と再現性

Riderがresumeとreplayでシークレットを新しく解決する方法。

コントロールプレーン

デプロイ、レプリケーション、スケジューリング、RBAC、APIキー、MCP。