Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,20 @@

## 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;
- 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.

Expand Down
8 changes: 4 additions & 4 deletions DOCS-INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
20 changes: 15 additions & 5 deletions docs/delivery.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

Expand Down Expand Up @@ -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 <downloaded-archive.tgz>` 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.
6 changes: 3 additions & 3 deletions docs/public-safety.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
3 changes: 3 additions & 0 deletions docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ pnpm audit:dependencies
`pnpm check` runs formatting, lint, type checking, the complete test suite, documentation validation,
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
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
29 changes: 26 additions & 3 deletions scripts/check-package.mjs
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -8,15 +9,35 @@ 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'),
)
const read = (path) => execFileSync('tar', ['-xOzf', archive, path], { encoding: 'utf8' })
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,
Expand All @@ -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)) {
Expand Down Expand Up @@ -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 })
Expand Down
59 changes: 57 additions & 2 deletions scripts/package-smoke.mjs
Original file line number Diff line number Diff line change
@@ -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)
Expand Down Expand Up @@ -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',
)
2 changes: 1 addition & 1 deletion spec/openapi.yaml
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/providers/local.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export class LocalSandboxProvider implements SandboxProvider {
async describe(_context: ProviderContext): Promise<ProviderManifest> {
return {
name: 'local',
version: '0.1.1',
version: '0.1.2',
runtimeClass: 'local-process-unsafe',
capabilities: {
commandExecution: true,
Expand Down
2 changes: 1 addition & 1 deletion src/providers/mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export class MockSandboxProvider implements SandboxProvider {
constructor(capabilities: Partial<RuntimeCapabilities> = {}) {
this.#manifest = {
name: 'mock',
version: '0.1.1',
version: '0.1.2',
runtimeClass: 'mock-process',
capabilities: allCapabilities(capabilities),
}
Expand Down
9 changes: 5 additions & 4 deletions tests/commit-identity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -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()
Expand All @@ -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')
Expand All @@ -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)
})
Loading
Loading