browser-flow におけるシークレットは、受け渡しする値ではありません。値が最後の瞬間まで越えない境界です。secret: true と宣言された入力は、FlowSpec、Journal、ログ行、公開された Pack には一切現れません。Flow の中では不透明な参照としてのみ移動し、実際のバイト列として具体化されるのは一度だけです。それは、secrets capability を宣言した単一の Tool の内部で、実行時に、worker 上で起きます。コンパイル時の taint check は、Run が開始する前にこの境界が保たれていることを証明します。 このページでは一貫して1つの例を使います。daily-price-watch Flow です。ブラウザー Tool search-price@1.2.0 が価格を検索し、transform 手順がしきい値を超えたかどうかを判断し、条件付きの http.post がシークレットの webhook URL へ通知します。

値ではなく境界

入力スロットは、Tool または Flow の contract 上で secret: true と宣言されます。InputDecl contract では、secret は必須の boolean です。つまり、すべてのスロットが境界の内側にあるのか外側にあるのかを明示的に決めます。 secret: true の入力は次のように扱われます。
  • FlowSpec、Journal、ログ行、公開された Pack には一切現れません
  • secrets capability を持つ Tool が、注入された Secrets provider を通じて解決する瞬間まで、参照、つまり \{ slot, refers_to \} としてのみ運ばれます。
  • 漏えいし得るすべての場所(run inspector、構造化ログ、エラーフレーム)で maskInputs によってマスクされます。
Flow の作成者は ${{ secret.WEBHOOK_URL }} と書きます。その値は、作成された Flow、コンパイル済み FlowSpec、Run の記録のどこにも存在しません。secrets capability を宣言した手順の内部でのみ、実行時にだけ具体化されます。
// daily-price-watch: 価格がしきい値を超えた場合にだけシークレット webhook へ通知する
flow.step("notify", "http.post", {
  when: "${{ steps.decide.output.shouldAlert }}",
  with: {
    // `secret.WEBHOOK_URL` は、http.post が `secrets` capability を宣言している場合に限り、
    // 全体が未変換のままバインドされる。
    url: "${{ secret.WEBHOOK_URL }}",
    body: { price: "${{ steps.lookup.output.price }}" },
  },
});
steps:
  - id: notify
    use: http.post
    when: ${{ steps.decide.output.shouldAlert }}
    with:
      url: ${{ secret.WEBHOOK_URL }}      # 全体を未変換のまま、secrets 対応 Tool へ渡す
      body:
        price: ${{ steps.lookup.output.price }}
secrets capability は、ToolContract 上で宣言される effect の1つです(netfsbrowserclockrandomenvspawnsecrets)。すべての capability と同じく、Rider は宣言された effect だけを注入し、それ以上は注入しません。secrets を列挙していない Tool には Secrets provider が渡されないため、仮に参照がそこへ到達しても、その Tool には参照を解決する手段がありません。

taint rule(静的に強制)

secret. 参照は不透明です。コンパイラーはそれを汚染されたトークンとして扱います。このトークンは、Tool 入力への引数として、かつその Tool が secrets capability を宣言している場合に限り、全体が未変換のままバインドできます。
シークレット参照では次のことはできません
  • 演算子、メンバーアクセス、組み込み関数を通過することsecret.x + yjson(secret.x)default(secret.x, z)secret.x.foo はすべて拒否されます。
  • secrets を宣言していない Tool へバインドされること
  • output のどこかに現れること
いずれの違反も、コンパイル時の単一パス taint checkによって証明される secret_leak diagnostic です。実行時フォールバックも、上書きフラグもありません。シークレットを漏えいし得る Flow はコンパイルされません。
このルールは純粋に構造的なので、機械的に判定できます。シークレット参照は、secrets 対応 Tool 上のスロットの全体をバインドしているか、secret_leak であるかのどちらかです。第三のケースはありません。
記述判定理由
http.postsecrets を宣言)上の url: ${{ secret.WEBHOOK_URL }}許可全体が未変換のまま、secrets 対応 Tool へ渡される
secrets なしの Tool 上の url: ${{ secret.WEBHOOK_URL }}secret_leak対象 Tool はシークレットを解決できない
auth: "Bearer " + secret.TOKENsecret_leak演算子を通過する
host: ${{ secret.WEBHOOK_URL.host }}secret_leakシークレット上のメンバーアクセス
url: ${{ default(secret.WEBHOOK_URL, x) }}secret_leak組み込み関数を通過する
output: ${{ secret.WEBHOOK_URL }}secret_leakシークレットは output に現れてはならない
この設計全体の効果はここにあります。シークレットは expression layer を流れることが決してないため、その値が URL、ヘッダー、ブラウザーフィールドへ組み込まれるのは、解決を行う Tool の内部だけです。そこはまさに effect を記録する層であり、したがって redact が必要な唯一の層でもあります。この静的ルールによって、実行時の保証は追加コストなしで成立します。
シークレットから何かを派生させる必要がありますか。たとえばルーティング用のホスト名や、ヘッダー用のキープレフィックスでしょうか。そうしないでください。シークレット全体を secrets 対応 Tool へ渡し、その Tool の内部で組み立てさせます。そこでは記録と redact が行われます。設計上、expression layer がシークレット由来の値を見ることはありません。

漏えいさせずに effect を記録する

Rider は、Run を hermetic にリプレイできるよう、すべての外部 effect を Journal へ記録します。リクエストとレスポンスは content-addressed で保存されます。ここで明らかな緊張が生じます。シークレットがリクエストの一部である場合、それを記録してもなぜシークレットが漏えいしないのでしょうか。 taint rule がこの緊張を解消します。シークレットは解決する Tool の内部でのみ具体化されるため、その Tool こそが effect を記録する層です。したがって、どのバイト列がシークレット由来なのかを正確に知っています。recorder はそれらをredactします。シークレットを含むヘッダー、クエリパラメーター、body フィールドは、値そのものではなく、安定したプレースホルダー \{secret:<slot>\} として journal されます。
1

解決

daily-price-watch の notify 手順が実行されます。http.postsecrets を宣言しているため、Rider は Secrets provider を注入し、Tool は WEBHOOK_URL を実際の URL へ解決します。これはメモリ上で、Tool 本体の内部でのみ行われます。
2

実行

Tool は解決済み webhook へ live の POST を送信します。Run のどこかにシークレット値がバイト列として存在するのは、この瞬間だけです。
3

記録、redact済み

recorder は、シークレット由来の URL を安定したプレースホルダー \{secret:WEBHOOK_URL\} に置き換えてリクエストを journal します。記録されたレスポンスは、redact 済みのリクエスト形状と冪等性キーによって key 付けされます。
4

リプレイ、hermetic

hermetic リプレイでは、その redact 済みキーによって、記録されたレスポンスが Journal から返されます。外部呼び出しは行われず、シークレットも不要です。effect が記録されていても、境界の不変条件は保たれます。
そのため、determinism table では、secrets各 Run で新たに解決され、決して記録されない唯一の capability として示されています。一方、netclockrandomfsbrowser はすべて、リプレイ時に Journal によって裏付けられます。シークレットは意図的な例外です。決して永続化されません。
大きな body やバイナリ body はインラインではなく、blob store 内に digest で保存され、Journal は digest だけを保持します。redaction は、何かが Journal や blob store へ到達する前に行われます。そのため、payload のサイズにかかわらず、どちらもシークレット由来のバイト列を保持しません。

各 Run で新たに再解決される

シークレットは永続的なものに焼き込まれることがないため、ローテーションは透過的です。ローテーションされたシークレットは、次の Run で単に新しい値へ解決されます。再ビルド、再デプロイ、再 pin は不要です。シークレットはそもそも FlowSpec の一部ではなかったため、シークレットがローテーションされても FlowSpec digest は変わりません。 この鮮度のルールは、Run が保存済みの値に手を伸ばし得る次の2つの場所にも及びます。
  • 再開。 クラッシュ、デプロイ、SIGTERM によって中断された Run は、Journal から再開されます。完了済みの手順は記録済みの output を返し、記録済みの effect は記録済みの結果を返すため、外部のものが再度発火することはありません。シークレットは唯一の例外です。シークレットは Journal から読み取られることはなく、再開時に live で再解決されます。そもそも Journal に書き込まれていないためです。
  • Replay --rerun Hermetic な browserflow replaybrowserflow replay --verify には、シークレットがまったく不要です(記録されたレスポンスが Journal から返されます)。opt-in の browserflow replay <run-id> --rerun は effect を live で再実行します。つまり http.post が実際にもう一度発火します。そしてこのモードでは、関連するシークレットが実行時に再解決され、Journal から読み取られることはありません。
browserflow replay <run-id>            # hermetic: 外部呼び出しなし、シークレット不要
browserflow replay <run-id> --verify   # hermetic + すべての手順のoutput一致を検証
browserflow replay <run-id> --rerun    # effectをliveで再実行: シークレットを新たに再解決

サービスのシークレットブローカー

インフラストラクチャがない場合、RunDeps 内の Secrets dependency は launcher が構築したものになります。たとえば、ローカルの env-backed provider です。任意のコントロールプレーンは、管理されたシークレットブローカーを追加します。これにより、FlowSpec、Journal、ログにシークレットを保存することなく、fleet 全体で同じ境界の不変条件を保ちます。 仕組みはエンジンと完全に一致します。Flow の作成者は secret.WEBHOOK_URL を参照します。plane の Secrets provider は値をworker 上で、実行時に解決し、secrets capability を宣言した手順にだけ注入します。コントロールプレーンは Run をスケジュールし、観測しますが、実行はしません。そのため、シークレット値が解決済みバイト列として API tier を通過することはありません。
プロパティ振る舞い
解決される場所worker 上、実行時。コントロールプレーン API 内では決して解決されない
注入先secrets capability を宣言した Tool の手順のみ
BackendPluggable: plane 自身の暗号化 store、Vault、クラウド secret manager
ローテーション透過的。各 Run で再解決され、焼き込まれることはない
露出RBAC role にかかわらず、シークレットのは run inspector に表示されない
シークレットアクセスを含むすべての特権アクションは、actor、timestamp、関係する digest とともに監査ログへ記録されます。これにより、シークレット値を監査証跡に一切含めずに、誰が、何を、いつ解決したかを示す完全な content-addressed trail を得られます。

関連

secret_leak diagnostic

taint check が拒否する secret_leak のすべての形を含む、完全な diagnostic catalog です。

シークレットブローカー

worker 上での管理された pluggable なシークレット解決です。Vault、クラウド manager、または plane の store を利用できます。

実行とリプレイ

effect がどのように記録・redact されるか、そして hermetic リプレイにシークレットが不要な理由を説明します。