From 6b9de08c8540f67f358d672f0b85e1289937b376 Mon Sep 17 00:00:00 2001 From: KevinTen <22876610+kevinten10@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:32:08 +0800 Subject: [PATCH 1/4] release: prepare v0.1.2 with installed archive security verification --- CHANGELOG.md | 6 ++++ DOCS-INDEX.md | 8 +++--- README.md | 2 +- README.zh-CN.md | 2 +- SECURITY.md | 4 +-- docs/delivery.md | 20 +++++++++---- docs/public-safety.md | 6 ++-- package.json | 2 +- scripts/check-package.mjs | 29 +++++++++++++++++-- scripts/package-smoke.mjs | 59 +++++++++++++++++++++++++++++++++++++-- spec/openapi.yaml | 2 +- src/providers/local.ts | 2 +- src/providers/mock.ts | 2 +- 13 files changed, 119 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4107ec2..a38de64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,17 @@ ## Unreleased +No changes queued. + +## 0.1.2 - 2026-10-14 + - replace Provider exception details and stored failure messages with fixed public messages, preserve legal portable error codes, and normalize arbitrary failure codes; - suppress Provider exception details in conformance reports without losing runner deadline evidence; - reject embedded credentials in Provider endpoint URLs; - add synthetic HTTP/SDK/resource-read regressions and a value-withholding commit-identity gate; +- verify exact supplied release archives, diagram integrity and Provider redaction in the installed + distribution, including downloaded release assets; - document the difference between content scans, commit-identity checks, explicit legacy baselines, and authorized history remediation. The existing `v0.1.1` archive remains unchanged. diff --git a/DOCS-INDEX.md b/DOCS-INDEX.md index 46e22da..e3b5ee3 100644 --- a/DOCS-INDEX.md +++ b/DOCS-INDEX.md @@ -4,19 +4,19 @@ authority: canonical status: canonical title: Sandbox Runtime API documentation index genre: spec -last_verified: 2026-10-13 +last_verified: 2026-10-14 --- # Documentation Index ## Active context -The `v0.1.1` delivery implements the `0.1` contract. It owns portable lifecycle, capability negotiation, +The `v0.1.2` delivery implements the `0.1` contract. It owns portable lifecycle, capability negotiation, command and file operations, event replay, the TypeScript SDK, the loopback-only reference server, the unsafe Local Provider, and provider conformance. -Post-`v0.1.1` security fixes are tracked under [Unreleased](CHANGELOG.md). The development source's -Provider data-minimization behavior is not proof that an older release archive contains the fix. +The [v0.1.2 changelog](CHANGELOG.md) records Provider data-minimization fixes. Older release archives +do not contain these fixes; use the current [release guide](docs/delivery.md). Real cloud providers, production multi-tenancy, strong isolation, durable storage, authentication, network policy enforcement, snapshots, PTY, and port forwarding remain outside the completed MVP. diff --git a/README.md b/README.md index 6b74a91..0e23452 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ portable sandbox semantics. ## v0.1 Features -Version `0.1.1` builds on the original `0.1.0` baseline and contains: +Version `0.1.2` builds on the original `0.1.0` baseline and contains: - a normative runtime model, protocol, capability vocabulary, and OpenAPI document; - capability preflight; diff --git a/README.zh-CN.md b/README.zh-CN.md index e9f0fa5..f5fa07c 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -39,7 +39,7 @@ Harness 执行;Sandbox Runtime API 规范 Harness 或工具运行所需的隔 ## v0.1 能力 -`0.1.1` 在原始 `0.1.0` 基础上包含: +`0.1.2` 在原始 `0.1.0` 基础上包含: - 规范化 Runtime 模型、协议、能力词表与 OpenAPI; - capability preflight; diff --git a/SECURITY.md b/SECURITY.md index 3863eb7..71cb769 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,7 +4,7 @@ authority: canonical status: canonical title: Security policy and deployment boundary genre: spec -last_verified: 2026-10-13 +last_verified: 2026-10-14 --- # Security Policy @@ -39,7 +39,7 @@ directly accessed development server. ## Provider error and metadata boundary -The development source after `v0.1.1` replaces Provider exception details with fixed public messages. +Version `0.1.2` replaces Provider exception details with fixed public messages. Legal portable error codes remain available to callers; arbitrary Provider failure codes normalize to `provider_unavailable`. Failure messages stored on resources are also fixed, so later list/get and SDK calls do not reveal the original exception. Conformance reports suppress Provider exception diff --git a/docs/delivery.md b/docs/delivery.md index 3912b62..96a1e94 100644 --- a/docs/delivery.md +++ b/docs/delivery.md @@ -4,30 +4,33 @@ authority: reference status: active title: Release delivery and acceptance genre: primer -last_verified: 2026-10-09 +last_verified: 2026-10-14 --- # Release Delivery and Acceptance ## Scope -Version `0.1.1` delivers the public `0.1` contract, TypeScript SDK, reference runtime/server/CLI, +Version `0.1.2` delivers the public `0.1` contract, TypeScript SDK, reference runtime/server/CLI, Mock and unsafe Local Providers, conformance runner, documentation, diagrams, and a prebuilt archive. This is development infrastructure, not a hosted or production-isolated sandbox service. +Version `0.1.2` includes the Provider diagnostic/failure data-minimization fix; the original `0.1.1` +archive does not. Existing older tags and assets remain immutable. + The [roadmap](../ROADMAP.md) distinguishes completed work from candidate extensions. A real cloud, container, or Kubernetes Provider requires a separate public-source RFC, capability map, isolation boundary, and opt-in integration evidence. Those candidates are not unfinished parts of this release. ## Install a release archive -Download `sandbox-runtime-api-0.1.1.tgz` and `SHA256SUMS.txt` from the -[v0.1.1 GitHub release](https://github.com/capa-cloud/sandbox-runtime-api/releases/tag/v0.1.1). +Download `sandbox-runtime-api-0.1.2.tgz` and `SHA256SUMS.txt` from the +[v0.1.2 GitHub release](https://github.com/capa-cloud/sandbox-runtime-api/releases/tag/v0.1.2). Verify the archive before installing: ```bash shasum -a 256 -c SHA256SUMS.txt -npm install --ignore-scripts ./sandbox-runtime-api-0.1.1.tgz +npm install --ignore-scripts ./sandbox-runtime-api-0.1.2.tgz npx sandbox-runtime serve --host 127.0.0.1 --port 4311 ``` @@ -88,3 +91,10 @@ directory alone does not establish an active phase. Coordinate Vitest/coverage upgrades, rerun all gates after substantive changes, and preserve existing release tags. Do not lower capability or security assertions just to make tests pass. + +From a matching source checkout, `pnpm build` followed by +`node scripts/check-package.mjs ` validates an exact downloaded archive rather +than repacking. It checks package identity, safe paths, documentation links, five diagram hashes, +independent installation/types/example, CLI/SDK/SSE/cleanup, and synthetic Provider exception and +stored-failure redaction in the installed distribution. A checksum and byte-for-byte publication +read-back are separate release gates. These checks do not certify isolation or erase old Git metadata. diff --git a/docs/public-safety.md b/docs/public-safety.md index 25dcf22..a3c2ba0 100644 --- a/docs/public-safety.md +++ b/docs/public-safety.md @@ -4,7 +4,7 @@ authority: reference status: active title: Public safety and commit identity genre: how-to -last_verified: 2026-10-13 +last_verified: 2026-10-14 --- # Public Safety and Commit Identity @@ -56,7 +56,7 @@ are not authorized by a generic code-fix request. Never claim all public copies ## Runtime data boundary -Provider exceptions and failure observations use fixed messages in the post-`v0.1.1` development -source; see [SECURITY](../SECURITY.md) for release status and trusted metadata responsibilities. +Provider exceptions and failure observations use fixed messages from `v0.1.2`; +see [SECURITY](../SECURITY.md) for release status and trusted metadata responsibilities. Intentional command output/file content is not rewritten. Local processes still run with host-user permissions. Do not supply real secrets or untrusted code to development fixtures. diff --git a/package.json b/package.json index 3ec25e5..21b6760 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sandbox-runtime-api", - "version": "0.1.1", + "version": "0.1.2", "private": true, "description": "A provider-neutral runtime contract for isolated AI agent execution environments.", "license": "Apache-2.0", diff --git a/scripts/check-package.mjs b/scripts/check-package.mjs index 2e5011f..410ddd7 100644 --- a/scripts/check-package.mjs +++ b/scripts/check-package.mjs @@ -1,4 +1,5 @@ import { execFileSync } from 'node:child_process' +import { createHash } from 'node:crypto' import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { dirname, join, posix, resolve } from 'node:path' @@ -8,8 +9,9 @@ const root = resolve(dirname(fileURLToPath(import.meta.url)), '..') const temporary = await mkdtemp(join(tmpdir(), 'sandbox-runtime-pack-')) try { - const archive = join(temporary, 'package.tgz') - execFileSync('pnpm', ['pack', '--out', archive], { cwd: root, stdio: 'pipe' }) + const archive = process.argv[2] ? resolve(process.argv[2]) : join(temporary, 'package.tgz') + if (!process.argv[2]) + execFileSync('pnpm', ['pack', '--out', archive], { cwd: root, stdio: 'pipe' }) const files = new Set( execFileSync('tar', ['-tzf', archive], { encoding: 'utf8' }).trim().split('\n'), ) @@ -17,6 +19,25 @@ try { const packaged = JSON.parse(read('package/package.json')) const source = JSON.parse(await readFile(join(root, 'package.json'), 'utf8')) const missing = [] + for (const path of files) { + if (!path.startsWith('package/') || path.split('/').includes('..')) + missing.push('unsafe archive path') + if ( + /(?:^|\/)(?:\.git(?:\/|$)|\.env(?:\.|$)|\.DS_Store$|id_rsa|id_ed25519)|\.(?:pem|key|p12|keystore)$/i.test( + path, + ) + ) + missing.push('dangerous archive file') + } + const sourceRegister = await readFile(join(root, 'docs/visuals/README.md'), 'utf8') + let verifiedAssets = 0 + for (const match of sourceRegister.matchAll(/\| `([^`]+\.(?:png|svg))` \| `([0-9a-f]{64})`/g)) { + const bytes = execFileSync('tar', ['-xOzf', archive, `package/docs/assets/${match[1]}`]) + if (createHash('sha256').update(bytes).digest('hex') !== match[2]) + missing.push(`asset integrity differs: ${match[1]}`) + verifiedAssets += 1 + } + if (verifiedAssets !== 5) missing.push('incomplete visual source register') const required = [ packaged.exports['.'].import, packaged.exports['.'].types, @@ -33,6 +54,8 @@ try { if (packaged.name !== source.name || packaged.version !== source.version) { missing.push('package identity differs from source') } + if (packaged.private !== true || Object.keys(packaged.dependencies ?? {}).length !== 0) + missing.push('expected a private archive with no production dependencies') for (const path of files) { if (!path.endsWith('.md')) continue for (const match of read(path).matchAll(/\[[^\]]+\]\(([^)]+)\)/g)) { @@ -83,7 +106,7 @@ try { timeout: 20_000, }) process.stdout.write( - `package validation passed: ${files.size} entries; local links, installed types/example, SDK, CLI, SSE, and shutdown\n`, + `package validation passed: ${files.size} entries, ${verifiedAssets} asset hashes; local links, installed types/example, SDK, CLI, SSE, shutdown, and Provider redaction\n`, ) } finally { await rm(temporary, { recursive: true, force: true }) diff --git a/scripts/package-smoke.mjs b/scripts/package-smoke.mjs index dbd7c91..1e2cf0e 100644 --- a/scripts/package-smoke.mjs +++ b/scripts/package-smoke.mjs @@ -1,10 +1,17 @@ import assert from 'node:assert/strict' import { spawn, spawnSync } from 'node:child_process' +import { randomUUID } from 'node:crypto' import { once } from 'node:events' import { mkdir, readdir } from 'node:fs/promises' import { join } from 'node:path' import { createInterface } from 'node:readline' -import { SandboxRuntimeClient } from 'sandbox-runtime-api' +import { + InMemorySandboxRuntime, + MockSandboxProvider, + RuntimeError, + SandboxRuntimeClient, + startSandboxRuntimeServer, +} from 'sandbox-runtime-api' const root = join(process.cwd(), 'cli-resources') await mkdir(root) @@ -132,6 +139,54 @@ try { } if (failure) throw failure assert.deepEqual(await readdir(root), []) + +// These checks import the installed distribution, not the source implementation. +for (const scenario of ['ordinary', 'typed', 'provision']) { + const marker = `synthetic-${randomUUID()}` + class Probe extends MockSandboxProvider { + async provision(request, context) { + if (scenario === 'provision') throw new Error(marker) + return super.provision(request, context) + } + async execute() { + throw scenario === 'typed' ? new RuntimeError('not_found', marker) : new Error(marker) + } + } + const runtime = new InMemorySandboxRuntime(new Probe(), { idFactory: () => 'package-data-probe' }) + const handle = await startSandboxRuntimeServer(runtime, { port: 0 }) + try { + const signal = AbortSignal.timeout(5_000) + const sdk = new SandboxRuntimeClient(handle.baseUrl) + const resource = await sdk.create({ clientRequestId: 'package-data-create' }, signal) + if (scenario === 'provision') { + assert.deepEqual(resource.failure, { + code: 'provider_unavailable', + message: 'provider operation failed', + }) + } else { + await assert.rejects( + sdk.execute( + resource.id, + { expectedGeneration: resource.generation, argv: ['synthetic-noop'] }, + signal, + ), + { + code: scenario === 'typed' ? 'not_found' : 'provider_unavailable', + message: 'provider execute failed', + }, + ) + } + for (const value of [ + resource, + await sdk.get(resource.id, signal), + await sdk.list(signal), + await sdk.listEvents(0, resource.id, signal), + ]) + assert.equal(JSON.stringify(value).includes(marker), false) + } finally { + await handle.close() + } +} console.log( - 'installed package SDK, CLI, lifecycle, files, command, SSE, fencing, and shutdown passed', + 'installed package SDK, CLI, lifecycle, files, command, SSE, fencing, shutdown, and Provider redaction passed', ) diff --git a/spec/openapi.yaml b/spec/openapi.yaml index e67c9a6..7a91c35 100644 --- a/spec/openapi.yaml +++ b/spec/openapi.yaml @@ -1,7 +1,7 @@ openapi: 3.1.0 info: title: Sandbox Runtime API - version: 0.1.1 + version: 0.1.2 description: Provider-neutral lifecycle and data-plane contract for isolated execution environments. servers: - url: http://127.0.0.1:4311 diff --git a/src/providers/local.ts b/src/providers/local.ts index 0f9eb67..3fa5124 100644 --- a/src/providers/local.ts +++ b/src/providers/local.ts @@ -81,7 +81,7 @@ export class LocalSandboxProvider implements SandboxProvider { async describe(_context: ProviderContext): Promise { return { name: 'local', - version: '0.1.1', + version: '0.1.2', runtimeClass: 'local-process-unsafe', capabilities: { commandExecution: true, diff --git a/src/providers/mock.ts b/src/providers/mock.ts index b7989c2..4a089c1 100644 --- a/src/providers/mock.ts +++ b/src/providers/mock.ts @@ -39,7 +39,7 @@ export class MockSandboxProvider implements SandboxProvider { constructor(capabilities: Partial = {}) { this.#manifest = { name: 'mock', - version: '0.1.1', + version: '0.1.2', runtimeClass: 'mock-process', capabilities: allCapabilities(capabilities), } From abf3df42a9d6dab4574d8e80c4c07525aafceed8 Mon Sep 17 00:00:00 2001 From: KevinTen <22876610+kevinten10@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:35:07 +0800 Subject: [PATCH 2/4] test: wait for owned descendant readiness before cancellation --- CHANGELOG.md | 2 ++ tests/local-boundaries.test.ts | 11 +++++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a38de64..05024f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ No changes queued. - add synthetic HTTP/SDK/resource-read regressions and a value-withholding commit-identity gate; - verify exact supplied release archives, diagram integrity and Provider redaction in the installed distribution, including downloaded release assets; +- separate descendant fixture startup headroom from its unchanged cancellation deadline, assert + the child is alive before cancellation, and clean up owned execution after failed readiness; - document the difference between content scans, commit-identity checks, explicit legacy baselines, and authorized history remediation. The existing `v0.1.1` archive remains unchanged. diff --git a/tests/local-boundaries.test.ts b/tests/local-boundaries.test.ts index 041004c..e692187 100644 --- a/tests/local-boundaries.test.ts +++ b/tests/local-boundaries.test.ts @@ -66,19 +66,20 @@ describe.skipIf(process.platform === 'win32')('Unix Local Provider boundaries', const controller = new AbortController() const script = [ 'const {spawn}=require("node:child_process")', - 'const child=spawn(process.execPath,["-e","setTimeout(()=>{},2000)"],{stdio:"inherit"})', + 'const child=spawn(process.execPath,["-e","setTimeout(()=>{},10000)"],{stdio:"inherit"})', 'require("node:fs").writeFileSync("owned-pids.json",JSON.stringify({leader:process.pid,child:child.pid}))', 'process.exit(0)', ].join(';') const execution = provider.execute( key, - { expectedGeneration: 1, argv: [process.execPath, '-e', script], timeoutSeconds: 5 }, + { expectedGeneration: 1, argv: [process.execPath, '-e', script], timeoutSeconds: 15 }, { ...context, signal: controller.signal }, ) let ownedChild: number | undefined let completed = false try { - const deadline = Date.now() + 1_500 + // Startup headroom is separate from the unchanged 500ms cancellation assertion. + const deadline = Date.now() + 5_000 let leader: number | undefined while (Date.now() < deadline) { const content = await provider @@ -106,6 +107,7 @@ describe.skipIf(process.platform === 'win32')('Unix Local Provider boundaries', expect(leader).toBeDefined() expect(ownedChild).toBeDefined() expect(() => process.kill(leader as number, 0)).toThrow() + expect(() => process.kill(ownedChild as number, 0)).not.toThrow() await delay(20) controller.abort() await expect(bounded(execution, 500)).resolves.toMatchObject({ @@ -114,6 +116,7 @@ describe.skipIf(process.platform === 'win32')('Unix Local Provider boundaries', }) completed = true } finally { + controller.abort() // Only this fixture's known, still-live child is eligible for cleanup. if (!completed && ownedChild) { try { @@ -124,5 +127,5 @@ describe.skipIf(process.platform === 'win32')('Unix Local Provider boundaries', } await execution.catch(() => undefined) } - }) + }, 20_000) }) From 2609f8eacf2e1fc5a158a6b991fdd1270fc23849 Mon Sep 17 00:00:00 2001 From: KevinTen <22876610+kevinten10@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:36:56 +0800 Subject: [PATCH 3/4] test: bound worker concurrency for reliable release verification --- CHANGELOG.md | 1 + docs/testing.md | 1 + vitest.config.ts | 1 + 3 files changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 05024f0..10aa113 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ No changes queued. distribution, including downloaded release assets; - separate descendant fixture startup headroom from its unchanged cancellation deadline, assert the child is alive before cancellation, and clean up owned execution after failed readiness; +- cap concurrent Vitest workers at two without skipping tests or lowering coverage/cancellation gates; - document the difference between content scans, commit-identity checks, explicit legacy baselines, and authorized history remediation. The existing `v0.1.1` archive remains unchanged. diff --git a/docs/testing.md b/docs/testing.md index 511cf9e..dbc46c1 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -22,6 +22,7 @@ pnpm audit:dependencies ``` `pnpm check` runs formatting, lint, type checking, the complete test suite, documentation validation, +with at most two Vitest workers to bound concurrent CPU and fixture startup pressure, build, coverage thresholds, and actual tarball validation. The package gate checks exports, CLI, documentation, and every packaged Markdown local link, including image assets. The package gate also performs an isolated install, compiles the packaged Mock example, and runs the diff --git a/vitest.config.ts b/vitest.config.ts index feb94a9..e4d9042 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -2,6 +2,7 @@ import { defineConfig } from 'vitest/config' export default defineConfig({ test: { + maxWorkers: 2, coverage: { provider: 'v8', reporter: ['text', 'json-summary'], From e7843f4f136cff335e3815013783c23c4e70c4de Mon Sep 17 00:00:00 2001 From: KevinTen <22876610+kevinten10@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:41:08 +0800 Subject: [PATCH 4/4] test: separate git fixture setup budget from scanner deadlines --- docs/testing.md | 4 +++- tests/commit-identity.test.ts | 9 +++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/docs/testing.md b/docs/testing.md index dbc46c1..4f4ff71 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -22,9 +22,11 @@ pnpm audit:dependencies ``` `pnpm check` runs formatting, lint, type checking, the complete test suite, documentation validation, -with at most two Vitest workers to bound concurrent CPU and fixture startup pressure, build, coverage thresholds, and actual tarball validation. The package gate checks exports, CLI, documentation, and every packaged Markdown local link, including image assets. +Vitest uses at most two concurrent workers. Git-identity fixture cases allow 20 seconds for multiple +process starts; each scanner subprocess still has a five-second watchdog. Provider command limits, +cancellation deadlines and coverage thresholds are unchanged. The package gate also performs an isolated install, compiles the packaged Mock example, and runs the actual SDK/CLI through command, files, SSE, recreation, fencing, and shutdown cleanup. CI runs the same gates on Node.js 22 and 24. The final check gate audits all dependencies, including diff --git a/tests/commit-identity.test.ts b/tests/commit-identity.test.ts index ca6e2fd..86d99fb 100644 --- a/tests/commit-identity.test.ts +++ b/tests/commit-identity.test.ts @@ -51,7 +51,7 @@ describe('public commit identity gate', () => { const root = await fixture() commit(root, publicEmail, 'noreply@github.com') expect(scan(root).status).toBe(0) - }) + }, 20_000) it.each(['author', 'committer'])( 'rejects a non-noreply %s without printing values', async (field) => { @@ -67,6 +67,7 @@ describe('public commit identity gate', () => { expect(result.stderr).toContain(`${field} email`) expect(result.stderr).not.toContain(synthetic) }, + 20_000, ) it('checks every commit in an explicit range, not just its head', async () => { const root = await fixture() @@ -75,7 +76,7 @@ describe('public commit identity gate', () => { commit(root, publicEmail) expect(scan(root, ['--since', base]).status).toBe(1) expect(scan(root).status).toBe(1) - }) + }, 20_000) it('keeps a declared legacy baseline explicit without claiming history is scrubbed', async () => { const root = await fixture() const base = commit(root, 'legacy-fixture@example.test') @@ -85,12 +86,12 @@ describe('public commit identity gate', () => { expect(result.stdout).toContain('1 commits') expect(scan(root, ['--', '--since', base]).status).toBe(0) expect(scan(root).status).toBe(1) - }) + }, 20_000) it('fails closed for a missing repository or invalid baseline', async () => { const root = await fixture() expect(scan(root).status).toBe(2) commit(root, publicEmail) expect(scan(root, ['--since', 'missing-ref']).status).toBe(2) expect(scan(root, ['--unknown']).status).toBe(2) - }) + }, 20_000) })