mtech-ai-local-runner 0.1.2 → 0.1.4
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 +16 -0
- package/dist/src/cli.js +2 -2
- package/dist/src/daemon/execution-coordinator.js +116 -21
- package/dist/src/daemon/job-poller.js +1 -0
- package/dist/src/daemon/process-lock.js +29 -3
- package/dist/src/daemon/runtime.js +15 -5
- package/dist/src/gateway-client.js +28 -0
- package/dist/src/platform/macos/launchd.js +1 -0
- package/package.json +12 -4
package/README.md
CHANGED
|
@@ -7,3 +7,19 @@ npx mtech-ai-local-runner setup
|
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
The package stores safe runner configuration locally and keeps OAuth refresh credentials in macOS Keychain when available.
|
|
10
|
+
|
|
11
|
+
Public npm installation requires no npm account or publishing OTP. Setup requires Gateway OAuth login, and local Claude Code must already be authenticated (`claude auth status`).
|
|
12
|
+
|
|
13
|
+
## 0.1.4
|
|
14
|
+
|
|
15
|
+
- Independently verify the approved contract payload and task-package fingerprints before creating a runtime binding.
|
|
16
|
+
- Include actual diff and changed-file manifest hashes in terminal events, with a final checkpoint before completion.
|
|
17
|
+
- Fail closed on missing terminal events, failed launches, unavailable evidence, or rejected event reports.
|
|
18
|
+
- Read and acknowledge supervisor controls; preserve the terminal status when reconciling a daemon restart.
|
|
19
|
+
|
|
20
|
+
Existing employees can upgrade their installed daemon with:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npx mtech-ai-local-runner@0.1.4 setup
|
|
24
|
+
npx mtech-ai-local-runner@0.1.4 doctor
|
|
25
|
+
```
|
package/dist/src/cli.js
CHANGED
|
@@ -184,7 +184,7 @@ export async function main(argv, env, io = {}) {
|
|
|
184
184
|
const fetchFn = io.fetchFn || fetch;
|
|
185
185
|
if (isServiceConfig(config)) {
|
|
186
186
|
const localStore = new FileLocalConfigStore(config.stateRoot);
|
|
187
|
-
const launchd = io.launchdService ?? new LaunchdService({ home: env.HOME ?? "", uid: process.getuid?.() ?? 0 });
|
|
187
|
+
const launchd = io.launchdService ?? new LaunchdService({ home: env.HOME ?? "", uid: process.getuid?.() ?? 0, path: env.PATH });
|
|
188
188
|
try {
|
|
189
189
|
if (config.mode === "setup") {
|
|
190
190
|
if (process.platform !== "darwin")
|
|
@@ -203,7 +203,7 @@ export async function main(argv, env, io = {}) {
|
|
|
203
203
|
const authenticated = await runFile(binary, ["auth", "status"], { encoding: "utf8" }).then(() => true).catch(() => false);
|
|
204
204
|
const registered = await gateway.registerLocalRunner({
|
|
205
205
|
deviceFingerprint: createHash("sha256").update(`${process.getuid?.() ?? 0}|${runnerDisplayName()}|${config.gatewayMcpUrl}`).digest("hex"),
|
|
206
|
-
displayName: runnerDisplayName(), packageVersion: "0.1.
|
|
206
|
+
displayName: runnerDisplayName(), packageVersion: "0.1.4",
|
|
207
207
|
capabilities: [{ adapter: "claude_code_cli", version: "1", capabilities: ["repository_read", "repository_edit", "shell_test", "git_diff"], authenticated }],
|
|
208
208
|
});
|
|
209
209
|
await localStore.save({ schemaVersion: 1, gatewayMcpUrl: config.gatewayMcpUrl, runnerId: registered.runner.id, workspaceRoot: config.workspaceRoot, concurrency: 1 });
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { PROTOCOL_VERSION } from "../protocol.js";
|
|
2
|
+
import { PROTOCOL_VERSION, isTerminalEventType, validateEvent, MAX_EVENT_SUMMARY_CHARS } from "../protocol.js";
|
|
3
3
|
function canonical(value) {
|
|
4
4
|
if (Array.isArray(value))
|
|
5
5
|
return value.map(canonical);
|
|
@@ -12,6 +12,17 @@ export function fingerprint(value) {
|
|
|
12
12
|
.replace(/[\u007f-\uffff]/g, (character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
13
13
|
return createHash("sha256").update(json).digest("hex");
|
|
14
14
|
}
|
|
15
|
+
export function contractFingerprint(payload) {
|
|
16
|
+
const unsigned = structuredClone(payload);
|
|
17
|
+
if (unsigned.provenance && typeof unsigned.provenance === "object") {
|
|
18
|
+
delete unsigned.provenance.compiled_at;
|
|
19
|
+
}
|
|
20
|
+
// Contract compiler uses PHP JSON's default escaped slashes. Job packages
|
|
21
|
+
// use JSON_UNESCAPED_SLASHES; the two hashing domains must stay distinct.
|
|
22
|
+
const json = JSON.stringify(canonical(unsigned)).replace(/\//g, "\\/")
|
|
23
|
+
.replace(/[\u007f-\uffff]/g, (character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
24
|
+
return createHash("sha256").update(json).digest("hex");
|
|
25
|
+
}
|
|
15
26
|
function text(value, label) {
|
|
16
27
|
if (typeof value !== "string" || value.length === 0)
|
|
17
28
|
throw new Error(`task package is missing ${label}`);
|
|
@@ -27,7 +38,7 @@ export class ExecutionCoordinator {
|
|
|
27
38
|
const repository = pkg.repository;
|
|
28
39
|
const adapterId = text(pkg.runtime_adapter, "runtime adapter");
|
|
29
40
|
const adapterVersion = text(pkg.runtime_adapter_version, "runtime adapter version");
|
|
30
|
-
const
|
|
41
|
+
const expectedContractFingerprint = text(pkg.contract_fingerprint, "contract fingerprint");
|
|
31
42
|
const workspace = await this.deps.workspaces.create({
|
|
32
43
|
jobId: job.id,
|
|
33
44
|
repositoryKey: text(repository?.key, "repository key"),
|
|
@@ -37,45 +48,129 @@ export class ExecutionCoordinator {
|
|
|
37
48
|
const adapter = await this.deps.registry.select(adapterId, adapterVersion, { worktreePath: workspace.path, jobId: job.id });
|
|
38
49
|
await this.deps.state.save({ jobId: job.id, generation: job.generation, leaseToken: job.leaseToken, phase: "shadow", workspaceManifest: workspace.manifest });
|
|
39
50
|
const gatewayShadow = await this.deps.gateway.shadow(job);
|
|
40
|
-
if (gatewayShadow.
|
|
51
|
+
if (contractFingerprint(gatewayShadow.contractPayload) !== expectedContractFingerprint || gatewayShadow.contractFingerprint !== expectedContractFingerprint || gatewayShadow.taskPackageFingerprint !== fingerprint(pkg)) {
|
|
41
52
|
throw new Error("contract or task-package fingerprint mismatch before binding");
|
|
42
53
|
}
|
|
43
54
|
const binding = await this.deps.gateway.createBinding({ ...job, checkoutPath: workspace.path });
|
|
44
55
|
await this.deps.state.save({ jobId: job.id, generation: job.generation, leaseToken: job.leaseToken, phase: "running", bindingId: binding.bindingId, workspaceManifest: workspace.manifest });
|
|
45
|
-
await adapter.start({ bindingId: binding.bindingId, sessionId: binding.bindingId, taskPackage: binding.taskPackage, resolvedModelId: binding.resolvedModelId });
|
|
46
56
|
let heartbeatFailure = null;
|
|
47
|
-
|
|
48
|
-
|
|
57
|
+
let requestedTerminal;
|
|
58
|
+
let commandCursor = 0;
|
|
59
|
+
let controlsStopped = false;
|
|
60
|
+
let controlsInFlight;
|
|
61
|
+
const readControls = async () => {
|
|
62
|
+
if (!this.deps.gateway.readCommands || controlsStopped || requestedTerminal)
|
|
63
|
+
return;
|
|
64
|
+
if (controlsInFlight)
|
|
65
|
+
return controlsInFlight;
|
|
66
|
+
controlsInFlight = (async () => {
|
|
67
|
+
for (const command of await this.deps.gateway.readCommands(binding.bindingId, commandCursor)) {
|
|
68
|
+
if (controlsStopped)
|
|
69
|
+
return;
|
|
70
|
+
if (["cancel", "pause", "request_replan"].includes(command.command)) {
|
|
71
|
+
requestedTerminal = { eventType: command.command === "cancel" ? "cancelled" : "blocked", summary: `supervisor ${command.command} acknowledged` };
|
|
72
|
+
await adapter.cancel();
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
await adapter.command(command);
|
|
76
|
+
}
|
|
77
|
+
if (!this.deps.gateway.acknowledgeCommand)
|
|
78
|
+
throw new Error("daemon command acknowledgement is unavailable");
|
|
79
|
+
await this.deps.gateway.acknowledgeCommand(binding.bindingId, command.sequence);
|
|
80
|
+
commandCursor = command.sequence;
|
|
81
|
+
if (requestedTerminal)
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
})();
|
|
85
|
+
try {
|
|
86
|
+
await controlsInFlight;
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
controlsInFlight = undefined;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
const heartbeatTimer = this.deps.gateway.heartbeat || this.deps.gateway.readCommands ? setInterval(() => {
|
|
93
|
+
void (async () => { await this.deps.gateway.heartbeat?.(binding.bindingId, job); await readControls(); })().catch(async (error) => {
|
|
49
94
|
heartbeatFailure = error instanceof Error ? error : new Error(String(error));
|
|
50
95
|
await adapter.cancel().catch(() => undefined);
|
|
51
96
|
});
|
|
52
97
|
}, 30_000) : undefined;
|
|
53
98
|
heartbeatTimer?.unref();
|
|
54
|
-
let terminal
|
|
99
|
+
let terminal;
|
|
55
100
|
let sequence = 0;
|
|
101
|
+
let checkpointSeen = false;
|
|
102
|
+
const append = async (event) => {
|
|
103
|
+
let payload = event.payload;
|
|
104
|
+
if (isTerminalEventType(event.eventType)) {
|
|
105
|
+
const evidence = this.deps.terminalEvidenceCollector.collect({ checkoutPath: workspace.path, baseSha: workspace.baseCommitSha });
|
|
106
|
+
payload = { ...payload, diff_hash: evidence.diffHash, changed_file_manifest_hash: evidence.changedFileManifestHash };
|
|
107
|
+
}
|
|
108
|
+
const normalized = {
|
|
109
|
+
protocolVersion: PROTOCOL_VERSION, bindingId: binding.bindingId,
|
|
110
|
+
sequence: sequence + 1, eventType: event.eventType,
|
|
111
|
+
summary: event.summary.slice(0, MAX_EVENT_SUMMARY_CHARS),
|
|
112
|
+
...(payload ? { payload } : {}),
|
|
113
|
+
idempotencyKey: `${binding.bindingId}:${sequence + 1}`, occurredAt: new Date().toISOString(),
|
|
114
|
+
};
|
|
115
|
+
validateEvent(normalized);
|
|
116
|
+
if (!this.deps.gateway.appendEvent)
|
|
117
|
+
throw new Error("daemon event reporting is unavailable");
|
|
118
|
+
await this.deps.gateway.appendEvent(binding.bindingId, normalized);
|
|
119
|
+
sequence = normalized.sequence;
|
|
120
|
+
if (event.eventType === "checkpoint_ready")
|
|
121
|
+
checkpointSeen = true;
|
|
122
|
+
await this.deps.state.save({ jobId: job.id, generation: job.generation, leaseToken: job.leaseToken, phase: "running", bindingId: binding.bindingId, eventCursor: sequence, ...(isTerminalEventType(event.eventType) ? { terminalEventType: event.eventType } : {}), workspaceManifest: workspace.manifest });
|
|
123
|
+
};
|
|
56
124
|
try {
|
|
57
|
-
|
|
125
|
+
await adapter.start({ bindingId: binding.bindingId, sessionId: binding.bindingId, taskPackage: binding.taskPackage, resolvedModelId: binding.resolvedModelId });
|
|
126
|
+
await readControls();
|
|
127
|
+
if (adapter.events && !requestedTerminal) {
|
|
58
128
|
for await (const event of adapter.events()) {
|
|
59
129
|
if (heartbeatFailure)
|
|
60
130
|
throw heartbeatFailure;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
|
|
131
|
+
if (requestedTerminal)
|
|
132
|
+
break;
|
|
133
|
+
if (event.eventType === "completed" && !checkpointSeen) {
|
|
134
|
+
await append({ eventType: "checkpoint_ready", summary: "final checkpoint ready" });
|
|
135
|
+
}
|
|
136
|
+
if (isTerminalEventType(event.eventType)) {
|
|
137
|
+
if (controlsInFlight)
|
|
138
|
+
await controlsInFlight;
|
|
139
|
+
if (requestedTerminal)
|
|
140
|
+
break;
|
|
141
|
+
controlsStopped = true;
|
|
142
|
+
}
|
|
143
|
+
await append(event);
|
|
144
|
+
if (isTerminalEventType(event.eventType) || ["blocked", "scope_conflict"].includes(event.eventType)) {
|
|
145
|
+
controlsStopped = true;
|
|
146
|
+
terminal = event;
|
|
74
147
|
break;
|
|
148
|
+
}
|
|
149
|
+
await readControls();
|
|
75
150
|
}
|
|
76
151
|
}
|
|
152
|
+
if (controlsInFlight)
|
|
153
|
+
await controlsInFlight;
|
|
154
|
+
if (requestedTerminal) {
|
|
155
|
+
terminal = requestedTerminal;
|
|
156
|
+
await append(terminal);
|
|
157
|
+
}
|
|
158
|
+
if (heartbeatFailure)
|
|
159
|
+
throw heartbeatFailure;
|
|
160
|
+
if (!terminal) {
|
|
161
|
+
terminal = { eventType: "failed", summary: "adapter ended without a terminal event" };
|
|
162
|
+
await append(terminal);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
await adapter.cancel().catch(() => undefined);
|
|
167
|
+
// Do not complete a job whose terminal evidence was not acknowledged.
|
|
168
|
+
// A safe bounded reason prevents raw adapter/transport errors leaking.
|
|
169
|
+
terminal = { eventType: "failed", summary: "daemon execution or event reporting failed" };
|
|
170
|
+
await append(terminal);
|
|
77
171
|
}
|
|
78
172
|
finally {
|
|
173
|
+
controlsStopped = true;
|
|
79
174
|
if (heartbeatTimer)
|
|
80
175
|
clearInterval(heartbeatTimer);
|
|
81
176
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdir, open, rm } from "node:fs/promises";
|
|
1
|
+
import { mkdir, open, rm, readFile, unlink } from "node:fs/promises";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
export class ProcessLock {
|
|
4
4
|
path;
|
|
@@ -8,15 +8,41 @@ export class ProcessLock {
|
|
|
8
8
|
}
|
|
9
9
|
async acquire() {
|
|
10
10
|
await mkdir(dirname(this.path), { recursive: true, mode: 0o700 });
|
|
11
|
+
const guard = `${this.path}.acquire`;
|
|
11
12
|
try {
|
|
12
|
-
|
|
13
|
+
await mkdir(guard, { mode: 0o700 });
|
|
13
14
|
}
|
|
14
15
|
catch (error) {
|
|
15
16
|
if (error.code === "EEXIST")
|
|
16
17
|
throw new Error("local runner daemon is already running");
|
|
17
18
|
throw error;
|
|
18
19
|
}
|
|
19
|
-
|
|
20
|
+
try {
|
|
21
|
+
try {
|
|
22
|
+
const stored = (await readFile(this.path, "utf8")).trim();
|
|
23
|
+
const pid = Number(stored);
|
|
24
|
+
if (!/^\d+$/.test(stored) || !Number.isSafeInteger(pid) || pid <= 0)
|
|
25
|
+
throw new Error("local runner daemon lock has an invalid owner");
|
|
26
|
+
try {
|
|
27
|
+
process.kill(pid, 0);
|
|
28
|
+
throw new Error("local runner daemon is already running");
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
if (error.code !== "ESRCH")
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
await unlink(this.path);
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
if (error.code !== "ENOENT")
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
this.handle = await open(this.path, "wx", 0o600);
|
|
41
|
+
await this.handle.writeFile(String(process.pid));
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
await rm(guard, { recursive: true });
|
|
45
|
+
}
|
|
20
46
|
}
|
|
21
47
|
async release() {
|
|
22
48
|
await this.handle?.close();
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
3
|
import { hostname } from "node:os";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
4
5
|
import { join } from "node:path";
|
|
5
6
|
import { mkdirSync, writeFileSync, chmodSync } from "node:fs";
|
|
6
7
|
import { McpGatewayClient } from "../gateway-client.js";
|
|
@@ -14,6 +15,7 @@ import { ExecutionCoordinator } from "./execution-coordinator.js";
|
|
|
14
15
|
import { JobPoller } from "./job-poller.js";
|
|
15
16
|
import { ProcessLock } from "./process-lock.js";
|
|
16
17
|
import { Daemon } from "./daemon.js";
|
|
18
|
+
import { buildTerminalEvidenceCollector } from "../safety/terminal-evidence.js";
|
|
17
19
|
import { McpRelay } from "../auth/mcp-relay.js";
|
|
18
20
|
const execFileAsync = promisify(execFile);
|
|
19
21
|
export async function buildDaemon(input) {
|
|
@@ -52,15 +54,17 @@ export async function buildDaemon(input) {
|
|
|
52
54
|
const versions = Object.fromEntries(probes.filter((probe) => probe.available && probe.authenticated).map((probe) => [probe.id, probe.version]));
|
|
53
55
|
const state = new FileDaemonStateStore(input.stateRoot);
|
|
54
56
|
const coordinator = new ExecutionCoordinator({
|
|
57
|
+
terminalEvidenceCollector: buildTerminalEvidenceCollector(),
|
|
55
58
|
gateway: {
|
|
56
|
-
shadow:
|
|
59
|
+
shadow: (job) => gateway.localJobShadow({ workLoopId: job.workLoopId,
|
|
57
60
|
contractFingerprint: job.gatewayContractFingerprint ?? "",
|
|
58
|
-
taskPackageFingerprint: job.gatewayTaskPackageFingerprint ?? "",
|
|
59
|
-
}),
|
|
61
|
+
taskPackageFingerprint: job.gatewayTaskPackageFingerprint ?? "" }),
|
|
60
62
|
createBinding: async (job) => gateway.createLocalJobBinding({
|
|
61
63
|
jobId: job.id, generation: job.generation, jobLeaseToken: job.leaseToken,
|
|
62
64
|
launchGrant: job.launchGrant, workLoopId: job.workLoopId, checkoutPath: job.checkoutPath,
|
|
63
65
|
}),
|
|
66
|
+
readCommands: (bindingId, cursor) => gateway.readCommands(bindingId, cursor),
|
|
67
|
+
acknowledgeCommand: (bindingId, sequence) => gateway.acknowledgeCommand(bindingId, sequence),
|
|
64
68
|
appendEvent: (bindingId, event) => gateway.appendEvent(bindingId, event),
|
|
65
69
|
heartbeat: async (bindingId, job) => {
|
|
66
70
|
await Promise.all([gateway.heartbeat(bindingId), gateway.heartbeatLocalJob({ jobId: job.id, generation: job.generation, leaseToken: job.leaseToken })]);
|
|
@@ -72,6 +76,12 @@ export async function buildDaemon(input) {
|
|
|
72
76
|
state,
|
|
73
77
|
});
|
|
74
78
|
const poller = new JobPoller({
|
|
79
|
+
heartbeat: () => gateway.heartbeatLocalRunner({
|
|
80
|
+
deviceFingerprint: createHash("sha256").update(`${process.getuid?.() ?? 0}|${runnerDisplayName()}|${input.gatewayMcpUrl}`).digest("hex"),
|
|
81
|
+
displayName: runnerDisplayName(), packageVersion: "0.1.4",
|
|
82
|
+
capabilities: probes.map((probe) => ({ adapter: probe.id, version: probe.version,
|
|
83
|
+
capabilities: probe.capabilities, authenticated: probe.available && probe.authenticated })),
|
|
84
|
+
}).then(() => undefined),
|
|
75
85
|
wait: async () => {
|
|
76
86
|
const claim = await gateway.waitForLocalJob(versions, 25);
|
|
77
87
|
if (claim.status !== "claimed" || !claim.job || !claim.task_package || !claim.lease_token || !claim.launch_grant)
|
|
@@ -96,8 +106,8 @@ export async function buildDaemon(input) {
|
|
|
96
106
|
try {
|
|
97
107
|
const receipt = await gateway.readBindingEvents(previous.bindingId, previous.eventCursor ?? 0);
|
|
98
108
|
const status = receipt.terminal
|
|
99
|
-
? receipt.terminalEventType === "completed" ? "completed"
|
|
100
|
-
: receipt.terminalEventType === "cancelled" ? "cancelled" : "failed"
|
|
109
|
+
? (receipt.terminalEventType ?? previous.terminalEventType) === "completed" ? "completed"
|
|
110
|
+
: (receipt.terminalEventType ?? previous.terminalEventType) === "cancelled" ? "cancelled" : "failed"
|
|
101
111
|
: "failed";
|
|
102
112
|
await gateway.completeLocalJob({
|
|
103
113
|
jobId: previous.jobId, generation: previous.generation, leaseToken: previous.leaseToken, status,
|
|
@@ -403,9 +403,37 @@ export class McpGatewayClient {
|
|
|
403
403
|
capabilities: input.capabilities,
|
|
404
404
|
});
|
|
405
405
|
}
|
|
406
|
+
async heartbeatLocalRunner(input) {
|
|
407
|
+
return this.callTool("local_agent_runner_heartbeat", {
|
|
408
|
+
runner_id: this.runnerId,
|
|
409
|
+
runner: {
|
|
410
|
+
device_fingerprint: input.deviceFingerprint,
|
|
411
|
+
display_name: input.displayName,
|
|
412
|
+
package_version: input.packageVersion,
|
|
413
|
+
capabilities: input.capabilities,
|
|
414
|
+
},
|
|
415
|
+
});
|
|
416
|
+
}
|
|
406
417
|
async localRunnerStatus() {
|
|
407
418
|
return this.callTool("local_agent_runner_status", { runner_id: this.runnerId });
|
|
408
419
|
}
|
|
420
|
+
async localJobShadow(input) {
|
|
421
|
+
const shown = await this.callTool("work_loop_show", { work_loop_id: input.workLoopId });
|
|
422
|
+
if (shown.loop.environment !== "development")
|
|
423
|
+
throw new Error("local agent runtime is development-only");
|
|
424
|
+
const result = await this.callTool("work_execution_contract_show", {
|
|
425
|
+
contract_id: requiredString(shown.loop.work_execution_contract_id, "execution contract ID"),
|
|
426
|
+
});
|
|
427
|
+
if (result.contract.status !== "approved")
|
|
428
|
+
throw new Error("execution contract is not approved");
|
|
429
|
+
const payload = result.contract.contract_payload;
|
|
430
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload))
|
|
431
|
+
throw new Error("Gateway response is missing contract payload");
|
|
432
|
+
const storedFingerprint = requiredString(result.contract.content_fingerprint, "contract fingerprint");
|
|
433
|
+
if (storedFingerprint !== input.contractFingerprint)
|
|
434
|
+
throw new Error("claimed contract fingerprint differs from approved contract");
|
|
435
|
+
return { contractFingerprint: storedFingerprint, taskPackageFingerprint: input.taskPackageFingerprint, contractPayload: payload };
|
|
436
|
+
}
|
|
409
437
|
async createLocalJobBinding(input) {
|
|
410
438
|
const clientSessionReference = uuidv5(`local-agent-job:${input.jobId}:${input.generation}`, uuidv5.URL);
|
|
411
439
|
const result = await this.callTool("local_agent_job_binding_create", {
|
|
@@ -31,6 +31,7 @@ export class LaunchdService {
|
|
|
31
31
|
<plist version="1.0"><dict>
|
|
32
32
|
<key>Label</key><string>${LAUNCHD_LABEL}</string>
|
|
33
33
|
<key>ProgramArguments</key><array><string>${xml(process.execPath)}</string><string>${xml(executablePath)}</string><string>run</string><string>--state-root=${xml(stateRoot)}</string></array>
|
|
34
|
+
<key>EnvironmentVariables</key><dict><key>PATH</key><string>${xml(this.deps.path ?? process.env.PATH ?? "/usr/bin:/bin:/usr/sbin:/sbin")}</string></dict>
|
|
34
35
|
<key>RunAtLoad</key><true/><key>KeepAlive</key><true/>
|
|
35
36
|
<key>ProcessType</key><string>Background</string>
|
|
36
37
|
</dict></plist>
|
package/package.json
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mtech-ai-local-runner",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
|
-
"bin": {
|
|
7
|
-
|
|
6
|
+
"bin": {
|
|
7
|
+
"mtec-ai-local-runner": "dist/src/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist/src",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE"
|
|
13
|
+
],
|
|
8
14
|
"type": "module",
|
|
9
|
-
"engines": {
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=20"
|
|
17
|
+
},
|
|
10
18
|
"scripts": {
|
|
11
19
|
"build": "tsc",
|
|
12
20
|
"prepack": "npm run build && chmod 755 dist/src/cli.js",
|