diff --git a/docs/delivery/target-state-implementation/phases/04-execution-host-containment.md b/docs/delivery/target-state-implementation/phases/04-execution-host-containment.md index 70d2174..8f340f8 100644 --- a/docs/delivery/target-state-implementation/phases/04-execution-host-containment.md +++ b/docs/delivery/target-state-implementation/phases/04-execution-host-containment.md @@ -111,6 +111,18 @@ no-phone-home among its unproven gaps. - Reviewer axes: is the proof exercised or derived; honesty of the strength category; failure tokens wired to policy consequence; no autonomy on report. +### Post-merge notes + +- The negative-egress check originally shipped as a hardcoded `negativeEgressProbePassed: false` + with no mechanism behind it and no escalation recorded — exactly the "declared constant" shape + this phase exists to forbid. The remediation replaces the constant with an exercised, injectable + egress-attempt check: the confined probe child dials a routable non-loopback target before + teardown and records the observed outcome (`blocked`/`open`/`ambiguous`) in the probe result + and attestation; only an explicit local/policy denial counts as `blocked` for the TEST-NET + target. On the current macOS process-group (weak) host the honest observation remains + not-blocked, so `strong` stays unreachable — the deliverable is the exercised mechanism and + recorded observation, not an EVRUN-full real-effect proof. + ## Out Of Scope - Remote execution hosts (product deferral). diff --git a/packages/jig-sdk/src/ports.ts b/packages/jig-sdk/src/ports.ts index ddd2443..06a4658 100644 --- a/packages/jig-sdk/src/ports.ts +++ b/packages/jig-sdk/src/ports.ts @@ -1,4 +1,4 @@ -import type { ContainmentMechanism } from './providers/real/confinement.js'; +import type { ContainmentMechanism, NegativeEgressOutcome } from './providers/real/confinement.js'; import type { PlanInstance, RunEvent, Story, WorkerResult } from './types.js'; export type IsolationStrength = 'none' | 'weak' | 'strong'; @@ -19,6 +19,11 @@ export interface CapabilityAttestation { provenIsolationStrength?: IsolationStrength; provenBy?: 'exercised-confinement-proof'; containmentMechanism?: ContainmentMechanism; + /** + * Raw observed outcome of the probe's negative-egress dial attempt, when the probe exercised + * one — lets downstream evidence cite the observation, not just derived booleans. + */ + negativeEgressObservedOutcome?: NegativeEgressOutcome; failureToken?: HostFailureToken; } diff --git a/packages/jig-sdk/src/providers/real/confinement.ts b/packages/jig-sdk/src/providers/real/confinement.ts index e382a1e..edbcad1 100644 --- a/packages/jig-sdk/src/providers/real/confinement.ts +++ b/packages/jig-sdk/src/providers/real/confinement.ts @@ -1,6 +1,6 @@ import { spawn } from 'node:child_process'; import { once } from 'node:events'; -import { createServer } from 'node:net'; +import { createConnection, createServer } from 'node:net'; import { setTimeout as delay } from 'node:timers/promises'; import { type Clock, decideFreshness } from '../../clock.js'; import type { CapabilityFreshness, HostFailureToken, IsolationStrength } from '../../ports.js'; @@ -8,14 +8,45 @@ import type { SubstrateRequest } from '../../substrate.js'; export type ContainmentMechanism = 'process-group' | 'kernel-tree' | 'job-object'; const LOOPBACK_EGRESS_REQUEST = 'loopback-tcp-connect'; +const NEGATIVE_EGRESS_REQUEST = 'negative-egress-tcp-connect'; const PROBE_GRACE_PERIOD_MS = 250; const PROBE_FRESHNESS_WINDOW_MS = 60_000; +// TEST-NET-3 (RFC 5737, 203.0.113.0/24) is reserved documentation address space: it is +// routable-shaped (not loopback or link-local) but never assigned, so the dial attempt is a +// genuine outbound egress exercise that can never reach or disturb a real service — the +// no-phone-home discipline holds even when the real dialer runs in the smoke lane. +const NEGATIVE_EGRESS_TARGET_HOST = '203.0.113.1'; +const NEGATIVE_EGRESS_TARGET_PORT = 443; +const NEGATIVE_EGRESS_TIMEOUT_MS = 1_500; + +/** + * Observed outcome of the negative-egress dial attempt: + * + * - `blocked` — the local stack demonstrably denied the attempt by explicit local/policy + * refusal (EPERM/EACCES) before any route or peer response. Only this outcome may count + * toward a passed negative-egress probe. + * - `open` — the attempt demonstrably left the host: either the connection established, or a + * peer/network response came back (ECONNREFUSED/ECONNRESET means a packet round-trip + * happened, which proves egress was not blocked). + * - `ambiguous` — nothing conclusive was observed (timeout with no answer, or an unrecognized + * error). On an open-egress host dialing TEST-NET-3 this is the common honest outcome: no + * answer is indistinguishable from a silent block, so it must never count as `blocked`. + */ +export type NegativeEgressOutcome = 'blocked' | 'open' | 'ambiguous'; + +export interface NegativeEgressAttemptOptions { + host: string; + port: number; + timeoutMs: number; +} + interface ChildProbePayload { observedAt: string; observedExecArgv: string[]; parentPid: number; loopbackReachable: boolean; + negativeEgressObservedOutcome: NegativeEgressOutcome; } type ProbeStream = Pick; @@ -65,11 +96,36 @@ const PROBE_CHILD_SOURCE = [ " socket.once('error', () => finish(false));", ' socket.setTimeout(500, () => finish(false));', '});', + 'const negativeEgressObservedOutcome = await new Promise((resolve) => {', + ` const socket = createConnection({ host: ${JSON.stringify(NEGATIVE_EGRESS_TARGET_HOST)}, port: ${String(NEGATIVE_EGRESS_TARGET_PORT)} });`, + ' let settled = false;', + ' const finish = (outcome) => {', + ' if (settled) return;', + ' settled = true;', + ' socket.destroy();', + ' resolve(outcome);', + ' };', + " socket.once('connect', () => finish('open'));", + " socket.once('error', (error) => {", + " const code = error instanceof Error && 'code' in error && typeof error.code === 'string' ? error.code : undefined;", + " if (code === 'ECONNREFUSED' || code === 'ECONNRESET') {", + " finish('open');", + ' return;', + ' }', + " if (code === 'EPERM' || code === 'EACCES') {", + " finish('blocked');", + ' return;', + ' }', + " finish('ambiguous');", + ' });', + ` socket.setTimeout(${String(NEGATIVE_EGRESS_TIMEOUT_MS)}, () => finish('ambiguous'));`, + '});', 'const payload = {', ' observedAt: new Date().toISOString(),', ' observedExecArgv: process.execArgv,', ' parentPid: process.ppid,', ' loopbackReachable,', + ' negativeEgressObservedOutcome,', '};', 'await new Promise((resolve, reject) => {', " process.stdout.write(JSON.stringify(payload) + '\\n', (error) => {", @@ -88,6 +144,13 @@ export interface ConfinementProbeResult { freshnessWindowMs: number; terminationProvedEmpty: boolean; negativeEgressProbePassed: boolean; + /** + * The raw observed outcome of the negative-egress dial attempt, recorded alongside the + * derived pass/fail boolean so downstream evidence (attestations, the EVRUN-full + * no-phone-home leg) can cite what an exercised check actually observed instead of a + * constant. Absent only for probe doubles that do not exercise the check at all. + */ + negativeEgressObservedOutcome?: NegativeEgressOutcome; containmentMechanism?: ContainmentMechanism; commandBindingPassed: boolean; parentageProbePassed: boolean; @@ -101,6 +164,7 @@ export interface ConfinementProof { provenIsolationStrength?: IsolationStrength; provenBy?: 'exercised-confinement-proof'; containmentMechanism?: ContainmentMechanism; + negativeEgressObservedOutcome?: NegativeEgressOutcome; failureToken?: HostFailureToken; } @@ -130,6 +194,57 @@ function proofSupportsClaimedStrength(result: ConfinementProbeResult): boolean { return result.provenIsolationStrength === 'weak' || result.provenIsolationStrength === 'none'; } +const BLOCKED_EGRESS_ERROR_CODES = new Set(['EPERM', 'EACCES']); +const OPEN_EGRESS_ERROR_CODES = new Set(['ECONNREFUSED', 'ECONNRESET']); + +export function classifyNegativeEgressDialError(error: unknown): NegativeEgressOutcome { + if (error instanceof Error && 'code' in error && typeof error.code === 'string') { + if (OPEN_EGRESS_ERROR_CODES.has(error.code)) { + return 'open'; + } + if (BLOCKED_EGRESS_ERROR_CODES.has(error.code)) { + return 'blocked'; + } + } + + return 'ambiguous'; +} + +/** + * The real negative-egress dialer: attempts a TCP connect toward the configured target and + * classifies what was actually observed. The confinement probe child uses the same semantics; + * this helper exists so hermetic unit tests can exercise that classification with an injected + * `createConnection` double and without real network I/O. + */ +export async function attemptNegativeEgressDial( + options: NegativeEgressAttemptOptions, + createConnectionImpl: typeof createConnection = createConnection, +): Promise { + return await new Promise((resolve) => { + const socket = createConnectionImpl({ host: options.host, port: options.port }); + let settled = false; + const finish = (outcome: NegativeEgressOutcome) => { + if (settled) { + return; + } + settled = true; + socket.destroy(); + resolve(outcome); + }; + socket.once('connect', () => finish('open')); + socket.once('error', (error) => finish(classifyNegativeEgressDialError(error))); + socket.setTimeout(options.timeoutMs, () => finish('ambiguous')); + }); +} + +export function defaultNegativeEgressAttemptOptions(): NegativeEgressAttemptOptions { + return { + host: NEGATIVE_EGRESS_TARGET_HOST, + port: NEGATIVE_EGRESS_TARGET_PORT, + timeoutMs: NEGATIVE_EGRESS_TIMEOUT_MS, + }; +} + function defaultConfinementRuntime(): ProcessGroupConfinementRuntime { return { currentPid: process.pid, @@ -168,6 +283,7 @@ export function localProcessGroupProbeSubstrateRequests(execPath = process.execP return [ { kind: 'argv', value: localProcessGroupProbeCommand(execPath) }, { kind: 'egress', value: LOOPBACK_EGRESS_REQUEST }, + { kind: 'egress', value: NEGATIVE_EGRESS_REQUEST }, ]; } @@ -276,7 +392,8 @@ export function createMacosProcessGroupConfinementProbe( observedAt: payload.observedAt, freshnessWindowMs: PROBE_FRESHNESS_WINDOW_MS, terminationProvedEmpty, - negativeEgressProbePassed: false, + negativeEgressProbePassed: payload.negativeEgressObservedOutcome === 'blocked', + negativeEgressObservedOutcome: payload.negativeEgressObservedOutcome, containmentMechanism: 'process-group', commandBindingPassed, parentageProbePassed, @@ -305,6 +422,7 @@ export async function exerciseConfinementProbe(probe: ConfinementProbe, clock: C provenIsolationStrength: result.provenIsolationStrength, provenBy: positive ? 'exercised-confinement-proof' : undefined, containmentMechanism: result.containmentMechanism, + negativeEgressObservedOutcome: result.negativeEgressObservedOutcome, failureToken: positive ? undefined : 'containment-unproven', }; } diff --git a/packages/jig-sdk/src/providers/real/host.ts b/packages/jig-sdk/src/providers/real/host.ts index f2a1e30..85c6545 100644 --- a/packages/jig-sdk/src/providers/real/host.ts +++ b/packages/jig-sdk/src/providers/real/host.ts @@ -1,6 +1,10 @@ import { type Clock, systemClock } from '../../clock.js'; import type { CapabilityAttestation, ExecutionHostPort, HostAttestation, IsolationStrength } from '../../ports.js'; -import { type ApprovedSubstrateManifest, validateSubstrateRequest } from '../../substrate.js'; +import { + type ApprovedSubstrateManifest, + SubstrateAuthorizationError, + validateSubstrateRequest, +} from '../../substrate.js'; import { type ConfinementProbe, createMacosProcessGroupConfinementProbe, @@ -42,8 +46,17 @@ function isolationStrengthRank(strength: IsolationStrength): number { export async function createRealExecutionHost(options: RealExecutionHostOptions): Promise { const capability = options.capability ?? 'filesystem-edit'; const runContext = options.runContext ?? 'local-real-host'; - for (const request of options.probe.substrateRequests ?? []) { - if (options.substrateManifest) { + const probeSubstrateRequests = options.probe.substrateRequests ?? []; + if (probeSubstrateRequests.length > 0) { + // Fail closed: a probe that declares substrate requests without an approved manifest must + // refuse — silently skipping validation would let an unvetted request through unrecorded. + if (!options.substrateManifest) { + throw new SubstrateAuthorizationError( + probeSubstrateRequests[0], + `substrate-escalation: probe declared ${String(probeSubstrateRequests.length)} substrate request(s) but no approved substrate manifest was supplied`, + ); + } + for (const request of probeSubstrateRequests) { validateSubstrateRequest(options.substrateManifest, request); } } @@ -62,6 +75,7 @@ export async function createRealExecutionHost(options: RealExecutionHostOptions) provenIsolationStrength, provenBy: proof.positive && !overstated ? proof.provenBy : undefined, containmentMechanism: proof.containmentMechanism, + negativeEgressObservedOutcome: proof.negativeEgressObservedOutcome, failureToken: overstated ? 'isolation-strength-overstated' : proof.failureToken, }; @@ -81,6 +95,7 @@ export function createControlledStrongConfinementProbe(observedAt = new Date().t freshnessWindowMs: 60_000, terminationProvedEmpty: true, negativeEgressProbePassed: true, + negativeEgressObservedOutcome: 'blocked', containmentMechanism: 'process-group', commandBindingPassed: true, parentageProbePassed: true, diff --git a/packages/jig-sdk/src/substrate.ts b/packages/jig-sdk/src/substrate.ts index 9739ff6..69fad2c 100644 --- a/packages/jig-sdk/src/substrate.ts +++ b/packages/jig-sdk/src/substrate.ts @@ -27,8 +27,8 @@ export interface ApprovedSubstrateManifest { export class SubstrateAuthorizationError extends Error { readonly request: SubstrateRequest; - constructor(request: SubstrateRequest) { - super(`substrate-escalation: ${request.kind} request is outside the approved manifest tuple`); + constructor(request: SubstrateRequest, message?: string) { + super(message ?? `substrate-escalation: ${request.kind} request is outside the approved manifest tuple`); this.name = 'SubstrateAuthorizationError'; this.request = request; } diff --git a/packages/jig-sdk/tests/providers.real-confinement.p4.unit.test.ts b/packages/jig-sdk/tests/providers.real-confinement.p4.unit.test.ts index 1773f5d..66fe285 100644 --- a/packages/jig-sdk/tests/providers.real-confinement.p4.unit.test.ts +++ b/packages/jig-sdk/tests/providers.real-confinement.p4.unit.test.ts @@ -4,10 +4,13 @@ import { PassThrough } from 'node:stream'; import { test } from 'vitest'; import { fixedClock } from '../src/clock.js'; import { + attemptNegativeEgressDial, + classifyNegativeEgressDialError, createMacosProcessGroupConfinementProbe, exerciseConfinementProbe, localProcessGroupProbeCommand, localProcessGroupProbeSubstrateRequests, + type NegativeEgressOutcome, type ProcessGroupConfinementRuntime, type ProcessGroupProbeChildProcess, processGroupIsEmptyForTest, @@ -24,6 +27,7 @@ interface FakeRuntimeOptions { isEmptySequence?: boolean[]; killErrorCode?: string; loopbackReachable?: boolean; + negativeEgressOutcome?: NegativeEgressOutcome; } function fakeChild(): ProcessGroupProbeChildProcess & EventEmitter { @@ -80,6 +84,7 @@ function fakeRuntime(options: FakeRuntimeOptions = {}): { observedExecArgv: options.observedExecArgv ?? localProcessGroupProbeCommand('/usr/bin/node').slice(1), parentPid: options.parentPid ?? 9001, loopbackReachable: options.loopbackReachable ?? true, + negativeEgressObservedOutcome: options.negativeEgressOutcome ?? 'ambiguous', }; (child.stdout as PassThrough).emit('data', Buffer.from(`${JSON.stringify(payload)}\n`, 'utf8')); } @@ -121,6 +126,7 @@ test('P04-AC-1: the macOS process-group probe proves honest weak containment fro assert.strictEqual(result.parentageProbePassed, true); assert.strictEqual(result.terminationProvedEmpty, true); assert.strictEqual(result.negativeEgressProbePassed, false); + assert.strictEqual(result.negativeEgressObservedOutcome, 'ambiguous'); } finally { if (previousSecret === undefined) { delete process.env.AMBIENT_PROBE_SECRET; @@ -407,3 +413,146 @@ test('P04-AC-2: missing base-proof elements keep exercised weak claims non-posit assert.strictEqual(proof.failureToken, 'containment-unproven'); } }); + +test('F8: an observed-blocked negative-egress attempt records a passed probe with its observation', async () => { + const state = fakeRuntime({ negativeEgressOutcome: 'blocked' }); + const result = await createMacosProcessGroupConfinementProbe(state.runtime).run(); + + assert.strictEqual(result.negativeEgressProbePassed, true); + assert.strictEqual(result.negativeEgressObservedOutcome, 'blocked'); +}); + +test('F8: an observed-open negative-egress attempt records an honest failed probe', async () => { + const state = fakeRuntime({ negativeEgressOutcome: 'open' }); + const result = await createMacosProcessGroupConfinementProbe(state.runtime).run(); + + assert.strictEqual(result.negativeEgressProbePassed, false); + assert.strictEqual(result.negativeEgressObservedOutcome, 'open'); +}); + +test('F8: exercised proof threads the negative-egress observation into the confinement proof', async () => { + const proof = await exerciseConfinementProbe( + { + run: async () => ({ + reportedIsolationStrength: 'weak' as const, + observedAt: '2026-07-06T09:00:00.000Z', + freshnessWindowMs: 60_000, + terminationProvedEmpty: true, + negativeEgressProbePassed: false, + negativeEgressObservedOutcome: 'open' as const, + containmentMechanism: 'process-group' as const, + commandBindingPassed: true, + parentageProbePassed: true, + provenIsolationStrength: 'weak' as const, + }), + }, + fixedClock('2026-07-06T09:00:00.500Z'), + ); + + assert.strictEqual(proof.positive, true); + assert.strictEqual(proof.negativeEgressObservedOutcome, 'open'); +}); + +class FakeDialSocket extends EventEmitter { + destroyed = false; + timeoutMs: number | undefined; + private timeoutCallback: (() => void) | undefined; + + destroy(): void { + this.destroyed = true; + } + + setTimeout(ms: number, callback: () => void): this { + this.timeoutMs = ms; + this.timeoutCallback = callback; + return this; + } + + fireTimeout(): void { + this.timeoutCallback?.(); + } +} + +function fakeDialer(): { socket: FakeDialSocket; createConnection: typeof import('node:net').createConnection } { + const socket = new FakeDialSocket(); + const createConnection = (() => socket) as unknown as typeof import('node:net').createConnection; + return { socket, createConnection }; +} + +function codedError(code: string): Error & { code: string } { + const error = new Error(code) as Error & { code: string }; + error.code = code; + return error; +} + +test('F8: the real dialer classifies an established connection as observed-open', async () => { + const { socket, createConnection } = fakeDialer(); + const options = { host: '203.0.113.1', port: 443, timeoutMs: 1_500 }; + const pending = attemptNegativeEgressDial(options, createConnection); + socket.emit('connect'); + + assert.strictEqual(await pending, 'open'); + assert.strictEqual(socket.destroyed, true); + assert.strictEqual(socket.timeoutMs, 1_500); +}); + +test('F8: the real dialer classifies a peer refusal as observed-open because packets round-tripped', async () => { + const { socket, createConnection } = fakeDialer(); + const pending = attemptNegativeEgressDial({ host: '203.0.113.1', port: 443, timeoutMs: 1_500 }, createConnection); + socket.emit('error', codedError('ECONNREFUSED')); + + assert.strictEqual(await pending, 'open'); +}); + +test('F8: the real dialer classifies only explicit local denial as observed-blocked for TEST-NET', async () => { + for (const code of ['EPERM', 'EACCES']) { + const { socket, createConnection } = fakeDialer(); + const pending = attemptNegativeEgressDial({ host: '203.0.113.1', port: 443, timeoutMs: 1_500 }, createConnection); + socket.emit('error', codedError(code)); + + assert.strictEqual(await pending, 'blocked', `expected ${code} to classify as blocked`); + } +}); + +test('P1: the contained child payload, not the parent runtime, owns the negative-egress observation', async () => { + const state = fakeRuntime({ negativeEgressOutcome: 'blocked' }); + const result = await createMacosProcessGroupConfinementProbe(state.runtime).run(); + + assert.strictEqual(result.negativeEgressObservedOutcome, 'blocked'); + assert.deepStrictEqual(state.killSignals, ['SIGTERM', 'SIGKILL']); +}); + +test('P2: route-unreachable outcomes against TEST-NET stay ambiguous, not blocked', async () => { + for (const code of ['ENETUNREACH', 'EHOSTUNREACH', 'ENETDOWN']) { + const { socket, createConnection } = fakeDialer(); + const pending = attemptNegativeEgressDial({ host: '203.0.113.1', port: 443, timeoutMs: 1_500 }, createConnection); + socket.emit('error', codedError(code)); + + assert.strictEqual(await pending, 'ambiguous', `expected ${code} to classify as ambiguous`); + } +}); + +test('F8: the real dialer classifies a silent timeout as ambiguous, never as blocked', async () => { + const { socket, createConnection } = fakeDialer(); + const pending = attemptNegativeEgressDial({ host: '203.0.113.1', port: 443, timeoutMs: 1_500 }, createConnection); + socket.fireTimeout(); + + assert.strictEqual(await pending, 'ambiguous'); +}); + +test('F8: the real dialer settles once — later signals cannot rewrite the observed outcome', async () => { + const { socket, createConnection } = fakeDialer(); + const pending = attemptNegativeEgressDial({ host: '203.0.113.1', port: 443, timeoutMs: 1_500 }, createConnection); + socket.emit('connect'); + socket.emit('error', codedError('EPERM')); + socket.fireTimeout(); + + assert.strictEqual(await pending, 'open'); +}); + +test('F8: unrecognized dial errors classify as ambiguous', () => { + assert.strictEqual(classifyNegativeEgressDialError(codedError('ESOMETHINGELSE')), 'ambiguous'); + assert.strictEqual(classifyNegativeEgressDialError(new Error('no code at all')), 'ambiguous'); + assert.strictEqual(classifyNegativeEgressDialError('not-an-error'), 'ambiguous'); + assert.strictEqual(classifyNegativeEgressDialError(codedError('ECONNRESET')), 'open'); +}); diff --git a/packages/jig-sdk/tests/providers.real-host.p6.unit.test.ts b/packages/jig-sdk/tests/providers.real-host.p6.unit.test.ts index c59feb8..f993f53 100644 --- a/packages/jig-sdk/tests/providers.real-host.p6.unit.test.ts +++ b/packages/jig-sdk/tests/providers.real-host.p6.unit.test.ts @@ -108,6 +108,7 @@ test('P6-AC-2: honest weak proof remains positive for a weak real-host posture', freshnessWindowMs: 1_000, terminationProvedEmpty: true, negativeEgressProbePassed: false, + negativeEgressObservedOutcome: 'ambiguous', containmentMechanism: 'process-group', commandBindingPassed: true, parentageProbePassed: true, @@ -119,6 +120,7 @@ test('P6-AC-2: honest weak proof remains positive for a weak real-host posture', assert.strictEqual(host.describe().capabilityAttestations[0]?.positive, true); assert.strictEqual(host.describe().capabilityAttestations[0]?.reportedIsolationStrength, 'weak'); assert.strictEqual(host.describe().capabilityAttestations[0]?.provenIsolationStrength, 'weak'); + assert.strictEqual(host.describe().capabilityAttestations[0]?.negativeEgressObservedOutcome, 'ambiguous'); }); test('P04-AC-1: host defaults preserve run context, capability, and none proof reporting', async () => { @@ -242,6 +244,41 @@ test('P04-AC-4: real host validates probe substrate requests against the manifes ); }); +test('F9: real host fails closed when a probe declares substrate requests without a manifest', async () => { + let probeExercised = false; + const declaredRequest = { kind: 'egress' as const, value: 'tcp:127.0.0.1:ephemeral' }; + + await assert.rejects( + () => + createRealExecutionHost({ + clock: fixedClock('2026-07-03T10:00:00.000Z'), + probe: { + substrateRequests: [declaredRequest], + run: async () => { + probeExercised = true; + return { + observedAt: '2026-07-03T10:00:00.000Z', + freshnessWindowMs: 1_000, + terminationProvedEmpty: true, + negativeEgressProbePassed: false, + containmentMechanism: 'process-group' as const, + commandBindingPassed: true, + parentageProbePassed: true, + provenIsolationStrength: 'weak' as const, + }; + }, + }, + }), + (error: unknown) => { + assert.ok(error instanceof SubstrateAuthorizationError); + assert.match(error.message, /no approved substrate manifest was supplied/); + assert.deepStrictEqual(error.request, declaredRequest); + return true; + }, + ); + assert.strictEqual(probeExercised, false); +}); + test('P6-AC-2: controlled strong confinement probe remains available for hermetic doubles', async () => { const host = await createRealExecutionHost({ clock: fixedClock('2026-07-03T10:00:00.000Z'), diff --git a/packages/jig-sdk/tests/smoke/real-host.p4.smoke.test.ts b/packages/jig-sdk/tests/smoke/real-host.p4.smoke.test.ts index 1476df0..48ab36d 100644 --- a/packages/jig-sdk/tests/smoke/real-host.p4.smoke.test.ts +++ b/packages/jig-sdk/tests/smoke/real-host.p4.smoke.test.ts @@ -1,6 +1,7 @@ import assert from 'node:assert'; import { describe, test } from 'vitest'; import { composeReferenceRun } from '../../src/bootstrap.js'; +import { createMacosProcessGroupConfinementProbe } from '../../src/providers/real/confinement.js'; import type { ConfigDoc, PlanInstance } from '../../src/types.js'; const planInstance: PlanInstance = { @@ -38,5 +39,29 @@ describe.skipIf(!process.env.EVRUN_SMOKE)('P04 real-host smoke', () => { assert.strictEqual(attestation.provenIsolationStrength, 'weak'); assert.strictEqual(attestation.positive, true); assert.strictEqual(attestation.failureToken, undefined); + // The negative-egress check must have been exercised and its observation recorded. On this + // host egress is not blocked, so the honest observation can never be 'blocked' — the + // assertion is on observed reality and cannot be satisfied by a refusal (a probe failure + // rejects composition, failing the test). + assert.ok(attestation.negativeEgressObservedOutcome); + assert.notStrictEqual(attestation.negativeEgressObservedOutcome, 'blocked'); + }); + + test('real negative-egress dial observes an honest not-blocked outcome on an open-egress host', async () => { + if (process.platform !== 'darwin') { + return; + } + + const result = await createMacosProcessGroupConfinementProbe().run(); + + // The real dialer ran: an observation must exist, and on this open-egress host it must be + // 'open' (a response round-tripped) or 'ambiguous' (silent timeout) — never 'blocked'. + // Consequently the pass/fail boolean stays honestly false and posture remains proven weak; + // 'strong' is unreachable on this host and that is the correct recorded truth. + assert.ok(result.negativeEgressObservedOutcome); + assert.ok(['open', 'ambiguous'].includes(result.negativeEgressObservedOutcome)); + assert.strictEqual(result.negativeEgressProbePassed, false); + assert.strictEqual(result.provenIsolationStrength, 'weak'); + assert.strictEqual(result.containmentMechanism, 'process-group'); }); });