Packは、browser-flowの構成要素を配布する方法です。Toolが最小の再利用可能単位で、Flowが手順のDAGだとすると、Packはそれらを束ね、1つのものとしてデプロイ、バージョン管理、複製できるようにするアーティファクトです。これは、このプロジェクトにおける「SDK」の概念です。一度公開し、どこでも同じように利用できる単位です。 Packがtarballと異なるのは、そのアイデンティティです。Packはcontent-addressedです。中に含まれるすべてのtoolはcontract digestで、すべてのflowはFlowSpec digestで列挙され、manifest自体もハッシュ化されてsha256 digestになります。一度取得し、それぞれのdigestを内容に照らして検証すれば、同じバイト列がオフラインでも、CIでも、エアギャップの内側でも、いつまでも実行されます。

Packがデプロイ単位である理由

Packは、本来なら3つの異なる方法で答えられがちな3つの問いに対する、単一の答えです。

デプロイ

1つのアーティファクトを公開します。そのflowは、選択したlauncherでデプロイ可能になります。

バージョン管理

人間向けにはsemver、機械向けには不変のmanifest digestを提供します。

複製

RegistryはPackをdigestでミラーするため、固定され検証済みの状態で届きます。
ToolsとFlowsはどちらもPackの中に存在します(domain modelのとおり)。PackはRegistryの中に存在します。このページ全体では、標準例として@acme/pricing packを使います。これはsearch-price@1.2.0 browser toolとdaily-price-watch flowを保持しています(ブラウザーでの検索、変換、その後にシークレットwebhookへの条件付きhttp.post)。

packディレクトリのレイアウト

ディスク上のPackは、manifest、2つのフォルダー、readmeで構成されます。
@acme/pricing@2.3.0/
├── pack.json                 # PackManifest (name、version、tool/flow digests、deps、digest)
├── tools/
│   ├── search-price@1.2.0/   # ToolContract + body (recordingまたはcode) + contract digest
│   └── …
├── flows/
│   └── daily-price-watch/    # FlowSpec + flow digest
└── README.md
各Toolディレクトリは、そのToolContractとbodyを持ちます。search-priceの場合、そのbodyは正規化されたブラウザーRecordingであり、そのdigestがToolのprovenance.source_digestです(ブラウザーToolを参照)。各Flowディレクトリは、コンパイル済みで完全に固定されたFlowSpecを持ちます。そのうえで、manifestがそれらすべてをdigestで閉じ込めます。

PackManifestスキーマ

pack.jsonPackManifestです。含まれるすべてのToolとFlowをdigestで列挙し、すべてのdependencyを固定します。これにより、Pack全体が再現可能になります。
export const PackManifestSchema = z
  .object({
    schema_version: z.literal("1.0"),
    name: z.string().min(1), // "@acme/pricing"
    version: z.string().min(1), // semver
    tools: z.array(z.object({ ref: z.string(), contract_digest: z.string() }).strict()),
    flows: z.array(z.object({ name: z.string(), flow_digest: z.string() }).strict()),
    dependencies: z.array(z.string()), // 他のpack、固定済み
    digest: z.string(), // 正規manifestのsha256
  })
  .strict();

export type PackManifest = z.infer<typeof PackManifestSchema>;
@acme/pricingでは、tools[]{ ref: "search-price@1.2.0", contract_digest: … }とそのhttp.post dependencyを持ち、flows[]{ name: "daily-price-watch", flow_digest: … }を持ち、dependencies[]がFlowの依存する標準Packを固定します。トップレベルのdigestは、Pack全体の不変のアイデンティティです。
toolのcontract_digestは、その入力、出力、capabilitiesが変わるたびに変わります。そのため、互換性のない変更がpatchのふりをすることはできません。digest mismatchはruntimeではなくresolve時に表面化します。

Packを公開する

Packのオーサリングは3つのコマンドで行います。manifestを初期化し、アーティファクトを追加して(そのdigestを計算し)、公開します。
1

manifestを初期化する

namespace用の空のpack.jsonを作成します。
browserflow pack init @acme/pricing
2

toolsとflowsを追加する

addはアーティファクトを正規化し、そのdigestをmanifestに記録します。toolはcontract digestで、flowはFlowSpec digestで記録されます。
browserflow pack add ./tools/search-price       # toolを追加し、そのcontract digestを計算する
browserflow pack add ./flows/daily-price-watch.yaml
3

Registryに公開する

publishはすべてのアーティファクトを正規化し、digestを計算し、任意でmanifestに署名して、アップロードします。
browserflow pack publish --registry https://registry.acme.dev
VersionはsemverとChangesets風の規律に従います。manifest digestが不変のアイデンティティであるため、あるname@versionは常にまったく同じバイト列へresolveされます。Manifestの署名は任意であり、digestの上に重ねられるものです。完全性はdigestから、真正性は署名から得られます。またRegistryは、Packを受け入れる前に、信頼されたキーによる有効な署名を要求できます(replicationを参照)。

Packを利用する

利用側ではrangeを追加してからinstallします。追加するとresolveされて固定され、installすると取得と検証が行われます。
browserflow pack add @acme/pricing@^2.3      # resolve + local lockfileへのpin
browserflow install                          # すべての固定済みpackをfetch + verify
browserflow installは固定済みのすべてのPackを取得し、何かがresolveまたは実行を許可される前に、すべてのdigestを内容に照らして検証します。改ざんされたPackや不完全なPackは、最初の段階で検証に失敗します。

lockfileは再現性の契約

browserflow compile(およびinstall / pack addコマンド)は、固定済みの各Tool refについて、正確にresolveされたversionとそのcontract digestを記録するbrowserflow.lockを書き出します。
{
  "entries": [
    { "ref": "http.get",         "version": "1.0.0", "contract_digest": "sha256:bddc…" },
    { "ref": "search-price@1.2.0", "version": "1.2.0", "contract_digest": "sha256:7f3a…" }
  ]
}
各entryはversion labelとcontract digestの両方を固定するため、同じbrowserflow.lockからinstallする2台のマシンはバイト単位で同一のtoolsを取得します。あるマシンがリプレイするsearch-price@1.2.0 Recordingは、別のマシンがリプレイするRecordingとbit-for-bitで同じです。これは、システムの残りの部分が前提としている性質です。
browserflow.lockをコミットしてください。version labelは人間にどのreleaseを使っているかを伝えます。digestは、CI runner、チームメイト、エアギャップ環境のworkerがすべて同じ振る舞いを実行することを保証します。

なぜ「npmだけ」ではなくpacksなのか

Packsはnpm、OCI registry、object storageを経由して移動できます。しかし、それらはtransport、つまりバイト列を運ぶ方法であり、Packが実際に追加するものではありません。
汎用registryが提供するのは、tarballを指す変更可能なタグです。@acme/pricing@2.3.0の背後にあるバイト列が変わることを止めるものはなく、取得したものが作者の実行意図どおりであることを検証するものもありません。
Packが任意のtransportの上に追加するのは、実行可能な振る舞いのcontent-addressed検証です。Packのアイデンティティは、そのtoolsのcontractsとbodies、およびflowsのspecsのdigestです。付け替え可能なlabelではありません。具体的には、次のとおりです。
汎用タグ(npm / OCI)Pack
アイデンティティ変更可能なname@version labelcontracts + bodies + specsのsha256 digest
検証対象transportの完全性以外は何もない到着時に、すべてのtoolとflowのdigest
付け替えリスクtagはひそかに移動されうる不変のdigestは付け替えられない
エアギャップsourceを信頼するバイト列が主張されたdigestへhashされることを検証する
これにより、replicationは信頼できるものになり、エアギャップ環境での再現は可能になります。Registryは任意のtransportを使ってPackをミラーし、到着時に検証できます。そして、主張されたdigestへhashされないPackは、resolveまたは実行される前に拒否されます。
実行前に検証が失敗します。改ざんされた、切り詰められた、または部分的に取得されたPack、つまり内容がmanifestまたはlockfileに記録されたdigestへhashされないものは、install時またはmirror時に拒否され、黙って実行されることはありません。同じチェックは、バイト列がHTTPS、OCI、object storage、file bundleのどれで届いた場合でも同一に実行されます。
同じname@versionの下で2つの異なるbodyが公開されることはhard conflictであり、黙って上書きされることはありません。semverとdigestの不変性がそれを禁止します。sync mirrorは勝者を選ぶのではなく、conflictを表面化します。replicationを参照してください。
ありません。secret: true inputがFlowSpec、Journal、log line、公開済みPackに現れることは決してありません。daily-price-watchがpostする先のシークレットWEBHOOK_URLは参照としてのみ運ばれ、secrets capabilityを宣言する手順の中で実行時にresolveされます。secretsを参照してください。

関連

Registryの複製

RegistryがPackをdigestでミラーする方法です。pull、push、sync、scheduledで、到着時に検証します。

スキーマ

PackManifest、ToolContract、FlowSpecの完全なスキーマを1つのリファレンスにまとめています。

エアギャップデプロイ

live registryなしで、エアギャップ越しにpacksをbundle、transfer、verifyします。

ToolとPackの概要

Tool contract、標準Pack、ブラウザーToolがどのように記録されるかを説明します。