mtech-ai-local-runner 0.1.30 → 0.1.32
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 +64 -1
- package/dist/src/adapters/claude-code/adapter.js +76 -17
- package/dist/src/adapters/codex/adapter.js +146 -30
- package/dist/src/adapters/codex/exec-launch-host.js +50 -11
- package/dist/src/adapters/codex/stream-parser.js +8 -17
- package/dist/src/adapters/completion-report.js +8 -5
- package/dist/src/cli.js +23 -0
- package/dist/src/config.js +4 -4
- package/dist/src/daemon/daemon.js +62 -4
- package/dist/src/daemon/execution-coordinator.js +351 -77
- package/dist/src/daemon/job-poller.js +4 -1
- package/dist/src/daemon/manual-codex.js +10 -3
- package/dist/src/daemon/preparation-lease.js +142 -0
- package/dist/src/daemon/runtime.js +65 -23
- package/dist/src/daemon/terminal-recovery.js +29 -5
- package/dist/src/gateway-client.js +120 -22
- package/dist/src/platform/macos/launchd.js +13 -2
- package/dist/src/process/node-process-group-host.js +89 -34
- package/dist/src/process/owned-process-guardian.js +140 -0
- package/dist/src/publication/candidate.js +95 -0
- package/dist/src/publication/execution-host.js +55 -0
- package/dist/src/publication/git-credential-broker.js +150 -0
- package/dist/src/publication/git-credential-helper.js +29 -0
- package/dist/src/publication/git-snapshot.js +321 -0
- package/dist/src/publication/git-transport.js +156 -0
- package/dist/src/publication/installed-host.js +120 -0
- package/dist/src/publication/isolated-verifier.js +67 -0
- package/dist/src/publication/journal.js +87 -0
- package/dist/src/publication/production-host.js +155 -0
- package/dist/src/publication/project-registration.js +49 -0
- package/dist/src/publication/publisher.js +166 -0
- package/dist/src/publication/recovery.js +53 -0
- package/dist/src/publication/runtime-release.js +91 -0
- package/dist/src/publication/server-ci-error.js +15 -0
- package/dist/src/publication/server-ci-host.js +160 -0
- package/dist/src/runner.js +33 -4
- package/dist/src/safety/codex-daemon-host.js +2 -2
- package/dist/src/safety/command-policy-core.js +124 -0
- package/dist/src/safety/command-policy.js +1 -124
- package/dist/src/safety/execution-failure.js +4 -0
- package/dist/src/safety/job-tool-policy.js +9 -0
- package/dist/src/safety/local-tool-policy.js +1 -1
- package/dist/src/safety/terminal-evidence.js +67 -16
- package/dist/src/safety/unsupported-supervisor-command.js +12 -0
- package/dist/src/state-store.js +50 -7
- package/dist/src/workspace/workspace-manager.js +14 -0
- package/package.json +3 -2
- package/verification/README.md +63 -0
- package/verification/runner-policy.cjs +24 -0
|
@@ -36,9 +36,10 @@ 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, 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 };
|
|
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
|
+
const confirmJobLease = (j, expires) => { host?.heartbeat(j.id, j.generation, expires); leaseExpires = expires; claim.job.lease_expires_at = expires; };
|
|
42
43
|
const coordinator = new ExecutionCoordinator({ state, workspaces: deps?.workspaces ?? new WorkspaceManager({ root: input.workspaceRoot, git: new GitClient() }), terminalEvidenceCollector: deps?.terminalEvidenceCollector ?? buildTerminalEvidenceCollector(),
|
|
43
44
|
reportExecutionFailure: d => process.stderr.write(JSON.stringify({ event: 'manual_execution_failed', ...d }) + '\n'),
|
|
44
45
|
reportPreparationFailure: d => process.stderr.write(JSON.stringify({ event: 'manual_preparation_failed', ...d }) + '\n'),
|
|
@@ -61,8 +62,14 @@ export async function runManualCodexJob(input, deps) {
|
|
|
61
62
|
adapter = await plugin.create(options);
|
|
62
63
|
return adapter;
|
|
63
64
|
} },
|
|
64
|
-
gateway: {
|
|
65
|
-
|
|
65
|
+
gateway: {
|
|
66
|
+
renewJob: (j, signal) => input.gateway.heartbeatLocalJob({ jobId: j.id, generation: j.generation, leaseToken: j.leaseToken }, signal), confirmJobLease,
|
|
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),
|
|
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
|
+
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
|
+
async complete(value) { await input.gateway.completeLocalJob(value); result = { jobId: value.jobId, status: value.status }; }
|
|
72
|
+
},
|
|
66
73
|
});
|
|
67
74
|
await coordinator.execute(job);
|
|
68
75
|
return result;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { GatewayToolError } from '../gateway-client.js';
|
|
2
|
+
function requireJobIdentity(job) {
|
|
3
|
+
if (typeof job.id !== 'string' || !job.id.trim() || !Number.isSafeInteger(job.generation) || job.generation < 1) {
|
|
4
|
+
throw new GatewayToolError('Job claim identity is invalid', 'local_agent_jobs_wait', 'JOB_LEASE_FENCE_INVALID');
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export function validateJobLease(receipt, job, allowRunning, allowReviewPending = allowRunning) {
|
|
8
|
+
requireJobIdentity(job);
|
|
9
|
+
const renewed = receipt?.job;
|
|
10
|
+
if (!renewed || renewed.id !== job.id || renewed.generation !== job.generation
|
|
11
|
+
|| !(allowRunning ? ['claimed', 'shadowing', 'running', ...(allowReviewPending ? ['review_pending'] : [])] : ['claimed', 'shadowing']).includes(renewed.status)
|
|
12
|
+
|| !Number.isSafeInteger(renewed.lease_expires_at) || renewed.lease_expires_at * 1000 <= Date.now()) {
|
|
13
|
+
throw new GatewayToolError('Job lease fence is invalid', 'local_agent_job_heartbeat', 'JOB_LEASE_FENCE_INVALID');
|
|
14
|
+
}
|
|
15
|
+
return renewed.lease_expires_at;
|
|
16
|
+
}
|
|
17
|
+
/** Preparation-only rolling Job fence. The Work Loop remains Gateway-owned. */
|
|
18
|
+
export class PreparationLease {
|
|
19
|
+
job;
|
|
20
|
+
deadline;
|
|
21
|
+
renew;
|
|
22
|
+
confirmed;
|
|
23
|
+
controller = new AbortController();
|
|
24
|
+
failure;
|
|
25
|
+
leaseExpiresAt;
|
|
26
|
+
bindingStarted = false;
|
|
27
|
+
stopped = false;
|
|
28
|
+
schedule;
|
|
29
|
+
boundary;
|
|
30
|
+
backoff;
|
|
31
|
+
pending;
|
|
32
|
+
rejectAbort;
|
|
33
|
+
aborted = new Promise((_, reject) => { this.rejectAbort = reject; });
|
|
34
|
+
constructor(job, deadline, renew, confirmed) {
|
|
35
|
+
this.job = job;
|
|
36
|
+
this.deadline = deadline;
|
|
37
|
+
this.renew = renew;
|
|
38
|
+
this.confirmed = confirmed;
|
|
39
|
+
void this.aborted.catch(() => { });
|
|
40
|
+
requireJobIdentity(job);
|
|
41
|
+
if (Number.isSafeInteger(job.leaseExpiresAt))
|
|
42
|
+
this.leaseExpiresAt = job.leaseExpiresAt;
|
|
43
|
+
this.armBoundary();
|
|
44
|
+
}
|
|
45
|
+
get signal() { return this.controller.signal; }
|
|
46
|
+
get hasLiveFence() { return this.leaseExpiresAt !== undefined && this.leaseExpiresAt * 1000 > Date.now() && (!this.failure || this.failure instanceof PreparationDeadlineError); }
|
|
47
|
+
assert() {
|
|
48
|
+
if (this.failure)
|
|
49
|
+
throw this.failure;
|
|
50
|
+
if (Date.now() >= this.deadline)
|
|
51
|
+
this.fail(new PreparationDeadlineError());
|
|
52
|
+
if (this.leaseExpiresAt !== undefined && Date.now() >= this.leaseExpiresAt * 1000)
|
|
53
|
+
this.fail(new Error('Job lease expired during preparation'));
|
|
54
|
+
if (this.failure)
|
|
55
|
+
throw this.failure;
|
|
56
|
+
}
|
|
57
|
+
fail(error) {
|
|
58
|
+
if (this.failure)
|
|
59
|
+
return;
|
|
60
|
+
this.failure = error;
|
|
61
|
+
this.clearTimers();
|
|
62
|
+
this.controller.abort();
|
|
63
|
+
this.rejectAbort(error);
|
|
64
|
+
}
|
|
65
|
+
async step(operation, late) {
|
|
66
|
+
this.assert();
|
|
67
|
+
const work = operation().then(async (value) => {
|
|
68
|
+
if (this.signal.aborted) {
|
|
69
|
+
await late?.(value);
|
|
70
|
+
this.assert();
|
|
71
|
+
}
|
|
72
|
+
this.assert();
|
|
73
|
+
return value;
|
|
74
|
+
});
|
|
75
|
+
return Promise.race([work, this.aborted]);
|
|
76
|
+
}
|
|
77
|
+
start() { return this.renewOnce(); }
|
|
78
|
+
beginBinding() { this.assert(); this.bindingStarted = true; }
|
|
79
|
+
async stop() {
|
|
80
|
+
this.stopped = true;
|
|
81
|
+
if (this.schedule)
|
|
82
|
+
clearTimeout(this.schedule);
|
|
83
|
+
try {
|
|
84
|
+
if (this.pending)
|
|
85
|
+
await this.pending;
|
|
86
|
+
this.assert();
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
this.clearTimers();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
dispose() { this.stopped = true; this.clearTimers(); this.controller.abort(); }
|
|
93
|
+
clearTimers() { if (this.schedule)
|
|
94
|
+
clearTimeout(this.schedule); if (this.boundary)
|
|
95
|
+
clearTimeout(this.boundary); if (this.backoff)
|
|
96
|
+
clearTimeout(this.backoff); }
|
|
97
|
+
armBoundary() {
|
|
98
|
+
if (this.boundary)
|
|
99
|
+
clearTimeout(this.boundary);
|
|
100
|
+
const expires = this.leaseExpiresAt === undefined ? this.deadline : Math.min(this.deadline, this.leaseExpiresAt * 1000);
|
|
101
|
+
this.boundary = setTimeout(() => this.fail(expires >= this.deadline ? new PreparationDeadlineError() : new Error('Job lease expired during preparation')), Math.max(0, expires - Date.now()));
|
|
102
|
+
this.boundary.unref?.();
|
|
103
|
+
}
|
|
104
|
+
renewOnce() {
|
|
105
|
+
if (this.pending)
|
|
106
|
+
return this.pending;
|
|
107
|
+
const run = async () => {
|
|
108
|
+
let retries = 0;
|
|
109
|
+
while (true) {
|
|
110
|
+
this.assert();
|
|
111
|
+
try {
|
|
112
|
+
const receipt = await Promise.race([this.renew(this.signal), this.aborted]);
|
|
113
|
+
this.assert();
|
|
114
|
+
const expiry = validateJobLease(receipt, this.job, this.bindingStarted, false);
|
|
115
|
+
this.confirmed?.(expiry);
|
|
116
|
+
this.leaseExpiresAt = expiry;
|
|
117
|
+
if (!this.stopped)
|
|
118
|
+
this.armBoundary();
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
this.assert();
|
|
123
|
+
const transient = error instanceof GatewayToolError && (error.code === 'GATEWAY_TRANSPORT_ERROR' || /^GATEWAY_HTTP_50[0234]$/.test(error.code));
|
|
124
|
+
if (!transient || !this.hasLiveFence || retries >= 3)
|
|
125
|
+
throw error;
|
|
126
|
+
const delay = Math.min(1000 * 2 ** retries++, 4000);
|
|
127
|
+
await this.step(() => new Promise(resolve => { this.backoff = setTimeout(resolve, delay); }));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
this.pending = run().catch(error => { this.fail(error); throw error; }).finally(() => {
|
|
132
|
+
this.pending = undefined;
|
|
133
|
+
if (!this.stopped && !this.signal.aborted) {
|
|
134
|
+
this.schedule = setTimeout(() => { void this.renewOnce().catch(error => this.fail(error)); }, 30_000);
|
|
135
|
+
this.schedule.unref?.();
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
return this.pending;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
export class PreparationDeadlineError extends Error {
|
|
142
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { loadInstalledLocalFixHost } from '../publication/installed-host.js';
|
|
1
2
|
import { createCodexDaemonHost } from '../safety/codex-daemon-host.js';
|
|
2
3
|
import { reconcileTerminalState } from './terminal-recovery.js';
|
|
3
4
|
import { packageVersion } from '../package-version.js';
|
|
@@ -25,7 +26,18 @@ export async function buildDaemon(input) {
|
|
|
25
26
|
throw new Error('CODEX_SUPPORTED_HOST_REQUIRED');
|
|
26
27
|
const production = !input.codexSecurityHost && !input.codexSupportedProbe ? createCodexDaemonHost({ runnerId: input.runnerId, binary: input.env.AI_GATEWAY_LOCAL_RUNNER_CODEX_BINARY ?? 'codex', home: input.env.HOME ?? '', codexHome: input.env.CODEX_HOME, searchPath: input.env.PATH }) : undefined;
|
|
27
28
|
let pendingCodexClaim;
|
|
29
|
+
let registeredCodexJob;
|
|
30
|
+
const confirmJobLease = (job, expiresAt) => {
|
|
31
|
+
if (job.taskPackage.runtime_adapter !== 'codex_cli' || !production)
|
|
32
|
+
return;
|
|
33
|
+
if (!pendingCodexClaim?.job || pendingCodexClaim.job.id !== job.id || pendingCodexClaim.job.generation !== job.generation)
|
|
34
|
+
throw new Error('CODEX_JOB_LEASE_FENCE_MISMATCH');
|
|
35
|
+
if (registeredCodexJob === job.id)
|
|
36
|
+
production.heartbeat(job.id, job.generation, expiresAt);
|
|
37
|
+
pendingCodexClaim.job.lease_expires_at = expiresAt;
|
|
38
|
+
};
|
|
28
39
|
const gateway = new McpGatewayClient({ mcpUrl: input.gatewayMcpUrl, tokenProvider: input.tokenProvider, runnerId: input.runnerId, fetchFn: input.fetchFn });
|
|
40
|
+
const localFixHost = input.localFixHost ?? await loadInstalledLocalFixHost({ stateRoot: input.stateRoot, runnerId: input.runnerId, gateway });
|
|
29
41
|
// The same parts the CLI self-test ran before starting this daemon (verifyPackagedHook).
|
|
30
42
|
const hook = packagedHookCommandParts();
|
|
31
43
|
const plugin = new ClaudeCodePlugin({
|
|
@@ -45,36 +57,37 @@ export async function buildDaemon(input) {
|
|
|
45
57
|
const versions = Object.fromEntries(probes.filter((probe) => probe.available && probe.authenticated).map((probe) => [probe.id, probe.version]));
|
|
46
58
|
const state = new FileDaemonStateStore(input.stateRoot);
|
|
47
59
|
const coordinator = new ExecutionCoordinator({
|
|
60
|
+
localFixHost,
|
|
48
61
|
reportExecutionFailure: (diagnostic) => { process.stderr.write(`${JSON.stringify({ event: "execution_failed", ...diagnostic })}\n`); },
|
|
49
62
|
reportPreparationFailure: (diagnostic) => { process.stderr.write(`${JSON.stringify({ event: "preparation_failed", ...diagnostic })}\n`); },
|
|
50
63
|
terminalEvidenceCollector: buildTerminalEvidenceCollector(),
|
|
51
64
|
gateway: {
|
|
52
|
-
|
|
65
|
+
renewJob: (job, signal) => gateway.heartbeatLocalJob({ jobId: job.id, generation: job.generation, leaseToken: job.leaseToken }, signal),
|
|
66
|
+
confirmJobLease,
|
|
67
|
+
shadow: (job, signal) => gateway.localJobShadow({ workLoopId: job.workLoopId,
|
|
53
68
|
contractFingerprint: job.gatewayContractFingerprint ?? "",
|
|
54
|
-
taskPackageFingerprint: job.gatewayTaskPackageFingerprint ?? "" }),
|
|
55
|
-
createBinding: async (job) => gateway.createLocalJobBinding({
|
|
69
|
+
taskPackageFingerprint: job.gatewayTaskPackageFingerprint ?? "" }, signal),
|
|
70
|
+
createBinding: async (job, signal) => gateway.createLocalJobBinding({
|
|
56
71
|
jobId: job.id, generation: job.generation, jobLeaseToken: job.leaseToken,
|
|
57
72
|
launchGrant: job.launchGrant, workLoopId: job.workLoopId, checkoutPath: job.checkoutPath,
|
|
58
|
-
}),
|
|
59
|
-
|
|
60
|
-
|
|
73
|
+
}, signal),
|
|
74
|
+
publicationRecoveryContext: bindingId => gateway.exportLocalPublicationRecoveryContext(bindingId),
|
|
75
|
+
syncPublicationCursor: (bindingId, cursor) => gateway.syncLocalPublicationCursor(bindingId, cursor),
|
|
76
|
+
readCommands: (bindingId, cursor, signal) => gateway.readCommands(bindingId, cursor, signal),
|
|
77
|
+
acknowledgeCommand: (bindingId, sequence, signal) => gateway.acknowledgeCommand(bindingId, sequence, signal),
|
|
61
78
|
appendEvent: (bindingId, event) => gateway.appendEvent(bindingId, event),
|
|
62
|
-
heartbeat: async (bindingId, job) => {
|
|
63
|
-
const [, renewed] = await Promise.all([gateway.heartbeat(bindingId), gateway.heartbeatLocalJob({ jobId: job.id, generation: job.generation, leaseToken: job.leaseToken })]);
|
|
64
|
-
|
|
65
|
-
if (renewed.job.id !== job.id || renewed.job.generation !== job.generation)
|
|
66
|
-
throw new Error('CODEX_JOB_LEASE_FENCE_MISMATCH');
|
|
67
|
-
if (job.taskPackage.runtime_adapter === 'codex_cli')
|
|
68
|
-
production?.heartbeat(job.id, job.generation, renewed.job.lease_expires_at);
|
|
69
|
-
}
|
|
79
|
+
heartbeat: async (bindingId, job, signal) => {
|
|
80
|
+
const [, renewed] = await Promise.all([gateway.heartbeat(bindingId, signal), gateway.heartbeatLocalJob({ jobId: job.id, generation: job.generation, leaseToken: job.leaseToken }, signal)]);
|
|
81
|
+
return renewed;
|
|
70
82
|
},
|
|
71
|
-
complete: async (result) => { await gateway.completeLocalJob(result); await production?.releaseJob(result.jobId); pendingCodexClaim = undefined; },
|
|
83
|
+
complete: async (result) => { await gateway.completeLocalJob(result); await production?.releaseJob(result.jobId); pendingCodexClaim = undefined; registeredCodexJob = undefined; },
|
|
72
84
|
},
|
|
73
85
|
registry: { select: async (id, version, options) => {
|
|
74
86
|
if (production && id === 'codex_cli') {
|
|
75
87
|
if (!pendingCodexClaim || pendingCodexClaim.job?.id !== options.jobId)
|
|
76
88
|
throw new Error('CODEX_TRUSTED_CLAIM_REQUIRED');
|
|
77
89
|
production.registerClaim(pendingCodexClaim);
|
|
90
|
+
registeredCodexJob = options.jobId;
|
|
78
91
|
}
|
|
79
92
|
return registry.select(id, version, options);
|
|
80
93
|
} },
|
|
@@ -102,8 +115,7 @@ export async function buildDaemon(input) {
|
|
|
102
115
|
await gateway.heartbeatLocalRunner({
|
|
103
116
|
deviceFingerprint: createHash("sha256").update(`${process.getuid?.() ?? 0}|${runnerDisplayName()}|${input.gatewayMcpUrl}`).digest("hex"),
|
|
104
117
|
displayName: runnerDisplayName(), packageVersion: packageVersion(),
|
|
105
|
-
capabilities: probes
|
|
106
|
-
capabilities: probe.capabilities, authenticated: probe.available && probe.authenticated })),
|
|
118
|
+
capabilities: localRunnerCapabilities(probes, !!localFixHost, localFixHost?.verificationMode),
|
|
107
119
|
});
|
|
108
120
|
},
|
|
109
121
|
wait: async () => {
|
|
@@ -111,12 +123,13 @@ export async function buildDaemon(input) {
|
|
|
111
123
|
if (claim.status !== "claimed" || !claim.job || !claim.task_package || !claim.lease_token || !claim.launch_grant)
|
|
112
124
|
return { status: "no_job" };
|
|
113
125
|
if (production && claim.task_package.runtime_adapter === 'codex_cli')
|
|
114
|
-
pendingCodexClaim = claim;
|
|
126
|
+
pendingCodexClaim = structuredClone(claim);
|
|
115
127
|
const job = {
|
|
116
128
|
id: claim.job.id, generation: claim.job.generation, leaseToken: claim.lease_token, launchGrant: claim.launch_grant,
|
|
117
129
|
workLoopId: claim.job.work_loop_id, taskPackage: claim.task_package,
|
|
118
130
|
launchFingerprint: claim.job.launch_fingerprint, gatewayContractFingerprint: claim.contract_fingerprint,
|
|
119
131
|
gatewayTaskPackageFingerprint: claim.task_package_fingerprint,
|
|
132
|
+
leaseExpiresAt: claim.job.lease_expires_at,
|
|
120
133
|
};
|
|
121
134
|
await state.save({ jobId: job.id, generation: job.generation, leaseToken: job.leaseToken, phase: "claimed" });
|
|
122
135
|
return { status: "claimed", job };
|
|
@@ -133,21 +146,37 @@ export async function buildDaemon(input) {
|
|
|
133
146
|
if (await reconcileTerminalState(previous, { gateway: { complete: request => gateway.completeLocalJob(request) }, state }))
|
|
134
147
|
return;
|
|
135
148
|
if (await reconcilePreparationState(previous, {
|
|
136
|
-
complete: (result) => gateway.completeLocalJob(result), clear: () => state.clear(),
|
|
149
|
+
complete: (result) => gateway.completeLocalJob(result), clear: () => state.clear(), save: receipt => state.save(receipt),
|
|
137
150
|
}))
|
|
138
151
|
return;
|
|
139
|
-
|
|
152
|
+
throw new Error('Runner state requires authoritative reconciliation');
|
|
140
153
|
},
|
|
141
154
|
poller,
|
|
155
|
+
reportRecoveryRequired: reason => { process.stderr.write(`${JSON.stringify({ event: 'runner_recovery_required', reason })}\n`); },
|
|
142
156
|
});
|
|
143
157
|
// Model tools have no Gateway relay. Each adapter owns and closes its job endpoint.
|
|
144
|
-
return { daemon, close: async () => {
|
|
158
|
+
return { daemon, close: async () => { daemon.stop(); await production?.close(); } };
|
|
145
159
|
}
|
|
146
160
|
export function runnerDisplayName() { return `${hostname()} local AI`; }
|
|
147
161
|
/** Unknown mutation outcomes halt polling; known failures replay only their terminal receipt. */
|
|
148
162
|
export async function reconcilePreparationState(previous, deps) {
|
|
149
|
-
if (
|
|
150
|
-
throw new Error("
|
|
163
|
+
if (["publication_pending", "publication_terminal_pending"].includes(previous.phase ?? ""))
|
|
164
|
+
throw new Error("Publication outcome requires reconciliation before runner polling");
|
|
165
|
+
if (previous.phase === 'running' || previous.phase === 'reconciling')
|
|
166
|
+
throw new Error('Executor cleanup requires authoritative reconciliation');
|
|
167
|
+
if (['claimed', 'shadow', 'binding', 'binding_uncertain'].includes(previous.phase ?? '')) {
|
|
168
|
+
if (!deps.save)
|
|
169
|
+
throw new Error('Binding outcome requires reconciliation before runner polling');
|
|
170
|
+
if (typeof previous.jobId !== 'string' || !previous.jobId.trim() || !Number.isSafeInteger(previous.generation) || Number(previous.generation) < 1 || typeof previous.leaseToken !== 'string' || !previous.leaseToken.trim())
|
|
171
|
+
throw new Error('Recovery receipt is missing its job fence');
|
|
172
|
+
const reason = previous.phase === 'binding' || previous.phase === 'binding_uncertain' ? 'binding_outcome_unknown' : 'runner_restarted';
|
|
173
|
+
// A Job recovery result records client interruption facts. The server closer proves
|
|
174
|
+
// Job/binding lineage independently and supplies its own server fact reason.
|
|
175
|
+
const completionRequest = { jobId: previous.jobId, generation: previous.generation, leaseToken: previous.leaseToken, status: reason === 'binding_outcome_unknown' ? 'blocked' : 'failed', result: { reason, recovery: { version: 'local-runner-recovery-v1', phase: previous.phase } } };
|
|
176
|
+
await deps.save({ jobId: previous.jobId, generation: previous.generation, leaseToken: previous.leaseToken, phase: 'terminal_pending', completionRequest: structuredClone(completionRequest) });
|
|
177
|
+
await deps.complete(structuredClone(completionRequest));
|
|
178
|
+
await deps.clear();
|
|
179
|
+
return true;
|
|
151
180
|
}
|
|
152
181
|
if (!previous.preparationFailure)
|
|
153
182
|
return false;
|
|
@@ -159,3 +188,16 @@ export async function reconcilePreparationState(previous, deps) {
|
|
|
159
188
|
await deps.clear();
|
|
160
189
|
return true;
|
|
161
190
|
}
|
|
191
|
+
/** Vendor capability text cannot claim a host-owned publication facility. */
|
|
192
|
+
export function localRunnerCapabilities(probes, trustedLocalFixHost = false, verificationMode) {
|
|
193
|
+
return probes.map(probe => {
|
|
194
|
+
const authenticated = probe.available && probe.authenticated;
|
|
195
|
+
const capabilities = probe.capabilities.filter(value => value !== 'local_fix_publication_v1' && value !== 'local_fix_gateway_ci_v1');
|
|
196
|
+
if (trustedLocalFixHost && authenticated && probe.id === 'codex_cli') {
|
|
197
|
+
capabilities.push('local_fix_publication_v1');
|
|
198
|
+
if (verificationMode === 'gateway_ci_v1')
|
|
199
|
+
capabilities.push('local_fix_gateway_ci_v1');
|
|
200
|
+
}
|
|
201
|
+
return { adapter: probe.id, version: probe.version, authenticated, capabilities };
|
|
202
|
+
});
|
|
203
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isTerminalEventType } from '../protocol.js';
|
|
2
|
+
import { DAEMON_BLOCKED_REASONS, DAEMON_CANCELLED_REASONS } from '../state-store.js';
|
|
2
3
|
function record(value) {
|
|
3
4
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
4
5
|
}
|
|
@@ -11,13 +12,19 @@ export async function reconcileTerminalState(previous, deps) {
|
|
|
11
12
|
if (!record(previous))
|
|
12
13
|
invalid();
|
|
13
14
|
if (previous.phase !== 'terminal_pending') {
|
|
14
|
-
if (!['claimed', 'shadow', 'binding', 'binding_uncertain', 'preparation_failed'].includes(
|
|
15
|
+
if (typeof previous.phase !== 'string' || !['claimed', 'shadow', 'binding', 'binding_uncertain', 'preparation_failed', 'running'].includes(previous.phase)
|
|
15
16
|
|| !nonempty(previous.jobId) || !Number.isSafeInteger(previous.generation) || previous.generation < 1
|
|
16
17
|
|| !nonempty(previous.leaseToken))
|
|
17
18
|
invalid();
|
|
18
|
-
//
|
|
19
|
-
//
|
|
19
|
+
// Validate known states for the recovery handler. A running state remains
|
|
20
|
+
// suspended there: neither a terminal flag nor its fence proves OS cleanup.
|
|
20
21
|
const allowed = ['phase', 'jobId', 'generation', 'leaseToken', 'workspaceManifest'];
|
|
22
|
+
if (previous.phase === 'running') {
|
|
23
|
+
allowed.push('bindingId', 'eventCursor', 'terminalEventType');
|
|
24
|
+
if (!nonempty(previous.bindingId) || (previous.eventCursor !== undefined && (!Number.isSafeInteger(previous.eventCursor) || previous.eventCursor < 0))
|
|
25
|
+
|| (previous.terminalEventType !== undefined && (typeof previous.terminalEventType !== 'string' || !isTerminalEventType(previous.terminalEventType))))
|
|
26
|
+
invalid();
|
|
27
|
+
}
|
|
21
28
|
if (previous.phase === 'preparation_failed' || previous.phase === 'binding_uncertain')
|
|
22
29
|
allowed.push('preparationFailure');
|
|
23
30
|
if (Object.keys(previous).some(key => !allowed.includes(key)))
|
|
@@ -38,12 +45,29 @@ export async function reconcileTerminalState(previous, deps) {
|
|
|
38
45
|
|| request.generation !== previous.generation || request.leaseToken !== previous.leaseToken || !record(request.result))
|
|
39
46
|
invalid();
|
|
40
47
|
const result = request.result;
|
|
41
|
-
if (
|
|
48
|
+
if (result.recovery !== undefined) {
|
|
49
|
+
const recovery = result.recovery;
|
|
50
|
+
if (Object.keys(previous).sort().join(',') !== 'completionRequest,generation,jobId,leaseToken,phase'
|
|
51
|
+
|| Object.keys(result).sort().join(',') !== 'reason,recovery' || !record(recovery)
|
|
52
|
+
|| Object.keys(recovery).sort().join(',') !== 'phase,version' || recovery.version !== 'local-runner-recovery-v1'
|
|
53
|
+
|| typeof recovery.phase !== 'string' || !['claimed', 'shadow', 'binding', 'binding_uncertain'].includes(recovery.phase)
|
|
54
|
+
|| result.reason !== (recovery.phase === 'binding' || recovery.phase === 'binding_uncertain' ? 'binding_outcome_unknown' : 'runner_restarted')
|
|
55
|
+
|| request.status !== (result.reason === 'binding_outcome_unknown' ? 'blocked' : 'failed'))
|
|
56
|
+
invalid();
|
|
57
|
+
}
|
|
58
|
+
else if (previous.bindingId !== undefined) {
|
|
42
59
|
const eventType = result.event_type;
|
|
43
60
|
if (!nonempty(previous.bindingId) || result.binding_id !== previous.bindingId || typeof eventType !== 'string'
|
|
44
61
|
|| (!isTerminalEventType(eventType) && eventType !== 'blocked' && eventType !== 'scope_conflict') || previous.terminalEventType !== eventType || typeof result.summary !== 'string')
|
|
45
62
|
invalid();
|
|
46
|
-
const
|
|
63
|
+
const reason = result.reason;
|
|
64
|
+
const blocked = eventType === 'failed' && typeof reason === 'string' && DAEMON_BLOCKED_REASONS.includes(reason);
|
|
65
|
+
// Old blocked/scope_conflict receipts remain replayable byte-for-byte. New
|
|
66
|
+
// failed/blocked pairs require one explicit daemon outcome reason.
|
|
67
|
+
if (reason !== undefined && eventType !== 'blocked' && eventType !== 'scope_conflict' && !(blocked || (eventType === 'cancelled' && typeof reason === 'string'
|
|
68
|
+
&& DAEMON_CANCELLED_REASONS.includes(reason))))
|
|
69
|
+
invalid();
|
|
70
|
+
const status = blocked ? 'blocked' : eventType === 'completed' ? 'completed' : eventType === 'cancelled' ? 'cancelled'
|
|
47
71
|
: eventType === 'blocked' || eventType === 'scope_conflict' ? 'blocked' : 'failed';
|
|
48
72
|
if (request.status !== status)
|
|
49
73
|
invalid();
|
|
@@ -122,16 +122,25 @@ export class McpGatewayClient {
|
|
|
122
122
|
}
|
|
123
123
|
throw new Error("No token provider or static token configured");
|
|
124
124
|
}
|
|
125
|
-
async callTool(name, args) {
|
|
125
|
+
async callTool(name, args, signal) {
|
|
126
126
|
let failureCode = "GATEWAY_TRANSPORT_ERROR";
|
|
127
127
|
try {
|
|
128
128
|
const token = await this.resolveToken();
|
|
129
|
+
signal?.throwIfAborted();
|
|
129
130
|
const response = await this.fetchFn(this.mcpUrl, {
|
|
131
|
+
...(signal ? { signal } : {}),
|
|
130
132
|
method: "POST",
|
|
131
133
|
headers: { Authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
132
134
|
body: JSON.stringify({ jsonrpc: "2.0", id: ++this.requestId, method: "tools/call", params: { name, arguments: args } }),
|
|
133
135
|
});
|
|
136
|
+
signal?.throwIfAborted();
|
|
137
|
+
if (!response.ok) {
|
|
138
|
+
failureCode = `GATEWAY_HTTP_${response.status}`;
|
|
139
|
+
throw new Error(`Gateway MCP ${name} failed with status ${response.status}`);
|
|
140
|
+
}
|
|
141
|
+
failureCode = 'GATEWAY_INVALID_TOOL_RESULT';
|
|
134
142
|
const body = (await response.json());
|
|
143
|
+
signal?.throwIfAborted();
|
|
135
144
|
if (!response.ok || body.error) {
|
|
136
145
|
failureCode = !response.ok ? `GATEWAY_HTTP_${response.status}` : "GATEWAY_RPC_ERROR";
|
|
137
146
|
throw new Error(`Gateway MCP ${name} failed with status ${response.status}: ${JSON.stringify(body.error ?? body)}`);
|
|
@@ -146,6 +155,7 @@ export class McpGatewayClient {
|
|
|
146
155
|
failureCode = envelope.status === "failed" ? "GATEWAY_TOOL_REJECTED" : "GATEWAY_INVALID_TOOL_RESULT";
|
|
147
156
|
throw new Error(`Gateway MCP ${name} did not complete: ${JSON.stringify(envelope)}`);
|
|
148
157
|
}
|
|
158
|
+
signal?.throwIfAborted();
|
|
149
159
|
return nested(envelope, "result.data");
|
|
150
160
|
}
|
|
151
161
|
catch (error) {
|
|
@@ -377,28 +387,29 @@ export class McpGatewayClient {
|
|
|
377
387
|
: {}),
|
|
378
388
|
};
|
|
379
389
|
}
|
|
380
|
-
async readCommands(bindingId, afterSequence) {
|
|
390
|
+
async readCommands(bindingId, afterSequence, signal) {
|
|
381
391
|
const result = await this.callTool("agent_runtime_commands_read", {
|
|
382
392
|
binding_id: bindingId,
|
|
383
393
|
runner_id: this.runnerId,
|
|
384
394
|
after_sequence: afterSequence,
|
|
385
395
|
limit: 100,
|
|
386
|
-
});
|
|
396
|
+
}, signal);
|
|
387
397
|
return result.commands ?? [];
|
|
388
398
|
}
|
|
389
|
-
async acknowledgeCommand(bindingId, sequence) {
|
|
399
|
+
async acknowledgeCommand(bindingId, sequence, signal) {
|
|
390
400
|
await this.callTool("agent_runtime_command_acknowledge", {
|
|
391
401
|
binding_id: bindingId,
|
|
392
402
|
runner_id: this.runnerId,
|
|
393
403
|
command_sequence: sequence,
|
|
394
|
-
});
|
|
404
|
+
}, signal);
|
|
395
405
|
}
|
|
396
|
-
async heartbeat(bindingId) {
|
|
406
|
+
async heartbeat(bindingId, signal) {
|
|
397
407
|
const context = this.requireContext(bindingId);
|
|
398
408
|
const result = await this.callTool("work_loop_heartbeat", {
|
|
399
409
|
work_loop_id: context.workLoopId,
|
|
400
410
|
lease_token: context.leaseToken,
|
|
401
|
-
});
|
|
411
|
+
}, signal);
|
|
412
|
+
signal?.throwIfAborted();
|
|
402
413
|
context.leaseExpiresAt = epochToIso(result.loop.lease_expires_at);
|
|
403
414
|
return { leaseExpiresAt: context.leaseExpiresAt };
|
|
404
415
|
}
|
|
@@ -432,26 +443,31 @@ export class McpGatewayClient {
|
|
|
432
443
|
async localRunnerStatus() {
|
|
433
444
|
return this.callTool("local_agent_runner_status", { runner_id: this.runnerId });
|
|
434
445
|
}
|
|
435
|
-
async localJobShadow(input) {
|
|
436
|
-
const shown = await this.callTool("work_loop_show", { work_loop_id: input.workLoopId });
|
|
437
|
-
if (shown
|
|
438
|
-
throw new
|
|
446
|
+
async localJobShadow(input, signal) {
|
|
447
|
+
const shown = await this.callTool("work_loop_show", { work_loop_id: input.workLoopId }, signal);
|
|
448
|
+
if (shown?.loop?.environment !== "development")
|
|
449
|
+
throw new GatewayToolError("local agent runtime is development-only", 'work_loop_show', 'GATEWAY_INVALID_TOOL_RESULT');
|
|
439
450
|
if (shown.loop.status === "cancelled")
|
|
440
451
|
throw new GatewayToolError("Work Loop is cancelled", "work_loop_show", "WORK_LOOP_CANCELLED");
|
|
452
|
+
const contractId = shown.loop.work_execution_contract_id;
|
|
453
|
+
if (typeof contractId !== 'string' || !contractId)
|
|
454
|
+
throw new GatewayToolError('Gateway response is missing execution contract ID', 'work_loop_show', 'GATEWAY_INVALID_TOOL_RESULT');
|
|
441
455
|
const result = await this.callTool("work_execution_contract_show", {
|
|
442
|
-
contract_id:
|
|
443
|
-
});
|
|
444
|
-
if (result
|
|
445
|
-
throw new
|
|
456
|
+
contract_id: contractId,
|
|
457
|
+
}, signal);
|
|
458
|
+
if (result?.contract?.status !== "approved")
|
|
459
|
+
throw new GatewayToolError("execution contract is not approved", 'work_execution_contract_show', 'GATEWAY_TOOL_REJECTED');
|
|
446
460
|
const payload = result.contract.contract_payload;
|
|
447
461
|
if (!payload || typeof payload !== "object" || Array.isArray(payload))
|
|
448
|
-
throw new
|
|
449
|
-
const storedFingerprint =
|
|
462
|
+
throw new GatewayToolError("Gateway response is missing contract payload", 'work_execution_contract_show', 'GATEWAY_INVALID_TOOL_RESULT');
|
|
463
|
+
const storedFingerprint = result.contract.content_fingerprint;
|
|
464
|
+
if (typeof storedFingerprint !== 'string' || !storedFingerprint)
|
|
465
|
+
throw new GatewayToolError('Gateway response is missing contract fingerprint', 'work_execution_contract_show', 'GATEWAY_INVALID_TOOL_RESULT');
|
|
450
466
|
if (storedFingerprint !== input.contractFingerprint)
|
|
451
|
-
throw new
|
|
467
|
+
throw new GatewayToolError("claimed contract fingerprint differs from approved contract", 'work_execution_contract_show', 'GATEWAY_INVALID_TOOL_RESULT');
|
|
452
468
|
return { contractFingerprint: storedFingerprint, taskPackageFingerprint: input.taskPackageFingerprint, contractPayload: payload };
|
|
453
469
|
}
|
|
454
|
-
async createLocalJobBinding(input) {
|
|
470
|
+
async createLocalJobBinding(input, signal) {
|
|
455
471
|
const clientSessionReference = uuidv5(`local-agent-job:${input.jobId}:${input.generation}`, uuidv5.URL);
|
|
456
472
|
const result = await this.callTool("local_agent_job_binding_create", {
|
|
457
473
|
job_id: input.jobId,
|
|
@@ -460,9 +476,10 @@ export class McpGatewayClient {
|
|
|
460
476
|
launch_grant: input.launchGrant,
|
|
461
477
|
runner_id: this.runnerId,
|
|
462
478
|
client_session_reference: clientSessionReference,
|
|
463
|
-
});
|
|
479
|
+
}, signal);
|
|
464
480
|
const bindingId = requiredString(result.binding.id, "binding ID");
|
|
465
481
|
const iterationId = requiredString(result.binding.iteration_id, "iteration ID");
|
|
482
|
+
signal?.throwIfAborted();
|
|
466
483
|
this.context = {
|
|
467
484
|
workLoopId: requiredString(result.work_loop_id, "Work Loop ID"),
|
|
468
485
|
leaseToken: requiredString(result.lease_token, "Work Loop lease token"),
|
|
@@ -480,6 +497,87 @@ export class McpGatewayClient {
|
|
|
480
497
|
resolvedModelId: requiredString(result.binding.resolved_model_id, "resolved model ID"),
|
|
481
498
|
};
|
|
482
499
|
}
|
|
500
|
+
/** Account for the server-authored verification event before appending a
|
|
501
|
+
* terminal event. Never infer a cursor from model output or a local counter. */
|
|
502
|
+
async syncLocalPublicationCursor(bindingId, afterSequence) {
|
|
503
|
+
const page = await this.callTool('agent_runtime_events_read', {
|
|
504
|
+
binding_id: bindingId, after_sequence: afterSequence, limit: 100,
|
|
505
|
+
});
|
|
506
|
+
const events = page.events ?? [];
|
|
507
|
+
if (page.terminal || events.length === 0 || events.length >= 100)
|
|
508
|
+
throw new Error('LOCAL_PUBLICATION_EVENT_CURSOR_REJECTED');
|
|
509
|
+
let cursor = afterSequence, verified = false;
|
|
510
|
+
for (const event of events) {
|
|
511
|
+
if (!Number.isSafeInteger(event.sequence) || event.sequence !== cursor + 1 || isTerminalEventType(String(event.event_type)))
|
|
512
|
+
throw new Error('LOCAL_PUBLICATION_EVENT_CURSOR_REJECTED');
|
|
513
|
+
cursor = event.sequence;
|
|
514
|
+
const proof = event.payload?.local_publication;
|
|
515
|
+
if (event.event_type === 'test_result' && event.stage === 'local_publication_verification' && typeof proof?.gateway_mac === 'string' && /^[a-f0-9]{64}$/.test(proof.gateway_mac))
|
|
516
|
+
verified = true;
|
|
517
|
+
}
|
|
518
|
+
if (!verified)
|
|
519
|
+
throw new Error('LOCAL_PUBLICATION_EVENT_CURSOR_REJECTED');
|
|
520
|
+
return cursor;
|
|
521
|
+
}
|
|
522
|
+
/** Host-only publication envelope. This client is never a model MCP relay. */
|
|
523
|
+
localPublicationFence(input) {
|
|
524
|
+
return { job_id: input.jobId, generation: input.generation, lease_token: input.leaseToken,
|
|
525
|
+
launch_grant: input.launchGrant, binding_id: input.bindingId, runner_id: this.runnerId };
|
|
526
|
+
}
|
|
527
|
+
exportLocalPublicationRecoveryContext(bindingId) {
|
|
528
|
+
const c = this.requireContext(bindingId);
|
|
529
|
+
return structuredClone({ ...c, bundle: { contract: { content_fingerprint: requiredString(nested(c.bundle, 'contract.content_fingerprint'), 'contract fingerprint') } } });
|
|
530
|
+
}
|
|
531
|
+
async restoreLocalPublicationRecoveryContext(input, stored, terminalReplay = false) {
|
|
532
|
+
if (terminalReplay) {
|
|
533
|
+
if (stored.bindingId !== input.bindingId || typeof stored.leaseToken !== 'string' || !stored.leaseToken)
|
|
534
|
+
throw new Error('LOCAL_PUBLICATION_RECOVERY_CONTEXT_REJECTED');
|
|
535
|
+
this.context = structuredClone(stored);
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
const current = (await this.localPublicationContext(input)).context;
|
|
539
|
+
if (stored.bindingId !== input.bindingId || stored.workLoopId !== current.work_loop_id || nested(stored.bundle, 'contract.content_fingerprint') !== current.contract_fingerprint || typeof stored.leaseToken !== 'string' || !stored.leaseToken)
|
|
540
|
+
throw new Error('LOCAL_PUBLICATION_RECOVERY_CONTEXT_REJECTED');
|
|
541
|
+
this.context = structuredClone(stored);
|
|
542
|
+
}
|
|
543
|
+
async localPublicationContext(input) {
|
|
544
|
+
return this.callTool('local_agent_publication_context', this.localPublicationFence(input));
|
|
545
|
+
}
|
|
546
|
+
async executeServerCiPublication(input) {
|
|
547
|
+
const body = { ...this.localPublicationFence(input), operation: input.operation };
|
|
548
|
+
if (input.operation === 'stage') {
|
|
549
|
+
const receipt = { ...input.securityReceipt };
|
|
550
|
+
if (receipt.transport === 'official_exec_v1' && ['cli-exec-job-security-receipt-v2', 'cli-exec-job-security-receipt-v3'].includes(String(receipt.receipt_version))) {
|
|
551
|
+
delete receipt.preparation_timing;
|
|
552
|
+
delete receipt.launch_process;
|
|
553
|
+
}
|
|
554
|
+
Object.assign(body, { files: input.files, security_receipt: receipt, host_receipt: input.hostReceipt });
|
|
555
|
+
}
|
|
556
|
+
else
|
|
557
|
+
Object.assign(body, { publication_id: input.publicationId, ...(input.canonicalApprovalId ? { canonical_approval_id: input.canonicalApprovalId } : {}) });
|
|
558
|
+
return this.callTool('local_agent_server_publication', body);
|
|
559
|
+
}
|
|
560
|
+
async executeLocalPublicationPr(input) {
|
|
561
|
+
return this.callTool('local_agent_publication_execute_pr', { ...this.localPublicationFence(input), publication_id: input.publicationId, signed_request: input.signedRequest, ...(input.canonicalApprovalId ? { canonical_approval_id: input.canonicalApprovalId } : {}) });
|
|
562
|
+
}
|
|
563
|
+
async authorizeLocalPublication(input) {
|
|
564
|
+
const receipt = { ...input.securityReceipt };
|
|
565
|
+
if (receipt.transport === 'official_exec_v1' && ['cli-exec-job-security-receipt-v2', 'cli-exec-job-security-receipt-v3'].includes(String(receipt.receipt_version))) {
|
|
566
|
+
delete receipt.preparation_timing;
|
|
567
|
+
delete receipt.launch_process;
|
|
568
|
+
}
|
|
569
|
+
return this.callTool('local_agent_publication_authorize', { ...this.localPublicationFence(input),
|
|
570
|
+
candidate: input.candidate, test_receipts: input.testReceipts, security_receipt: receipt, host_receipt: input.hostReceipt });
|
|
571
|
+
}
|
|
572
|
+
async issueLocalPublicationAction(input) {
|
|
573
|
+
return this.callTool('local_agent_publication_issue_action', { ...this.localPublicationFence(input),
|
|
574
|
+
publication_id: input.publicationId, action: input.action, commit_message: input.commitMessage,
|
|
575
|
+
pull_request_title: input.pullRequestTitle, pull_request_body: input.pullRequestBody });
|
|
576
|
+
}
|
|
577
|
+
async acceptLocalPublicationResult(input) {
|
|
578
|
+
return this.callTool('local_agent_publication_accept_result', { ...this.localPublicationFence(input),
|
|
579
|
+
publication_id: input.publicationId, action: input.action, result: input.result });
|
|
580
|
+
}
|
|
483
581
|
async completeLocalJob(input) {
|
|
484
582
|
let result = input.result;
|
|
485
583
|
const receipt = result.security_receipt;
|
|
@@ -502,13 +600,13 @@ export class McpGatewayClient {
|
|
|
502
600
|
result,
|
|
503
601
|
});
|
|
504
602
|
}
|
|
505
|
-
async heartbeatLocalJob(input) {
|
|
603
|
+
async heartbeatLocalJob(input, signal) {
|
|
506
604
|
return this.callTool("local_agent_job_heartbeat", {
|
|
507
605
|
job_id: input.jobId,
|
|
508
606
|
generation: input.generation,
|
|
509
607
|
lease_token: input.leaseToken,
|
|
510
608
|
runner_id: this.runnerId,
|
|
511
|
-
});
|
|
609
|
+
}, signal);
|
|
512
610
|
}
|
|
513
611
|
requireContext(bindingId) {
|
|
514
612
|
if (!this.context || this.context.bindingId !== bindingId) {
|