From cf73b0dfa96cf2701e3283cf037b5cd8efadec78 Mon Sep 17 00:00:00 2001 From: Yixuan Xu Date: Sat, 17 Oct 2026 17:08:24 +0000 Subject: [PATCH] Audit runtime reuse and probe offline isolation Keep the room driver thin over existing Cordis and DSH capabilities. Add an explicit synthetic Docker probe with pinned images, separate workspaces, denied network and credentials, controlled artifacts, bounded resources and observed cleanup. --- dev/interop/isolation/probe.mjs | 151 ++++++++++++++++++++++ dev/interop/test/isolation-probe.test.mjs | 58 +++++++++ docs/interop/runtime-reuse-isolation.md | 124 ++++++++++++++++++ docs/subsystems/interop.md | 8 +- 4 files changed, 340 insertions(+), 1 deletion(-) create mode 100644 dev/interop/isolation/probe.mjs create mode 100644 dev/interop/test/isolation-probe.test.mjs create mode 100644 docs/interop/runtime-reuse-isolation.md diff --git a/dev/interop/isolation/probe.mjs b/dev/interop/isolation/probe.mjs new file mode 100644 index 000000000..9082038a0 --- /dev/null +++ b/dev/interop/isolation/probe.mjs @@ -0,0 +1,151 @@ +// Opt-in OS boundary probe, separate from the no-process contract simulator. +import { execFile } from 'node:child_process' +import { chmodSync, copyFileSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { basename, dirname, isAbsolute, join, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' +import { createHash, randomUUID } from 'node:crypto' +import { promisify } from 'node:util' + +export function validateImage(image) { + if (typeof image !== 'string' || !/^[a-z0-9][a-z0-9./:_-]*@sha256:[a-f0-9]{64}$/.test(image) || image.trim() !== image) throw new Error('Use an explicit image repository@sha256:digest') + return image +} + +export function containerArgs({ image, name, workspace, script, role }) { + validateImage(image) + if (!/^flybot-probe-[a-z0-9-]+$/.test(name) || !['writer', 'verifier'].includes(role)) throw new Error('Invalid probe identity') + for (const path of [workspace, script]) { + if (!isAbsolute(path) || path === '/' || resolve(path) !== path || /[,\r\n]/.test(path)) throw new Error('Invalid probe mount') + } + if (dirname(workspace) !== dirname(script) || basename(workspace) !== role || basename(script) !== 'probe.mjs') throw new Error('Probe mounts must belong to its disposable root') + return ['create', '--name', name, '--read-only', '--network', 'none', + '--user', '1000:1000', '--cap-drop', 'ALL', '--security-opt', 'no-new-privileges', + '--memory', '128m', '--cpus', '1', '--pids-limit', '64', + '--tmpfs', '/tmp:rw,noexec,nosuid,nodev,size=16m', + '--mount', `type=bind,src=${workspace},dst=/work`, + '--mount', `type=bind,src=${script},dst=/probe.mjs,readonly`, + '--workdir', '/work', '--entrypoint', '/usr/bin/env', image, + '-i', 'PATH=/usr/local/bin:/usr/bin:/bin', 'TZ=UTC', 'node', '/probe.mjs', role] +} + +const probeScript = `import assert from 'node:assert/strict' +import { readFileSync, writeFileSync } from 'node:fs' +import { createConnection } from 'node:net' +const role = process.argv[2] +const checks = {} +writeFileSync('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/work/own-write.txt', role) +assert.equal(readFileSync('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/work/own-write.txt', 'utf8'), role) +checks.ownWrite = true +for (const [name, path] of [['peerPrivateRead', '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/peer/private.txt'], ['hostPrivateRead', '/host-private.txt']]) { + assert.throws(() => readFileSync(path), error => ['ENOENT', 'EACCES', 'EPERM'].includes(error.code)) + checks[name] = 'denied' +} +assert.throws(() => writeFileSync('/outside-workspace.txt', 'denied'), error => ['EROFS', 'EACCES', 'EPERM'].includes(error.code)) +checks.rootWrite = 'denied' +assert.deepEqual(Object.keys(process.env).sort(), ['PATH', 'TZ']) +checks.ambientEnvironment = 'PATH,TZ only' +const networkCode = await new Promise((resolve, reject) => { + const socket = createConnection({ host: '192.0.2.1', port: 80 }) + socket.setTimeout(1000, () => { socket.destroy(); reject(new Error('Network denial was not observed')) }) + socket.once('connect', () => { socket.destroy(); reject(new Error('Unexpected network connection')) }) + socket.once('error', error => { socket.destroy(); resolve(error.code) }) +}) +assert.equal(networkCode, 'ENETUNREACH') +checks.externalNetwork = networkCode +if (role === 'writer') writeFileSync('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/work/result.json', JSON.stringify({ fixture: 'synthetic', total: 140 })) +else assert.deepEqual(JSON.parse(readFileSync('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/work/result.json', 'utf8')), { fixture: 'synthetic', total: 140 }) +checks.artifact = role === 'writer' ? 'produced' : 'verified' +console.log(JSON.stringify({ role, node: process.version, checks })) +` + +async function docker(args, { signal } = {}) { + const env = { ...process.env } + // Select only the managed local daemon; keep registry/proxy configuration. + for (const key of ['DOCKER_HOST', 'DOCKER_CONTEXT', 'DOCKER_TLS', 'DOCKER_TLS_VERIFY', 'DOCKER_CERT_PATH']) delete env[key] + const { stdout } = await promisify(execFile)('docker', ['--host=unix:///var/run/docker.sock', ...args], { + env, signal, encoding: 'utf8', timeout: 20_000, maxBuffer: 128 * 1024, + }) + return stdout.trim() +} + +function verifyConfiguration(inspect) { + const config = inspect.HostConfig + assertConfigured(inspect.Config.User === '1000:1000', 'user') + assertConfigured(config.ReadonlyRootfs && config.NetworkMode === 'none', 'root/network') + assertConfigured(config.Memory === 128 * 1024 * 1024 && config.NanoCpus === 1_000_000_000 && config.PidsLimit === 64, 'resource limits') + assertConfigured(config.CapDrop?.includes('ALL') && config.SecurityOpt?.includes('no-new-privileges'), 'privileges') + assertConfigured(config.Privileged === false && config.PidMode !== 'host' && config.IpcMode !== 'host', 'namespaces') + assertConfigured(inspect.Mounts.length === 2 && inspect.Mounts.every(m => ['/work', '/probe.mjs'].includes(m.Destination)), 'mounts') + return { user: '1000:1000', readOnlyRoot: true, network: 'none', memoryBytes: config.Memory, nanoCpus: config.NanoCpus, pids: config.PidsLimit, capDrop: ['ALL'], noNewPrivileges: true } +} + +function assertConfigured(value, name) { + if (!value) throw new Error(`Probe configuration mismatch: ${name}`) +} + +export async function runIsolationProbe(image, { invoke = docker, signal } = {}) { + validateImage(image) + const started = new Date().toISOString() + const serverVersion = await invoke(['info', '--format', '{{.ServerVersion}}'], { signal }) + const imageId = await invoke(['image', 'inspect', image, '--format', '{{.Id}}'], { signal }) + const root = mkdtempSync(join(tmpdir(), 'flybot-isolation-')) + const names = [] + const observations = [] + let report + try { + chmodSync(root, 0o755) + const script = join(root, 'probe.mjs') + writeFileSync(script, probeScript, { mode: 0o444 }) + writeFileSync(join(root, 'host-private.txt'), 'SYNTHETIC_HOST_PRIVATE') + for (const role of ['writer', 'verifier']) { + const workspace = join(root, role) + mkdirSync(workspace, { mode: 0o777 }) + chmodSync(workspace, 0o777) + writeFileSync(join(workspace, 'private.txt'), `SYNTHETIC_${role.toUpperCase()}_PRIVATE`, { mode: 0o444 }) + } + for (const role of ['writer', 'verifier']) { + if (role === 'verifier') copyFileSync(join(root, 'writer/result.json'), join(root, 'verifier/result.json')) + const name = `flybot-probe-${randomUUID()}-${role}` + names.push(name) + await invoke(containerArgs({ image, name, workspace: join(root, role), script, role }), { signal }) + const configured = verifyConfiguration(JSON.parse(await invoke(['inspect', name], { signal }))[0]) + const observed = JSON.parse(await invoke(['start', '--attach', name], { signal })) + assertConfigured(observed.role === role, 'role evidence') + const state = JSON.parse(await invoke(['inspect', name], { signal }))[0].State + assertConfigured(state.ExitCode === 0 && !state.Running && !state.OOMKilled, 'execution outcome') + observations.push({ ...observed, configured }) + } + const bytes = readFileSync(join(root, 'writer/result.json')) + assertConfigured(bytes.equals(readFileSync(join(root, 'verifier/result.json'))), 'artifact handoff') + report = { schemaVersion: 1, purpose: 'synthetic-offline-isolation-probe', status: 'passed', started, ended: new Date().toISOString(), image, imageId, docker: serverVersion, artifactSha256: createHash('sha256').update(bytes).digest('hex'), observations } + } finally { + const failures = [] + for (const name of names) { + try { await invoke(['rm', '--force', name]) } catch (error) { + if (!/No such container/.test(String(error.stderr))) failures.push(error) + } + } + rmSync(root, { recursive: true, force: true }) + if (failures.length) throw new AggregateError(failures, 'Probe container cleanup failed') + } + signal?.throwIfAborted() + return { ...report, cleanup: 'completed' } +} + +if (process.argv[1] === fileURLToPath(import.meta.url)) { + const controller = new AbortController() + const stop = () => controller.abort() + process.on('SIGINT', stop) + process.on('SIGTERM', stop) + try { + if (process.argv.length !== 4 || process.argv[2] !== '--image') throw new Error('Expected --image repository@sha256:digest') + console.log(JSON.stringify(await runIsolationProbe(process.argv[3], { signal: controller.signal }), null, 2)) + } catch (error) { + console.error(JSON.stringify({ status: 'failed', category: error.code ?? error.name, message: 'Isolation probe did not pass; no runtime integration is authorized by this result.' })) + process.exitCode = 1 + } finally { + process.removeListener('SIGINT', stop) + process.removeListener('SIGTERM', stop) + } +} diff --git a/dev/interop/test/isolation-probe.test.mjs b/dev/interop/test/isolation-probe.test.mjs new file mode 100644 index 000000000..4f74ad861 --- /dev/null +++ b/dev/interop/test/isolation-probe.test.mjs @@ -0,0 +1,58 @@ +import assert from 'node:assert/strict' +import { test } from 'node:test' +import { containerArgs, runIsolationProbe, validateImage } from '../isolation/probe.mjs' + +const image = `node@sha256:${'a'.repeat(64)}` +const spec = { image, name: 'flybot-probe-synthetic-writer', workspace: '/tmp/pilot/writer', script: '/tmp/pilot/probe.mjs', role: 'writer' } + +test('isolation probe refuses mutable image tags before starting resources', () => { + for (const value of ['', 'node:24', 'node@sha256:abc', `${image}\n`, '--privileged']) assert.throws(() => validateImage(value)) + assert.equal(validateImage(image), image) +}) + +test('isolation probe limits mounts and drops execution privileges', () => { + const args = containerArgs(spec) + const value = flag => args[args.indexOf(flag) + 1] + assert.equal(value('--network'), 'none') + assert.equal(value('--user'), '1000:1000') + assert.equal(value('--cap-drop'), 'ALL') + assert.equal(value('--security-opt'), 'no-new-privileges') + assert.equal(value('--memory'), '128m') + assert.equal(value('--pids-limit'), '64') + assert.ok(args.includes('--read-only')) + assert.equal(args.filter(arg => arg === '--mount').length, 2) + assert.ok(args.includes('type=bind,src=/tmp/pilot/writer,dst=/work')) + assert.ok(args.includes('type=bind,src=/tmp/pilot/probe.mjs,dst=/probe.mjs,readonly')) + assert.ok(!args.some(arg => /privileged|docker\.sock|host=|network=host/.test(arg))) + assert.deepEqual(args.slice(args.indexOf(image) + 1), ['-i', 'PATH=/usr/local/bin:/usr/bin:/bin', 'TZ=UTC', 'node', '/probe.mjs', 'writer']) + assert.equal(value('--entrypoint'), '/usr/bin/env') +}) + +test('isolation probe rejects argument or mount injection and unknown roles', () => { + for (const patch of [ + { workspace: '/' }, { workspace: 'relative' }, { workspace: '/tmp/pilot,src=/' }, + { workspace: '/tmp/..' }, { script: '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/other/probe.mjs' }, + { script: '/tmp/probe,readonly=false' }, { name: '--privileged' }, { role: 'shell' }, + ]) assert.throws(() => containerArgs({ ...spec, ...patch })) +}) + +test('partial container creation is cleaned up even after cancellation', async () => { + const calls = [] + const controller = new AbortController() + await assert.rejects(runIsolationProbe(image, { signal: controller.signal, invoke: async (args, options) => { + calls.push(args) + if (args[0] === 'create') { controller.abort(); throw new Error('creation interrupted') } + if (args[0] === 'rm') assert.equal(options, undefined, 'cleanup must not use the aborted execution signal') + return 'synthetic' + } }), /creation interrupted/) + const create = calls.find(args => args[0] === 'create') + assert.deepEqual(calls.at(-1), ['rm', '--force', create[create.indexOf('--name') + 1]]) +}) + +test('container cleanup failure cannot produce a passing isolation report', async () => { + await assert.rejects(runIsolationProbe(image, { invoke: async args => { + if (args[0] === 'create') throw new Error('creation failed') + if (args[0] === 'rm') throw new Error('daemon unavailable') + return 'synthetic' + } }), /cleanup failed/) +}) diff --git a/docs/interop/runtime-reuse-isolation.md b/docs/interop/runtime-reuse-isolation.md new file mode 100644 index 000000000..b70a40658 --- /dev/null +++ b/docs/interop/runtime-reuse-isolation.md @@ -0,0 +1,124 @@ +# Runtime reuse and offline deployment boundary (#198 / #221) + +This records the P0 review requested in #198 before enabling an external room +participant. It adds no runtime registration, execution authority or live trial. +The acceptance target is one FlyBot room: DSH + DSH, then DSH + OpenCode, using +the same code fixture, independent checks and explicit human acceptance. + +The older `opencode-pilot.md` describes the separate cross-owner Bot Social DM +experiment and its offline harness. Its text-only/no-tools/DM scope does not +define the same-room code trial. Preserve the same-owner friendship constraint +and all cross-owner gates. #218 documents this distinction with a thin DSH room +boundary; #220 supplies a scripted host fixture. Neither is live integration. + +## Pinned capability audit + +Inspected DSH **0.2.0-rc.2**, source commit +`639ed015397290b3745d163aafe02ffee4aa3f84`, and FlyBot base +`40ddca97fbd76f256c4b7dd04cb9cf2bf9b656f2`. These are source/API observations, +not executions of the providers below. No provider process or model was started. + +| Existing capability | Reuse and limit | +| --- | --- | +| Cordis context/service, dependency injection, typed events, `effect` and disposers | Use the existing plugin lifecycle for mounting an adapter and cleaning its owned work. Do not introduce a second service registry, plugin framework or event bus. Context/realm separation does not confine filesystem, network or processes. | +| FlyBot room relay, delivery, membership/authorship and turn budgets | Keep the current scheduler and room transcript. A thin participant operation delegates execution; it does not select who may join/speak or own a new conversation scheduler. | +| `ctx.flybot.bot.send`, middleware priorities, grants, approvals, transports | Preserve the existing authorization and outbound path where applicable. A transport or provider being installed is not an execution grant. Send approval and remote tool approval remain distinct decisions. | +| FlyBot task channel | Keep its authoritative requester, state, question ID and final acceptance. There is no public `ctx.flybot.tasks` service in the inspected types. An external plugin needs a reviewed actor-bound seam; it must not call private `rt.taskEndpoint` or borrow the owner's session token. | +| `ctx.subagents` and provider registry | Reuse the public service when the requested semantics match a provider. Discover a mounted service/provider at runtime, inspect supported start options, and reject missing capabilities. Package installation alone does not establish a mounted capability. | +| In-process spawn/fork providers | Implement `prepareContinuable`; the existing continuation manager owns activation, FIFO inbox, identity, recovery and disposal. Spawn begins fresh; fork can seed parent history. The direct-parent/child authority graph is not room membership, and shared in-process services are not OS isolation. Do not automatically fork private history into a room. | +| ACP provider | Fresh process and `newSession` per one-shot run, final result, managed disposal; no optional start-time capabilities or `prepareContinuable`. Intermediate tool/message traffic is outside the parent conversation; permission prompts use configured reject/allow policy, not a human approval UI. It cannot directly satisfy the room's persistent clarification/permission workflow. | +| Codex / Claude Code providers | One-shot external providers, no optional start-time capabilities and no `prepareContinuable` at this pin. Their native SDK features do not automatically become capabilities of this DSH seam. No room continuation or permission bridge is established by their presence. | +| DSH SDK provider | Separate child runtime, one-shot result/disposal; supports `agentOptions` route overrides, but not `prepareContinuable` at this pin. It is not a drop-in persistent room participant. | +| DSH sandbox policy/provider | Reuse documented confinement where appropriate, but `SandboxMode` describes **filesystem effects only**. Network/process visibility are outside it. `partial` enforcement cannot be reported as `full`; a confined request must never silently fall back to unconfined execution. | + +The service checks one-shot flags (`agentOptions`, `outputSchema`, `depthLimit`, +`toolFilter`, `persona`) before dispatch. Continuability is separately indicated +by the provider's optional `prepareContinuable` method; it is not inferred from +those flags. A future room adapter must additionally establish persistent +session addressing, observable intermediate/permission events, cancellation +evidence and restart reconciliation. None is inferred from a final-text API. + +The internal extraction in #218 is deliberately smaller than a new provider +system: `open / runTurn / snapshot / cancel` wrap the existing DSH path. Generic +permission decisions, capability negotiation and task-specific bindings remain +future seams. Add only what cannot be expressed through existing services, and +do not modify or depend at runtime on DSH installation internals. + +## Selected offline probe backend + +Use two disposable Linux Docker containers for the **synthetic offline boundary +probe**, with one pinned Node image digest. This selects a backend for the probe, +not a production DSH/OpenCode deployment. A live trial needs its own pinned +runtime images, credentials, provider egress and lifecycle integration before it +can claim this boundary. No default live backend is enabled by this document. + +| Concern | Probe policy | +| --- | --- | +| Bot and runtime identity | Separate Writer/verifier container names; do not derive identity or authority from display text. This probe has no FlyBot Bot/session binding. | +| Writable state | Each container mounts only its own disposable workspace. The other workspace, host-private marker, ordinary DSH/OpenCode homes, repository and Docker socket are not mounted. Root is read-only; `/tmp` is a bounded disposable tmpfs. | +| OS execution | Non-root UID/GID 1000, all capabilities dropped, no-new-privileges, default private PID/IPC namespaces, no privileged or host-network mode. Containers share the host kernel: this is not a VM or a general escape-proof boundary. | +| Network and credentials | `--network none`; the Node probe receives only fresh `PATH`/`TZ`, with no ambient variables. It attempts one documentation-range TCP address and must observe `ENETUNREACH`; timeout is not accepted as proof of denial. No real credentials are used. | +| Resource configuration | 128 MiB RAM, one CPU, 64 PIDs, 16 MiB tmpfs; inspect the actual container configuration. This checks configured limits, not exhaustion enforcement or resistance to resource attacks. | +| Handoff | Only `result.json` is copied from Writer to the verifier after Writer exits. Compare identical bytes and record SHA-256. No private marker, history or worker tests are exported. This is a fixed synthetic artifact, not a generic archive/path importer. | +| Ownership and cleanup | The opt-in runner owns uniquely named containers and a temporary root. On success, failure or handled SIGINT/SIGTERM, remove its containers and files. Cleanup failure fails the run. SIGKILL, daemon failure and machine loss still require operator recovery; never claim confirmed cleanup then. | + +The probe's host directories contain synthetic data and allow its non-root +container UID to write. They are not a host multi-user confidentiality boundary. +A live deployment must use owner-restricted host paths and an explicit UID or +user-namespace mapping, rather than copying these test directory permissions. +The host Docker daemon and coordinator remain trusted and can access both sides. + +Current FlyBot `workDirOf` prioritizes `room.workDir`, then the Bot's directory; +members can therefore share writable state. Separate Sessions or a room folder +must not be described as per-Bot sandboxing. For a live trial, keep independent +workspaces and runtime state, export only the selected artifact, and run verifier +checks in the verifier's execution boundary. Do not share a mutable checkout. + +An online trial cannot reuse `--network none` unchanged. It must define a bounded +provider gateway and allowed destinations, separate runtime credentials, tool +permission enforcement and credential removal before running returned code. +Allowing a model endpoint does not authorize arbitrary network or external sends. + +## Reproduce the synthetic probe + +Docker must be available at the managed local socket. Pull the image explicitly +first; the probe requires it to be present and accepts a digest, never a tag: + +```sh +docker pull node@sha256:a5f4366e834695d5f363b920c104953a535d3a9864d1bdf74566a0ebe01daee5 +node dev/interop/isolation/probe.mjs \ + --image node@sha256:a5f4366e834695d5f363b920c104953a535d3a9864d1bdf74566a0ebe01daee5 \ + > isolation-report.json +``` + +The runner pins the local daemon socket and clears remote Docker endpoint/TLS +selectors for its client commands; registry and proxy configuration is retained. +Containers get an explicit clean process environment. It prints a safe structured +report with image/engine/Node versions, observations, selected limits, timestamps, +artifact digest and cleanup result. Failure exits nonzero. It does not read or +print credential files, full Docker configuration or the ambient environment. + +This command is opt-in and is **not** part of `npm run verify` or required CI. +The ordinary no-network/no-process interop suite only checks argument validation, +confinement settings and cleanup error paths using injected Docker calls. + +## Unfinished gates + +No real DSH/OpenCode process was confined or connected by this probe. No claim +is made about human approval UI, task/session isolation, closed-room callbacks, +restarts, cancellation generations, durable budgets, adversarial code/archives, +symlink attacks, kernel/container escape, credential exfiltration under online +policy, memory/CPU exhaustion, parallel workloads or performance. Those need +explicit tests at the actual execution seam. Mock cost remains distinct from +unknown real usage. #198 stays open. + +## Audit references + +All DSH links below are fixed to the inspected commit, not `main`: + +- [Cordis context, injection and lifecycle](https://github.com/deepseek-ai/deepseek-harness/blob/639ed015397290b3745d163aafe02ffee4aa3f84/docs/cordis-primer.md) +- [Subagent public types](https://github.com/deepseek-ai/deepseek-harness/blob/639ed015397290b3745d163aafe02ffee4aa3f84/packages/subagent/subagent/src/types.ts), [provider/continuation contract](https://github.com/deepseek-ai/deepseek-harness/blob/639ed015397290b3745d163aafe02ffee4aa3f84/docs/subsystems/subagent.md) +- [ACP behavior and limitations](https://github.com/deepseek-ai/deepseek-harness/blob/639ed015397290b3745d163aafe02ffee4aa3f84/packages/subagent/subagent-acp/README.md), [DSH SDK capability flags](https://github.com/deepseek-ai/deepseek-harness/blob/639ed015397290b3745d163aafe02ffee4aa3f84/packages/subagent/subagent-dsh-sdk/src/index.ts) +- [Codex provider](https://github.com/deepseek-ai/deepseek-harness/blob/639ed015397290b3745d163aafe02ffee4aa3f84/packages/subagent/subagent-codex/src/index.ts), [Claude Code provider](https://github.com/deepseek-ai/deepseek-harness/blob/639ed015397290b3745d163aafe02ffee4aa3f84/packages/subagent/subagent-claude-code/src/index.ts) +- [Filesystem sandbox guarantee and enforcement levels](https://github.com/deepseek-ai/deepseek-harness/blob/639ed015397290b3745d163aafe02ffee4aa3f84/docs/subsystems/sandbox.md) +- FlyBot: `core/dev/harness/package.json`, `core/src/host/hooks.js`, `core/types/flybot.d.ts`, `core/src/host/folders.js`, `core/src/host/tasks.js` at the base commit above. diff --git a/docs/subsystems/interop.md b/docs/subsystems/interop.md index 55434473a..4a5c0c381 100644 --- a/docs/subsystems/interop.md +++ b/docs/subsystems/interop.md @@ -11,6 +11,12 @@ An offline contract simulator for [#198](https://github.com/ThinkFlowLab/FlyBot/ and [#200](https://github.com/ThinkFlowLab/FlyBot/issues/200), not a shipped adapter. Commands, semantics and limitations are in [the harness README](../../dev/interop/README.md). +The separate opt-in [isolation probe](../interop/runtime-reuse-isolation.md) +(#221) runs synthetic scripts in disposable Docker containers. It is outside +the no-process simulator's command and required CI. It records selected OS +observations and configured limits; it does not connect a runtime or certify a +live deployment. Its ordinary unit tests use injected Docker calls only. + ## Owns - `dev/interop/`: bounded versioned codec, durable synthetic task coordinator, @@ -19,7 +25,7 @@ Commands, semantics and limitations are in [the harness README](../../dev/intero ## Invariants -- No model, credential, real process, DSH data or network access. The command +- The contract simulator has no model, credential, real process, DSH data or network access. The command preload rejects accidental network/process calls; it is not a security sandbox. - Owner identities stay distinct. Same-owner social friendship is rejected; sending and executing require separate owner-bound mock decisions.