mtech-ai-local-runner 0.1.73 → 0.1.74
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -0
- package/dist/src/adapters/claude-code/prompt-compiler.js +3 -1
- package/dist/src/adapters/codex/exec-invocation.js +24 -0
- package/dist/src/adapters/codex/exec-launch-host.js +5 -0
- package/dist/src/adapters/codex/exec-runtime-options.js +1 -1
- package/dist/src/adapters/codex/prompt-compiler.js +3 -1
- package/dist/src/asks/runtime.js +22 -1
- package/dist/src/daemon/execution-coordinator.js +65 -9
- package/dist/src/daemon/manual-codex.js +2 -2
- package/dist/src/daemon/runtime.js +20 -9
- package/dist/src/gateway-client.js +65 -5
- package/dist/src/protocol.js +33 -0
- package/dist/src/publication/server-ci-host.js +5 -6
- package/dist/src/rooms/activity.js +84 -0
- package/dist/src/rooms/notifier.js +21 -0
- package/dist/src/rooms/store.js +56 -0
- package/dist/src/safety/claude-managed-scope.js +7 -19
- package/dist/src/safety/claude-release.js +0 -2
- package/dist/src/safety/claude-review-evidence.js +2 -2
- package/dist/src/safety/codex-exec-production-host.js +15 -2
- package/dist/src/safety/job-tool-policy.js +4 -3
- package/dist/src/verification/continuation-runner-contract.js +228 -0
- package/dist/src/verification/p1-execution-host.js +25 -6
- package/dist/src/verification/p1-host-composition.js +2 -0
- package/dist/src/verification/reviewed-runner-contract.js +7 -0
- package/dist/src/verification/runner-observed-verifier.js +29 -5
- package/dist/src/workspace/registered-submodules.js +204 -0
- package/dist/src/workspace/workspace-manager.js +128 -18
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,3 +1,31 @@
|
|
|
1
|
+
## 0.1.74: exact-parent reviewed correction attempts (P3D)
|
|
2
|
+
|
|
3
|
+
This local source supports newly approved executable V3 launches behind the
|
|
4
|
+
Gateway's default-off `correction_execution_enabled` switch. It does not activate
|
|
5
|
+
an installed runner or upgrade older V1/V2/unavailable-V3 approvals.
|
|
6
|
+
|
|
7
|
+
Authenticated claim context arrives before checkout. The approved task package,
|
|
8
|
+
canonical repository lease and binding retain ROOT identity; each attempt checks
|
|
9
|
+
out its exact Gateway-reviewed parent P and emits fresh context-bound receipts,
|
|
10
|
+
snapshot and handoff. The original approval fingerprint differs from a successor
|
|
11
|
+
job's reservation fingerprint and remains preserved through at most three attempts.
|
|
12
|
+
Native launch seals the exact context and binding in the host-owned immutable
|
|
13
|
+
plan, measures P immediately before dispatch, and keeps existing ROOT security
|
|
14
|
+
receipt semantics. A replayed binding or uncertain model outcome pauses.
|
|
15
|
+
|
|
16
|
+
A measured trusted host registration supplies registered submodule providers.
|
|
17
|
+
The runner fetches exact P gitlinks through owned mirrors and refuses unknown
|
|
18
|
+
providers, metadata redirects and nested P-child gitlinks. No profile is installed
|
|
19
|
+
by these source changes. Correction findings are bounded untrusted data scoped to
|
|
20
|
+
the canonical bundle paths; they cannot expand authority or become commands.
|
|
21
|
+
|
|
22
|
+
Local fake transport/provider/process tests are distinct from real CI or model
|
|
23
|
+
acceptance. A final release commit/version, package manifest/hash and compatible
|
|
24
|
+
reviewed host registration remain pending delivery approval. See the
|
|
25
|
+
[P3D-4 review](../../docs/superpowers/reviews/2026-10-30-local-runner-p3d4-implementation-review.md)
|
|
26
|
+
for verification and compatibility limitations. No setup/update/restart is needed
|
|
27
|
+
to inspect this source tree.
|
|
28
|
+
|
|
1
29
|
## 0.1.60: answers go back to the session that asked
|
|
2
30
|
|
|
3
31
|
An answer to a colleague ask now goes first to the session that made the ask, when the asking AI passed its client
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { renderBundleCorrectionBrief } from '../../verification/continuation-runner-contract.js';
|
|
1
2
|
import { compilePrompt as compileBrief } from "../prompt-compiler.js";
|
|
2
3
|
import { LOCAL_TOOL_GUIDANCE, repositoryScope } from "../local-tool-prompt.js";
|
|
3
4
|
// Claude's working directory is the job's private control directory, so without these rules it sends that directory's
|
|
@@ -16,5 +17,6 @@ const CLAUDE_PATH_RULES = "## Local tool path rules\n" +
|
|
|
16
17
|
"command: send a corrected call instead of concluding that source is unavailable. LOCAL_TOOL_FAILED means the call " +
|
|
17
18
|
"failed for another reason, such as a file that does not exist.";
|
|
18
19
|
export function compilePrompt(taskPackage, options) {
|
|
19
|
-
|
|
20
|
+
const correction = renderBundleCorrectionBrief(taskPackage);
|
|
21
|
+
return `${compileBrief(taskPackage, options)}\n\n${repositoryScope(taskPackage)}\n\n${LOCAL_TOOL_GUIDANCE}\n\n${CLAUDE_PATH_RULES}${correction}`;
|
|
20
22
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { validateAttemptContext, attemptContextFingerprint } from '../../verification/continuation-runner-contract.js';
|
|
1
2
|
import { UNMANAGED_EXEC_MODE } from './unmanaged-scope.js';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import { CODEX_GLOBAL_INSTRUCTION_WRAPPER, globalInstructionProfile } from './global-instructions.js';
|
|
@@ -10,10 +11,18 @@ export const EXEC_MODE = 'supported_cli_exec_policy_v1';
|
|
|
10
11
|
export const isReleaseExecMode = (mode) => mode === EXEC_RELEASE_MODE || mode === UNMANAGED_EXEC_MODE;
|
|
11
12
|
const plans = new WeakMap();
|
|
12
13
|
const consumed = new WeakSet();
|
|
14
|
+
const executionAttempts = new WeakMap();
|
|
13
15
|
const fail = () => new Error('CODEX_EXEC_PLAN_REJECTED');
|
|
14
16
|
export function buildOfficialExecPlan(input) {
|
|
15
17
|
try {
|
|
16
18
|
const i = structuredClone(input.identity), inv = structuredClone(input.invocation);
|
|
19
|
+
const attempt = input.executionAttempt === undefined ? undefined : validateAttemptContext(input.executionAttempt);
|
|
20
|
+
if ((attempt === undefined) !== (input.executionAttemptFingerprint === undefined) || (attempt === undefined) !== (input.executionBindingId === undefined))
|
|
21
|
+
throw fail();
|
|
22
|
+
if (attempt && (typeof input.executionBindingId !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(input.executionBindingId)))
|
|
23
|
+
throw fail();
|
|
24
|
+
if (attempt && (attemptContextFingerprint(attempt) !== input.executionAttemptFingerprint || attempt.job_id !== i.jobId || attempt.generation !== i.generation || attempt.contract_base_commit_sha !== i.sourceSha || attempt.contract_fingerprint !== i.contractFingerprint || (attempt.attempt_number === 1 && attempt.launch_approval_fingerprint !== i.launchFingerprint)))
|
|
25
|
+
throw fail();
|
|
17
26
|
if (input.policyMode !== undefined && !isReleaseExecMode(input.policyMode))
|
|
18
27
|
throw fail();
|
|
19
28
|
const keys = ['jobId', 'generation', 'actorId', 'runnerId', 'sourceSha', 'binarySha256', 'releaseManifestFingerprint', 'requirementsFingerprint', 'contractFingerprint', 'taskPackageFingerprint', 'launchFingerprint', 'commandsFingerprint', 'toolSchemaFingerprint', 'barrierFingerprint'];
|
|
@@ -36,6 +45,8 @@ export function buildOfficialExecPlan(input) {
|
|
|
36
45
|
const actual = isReleaseExecMode(input.policyMode) ? { binary: CODEX_GLOBAL_INSTRUCTION_WRAPPER, args: ['-p', globalInstructionProfile(accountRoot), input.binary, ...generated.args], env: generated.env } : { binary: input.binary, ...generated };
|
|
37
46
|
const executionNonce = randomBytes(32).toString('hex');
|
|
38
47
|
const plan = Object.freeze({ transport: EXEC_TRANSPORT, mode: input.policyMode ?? EXEC_MODE, executionNonce, identity: Object.freeze(i), modelId: inv.modelId, home: inv.env.HOME, accountRoot, credentialBackend: input.credentialBackend, binary: input.binary, cwd, endpoint: inv.endpoint.url, invocationFingerprint: hash({ ...actual }) });
|
|
48
|
+
if (attempt)
|
|
49
|
+
executionAttempts.set(plan, { fingerprint: input.executionAttemptFingerprint, bindingId: input.executionBindingId, sourceRoot: source });
|
|
39
50
|
plans.set(plan, structuredClone(actual));
|
|
40
51
|
return plan;
|
|
41
52
|
}
|
|
@@ -60,3 +71,16 @@ export function assertOfficialExecPlanAvailable(plan) {
|
|
|
60
71
|
if (consumed.has(plan))
|
|
61
72
|
throw fail();
|
|
62
73
|
}
|
|
74
|
+
/** Private plan seal is execution identity, separate from legacy ROOT approval
|
|
75
|
+
* and security receipt identity. Native host still verifies Gateway authority. */
|
|
76
|
+
export function assertOfficialExecAttempt(plan, context, fingerprint, bindingId, sourceRoot) {
|
|
77
|
+
requireOfficialExecPlan(plan);
|
|
78
|
+
const expected = executionAttempts.get(plan);
|
|
79
|
+
if (expected === undefined) {
|
|
80
|
+
if (context !== undefined || fingerprint !== undefined)
|
|
81
|
+
throw fail();
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (context === undefined || fingerprint !== expected.fingerprint || attemptContextFingerprint(context) !== expected.fingerprint || bindingId !== expected.bindingId || sourceRoot !== undefined && path.resolve(sourceRoot) !== expected.sourceRoot)
|
|
85
|
+
throw fail();
|
|
86
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { assertOfficialExecAttempt } from './exec-invocation.js';
|
|
1
2
|
import { performance } from 'node:perf_hooks';
|
|
2
3
|
import { PreparationClock, CODEX_PREPARATION_BUDGET_MS } from '../../safety/preparation-timing.js';
|
|
3
4
|
import { localEndpointObservations, localEndpointReadObservations } from '../../safety/local-tool-server.js';
|
|
@@ -42,6 +43,7 @@ export class CodexExecLaunchHost {
|
|
|
42
43
|
throw fail();
|
|
43
44
|
} }
|
|
44
45
|
assertContext(ctx, jobId) {
|
|
46
|
+
assertOfficialExecAttempt(this.plan, ctx.executionAttempt, ctx.executionAttemptFingerprint, ctx.bindingId);
|
|
45
47
|
const a = ctx.approvedTaskPackage, r = a?.security_requirements, b = ctx.taskPackage, execution = b?.execution;
|
|
46
48
|
if (isReleaseExecMode(this.plan.mode)) {
|
|
47
49
|
if (!r)
|
|
@@ -63,6 +65,7 @@ export class CodexExecLaunchHost {
|
|
|
63
65
|
async start(ctx, jobId, prompt, signal) {
|
|
64
66
|
if (this.used || this.stopped)
|
|
65
67
|
throw fail();
|
|
68
|
+
ctx = structuredClone(ctx);
|
|
66
69
|
this.assertAvailableContext(ctx, jobId);
|
|
67
70
|
claimOfficialExecPlan(this.plan);
|
|
68
71
|
this.ownsAttempt = true;
|
|
@@ -114,6 +117,8 @@ export class CodexExecLaunchHost {
|
|
|
114
117
|
this.inspection.beforeSpawn?.();
|
|
115
118
|
timing.mark('before_spawn_complete');
|
|
116
119
|
assertPreparationBudget();
|
|
120
|
+
this.current();
|
|
121
|
+
this.assertContext(ctx, jobId);
|
|
117
122
|
stage = 'codex_spawn';
|
|
118
123
|
timing.mark('spawn_requested');
|
|
119
124
|
const h = this.deps.processHost.spawn(invocation.binary, invocation.args, { cwd: this.plan.cwd, env: invocation.env });
|
|
@@ -99,7 +99,7 @@ export function buildCodexExecJobOptions(input) {
|
|
|
99
99
|
if (closed || signal.aborted)
|
|
100
100
|
throw reject();
|
|
101
101
|
stage = 'codex_invocation';
|
|
102
|
-
const plan = buildOfficialExecPlan({ policyMode: admitted.release.securityMode, binary: admitted.binary, credentialBackend: admitted.release.credentialBackend, identity: { jobId: input.jobId, generation: ctx.generation, actorId: admitted.actorId, runnerId: admitted.runnerId, sourceSha: String(approved.repository.base_commit_sha), binarySha256: admitted.release.binarySha256, releaseManifestFingerprint: String(requirements.release_manifest_sha256), requirementsFingerprint: String(requirements.fingerprint), contractFingerprint: ctx.securityBinding.contractFingerprint, taskPackageFingerprint: ctx.securityBinding.taskPackageFingerprint, launchFingerprint: ctx.securityBinding.launchFingerprint, commandsFingerprint: commandRegistryFingerprint(admitted.release.commands), toolSchemaFingerprint: fingerprint({ tools: localToolInventory() }), barrierFingerprint: admitted.release.fixturesFingerprint }, invocation: { modelId: ctx.resolvedModelId, worktreeRoot: cwd, sourceRoot: source, schemaPath: schema, endpoint, env, scratch } });
|
|
102
|
+
const plan = buildOfficialExecPlan({ executionAttempt: ctx.executionAttempt, executionAttemptFingerprint: ctx.executionAttemptFingerprint, ...(ctx.executionAttempt ? { executionBindingId: ctx.bindingId } : {}), policyMode: admitted.release.securityMode, binary: admitted.binary, credentialBackend: admitted.release.credentialBackend, identity: { jobId: input.jobId, generation: ctx.generation, actorId: admitted.actorId, runnerId: admitted.runnerId, sourceSha: String(approved.repository.base_commit_sha), binarySha256: admitted.release.binarySha256, releaseManifestFingerprint: String(requirements.release_manifest_sha256), requirementsFingerprint: String(requirements.fingerprint), contractFingerprint: ctx.securityBinding.contractFingerprint, taskPackageFingerprint: ctx.securityBinding.taskPackageFingerprint, launchFingerprint: ctx.securityBinding.launchFingerprint, commandsFingerprint: commandRegistryFingerprint(admitted.release.commands), toolSchemaFingerprint: fingerprint({ tools: localToolInventory() }), barrierFingerprint: admitted.release.fixturesFingerprint }, invocation: { modelId: ctx.resolvedModelId, worktreeRoot: cwd, sourceRoot: source, schemaPath: schema, endpoint, env, scratch } });
|
|
103
103
|
gate = new ExecToolGate(plan);
|
|
104
104
|
const host = new CodexExecLaunchHost(plan, { processHost, inspectRelease: p => job.inspect(p), execTools: gate, revokeTools: () => { gate?.revoke(); server?.revoke(); policy?.revoke(); job?.revoke(); }, closeTools: dispose });
|
|
105
105
|
launchHost = host;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { renderBundleCorrectionBrief } from '../../verification/continuation-runner-contract.js';
|
|
1
2
|
import { compilePrompt as compileBrief } from "../prompt-compiler.js";
|
|
2
3
|
import { LOCAL_TOOL_GUIDANCE, repositoryScope } from "../local-tool-prompt.js";
|
|
3
4
|
export function compileCodexPrompt(taskPackage, label) {
|
|
4
|
-
|
|
5
|
+
const correction = renderBundleCorrectionBrief(taskPackage);
|
|
6
|
+
return `${compileBrief(taskPackage, { workItemLabel: label, progressReporting: "native_messages" })}\n\n${repositoryScope(taskPackage)}\n\n${LOCAL_TOOL_GUIDANCE}${correction}`;
|
|
5
7
|
}
|
|
6
8
|
export const CODEX_REPORT_SCHEMA = {
|
|
7
9
|
type: "object", additionalProperties: false, required: ["status", "report", "evidence", "pending_work"],
|
package/dist/src/asks/runtime.js
CHANGED
|
@@ -21,6 +21,25 @@ const SHUTDOWN_WAIT_MS = 5000;
|
|
|
21
21
|
* would end. Bounded, so a heartbeat, and the job claim behind it, is never held up longer than this.
|
|
22
22
|
*/
|
|
23
23
|
export const ADVERTISEMENT_START_WAIT_MS = 2000;
|
|
24
|
+
/** Room notices use only a human prompt already arriving at the Ask hub; they never wake a model. */
|
|
25
|
+
export function withRoomNotice(handler, claim) {
|
|
26
|
+
return {
|
|
27
|
+
status: () => handler.status(),
|
|
28
|
+
watch: (request, watcher) => handler.watch(request, watcher),
|
|
29
|
+
event: async (request, client) => {
|
|
30
|
+
const text = await handler.event(request, client);
|
|
31
|
+
if (request.event !== 'prompt' || request.human !== true || client.gone)
|
|
32
|
+
return text;
|
|
33
|
+
try {
|
|
34
|
+
const notice = await claim();
|
|
35
|
+
return notice === null ? text : text === null ? notice : `${text}\n${notice}`;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return text;
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
24
43
|
function errorCode(error) {
|
|
25
44
|
const code = error?.code;
|
|
26
45
|
if (typeof code === "string" && /^[A-Z0-9_]{1,64}$/.test(code))
|
|
@@ -51,7 +70,9 @@ export class AskRuntime {
|
|
|
51
70
|
openSession: deps.openSession ?? createSessionOpener({ home: deps.env.HOME ?? "" }),
|
|
52
71
|
presenceRecheckMs: deps.presenceRecheckMs,
|
|
53
72
|
onChange: () => this.flushSoon(), log: event => this.log(event) });
|
|
54
|
-
this.hub = new AskHub({ socketPath: hubSocketPath(deps.stateRoot),
|
|
73
|
+
this.hub = new AskHub({ socketPath: hubSocketPath(deps.stateRoot),
|
|
74
|
+
handler: deps.claimRoomNotice ? withRoomNotice(this.inbox, deps.claimRoomNotice) : this.inbox,
|
|
75
|
+
log: event => this.log(event) });
|
|
55
76
|
}
|
|
56
77
|
/** True while the hub listens; only then are deliveries taken and ask_inbox advertised. */
|
|
57
78
|
get ready() {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { assertClaimedExecutionAttempt, assertBoundExecutionAttempt } from '../verification/continuation-runner-contract.js';
|
|
1
2
|
import { completionRejectionDiagnostic } from '../adapters/completion-report.js';
|
|
2
3
|
import { publicationError } from '../publication/failure.js';
|
|
3
4
|
import { PublicationOutcomeUnknownError } from '../publication/publisher.js';
|
|
@@ -9,7 +10,7 @@ import { requireSecurityRequirements } from '../safety/security-requirements.js'
|
|
|
9
10
|
import { GatewayToolError } from "../gateway-client.js";
|
|
10
11
|
import { createHash } from "node:crypto";
|
|
11
12
|
import { completionReportDiagnostic } from "../adapters/completion-report.js";
|
|
12
|
-
import { PROTOCOL_VERSION, isTerminalEventType, validateEvent, MAX_EVENT_SUMMARY_CHARS } from "../protocol.js";
|
|
13
|
+
import { PROTOCOL_VERSION, isTerminalEventType, validateEvent, validateAttemptEvidence, validateAttemptHandoff, MAX_EVENT_SUMMARY_CHARS } from "../protocol.js";
|
|
13
14
|
import { DAEMON_BLOCKED_REASONS, DAEMON_CANCELLED_REASONS } from "../state-store.js";
|
|
14
15
|
import { PreparationDeadlineError, PreparationLease, validateJobLease } from './preparation-lease.js';
|
|
15
16
|
function canonical(value) {
|
|
@@ -66,8 +67,24 @@ export class ExecutionCoordinator {
|
|
|
66
67
|
constructor(deps) {
|
|
67
68
|
this.deps = deps;
|
|
68
69
|
}
|
|
69
|
-
|
|
70
|
-
|
|
70
|
+
active;
|
|
71
|
+
execute(job) {
|
|
72
|
+
job = immutable(structuredClone(job));
|
|
73
|
+
const key = `${job.id}:${job.generation}`;
|
|
74
|
+
if (this.active) {
|
|
75
|
+
if (this.active.key !== key)
|
|
76
|
+
return Promise.reject(Error('EXECUTION_ALREADY_ACTIVE'));
|
|
77
|
+
if (this.active.identity !== fingerprint(job))
|
|
78
|
+
return Promise.reject(Error('EXECUTION_CLAIM_MISMATCH'));
|
|
79
|
+
return this.active.promise;
|
|
80
|
+
}
|
|
81
|
+
const promise = Promise.resolve().then(() => this.executeOwned(job));
|
|
82
|
+
this.active = { key, identity: fingerprint(job), promise };
|
|
83
|
+
void promise.finally(() => { if (this.active?.promise === promise)
|
|
84
|
+
this.active = undefined; }).catch(() => { });
|
|
85
|
+
return promise;
|
|
86
|
+
}
|
|
87
|
+
async executeOwned(job) {
|
|
71
88
|
let stage = "package";
|
|
72
89
|
let workspace;
|
|
73
90
|
let adapter;
|
|
@@ -77,8 +94,9 @@ export class ExecutionCoordinator {
|
|
|
77
94
|
const isLocalFix = job.taskPackage.local_fix_scope !== undefined;
|
|
78
95
|
const isP1 = hasP1(job.taskPackage);
|
|
79
96
|
const envelope = job.taskPackage.verification;
|
|
80
|
-
const runnerBinding =
|
|
97
|
+
const runnerBinding = ['local-runner-verification-v2', 'local-runner-verification-v3'].includes(String(envelope?.schema_version)) ? envelope?.runner_binding : envelope;
|
|
81
98
|
const isP2 = isP1 && runnerBinding?.gateway_verification !== undefined;
|
|
99
|
+
let executionAttempt;
|
|
82
100
|
let p1Result;
|
|
83
101
|
let p2Handoff;
|
|
84
102
|
const p1ReceiptIds = [];
|
|
@@ -89,6 +107,19 @@ export class ExecutionCoordinator {
|
|
|
89
107
|
const step = (operation) => preparation.step(operation);
|
|
90
108
|
let bindingAttempted = false;
|
|
91
109
|
try {
|
|
110
|
+
try {
|
|
111
|
+
executionAttempt = assertClaimedExecutionAttempt(job);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
throw new GatewayToolError('Continuation execution is unavailable', 'local_agent_jobs_wait', 'CORRECTION_EXECUTION_UNAVAILABLE');
|
|
115
|
+
}
|
|
116
|
+
// Trusted host admission and measurement precede checkout. Package/schema/hash alone is never authority.
|
|
117
|
+
if (executionAttempt) {
|
|
118
|
+
if (!this.deps.p1Host?.assertCurrent || !this.deps.gateway.readCommands || !this.deps.gateway.heartbeat)
|
|
119
|
+
throw Error('CORRECTION_HOST_INCOMPATIBLE');
|
|
120
|
+
this.deps.p1Host.assertEligible(job);
|
|
121
|
+
await step(() => this.deps.p1Host.assertCurrent(job));
|
|
122
|
+
}
|
|
92
123
|
await preparation.start();
|
|
93
124
|
const pkg = job.taskPackage;
|
|
94
125
|
if (isP1) {
|
|
@@ -116,8 +147,9 @@ export class ExecutionCoordinator {
|
|
|
116
147
|
repositoryKey: text(repository?.key, "repository key"),
|
|
117
148
|
providerUrl: text(repository?.provider_url, "repository provider URL"),
|
|
118
149
|
baseCommitSha: text(repository?.base_commit_sha, "repository SHA"),
|
|
119
|
-
...(isLocalFix || isP1 ? { workingBranch: text(repository?.working_branch, "approved fix branch") } : {}),
|
|
120
|
-
...(isP1 ? { p1WorkingBranch: text(repository?.working_branch, 'approved P1 branch') } : {})
|
|
150
|
+
...(isLocalFix || isP1 ? { workingBranch: executionAttempt?.attempt_working_branch ?? text(repository?.working_branch, "approved fix branch") } : {}),
|
|
151
|
+
...(isP1 ? { p1WorkingBranch: executionAttempt?.attempt_working_branch ?? text(repository?.working_branch, 'approved P1 branch') } : {}),
|
|
152
|
+
...(executionAttempt ? { attemptContext: executionAttempt, attemptContextFingerprint: job.executionAttemptFingerprint } : {})
|
|
121
153
|
}));
|
|
122
154
|
stage = "adapter";
|
|
123
155
|
adapter = await preparation.step(() => this.deps.registry.select(adapterId, adapterVersion, { worktreePath: workspace.path, jobId: job.id }), late => late.cancel());
|
|
@@ -137,6 +169,10 @@ export class ExecutionCoordinator {
|
|
|
137
169
|
preparation.beginBinding();
|
|
138
170
|
bindingAttempted = true;
|
|
139
171
|
binding = await step(() => this.deps.gateway.createBinding({ ...job, checkoutPath: workspace.path }, preparation.signal));
|
|
172
|
+
assertBoundExecutionAttempt(job, binding.taskPackage, binding.iterationId);
|
|
173
|
+
if (executionAttempt && binding.bindingReplayed !== false)
|
|
174
|
+
throw Error('CORRECTION_BINDING_RECONCILIATION_REQUIRED');
|
|
175
|
+
binding = immutable(structuredClone(binding));
|
|
140
176
|
if (isP1)
|
|
141
177
|
assertP1Bindings(pkg, binding.taskPackage, true);
|
|
142
178
|
await step(() => this.deps.state.save({ jobId: job.id, generation: job.generation, leaseToken: job.leaseToken, phase: "running", bindingId: binding.bindingId, workspaceManifest: workspace.manifest }));
|
|
@@ -474,8 +510,25 @@ export class ExecutionCoordinator {
|
|
|
474
510
|
return session;
|
|
475
511
|
}));
|
|
476
512
|
}
|
|
513
|
+
if (executionAttempt) {
|
|
514
|
+
await readControls();
|
|
515
|
+
requireControlFence();
|
|
516
|
+
if (!requestedTerminal) {
|
|
517
|
+
const renewed = await fenced(this.deps.gateway.heartbeat(binding.bindingId, job, controlAbort.signal));
|
|
518
|
+
requireControlFence();
|
|
519
|
+
const expiry = validateJobLease(renewed, job, true);
|
|
520
|
+
if (!['running', 'review_pending'].includes(renewed.job.status))
|
|
521
|
+
throw Error('JOB_LEASE_FENCE_INVALID');
|
|
522
|
+
this.deps.gateway.confirmJobLease?.(job, expiry);
|
|
523
|
+
leaseExpiresAt = expiry;
|
|
524
|
+
armLeaseBoundary();
|
|
525
|
+
await fenced(this.deps.p1Host.assertCurrent(job));
|
|
526
|
+
await readControls();
|
|
527
|
+
requireControlFence();
|
|
528
|
+
}
|
|
529
|
+
}
|
|
477
530
|
if (!requestedTerminal) {
|
|
478
|
-
await fenced(Promise.race([adapter.start({ securityBinding: { launchFingerprint: job.launchFingerprint, contractFingerprint: job.gatewayContractFingerprint, taskPackageFingerprint: job.gatewayTaskPackageFingerprint }, generation: job.generation, approvedTaskPackage: job.taskPackage, bindingId: binding.bindingId, sessionId: binding.bindingId, taskPackage: binding.taskPackage, resolvedModelId: binding.resolvedModelId }), unconfirmedStop]));
|
|
531
|
+
await fenced(Promise.race([adapter.start({ ...(executionAttempt ? { executionAttempt, executionAttemptFingerprint: job.executionAttemptFingerprint } : {}), securityBinding: { launchFingerprint: job.launchFingerprint, contractFingerprint: job.gatewayContractFingerprint, taskPackageFingerprint: job.gatewayTaskPackageFingerprint }, generation: job.generation, approvedTaskPackage: job.taskPackage, bindingId: binding.bindingId, sessionId: binding.bindingId, taskPackage: binding.taskPackage, resolvedModelId: binding.resolvedModelId }), unconfirmedStop]));
|
|
479
532
|
}
|
|
480
533
|
executionStage = 'commands';
|
|
481
534
|
await readControls();
|
|
@@ -508,9 +561,12 @@ export class ExecutionCoordinator {
|
|
|
508
561
|
await freezeOwnedModel();
|
|
509
562
|
await readControls();
|
|
510
563
|
executionStage = 'p1_verify';
|
|
511
|
-
p1Result = await publicationStep(() => this.deps.p1Host.verify({ job, checkoutPath: workspace.path, bindingId: binding.bindingId, signal: verificationAbort.signal, current, freezeOwnedModel, assertModelFrozen,
|
|
564
|
+
p1Result = await publicationStep(() => this.deps.p1Host.verify({ ...(executionAttempt ? { executionAttempt } : {}), job, checkoutPath: workspace.path, bindingId: binding.bindingId, signal: verificationAbort.signal, current, freezeOwnedModel, assertModelFrozen,
|
|
512
565
|
exportCandidate: isP2 ? async (handoff) => { await readControls(); await assertModelFrozen(); if (p2Handoff)
|
|
513
|
-
throw Error('P2_DUPLICATE_HANDOFF');
|
|
566
|
+
throw Error('P2_DUPLICATE_HANDOFF'); if (executionAttempt)
|
|
567
|
+
validateAttemptHandoff(handoff, executionAttempt); p2Handoff = structuredClone(handoff); } : undefined }));
|
|
568
|
+
if (executionAttempt)
|
|
569
|
+
validateAttemptEvidence(p1Result, executionAttempt);
|
|
514
570
|
executionStage = 'p1_receipts';
|
|
515
571
|
await readControls();
|
|
516
572
|
await assertModelFrozen();
|
|
@@ -36,7 +36,7 @@ export async function runManualCodexJob(input, deps) {
|
|
|
36
36
|
let leaseExpires = claim.job.lease_expires_at;
|
|
37
37
|
if (!Number.isSafeInteger(leaseExpires))
|
|
38
38
|
throw Error('MANUAL_PILOT_LEASE_MISSING');
|
|
39
|
-
const job = { id: claim.job.id, generation: claim.job.generation, leaseExpiresAt: claim.job.lease_expires_at, workLoopId: claim.job.work_loop_id, leaseToken: claim.lease_token, launchGrant: claim.launch_grant, taskPackage: claim.task_package, launchFingerprint: claim.job.launch_fingerprint, gatewayContractFingerprint: claim.contract_fingerprint, gatewayTaskPackageFingerprint: claim.task_package_fingerprint };
|
|
39
|
+
const job = { id: claim.job.id, generation: claim.job.generation, leaseExpiresAt: claim.job.lease_expires_at, workLoopId: claim.job.work_loop_id, leaseToken: claim.lease_token, launchGrant: claim.launch_grant, taskPackage: claim.task_package, launchFingerprint: claim.job.launch_fingerprint, gatewayContractFingerprint: claim.contract_fingerprint, gatewayTaskPackageFingerprint: claim.task_package_fingerprint, executionAttempt: claim.attempt_context, executionAttemptFingerprint: claim.attempt_context_fingerprint };
|
|
40
40
|
await state.save({ jobId: job.id, generation: job.generation, leaseToken: job.leaseToken, phase: 'claimed' });
|
|
41
41
|
let result = { jobId: job.id, status: 'unacknowledged' };
|
|
42
42
|
const confirmJobLease = (j, expires) => { host?.heartbeat(j.id, j.generation, expires); leaseExpires = expires; claim.job.lease_expires_at = expires; };
|
|
@@ -65,7 +65,7 @@ export async function runManualCodexJob(input, deps) {
|
|
|
65
65
|
gateway: {
|
|
66
66
|
renewJob: (j, signal) => input.gateway.heartbeatLocalJob({ jobId: j.id, generation: j.generation, leaseToken: j.leaseToken }, signal), confirmJobLease,
|
|
67
67
|
shadow: (j, signal) => input.gateway.localJobShadow({ workLoopId: j.workLoopId, contractFingerprint: j.gatewayContractFingerprint, taskPackageFingerprint: j.gatewayTaskPackageFingerprint }, signal),
|
|
68
|
-
createBinding: (j, signal) => input.gateway.createLocalJobBinding({ jobId: j.id, generation: j.generation, jobLeaseToken: j.leaseToken, launchGrant: j.launchGrant, workLoopId: j.workLoopId, checkoutPath: j.checkoutPath }, signal),
|
|
68
|
+
createBinding: (j, signal) => input.gateway.createLocalJobBinding({ jobId: j.id, generation: j.generation, jobLeaseToken: j.leaseToken, launchGrant: j.launchGrant, workLoopId: j.workLoopId, checkoutPath: j.checkoutPath, claimedJob: j }, signal),
|
|
69
69
|
appendEvent: (id, event) => input.gateway.appendEvent(id, event), readCommands: (id, cursor, signal) => input.gateway.readCommands(id, cursor, signal), acknowledgeCommand: (id, sequence, signal) => input.gateway.acknowledgeCommand(id, sequence, signal),
|
|
70
70
|
async heartbeat(id, j, signal) { await input.gateway.heartbeat(id, signal); signal.throwIfAborted(); return input.gateway.heartbeatLocalJob({ jobId: j.id, generation: j.generation, leaseToken: j.leaseToken }, signal); },
|
|
71
71
|
async complete(value) { await input.gateway.completeLocalJob(value); result = { jobId: value.jobId, status: value.status }; }
|
|
@@ -35,6 +35,8 @@ import { composeReviewedP1Host } from '../verification/p1-host-composition.js';
|
|
|
35
35
|
import { AskRuntime, withAsks } from '../asks/runtime.js';
|
|
36
36
|
import { GuideRuntime, withGuide } from '../guide/runtime.js';
|
|
37
37
|
import { FixConsentRuntime } from './fix-consent.js';
|
|
38
|
+
import { RoomActivityRuntime } from '../rooms/activity.js';
|
|
39
|
+
import { notifyRoomActivity } from '../rooms/notifier.js';
|
|
38
40
|
export async function buildDaemon(input) {
|
|
39
41
|
const claudeParts = [input.claudeSecurityHost, input.claudeSupportedProbe, input.claudeBinaryIdentity].filter(Boolean).length;
|
|
40
42
|
if (claudeParts !== 0 && claudeParts !== 3)
|
|
@@ -63,8 +65,11 @@ export async function buildDaemon(input) {
|
|
|
63
65
|
pendingCodexClaim.job.lease_expires_at = expiresAt;
|
|
64
66
|
};
|
|
65
67
|
const gateway = new McpGatewayClient({ mcpUrl: input.gatewayMcpUrl, tokenProvider: input.tokenProvider, runnerId: input.runnerId, fetchFn: input.fetchFn });
|
|
68
|
+
const roomActivity = new RoomActivityRuntime({ stateRoot: input.stateRoot,
|
|
69
|
+
read: (cursor, limit) => gateway.readRoomActivity(cursor, limit), notify: notifyRoomActivity });
|
|
66
70
|
// AI-first ask delivery (Work Item e1c08b33) rides on the session-open loop below; it starts no executor.
|
|
67
71
|
const asks = new AskRuntime({ stateRoot: input.stateRoot, env: input.env,
|
|
72
|
+
claimRoomNotice: () => roomActivity.claimNotice(),
|
|
68
73
|
report: (deliveryId, report, signal) => gateway.reportAskDelivery({ deliveryId, ...report }, signal),
|
|
69
74
|
log: event => { process.stderr.write(`${JSON.stringify(event)}\n`); } });
|
|
70
75
|
// Guide hooks (Work Item d38d60f9) ride on the same sync and heartbeat; they start no executor.
|
|
@@ -121,7 +126,7 @@ export async function buildDaemon(input) {
|
|
|
121
126
|
taskPackageFingerprint: job.gatewayTaskPackageFingerprint ?? "" }, signal),
|
|
122
127
|
createBinding: async (job, signal) => gateway.createLocalJobBinding({
|
|
123
128
|
jobId: job.id, generation: job.generation, jobLeaseToken: job.leaseToken,
|
|
124
|
-
launchGrant: job.launchGrant, workLoopId: job.workLoopId, checkoutPath: job.checkoutPath,
|
|
129
|
+
launchGrant: job.launchGrant, workLoopId: job.workLoopId, checkoutPath: job.checkoutPath, claimedJob: job,
|
|
125
130
|
}, signal),
|
|
126
131
|
publicationRecoveryContext: bindingId => gateway.exportLocalPublicationRecoveryContext(bindingId),
|
|
127
132
|
syncPublicationCursor: (bindingId, cursor, job, receipt) => gateway.syncLocalPublicationCursor(bindingId, cursor, job, receipt),
|
|
@@ -145,7 +150,7 @@ export async function buildDaemon(input) {
|
|
|
145
150
|
}
|
|
146
151
|
return registry.select(id, version, options);
|
|
147
152
|
} },
|
|
148
|
-
workspaces: new WorkspaceManager({ root: input.workspaceRoot, git: new GitClient() }),
|
|
153
|
+
workspaces: new WorkspaceManager({ root: input.workspaceRoot, git: new GitClient(), registeredSubmodules: p1Host?.registeredSubmodules }),
|
|
149
154
|
state,
|
|
150
155
|
});
|
|
151
156
|
let transport;
|
|
@@ -167,6 +172,7 @@ export async function buildDaemon(input) {
|
|
|
167
172
|
launchFingerprint: claim.job.launch_fingerprint, gatewayContractFingerprint: claim.contract_fingerprint,
|
|
168
173
|
gatewayTaskPackageFingerprint: claim.task_package_fingerprint,
|
|
169
174
|
leaseExpiresAt: claim.job.lease_expires_at,
|
|
175
|
+
executionAttempt: claim.attempt_context, executionAttemptFingerprint: claim.attempt_context_fingerprint,
|
|
170
176
|
};
|
|
171
177
|
await state.save({ jobId: job.id, generation: job.generation, leaseToken: job.leaseToken, phase: "claimed" });
|
|
172
178
|
return { status: "claimed", job };
|
|
@@ -285,7 +291,7 @@ export async function buildDaemon(input) {
|
|
|
285
291
|
const acknowledged = closureAcks.take();
|
|
286
292
|
// A runner that takes no jobs right now (reconciling, or suspended for recovery) names no adapter.
|
|
287
293
|
try {
|
|
288
|
-
return await gateway.syncRunner({ adapterVersions: poller.accepting ? versions : {}, reason, acknowledgedClosureIds: acknowledged, guidancePolicyVersion: await guide.policyVersion() }, signal);
|
|
294
|
+
return await gateway.syncRunner({ adapterVersions: poller.accepting ? versions : {}, reason, acknowledgedClosureIds: acknowledged, guidancePolicyVersion: await guide.policyVersion(), roomActivityV1: true }, signal);
|
|
289
295
|
}
|
|
290
296
|
catch (error) {
|
|
291
297
|
closureAcks.restore(acknowledged);
|
|
@@ -293,12 +299,17 @@ export async function buildDaemon(input) {
|
|
|
293
299
|
}
|
|
294
300
|
},
|
|
295
301
|
authorize: (socketId, signal) => gateway.authorizeRunnerChannel(socketId, signal),
|
|
296
|
-
dispatch: async (result) => {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
+
dispatch: async (result) => {
|
|
303
|
+
void roomActivity.handleSync(result.roomActivityEnabled === true).catch(() => {
|
|
304
|
+
process.stderr.write(`${JSON.stringify({ event: 'room_activity_scan_failed' })}\n`);
|
|
305
|
+
});
|
|
306
|
+
try {
|
|
307
|
+
await dispatch(result);
|
|
308
|
+
}
|
|
309
|
+
finally {
|
|
310
|
+
await fixConsent.afterSync(result);
|
|
311
|
+
}
|
|
312
|
+
},
|
|
302
313
|
openSocket: (target, events) => new ReverbSocket(target, events, { packageVersion: packageVersion() }),
|
|
303
314
|
report: event => { process.stderr.write(`${JSON.stringify(event)}\n`); },
|
|
304
315
|
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { assertClaimedExecutionAttempt, assertBoundExecutionAttempt, CONTINUATION_SCHEMA, CONTINUATION_POLICY } from './verification/continuation-runner-contract.js';
|
|
1
2
|
import { publicationError } from './publication/failure.js';
|
|
2
3
|
import { issueTransportDiagnostic } from './safety/transport-diagnostic.js';
|
|
3
4
|
import { hasP1 } from './verification/p1-execution-host.js';
|
|
@@ -17,6 +18,7 @@ export class GatewayToolError extends Error {
|
|
|
17
18
|
this.code = code;
|
|
18
19
|
}
|
|
19
20
|
}
|
|
21
|
+
const ROOM_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
20
22
|
const FIX_CONSENT_GRANT_ID = /^[a-zA-Z0-9-]{1,64}$/;
|
|
21
23
|
const FIX_CONSENT_REPOSITORY_KEY = /^[a-z0-9][a-z0-9._-]{0,99}$/;
|
|
22
24
|
const FIX_CONSENT_GRANTS_MAX = 20;
|
|
@@ -60,6 +62,7 @@ export function parseRunnerSync(result, runnerId) {
|
|
|
60
62
|
...(typeof body.guidance_policy_version === "string" ? { guidancePolicyVersion: body.guidance_policy_version } : {}),
|
|
61
63
|
...("claude_release" in body ? { claudeRelease: parseClaudeSyncRelease(body.claude_release) } : {}),
|
|
62
64
|
...(body.ask_alert === true ? { askAlert: true } : {}),
|
|
65
|
+
...(body.room_activity_enabled === true ? { roomActivityEnabled: true } : {}),
|
|
63
66
|
...(Array.isArray(body.fix_consent_grants) ? { fixConsentGrants: parseFixConsentGrants(body.fix_consent_grants) } : {}),
|
|
64
67
|
jobAvailable: body.job_available === true,
|
|
65
68
|
push: parsePush(body.push, runnerId),
|
|
@@ -116,7 +119,7 @@ export function shadowTaskPackage(contract) {
|
|
|
116
119
|
const verification = hasP1(payload) ? payload.verification : undefined;
|
|
117
120
|
if (hasP1(payload)) {
|
|
118
121
|
const resolved = resolveRunnerVerification(verification);
|
|
119
|
-
if (resolved.version
|
|
122
|
+
if (resolved.version !== 1 && (nested(payload, 'policy.policy_key') !== (resolved.version === 3 ? CONTINUATION_POLICY : REVIEWED_POLICY) || nested(payload, 'policy.loop_type') !== REVIEWED_LOOP_TYPE))
|
|
120
123
|
throw Error('P1_CONTRACT_REJECTED');
|
|
121
124
|
}
|
|
122
125
|
const repositories = (payload.repositories ?? []).map((repository) => ({
|
|
@@ -315,10 +318,14 @@ export class McpGatewayClient {
|
|
|
315
318
|
if (!this.checkoutPath) {
|
|
316
319
|
throw new Error("local checkout path is required before creating an execution binding");
|
|
317
320
|
}
|
|
318
|
-
const
|
|
321
|
+
const qualified = input.expectedJobId !== undefined || input.expectedAttemptNumber !== undefined;
|
|
322
|
+
if (qualified && (typeof input.expectedJobId !== 'string' || !input.expectedJobId.trim() || !Number.isSafeInteger(input.expectedAttemptNumber) || input.expectedAttemptNumber < 1 || input.expectedAttemptNumber > 3))
|
|
323
|
+
throw new GatewayToolError('Qualified next identity is invalid', 'work_loop_next', 'CORRECTION_LINEAGE_MISMATCH');
|
|
324
|
+
const nextArguments = { work_loop_id: input.workLoopId, ...(qualified ? { expected_job_id: input.expectedJobId, expected_attempt_number: input.expectedAttemptNumber } : {}) };
|
|
325
|
+
const useProvidedLease = Boolean(this.initialLeaseToken) && !input.claimNext && !qualified;
|
|
319
326
|
const claim = useProvidedLease
|
|
320
327
|
? await this.callTool("work_loop_show", { work_loop_id: input.workLoopId })
|
|
321
|
-
: await this.callTool("work_loop_next",
|
|
328
|
+
: await this.callTool("work_loop_next", nextArguments);
|
|
322
329
|
const leaseToken = useProvidedLease
|
|
323
330
|
? this.initialLeaseToken
|
|
324
331
|
: requiredString(claim.lease_token, "lease token");
|
|
@@ -505,12 +512,25 @@ export class McpGatewayClient {
|
|
|
505
512
|
return { leaseExpiresAt: context.leaseExpiresAt };
|
|
506
513
|
}
|
|
507
514
|
async waitForLocalJob(adapterVersions, waitSeconds = 25, jobId) {
|
|
508
|
-
|
|
515
|
+
const claim = await this.callTool("local_agent_jobs_wait", {
|
|
509
516
|
runner_id: this.runnerId,
|
|
510
517
|
adapter_versions: adapterVersions,
|
|
511
518
|
...(jobId ? { job_id: jobId } : {}),
|
|
512
519
|
wait_seconds: Math.max(0, Math.min(30, waitSeconds)),
|
|
513
520
|
});
|
|
521
|
+
if (['continuation', 'execution_attempt', 'execution_attempt_fingerprint'].some(key => Object.hasOwn(claim, key)))
|
|
522
|
+
throw new GatewayToolError('Unexpected claimed attempt identity', 'local_agent_jobs_wait', 'GATEWAY_INVALID_TOOL_RESULT');
|
|
523
|
+
if (claim.status === 'claimed' && claim.task_package && claim.job) {
|
|
524
|
+
try {
|
|
525
|
+
assertClaimedExecutionAttempt({ id: claim.job.id, generation: claim.job.generation, workLoopId: claim.job.work_loop_id, launchFingerprint: claim.job.launch_fingerprint, launchGrant: claim.launch_grant ?? '', leaseToken: claim.lease_token ?? '', taskPackage: claim.task_package, executionAttempt: claim.attempt_context, executionAttemptFingerprint: claim.attempt_context_fingerprint });
|
|
526
|
+
}
|
|
527
|
+
catch {
|
|
528
|
+
throw new GatewayToolError('Claimed attempt identity is invalid', 'local_agent_jobs_wait', 'CORRECTION_EXECUTION_UNAVAILABLE');
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
else if (claim.attempt_context !== undefined || claim.attempt_context_fingerprint !== undefined)
|
|
532
|
+
throw new GatewayToolError('Unexpected claimed attempt identity', 'local_agent_jobs_wait', 'GATEWAY_INVALID_TOOL_RESULT');
|
|
533
|
+
return claim;
|
|
514
534
|
}
|
|
515
535
|
async registerLocalRunner(input) {
|
|
516
536
|
return this.callTool("local_agent_runner_register", {
|
|
@@ -587,9 +607,40 @@ export class McpGatewayClient {
|
|
|
587
607
|
reason: input.reason,
|
|
588
608
|
...(acknowledged.length > 0 ? { acknowledged_closure_ids: acknowledged } : {}),
|
|
589
609
|
...(input.guidancePolicyVersion ? { guidance_policy_version: input.guidancePolicyVersion } : {}),
|
|
610
|
+
...(input.roomActivityV1 ? { room_activity_v1: true } : {}),
|
|
590
611
|
}, signal);
|
|
591
612
|
return parseRunnerSync(result, this.runnerId);
|
|
592
613
|
}
|
|
614
|
+
async readRoomActivity(afterRoomId = '', limit = 50, signal) {
|
|
615
|
+
if ((afterRoomId !== '' && !ROOM_ID.test(afterRoomId)) || !Number.isInteger(limit) || limit < 1 || limit > 100) {
|
|
616
|
+
throw new GatewayToolError('Invalid Room activity query', 'work_room_read', 'GATEWAY_INVALID_TOOL_RESULT');
|
|
617
|
+
}
|
|
618
|
+
const result = await this.callTool('work_room_read', { operation: 'activity', after_room_id: afterRoomId, limit, share_only: true }, signal);
|
|
619
|
+
const body = result && typeof result === 'object' && !Array.isArray(result) ? result : undefined;
|
|
620
|
+
if (!body || !Array.isArray(body.rooms) || body.rooms.length > limit
|
|
621
|
+
|| !(body.next_cursor === null || (typeof body.next_cursor === 'string' && ROOM_ID.test(body.next_cursor)))) {
|
|
622
|
+
throw new GatewayToolError('Invalid Room activity page', 'work_room_read', 'GATEWAY_INVALID_TOOL_RESULT');
|
|
623
|
+
}
|
|
624
|
+
const rooms = body.rooms.map(row => {
|
|
625
|
+
const item = row && typeof row === 'object' && !Array.isArray(row) ? row : {};
|
|
626
|
+
if (typeof item.room_id !== 'string' || !ROOM_ID.test(item.room_id)
|
|
627
|
+
|| !Number.isSafeInteger(item.unread_count) || item.unread_count < 0
|
|
628
|
+
|| !Number.isSafeInteger(item.latest_message_sequence) || item.latest_message_sequence < 0) {
|
|
629
|
+
throw new GatewayToolError('Invalid Room activity entry', 'work_room_read', 'GATEWAY_INVALID_TOOL_RESULT');
|
|
630
|
+
}
|
|
631
|
+
return { roomId: item.room_id, unreadCount: item.unread_count, latestMessageSequence: item.latest_message_sequence };
|
|
632
|
+
});
|
|
633
|
+
let previous = afterRoomId;
|
|
634
|
+
for (const room of rooms) {
|
|
635
|
+
if (room.roomId <= previous)
|
|
636
|
+
throw new GatewayToolError('Room activity order is invalid', 'work_room_read', 'GATEWAY_INVALID_TOOL_RESULT');
|
|
637
|
+
previous = room.roomId;
|
|
638
|
+
}
|
|
639
|
+
if (body.next_cursor !== null && (rooms.length === 0 || body.next_cursor !== previous)) {
|
|
640
|
+
throw new GatewayToolError('Room activity cursor is invalid', 'work_room_read', 'GATEWAY_INVALID_TOOL_RESULT');
|
|
641
|
+
}
|
|
642
|
+
return { rooms, nextCursor: body.next_cursor };
|
|
643
|
+
}
|
|
593
644
|
async authorizeRunnerChannel(socketId, signal) {
|
|
594
645
|
const result = await this.callTool("local_agent_runner_channel_auth", {
|
|
595
646
|
runner_id: this.runnerId,
|
|
@@ -640,6 +691,15 @@ export class McpGatewayClient {
|
|
|
640
691
|
const bindingId = requiredString(result.binding.id, "binding ID");
|
|
641
692
|
const iterationId = requiredString(result.binding.iteration_id, "iteration ID");
|
|
642
693
|
signal?.throwIfAborted();
|
|
694
|
+
if (input.claimedJob) {
|
|
695
|
+
if (input.claimedJob.id !== input.jobId || input.claimedJob.generation !== input.generation || input.claimedJob.workLoopId !== input.workLoopId || input.claimedJob.leaseToken !== input.jobLeaseToken || input.claimedJob.launchGrant !== input.launchGrant || result.work_loop_id !== input.workLoopId)
|
|
696
|
+
throw Error('CORRECTION_LINEAGE_MISMATCH');
|
|
697
|
+
assertBoundExecutionAttempt(input.claimedJob, result.bundle, iterationId);
|
|
698
|
+
if (input.claimedJob.executionAttempt && typeof result.binding_replayed !== 'boolean')
|
|
699
|
+
throw Error('CORRECTION_BINDING_RECONCILIATION_REQUIRED');
|
|
700
|
+
}
|
|
701
|
+
else if (Object.hasOwn(result.bundle, 'attempt_context') || Object.hasOwn(result.bundle, 'attempt_context_fingerprint') || nested(result.bundle, 'contract.verification.schema_version') === CONTINUATION_SCHEMA)
|
|
702
|
+
throw Error('CORRECTION_LINEAGE_MISMATCH');
|
|
643
703
|
this.context = {
|
|
644
704
|
jobFence: { job_id: input.jobId, generation: input.generation, lease_token: input.jobLeaseToken },
|
|
645
705
|
workLoopId: requiredString(result.work_loop_id, "Work Loop ID"),
|
|
@@ -653,7 +713,7 @@ export class McpGatewayClient {
|
|
|
653
713
|
runtimeAdapterVersion: requiredString(result.binding.runtime_adapter_version, "runtime adapter version"),
|
|
654
714
|
};
|
|
655
715
|
return {
|
|
656
|
-
bindingId,
|
|
716
|
+
bindingId, iterationId, ...(input.claimedJob?.executionAttempt ? { bindingReplayed: result.binding_replayed } : {}),
|
|
657
717
|
taskPackage: result.bundle,
|
|
658
718
|
resolvedModelId: requiredString(result.binding.resolved_model_id, "resolved model ID"),
|
|
659
719
|
};
|
package/dist/src/protocol.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { validateAttemptContext, attemptContextFingerprint } from './verification/continuation-runner-contract.js';
|
|
2
|
+
import { protocolHash } from './publication/candidate.js';
|
|
1
3
|
export const PROTOCOL_VERSION = "agent-execution-v1";
|
|
2
4
|
export const ROLES = ["planner", "supervisor", "executor", "reviewer"];
|
|
3
5
|
export const EVENT_TYPES = [
|
|
@@ -73,3 +75,34 @@ export class TerminalGuard {
|
|
|
73
75
|
return this.terminalSeen !== null;
|
|
74
76
|
}
|
|
75
77
|
}
|
|
78
|
+
/** Wire-domain/identity check only. Scope, measured command results and grant authority
|
|
79
|
+
* are verified by their owners; a syntactically valid payload is not completion proof. */
|
|
80
|
+
export function validateAttemptEvidence(value, expected) {
|
|
81
|
+
const c = validateAttemptContext(expected), fp = attemptContextFingerprint(c);
|
|
82
|
+
const reject = () => { throw Error('CORRECTION_LINEAGE_MISMATCH'); };
|
|
83
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
84
|
+
reject();
|
|
85
|
+
const p = value, m = p.changed_paths, receipts = p.receipts;
|
|
86
|
+
const hex = (v, n) => typeof v === 'string' && new RegExp(`^[a-f0-9]{${n}}$`).test(v);
|
|
87
|
+
if (!hex(p.candidate_commit_sha, 40) || !hex(p.snapshot_fingerprint, 64) || !m || m.schema_version !== 'runner-changed-paths-v2' || m.digest_domain !== 'p1-changed-paths-v2' || m.attempt_context_fingerprint !== fp || m.base_commit_sha !== c.attempt_base_commit_sha || m.candidate_commit_sha !== p.candidate_commit_sha || !Array.isArray(m.entries) || !m.entries.length || !Array.isArray(receipts) || !receipts.length)
|
|
88
|
+
reject();
|
|
89
|
+
const { manifest_sha256, ...unsigned } = m;
|
|
90
|
+
if (manifest_sha256 !== protocolHash(unsigned))
|
|
91
|
+
reject();
|
|
92
|
+
for (const value of receipts) {
|
|
93
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
94
|
+
reject();
|
|
95
|
+
const r = value;
|
|
96
|
+
if (r.schema_version !== 'runner-command-receipt-v2' || r.attempt_context_fingerprint !== fp || r.job_id !== c.job_id || r.generation !== c.generation || r.contract_fingerprint !== c.contract_fingerprint || r.candidate_commit_sha !== p.candidate_commit_sha || r.snapshot_fingerprint !== p.snapshot_fingerprint)
|
|
97
|
+
reject();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/** Versioned host handoff check, distinct from ordinary terminal event transport. */
|
|
101
|
+
export function validateAttemptHandoff(value, expected) {
|
|
102
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
103
|
+
throw Error('CORRECTION_LINEAGE_MISMATCH');
|
|
104
|
+
const h = value, fp = attemptContextFingerprint(expected);
|
|
105
|
+
if (h.schema_version !== 'gateway-candidate-handoff-v2' || h.attempt_context_fingerprint !== fp || attemptContextFingerprint(h.attempt_context) !== fp)
|
|
106
|
+
throw Error('CORRECTION_LINEAGE_MISMATCH');
|
|
107
|
+
validateAttemptEvidence(h, expected);
|
|
108
|
+
}
|