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
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@

## Unreleased

No changes queued.
- 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;
- document the difference between content scans, commit-identity checks, explicit legacy baselines,
and authorized history remediation. The existing `v0.1.1` archive remains unchanged.

## 0.1.1 - 2026-10-09

Expand Down
5 changes: 5 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,8 @@ an independent design rationale. Do not include proprietary code, private infras
credentials, user data, or unpublished behavior.

Contributions are accepted under the Apache License 2.0.

Before pushing and after merging, review [public-safety guidance](docs/public-safety.md) and run the
commit-identity gate against the declared candidate range. Configure an approved public name and
GitHub noreply email. For squash merges, pass an explicit approved author email and verify the actual
merged commit; local Git configuration alone does not control GitHub merge authorship.
6 changes: 5 additions & 1 deletion DOCS-INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ authority: canonical
status: canonical
title: Sandbox Runtime API documentation index
genre: spec
last_verified: 2026-10-09
last_verified: 2026-10-13
---

# Documentation Index
Expand All @@ -15,6 +15,9 @@ The `v0.1.1` delivery implements the `0.1` contract. It owns portable lifecycle,
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.

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 All @@ -37,6 +40,7 @@ network policy enforcement, snapshots, PTY, and port forwarding remain outside t
- [Testing and conformance](docs/testing.md)
- [Release archive and acceptance map](docs/delivery.md)
- [Clean-room policy](docs/clean-room-policy.md)
- [Public safety and commit identity](docs/public-safety.md)
- [Visual source and validation register](docs/visuals/README.md)

## Process and release evidence
Expand Down
19 changes: 18 additions & 1 deletion 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-09
last_verified: 2026-10-13
---

# Security Policy
Expand Down Expand Up @@ -37,6 +37,23 @@ still use the server. Do not expose it through a public reverse proxy. The ratio
on simple content types and complete-origin comparison, plus independent authority validation for a
directly accessed development server.

## Provider error and metadata boundary

The development source after `v0.1.1` 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
details while retaining runner-generated validation and deadline messages. This fix is not present
in the original `v0.1.1` archive; see the [changelog](CHANGELOG.md) for release status.

Provider endpoints must not contain URL userinfo credentials. Provider extensions, manifest fields,
endpoint paths/query parameters, command output, and file bytes are intentionally returned data, not
automatic secret-redaction surfaces. Providers must publish only approved metadata; do not put secrets
in extensions or endpoint URLs. Namespacing does not provide redaction. Diagnostics belong in a
separately authorized, redacted Provider-side channel; the reference runtime does not log raw errors.

See [public-safety guidance](docs/public-safety.md) for content and commit-identity checks.

## Deployment boundary

The reference runtime and mock provider are development implementations. They do not provide:
Expand Down
62 changes: 62 additions & 0 deletions docs/public-safety.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
---
id: public-safety
authority: reference
status: active
title: Public safety and commit identity
genre: how-to
last_verified: 2026-10-13
---

# Public Safety and Commit Identity

## Separate checks

`pnpm sanitize` scans source, available Git patch history and image trace metadata for credential
shapes, private paths and dangerous files. It is not a full semantic, personal-identity or forensic
history audit. Synthetic tests and approved public identities still need human review.

`pnpm identity:check` checks author and committer email formats in all commits reachable from `HEAD`.
It accepts GitHub noreply formats, prints commit IDs and field names only, and fails closed when Git
or the requested range is unavailable. It checks format, not authenticated identity or approved names.
Known organization fingerprints and person names require a separate private, value-withholding audit.

## Candidate range gate

Existing historical identity findings require an explicit disposition. Do not silently exempt or
rewrite them. When the owner approves retaining a historical baseline, verify every new commit:

```bash
pnpm identity:check -- --since <approved-full-base-sha>
pnpm sanitize
```

The baseline must exist and be an ancestor of `HEAD`; invalid refs fail closed. A green range check
proves only that range's author/committer email formats, not that older history has been scrubbed.
Shallow CI clones may not have the baseline. Run against a complete owning clone before push and
again on the actual merged commit; synthetic CI merge identities are not a substitute.

Use an approved public name and GitHub noreply email in local configuration. For GitHub squash merges,
the author email is independent of local configuration. Pass an explicit approved address:

```bash
gh pr merge <number> --squash --match-head-commit <verified-head-sha> \
--author-email <approved-github-noreply-email>
```

Inspect the merged commit with the range gate after merge. Never paste personal email values into a
public finding, report or fixture; use dynamic synthetic values and reserved example identities.

## Historical remediation

Commit history, released tags, open/closed PR refs, forks, caches and release metadata are distinct
surfaces. Ordinary new commits and `.mailmap` do not remove the original objects. Prepare exact ref
and tag mappings, backup/recovery, clone migration, release/source links and cache limitations before
requesting approval for rewriting. Force-pushes, tag moves, repository deletion and support removal
are not authorized by a generic code-fix request. Never claim all public copies have disappeared.

## 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.
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.
10 changes: 9 additions & 1 deletion docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ authority: reference
status: active
title: Testing and provider conformance
genre: how-to
last_verified: 2026-10-09
last_verified: 2026-10-13
---

# Testing and Provider Conformance
Expand Down Expand Up @@ -37,6 +37,14 @@ ripgrep from `PATH` and verify that detection still blocks publication without p

## Test dimensions

Provider data-safety regressions cover ordinary/typed/non-Error exceptions at every runtime entry,
stored create/recreate/terminate failures, get/list/events and SDK reads, returned observation codes,
conformance output, endpoint userinfo, and unchanged intentional command output. Commit-identity
fixtures separately verify author and committer emails, complete candidate ranges, explicit legacy
baselines, and fail-closed behavior without printing matched values. Before pushing and after merging,
run the [identity gate](public-safety.md) against the exact candidate range; ordinary content scanning
is not an identity audit.

| Dimension | Coverage |
| --- | --- |
| Unit and boundary | validation, lifecycle, errors, event cursors |
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"build": "tsc -p tsconfig.build.json",
"check": "pnpm format:check && pnpm lint && pnpm typecheck && pnpm test && pnpm docs:check && pnpm build && pnpm test:coverage && pnpm pack:check && pnpm audit:dependencies",
"docs:check": "node scripts/check-docs.mjs",
"identity:check": "node scripts/check-identity.mjs",
"format": "biome format --write .",
"format:check": "biome format .",
"lint": "biome lint .",
Expand Down
35 changes: 35 additions & 0 deletions scripts/check-identity.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { execFileSync } from 'node:child_process'

const suppliedArguments = process.argv.slice(2)
const arguments_ = suppliedArguments[0] === '--' ? suppliedArguments.slice(1) : suppliedArguments
const git = (args) =>
execFileSync('git', args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim()
const isPublicEmail = (value) =>
/^[A-Za-z0-9_.+\-[\]]+@users\.noreply\.github\.com$|^noreply@github\.com$/.test(value)
try {
if (arguments_.length !== 0 && (arguments_.length !== 2 || arguments_[0] !== '--since'))
throw new Error('invalid arguments')
const head = git(['rev-parse', '--verify', 'HEAD^{commit}'])
let range = head
if (arguments_.length) {
const base = git(['rev-parse', '--verify', '--end-of-options', `${arguments_[1]}^{commit}`])
git(['merge-base', '--is-ancestor', base, head])
range = `${base}..${head}`
}
const records = git(['log', '--format=%H%x00%ae%x00%ce', range]).split('\n').filter(Boolean)
const hits = []
for (const record of records) {
const [commit, author, committer] = record.split('\0')
if (!isPublicEmail(author)) hits.push({ commit, field: 'author email' })
if (!isPublicEmail(committer)) hits.push({ commit, field: 'committer email' })
}
if (hits.length) {
process.stderr.write(
`non-public commit identity format detected; values withheld\n${hits.map(({ commit, field }) => `${commit}: ${field}`).join('\n')}\n`,
)
process.exitCode = 1
} else process.stdout.write(`commit identity check passed: ${records.length} commits\n`)
} catch {
process.stderr.write('commit identity check could not complete; refusing a clean result\n')
process.exitCode = 2
}
9 changes: 8 additions & 1 deletion spec/protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ authority: canonical
status: canonical
title: Portable protocol semantics
genre: spec
last_verified: 2026-09-02
last_verified: 2026-10-13
---

# Portable Protocol Semantics
Expand All @@ -25,6 +25,13 @@ The normative HTTP projection uses JSON under `/v1`. Errors use:
The stable machine field is `error.code`. Human-readable messages may improve without a protocol
version change.

The reference runtime uses fixed public messages for Provider exceptions and stored failure
observations. Only portable error codes from the published vocabulary are retained; other Provider
failure codes map to `provider_unavailable`. Clients must branch on codes, not diagnostic text.
Raw Provider diagnostics and credentials must not be placed in returned metadata or failure fields.
Endpoint URLs containing userinfo credentials are rejected. Intentionally returned command output
and file bytes are not rewritten by this policy.

## Lifecycle

Create is idempotent by `clientRequestId`. Concurrent identical requests converge to one logical
Expand Down
6 changes: 5 additions & 1 deletion src/conformance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,11 @@ const callBefore = async <T>(
let timer: ReturnType<typeof setTimeout> | undefined
try {
return await Promise.race([
Promise.resolve().then(() => operation({ requestId, signal: controller.signal })),
Promise.resolve()
.then(() => operation({ requestId, signal: controller.signal }))
.catch(() => {
throw new Error('provider operation failed')
}),
new Promise<never>((_resolve, reject) => {
timer = setTimeout(() => {
reject(new Error(timeoutMessage))
Expand Down
43 changes: 27 additions & 16 deletions src/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
capabilityNames,
type ProviderManifest,
RuntimeError,
runtimeErrorCodes,
type RuntimeErrorCode,
type RuntimeInfo,
type SandboxEvent,
type SandboxEventType,
Expand Down Expand Up @@ -50,6 +52,10 @@ const canonicalize = (value: unknown): unknown => {
}

const stableJson = (value: unknown): string => JSON.stringify(canonicalize(value))
const publicProviderErrorCode = (value: unknown): RuntimeErrorCode =>
runtimeErrorCodes.includes(value as RuntimeErrorCode)
? (value as RuntimeErrorCode)
: 'provider_unavailable'
const extensionKeyPattern = /^[a-z][a-z0-9-]*\.[A-Za-z][A-Za-z0-9_.-]*$/

const isJsonValue = (value: unknown, seen = new WeakSet<object>()): value is JsonValue => {
Expand Down Expand Up @@ -245,7 +251,7 @@ export class InMemorySandboxRuntime {
} catch (error) {
this.#writeObservation(id, {
state: 'failed',
failure: { code: 'provider_unavailable', message: this.#errorMessage(error) },
failure: { code: 'provider_unavailable', message: 'provider operation failed' },
})
throw this.#providerError('terminate', error)
}
Expand Down Expand Up @@ -359,10 +365,10 @@ export class InMemorySandboxRuntime {
{ sandboxId: id, generation: next.generation, spec },
context,
)
} catch (error) {
} catch {
return this.#writeObservation(id, {
state: 'failed',
failure: { code: 'provider_unavailable', message: this.#errorMessage(error) },
failure: { code: 'provider_unavailable', message: 'provider operation failed' },
})
}
return this.#writeObservation(id, observation)
Expand Down Expand Up @@ -396,7 +402,6 @@ export class InMemorySandboxRuntime {
})
return result
} catch (error) {
if (error instanceof RuntimeError) throw error
throw this.#providerError('execute', error)
}
})()
Expand Down Expand Up @@ -528,10 +533,10 @@ export class InMemorySandboxRuntime {
{ sandboxId: id, generation: requested.generation, spec },
context,
)
} catch (error) {
} catch {
return this.#writeObservation(id, {
state: 'failed',
failure: { code: 'provider_unavailable', message: this.#errorMessage(error) },
failure: { code: 'provider_unavailable', message: 'provider operation failed' },
})
}
return this.#writeObservation(id, observation)
Expand Down Expand Up @@ -563,7 +568,11 @@ export class InMemorySandboxRuntime {
if (observation.endpoint) {
try {
const endpoint = new URL(observation.endpoint.baseUrl)
if (!['http:', 'https:'].includes(endpoint.protocol))
if (
!['http:', 'https:'].includes(endpoint.protocol) ||
endpoint.username ||
endpoint.password
)
throw new Error('unsupported protocol')
} catch {
throw new RuntimeError('provider_protocol_error', 'provider endpoint must be an HTTP URL')
Expand All @@ -572,7 +581,7 @@ export class InMemorySandboxRuntime {
if (!transitions[current.state].includes(observation.state)) {
throw new RuntimeError(
'provider_protocol_error',
`provider attempted invalid transition ${current.state} -> ${observation.state}`,
'provider attempted an invalid state transition',
)
}
const endpoint = observation.endpoint ?? current.endpoint
Expand All @@ -587,7 +596,14 @@ export class InMemorySandboxRuntime {
updatedAt: this.#clock().toISOString(),
...(observation.state !== 'terminated' && endpoint ? { endpoint } : {}),
...(providerExtensions ? { providerExtensions } : {}),
...(observation.failure ? { failure: observation.failure } : {}),
...(observation.failure
? {
failure: {
code: publicProviderErrorCode(observation.failure.code),
message: 'provider operation failed',
},
}
: {}),
}
this.#resources.set(id, updated)
if (current.state !== updated.state) {
Expand Down Expand Up @@ -868,15 +884,10 @@ export class InMemorySandboxRuntime {
}
}

#errorMessage(error: unknown): string {
return error instanceof Error ? error.message : 'provider failed without an error'
}

#providerError(operation: string, error: unknown): RuntimeError {
if (error instanceof RuntimeError) return error
return new RuntimeError(
'provider_unavailable',
`provider ${operation} failed: ${this.#errorMessage(error)}`,
error instanceof RuntimeError ? publicProviderErrorCode(error.code) : 'provider_unavailable',
`provider ${operation} failed`,
)
}

Expand Down
Loading
Loading