mirror of
https://github.com/browseros-ai/BrowserOS.git
synced 2026-05-14 08:03:58 +00:00
Compare commits
6 Commits
fix/github
...
feat/aent-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b3b22ce17 | ||
|
|
19fff97a9c | ||
|
|
25c027863c | ||
|
|
8440ae09ce | ||
|
|
95f34da014 | ||
|
|
fe9913a4fe |
170
.github/workflows/build-agent-container.yml
vendored
Normal file
170
.github/workflows/build-agent-container.yml
vendored
Normal 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
|
||||
1
packages/browseros-agent/.gitignore
vendored
1
packages/browseros-agent/.gitignore
vendored
@@ -14,6 +14,7 @@ lerna-debug.log*
|
||||
# Ignore all .env files except .env.example
|
||||
**/.env.*
|
||||
!**/.env.example
|
||||
!**/.env.sample
|
||||
!**/.env.production.example
|
||||
|
||||
|
||||
|
||||
@@ -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=="],
|
||||
|
||||
@@ -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=
|
||||
52
packages/browseros-agent/packages/agent-container/README.md
Normal file
52
packages/browseros-agent/packages/agent-container/README.md
Normal 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.
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"schema": "v1",
|
||||
"agents": [
|
||||
{
|
||||
"name": "openclaw",
|
||||
"image": "ghcr.io/openclaw/openclaw",
|
||||
"version": "2026.4.12",
|
||||
"arches": ["amd64", "arm64"],
|
||||
"publishAs": "openclaw"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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))
|
||||
@@ -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 }))
|
||||
@@ -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'],
|
||||
})
|
||||
@@ -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']),
|
||||
})
|
||||
470
packages/browseros-agent/packages/agent-container/src/build.ts
Normal file
470
packages/browseros-agent/packages/agent-container/src/build.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -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')
|
||||
}
|
||||
439
packages/browseros-agent/packages/agent-container/src/publish.ts
Normal file
439
packages/browseros-agent/packages/agent-container/src/publish.ts
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}`)
|
||||
}
|
||||
@@ -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'
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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`
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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}`,
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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',
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
Reference in New Issue
Block a user