Compare commits

...

6 Commits

Author SHA1 Message Date
Nikhil Sonti
6b3b22ce17 fix: align agent container artifact paths 2026-04-22 13:08:52 -07:00
Nikhil Sonti
19fff97a9c fix: emit clean matrix JSON in CI 2026-04-22 12:57:54 -07:00
Nikhil Sonti
25c027863c fix: address review feedback for PR #782 2026-04-22 12:50:57 -07:00
Nikhil Sonti
8440ae09ce refactor: simplify agent container pipeline 2026-04-22 12:41:10 -07:00
Nikhil Sonti
95f34da014 docs: add agent-container env sample 2026-04-22 12:22:31 -07:00
Nikhil Sonti
fe9913a4fe feat: add agent container tarball pipeline 2026-04-22 12:13:59 -07:00
28 changed files with 2817 additions and 0 deletions

View File

@@ -0,0 +1,170 @@
name: Build Agent Container
on:
workflow_dispatch:
inputs:
agent:
description: Optional agent filter from recipe/agents.json
required: false
type: string
version:
description: Optional dry-run version override for the selected agent
required: false
type: string
publish:
description: Publish artifacts to R2
required: false
default: false
type: boolean
pull_request:
paths:
- .github/workflows/build-agent-container.yml
- packages/browseros-agent/packages/agent-container/**
schedule:
- cron: '0 7 * * 1'
permissions:
contents: read
jobs:
list-matrix:
runs-on: ubuntu-24.04
defaults:
run:
working-directory: packages/browseros-agent
outputs:
matrix: ${{ steps.matrix.outputs.matrix }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Install dependencies
run: bun ci
- name: Typecheck package
run: bun run --filter @browseros/agent-container typecheck
- name: Run package tests
run: bun run --filter @browseros/agent-container test
- name: Compute matrix
id: matrix
env:
INPUT_AGENT: ${{ inputs.agent }}
run: |
set -euo pipefail
args=()
if [ -n "$INPUT_AGENT" ]; then
args+=(--agent "$INPUT_AGENT")
fi
matrix="$(bun run packages/agent-container/scripts/list-matrix.ts "${args[@]}")"
echo "matrix=$matrix" >> "$GITHUB_OUTPUT"
build:
needs: list-matrix
runs-on: ${{ matrix.arch == 'arm64' && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }}
timeout-minutes: 60
defaults:
run:
working-directory: packages/browseros-agent
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.list-matrix.outputs.matrix) }}
env:
ARTIFACT_ROOT: ${{ github.workspace }}/packages/browseros-agent/packages/agent-container/dist/agent-container
OUTPUT_DIR: ${{ github.workspace }}/packages/browseros-agent/packages/agent-container/dist/agent-container/${{ matrix.agent }}/${{ matrix.arch }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Install dependencies
run: bun ci
- name: Install podman
run: |
sudo apt-get update
sudo apt-get install -y podman
- name: Build tarball
env:
INPUT_VERSION: ${{ inputs.version }}
run: |
set -euo pipefail
cmd=(
bun run --filter @browseros/agent-container build --
--agent "${{ matrix.agent }}"
--arch "${{ matrix.arch }}"
--output-dir "$OUTPUT_DIR"
)
if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ -n "$INPUT_VERSION" ]; then
cmd+=(--version "$INPUT_VERSION")
fi
"${cmd[@]}"
- name: Smoke test archive
run: |
set -euo pipefail
result_json="$OUTPUT_DIR/build-result.json"
tarball="$(bun -e "const result = await Bun.file(process.argv[1]).json(); console.log(result.tarballPath)" "$result_json")"
expected_image="$(bun -e 'const result = await Bun.file(process.argv[1]).json(); console.log(result.image + ":" + result.version)' "$result_json")"
expected_fingerprint="$(bun -e "const result = await Bun.file(process.argv[1]).json(); console.log(result.smokeFingerprint)" "$result_json")"
bun run --filter @browseros/agent-container smoke -- \
--tarball "$tarball" \
--expected-image "$expected_image" \
--expected-fingerprint "$expected_fingerprint"
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: agent-container-${{ matrix.agent }}-${{ matrix.arch }}
path: ${{ env.ARTIFACT_ROOT }}
publish:
needs: build
if: ${{ github.event_name == 'workflow_dispatch' && inputs.publish == true }}
runs-on: ubuntu-24.04
defaults:
run:
working-directory: packages/browseros-agent
env:
ARTIFACT_ROOT: ${{ github.workspace }}/packages/browseros-agent/packages/agent-container/dist/agent-container
steps:
- name: Guard recipe source of truth
if: ${{ inputs.version != '' }}
run: |
echo "Refusing to publish a workflow_dispatch version override. Update recipe/agents.json instead." >&2
exit 1
- name: Checkout
uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Install dependencies
run: bun ci
- name: Download matrix artifacts
uses: actions/download-artifact@v4
with:
pattern: agent-container-*
merge-multiple: true
path: ${{ env.ARTIFACT_ROOT }}
- name: Publish to R2
env:
R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
R2_BUCKET: ${{ secrets.R2_BUCKET }}
run: |
set -euo pipefail
bun run --filter @browseros/agent-container upload -- \
--artifact-dir "$ARTIFACT_ROOT" \
--update-aggregate

View File

@@ -14,6 +14,7 @@ lerna-debug.log*
# Ignore all .env files except .env.example
**/.env.*
!**/.env.example
!**/.env.sample
!**/.env.production.example

View File

@@ -216,6 +216,17 @@
"chrome-devtools-mcp": "latest",
},
},
"packages/agent-container": {
"name": "@browseros/agent-container",
"version": "0.0.0",
"dependencies": {
"@aws-sdk/client-s3": "^3.933.0",
"zod": "^3.24.2",
},
"devDependencies": {
"@types/node": "^24.3.3",
},
},
"packages/agent-sdk": {
"name": "@browseros-ai/agent-sdk",
"version": "0.0.7",
@@ -463,6 +474,8 @@
"@browseros/agent": ["@browseros/agent@workspace:apps/agent"],
"@browseros/agent-container": ["@browseros/agent-container@workspace:packages/agent-container"],
"@browseros/cdp-protocol": ["@browseros/cdp-protocol@workspace:packages/cdp-protocol"],
"@browseros/eval": ["@browseros/eval@workspace:apps/eval"],
@@ -4489,6 +4502,8 @@
"@browseros/agent/zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="],
"@browseros/agent-container/@aws-sdk/client-s3": ["@aws-sdk/client-s3@3.1014.0", "", { "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.23", "@aws-sdk/credential-provider-node": "^3.972.24", "@aws-sdk/middleware-bucket-endpoint": "^3.972.8", "@aws-sdk/middleware-expect-continue": "^3.972.8", "@aws-sdk/middleware-flexible-checksums": "^3.974.3", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-location-constraint": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.8", "@aws-sdk/middleware-sdk-s3": "^3.972.23", "@aws-sdk/middleware-ssec": "^3.972.8", "@aws-sdk/middleware-user-agent": "^3.972.24", "@aws-sdk/region-config-resolver": "^3.972.9", "@aws-sdk/signature-v4-multi-region": "^3.996.11", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", "@aws-sdk/util-user-agent-node": "^3.973.10", "@smithy/config-resolver": "^4.4.13", "@smithy/core": "^3.23.12", "@smithy/eventstream-serde-browser": "^4.2.12", "@smithy/eventstream-serde-config-resolver": "^4.3.12", "@smithy/eventstream-serde-node": "^4.2.12", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/hash-blob-browser": "^4.2.13", "@smithy/hash-node": "^4.2.12", "@smithy/hash-stream-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/md5-js": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", "@smithy/middleware-endpoint": "^4.4.27", "@smithy/middleware-retry": "^4.4.44", "@smithy/middleware-serde": "^4.2.15", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/node-http-handler": "^4.5.0", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.43", "@smithy/util-defaults-mode-node": "^4.2.47", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", "@smithy/util-stream": "^4.5.20", "@smithy/util-utf8": "^4.2.2", "@smithy/util-waiter": "^4.2.13", "tslib": "^2.6.2" } }, "sha512-0XLrOT4Cm3NEhhiME7l/8LbTXS4KdsbR4dSrY207KNKTcHLLTZ9EXt4ZpgnTfLvWQF3pGP2us4Zi1fYLo0N+Ow=="],
"@browseros/eval/@aws-sdk/client-s3": ["@aws-sdk/client-s3@3.1014.0", "", { "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.23", "@aws-sdk/credential-provider-node": "^3.972.24", "@aws-sdk/middleware-bucket-endpoint": "^3.972.8", "@aws-sdk/middleware-expect-continue": "^3.972.8", "@aws-sdk/middleware-flexible-checksums": "^3.974.3", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-location-constraint": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.8", "@aws-sdk/middleware-sdk-s3": "^3.972.23", "@aws-sdk/middleware-ssec": "^3.972.8", "@aws-sdk/middleware-user-agent": "^3.972.24", "@aws-sdk/region-config-resolver": "^3.972.9", "@aws-sdk/signature-v4-multi-region": "^3.996.11", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", "@aws-sdk/util-user-agent-node": "^3.973.10", "@smithy/config-resolver": "^4.4.13", "@smithy/core": "^3.23.12", "@smithy/eventstream-serde-browser": "^4.2.12", "@smithy/eventstream-serde-config-resolver": "^4.3.12", "@smithy/eventstream-serde-node": "^4.2.12", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/hash-blob-browser": "^4.2.13", "@smithy/hash-node": "^4.2.12", "@smithy/hash-stream-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/md5-js": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", "@smithy/middleware-endpoint": "^4.4.27", "@smithy/middleware-retry": "^4.4.44", "@smithy/middleware-serde": "^4.2.15", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/node-http-handler": "^4.5.0", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.43", "@smithy/util-defaults-mode-node": "^4.2.47", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", "@smithy/util-stream": "^4.5.20", "@smithy/util-utf8": "^4.2.2", "@smithy/util-waiter": "^4.2.13", "tslib": "^2.6.2" } }, "sha512-0XLrOT4Cm3NEhhiME7l/8LbTXS4KdsbR4dSrY207KNKTcHLLTZ9EXt4ZpgnTfLvWQF3pGP2us4Zi1fYLo0N+Ow=="],
"@browseros/eval/@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="],
@@ -5211,6 +5226,100 @@
"@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/core": ["@aws-sdk/core@3.973.23", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@aws-sdk/xml-builder": "^3.972.15", "@smithy/core": "^3.23.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/property-provider": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/signature-v4": "^5.3.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.12", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-aoJncvD1XvloZ9JLnKqTRL9dBy+Szkryoag9VT+V1TqsuUgIxV9cnBVM/hrDi2vE8bDqLiDR8nirdRcCdtJu0w=="],
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.24", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.21", "@aws-sdk/credential-provider-http": "^3.972.23", "@aws-sdk/credential-provider-ini": "^3.972.23", "@aws-sdk/credential-provider-process": "^3.972.21", "@aws-sdk/credential-provider-sso": "^3.972.23", "@aws-sdk/credential-provider-web-identity": "^3.972.23", "@aws-sdk/types": "^3.973.6", "@smithy/credential-provider-imds": "^4.2.12", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-9Jwi7aps3AfUicJyF5udYadPypPpCwUZ6BSKr/QjRbVCpRVS1wc+1Q6AEZ/qz8J4JraeRd247pSzyMQSIHVebw=="],
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/middleware-bucket-endpoint": ["@aws-sdk/middleware-bucket-endpoint@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-arn-parser": "^3.972.3", "@smithy/node-config-provider": "^4.3.12", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-config-provider": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-WR525Rr2QJSETa9a050isktyWi/4yIGcmY3BQ1kpHqb0LqUglQHCS8R27dTJxxWNZvQ0RVGtEZjTCbZJpyF3Aw=="],
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/middleware-expect-continue": ["@aws-sdk/middleware-expect-continue@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-5DTBTiotEES1e2jOHAq//zyzCjeMB78lEHd35u15qnrid4Nxm7diqIf9fQQ3Ov0ChH1V3Vvt13thOnrACmfGVQ=="],
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/middleware-flexible-checksums": ["@aws-sdk/middleware-flexible-checksums@3.974.3", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", "@aws-sdk/core": "^3.973.23", "@aws-sdk/crc64-nvme": "^3.972.5", "@aws-sdk/types": "^3.973.6", "@smithy/is-array-buffer": "^4.2.2", "@smithy/node-config-provider": "^4.3.12", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-middleware": "^4.2.12", "@smithy/util-stream": "^4.5.20", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-fB7FNLH1+VPUs0QL3PLrHW+DD4gKu6daFgWtyq3R0Y0Lx8DLZPvyGAxCZNFBxH+M2xt9KvBJX6USwjuqvitmCQ=="],
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-wAr2REfKsqoKQ+OkNqvOShnBoh+nkPurDKW7uAeVSu6kUECnWlSJiPvnoqxGlfousEY/v9LfS9sNc46hjSYDIQ=="],
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/middleware-location-constraint": ["@aws-sdk/middleware-location-constraint@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-KaUoFuoFPziIa98DSQsTPeke1gvGXlc5ZGMhy+b+nLxZ4A7jmJgLzjEF95l8aOQN2T/qlPP3MrAyELm8ExXucw=="],
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-CWl5UCM57WUFaFi5kB7IBY1UmOeLvNZAZ2/OZ5l20ldiJ3TiIz1pC65gYj8X0BCPWkeR1E32mpsCk1L1I4n+lA=="],
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-BnnvYs2ZEpdlmZ2PNlV2ZyQ8j8AEkMTjN79y/YA475ER1ByFYrkVR85qmhni8oeTaJcDqbx364wDpitDAA/wCA=="],
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/middleware-sdk-s3": ["@aws-sdk/middleware-sdk-s3@3.972.23", "", { "dependencies": { "@aws-sdk/core": "^3.973.23", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-arn-parser": "^3.972.3", "@smithy/core": "^3.23.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/protocol-http": "^5.3.12", "@smithy/signature-v4": "^5.3.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/util-config-provider": "^4.2.2", "@smithy/util-middleware": "^4.2.12", "@smithy/util-stream": "^4.5.20", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-50QgHGPQAb2veqFOmTF1A3GsAklLHZXL47KbY35khIkfbXH5PLvqpEc/gOAEBPj/yFxrlgxz/8mqWcWTNxBkwQ=="],
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/middleware-ssec": ["@aws-sdk/middleware-ssec@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-wqlK0yO/TxEC2UsY9wIlqeeutF6jjLe0f96Pbm40XscTo57nImUk9lBcw0dPgsm0sppFtAkSlDrfpK+pC30Wqw=="],
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.24", "", { "dependencies": { "@aws-sdk/core": "^3.973.23", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@smithy/core": "^3.23.12", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-retry": "^4.2.12", "tslib": "^2.6.2" } }, "sha512-dLTWy6IfAMhNiSEvMr07g/qZ54be6pLqlxVblbF6AzafmmGAzMMj8qMoY9B4+YgT+gY9IcuxZslNh03L6PyMCQ=="],
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.9", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/config-resolver": "^4.4.13", "@smithy/node-config-provider": "^4.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-eQ+dFU05ZRC/lC2XpYlYSPlXtX3VT8sn5toxN2Fv7EXlMoA2p9V7vUBKqHunfD4TRLpxUq8Y8Ol/nCqiv327Ng=="],
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.11", "", { "dependencies": { "@aws-sdk/middleware-sdk-s3": "^3.972.23", "@aws-sdk/types": "^3.973.6", "@smithy/protocol-http": "^5.3.12", "@smithy/signature-v4": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-SKgZY7x6AloLUXO20FJGnkKJ3a6CXzNDt6PYs2yqoPzgU0xKWcUoGGJGEBTsfM5eihKW42lbwp+sXzACLbSsaA=="],
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/types": ["@aws-sdk/types@3.973.6", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw=="],
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.996.5", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-endpoints": "^3.3.3", "tslib": "^2.6.2" } }, "sha512-Uh93L5sXFNbyR5sEPMzUU8tJ++Ku97EY4udmC01nB8Zu+xfBPwpIwJ6F7snqQeq8h2pf+8SGN5/NoytfKgYPIw=="],
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/types": "^4.13.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-B3KGXJviV2u6Cdw2SDY2aDhoJkVfY/Q/Trwk2CMSkikE1Oi6gRzxhvhIfiRpHfmIsAhV4EA54TVEX8K6CbHbkA=="],
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.973.10", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "^3.972.24", "@aws-sdk/types": "^3.973.6", "@smithy/node-config-provider": "^4.3.12", "@smithy/types": "^4.13.1", "@smithy/util-config-provider": "^4.2.2", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-E99zeTscCc+pTMfsvnfi6foPpKmdD1cZfOC7/P8UUrjsoQdg9VEWPRD+xdFduKnfPXwcvby58AlO9jwwF6U96g=="],
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/config-resolver": ["@smithy/config-resolver@4.4.13", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.12", "@smithy/types": "^4.13.1", "@smithy/util-config-provider": "^4.2.2", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "tslib": "^2.6.2" } }, "sha512-iIzMC5NmOUP6WL6o8iPBjFhUhBZ9pPjpUpQYWMUFQqKyXXzOftbfK8zcQCz/jFV1Psmf05BK5ypx4K2r4Tnwdg=="],
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/core": ["@smithy/core@3.23.12", "", { "dependencies": { "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-middleware": "^4.2.12", "@smithy/util-stream": "^4.5.20", "@smithy/util-utf8": "^4.2.2", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-o9VycsYNtgC+Dy3I0yrwCqv9CWicDnke0L7EVOrZtJpjb2t0EjaEofmMrYc0T1Kn3yk32zm6cspxF9u9Bj7e5w=="],
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/eventstream-serde-browser": ["@smithy/eventstream-serde-browser@4.2.12", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-XUSuMxlTxV5pp4VpqZf6Sa3vT/Q75FVkLSpSSE3KkWBvAQWeuWt1msTv8fJfgA4/jcJhrbrbMzN1AC/hvPmm5A=="],
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/eventstream-serde-config-resolver": ["@smithy/eventstream-serde-config-resolver@4.3.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-7epsAZ3QvfHkngz6RXQYseyZYHlmWXSTPOfPmXkiS+zA6TBNo1awUaMFL9vxyXlGdoELmCZyZe1nQE+imbmV+Q=="],
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/eventstream-serde-node": ["@smithy/eventstream-serde-node@4.2.12", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-D1pFuExo31854eAvg89KMn9Oab/wEeJR6Buy32B49A9Ogdtx5fwZPqBHUlDzaCDpycTFk2+fSQgX689Qsk7UGA=="],
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.15", "", { "dependencies": { "@smithy/protocol-http": "^5.3.12", "@smithy/querystring-builder": "^4.2.12", "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "tslib": "^2.6.2" } }, "sha512-T4jFU5N/yiIfrtrsb9uOQn7RdELdM/7HbyLNr6uO/mpkj1ctiVs7CihVr51w4LyQlXWDpXFn4BElf1WmQvZu/A=="],
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/hash-blob-browser": ["@smithy/hash-blob-browser@4.2.13", "", { "dependencies": { "@smithy/chunked-blob-reader": "^5.2.2", "@smithy/chunked-blob-reader-native": "^4.2.3", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-YrF4zWKh+ghLuquldj6e/RzE3xZYL8wIPfkt0MqCRphVICjyyjH8OwKD7LLlKpVEbk4FLizFfC1+gwK6XQdR3g=="],
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/hash-node": ["@smithy/hash-node@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-QhBYbGrbxTkZ43QoTPrK72DoYviDeg6YKDrHTMJbbC+A0sml3kSjzFtXP7BtbyJnXojLfTQldGdUR0RGD8dA3w=="],
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/hash-stream-node": ["@smithy/hash-stream-node@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-O3YbmGExeafuM/kP7Y8r6+1y0hIh3/zn6GROx0uNlB54K9oihAL75Qtc+jFfLNliTi6pxOAYZrRKD9A7iA6UFw=="],
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/invalid-dependency": ["@smithy/invalid-dependency@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-/4F1zb7Z8LOu1PalTdESFHR0RbPwHd3FcaG1sI3UEIriQTWakysgJr65lc1jj6QY5ye7aFsisajotH6UhWfm/g=="],
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/md5-js": ["@smithy/md5-js@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-W/oIpHCpWU2+iAkfZYyGWE+qkpuf3vEXHLxQQDx9FPNZTTdnul0dZ2d/gUFrtQ5je1G2kp4cjG0/24YueG2LbQ=="],
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.2.12", "", { "dependencies": { "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-YE58Yz+cvFInWI/wOTrB+DbvUVz/pLn5mC5MvOV4fdRUc6qGwygyngcucRQjAhiCEbmfLOXX0gntSIcgMvAjmA=="],
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.27", "", { "dependencies": { "@smithy/core": "^3.23.12", "@smithy/middleware-serde": "^4.2.15", "@smithy/node-config-provider": "^4.3.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-middleware": "^4.2.12", "tslib": "^2.6.2" } }, "sha512-T3TFfUgXQlpcg+UdzcAISdZpj4Z+XECZ/cefgA6wLBd6V4lRi0svN2hBouN/be9dXQ31X4sLWz3fAQDf+nt6BA=="],
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/middleware-retry": ["@smithy/middleware-retry@4.4.44", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.12", "@smithy/protocol-http": "^5.3.12", "@smithy/service-error-classification": "^4.2.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-Y1Rav7m5CFRPQyM4CI0koD/bXjyjJu3EQxZZhtLGD88WIrBrQ7kqXM96ncd6rYnojwOo/u9MXu57JrEvu/nLrA=="],
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.15", "", { "dependencies": { "@smithy/core": "^3.23.12", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-ExYhcltZSli0pgAKOpQQe1DLFBLryeZ22605y/YS+mQpdNWekum9Ujb/jMKfJKgjtz1AZldtwA/wCYuKJgjjlg=="],
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-kruC5gRHwsCOuyCd4ouQxYjgRAym2uDlCvQ5acuMtRrcdfg7mFBg6blaxcJ09STpt3ziEkis6bhg1uwrWU7txw=="],
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.12", "", { "dependencies": { "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-tr2oKX2xMcO+rBOjobSwVAkV05SIfUKz8iI53rzxEmgW3GOOPOv0UioSDk+J8OpRQnpnhsO3Af6IEBabQBVmiw=="],
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/node-http-handler": ["@smithy/node-http-handler@4.5.0", "", { "dependencies": { "@smithy/abort-controller": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/querystring-builder": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-Rnq9vQWiR1+/I6NZZMNzJHV6pZYyEHt2ZnuV3MG8z2NNenC4i/8Kzttz7CjZiHSmsN5frhXhg17z3Zqjjhmz1A=="],
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/protocol-http": ["@smithy/protocol-http@5.3.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-fit0GZK9I1xoRlR4jXmbLhoN0OdEpa96ul8M65XdmXnxXkuMxM0Y8HDT0Fh0Xb4I85MBvBClOzgSrV1X2s1Hxw=="],
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/smithy-client": ["@smithy/smithy-client@4.12.7", "", { "dependencies": { "@smithy/core": "^3.23.12", "@smithy/middleware-endpoint": "^4.4.27", "@smithy/middleware-stack": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-stream": "^4.5.20", "tslib": "^2.6.2" } }, "sha512-q3gqnwml60G44FECaEEsdQMplYhDMZYCtYhMCzadCnRnnHIobZJjegmdoUo6ieLQlPUzvrMdIJUpx6DoPmzANQ=="],
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/types": ["@smithy/types@4.13.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g=="],
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/url-parser": ["@smithy/url-parser@4.2.12", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-wOPKPEpso+doCZGIlr+e1lVI6+9VAKfL4kZWFgzVgGWY2hZxshNKod4l2LXS3PRC9otH/JRSjtEHqQ/7eLciRA=="],
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.43", "", { "dependencies": { "@smithy/property-provider": "^4.2.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-Qd/0wCKMaXxev/z00TvNzGCH2jlKKKxXP1aDxB6oKwSQthe3Og2dMhSayGCnsma1bK/kQX1+X7SMP99t6FgiiQ=="],
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.47", "", { "dependencies": { "@smithy/config-resolver": "^4.4.13", "@smithy/credential-provider-imds": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/property-provider": "^4.2.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-qSRbYp1EQ7th+sPFuVcVO05AE0QH635hycdEXlpzIahqHHf2Fyd/Zl+8v0XYMJ3cgDVPa0lkMefU7oNUjAP+DQ=="],
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/util-endpoints": ["@smithy/util-endpoints@3.3.3", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-VACQVe50j0HZPjpwWcjyT51KUQ4AnsvEaQ2lKHOSL4mNLD0G9BjEniQ+yCt1qqfKfiAHRAts26ud7hBjamrwig=="],
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/util-middleware": ["@smithy/util-middleware@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-Er805uFUOvgc0l8nv0e0su0VFISoxhJ/AwOn3gL2NWNY2LUEldP5WtVcRYSQBcjg0y9NfG8JYrCJaYDpupBHJQ=="],
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/util-retry": ["@smithy/util-retry@4.2.12", "", { "dependencies": { "@smithy/service-error-classification": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-1zopLDUEOwumjcHdJ1mwBHddubYF8GMQvstVCLC54Y46rqoHwlIU+8ZzUeaBcD+WCJHyDGSeZ2ml9YSe9aqcoQ=="],
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/util-stream": ["@smithy/util-stream@4.5.20", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.15", "@smithy/node-http-handler": "^4.5.0", "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-4yXLm5n/B5SRBR2p8cZ90Sbv4zL4NKsgxdzCzp/83cXw2KxLEumt5p+GAVyRNZgQOSrzXn9ARpO0lUe8XSlSDw=="],
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/util-waiter": ["@smithy/util-waiter@4.2.13", "", { "dependencies": { "@smithy/abort-controller": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-2zdZ9DTHngRtcYxJK1GUDxruNr53kv5W2Lupe0LMU+Imr6ohQg8M2T14MNkj1Y0wS3FFwpgpGQyvuaMF7CiTmQ=="],
"@browseros/agent/@types/bun/bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="],
"@browseros/eval/@aws-sdk/client-s3/@aws-sdk/core": ["@aws-sdk/core@3.973.23", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@aws-sdk/xml-builder": "^3.972.15", "@smithy/core": "^3.23.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/property-provider": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/signature-v4": "^5.3.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.12", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-aoJncvD1XvloZ9JLnKqTRL9dBy+Szkryoag9VT+V1TqsuUgIxV9cnBVM/hrDi2vE8bDqLiDR8nirdRcCdtJu0w=="],
@@ -5597,6 +5706,70 @@
"@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.15", "", { "dependencies": { "@smithy/types": "^4.13.1", "fast-xml-parser": "5.5.8", "tslib": "^2.6.2" } }, "sha512-PxMRlCFNiQnke9YR29vjFQwz4jq+6Q04rOVFeTDR2K7Qpv9h9FOWOxG+zJjageimYbWqE3bTuLjmryWHAWbvaA=="],
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/core/@smithy/property-provider": ["@smithy/property-provider@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A=="],
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.3.12", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.2", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-middleware": "^4.2.12", "@smithy/util-uri-escape": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-B/FBwO3MVOL00DaRSXfXfa/TRXRheagt/q5A2NM13u7q+sHS59EOVGQNfG7DkmVtdQm5m3vOosoKAXSqn/OEgw=="],
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.21", "", { "dependencies": { "@aws-sdk/core": "^3.973.23", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-BkAfKq8Bd4shCtec1usNz//urPJF/SZy14qJyxkSaRJQ/Vv1gVh0VZSTmS7aE6aLMELkFV5wHHrS9ZcdG8Kxsg=="],
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.23", "", { "dependencies": { "@aws-sdk/core": "^3.973.23", "@aws-sdk/types": "^3.973.6", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/node-http-handler": "^4.5.0", "@smithy/property-provider": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/util-stream": "^4.5.20", "tslib": "^2.6.2" } }, "sha512-4XZ3+Gu5DY8/n8zQFHBgcKTF7hWQl42G6CY9xfXVo2d25FM/lYkpmuzhYopYoPL1ITWkJ2OSBQfYEu5JRfHOhA=="],
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.972.23", "", { "dependencies": { "@aws-sdk/core": "^3.973.23", "@aws-sdk/credential-provider-env": "^3.972.21", "@aws-sdk/credential-provider-http": "^3.972.23", "@aws-sdk/credential-provider-login": "^3.972.23", "@aws-sdk/credential-provider-process": "^3.972.21", "@aws-sdk/credential-provider-sso": "^3.972.23", "@aws-sdk/credential-provider-web-identity": "^3.972.23", "@aws-sdk/nested-clients": "^3.996.13", "@aws-sdk/types": "^3.973.6", "@smithy/credential-provider-imds": "^4.2.12", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-PZLSmU0JFpNCDFReidBezsgL5ji9jOBry8CnZdw4Jj6d0K2z3Ftnp44NXgADqYx5BLMu/ZHujfeJReaDoV+IwQ=="],
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.21", "", { "dependencies": { "@aws-sdk/core": "^3.973.23", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-nRxbeOJ1E1gVA0lNQezuMVndx+ZcuyaW/RB05pUsznN5BxykSlH6KkZ/7Ca/ubJf3i5N3p0gwNO5zgPSCzj+ww=="],
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.972.23", "", { "dependencies": { "@aws-sdk/core": "^3.973.23", "@aws-sdk/nested-clients": "^3.996.13", "@aws-sdk/token-providers": "3.1014.0", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-APUccADuYPLL0f2htpM8Z4czabSmHOdo4r41W6lKEZdy++cNJ42Radqy6x4TopENzr3hR6WYMyhiuiqtbf/nAA=="],
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.23", "", { "dependencies": { "@aws-sdk/core": "^3.973.23", "@aws-sdk/nested-clients": "^3.996.13", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-H5JNqtIwOu/feInmMMWcK0dL5r897ReEn7n2m16Dd0DPD9gA2Hg8Cq4UDzZ/9OzaLh/uqBM6seixz0U6Fi2Eag=="],
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.12", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.12", "@smithy/property-provider": "^4.2.12", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "tslib": "^2.6.2" } }, "sha512-cr2lR792vNZcYMriSIj+Um3x9KWrjcu98kn234xA6reOAFMmbRpQMOv8KPgEmLLtx3eldU6c5wALKFqNOhugmg=="],
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@smithy/property-provider": ["@smithy/property-provider@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A=="],
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.7", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-HrOKWsUb+otTeo1HxVWeEb99t5ER1XrBi/xka2Wv6NVmTbuCUC1dvlrksdvxFtODLBjsC+PHK+fuy2x/7Ynyiw=="],
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/middleware-bucket-endpoint/@aws-sdk/util-arn-parser": ["@aws-sdk/util-arn-parser@3.972.3", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA=="],
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/middleware-flexible-checksums/@aws-sdk/crc64-nvme": ["@aws-sdk/crc64-nvme@3.972.5", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-2VbTstbjKdT+yKi8m7b3a9CiVac+pL/IY2PHJwsaGkkHmuuqkJZIErPck1h6P3T9ghQMLSdMPyW6Qp7Di5swFg=="],
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/middleware-sdk-s3/@aws-sdk/util-arn-parser": ["@aws-sdk/util-arn-parser@3.972.3", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA=="],
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/middleware-sdk-s3/@smithy/signature-v4": ["@smithy/signature-v4@5.3.12", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.2", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-middleware": "^4.2.12", "@smithy/util-uri-escape": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-B/FBwO3MVOL00DaRSXfXfa/TRXRheagt/q5A2NM13u7q+sHS59EOVGQNfG7DkmVtdQm5m3vOosoKAXSqn/OEgw=="],
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/signature-v4-multi-region/@smithy/signature-v4": ["@smithy/signature-v4@5.3.12", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.2", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-middleware": "^4.2.12", "@smithy/util-uri-escape": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-B/FBwO3MVOL00DaRSXfXfa/TRXRheagt/q5A2NM13u7q+sHS59EOVGQNfG7DkmVtdQm5m3vOosoKAXSqn/OEgw=="],
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/eventstream-serde-browser/@smithy/eventstream-serde-universal": ["@smithy/eventstream-serde-universal@4.2.12", "", { "dependencies": { "@smithy/eventstream-codec": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-+yNuTiyBACxOJUTvbsNsSOfH9G9oKbaJE1lNL3YHpGcuucl6rPZMi3nrpehpVOVR2E07YqFFmtwpImtpzlouHQ=="],
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/eventstream-serde-node/@smithy/eventstream-serde-universal": ["@smithy/eventstream-serde-universal@4.2.12", "", { "dependencies": { "@smithy/eventstream-codec": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-+yNuTiyBACxOJUTvbsNsSOfH9G9oKbaJE1lNL3YHpGcuucl6rPZMi3nrpehpVOVR2E07YqFFmtwpImtpzlouHQ=="],
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/fetch-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "@smithy/util-uri-escape": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-6wTZjGABQufekycfDGMEB84BgtdOE/rCVTov+EDXQ8NHKTUNIp/j27IliwP7tjIU9LR+sSzyGBOXjeEtVgzCHg=="],
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/middleware-endpoint/@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.7", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-HrOKWsUb+otTeo1HxVWeEb99t5ER1XrBi/xka2Wv6NVmTbuCUC1dvlrksdvxFtODLBjsC+PHK+fuy2x/7Ynyiw=="],
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/middleware-retry/@smithy/service-error-classification": ["@smithy/service-error-classification@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1" } }, "sha512-LlP29oSQN0Tw0b6D0Xo6BIikBswuIiGYbRACy5ujw/JgWSzTdYj46U83ssf6Ux0GyNJVivs2uReU8pt7Eu9okQ=="],
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/node-config-provider/@smithy/property-provider": ["@smithy/property-provider@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A=="],
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/node-config-provider/@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.7", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-HrOKWsUb+otTeo1HxVWeEb99t5ER1XrBi/xka2Wv6NVmTbuCUC1dvlrksdvxFtODLBjsC+PHK+fuy2x/7Ynyiw=="],
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/node-http-handler/@smithy/abort-controller": ["@smithy/abort-controller@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-xolrFw6b+2iYGl6EcOL7IJY71vvyZ0DJ3mcKtpykqPe2uscwtzDZJa1uVQXyP7w9Dd+kGwYnPbMsJrGISKiY/Q=="],
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/node-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "@smithy/util-uri-escape": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-6wTZjGABQufekycfDGMEB84BgtdOE/rCVTov+EDXQ8NHKTUNIp/j27IliwP7tjIU9LR+sSzyGBOXjeEtVgzCHg=="],
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/url-parser/@smithy/querystring-parser": ["@smithy/querystring-parser@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-P2OdvrgiAKpkPNKlKUtWbNZKB1XjPxM086NeVhK+W+wI46pIKdWBe5QyXvhUm3MEcyS/rkLvY8rZzyUdmyDZBw=="],
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/util-defaults-mode-browser/@smithy/property-provider": ["@smithy/property-provider@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A=="],
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/util-defaults-mode-node/@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.12", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.12", "@smithy/property-provider": "^4.2.12", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "tslib": "^2.6.2" } }, "sha512-cr2lR792vNZcYMriSIj+Um3x9KWrjcu98kn234xA6reOAFMmbRpQMOv8KPgEmLLtx3eldU6c5wALKFqNOhugmg=="],
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/util-defaults-mode-node/@smithy/property-provider": ["@smithy/property-provider@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A=="],
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/util-retry/@smithy/service-error-classification": ["@smithy/service-error-classification@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1" } }, "sha512-LlP29oSQN0Tw0b6D0Xo6BIikBswuIiGYbRACy5ujw/JgWSzTdYj46U83ssf6Ux0GyNJVivs2uReU8pt7Eu9okQ=="],
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/util-waiter/@smithy/abort-controller": ["@smithy/abort-controller@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-xolrFw6b+2iYGl6EcOL7IJY71vvyZ0DJ3mcKtpykqPe2uscwtzDZJa1uVQXyP7w9Dd+kGwYnPbMsJrGISKiY/Q=="],
"@browseros/eval/@aws-sdk/client-s3/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.15", "", { "dependencies": { "@smithy/types": "^4.13.1", "fast-xml-parser": "5.5.8", "tslib": "^2.6.2" } }, "sha512-PxMRlCFNiQnke9YR29vjFQwz4jq+6Q04rOVFeTDR2K7Qpv9h9FOWOxG+zJjageimYbWqE3bTuLjmryWHAWbvaA=="],
"@browseros/eval/@aws-sdk/client-s3/@aws-sdk/core/@smithy/property-provider": ["@smithy/property-provider@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A=="],
@@ -5705,6 +5878,22 @@
"wxt/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.5.8", "", { "dependencies": { "fast-xml-builder": "^1.1.4", "path-expression-matcher": "^1.2.0", "strnum": "^2.2.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ=="],
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.23", "", { "dependencies": { "@aws-sdk/core": "^3.973.23", "@aws-sdk/nested-clients": "^3.996.13", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-OmE/pSkbMM3dCj1HdOnZ5kXnKK+R/Yz+kbBugraBecp0pGAs21eEURfQRz+1N2gzIHLVyGIP1MEjk/uSrFsngg=="],
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.996.13", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.23", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.8", "@aws-sdk/middleware-user-agent": "^3.972.24", "@aws-sdk/region-config-resolver": "^3.972.9", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", "@aws-sdk/util-user-agent-node": "^3.973.10", "@smithy/config-resolver": "^4.4.13", "@smithy/core": "^3.23.12", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/hash-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", "@smithy/middleware-endpoint": "^4.4.27", "@smithy/middleware-retry": "^4.4.44", "@smithy/middleware-serde": "^4.2.15", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/node-http-handler": "^4.5.0", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.43", "@smithy/util-defaults-mode-node": "^4.2.47", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-ptZ1HF4yYHNJX8cgFF+8NdYO69XJKZn7ft0/ynV3c0hCbN+89fAbrLS+fqniU2tW8o9Kfqhj8FUh+IPXb2Qsuw=="],
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.996.13", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.23", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.8", "@aws-sdk/middleware-user-agent": "^3.972.24", "@aws-sdk/region-config-resolver": "^3.972.9", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", "@aws-sdk/util-user-agent-node": "^3.973.10", "@smithy/config-resolver": "^4.4.13", "@smithy/core": "^3.23.12", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/hash-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", "@smithy/middleware-endpoint": "^4.4.27", "@smithy/middleware-retry": "^4.4.44", "@smithy/middleware-serde": "^4.2.15", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/node-http-handler": "^4.5.0", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.43", "@smithy/util-defaults-mode-node": "^4.2.47", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-ptZ1HF4yYHNJX8cgFF+8NdYO69XJKZn7ft0/ynV3c0hCbN+89fAbrLS+fqniU2tW8o9Kfqhj8FUh+IPXb2Qsuw=="],
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1014.0", "", { "dependencies": { "@aws-sdk/core": "^3.973.23", "@aws-sdk/nested-clients": "^3.996.13", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-gHTHNUoaOGNrSWkl32A7wFsU78jlNTlqMccLu0byUk5CysYYXaxNMIonIVr4YcykC7vgtDS5ABuz83giy6fzJA=="],
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.996.13", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.23", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.8", "@aws-sdk/middleware-user-agent": "^3.972.24", "@aws-sdk/region-config-resolver": "^3.972.9", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", "@aws-sdk/util-user-agent-node": "^3.973.10", "@smithy/config-resolver": "^4.4.13", "@smithy/core": "^3.23.12", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/hash-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", "@smithy/middleware-endpoint": "^4.4.27", "@smithy/middleware-retry": "^4.4.44", "@smithy/middleware-serde": "^4.2.15", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/node-http-handler": "^4.5.0", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.43", "@smithy/util-defaults-mode-node": "^4.2.47", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-ptZ1HF4yYHNJX8cgFF+8NdYO69XJKZn7ft0/ynV3c0hCbN+89fAbrLS+fqniU2tW8o9Kfqhj8FUh+IPXb2Qsuw=="],
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/eventstream-serde-browser/@smithy/eventstream-serde-universal/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.12", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.13.1", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-FE3bZdEl62ojmy8x4FHqxq2+BuOHlcxiH5vaZ6aqHJr3AIZzwF5jfx8dEiU/X0a8RboyNDjmXjlbr8AdEyLgiA=="],
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/eventstream-serde-node/@smithy/eventstream-serde-universal/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.12", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.13.1", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-FE3bZdEl62ojmy8x4FHqxq2+BuOHlcxiH5vaZ6aqHJr3AIZzwF5jfx8dEiU/X0a8RboyNDjmXjlbr8AdEyLgiA=="],
"@browseros/eval/@aws-sdk/client-s3/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.5.8", "", { "dependencies": { "fast-xml-builder": "^1.1.4", "path-expression-matcher": "^1.2.0", "strnum": "^2.2.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ=="],
"@browseros/eval/@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.23", "", { "dependencies": { "@aws-sdk/core": "^3.973.23", "@aws-sdk/nested-clients": "^3.996.13", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-OmE/pSkbMM3dCj1HdOnZ5kXnKK+R/Yz+kbBugraBecp0pGAs21eEURfQRz+1N2gzIHLVyGIP1MEjk/uSrFsngg=="],
@@ -5733,6 +5922,8 @@
"publish-browser-extension/listr2/cli-truncate/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/fast-xml-builder": ["fast-xml-builder@1.1.4", "", { "dependencies": { "path-expression-matcher": "^1.1.3" } }, "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg=="],
"@browseros/eval/@aws-sdk/client-s3/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/fast-xml-builder": ["fast-xml-builder@1.1.4", "", { "dependencies": { "path-expression-matcher": "^1.1.3" } }, "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg=="],
"@google/genai/google-auth-library/gaxios/rimraf/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],

View File

@@ -0,0 +1,14 @@
# Required for `bun run --filter @browseros/agent-container upload`
R2_ACCOUNT_ID=
R2_ACCESS_KEY_ID=
R2_SECRET_ACCESS_KEY=
R2_BUCKET=
# Optional overrides
R2_PUBLIC_BASE_URL=https://cdn.browseros.com
PODMAN_BIN=podman
# Optional recipe-driven registry auth
# If an agent entry in recipe/agents.json sets `requires_auth.secret`,
# define that env var before running `build`.
# EXAMPLE_REGISTRY_TOKEN=

View File

@@ -0,0 +1,52 @@
# @browseros/agent-container
OCI tarball producer for BrowserOS-bundled agent containers.
This package owns the WS2 pipeline:
- Read the active agent set from `recipe/agents.json`
- Pull the upstream image with `podman`
- Save it as an OCI archive and gzip it
- Smoke-test the archive with `podman load`
- Publish tarballs, checksum sidecars, and manifests to R2
Package env requirements are documented in [.env.sample](./.env.sample).
## Local usage
```bash
cd packages/browseros-agent
# Print the GitHub Actions matrix JSON
bun run --filter @browseros/agent-container list-matrix
# Build one artifact locally
bun run --filter @browseros/agent-container build -- \
--agent openclaw \
--arch arm64 \
--output-dir packages/agent-container/dist/agent-container/openclaw/arm64
# Smoke-test a built tarball
bun run --filter @browseros/agent-container smoke -- \
--tarball packages/agent-container/dist/agent-container/openclaw/arm64/openclaw-2026.4.12-arm64.tar.gz \
--expected-image ghcr.io/openclaw/openclaw:2026.4.12 \
--expected-fingerprint ...
# Upload pre-built artifacts
# Fill these from packages/agent-container/.env.sample
R2_ACCOUNT_ID=... \
R2_ACCESS_KEY_ID=... \
R2_SECRET_ACCESS_KEY=... \
R2_BUCKET=... \
bun run --filter @browseros/agent-container upload -- \
--artifact-dir packages/agent-container/dist/agent-container \
--update-aggregate
```
## Notes
- `recipe/agents.json` is the source of truth for the active set.
- `workflow_dispatch` version overrides are intended for dry runs. Publishing still needs the recipe to be authoritative.
- `src/load.ts` is intentionally stubbed. WS6 fills in the runtime consumer path.
- Private registry auth is recipe-driven: if `requires_auth.secret` is set for an agent, export that env var before running `build`.
- When invoking package scripts with `bun run --filter @browseros/agent-container`, pass artifact paths that are explicit from `packages/browseros-agent`. Filtered scripts run inside the package directory, so bare `dist/...` paths are ambiguous.

View File

@@ -0,0 +1,32 @@
{
"name": "@browseros/agent-container",
"version": "0.0.0",
"private": true,
"type": "module",
"description": "BrowserOS agent container OCI tarball producer",
"exports": {
"./schema": {
"types": "./src/schema/index.ts",
"default": "./src/schema/index.ts"
},
"./load": {
"types": "./src/load.ts",
"default": "./src/load.ts"
}
},
"scripts": {
"build": "bun run scripts/build.ts",
"upload": "bun run scripts/upload.ts",
"smoke": "bun run scripts/smoke.ts",
"list-matrix": "bun run scripts/list-matrix.ts",
"test": "bun test",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.933.0",
"zod": "^3.24.2"
},
"devDependencies": {
"@types/node": "^24.3.3"
}
}

View File

@@ -0,0 +1,12 @@
{
"schema": "v1",
"agents": [
{
"name": "openclaw",
"image": "ghcr.io/openclaw/openclaw",
"version": "2026.4.12",
"arches": ["amd64", "arm64"],
"publishAs": "openclaw"
}
]
}

View File

@@ -0,0 +1,51 @@
#!/usr/bin/env bun
import { resolve } from 'node:path'
import { parseArgs } from 'node:util'
import { buildTarball } from '../src/build'
import { readAgentsConfig } from '../src/catalog'
import { parseArch } from '../src/schema/arch'
const packageRoot = resolve(import.meta.dir, '..')
const recipePath = resolve(packageRoot, 'recipe', 'agents.json')
const { values } = parseArgs({
args: Bun.argv.slice(2),
options: {
agent: { type: 'string' },
version: { type: 'string' },
arch: { type: 'string' },
'output-dir': { type: 'string' },
help: { type: 'boolean', short: 'h' },
},
})
if (values.help) {
console.log(
'Usage: bun run build -- --agent <name> --arch <amd64|arm64> --output-dir <path> [--version <override>]',
)
process.exit(0)
}
if (!values.agent || !values.arch || !values['output-dir']) {
throw new Error('--agent, --arch, and --output-dir are required')
}
const config = await readAgentsConfig(recipePath)
const selected = config.agents.find((agent) => agent.name === values.agent)
if (!selected) {
throw new Error(`unknown agent: ${values.agent}`)
}
const result = await buildTarball({
agent: {
...selected,
version: values.version ?? selected.version,
},
arch: parseArch(values.arch),
outputDir: values['output-dir'],
recipePath,
})
console.log(JSON.stringify(result, null, 2))

View File

@@ -0,0 +1,35 @@
#!/usr/bin/env bun
import { resolve } from 'node:path'
import { parseArgs } from 'node:util'
import { expandMatrix, readAgentsConfig } from '../src/catalog'
const packageRoot = resolve(import.meta.dir, '..')
const recipePath = resolve(packageRoot, 'recipe', 'agents.json')
const { values } = parseArgs({
args: Bun.argv.slice(2),
options: {
agent: { type: 'string' },
help: { type: 'boolean', short: 'h' },
},
})
if (values.help) {
console.log('Usage: bun run list-matrix [--agent <name>]')
process.exit(0)
}
const config = await readAgentsConfig(recipePath)
const include = expandMatrix(config, { agent: values.agent })
if (include.length === 0) {
throw new Error(
values.agent
? `no agents matched filter: ${values.agent}`
: 'recipe/agents.json produced an empty matrix',
)
}
console.log(JSON.stringify({ include }))

View File

@@ -0,0 +1,42 @@
#!/usr/bin/env bun
import { parseArgs } from 'node:util'
import { roundTripPodmanLoad } from '../src/smoke'
const { values } = parseArgs({
args: Bun.argv.slice(2),
options: {
tarball: { type: 'string' },
'expected-image': { type: 'string' },
'expected-image-id': { type: 'string' },
'expected-fingerprint': { type: 'string' },
'expected-digest': { type: 'string' },
help: { type: 'boolean', short: 'h' },
},
})
if (values.help) {
console.log(
'Usage: bun run smoke -- --tarball <path> --expected-image <ref> [--expected-fingerprint <sha256-hex>] [--expected-image-id <sha256:...> | --expected-digest <sha256:...>]',
)
process.exit(0)
}
const expectedImageId = values['expected-image-id'] ?? values['expected-digest']
if (
!values.tarball ||
!values['expected-image'] ||
(!expectedImageId && !values['expected-fingerprint'])
) {
throw new Error(
'--tarball, --expected-image, and one verification flag are required',
)
}
await roundTripPodmanLoad({
tarballPath: values.tarball,
expectedImage: values['expected-image'],
expectedImageId,
expectedSmokeFingerprint: values['expected-fingerprint'],
})

View File

@@ -0,0 +1,62 @@
#!/usr/bin/env bun
import { readdir } from 'node:fs/promises'
import { join, resolve } from 'node:path'
import { parseArgs } from 'node:util'
import { loadBuildResult } from '../src/build'
import { publishAgents } from '../src/publish'
async function findBuildResultPaths(root: string): Promise<string[]> {
const entries = await readdir(root, { withFileTypes: true })
const paths: string[] = []
for (const entry of entries) {
const path = join(root, entry.name)
if (entry.isDirectory()) {
paths.push(...(await findBuildResultPaths(path)))
continue
}
if (entry.isFile() && entry.name === 'build-result.json') {
paths.push(path)
}
}
return paths.sort()
}
const { values } = parseArgs({
args: Bun.argv.slice(2),
options: {
'artifact-dir': { type: 'string' },
'update-aggregate': { type: 'boolean' },
help: { type: 'boolean', short: 'h' },
},
})
if (values.help) {
console.log(
'Usage: bun run upload -- --artifact-dir <path> [--update-aggregate]',
)
process.exit(0)
}
if (!values['artifact-dir']) {
throw new Error('--artifact-dir is required')
}
const artifactDir = resolve(values['artifact-dir'])
const buildResultPaths = await findBuildResultPaths(artifactDir)
if (buildResultPaths.length === 0) {
throw new Error(`no build-result.json files found under ${artifactDir}`)
}
const buildResults = await Promise.all(
buildResultPaths.map((path) => loadBuildResult(path)),
)
await publishAgents({
buildResults,
updateAggregate: Boolean(values['update-aggregate']),
})

View File

@@ -0,0 +1,470 @@
import { createHash } from 'node:crypto'
import { createReadStream } from 'node:fs'
import { access, mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises'
import { basename, dirname, join, resolve } from 'node:path'
import { type AgentEntry, publishNameForAgent } from './catalog'
import type { ContainerArch } from './schema/arch'
const PODMAN_BIN = process.env.PODMAN_BIN ?? 'podman'
interface PodmanCommandResult {
stdout: string
stderr: string
}
interface PodmanInspectShape {
Id?: string
Digest?: string
RepoDigests?: string[]
Architecture?: string
Os?: string
Config?: unknown
RootFS?: unknown
}
interface PodmanImageMetadata {
imageId: string
sourceOciDigest: string
smokeFingerprint: string
}
interface RepoDigestCount {
digest: string
count: number
}
export interface BuildOptions {
agent: AgentEntry
arch: ContainerArch
outputDir: string
recipePath?: string
builtBy?: string
}
export interface BuildResult {
name: string
publishAs: string
image: string
version: string
arch: ContainerArch
sourceOciDigest: string
imageId: string
smokeFingerprint: string
filename: string
tarballPath: string
tarballShaPath: string
compressedSha256: string
compressedSizeBytes: number
uncompressedSha256: string
uncompressedSizeBytes: number
podmanVersion: string
builtAt: string
builtBy: string
gitSha: string
gitDirty: boolean
configSha256: string
}
function stableJson(value: unknown): string {
if (Array.isArray(value)) {
return `[${value.map((entry) => stableJson(entry)).join(',')}]`
}
if (value && typeof value === 'object') {
const entries = Object.entries(value as Record<string, unknown>).sort(
([left], [right]) => left.localeCompare(right),
)
return `{${entries
.map(([key, entry]) => `${JSON.stringify(key)}:${stableJson(entry)}`)
.join(',')}}`
}
return JSON.stringify(value)
}
function smokeFingerprintForInspect(inspected: PodmanInspectShape): string {
const payload = stableJson({
Architecture: inspected.Architecture ?? '',
Os: inspected.Os ?? '',
Config: inspected.Config ?? null,
RootFS: inspected.RootFS ?? null,
})
return createHash('sha256').update(payload).digest('hex')
}
function normalizeSha256Like(value: string): string {
const trimmed = value.trim()
if (/^sha256:[a-f0-9]{64}$/.test(trimmed)) {
return trimmed
}
if (/^[a-f0-9]{64}$/.test(trimmed)) {
return `sha256:${trimmed}`
}
throw new Error(`unexpected sha256-like value: ${value}`)
}
function selectSourceOciDigest(
platformDigest: string,
repoDigests: string[],
): string {
const counts = new Map<string, number>()
for (const digest of repoDigests) {
counts.set(digest, (counts.get(digest) ?? 0) + 1)
}
const candidates: RepoDigestCount[] = [...counts.entries()]
.filter(([digest]) => digest !== platformDigest)
.map(([digest, count]) => ({ digest, count }))
.sort(
(left, right) =>
right.count - left.count || left.digest.localeCompare(right.digest),
)
const [firstCandidate, secondCandidate] = candidates
if (!firstCandidate) {
return platformDigest
}
if (!secondCandidate || firstCandidate.count > secondCandidate.count) {
return firstCandidate.digest
}
throw new Error(
`ambiguous source OCI digest for ${platformDigest}: ${candidates
.map((candidate) => `${candidate.digest} (${candidate.count})`)
.join(', ')}`,
)
}
async function runPodman(
args: string[],
options: { stdin?: string } = {},
): Promise<PodmanCommandResult> {
const proc = Bun.spawn([PODMAN_BIN, ...args], {
stdin: options.stdin ? Buffer.from(`${options.stdin}\n`) : undefined,
stdout: 'pipe',
stderr: 'pipe',
})
const [stdout, stderr] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
])
const exitCode = await proc.exited
if (exitCode !== 0) {
throw new Error(
`podman ${args.join(' ')} exited ${exitCode}\n${stderr.trim() || stdout.trim()}`,
)
}
return { stdout, stderr }
}
async function runCommand(command: string[]): Promise<string> {
const proc = Bun.spawn(command, {
stdout: 'pipe',
stderr: 'pipe',
})
const [stdout, stderr] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
])
const exitCode = await proc.exited
if (exitCode !== 0) {
throw new Error(
`${command.join(' ')} exited ${exitCode}\n${stderr.trim() || stdout.trim()}`,
)
}
return stdout.trim()
}
async function sha256OfFile(path: string): Promise<string> {
const hash = createHash('sha256')
const stream = createReadStream(path)
for await (const chunk of stream) {
hash.update(chunk)
}
return hash.digest('hex')
}
async function gzipArchive(tarPath: string): Promise<void> {
const proc = Bun.spawn(['gzip', '-9', '-f', '-k', tarPath], {
stdout: 'pipe',
stderr: 'pipe',
})
const stderr = await new Response(proc.stderr).text()
const exitCode = await proc.exited
if (exitCode !== 0) {
throw new Error(`gzip exited ${exitCode}\n${stderr.trim()}`)
}
}
async function gitSha(): Promise<string> {
return runCommand(['git', 'rev-parse', 'HEAD'])
}
async function gitDirty(): Promise<boolean> {
const stdout = await runCommand(['git', 'status', '--short'])
return stdout.length > 0
}
function recipePathForPackage(): string {
return resolve(import.meta.dir, '..', 'recipe', 'agents.json')
}
function imageRefForBuild(options: BuildOptions): string {
return `${options.agent.image}:${options.agent.version}`
}
function builtByForBuild(explicitBuiltBy?: string): string {
if (explicitBuiltBy) {
return explicitBuiltBy
}
const workflowRef = process.env.GITHUB_WORKFLOW_REF?.trim()
if (workflowRef) {
return workflowRef
}
const workflow = process.env.GITHUB_WORKFLOW?.trim()
const ref = process.env.GITHUB_REF?.trim()
if (workflow && ref) {
return `${workflow}@${ref}`
}
const user = process.env.USER ?? process.env.LOGNAME ?? 'unknown'
return `local:${user}`
}
export function registryForImage(image: string): string {
const firstSegment = image.split('/')[0]
if (
!firstSegment ||
(!firstSegment.includes('.') &&
!firstSegment.includes(':') &&
firstSegment !== 'localhost')
) {
return 'docker.io'
}
return firstSegment
}
async function podmanVersion(): Promise<string> {
const { stdout } = await runPodman(['--version'])
return stdout.trim()
}
async function podmanLogin(options: {
registry: string
username: string
password: string
}): Promise<void> {
await runPodman(
[
'login',
'--username',
options.username,
'--password-stdin',
options.registry,
],
{ stdin: options.password },
)
}
async function podmanPull(
imageRef: string,
arch: ContainerArch,
): Promise<void> {
await runPodman([
'pull',
'--quiet',
'--os',
'linux',
'--arch',
arch,
imageRef,
])
}
export async function podmanInspectImage(
imageRef: string,
): Promise<PodmanImageMetadata> {
const { stdout } = await runPodman([
'inspect',
'--type',
'image',
'--format',
'{{json .}}',
imageRef,
])
const inspected = JSON.parse(stdout.trim()) as PodmanInspectShape
const imageId = normalizeSha256Like(inspected.Id ?? '')
const platformDigest = normalizeSha256Like(inspected.Digest ?? imageId)
const repoDigests = (inspected.RepoDigests ?? [])
.map((entry) => entry.split('@')[1] ?? '')
.filter(Boolean)
.map((entry) => normalizeSha256Like(entry))
const sourceOciDigest = selectSourceOciDigest(platformDigest, repoDigests)
return {
imageId,
sourceOciDigest,
smokeFingerprint: smokeFingerprintForInspect(inspected),
}
}
async function podmanSaveOci(options: {
imageRef: string
outPath: string
}): Promise<void> {
await runPodman([
'save',
'--format',
'oci-archive',
'--output',
options.outPath,
options.imageRef,
])
}
export async function podmanLoadArchive(tarballPath: string): Promise<void> {
await runPodman(['load', '--input', tarballPath])
}
export async function podmanRemoveImage(imageRef: string): Promise<void> {
await runPodman(['rmi', '-f', imageRef])
}
async function maybeLoginForAgent(options: BuildOptions): Promise<void> {
const auth = options.agent.requires_auth
if (!auth) {
return
}
const password = process.env[auth.secret]?.trim()
if (!password) {
throw new Error(`missing registry credential env var: ${auth.secret}`)
}
await podmanLogin({
registry: registryForImage(options.agent.image),
username: auth.username ?? 'oauth2accesstoken',
password,
})
}
export async function buildTarball(
options: BuildOptions,
): Promise<BuildResult> {
const imageRef = imageRefForBuild(options)
const publishAs = publishNameForAgent(options.agent)
const outputDir = resolve(options.outputDir)
const recipePath = resolve(options.recipePath ?? recipePathForPackage())
const baseName = `${publishAs}-${options.agent.version}-${options.arch}.tar`
const tarPath = join(outputDir, baseName)
const tarballPath = `${tarPath}.gz`
const tarballShaPath = `${tarballPath}.sha256`
const buildResultPath = join(outputDir, 'build-result.json')
await mkdir(outputDir, { recursive: true })
await Promise.all([
rm(tarPath, { force: true }),
rm(tarballPath, { force: true }),
rm(tarballShaPath, { force: true }),
rm(buildResultPath, { force: true }),
])
const [gitShaValue, gitDirtyValue, configSha256, podmanVersionValue] =
await Promise.all([
gitSha(),
gitDirty(),
sha256OfFile(recipePath),
podmanVersion(),
])
const builtAt = new Date().toISOString()
const builtBy = builtByForBuild(options.builtBy)
await maybeLoginForAgent(options)
await podmanPull(imageRef, options.arch)
const inspection = await podmanInspectImage(imageRef)
await podmanSaveOci({ imageRef, outPath: tarPath })
await gzipArchive(tarPath)
const [
compressedSha256,
uncompressedSha256,
compressedStats,
uncompressedStats,
] = await Promise.all([
sha256OfFile(tarballPath),
sha256OfFile(tarPath),
stat(tarballPath),
stat(tarPath),
])
const filename = basename(tarballPath)
await writeFile(tarballShaPath, `${compressedSha256} ${filename}\n`, 'utf8')
await rm(tarPath, { force: true })
const result: BuildResult = {
name: options.agent.name,
publishAs,
image: options.agent.image,
version: options.agent.version,
arch: options.arch,
sourceOciDigest: inspection.sourceOciDigest,
imageId: inspection.imageId,
smokeFingerprint: inspection.smokeFingerprint,
filename,
tarballPath,
tarballShaPath,
compressedSha256,
compressedSizeBytes: compressedStats.size,
uncompressedSha256,
uncompressedSizeBytes: uncompressedStats.size,
podmanVersion: podmanVersionValue,
builtAt,
builtBy,
gitSha: gitShaValue,
gitDirty: gitDirtyValue,
configSha256,
}
await writeFile(
buildResultPath,
`${JSON.stringify(result, null, 2)}\n`,
'utf8',
)
return result
}
export async function loadBuildResult(path: string): Promise<BuildResult> {
const raw = await readFile(path, 'utf8')
const result = JSON.parse(raw) as BuildResult
const resultDir = dirname(path)
const tarballPath = (await pathExists(result.tarballPath))
? result.tarballPath
: join(resultDir, result.filename)
const tarballShaPath = (await pathExists(result.tarballShaPath))
? result.tarballShaPath
: `${tarballPath}.sha256`
return {
...result,
tarballPath,
tarballShaPath,
}
}
async function pathExists(path: string): Promise<boolean> {
try {
await access(path)
return true
} catch {
return false
}
}

View File

@@ -0,0 +1,94 @@
import { readFile } from 'node:fs/promises'
import { z } from 'zod'
import { ARCHES, type ContainerArch } from './schema/arch'
export const agentEntrySchema = z.object({
name: z.string().regex(/^[a-z0-9-]+$/),
image: z.string().min(1),
version: z.string().min(1),
arches: z.array(z.enum(ARCHES)).min(1),
publishAs: z
.string()
.regex(/^[a-z0-9-]+$/)
.optional(),
requires_auth: z
.object({
secret: z.string().min(1),
username: z.string().min(1).optional(),
})
.optional(),
})
export const agentsConfigSchema = z
.object({
schema: z.literal('v1'),
agents: z.array(agentEntrySchema).min(1),
})
.superRefine((config, ctx) => {
const seen = new Set<string>()
for (const [index, agent] of config.agents.entries()) {
if (seen.has(agent.name)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ['agents', index, 'name'],
message: `duplicate agent name: ${agent.name}`,
})
}
seen.add(agent.name)
}
})
export type AgentEntry = z.infer<typeof agentEntrySchema>
export type AgentsConfig = z.infer<typeof agentsConfigSchema>
export interface MatrixEntry {
agent: string
image: string
version: string
arch: ContainerArch
publishAs: string
}
export function publishNameForAgent(agent: AgentEntry): string {
return agent.publishAs ?? agent.name
}
export async function readAgentsConfig(path: string): Promise<AgentsConfig> {
const raw = await readFile(path, 'utf8')
const parsed = JSON.parse(raw) as unknown
return agentsConfigSchema.parse(parsed)
}
export function expandMatrix(
config: AgentsConfig,
filter: { agent?: string } = {},
): MatrixEntry[] {
const entries: MatrixEntry[] = []
for (const agent of config.agents) {
if (filter.agent && agent.name !== filter.agent) {
continue
}
for (const arch of agent.arches) {
entries.push({
agent: agent.name,
image: agent.image,
version: agent.version,
arch,
publishAs: publishNameForAgent(agent),
})
}
}
return entries.sort((left, right) => {
const byAgent = left.agent.localeCompare(right.agent)
if (byAgent !== 0) {
return byAgent
}
return left.arch.localeCompare(right.arch)
})
}

View File

@@ -0,0 +1,39 @@
import type {
AgentArtifact,
AgentManifest,
AggregateManifest,
ContainerArch,
} from './schema'
export async function fetchAggregateManifest(): Promise<AggregateManifest> {
throw new Error('fetchAggregateManifest: implemented in WS6')
}
export async function fetchAgentManifest(
_agent: string,
_version: string,
): Promise<AgentManifest> {
throw new Error('fetchAgentManifest: implemented in WS6')
}
export async function verifySha256(
_path: string,
_expectedSha256: string,
): Promise<void> {
throw new Error('verifySha256: implemented in WS6')
}
export async function findStagedTarball(
_name: string,
_version: string,
_arch: ContainerArch,
): Promise<string> {
throw new Error('findStagedTarball: implemented in WS6')
}
export async function loadTarball(
_artifact: AgentArtifact,
_destinationPath: string,
): Promise<void> {
throw new Error('loadTarball: implemented in WS6')
}

View File

@@ -0,0 +1,439 @@
import { createReadStream } from 'node:fs'
import { stat } from 'node:fs/promises'
import {
DeleteObjectCommand,
GetObjectCommand,
PutObjectCommand,
S3Client,
type S3ClientConfig,
} from '@aws-sdk/client-s3'
import type { BuildResult } from './build'
import { ARCHES } from './schema/arch'
import {
type AgentManifest,
type AggregateEntry,
type AggregateManifest,
agentManifestSchema,
aggregateManifestSchema,
} from './schema/manifest'
import {
keyForAggregateManifest,
keyForSha,
keyForTarball,
keyForVersionManifest,
} from './schema/r2-keys'
const CDN_BASE_URL =
process.env.R2_PUBLIC_BASE_URL ?? 'https://cdn.browseros.com'
const JSON_CONTENT_TYPE = 'application/json; charset=utf-8'
const SHA_CONTENT_TYPE = 'text/plain; charset=utf-8'
export interface PublishOptions {
buildResults: BuildResult[]
updateAggregate: boolean
bucket?: string
cdnBaseURL?: string
client?: S3Client
now?: () => Date
}
interface ResultGroup {
name: string
publishAs: string
image: string
version: string
sourceOciDigest: string
podmanVersions: string[]
gitSha: string
gitDirty: boolean
configSha256: string
builtBy: string
results: BuildResult[]
}
function requiredEnv(name: string): string {
const value = process.env[name]?.trim()
if (!value) {
throw new Error(`missing required env var: ${name}`)
}
return value
}
function createR2Client(): S3Client {
const config: S3ClientConfig = {
region: 'auto',
endpoint: `https://${requiredEnv('R2_ACCOUNT_ID')}.r2.cloudflarestorage.com`,
credentials: {
accessKeyId: requiredEnv('R2_ACCESS_KEY_ID'),
secretAccessKey: requiredEnv('R2_SECRET_ACCESS_KEY'),
},
}
return new S3Client(config)
}
function getBucket(): string {
return requiredEnv('R2_BUCKET')
}
async function uploadFile(
client: S3Client,
bucket: string,
key: string,
path: string,
contentType = 'application/gzip',
): Promise<void> {
const { size } = await stat(path)
await client.send(
new PutObjectCommand({
Bucket: bucket,
Key: key,
Body: createReadStream(path),
ContentLength: size,
ContentType: contentType,
}),
)
}
async function uploadBody(
client: S3Client,
bucket: string,
key: string,
body: string | Uint8Array,
contentType = JSON_CONTENT_TYPE,
): Promise<void> {
await client.send(
new PutObjectCommand({
Bucket: bucket,
Key: key,
Body: body,
ContentType: contentType,
}),
)
}
async function deleteObject(
client: S3Client,
bucket: string,
key: string,
): Promise<void> {
await client.send(
new DeleteObjectCommand({
Bucket: bucket,
Key: key,
}),
)
}
function keyForGroup(name: string, version: string): string {
return `${name}:${version}`
}
function compareByArch(left: BuildResult, right: BuildResult): number {
return ARCHES.indexOf(left.arch) - ARCHES.indexOf(right.arch)
}
function cdnUrl(baseUrl: string, key: string): string {
return `${baseUrl.replace(/\/+$/, '')}/${key}`
}
function createManifestForGroup(
group: ResultGroup,
builtAt: string,
cdnBaseURL: string,
): AgentManifest {
return agentManifestSchema.parse({
name: group.name,
schema: 'v1',
build: {
git_sha: group.gitSha,
git_dirty: group.gitDirty,
built_at: builtAt,
built_by: group.builtBy,
config_sha256: group.configSha256,
podman_versions: group.podmanVersions,
},
source: {
image: group.image,
version: group.version,
oci_digest: group.sourceOciDigest,
},
artifacts: [...group.results].sort(compareByArch).map((result) => {
const key = keyForTarball(
result.name,
result.version,
result.arch,
result.publishAs,
)
return {
arch: result.arch,
filename: result.filename,
format: 'oci-archive+gzip',
compressed_sha256: result.compressedSha256,
compressed_size_bytes: result.compressedSizeBytes,
uncompressed_sha256: result.uncompressedSha256,
uncompressed_size_bytes: result.uncompressedSizeBytes,
url: cdnUrl(cdnBaseURL, key),
}
}),
})
}
function mergeAggregateEntries(
existing: AggregateEntry[],
nextEntries: AggregateEntry[],
builtAt: string,
builtBy: string,
): AggregateManifest {
const merged = new Map<string, AggregateEntry>()
for (const entry of existing) {
merged.set(entry.name, entry)
}
for (const entry of nextEntries) {
merged.set(entry.name, entry)
}
return aggregateManifestSchema.parse({
schema: 'v1',
built_at: builtAt,
built_by: builtBy,
agents: [...merged.values()].sort((left, right) =>
left.name.localeCompare(right.name),
),
})
}
function buildAggregateEntries(
groups: ResultGroup[],
cdnBaseURL: string,
): AggregateEntry[] {
return groups
.map((group) => ({
name: group.name,
version: group.version,
oci_digest: group.sourceOciDigest,
manifest_url: cdnUrl(
cdnBaseURL,
keyForVersionManifest(group.name, group.version),
),
}))
.sort((left, right) => left.name.localeCompare(right.name))
}
function aggregateBuiltBy(groups: ResultGroup[]): string {
const builtByValues = [
...new Set(groups.map((group) => group.builtBy)),
].sort()
return builtByValues.join(', ')
}
function buildGroup(results: BuildResult[]): ResultGroup {
const [firstResult, ...rest] = results
if (!firstResult) {
throw new Error('cannot publish an empty build result group')
}
for (const result of rest) {
if (result.name !== firstResult.name) {
throw new Error('mixed agent names in publish group')
}
if (result.publishAs !== firstResult.publishAs) {
throw new Error('mixed publishAs values in publish group')
}
if (result.image !== firstResult.image) {
throw new Error('mixed source images in publish group')
}
if (result.version !== firstResult.version) {
throw new Error('mixed versions in publish group')
}
if (result.sourceOciDigest !== firstResult.sourceOciDigest) {
throw new Error('mixed source OCI digests in publish group')
}
if (
result.gitSha !== firstResult.gitSha ||
result.gitDirty !== firstResult.gitDirty
) {
throw new Error('mixed git metadata in publish group')
}
if (result.configSha256 !== firstResult.configSha256) {
throw new Error('mixed recipe config hashes in publish group')
}
if (result.builtBy !== firstResult.builtBy) {
throw new Error('mixed build provenance in publish group')
}
}
const podmanVersions = [
...new Set(results.map((result) => result.podmanVersion)),
].sort()
return {
name: firstResult.name,
publishAs: firstResult.publishAs,
image: firstResult.image,
version: firstResult.version,
sourceOciDigest: firstResult.sourceOciDigest,
podmanVersions,
gitSha: firstResult.gitSha,
gitDirty: firstResult.gitDirty,
configSha256: firstResult.configSha256,
builtBy: firstResult.builtBy,
results: [...results].sort(compareByArch),
}
}
function groupByAgentVersion(buildResults: BuildResult[]): ResultGroup[] {
const grouped = new Map<string, BuildResult[]>()
for (const result of buildResults) {
const key = keyForGroup(result.name, result.version)
const existing = grouped.get(key)
if (existing) {
existing.push(result)
continue
}
grouped.set(key, [result])
}
return [...grouped.values()]
.map((results) => buildGroup(results))
.sort((left, right) => left.name.localeCompare(right.name))
}
async function readBodyAsString(body: unknown): Promise<string> {
const withTransform = body as {
transformToByteArray?: () => Promise<Uint8Array>
}
if (!withTransform?.transformToByteArray) {
throw new Error('R2 response body is not readable')
}
const bytes = await withTransform.transformToByteArray()
return new TextDecoder().decode(bytes)
}
async function readExistingAggregateEntries(
client: S3Client,
bucket: string,
): Promise<AggregateEntry[]> {
try {
const response = await client.send(
new GetObjectCommand({
Bucket: bucket,
Key: keyForAggregateManifest(),
}),
)
const body = await readBodyAsString(response.Body)
const parsed = aggregateManifestSchema.parse(JSON.parse(body))
return parsed.agents
} catch (error) {
const maybeError = error as {
name?: string
$metadata?: { httpStatusCode?: number }
}
if (
maybeError?.name === 'NoSuchKey' ||
maybeError?.$metadata?.httpStatusCode === 404
) {
return []
}
throw error
}
}
async function rollbackKeys(
client: S3Client,
bucket: string,
uploadedKeys: string[],
): Promise<void> {
await Promise.allSettled(
[...uploadedKeys].reverse().map((key) => deleteObject(client, bucket, key)),
)
}
export async function publishAgents(options: PublishOptions): Promise<void> {
if (options.buildResults.length === 0) {
throw new Error('buildResults must not be empty')
}
const client = options.client ?? createR2Client()
const bucket = options.bucket ?? getBucket()
const cdnBaseURL = options.cdnBaseURL ?? CDN_BASE_URL
const now = options.now ?? (() => new Date())
const uploadedKeys: string[] = []
try {
const groups = groupByAgentVersion(options.buildResults)
const builtAt = now().toISOString()
for (const group of groups) {
for (const result of group.results) {
const tarKey = keyForTarball(
result.name,
result.version,
result.arch,
result.publishAs,
)
const shaKey = keyForSha(
result.name,
result.version,
result.arch,
result.publishAs,
)
await uploadFile(client, bucket, tarKey, result.tarballPath)
uploadedKeys.push(tarKey)
await uploadFile(
client,
bucket,
shaKey,
result.tarballShaPath,
SHA_CONTENT_TYPE,
)
uploadedKeys.push(shaKey)
}
const manifest = createManifestForGroup(group, builtAt, cdnBaseURL)
const manifestKey = keyForVersionManifest(group.name, group.version)
await uploadBody(
client,
bucket,
manifestKey,
`${JSON.stringify(manifest, null, 2)}\n`,
JSON_CONTENT_TYPE,
)
uploadedKeys.push(manifestKey)
}
if (options.updateAggregate) {
const existingEntries = await readExistingAggregateEntries(client, bucket)
const aggregate = mergeAggregateEntries(
existingEntries,
buildAggregateEntries(groups, cdnBaseURL),
builtAt,
aggregateBuiltBy(groups),
)
const aggregateKey = keyForAggregateManifest()
await uploadBody(
client,
bucket,
aggregateKey,
`${JSON.stringify(aggregate, null, 2)}\n`,
JSON_CONTENT_TYPE,
)
uploadedKeys.push(aggregateKey)
}
} catch (error) {
await rollbackKeys(client, bucket, uploadedKeys)
throw error
} finally {
if (!options.client) {
client.destroy()
}
}
}

View File

@@ -0,0 +1,11 @@
export const ARCHES = ['amd64', 'arm64'] as const
export type ContainerArch = (typeof ARCHES)[number]
export function parseArch(value: string): ContainerArch {
if (value === 'amd64' || value === 'arm64') {
return value
}
throw new Error(`invalid container arch: ${value}`)
}

View File

@@ -0,0 +1,26 @@
export type { ContainerArch } from './arch'
export { ARCHES, parseArch } from './arch'
export type {
AgentArtifact,
AgentManifest,
AggregateEntry,
AggregateManifest,
} from './manifest'
export {
agentArtifactSchema,
agentManifestSchema,
aggregateEntrySchema,
aggregateManifestSchema,
MANIFEST_SCHEMA_VERSION,
ociDigestSchema,
parseAgentManifest,
parseAggregateManifest,
sha256HexSchema,
} from './manifest'
export {
keyForAggregateManifest,
keyForSha,
keyForTarball,
keyForVersionManifest,
R2_AGENTS_PREFIX,
} from './r2-keys'

View File

@@ -0,0 +1,65 @@
import { z } from 'zod'
import { ARCHES } from './arch'
export const MANIFEST_SCHEMA_VERSION = 'v1' as const
export const sha256HexSchema = z.string().regex(/^[a-f0-9]{64}$/)
export const ociDigestSchema = z.string().regex(/^sha256:[a-f0-9]{64}$/)
export const agentArtifactSchema = z.object({
arch: z.enum(ARCHES),
filename: z.string().min(1),
format: z.literal('oci-archive+gzip'),
compressed_sha256: sha256HexSchema,
compressed_size_bytes: z.number().int().positive(),
uncompressed_sha256: sha256HexSchema,
uncompressed_size_bytes: z.number().int().positive(),
url: z.string().url(),
})
export const agentManifestSchema = z.object({
name: z.string().min(1),
schema: z.literal(MANIFEST_SCHEMA_VERSION),
build: z.object({
git_sha: z.string().min(1),
git_dirty: z.boolean(),
built_at: z.string().datetime(),
built_by: z.string().min(1),
config_sha256: sha256HexSchema,
podman_versions: z.array(z.string().min(1)).min(1),
}),
source: z.object({
image: z.string().min(1),
version: z.string().min(1),
oci_digest: ociDigestSchema,
}),
artifacts: z.array(agentArtifactSchema).min(1),
})
export const aggregateEntrySchema = z.object({
name: z.string().min(1),
version: z.string().min(1),
oci_digest: ociDigestSchema,
manifest_url: z.string().url(),
})
export const aggregateManifestSchema = z.object({
schema: z.literal(MANIFEST_SCHEMA_VERSION),
built_at: z.string().datetime(),
built_by: z.string().min(1),
agents: z.array(aggregateEntrySchema).min(1),
})
export type AgentArtifact = z.infer<typeof agentArtifactSchema>
export type AgentManifest = z.infer<typeof agentManifestSchema>
export type AggregateEntry = z.infer<typeof aggregateEntrySchema>
export type AggregateManifest = z.infer<typeof aggregateManifestSchema>
export function parseAgentManifest(raw: unknown): AgentManifest {
return agentManifestSchema.parse(raw)
}
export function parseAggregateManifest(raw: unknown): AggregateManifest {
return aggregateManifestSchema.parse(raw)
}

View File

@@ -0,0 +1,29 @@
import type { ContainerArch } from './arch'
export const R2_AGENTS_PREFIX = 'agents'
export function keyForTarball(
agent: string,
version: string,
arch: ContainerArch,
publishAs = agent,
): string {
return `${R2_AGENTS_PREFIX}/${agent}/${version}/${publishAs}-${version}-${arch}.tar.gz`
}
export function keyForSha(
agent: string,
version: string,
arch: ContainerArch,
publishAs = agent,
): string {
return `${keyForTarball(agent, version, arch, publishAs)}.sha256`
}
export function keyForVersionManifest(agent: string, version: string): string {
return `${R2_AGENTS_PREFIX}/${agent}/${version}/manifest.json`
}
export function keyForAggregateManifest(): string {
return `${R2_AGENTS_PREFIX}/manifest.json`
}

View File

@@ -0,0 +1,78 @@
import { createReadStream, createWriteStream } from 'node:fs'
import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { pipeline } from 'node:stream/promises'
import { createGunzip } from 'node:zlib'
import {
podmanInspectImage,
podmanLoadArchive,
podmanRemoveImage,
} from './build'
export interface RoundTripPodmanLoadOptions {
tarballPath: string
expectedImage: string
expectedImageId?: string
expectedSmokeFingerprint?: string
}
async function maybeDecompressTarball(tarballPath: string): Promise<{
tarPath: string
cleanupDir?: string
}> {
if (!tarballPath.endsWith('.gz')) {
return { tarPath: tarballPath }
}
const tempDir = await mkdtemp(join(tmpdir(), 'agent-container-smoke-'))
const tarPath = join(tempDir, 'image.tar')
await pipeline(
createReadStream(tarballPath),
createGunzip(),
createWriteStream(tarPath),
)
return { tarPath, cleanupDir: tempDir }
}
export async function roundTripPodmanLoad(
options: RoundTripPodmanLoadOptions,
): Promise<void> {
if (!options.expectedImageId && !options.expectedSmokeFingerprint) {
throw new Error(
'expectedImageId or expectedSmokeFingerprint is required for smoke verification',
)
}
const decompressed = await maybeDecompressTarball(options.tarballPath)
try {
await podmanRemoveImage(options.expectedImage).catch(() => {})
await podmanLoadArchive(decompressed.tarPath)
const inspection = await podmanInspectImage(options.expectedImage)
if (
options.expectedSmokeFingerprint &&
inspection.smokeFingerprint !== options.expectedSmokeFingerprint
) {
throw new Error(
`loaded image fingerprint mismatch: expected ${options.expectedSmokeFingerprint}, got ${inspection.smokeFingerprint}`,
)
}
if (
options.expectedImageId &&
inspection.imageId !== options.expectedImageId
) {
throw new Error(
`loaded image ID mismatch: expected ${options.expectedImageId}, got ${inspection.imageId}`,
)
}
} finally {
await podmanRemoveImage(options.expectedImage).catch(() => {})
if (decompressed.cleanupDir) {
await rm(decompressed.cleanupDir, { recursive: true, force: true })
}
}
}

View File

@@ -0,0 +1,18 @@
import { describe, expect, it } from 'bun:test'
import { ARCHES, parseArch } from '../src/schema/arch'
describe('schema/arch', () => {
it('exports the supported arches', () => {
expect(ARCHES).toEqual(['amd64', 'arm64'])
})
it('parses valid arches', () => {
expect(parseArch('amd64')).toBe('amd64')
expect(parseArch('arm64')).toBe('arm64')
})
it('rejects invalid arches', () => {
expect(() => parseArch('x64')).toThrow('invalid container arch')
})
})

View File

@@ -0,0 +1,112 @@
import { afterEach, describe, expect, it } from 'bun:test'
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { expandMatrix, readAgentsConfig } from '../src/catalog'
const tempPaths: string[] = []
async function writeTempConfig(contents: unknown): Promise<string> {
const dir = await mkdtemp(join(tmpdir(), 'agent-container-catalog-'))
const filePath = join(dir, 'agents.json')
tempPaths.push(dir)
await writeFile(filePath, `${JSON.stringify(contents, null, 2)}\n`, 'utf8')
return filePath
}
afterEach(async () => {
await Promise.all(
tempPaths
.splice(0)
.map((path) => rm(path, { recursive: true, force: true })),
)
})
describe('catalog', () => {
it('reads and expands the agent matrix', async () => {
const path = await writeTempConfig({
schema: 'v1',
agents: [
{
name: 'openclaw',
image: 'ghcr.io/openclaw/openclaw',
version: '2026.4.12',
arches: ['amd64', 'arm64'],
},
],
})
const config = await readAgentsConfig(path)
expect(expandMatrix(config)).toEqual([
{
agent: 'openclaw',
image: 'ghcr.io/openclaw/openclaw',
version: '2026.4.12',
arch: 'amd64',
publishAs: 'openclaw',
},
{
agent: 'openclaw',
image: 'ghcr.io/openclaw/openclaw',
version: '2026.4.12',
arch: 'arm64',
publishAs: 'openclaw',
},
])
})
it('filters the matrix by agent name', async () => {
const path = await writeTempConfig({
schema: 'v1',
agents: [
{
name: 'openclaw',
image: 'ghcr.io/openclaw/openclaw',
version: '2026.4.12',
arches: ['amd64'],
},
{
name: 'claude-code',
image: 'ghcr.io/example/claude-code',
version: '1.2.3',
arches: ['arm64'],
publishAs: 'claude',
},
],
})
const config = await readAgentsConfig(path)
expect(expandMatrix(config, { agent: 'claude-code' })).toEqual([
{
agent: 'claude-code',
image: 'ghcr.io/example/claude-code',
version: '1.2.3',
arch: 'arm64',
publishAs: 'claude',
},
])
})
it('rejects duplicate agent names', async () => {
const path = await writeTempConfig({
schema: 'v1',
agents: [
{
name: 'openclaw',
image: 'ghcr.io/openclaw/openclaw',
version: '2026.4.12',
arches: ['amd64'],
},
{
name: 'openclaw',
image: 'ghcr.io/example/openclaw',
version: '2026.4.13',
arches: ['arm64'],
},
],
})
await expect(readAgentsConfig(path)).rejects.toThrow('duplicate agent name')
})
})

View File

@@ -0,0 +1,47 @@
import { describe, expect, it } from 'bun:test'
import { readFile } from 'node:fs/promises'
import { resolve } from 'node:path'
import { readAgentsConfig } from '../src/catalog'
const packageRoot = resolve(import.meta.dir, '..')
const recipePath = resolve(packageRoot, 'recipe', 'agents.json')
const runtimePath = resolve(
import.meta.dir,
'..',
'..',
'..',
'apps',
'server',
'src',
'api',
'services',
'openclaw',
'openclaw-service.ts',
)
describe('OpenClaw drift guard', () => {
it('keeps recipe/agents.json in sync with the runtime image pin', async () => {
const [config, runtimeSource] = await Promise.all([
readAgentsConfig(recipePath),
readFile(runtimePath, 'utf8'),
])
const openclaw = config.agents.find((agent) => agent.name === 'openclaw')
expect(openclaw).toBeDefined()
const match = runtimeSource.match(
/return process\.env\.OPENCLAW_IMAGE \|\| ['"]([^'"]+)['"]/,
)
if (!match?.[1]) {
throw new Error(
`failed to extract OpenClaw image fallback from ${runtimePath}`,
)
}
const recipeImage = `${openclaw?.image}:${openclaw?.version}`
expect(recipeImage).toBe(match[1], {
message: `OpenClaw image drifted between ${recipePath} and ${runtimePath}`,
})
})
})

View File

@@ -0,0 +1,223 @@
import { afterEach, describe, expect, it, mock, spyOn } from 'bun:test'
import { existsSync } from 'node:fs'
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import {
buildTarball,
podmanInspectImage,
registryForImage,
} from '../src/build'
const tempDirs: string[] = []
function processResult(
stdout: string,
stderr = '',
exitCode = 0,
): Bun.Subprocess {
return {
stdout: new Response(stdout).body,
stderr: new Response(stderr).body,
exited: Promise.resolve(exitCode),
} as Bun.Subprocess
}
async function createTempDir(): Promise<string> {
const dir = await mkdtemp(join(tmpdir(), 'agent-container-build-'))
tempDirs.push(dir)
return dir
}
afterEach(async () => {
mock.restore()
await Promise.all(
tempDirs
.splice(0)
.map((path) => rm(path, { recursive: true, force: true })),
)
})
describe('build', () => {
it('resolves registry hosts correctly', () => {
expect(registryForImage('ghcr.io/openclaw/openclaw')).toBe('ghcr.io')
expect(registryForImage('localhost:5000/example/image')).toBe(
'localhost:5000',
)
expect(registryForImage('busybox')).toBe('docker.io')
})
it('builds a tarball result and writes the sidecar files', async () => {
const dir = await createTempDir()
const outputDir = join(dir, 'dist')
const recipePath = join(dir, 'agents.json')
await writeFile(
recipePath,
JSON.stringify({
schema: 'v1',
agents: [
{
name: 'openclaw',
image: 'ghcr.io/openclaw/openclaw',
version: '2026.4.12',
arches: ['arm64'],
},
],
}),
'utf8',
)
const originalSpawn = Bun.spawn
const podmanCommands: string[][] = []
spyOn(Bun, 'spawn').mockImplementation((command, options) => {
if (Array.isArray(command) && command[0] === 'podman') {
podmanCommands.push(command)
if (command[1] === '--version') {
return processResult('podman version 5.8.1\n')
}
if (command[1] === 'pull') {
return processResult('')
}
if (command[1] === 'inspect') {
return processResult(
JSON.stringify({
Id: 'f'.repeat(64),
Digest: `sha256:${'1'.repeat(64)}`,
RepoDigests: [
`ghcr.io/openclaw/openclaw@sha256:${'2'.repeat(64)}`,
`ghcr.io/openclaw/openclaw@sha256:${'1'.repeat(64)}`,
],
Architecture: 'arm64',
Os: 'linux',
Config: {
Entrypoint: ['/entrypoint.sh'],
Env: ['NODE_ENV=production'],
},
RootFS: {
Type: 'layers',
Layers: ['sha256:abc'],
},
}),
)
}
if (command[1] === 'save') {
const outPath = String(command[5])
void writeFile(outPath, 'oci archive payload', 'utf8')
return processResult('')
}
}
return originalSpawn(
command as string[],
options as SpawnOptions.OptionsObject<string[]>,
)
})
const result = await buildTarball({
agent: {
name: 'openclaw',
image: 'ghcr.io/openclaw/openclaw',
version: '2026.4.12',
arches: ['arm64'],
},
arch: 'arm64',
outputDir,
recipePath,
builtBy: 'test-run',
})
expect(result.filename).toBe('openclaw-2026.4.12-arm64.tar.gz')
expect(result.sourceOciDigest).toBe(`sha256:${'2'.repeat(64)}`)
expect(result.imageId).toBe(`sha256:${'f'.repeat(64)}`)
expect(result.smokeFingerprint).toHaveLength(64)
expect(existsSync(result.tarballPath)).toBe(true)
expect(existsSync(result.tarballShaPath)).toBe(true)
expect(existsSync(join(outputDir, 'openclaw-2026.4.12-arm64.tar'))).toBe(
false,
)
expect(existsSync(join(outputDir, 'build-result.json'))).toBe(true)
expect(
podmanCommands.some(
(command) =>
command[1] === 'pull' &&
command.includes('--arch') &&
command.includes('arm64'),
),
).toBe(true)
})
it('prefers the repeated non-platform repo digest as the source OCI digest', async () => {
const originalSpawn = Bun.spawn
spyOn(Bun, 'spawn').mockImplementation((command, options) => {
if (
Array.isArray(command) &&
command[0] === 'podman' &&
command[1] === 'inspect'
) {
return processResult(
JSON.stringify({
Id: 'f'.repeat(64),
Digest: `sha256:${'1'.repeat(64)}`,
RepoDigests: [
`ghcr.io/openclaw/openclaw@sha256:${'2'.repeat(64)}`,
`mirror.example/openclaw/openclaw@sha256:${'2'.repeat(64)}`,
`docker.io/openclaw/openclaw@sha256:${'1'.repeat(64)}`,
],
Architecture: 'arm64',
Os: 'linux',
Config: {},
RootFS: {},
}),
)
}
return originalSpawn(
command as string[],
options as SpawnOptions.OptionsObject<string[]>,
)
})
const inspection = await podmanInspectImage(
'ghcr.io/openclaw/openclaw:2026.4.12',
)
expect(inspection.sourceOciDigest).toBe(`sha256:${'2'.repeat(64)}`)
})
it('fails when repo digests disagree without a clear winner', async () => {
const originalSpawn = Bun.spawn
spyOn(Bun, 'spawn').mockImplementation((command, options) => {
if (
Array.isArray(command) &&
command[0] === 'podman' &&
command[1] === 'inspect'
) {
return processResult(
JSON.stringify({
Id: 'f'.repeat(64),
Digest: `sha256:${'1'.repeat(64)}`,
RepoDigests: [
`ghcr.io/openclaw/openclaw@sha256:${'2'.repeat(64)}`,
`mirror.example/openclaw/openclaw@sha256:${'3'.repeat(64)}`,
`docker.io/openclaw/openclaw@sha256:${'1'.repeat(64)}`,
],
Architecture: 'arm64',
Os: 'linux',
Config: {},
RootFS: {},
}),
)
}
return originalSpawn(
command as string[],
options as SpawnOptions.OptionsObject<string[]>,
)
})
await expect(
podmanInspectImage('ghcr.io/openclaw/openclaw:2026.4.12'),
).rejects.toThrow('ambiguous source OCI digest')
})
})

View File

@@ -0,0 +1,365 @@
import { afterEach, describe, expect, it } from 'bun:test'
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import {
DeleteObjectCommand,
GetObjectCommand,
PutObjectCommand,
type S3Client,
} from '@aws-sdk/client-s3'
import type { BuildResult } from '../src/build'
import { publishAgents } from '../src/publish'
const tempDirs: string[] = []
function sha(char: string): string {
return char.repeat(64)
}
async function createBuildResult(
root: string,
arch: 'amd64' | 'arm64',
overrides: Partial<BuildResult> = {},
): Promise<BuildResult> {
const dir = join(root, arch)
await mkdir(dir, { recursive: true })
const tarballPath = join(dir, `openclaw-2026.4.12-${arch}.tar.gz`)
const tarballShaPath = `${tarballPath}.sha256`
await writeFile(tarballPath, `${arch}-tarball`, 'utf8')
await writeFile(
tarballShaPath,
`${sha(arch === 'amd64' ? 'a' : 'b')} file\n`,
'utf8',
)
return {
name: 'openclaw',
publishAs: 'openclaw',
image: 'ghcr.io/openclaw/openclaw',
version: '2026.4.12',
arch,
sourceOciDigest: `sha256:${sha('c')}`,
imageId: `sha256:${sha(arch === 'amd64' ? 'd' : 'e')}`,
smokeFingerprint: sha(arch === 'amd64' ? '6' : '7'),
filename: `openclaw-2026.4.12-${arch}.tar.gz`,
tarballPath,
tarballShaPath,
compressedSha256: sha(arch === 'amd64' ? '1' : '2'),
compressedSizeBytes: 100,
uncompressedSha256: sha(arch === 'amd64' ? '3' : '4'),
uncompressedSizeBytes: 200,
podmanVersion: 'podman version 5.8.1',
builtAt: '2026-04-22T17:30:00.000Z',
builtBy: 'workflow@refs/heads/dev',
gitSha: 'abc123',
gitDirty: false,
configSha256: sha('5'),
...overrides,
}
}
async function createTempDir(): Promise<string> {
const dir = await mkdtemp(join(tmpdir(), 'agent-container-publish-'))
tempDirs.push(dir)
return dir
}
afterEach(async () => {
await Promise.all(
tempDirs
.splice(0)
.map((path) => rm(path, { recursive: true, force: true })),
)
})
describe('publish', () => {
it('uploads version manifests and updates aggregate last', async () => {
const root = await createTempDir()
const buildResults = await Promise.all([
createBuildResult(root, 'amd64'),
createBuildResult(root, 'arm64'),
])
const puts: Array<{ key: string; body: unknown }> = []
const client = {
send: async (command: unknown) => {
if (command instanceof GetObjectCommand) {
throw { name: 'NoSuchKey', $metadata: { httpStatusCode: 404 } }
}
if (command instanceof PutObjectCommand) {
puts.push({
key: String(command.input.Key),
body: command.input.Body,
})
return {}
}
if (command instanceof DeleteObjectCommand) {
return {}
}
throw new Error('unexpected command')
},
destroy: () => {},
} as unknown as S3Client
await publishAgents({
buildResults,
updateAggregate: true,
bucket: 'test-bucket',
cdnBaseURL: 'https://cdn.example.com',
client,
now: () => new Date('2026-04-22T18:00:00.000Z'),
})
expect(puts.map((entry) => entry.key)).toEqual([
'agents/openclaw/2026.4.12/openclaw-2026.4.12-amd64.tar.gz',
'agents/openclaw/2026.4.12/openclaw-2026.4.12-amd64.tar.gz.sha256',
'agents/openclaw/2026.4.12/openclaw-2026.4.12-arm64.tar.gz',
'agents/openclaw/2026.4.12/openclaw-2026.4.12-arm64.tar.gz.sha256',
'agents/openclaw/2026.4.12/manifest.json',
'agents/manifest.json',
])
const versionManifest = JSON.parse(
String(puts.find((entry) => entry.key.endsWith('/manifest.json'))?.body),
)
expect(versionManifest.source.oci_digest).toBe(`sha256:${sha('c')}`)
expect(versionManifest.artifacts[0].url).toBe(
'https://cdn.example.com/agents/openclaw/2026.4.12/openclaw-2026.4.12-amd64.tar.gz',
)
const aggregateManifest = JSON.parse(String(puts.at(-1)?.body))
expect(aggregateManifest.agents).toEqual([
{
name: 'openclaw',
version: '2026.4.12',
oci_digest: `sha256:${sha('c')}`,
manifest_url:
'https://cdn.example.com/agents/openclaw/2026.4.12/manifest.json',
},
])
})
it('rolls back uploaded keys when a later upload fails', async () => {
const root = await createTempDir()
const buildResults = [await createBuildResult(root, 'amd64')]
const deleted: string[] = []
const client = {
send: async (command: unknown) => {
if (command instanceof GetObjectCommand) {
throw { name: 'NoSuchKey', $metadata: { httpStatusCode: 404 } }
}
if (command instanceof PutObjectCommand) {
if (String(command.input.Key).endsWith('/manifest.json')) {
throw new Error('manifest upload failed')
}
return {}
}
if (command instanceof DeleteObjectCommand) {
deleted.push(String(command.input.Key))
return {}
}
throw new Error('unexpected command')
},
destroy: () => {},
} as unknown as S3Client
await expect(
publishAgents({
buildResults,
updateAggregate: true,
bucket: 'test-bucket',
client,
}),
).rejects.toThrow('manifest upload failed')
expect(deleted).toEqual([
'agents/openclaw/2026.4.12/openclaw-2026.4.12-amd64.tar.gz.sha256',
'agents/openclaw/2026.4.12/openclaw-2026.4.12-amd64.tar.gz',
])
})
it('merges new entries into an existing aggregate manifest', async () => {
const root = await createTempDir()
const buildResults = [await createBuildResult(root, 'amd64')]
const puts: Array<{ key: string; body: unknown }> = []
const client = {
send: async (command: unknown) => {
if (command instanceof GetObjectCommand) {
return {
Body: {
transformToByteArray: async () =>
new TextEncoder().encode(
JSON.stringify({
schema: 'v1',
built_at: '2026-04-21T00:00:00.000Z',
built_by: 'previous',
agents: [
{
name: 'claude-code',
version: '1.0.0',
oci_digest: `sha256:${sha('9')}`,
manifest_url:
'https://cdn.example.com/agents/claude-code/1.0.0/manifest.json',
},
{
name: 'openclaw',
version: '2026.4.11',
oci_digest: `sha256:${sha('8')}`,
manifest_url:
'https://cdn.example.com/agents/openclaw/2026.4.11/manifest.json',
},
],
}),
),
},
}
}
if (command instanceof PutObjectCommand) {
puts.push({
key: String(command.input.Key),
body: command.input.Body,
})
return {}
}
if (command instanceof DeleteObjectCommand) {
return {}
}
throw new Error('unexpected command')
},
destroy: () => {},
} as unknown as S3Client
await publishAgents({
buildResults,
updateAggregate: true,
bucket: 'test-bucket',
cdnBaseURL: 'https://cdn.example.com',
client,
now: () => new Date('2026-04-22T18:00:00.000Z'),
})
const aggregateManifest = JSON.parse(String(puts.at(-1)?.body))
expect(aggregateManifest.agents).toEqual([
{
name: 'claude-code',
version: '1.0.0',
oci_digest: `sha256:${sha('9')}`,
manifest_url:
'https://cdn.example.com/agents/claude-code/1.0.0/manifest.json',
},
{
name: 'openclaw',
version: '2026.4.12',
oci_digest: `sha256:${sha('c')}`,
manifest_url:
'https://cdn.example.com/agents/openclaw/2026.4.12/manifest.json',
},
])
})
it('records distinct podman versions across arches', async () => {
const root = await createTempDir()
const buildResults = await Promise.all([
createBuildResult(root, 'amd64', {
podmanVersion: 'podman version 5.8.1',
}),
createBuildResult(root, 'arm64', {
podmanVersion: 'podman version 5.9.0',
}),
])
const puts: Array<{ key: string; body: unknown }> = []
const client = {
send: async (command: unknown) => {
if (command instanceof GetObjectCommand) {
throw { name: 'NoSuchKey', $metadata: { httpStatusCode: 404 } }
}
if (command instanceof PutObjectCommand) {
puts.push({
key: String(command.input.Key),
body: command.input.Body,
})
return {}
}
if (command instanceof DeleteObjectCommand) {
return {}
}
throw new Error('unexpected command')
},
destroy: () => {},
} as unknown as S3Client
await publishAgents({
buildResults,
updateAggregate: false,
bucket: 'test-bucket',
client,
})
const versionManifest = JSON.parse(
String(puts.find((entry) => entry.key.endsWith('/manifest.json'))?.body),
)
expect(versionManifest.build.podman_versions).toEqual([
'podman version 5.8.1',
'podman version 5.9.0',
])
})
it('records all build provenance values in the aggregate manifest', async () => {
const root = await createTempDir()
const buildResults = [
await createBuildResult(root, 'amd64', {
name: 'claude-code',
publishAs: 'claude-code',
image: 'ghcr.io/example/claude-code',
version: '1.0.0',
builtBy: 'workflow-a@refs/heads/dev',
}),
await createBuildResult(root, 'arm64', {
name: 'openclaw',
publishAs: 'openclaw',
image: 'ghcr.io/openclaw/openclaw',
version: '2026.4.12',
builtBy: 'workflow-b@refs/heads/dev',
}),
]
const puts: Array<{ key: string; body: unknown }> = []
const client = {
send: async (command: unknown) => {
if (command instanceof GetObjectCommand) {
throw { name: 'NoSuchKey', $metadata: { httpStatusCode: 404 } }
}
if (command instanceof PutObjectCommand) {
puts.push({
key: String(command.input.Key),
body: command.input.Body,
})
return {}
}
if (command instanceof DeleteObjectCommand) {
return {}
}
throw new Error('unexpected command')
},
destroy: () => {},
} as unknown as S3Client
await publishAgents({
buildResults,
updateAggregate: true,
bucket: 'test-bucket',
client,
now: () => new Date('2026-04-22T18:00:00.000Z'),
})
const aggregateManifest = JSON.parse(String(puts.at(-1)?.body))
expect(aggregateManifest.built_by).toBe(
'workflow-a@refs/heads/dev, workflow-b@refs/heads/dev',
)
})
})

View File

@@ -0,0 +1,32 @@
import { describe, expect, it } from 'bun:test'
import {
keyForAggregateManifest,
keyForSha,
keyForTarball,
keyForVersionManifest,
} from '../src/schema/r2-keys'
describe('schema/r2-keys', () => {
it('builds tarball keys', () => {
expect(keyForTarball('openclaw', '2026.4.12', 'amd64')).toBe(
'agents/openclaw/2026.4.12/openclaw-2026.4.12-amd64.tar.gz',
)
})
it('supports a custom publishAs filename prefix', () => {
expect(keyForTarball('claude-code', '1.2.3', 'arm64', 'claude')).toBe(
'agents/claude-code/1.2.3/claude-1.2.3-arm64.tar.gz',
)
expect(keyForSha('claude-code', '1.2.3', 'arm64', 'claude')).toBe(
'agents/claude-code/1.2.3/claude-1.2.3-arm64.tar.gz.sha256',
)
})
it('builds manifest keys', () => {
expect(keyForVersionManifest('openclaw', '2026.4.12')).toBe(
'agents/openclaw/2026.4.12/manifest.json',
)
expect(keyForAggregateManifest()).toBe('agents/manifest.json')
})
})

View File

@@ -0,0 +1,100 @@
import { describe, expect, it } from 'bun:test'
import {
parseAgentManifest,
parseAggregateManifest,
} from '../src/schema/manifest'
function hex(char: string): string {
return char.repeat(64)
}
describe('schema/manifest', () => {
it('parses a valid agent manifest', () => {
const manifest = parseAgentManifest({
name: 'openclaw',
schema: 'v1',
build: {
git_sha: 'abc123',
git_dirty: false,
built_at: '2026-04-22T17:30:00.000Z',
built_by: 'workflow@refs/heads/dev',
config_sha256: hex('0'),
podman_versions: ['podman version 5.8.1'],
},
source: {
image: 'ghcr.io/openclaw/openclaw',
version: '2026.4.12',
oci_digest: `sha256:${hex('1')}`,
},
artifacts: [
{
arch: 'amd64',
filename: 'openclaw-2026.4.12-amd64.tar.gz',
format: 'oci-archive+gzip',
compressed_sha256: hex('2'),
compressed_size_bytes: 123,
uncompressed_sha256: hex('3'),
uncompressed_size_bytes: 456,
url: 'https://cdn.browseros.com/agents/openclaw/2026.4.12/openclaw-2026.4.12-amd64.tar.gz',
},
],
})
expect(manifest.source.version).toBe('2026.4.12')
expect(manifest.artifacts).toHaveLength(1)
})
it('rejects invalid artifact hashes', () => {
expect(() =>
parseAgentManifest({
name: 'openclaw',
schema: 'v1',
build: {
git_sha: 'abc123',
git_dirty: false,
built_at: '2026-04-22T17:30:00.000Z',
built_by: 'workflow@refs/heads/dev',
config_sha256: hex('0'),
podman_versions: ['podman version 5.8.1'],
},
source: {
image: 'ghcr.io/openclaw/openclaw',
version: '2026.4.12',
oci_digest: `sha256:${hex('1')}`,
},
artifacts: [
{
arch: 'amd64',
filename: 'openclaw-2026.4.12-amd64.tar.gz',
format: 'oci-archive+gzip',
compressed_sha256: 'bad',
compressed_size_bytes: 123,
uncompressed_sha256: hex('3'),
uncompressed_size_bytes: 456,
url: 'https://cdn.browseros.com/agents/openclaw/2026.4.12/openclaw-2026.4.12-amd64.tar.gz',
},
],
}),
).toThrow()
})
it('parses a valid aggregate manifest', () => {
const manifest = parseAggregateManifest({
schema: 'v1',
built_at: '2026-04-22T17:30:00.000Z',
built_by: 'workflow@refs/heads/dev',
agents: [
{
name: 'openclaw',
version: '2026.4.12',
oci_digest: `sha256:${hex('4')}`,
manifest_url:
'https://cdn.browseros.com/agents/openclaw/2026.4.12/manifest.json',
},
],
})
expect(manifest.agents[0]?.name).toBe('openclaw')
})
})

View File

@@ -0,0 +1,7 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "src"
},
"include": ["src/**/*"]
}