mtech-ai-local-runner 0.1.79 → 0.1.80
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 +5 -0
- package/claude-mod/.claude-plugin/plugin.json +1 -1
- package/claude-mod/hooks/gateway.ts +3 -3
- package/claude-mod/hooks/hooks.json +16 -1
- package/claude-mod/hooks/register.tsx +15 -8
- package/dist/src/adapters/codex/pre-model-gate.js +2 -1
- package/dist/src/cli.js +19 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -298,6 +298,11 @@ your own AI Gateway connector in Claude, and it never starts a model turn: its O
|
|
|
298
298
|
conversation. Without the connector the pane says so. The Claude mobile app draws no input field, so there the pane
|
|
299
299
|
reads, shares and marks read only.
|
|
300
300
|
|
|
301
|
+
The pane refreshes with no model turn, so a Claude session in auto mode would refuse those calls. The mod therefore
|
|
302
|
+
ships a PreToolUse hook that allows exactly the two reads, `work_room_read` and `work_inbox`, on any AI Gateway
|
|
303
|
+
connector. That hook allows them for every caller in the session, the model included. Every other call the pane makes
|
|
304
|
+
carries the press that raised it: Open for a Room and its presented receipt, and the confirm press for each write.
|
|
305
|
+
|
|
301
306
|
The runner copies the mod to `claude-mod/<version>/` in the state root, without its tests, and installs it with the
|
|
302
307
|
Claude CLI as a local marketplace (`plugin marketplace add` and `plugin install --scope user`). This needs a Claude CLI
|
|
303
308
|
2.1.299 or newer: the first `claude` on `PATH` when it is new enough, else the newest one the Claude desktop app
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{ "name": "ai-gateway-work-room", "version": "1.
|
|
1
|
+
{ "name": "ai-gateway-work-room", "version": "1.1.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" }
|
|
@@ -33,15 +33,15 @@ export async function callGateway($: EngineInterface, name: GatewayTool, input:
|
|
|
33
33
|
text?: string
|
|
34
34
|
isError?: boolean
|
|
35
35
|
}
|
|
36
|
-
if (answer.deny !== undefined) return { ok: false, state: 'refused', detail: clean(answer.deny,
|
|
36
|
+
if (answer.deny !== undefined) return { ok: false, state: 'refused', detail: clean(answer.deny, 400) }
|
|
37
37
|
if (answer.isError) {
|
|
38
|
-
const detail = clean(typeof answer.result === 'string' ? answer.result : answer.text,
|
|
38
|
+
const detail = clean(typeof answer.result === 'string' ? answer.result : answer.text, 400)
|
|
39
39
|
|
|
40
40
|
return { ok: false, state: 'error', detail: detail || 'Gateway error' }
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
return { ok: true, payload: { result: answer.result, text: answer.text } }
|
|
44
44
|
} catch (error) {
|
|
45
|
-
return { ok: false, state: 'error', detail: clean(error instanceof Error ? error.message : 'Gateway call failed',
|
|
45
|
+
return { ok: false, state: 'error', detail: clean(error instanceof Error ? error.message : 'Gateway call failed', 400) }
|
|
46
46
|
}
|
|
47
47
|
}
|
|
@@ -1 +1,16 @@
|
|
|
1
|
-
{
|
|
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
|
+
}
|
|
16
|
+
}
|
|
@@ -9,12 +9,16 @@ import { bandText, chatPrompt, findPayload, parseActivity, parseInbox, parseMess
|
|
|
9
9
|
* The Work Room pane (Work Item 98da2635, spec 2026-10-30): Room shares and requests from colleagues beside the
|
|
10
10
|
* conversation, read and answered through the person's own AI Gateway connector with no model turn. The band above
|
|
11
11
|
* the prompt says what waits; the pane opens only from the band or /work-room, never at session start.
|
|
12
|
+
*
|
|
13
|
+
* The refresh runs with no request of the person's behind it, which auto mode refuses, and a mod's own module hooks
|
|
14
|
+
* cannot approve its own calls. So hooks/hooks.json carries a PreToolUse hook that allows the two reads, work_room_read
|
|
15
|
+
* and work_inbox, for every caller (owner decision 2026-10-31). Every other call carries the press that raised it.
|
|
12
16
|
*/
|
|
13
17
|
|
|
14
18
|
export const PANE = 'work-room'
|
|
15
19
|
const TITLE = 'Work Room'
|
|
16
20
|
const REFRESH_MS = 60_000
|
|
17
|
-
const NO_CONNECTOR = 'Connect AI Gateway in Claude
|
|
21
|
+
const NO_CONNECTOR = 'This session has no AI Gateway Work Room tools. Connect AI Gateway in Claude, reconnect it with /mcp, or start a new session.'
|
|
18
22
|
|
|
19
23
|
const counts = atom({ plugin: 'ai-gateway-work-room', key: 'counts' } as const, { shares: 0, requests: 0 } as Counts)
|
|
20
24
|
const activity = atom({ plugin: 'ai-gateway-work-room', key: 'activity' } as const, [] as RoomActivity[])
|
|
@@ -35,19 +39,19 @@ async function fail($: EngineInterface, answer: { state: Status['state']; detail
|
|
|
35
39
|
await update($, status, () => ({ state: answer.state, detail: answer.state === 'no-connector' ? NO_CONNECTOR : answer.detail }))
|
|
36
40
|
}
|
|
37
41
|
|
|
38
|
-
/** Counts only: Room activity and open inbox items, never message bodies. */
|
|
42
|
+
/** Counts only: Room activity and open inbox items, never message bodies. Each half shows even when the other fails. */
|
|
39
43
|
export async function refreshCounts($: EngineInterface): Promise<void> {
|
|
40
44
|
const [roomAnswer, inboxAnswer] = await Promise.all([
|
|
41
45
|
callGateway($, 'work_room_read', { operation: 'activity', limit: 100 }),
|
|
42
46
|
callGateway($, 'work_inbox', { direction: 'to_me' }),
|
|
43
47
|
])
|
|
48
|
+
if (roomAnswer.ok) await update($, activity, () => parseActivity(findPayload(roomAnswer.payload, v => Array.isArray(v.rooms))) ?? [])
|
|
49
|
+
if (inboxAnswer.ok) await update($, inbox, () => parseInbox(findPayload(inboxAnswer.payload, v => Array.isArray(v.items))) ?? [])
|
|
50
|
+
const rows = await read($, activity)
|
|
51
|
+
const items = await read($, inbox)
|
|
52
|
+
await update($, counts, () => ({ shares: rows.reduce((sum, row) => sum + row.unread, 0), requests: items.length }))
|
|
44
53
|
if (!roomAnswer.ok) return fail($, roomAnswer)
|
|
45
54
|
if (!inboxAnswer.ok) return fail($, inboxAnswer)
|
|
46
|
-
const rows = parseActivity(findPayload(roomAnswer.payload, v => Array.isArray(v.rooms))) ?? []
|
|
47
|
-
const items = parseInbox(findPayload(inboxAnswer.payload, v => Array.isArray(v.items))) ?? []
|
|
48
|
-
await update($, activity, () => rows)
|
|
49
|
-
await update($, inbox, () => items)
|
|
50
|
-
await update($, counts, () => ({ shares: rows.reduce((sum, row) => sum + row.unread, 0), requests: items.length }))
|
|
51
55
|
await update($, status, () => ({ state: 'ok', detail: '' }))
|
|
52
56
|
}
|
|
53
57
|
|
|
@@ -76,7 +80,10 @@ export async function reportPresented($: EngineInterface, roomId: string, ids: s
|
|
|
76
80
|
const fresh = ids.filter(id => !presentedSent.has(id)).slice(0, 100)
|
|
77
81
|
if (fresh.length === 0) return
|
|
78
82
|
for (const id of fresh) presentedSent.add(id)
|
|
79
|
-
const
|
|
83
|
+
const name = (await read($, rooms))[roomId]?.info.name ?? 'a Room'
|
|
84
|
+
// The person's press on Open is what showed these messages, so the receipt carries it as consent.
|
|
85
|
+
const consent = `The user opened Room ${name} in the Work Room pane, which showed them ${fresh.length} message${fresh.length === 1 ? '' : 's'}.`
|
|
86
|
+
const sent = await callGateway($, 'work_room_receipt_report', { room_id: roomId, message_ids: fresh, state: 'presented' }, consent)
|
|
80
87
|
if (!sent.ok) {
|
|
81
88
|
for (const id of fresh) presentedSent.delete(id)
|
|
82
89
|
return fail($, sent)
|
|
@@ -57,7 +57,8 @@ export function assessCodexPreModel(actual, expected, supported) {
|
|
|
57
57
|
require(server.status === 'ready', 'MCP_NOT_READY');
|
|
58
58
|
require(server.toolSchemaFingerprint === expected.toolSchemaFingerprint, 'MCP_SCHEMA_MISMATCH');
|
|
59
59
|
// Exactly the tools of the inventory the expected fingerprint names (Work Decision f43e7aa0), never the other one.
|
|
60
|
-
|
|
60
|
+
// The native session fingerprints the bare tool array (validateNativeTools), not the descriptor's {tools} form.
|
|
61
|
+
const inventory = localToolInventoryFor(expected.toolSchemaFingerprint, value => fingerprint(value.tools));
|
|
61
62
|
require(inventory !== undefined && Array.isArray(server.tools) && server.tools.every(tool => typeof tool === 'string')
|
|
62
63
|
&& JSON.stringify([...server.tools].sort()) === JSON.stringify([...localToolNames(inventory)].sort()), 'MCP_TOOLS_MISMATCH');
|
|
63
64
|
}
|
package/dist/src/cli.js
CHANGED
|
@@ -42,7 +42,16 @@ import { localToolSelfTest } from "./safety/local-tool-self-test.js";
|
|
|
42
42
|
function localToolsReport(result) {
|
|
43
43
|
if (result.status === "passed")
|
|
44
44
|
return { ...result };
|
|
45
|
-
return { ...result, action:
|
|
45
|
+
return { ...result, action: localToolsAction(result) };
|
|
46
|
+
}
|
|
47
|
+
function localToolsAction(result) {
|
|
48
|
+
const failed = `Local file tools failed on this Mac (${result.tool ?? "setup"}: ${result.detail ?? "unknown"}), so a job here cannot read source.`;
|
|
49
|
+
// A bare restricted node that also fails cannot start in the sandbox at all. On 2026-10-31 Homebrew's node did this on
|
|
50
|
+
// an employee Mac: it maps libraries from /opt/homebrew, and the sandbox maps only /usr/lib and /System/Library.
|
|
51
|
+
if (result.sandbox?.status === "failed") {
|
|
52
|
+
return `${failed} The Node at ${result.node} cannot start inside the runner's restricted sandbox, which loads libraries only from /usr/lib and /System/Library; a Node built against libraries elsewhere, such as Homebrew's, stops there. Install an official Node.js build from https://nodejs.org, then rerun setup and doctor with its bin directory first on PATH, for example: PATH="<official Node>/bin:$PATH" npx mtech-ai-local-runner@latest setup`;
|
|
53
|
+
}
|
|
54
|
+
return `${failed} Send this doctor output to the AI Gateway owner.`;
|
|
46
55
|
}
|
|
47
56
|
function hookAction(result) {
|
|
48
57
|
return result.reason === "invalid_command"
|
|
@@ -252,6 +261,15 @@ export async function main(argv, env, io = {}) {
|
|
|
252
261
|
stdout(JSON.stringify({ ok: false, configured: false, hook: hookReport(hook), action: hookAction(hook) }));
|
|
253
262
|
throw new Error(`The PreToolUse hook did not deny the probe call (hook_not_enforcing: ${hook.reason})`);
|
|
254
263
|
}
|
|
264
|
+
// A full runner whose job file tools cannot read here would take jobs and stop executor_blocked on every one
|
|
265
|
+
// (Work Item 6f9e64ae), so setup refuses before login, as for the hook.
|
|
266
|
+
const tools = await (io.localToolSelfTest ?? localToolSelfTest)(config.workspaceRoot)
|
|
267
|
+
.catch(error => ({ status: "failed", detail: error instanceof Error ? error.message : String(error), node: process.execPath }));
|
|
268
|
+
if (tools.status !== "passed") {
|
|
269
|
+
const report = localToolsReport(tools);
|
|
270
|
+
stdout(JSON.stringify({ ok: false, configured: false, local_tools: report, action: report.action }));
|
|
271
|
+
throw new Error(`The job file tools failed on this Mac (local_tools_failed: ${tools.tool ?? "setup"}: ${tools.detail ?? "unknown"})`);
|
|
272
|
+
}
|
|
255
273
|
}
|
|
256
274
|
const loginCode = await handleLogin(config, stdout, stderr, fetchFn, io.browserOpener);
|
|
257
275
|
if (loginCode !== 0)
|