mtech-ai-local-runner 0.1.72 → 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/cli.js +4 -0
- package/dist/src/commands/fix.js +106 -26
- package/dist/src/daemon/execution-coordinator.js +65 -9
- package/dist/src/daemon/fix-consent.js +98 -0
- package/dist/src/daemon/manual-codex.js +2 -2
- package/dist/src/daemon/runtime.js +31 -5
- package/dist/src/gateway-client.js +88 -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/transport/sync-dispatch.js +9 -0
- 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() {
|
package/dist/src/cli.js
CHANGED
|
@@ -518,5 +518,9 @@ export function isDirectCliInvocation(moduleUrl, argvPath) {
|
|
|
518
518
|
if (isDirectCliInvocation(import.meta.url, process.argv[1])) {
|
|
519
519
|
main(process.argv.slice(2), process.env).then((code) => {
|
|
520
520
|
process.exitCode = code;
|
|
521
|
+
// A daemon that stopped (to apply Fix consent, decision baf0cd89) must leave so launchd starts a fresh one.
|
|
522
|
+
// The unref'd timer only fires if a socket or timer still holds the event loop five seconds later.
|
|
523
|
+
if (process.argv[2] === "run")
|
|
524
|
+
setTimeout(() => process.exit(code), 5000).unref();
|
|
521
525
|
});
|
|
522
526
|
}
|
package/dist/src/commands/fix.js
CHANGED
|
@@ -25,19 +25,108 @@ function flags(argv) {
|
|
|
25
25
|
return values;
|
|
26
26
|
}
|
|
27
27
|
/** A job owns the host until it ends: never replace or restart under it. */
|
|
28
|
-
async function
|
|
28
|
+
export async function fixHostBusy(stateRoot) {
|
|
29
29
|
for (const file of BUSY)
|
|
30
|
-
if (await exists(path.join(
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
if (await exists(path.join(stateRoot, file)))
|
|
31
|
+
return true;
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
const JOB_ACTIVE = 'FIX_HOST_JOB_ACTIVE: wait until the current job ends, then run this again.';
|
|
35
|
+
async function idle(io) {
|
|
36
|
+
if (await fixHostBusy(io.stateRoot)) {
|
|
37
|
+
io.stderr(JOB_ACTIVE);
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
34
40
|
return true;
|
|
35
41
|
}
|
|
36
|
-
async function backup(
|
|
37
|
-
const dir = path.join(
|
|
42
|
+
async function backup(stateRoot, file) {
|
|
43
|
+
const dir = path.join(stateRoot, 'backups');
|
|
38
44
|
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
39
45
|
await copyFile(file, path.join(dir, `local-fix-host-${new Date().toISOString().replace(/[:.]/g, '-')}.json`));
|
|
40
46
|
}
|
|
47
|
+
/** The existing host, when it is a v3 host this runner would load; anything else is replaced whole. */
|
|
48
|
+
async function currentConsentHost(file, runnerId) {
|
|
49
|
+
let raw;
|
|
50
|
+
try {
|
|
51
|
+
const s = await lstat(file);
|
|
52
|
+
if (!s.isFile() || s.isSymbolicLink() || s.size > 128 * 1024)
|
|
53
|
+
return undefined;
|
|
54
|
+
raw = JSON.parse(await readFile(file, 'utf8'));
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw) || Object.keys(raw).sort().join() !== 'consent,enabled,git,runnerId,schema')
|
|
60
|
+
return undefined;
|
|
61
|
+
if (raw.schema !== 'local-fix-host-installation-v3' || raw.enabled !== true || raw.runnerId !== runnerId)
|
|
62
|
+
return undefined;
|
|
63
|
+
const git = raw.git;
|
|
64
|
+
if (!git || typeof git !== 'object' || Array.isArray(git) || Object.keys(git).sort().join() !== 'executable,sha256' || typeof git.executable !== 'string' || typeof git.sha256 !== 'string')
|
|
65
|
+
return undefined;
|
|
66
|
+
try {
|
|
67
|
+
assertLocalFixConsent(raw.consent);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
return raw;
|
|
73
|
+
}
|
|
74
|
+
const union = (first, second) => [...first, ...second.filter(value => !first.includes(value))];
|
|
75
|
+
/**
|
|
76
|
+
* The one writer of local-fix-host.json consent, shared by `fix enable` (replace) and the daemon applying a Slack
|
|
77
|
+
* grant (extend; Work Item 6f9e64ae, decision baf0cd89). Refuses while a job or recovery is pending. Extend merges
|
|
78
|
+
* into a v3 host this runner would load and keeps its pinned Git; any other file is backed up and replaced by a
|
|
79
|
+
* fresh v3 host with newly pinned Git, as `fix enable` writes. Writes are owner-only and atomic; a replaced file is
|
|
80
|
+
* kept under backups/. Restarting the runner is the caller's job.
|
|
81
|
+
*/
|
|
82
|
+
export async function applyFixConsent(input) {
|
|
83
|
+
const requested = { repository_keys: [...input.repositoryKeys], verification_modes: [...input.verificationModes] };
|
|
84
|
+
try {
|
|
85
|
+
assertLocalFixConsent(requested);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return { ok: false, code: 'FIX_CONSENT_INVALID' };
|
|
89
|
+
}
|
|
90
|
+
if (await fixHostBusy(input.stateRoot))
|
|
91
|
+
return { ok: false, code: 'FIX_HOST_JOB_ACTIVE' };
|
|
92
|
+
const file = path.join(input.stateRoot, HOST_FILE);
|
|
93
|
+
const existing = input.mode === 'extend' ? await currentConsentHost(file, input.runnerId) : undefined;
|
|
94
|
+
let host;
|
|
95
|
+
if (existing) {
|
|
96
|
+
const consent = { repository_keys: union(existing.consent.repository_keys, requested.repository_keys), verification_modes: union(existing.consent.verification_modes, requested.verification_modes) };
|
|
97
|
+
try {
|
|
98
|
+
assertLocalFixConsent(consent);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return { ok: false, code: 'FIX_CONSENT_INVALID' };
|
|
102
|
+
}
|
|
103
|
+
if (consent.repository_keys.length === existing.consent.repository_keys.length && consent.verification_modes.length === existing.consent.verification_modes.length)
|
|
104
|
+
return { ok: true, changed: false, consent: existing.consent };
|
|
105
|
+
host = { schema: 'local-fix-host-installation-v3', enabled: true, runnerId: input.runnerId, git: { executable: existing.git.executable, sha256: existing.git.sha256 }, consent };
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
const executable = await realpath(input.git ?? await (input.findGit ?? defaultGit)());
|
|
109
|
+
const git = { executable, sha256: await measureCommandFile(executable) };
|
|
110
|
+
await assertPinnedGit(git);
|
|
111
|
+
host = { schema: 'local-fix-host-installation-v3', enabled: true, runnerId: input.runnerId, git, consent: requested };
|
|
112
|
+
}
|
|
113
|
+
const privateRoot = path.join(input.stateRoot, 'local-fix-publication');
|
|
114
|
+
if (!await exists(privateRoot))
|
|
115
|
+
await mkdir(privateRoot, { mode: 0o700 });
|
|
116
|
+
const s = await lstat(privateRoot);
|
|
117
|
+
if (!s.isDirectory() || s.isSymbolicLink() || (s.mode & 0o077) !== 0)
|
|
118
|
+
return { ok: false, code: 'FIX_HOST_PRIVATE_DIRECTORY_INSECURE', privateRoot };
|
|
119
|
+
const serialized = JSON.stringify(host, null, 1);
|
|
120
|
+
let previous;
|
|
121
|
+
if (await exists(file)) {
|
|
122
|
+
previous = await readFile(file, 'utf8').catch(() => undefined);
|
|
123
|
+
await backup(input.stateRoot, file);
|
|
124
|
+
}
|
|
125
|
+
const temporary = path.join(input.stateRoot, `.${HOST_FILE}.${randomUUID()}`);
|
|
126
|
+
await writeFile(temporary, serialized, { mode: 0o600, flag: 'wx' });
|
|
127
|
+
await rename(temporary, file);
|
|
128
|
+
return { ok: true, changed: previous !== serialized, consent: host.consent };
|
|
129
|
+
}
|
|
41
130
|
async function restart(io, summary) {
|
|
42
131
|
if (await io.serviceLoaded()) {
|
|
43
132
|
await io.restartService();
|
|
@@ -77,7 +166,7 @@ export async function runFixCommand(argv, io) {
|
|
|
77
166
|
io.stdout(JSON.stringify({ ok: true, removed: false }));
|
|
78
167
|
return 0;
|
|
79
168
|
}
|
|
80
|
-
await backup(io, file);
|
|
169
|
+
await backup(io.stateRoot, file);
|
|
81
170
|
await rm(file);
|
|
82
171
|
return restart(io, { ok: true, removed: true });
|
|
83
172
|
}
|
|
@@ -103,24 +192,15 @@ export async function runFixCommand(argv, io) {
|
|
|
103
192
|
io.stderr('MESSENGER_MODE_HAS_NO_EXECUTOR: log in to Claude Code or Codex, then rerun npx mtech-ai-local-runner@latest setup before fix enable.');
|
|
104
193
|
return 1;
|
|
105
194
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const s = await lstat(privateRoot);
|
|
115
|
-
if (!s.isDirectory() || s.isSymbolicLink() || (s.mode & 0o077) !== 0) {
|
|
116
|
-
io.stderr('FIX_HOST_PRIVATE_DIRECTORY_INSECURE: ' + privateRoot + ' must be a 0700 directory owned by this user.');
|
|
195
|
+
const applied = await applyFixConsent({ stateRoot: io.stateRoot, runnerId: local.runnerId, repositoryKeys: consent.repository_keys, verificationModes: consent.verification_modes, mode: 'replace', git: values.get('git'), findGit: io.findGit });
|
|
196
|
+
if (!applied.ok) {
|
|
197
|
+
if (applied.code === 'FIX_HOST_JOB_ACTIVE')
|
|
198
|
+
io.stderr(JOB_ACTIVE);
|
|
199
|
+
else if (applied.code === 'FIX_HOST_PRIVATE_DIRECTORY_INSECURE')
|
|
200
|
+
io.stderr('FIX_HOST_PRIVATE_DIRECTORY_INSECURE: ' + applied.privateRoot + ' must be a 0700 directory owned by this user.');
|
|
201
|
+
else
|
|
202
|
+
io.stderr('FIX_CONSENT_INVALID: give registered repository keys (lowercase, comma separated, no repeats) and modes gateway_publish_v1 or gateway_ci_v1.');
|
|
117
203
|
return 1;
|
|
118
204
|
}
|
|
119
|
-
|
|
120
|
-
if (await exists(file))
|
|
121
|
-
await backup(io, file);
|
|
122
|
-
const temporary = path.join(io.stateRoot, `.${HOST_FILE}.${randomUUID()}`);
|
|
123
|
-
await writeFile(temporary, JSON.stringify(host, null, 1), { mode: 0o600, flag: 'wx' });
|
|
124
|
-
await rename(temporary, file);
|
|
125
|
-
return restart(io, { ok: true, repository_keys: consent.repository_keys, verification_modes: consent.verification_modes });
|
|
205
|
+
return restart(io, { ok: true, repository_keys: applied.consent.repository_keys, verification_modes: applied.consent.verification_modes });
|
|
126
206
|
}
|
|
@@ -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();
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { applyFixConsent, fixHostBusy } from "../commands/fix.js";
|
|
2
|
+
const covers = (report, grant) => !!report && report.repository_keys.includes(grant.repositoryKey) && report.verification_modes.includes(grant.verificationMode);
|
|
3
|
+
export class FixConsentRuntime {
|
|
4
|
+
deps;
|
|
5
|
+
/** Grants deferred while busy. Each later sync's own list replaces them, so a grant the Gateway dropped is never applied from memory. */
|
|
6
|
+
deferred = [];
|
|
7
|
+
due = false;
|
|
8
|
+
stopping = false;
|
|
9
|
+
logged = new Set();
|
|
10
|
+
queue = Promise.resolve();
|
|
11
|
+
constructor(deps) {
|
|
12
|
+
this.deps = deps;
|
|
13
|
+
}
|
|
14
|
+
/** True once a changed host waits for the restart; new claims are held until then. */
|
|
15
|
+
get restartDue() { return this.due; }
|
|
16
|
+
/** Applies the grants of one sync when idle. The restart waits for afterSync, so the rest of that sync (its session opens) is still handled. */
|
|
17
|
+
handleGrants(grants) {
|
|
18
|
+
return this.serial(async () => { this.deferred = grants.slice(0, 20); await this.apply(); });
|
|
19
|
+
}
|
|
20
|
+
/** After every sync: drops deferred grants the sync no longer lists, and restarts once a changed host is due and the runner is idle. */
|
|
21
|
+
afterSync(result) {
|
|
22
|
+
return this.serial(async () => {
|
|
23
|
+
if (!Array.isArray(result.fixConsentGrants) || result.fixConsentGrants.length === 0)
|
|
24
|
+
this.deferred = [];
|
|
25
|
+
await this.apply();
|
|
26
|
+
await this.restartWhenIdle();
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
serial(task) {
|
|
30
|
+
const next = this.queue.then(task, task);
|
|
31
|
+
this.queue = next.catch(() => undefined);
|
|
32
|
+
return next;
|
|
33
|
+
}
|
|
34
|
+
async busy() {
|
|
35
|
+
return this.deps.jobRunning() || await fixHostBusy(this.deps.stateRoot);
|
|
36
|
+
}
|
|
37
|
+
once(key, event) {
|
|
38
|
+
if (this.logged.has(key))
|
|
39
|
+
return;
|
|
40
|
+
this.logged.add(key);
|
|
41
|
+
this.deps.log(event);
|
|
42
|
+
}
|
|
43
|
+
async apply() {
|
|
44
|
+
if (this.stopping)
|
|
45
|
+
return;
|
|
46
|
+
const pending = this.deferred.filter(grant => !covers(this.deps.report(), grant));
|
|
47
|
+
if (pending.length > 0 && await this.busy()) {
|
|
48
|
+
this.deferred = pending;
|
|
49
|
+
for (const grant of pending)
|
|
50
|
+
this.once(`deferred:${grant.id}`, { event: "fix_consent_deferred", grant_id: grant.id, repository_key: grant.repositoryKey, reason: "job_active" });
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
this.deferred = [];
|
|
54
|
+
const applied = [];
|
|
55
|
+
for (const grant of pending) {
|
|
56
|
+
let result;
|
|
57
|
+
try {
|
|
58
|
+
result = await applyFixConsent({ stateRoot: this.deps.stateRoot, runnerId: this.deps.runnerId,
|
|
59
|
+
repositoryKeys: [grant.repositoryKey], verificationModes: [grant.verificationMode], mode: "extend" });
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// Git could not be pinned or the file could not be written; the error text may name local paths, so only the code is logged.
|
|
63
|
+
result = { ok: false, code: "FIX_CONSENT_APPLY_FAILED" };
|
|
64
|
+
}
|
|
65
|
+
if (!result.ok && result.code === "FIX_HOST_JOB_ACTIVE") {
|
|
66
|
+
this.deferred.push(grant);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (!result.ok) {
|
|
70
|
+
this.reject(grant, result.code);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (result.changed) {
|
|
74
|
+
this.due = true;
|
|
75
|
+
applied.push(grant);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
// Already on disk, yet the host loaded at start does not report it: a restart would load the same file again.
|
|
79
|
+
if (!this.due)
|
|
80
|
+
this.reject(grant, "FIX_HOST_NOT_LOADED");
|
|
81
|
+
}
|
|
82
|
+
if (applied.length > 0) {
|
|
83
|
+
this.deps.log({ event: "fix_consent_applied", grant_ids: applied.map(grant => grant.id),
|
|
84
|
+
repository_keys: [...new Set(applied.map(grant => grant.repositoryKey))], verification_modes: [...new Set(applied.map(grant => grant.verificationMode))] });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async restartWhenIdle() {
|
|
88
|
+
if (this.stopping || !this.due || await fixHostBusy(this.deps.stateRoot) || this.deps.jobRunning())
|
|
89
|
+
return;
|
|
90
|
+
// No await between the last check and the stop: the daemon stops its job runner first, so no claim starts after it.
|
|
91
|
+
this.stopping = true;
|
|
92
|
+
this.deps.log({ event: "runner_restarting", reason: "fix_consent" });
|
|
93
|
+
this.deps.restart();
|
|
94
|
+
}
|
|
95
|
+
reject(grant, code) {
|
|
96
|
+
this.once(`rejected:${grant.id}:${code}`, { event: "fix_consent_rejected", grant_id: grant.id, repository_key: grant.repositoryKey, code });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -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 }; }
|