mtech-ai-local-runner 0.1.112 → 0.1.113
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 +20 -49
- package/dist/src/asks/claude-mod.js +48 -87
- package/dist/src/asks/commands.js +17 -33
- package/dist/src/asks/doctor.js +3 -4
- package/dist/src/cli.js +3 -3
- package/dist/src/daemon/codex-app-host-entry.js +94 -0
- package/dist/src/daemon/codex-app-setup.js +125 -0
- package/dist/src/daemon/codex-app-tools-host.js +82 -0
- package/dist/src/daemon/runtime.js +17 -3
- package/dist/src/safety/claude-production-host.js +14 -3
- package/dist/src/safety/d4-host-composition.js +40 -1
- package/package.json +1 -3
- package/claude-mod/.claude-plugin/marketplace.json +0 -1
- package/claude-mod/.claude-plugin/plugin.json +0 -1
- package/claude-mod/hooks/gateway.ts +0 -47
- package/claude-mod/hooks/hooks.json +0 -34
- package/claude-mod/hooks/model.ts +0 -268
- package/claude-mod/hooks/register.tsx +0 -460
- package/claude-mod/tsconfig.json +0 -3
- package/claude-mod/types/index.d.ts +0 -28
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
3
|
+
const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
4
|
+
const text = (value) => typeof value === "string" && value.trim().length > 0;
|
|
5
|
+
const only = (value, allowed) => Object.keys(value).every(key => allowed.includes(key));
|
|
6
|
+
/**
|
|
7
|
+
* Consumes a fresh Gateway prepare receipt, never creates its own authority or binds a recipient.
|
|
8
|
+
* Gateway atomically grants a one-use dispatch claim before the external side effect. The local
|
|
9
|
+
* fence additionally blocks duplicate calls on this adapter, including lost claim acknowledgements.
|
|
10
|
+
* No concrete native endpoint is wired here. A caller must supply the supported authenticated host.
|
|
11
|
+
*/
|
|
12
|
+
export class CodexAppSetupAdapter {
|
|
13
|
+
deps;
|
|
14
|
+
attempted = new Set();
|
|
15
|
+
hostId;
|
|
16
|
+
projectId;
|
|
17
|
+
constructor(deps) {
|
|
18
|
+
this.deps = deps;
|
|
19
|
+
if (!text(deps.approvedHostId) || !text(deps.approvedProjectId))
|
|
20
|
+
throw new Error("Explicit approved host and project required");
|
|
21
|
+
this.hostId = deps.approvedHostId;
|
|
22
|
+
this.projectId = deps.approvedProjectId;
|
|
23
|
+
}
|
|
24
|
+
async execute(input) {
|
|
25
|
+
if (!this.validReceipt(input))
|
|
26
|
+
return { status: "rejected" };
|
|
27
|
+
if (!input.can_create || this.attempted.has(input.intent_id))
|
|
28
|
+
return { status: "skipped" };
|
|
29
|
+
if (input.setup_outcome !== "reserved" || input.target_ref !== null || input.authority_failure !== null
|
|
30
|
+
|| input.thread_id !== undefined || input.pending_handle !== undefined || input.client_link_id !== undefined)
|
|
31
|
+
return { status: "rejected" };
|
|
32
|
+
const args = this.createArguments(input.create_thread);
|
|
33
|
+
if (!args || createHash("sha256").update(args.prompt, "utf8").digest("hex") !== input.setup_prompt_sha256)
|
|
34
|
+
return { status: "rejected" };
|
|
35
|
+
const intentId = input.intent_id;
|
|
36
|
+
const expiresAt = input.expires_at;
|
|
37
|
+
const setupPromptSha256 = input.setup_prompt_sha256;
|
|
38
|
+
// Fence before any await. A discovery failure cannot cause an accidental concurrent retry.
|
|
39
|
+
this.attempted.add(intentId);
|
|
40
|
+
try {
|
|
41
|
+
const projects = await this.deps.host.listProjects();
|
|
42
|
+
if (!Array.isArray(projects) || !projects.some(project => record(project)
|
|
43
|
+
&& project.projectId === this.projectId && project.hostId === this.hostId))
|
|
44
|
+
return { status: "rejected" };
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return { status: "rejected" };
|
|
48
|
+
}
|
|
49
|
+
if (expiresAt <= Date.now() / 1000)
|
|
50
|
+
return { status: "rejected" };
|
|
51
|
+
let claim;
|
|
52
|
+
try {
|
|
53
|
+
claim = await this.deps.claimDispatch({ intent_id: intentId, host_id: this.hostId, project_id: this.projectId, outcome: "dispatch" });
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return { status: "uncertain", reportRecorded: false };
|
|
57
|
+
}
|
|
58
|
+
if (!this.validReceipt(claim) || claim.intent_id !== intentId)
|
|
59
|
+
return { status: "uncertain", reportRecorded: false };
|
|
60
|
+
if (!claim.can_create)
|
|
61
|
+
return { status: "skipped" };
|
|
62
|
+
if (claim.setup_prompt_sha256 !== setupPromptSha256 || createHash("sha256").update(args.prompt, "utf8").digest("hex") !== claim.setup_prompt_sha256
|
|
63
|
+
|| claim.setup_outcome !== "dispatching" || claim.target_ref !== null || claim.authority_failure !== null
|
|
64
|
+
|| claim.thread_id !== undefined || claim.pending_handle !== undefined || claim.client_link_id !== undefined
|
|
65
|
+
|| expiresAt <= Date.now() / 1000)
|
|
66
|
+
return { status: "uncertain", reportRecorded: false };
|
|
67
|
+
const report = { intent_id: intentId, host_id: this.hostId, project_id: this.projectId, outcome: "uncertain" };
|
|
68
|
+
try {
|
|
69
|
+
const result = await this.deps.host.createThread(args);
|
|
70
|
+
if (record(result)) {
|
|
71
|
+
if (text(result.threadId) && UUID.test(result.threadId) && result.hostId === this.hostId && result.clientThreadId === undefined) {
|
|
72
|
+
report.outcome = "created";
|
|
73
|
+
report.thread_id = result.threadId;
|
|
74
|
+
}
|
|
75
|
+
else if (text(result.clientThreadId) && result.threadId === undefined
|
|
76
|
+
&& (result.hostId === undefined || result.hostId === this.hostId)) {
|
|
77
|
+
report.outcome = "pending";
|
|
78
|
+
report.pending_handle = result.clientThreadId;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// The host may have created the thread before losing its acknowledgement. Never replay.
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
await this.deps.report(report);
|
|
87
|
+
return { status: report.outcome, reportRecorded: true };
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
// Even a successful host response is not reconciled when Gateway's acknowledgement is lost.
|
|
91
|
+
return { status: "uncertain", reportRecorded: false };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
validReceipt(input) {
|
|
95
|
+
if (!record(input) || !only(input, ["intent_id", "host_id", "project_id", "can_create", "create_thread", "setup_outcome", "target_ref", "grants_execution", "expires_at", "authority_failure", "thread_id", "pending_handle", "client_link_id", "setup_prompt_sha256"])
|
|
96
|
+
|| !text(input.intent_id) || !UUID.test(input.intent_id) || input.host_id !== this.hostId || input.project_id !== this.projectId
|
|
97
|
+
|| typeof input.setup_prompt_sha256 !== "string" || !/^[0-9a-f]{64}$/.test(input.setup_prompt_sha256)
|
|
98
|
+
|| typeof input.can_create !== "boolean" || input.grants_execution !== false
|
|
99
|
+
|| typeof input.expires_at !== "number" || !Number.isSafeInteger(input.expires_at) || input.expires_at <= Date.now() / 1000
|
|
100
|
+
|| (input.authority_failure !== null && !text(input.authority_failure))
|
|
101
|
+
|| !text(input.setup_outcome) || (input.target_ref !== null && !text(input.target_ref)))
|
|
102
|
+
return false;
|
|
103
|
+
for (const key of ["thread_id", "client_link_id"]) {
|
|
104
|
+
if (input[key] !== undefined && (!text(input[key]) || !UUID.test(input[key])))
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
if (input.pending_handle !== undefined && (!text(input.pending_handle) || !/^[a-zA-Z0-9_:.-]{1,128}$/.test(input.pending_handle)))
|
|
108
|
+
return false;
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
createArguments(input) {
|
|
112
|
+
if (!record(input) || !only(input, ["prompt", "title", "target"]) || !text(input.prompt)
|
|
113
|
+
|| (input.title !== undefined && !text(input.title)))
|
|
114
|
+
return null;
|
|
115
|
+
const target = input.target;
|
|
116
|
+
if (!record(target) || !only(target, ["type", "projectId", "environment"])
|
|
117
|
+
|| target.type !== "project" || target.projectId !== this.projectId
|
|
118
|
+
|| !record(target.environment) || !only(target.environment, ["type"])
|
|
119
|
+
|| target.environment.type !== "local")
|
|
120
|
+
return null;
|
|
121
|
+
// Copy only validated fields before awaiting discovery; do not pass mutable caller objects.
|
|
122
|
+
return { prompt: input.prompt, ...(input.title === undefined ? {} : { title: input.title }),
|
|
123
|
+
target: { type: "project", projectId: this.projectId, environment: { type: "local" } } };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { CodexAppSetupAdapter } from "./codex-app-setup.js";
|
|
2
|
+
const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
3
|
+
const uuid = (value) => typeof value === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
|
|
4
|
+
/** The observed official host envelope has one JSON text block. Fail closed on schema drift. */
|
|
5
|
+
function data(value) {
|
|
6
|
+
if (!object(value) || (value.isError !== undefined && value.isError !== false)
|
|
7
|
+
|| !Array.isArray(value.content) || value.content.length !== 1)
|
|
8
|
+
throw new Error("Unsupported app tool envelope");
|
|
9
|
+
const block = value.content[0];
|
|
10
|
+
if (!object(block) || block.type !== "text" || typeof block.text !== "string")
|
|
11
|
+
throw new Error("Missing app tool JSON");
|
|
12
|
+
const parsed = JSON.parse(block.text);
|
|
13
|
+
if (!object(parsed))
|
|
14
|
+
throw new Error("Unsupported app tool result");
|
|
15
|
+
return parsed;
|
|
16
|
+
}
|
|
17
|
+
export class CodexAppToolsHost {
|
|
18
|
+
tools;
|
|
19
|
+
hostId;
|
|
20
|
+
constructor(tools, hostId) {
|
|
21
|
+
this.tools = tools;
|
|
22
|
+
this.hostId = hostId;
|
|
23
|
+
}
|
|
24
|
+
async listProjects() {
|
|
25
|
+
const result = data(await this.tools.listProjects({}));
|
|
26
|
+
if (result.schemaVersion !== 2 || !Array.isArray(result.projects))
|
|
27
|
+
throw new Error("Unsupported project schema");
|
|
28
|
+
return result.projects.filter((project) => object(project)
|
|
29
|
+
&& project.projectKind === "local" && typeof project.projectId === "string" && project.projectId.length > 0
|
|
30
|
+
&& project.hostId === this.hostId).map(project => ({ projectId: project.projectId, hostId: this.hostId }));
|
|
31
|
+
}
|
|
32
|
+
async createThread(args) {
|
|
33
|
+
// Only the existing adapter calls this after Gateway's durable dispatch claim.
|
|
34
|
+
return data(await this.tools.createThread(args));
|
|
35
|
+
}
|
|
36
|
+
/** Observation only: never binds a session, reports bound, starts a turn or retries creation. */
|
|
37
|
+
async track(receipt) {
|
|
38
|
+
const unverified = { status: "unverified", grantsExecution: false };
|
|
39
|
+
if (!object(receipt) || Object.keys(receipt).some(key => !["threadId", "pendingHandle"].includes(key)))
|
|
40
|
+
return unverified;
|
|
41
|
+
if (receipt.pendingHandle !== undefined) {
|
|
42
|
+
if (receipt.threadId !== undefined || typeof receipt.pendingHandle !== "string"
|
|
43
|
+
|| !/^[a-zA-Z0-9_:.-]{1,128}$/.test(receipt.pendingHandle))
|
|
44
|
+
return unverified;
|
|
45
|
+
// No exposed app tool resolves clientThreadId. In particular do not feed it to read_thread.
|
|
46
|
+
return { status: "pending_resolution_unsupported", pendingHandle: receipt.pendingHandle, grantsExecution: false };
|
|
47
|
+
}
|
|
48
|
+
if (!uuid(receipt.threadId))
|
|
49
|
+
return unverified;
|
|
50
|
+
const threadId = receipt.threadId;
|
|
51
|
+
try {
|
|
52
|
+
const result = data(await this.tools.readThread({ threadId, hostId: this.hostId, turnLimit: 1, includeOutputs: false, maxOutputCharsPerItem: 100 }));
|
|
53
|
+
if (result.schemaVersion !== 1 || !object(result.thread) || result.thread.id !== threadId
|
|
54
|
+
|| result.thread.hostId !== this.hostId || result.thread.kind !== "codex")
|
|
55
|
+
return unverified;
|
|
56
|
+
return { status: "identity_observed", threadId, hostId: this.hostId, grantsExecution: false };
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return unverified;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export function connectCodexAppSetup(deps) {
|
|
64
|
+
const host = new CodexAppToolsHost(deps.tools, deps.approvedHostId);
|
|
65
|
+
return { host, adapter: new CodexAppSetupAdapter({ ...deps, host }),
|
|
66
|
+
/** Pass normalized data from Gateway report(outcome: status), not an old prepare response. */
|
|
67
|
+
trackReceipt: async (receipt) => {
|
|
68
|
+
const unverified = { status: "unverified", grantsExecution: false };
|
|
69
|
+
if (!object(receipt) || !uuid(receipt.intent_id) || receipt.host_id !== deps.approvedHostId
|
|
70
|
+
|| receipt.project_id !== deps.approvedProjectId || receipt.can_create !== false
|
|
71
|
+
|| receipt.grants_execution !== false || receipt.authority_failure !== null
|
|
72
|
+
|| receipt.create_thread !== undefined)
|
|
73
|
+
return unverified;
|
|
74
|
+
if (receipt.setup_outcome === "pending" && receipt.thread_id === undefined && typeof receipt.pending_handle === "string") {
|
|
75
|
+
return host.track({ pendingHandle: receipt.pending_handle });
|
|
76
|
+
}
|
|
77
|
+
if (receipt.setup_outcome === "created" && uuid(receipt.thread_id)) {
|
|
78
|
+
return host.track({ threadId: receipt.thread_id });
|
|
79
|
+
}
|
|
80
|
+
return unverified;
|
|
81
|
+
} };
|
|
82
|
+
}
|
|
@@ -80,9 +80,19 @@ export async function buildDaemon(input) {
|
|
|
80
80
|
const gateway = new McpGatewayClient({ mcpUrl: input.gatewayMcpUrl, tokenProvider: input.tokenProvider, runnerId: input.runnerId, fetchFn: input.fetchFn });
|
|
81
81
|
// Only this daemon's currently executing claim may revalidate a D4 pre-spawn request.
|
|
82
82
|
let activeD4Job, activeD4Binding;
|
|
83
|
-
let d4Host = input.d4ExecutionHost;
|
|
83
|
+
let d4Host = input.d4ExecutionHost, d4ProductionBinding;
|
|
84
84
|
if (!d4Host) {
|
|
85
|
-
const composition = await composeReviewedD4Host({
|
|
85
|
+
const composition = await composeReviewedD4Host({ currentJob: () => {
|
|
86
|
+
const job = activeD4Job;
|
|
87
|
+
if (!job || !activeD4Binding)
|
|
88
|
+
return undefined;
|
|
89
|
+
const pkg = job.taskPackage, repository = pkg.repository;
|
|
90
|
+
return { descriptor: pkg.candidate_review_execution, context: { jobId: job.id, bindingId: activeD4Binding, generation: job.generation,
|
|
91
|
+
launchFingerprint: job.launchFingerprint, contractFingerprint: job.gatewayContractFingerprint, taskPackageFingerprint: job.gatewayTaskPackageFingerprint },
|
|
92
|
+
request: { adapterId: String(pkg.runtime_adapter), adapterVersion: String(pkg.runtime_adapter_version), jobId: job.id, generation: job.generation,
|
|
93
|
+
modelId: String(pkg.resolved_model_id), sourceSha: String(repository?.base_commit_sha), requirements: pkg.security_requirements,
|
|
94
|
+
launchFingerprint: job.launchFingerprint, contractFingerprint: job.gatewayContractFingerprint, taskPackageFingerprint: job.gatewayTaskPackageFingerprint } };
|
|
95
|
+
}, binaryIdentity: () => claude.binaryIdentity(), revalidateJob: async (descriptor, context) => {
|
|
86
96
|
const job = activeD4Job;
|
|
87
97
|
if (!job || context.jobId !== job.id || context.bindingId !== activeD4Binding || context.generation !== 1 || job.generation !== 1
|
|
88
98
|
|| context.launchFingerprint !== job.launchFingerprint || context.contractFingerprint !== job.gatewayContractFingerprint
|
|
@@ -97,6 +107,8 @@ export async function buildDaemon(input) {
|
|
|
97
107
|
throw Error('D4_JOB_CONTEXT_REJECTED');
|
|
98
108
|
} });
|
|
99
109
|
d4Host = composition.host;
|
|
110
|
+
if (composition.status === 'composed')
|
|
111
|
+
d4ProductionBinding = composition.productionBinding;
|
|
100
112
|
process.stderr.write(`${JSON.stringify({ event: 'd4_host', status: composition.status })}\n`);
|
|
101
113
|
}
|
|
102
114
|
// The menu bar app (Work Item b9218937): counts and clickable notices; osascript stays the fallback.
|
|
@@ -144,7 +156,9 @@ export async function buildDaemon(input) {
|
|
|
144
156
|
options: async ({ worktreePath, jobId }) => createClaudeCodeAdapterOptions({
|
|
145
157
|
worktreePath, jobId, stateRoot: input.stateRoot, env: input.env,
|
|
146
158
|
maxBudgetUsd: Number.MAX_SAFE_INTEGER, hookExecutablePath: hook.hookScriptPath, hookNodeExecutablePath: hook.nodeExecutablePath,
|
|
147
|
-
securityHost:
|
|
159
|
+
securityHost: activeD4Job?.id === jobId && claudeProduction
|
|
160
|
+
? d4ProductionBinding ? claudeProduction.d4SecurityHost(d4ProductionBinding) : async () => { throw Error('D4_REVIEW_HOST_REQUIRED'); }
|
|
161
|
+
: claude.securityHost, binaryIdentity: await claude.binaryIdentity(),
|
|
148
162
|
pullRequestEvidence: (args, signal) => gateway.readPullRequestEvidence(args, signal),
|
|
149
163
|
readCandidateReviewContext: loopId => gateway.readCandidateReviewContext(loopId),
|
|
150
164
|
d4ExecutionHost: d4Host,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
import { assertReviewedD4Request } from './d4-host-composition.js';
|
|
2
3
|
import { execFile, spawn } from 'node:child_process';
|
|
3
4
|
import { CLAUDE_TOKEN_FD, CLAUDE_TOKEN_FD_ENV, pathWithKeychainStub } from './claude-runner-token.js';
|
|
4
5
|
import { lstat, realpath } from 'node:fs/promises';
|
|
@@ -256,13 +257,15 @@ export function createClaudeProductionHost(input) {
|
|
|
256
257
|
};
|
|
257
258
|
const blocked = (code, executableVersion) => ({ available: false, authenticated: false, loginStatus: 'unknown', capabilities: [], reason: code,
|
|
258
259
|
...(executableVersion ? { executableVersion } : {}), admission: { checks: [{ id: CHECK_IDS[code] ?? 'release_copy', status: 'blocked', code }] } });
|
|
259
|
-
const
|
|
260
|
+
const createSecurityHost = (d4) => async (request) => {
|
|
260
261
|
if (closed)
|
|
261
262
|
throw Error('CLAUDE_HOST_CLOSED');
|
|
262
263
|
const requirements = requireClaudeRequirements(request.requirements, { adapterId: request.adapterId, adapterVersion: request.adapterVersion }), release = requirements.release;
|
|
264
|
+
if (d4)
|
|
265
|
+
await assertReviewedD4Request(d4, request);
|
|
263
266
|
const { release: synced, binary } = await verifiedCopy();
|
|
264
267
|
if (release.version !== synced.version || release.platform !== synced.platform || release.binary_sha256 !== synced.binary_sha256
|
|
265
|
-
|| !isDeepStrictEqual([...release.model_ids], [...synced.model_ids]) || release.platform !== platform || !release.model_ids.includes(request.modelId))
|
|
268
|
+
|| (!d4 && !isDeepStrictEqual([...release.model_ids], [...synced.model_ids])) || release.platform !== platform || !release.model_ids.includes(request.modelId))
|
|
266
269
|
throw Error('CLAUDE_RELEASE_BINDING_REJECTED');
|
|
267
270
|
const tools = claudeReleaseToolInventory(release);
|
|
268
271
|
await verifyClaudeCodeSignature(binary, input.codesign);
|
|
@@ -277,6 +280,8 @@ export function createClaudeProductionHost(input) {
|
|
|
277
280
|
verify: async (captured) => {
|
|
278
281
|
if (closed || captured.binarySha256 !== release.binary_sha256 || captured.securityMode !== CLAUDE_RELEASE_MODE)
|
|
279
282
|
throw Error('CLAUDE_RELEASE_BINDING_REJECTED');
|
|
283
|
+
if (d4)
|
|
284
|
+
await assertReviewedD4Request(d4, request);
|
|
280
285
|
await verifyClaudeReleaseCopy(input.home, release).catch(error => { probeMemo = undefined; throw error; });
|
|
281
286
|
return { version: 2, pin: captured, checks: CLAUDE_MANDATORY_GATES.map(gate => ({ gate, status: 'passed' })) };
|
|
282
287
|
} };
|
|
@@ -328,7 +333,13 @@ export function createClaudeProductionHost(input) {
|
|
|
328
333
|
const { binary } = await verifiedCopy();
|
|
329
334
|
return captureClaudeBinaryIdentity({ launchPath: binary, allowedRoots: [claudeReleaseRoot(input.home)] });
|
|
330
335
|
},
|
|
331
|
-
securityHost,
|
|
336
|
+
securityHost: createSecurityHost(),
|
|
337
|
+
/** A separate capability issued by reviewed composition; no task/env field can relax generic model equality. */
|
|
338
|
+
d4SecurityHost(binding) {
|
|
339
|
+
if (!binding)
|
|
340
|
+
throw Error('D4_HOST_INPUT_REJECTED');
|
|
341
|
+
return createSecurityHost(binding);
|
|
342
|
+
},
|
|
332
343
|
async close() { closed = true; abort.abort(); await pending; },
|
|
333
344
|
};
|
|
334
345
|
}
|
|
@@ -9,7 +9,15 @@ import { assertCurrentClaudeBinary, claudeBinaryLaunchPath } from './claude-bina
|
|
|
9
9
|
import { localToolInventory } from './local-tool-server.js';
|
|
10
10
|
import { CLAUDE_LAUNCH_CONFIG } from './claude-production-host.js';
|
|
11
11
|
import { assertD4PackageLimitSupport, D4_RECORDED_EXECUTION_SCHEMA, D4_RECORDED_EXECUTION_CAPABILITY } from './d4-execution-limits.js';
|
|
12
|
+
import { requireClaudeRequirements } from './claude-release.js';
|
|
12
13
|
import { REVIEWED_D4_HOST_REGISTRATIONS } from './d4-reviewed-hosts.js';
|
|
14
|
+
const productionBindings = new WeakMap();
|
|
15
|
+
export async function assertReviewedD4Request(binding, request) {
|
|
16
|
+
const assert = productionBindings.get(binding);
|
|
17
|
+
if (!assert)
|
|
18
|
+
throw denied();
|
|
19
|
+
await assert(request);
|
|
20
|
+
}
|
|
13
21
|
const denied = () => Error('D4_HOST_INPUT_REJECTED');
|
|
14
22
|
const hex = (v) => typeof v === 'string' && /^[a-f0-9]{64}$/.test(v);
|
|
15
23
|
const uuid = (v) => typeof v === 'string' && /^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/.test(v);
|
|
@@ -92,7 +100,38 @@ export async function composeReviewedD4Host(input) {
|
|
|
92
100
|
throw denied();
|
|
93
101
|
}
|
|
94
102
|
} });
|
|
95
|
-
|
|
103
|
+
let productionBinding;
|
|
104
|
+
if (input.currentJob) {
|
|
105
|
+
const currentJob = input.currentJob;
|
|
106
|
+
productionBinding = Object.freeze({ purpose: 'd4-reviewed-production-binding-v1' });
|
|
107
|
+
productionBindings.set(productionBinding, async (request) => {
|
|
108
|
+
try {
|
|
109
|
+
const requirements = requireClaudeRequirements(request.requirements, request), release = requirements.release;
|
|
110
|
+
if (requirements.fingerprint !== p.cli_security_fingerprint || request.modelId !== p.model_id || request.generation !== 1
|
|
111
|
+
|| release.binary_sha256 !== p.claude_binary_sha256 || release.tools_sha256 !== p.tools_sha256
|
|
112
|
+
|| release.model_ids.length !== 1 || release.model_ids[0] !== p.model_id)
|
|
113
|
+
throw denied();
|
|
114
|
+
const current = currentJob();
|
|
115
|
+
if (!current)
|
|
116
|
+
throw denied();
|
|
117
|
+
const snapshot = structuredClone(current);
|
|
118
|
+
if (protocolHash(snapshot.request) !== protocolHash(request))
|
|
119
|
+
throw denied();
|
|
120
|
+
for (const key of ['jobId', 'generation', 'launchFingerprint', 'contractFingerprint', 'taskPackageFingerprint'])
|
|
121
|
+
if (snapshot.context[key] !== request[key])
|
|
122
|
+
throw denied();
|
|
123
|
+
await host.assertCurrent(snapshot.descriptor, snapshot.context);
|
|
124
|
+
// The current claim, binding, candidate/actor pins and requirements must survive every asynchronous check.
|
|
125
|
+
const after = currentJob();
|
|
126
|
+
if (!after || protocolHash(after) !== protocolHash(snapshot))
|
|
127
|
+
throw denied();
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
throw denied();
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
return { status: 'composed', host, ...(productionBinding ? { productionBinding } : {}) };
|
|
96
135
|
}
|
|
97
136
|
catch {
|
|
98
137
|
return { status: 'mismatch' };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mtech-ai-local-runner",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.113",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"bin": {
|
|
@@ -8,8 +8,6 @@
|
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"dist/src",
|
|
11
|
-
"claude-mod",
|
|
12
|
-
"!claude-mod/**/*.test.tsx",
|
|
13
11
|
"README.md",
|
|
14
12
|
"LICENSE",
|
|
15
13
|
"native/bin/codex-observe-darwin-arm64",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{ "name": "mtec-ai-gateway", "owner": { "name": "mtech-ai-local-runner" }, "plugins": [{ "name": "ai-gateway-work-room", "source": "./" }] }
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{ "name": "ai-gateway-work-room", "version": "1.5.0", "description": "Work Room shares and requests from colleagues beside the conversation, installed by mtech-ai-local-runner (Work Item 98da2635).", "types": "./types/index.d.ts" }
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import type { EngineInterface } from 'claude-code'
|
|
2
|
-
|
|
3
|
-
import { clean } from './model'
|
|
4
|
-
|
|
5
|
-
/*
|
|
6
|
-
* Every Gateway call the Work Room pane makes goes through the person's own Claude AI Gateway connector (Work Item
|
|
7
|
-
* 98da2635, owner choice 2026-10-30), with no model turn. The connector's server name differs by setup:
|
|
8
|
-
* `ai-gateway` from an MCP config, an id in the desktop app; tools are found by their name suffix.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
export type GatewayTool = 'work_room_read' | 'work_room_write' | 'work_room_receipt_report' | 'work_inbox' | 'work_request_answer'
|
|
12
|
-
export type GatewayAnswer =
|
|
13
|
-
| { ok: true; payload: unknown }
|
|
14
|
-
| { ok: false; state: 'no-connector' | 'refused' | 'error'; detail: string }
|
|
15
|
-
|
|
16
|
-
export function findTool(tools: readonly { name: string; mcp: boolean }[], name: GatewayTool): string | undefined {
|
|
17
|
-
const names = tools
|
|
18
|
-
.filter(tool => tool.mcp && tool.name.startsWith('mcp__') && tool.name.endsWith(`__${name}`))
|
|
19
|
-
.map(tool => tool.name)
|
|
20
|
-
|
|
21
|
-
return names.find(found => found === `mcp__ai-gateway__${name}`) ?? names[0]
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** One call. A press passes its consent; a timer passes none. Never throws. */
|
|
25
|
-
export async function callGateway($: EngineInterface, name: GatewayTool, input: Record<string, unknown>, consent?: string): Promise<GatewayAnswer> {
|
|
26
|
-
try {
|
|
27
|
-
const tool = findTool(await $.tool.list(), name)
|
|
28
|
-
if (tool === undefined) return { ok: false, state: 'no-connector', detail: name }
|
|
29
|
-
// Typed loosely: the MCP tool union is too deep for the type checker. $.tool.call stays spelled at the call site.
|
|
30
|
-
const answer = (await $.tool.call({ tool, ...input, ...(consent ? { consent } : {}) } as never)) as {
|
|
31
|
-
deny?: string
|
|
32
|
-
result?: unknown
|
|
33
|
-
text?: string
|
|
34
|
-
isError?: boolean
|
|
35
|
-
}
|
|
36
|
-
if (answer.deny !== undefined) return { ok: false, state: 'refused', detail: clean(answer.deny, 400) }
|
|
37
|
-
if (answer.isError) {
|
|
38
|
-
const detail = clean(typeof answer.result === 'string' ? answer.result : answer.text, 400)
|
|
39
|
-
|
|
40
|
-
return { ok: false, state: 'error', detail: detail || 'Gateway error' }
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return { ok: true, payload: { result: answer.result, text: answer.text } }
|
|
44
|
-
} catch (error) {
|
|
45
|
-
return { ok: false, state: 'error', detail: clean(error instanceof Error ? error.message : 'Gateway call failed', 400) }
|
|
46
|
-
}
|
|
47
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"modules": ["./register.tsx"],
|
|
3
|
-
"hooks": {
|
|
4
|
-
"PreToolUse": [
|
|
5
|
-
{
|
|
6
|
-
"matcher": "^mcp__.+__(work_room_read|work_inbox)$",
|
|
7
|
-
"hooks": [
|
|
8
|
-
{
|
|
9
|
-
"type": "command",
|
|
10
|
-
"command": "printf '%s' '{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"allow\",\"permissionDecisionReason\":\"Work Room pane: reading your own AI Gateway Rooms and requests is allowed.\"}}'"
|
|
11
|
-
}
|
|
12
|
-
]
|
|
13
|
-
},
|
|
14
|
-
{
|
|
15
|
-
"matcher": "^mcp__.+__work_room_receipt_report$",
|
|
16
|
-
"hooks": [
|
|
17
|
-
{
|
|
18
|
-
"type": "command",
|
|
19
|
-
"command": "if grep -Eq '\"state\"[[:space:]]*:[[:space:]]*\"presented\"'; then printf '%s' '{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"allow\",\"permissionDecisionReason\":\"Work Room pane: recording that it showed you these Room messages is allowed.\"}}'; else printf '%s' '{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"ask\",\"permissionDecisionReason\":\"Work Room: marking Room messages read tells the AI Gateway you read them. Allow it?\"}}'; fi"
|
|
20
|
-
}
|
|
21
|
-
]
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
"matcher": "^mcp__.+__(work_room_write|work_request_answer)$",
|
|
25
|
-
"hooks": [
|
|
26
|
-
{
|
|
27
|
-
"type": "command",
|
|
28
|
-
"command": "printf '%s' '{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"ask\",\"permissionDecisionReason\":\"Work Room: this sends what you wrote to colleagues through the AI Gateway. Allow it?\"}}'"
|
|
29
|
-
}
|
|
30
|
-
]
|
|
31
|
-
}
|
|
32
|
-
]
|
|
33
|
-
}
|
|
34
|
-
}
|