machine-bridge-mcp 0.4.2 → 0.5.0

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.
@@ -1,12 +1,16 @@
1
1
  import { createHash, randomBytes } from "node:crypto";
2
- import { closeSync, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, writeFileSync, chmodSync, realpathSync, rmSync, unlinkSync, readdirSync, statSync } from "node:fs";
2
+ import { closeSync, constants as fsConstants, existsSync, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync, readSync, renameSync, writeFileSync, chmodSync, realpathSync, rmSync, unlinkSync, readdirSync, statSync } from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
+ import serverMetadata from "../shared/server-metadata.json" with { type: "json" };
6
7
 
7
8
  export const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
8
- export const appName = "machine-bridge-mcp";
9
+ export const appName = String(serverMetadata.name);
9
10
  const STATE_MARKER = ".machine-bridge-mcp-state";
11
+ const MAX_STATE_JSON_BYTES = 2 * 1024 * 1024;
12
+ const MAX_LOCK_BYTES = 64 * 1024;
13
+ const MAX_MARKER_BYTES = 4096;
10
14
 
11
15
  export function expandHome(input = "") {
12
16
  if (!input || input === "~") return os.homedir();
@@ -29,7 +33,7 @@ export function defaultStateRoot() {
29
33
  }
30
34
 
31
35
 
32
- export function configPath(stateRoot = defaultStateRoot()) {
36
+ function configPath(stateRoot = defaultStateRoot()) {
33
37
  return path.join(expandHome(stateRoot), "config.json");
34
38
  }
35
39
 
@@ -78,13 +82,10 @@ export function workspaceHash(workspace) {
78
82
  return createHash("sha256").update(String(workspace)).digest("hex").slice(0, 24);
79
83
  }
80
84
 
81
- export function profileDirForWorkspace(workspace, stateRoot = defaultStateRoot()) {
85
+ function profileDirForWorkspace(workspace, stateRoot = defaultStateRoot()) {
82
86
  return path.join(expandHome(stateRoot), "profiles", workspaceHash(workspace));
83
87
  }
84
88
 
85
- export function statePathForWorkspace(workspace, stateRoot = defaultStateRoot()) {
86
- return path.join(profileDirForWorkspace(workspace, stateRoot), "state.json");
87
- }
88
89
 
89
90
  export function loadState(workspace, options = {}) {
90
91
  const stateRoot = ensureStateRoot(options.stateDir ? expandHome(options.stateDir) : defaultStateRoot());
@@ -96,7 +97,7 @@ export function loadState(workspace, options = {}) {
96
97
  ownerOnlyFile(statePath);
97
98
  state = readJsonObjectOrBackup(statePath);
98
99
  }
99
- state.schemaVersion = 3;
100
+ state.schemaVersion = 4;
100
101
  state.workspace = {
101
102
  path: workspace,
102
103
  hash: workspaceHash(workspace),
@@ -189,7 +190,7 @@ function releaseProcessLock(lockPath, token) {
189
190
 
190
191
  export function readDaemonLockOwner(lockPath) {
191
192
  try {
192
- const parsed = JSON.parse(readFileSync(lockPath, "utf8"));
193
+ const parsed = JSON.parse(readBoundedUtf8(lockPath, MAX_LOCK_BYTES, "lock file"));
193
194
  return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
194
195
  } catch {
195
196
  return null;
@@ -207,9 +208,31 @@ function isPidAlive(pid) {
207
208
  }
208
209
  }
209
210
 
211
+ function readBoundedUtf8(filePath, maxBytes, label) {
212
+ const pathInfo = lstatSync(filePath);
213
+ if (pathInfo.isSymbolicLink()) throw new Error(`${label} must not be a symbolic link`);
214
+ const noFollow = Number(fsConstants.O_NOFOLLOW || 0);
215
+ const fd = openSync(filePath, Number(fsConstants.O_RDONLY) | noFollow);
216
+ try {
217
+ const info = fstatSync(fd);
218
+ if (!info.isFile()) throw new Error(`${label} is not a regular file`);
219
+ if (info.size > maxBytes) throw new Error(`${label} exceeds ${maxBytes} bytes`);
220
+ const buffer = Buffer.alloc(info.size);
221
+ let offset = 0;
222
+ while (offset < buffer.length) {
223
+ const count = readSync(fd, buffer, offset, buffer.length - offset, offset);
224
+ if (count === 0) break;
225
+ offset += count;
226
+ }
227
+ return buffer.subarray(0, offset).toString("utf8");
228
+ } finally {
229
+ closeSync(fd);
230
+ }
231
+ }
232
+
210
233
  function readJsonObjectOrBackup(filePath) {
211
234
  try {
212
- const parsed = JSON.parse(readFileSync(filePath, "utf8"));
235
+ const parsed = JSON.parse(readBoundedUtf8(filePath, MAX_STATE_JSON_BYTES, "state JSON"));
213
236
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("JSON root must be an object");
214
237
  return parsed;
215
238
  } catch {
@@ -286,7 +309,7 @@ function assertSafeStateRootForRemoval(root) {
286
309
  }
287
310
 
288
311
  function assertValidStateMarker(marker, options = {}) {
289
- const content = readFileSync(marker, "utf8");
312
+ const content = readBoundedUtf8(marker, MAX_MARKER_BYTES, "state marker");
290
313
  try {
291
314
  const value = JSON.parse(content);
292
315
  if (value?.app === appName && value?.schema === 1) return;
@@ -315,7 +338,7 @@ function stateRootMatchesRecordedWorkspace(root) {
315
338
  if (!entry.isDirectory()) continue;
316
339
  const stateFile = path.join(profiles, entry.name, "state.json");
317
340
  if (!existsSync(stateFile)) continue;
318
- const value = JSON.parse(readFileSync(stateFile, "utf8"));
341
+ const value = JSON.parse(readBoundedUtf8(stateFile, MAX_STATE_JSON_BYTES, "state JSON"));
319
342
  if (typeof value?.workspace?.path !== "string") continue;
320
343
  try { if (realpathSync(value.workspace.path) === root) return true; } catch {}
321
344
  }
@@ -327,7 +350,7 @@ function isRecognizableLegacyStateRoot(root) {
327
350
  const config = path.join(root, "config.json");
328
351
  try {
329
352
  if (existsSync(config)) {
330
- const value = JSON.parse(readFileSync(config, "utf8"));
353
+ const value = JSON.parse(readBoundedUtf8(config, MAX_STATE_JSON_BYTES, "config JSON"));
331
354
  if (
332
355
  value &&
333
356
  typeof value.selectedWorkspace === "string" &&
@@ -343,7 +366,7 @@ function isRecognizableLegacyStateRoot(root) {
343
366
  if (!entry.isDirectory() || !/^[a-f0-9]{24}$/.test(entry.name)) continue;
344
367
  const stateFile = path.join(profiles, entry.name, "state.json");
345
368
  if (!existsSync(stateFile)) continue;
346
- const value = JSON.parse(readFileSync(stateFile, "utf8"));
369
+ const value = JSON.parse(readBoundedUtf8(stateFile, MAX_STATE_JSON_BYTES, "state JSON"));
347
370
  if (value?.workspace?.hash === entry.name && typeof value?.workspace?.path === "string") return true;
348
371
  }
349
372
  } catch {}
@@ -354,11 +377,13 @@ function atomicWriteJson(filePath, value) {
354
377
  const dir = path.dirname(filePath);
355
378
  ensureOwnerOnlyDir(dir);
356
379
  cleanupStaleAtomicTemps(dir, path.basename(filePath));
380
+ const serialized = `${JSON.stringify(value, null, 2)}\n`;
381
+ if (Buffer.byteLength(serialized) > MAX_STATE_JSON_BYTES) throw new Error(`state JSON exceeds ${MAX_STATE_JSON_BYTES} bytes`);
357
382
  const tempPath = path.join(dir, `.${path.basename(filePath)}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`);
358
383
  let fd;
359
384
  try {
360
385
  fd = openSync(tempPath, "wx", 0o600);
361
- writeFileSync(fd, `${JSON.stringify(value, null, 2)}\n`, "utf8");
386
+ writeFileSync(fd, serialized, "utf8");
362
387
  fsyncSync(fd);
363
388
  closeSync(fd);
364
389
  fd = undefined;
@@ -412,11 +437,11 @@ export function ensureWorkerSecrets(state, options = {}) {
412
437
  if (!state.worker.name || options.workerName) state.worker.name = options.workerName || defaultWorkerName(state.workspace.hash);
413
438
  }
414
439
 
415
- export function defaultWorkerName(hash) {
440
+ function defaultWorkerName(hash) {
416
441
  return `mbm-${String(hash || "default").slice(0, 12)}`;
417
442
  }
418
443
 
419
- export function randomToken(prefix) {
444
+ function randomToken(prefix) {
420
445
  return `${prefix}_${randomBytes(32).toString("base64url")}`;
421
446
  }
422
447
 
@@ -424,9 +449,6 @@ export function sha256(value) {
424
449
  return createHash("sha256").update(String(value)).digest("hex");
425
450
  }
426
451
 
427
- export function fileSha256(filePath) {
428
- return createHash("sha256").update(readFileSync(filePath)).digest("hex");
429
- }
430
452
 
431
453
  export function ensureOwnerOnlyDir(dir) {
432
454
  mkdirSync(dir, { recursive: true, mode: 0o700 });
@@ -434,6 +456,9 @@ export function ensureOwnerOnlyDir(dir) {
434
456
  }
435
457
 
436
458
  export function ownerOnlyFile(filePath) {
459
+ const info = lstatSync(filePath);
460
+ if (info.isSymbolicLink()) throw new Error(`owner-only file must not be a symbolic link: ${filePath}`);
461
+ if (!info.isFile()) throw new Error(`owner-only path is not a regular file: ${filePath}`);
437
462
  try { chmodSync(filePath, 0o600); } catch {}
438
463
  }
439
464
 
@@ -2,11 +2,12 @@ import readline from "node:readline";
2
2
  import { readFileSync } from "node:fs";
3
3
  import { randomBytes } from "node:crypto";
4
4
  import { LocalDaemon } from "./daemon.mjs";
5
- import { formatFields, sanitizeLogText } from "./log.mjs";
5
+ import { classifyOperationalError, createLogger } from "./log.mjs";
6
6
  import {
7
7
  MCP_INSTRUCTIONS,
8
8
  MCP_PROTOCOL_VERSION,
9
9
  MCP_SUPPORTED_PROTOCOL_VERSIONS,
10
+ SERVER_NAME,
10
11
  rpcError,
11
12
  rpcResult,
12
13
  toolResult,
@@ -14,10 +15,11 @@ import {
14
15
  } from "./tools.mjs";
15
16
 
16
17
  const MAX_LINE_BYTES = 8 * 1024 * 1024;
18
+ const SLOW_TOOL_CALL_MS = 30_000;
17
19
  const PACKAGE_VERSION = String(JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8")).version);
18
20
 
19
- export async function runStdioServer({ workspace, policy, verbose = false }) {
20
- const logger = stderrLogger(verbose);
21
+ export async function runStdioServer({ workspace, policy, logLevel = "info" }) {
22
+ const logger = createLogger({ component: "stdio", level: logLevel, stderrOnly: true, color: false });
21
23
  const runtime = new LocalDaemon({ workspace, policy, logger });
22
24
  const pending = new Map();
23
25
  let negotiatedVersion = MCP_PROTOCOL_VERSION;
@@ -40,7 +42,7 @@ export async function runStdioServer({ workspace, policy, verbose = false }) {
40
42
  return;
41
43
  }
42
44
  void handleLine(line).catch((error) => {
43
- logger.error("stdio request handler failed", { error: errorMessage(error) });
45
+ logger.error("stdio request handler failed", { error_class: classifyOperationalError(error) });
44
46
  });
45
47
  });
46
48
 
@@ -74,7 +76,7 @@ export async function runStdioServer({ workspace, policy, verbose = false }) {
74
76
  protocolVersion: negotiatedVersion,
75
77
  capabilities: { tools: { listChanged: false }, logging: {} },
76
78
  serverInfo: {
77
- name: "machine-bridge-mcp",
79
+ name: SERVER_NAME,
78
80
  title: "Machine Bridge MCP",
79
81
  version: PACKAGE_VERSION,
80
82
  description: "Workspace-scoped local coding tools over MCP stdio or authenticated remote relay.",
@@ -128,11 +130,15 @@ export async function runStdioServer({ workspace, policy, verbose = false }) {
128
130
  try {
129
131
  const result = await runtime.executeTool(name, args, { callId });
130
132
  send(rpcResult(message.id, toolResult(result)));
131
- logger.info("tool call completed", { call_id: callId.slice(0, 20), tool: name, duration_ms: Date.now() - started, ok: true });
133
+ const durationMs = Date.now() - started;
134
+ if (durationMs >= SLOW_TOOL_CALL_MS) logger.info("slow tool call completed", { tool: name, duration_ms: durationMs });
135
+ else logger.debug("tool call completed", { call_id: callId.slice(0, 20), tool: name, duration_ms: durationMs });
132
136
  } catch (error) {
133
137
  const safeError = runtime.safeErrorMessage(error);
134
138
  send(rpcResult(message.id, toolResult({ error: safeError }, true)));
135
- logger.warn("tool call failed", { call_id: callId.slice(0, 20), tool: name, duration_ms: Date.now() - started, ok: false, error_class: classifyError(error) });
139
+ const durationMs = Date.now() - started;
140
+ logger.warn("tool call failed", { tool: name, duration_ms: durationMs, error_class: classifyOperationalError(error) });
141
+ logger.debug("tool call failure correlation", { call_id: callId.slice(0, 20) });
136
142
  } finally {
137
143
  if (key) pending.delete(key);
138
144
  runtime.finishCall(callId);
@@ -162,33 +168,3 @@ function jsonRpcIdKey(value) {
162
168
  function asObject(value) {
163
169
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
164
170
  }
165
-
166
- function classifyError(error) {
167
- const message = error instanceof Error ? error.message : String(error);
168
- if (/cancel/i.test(message)) return "cancelled";
169
- if (/timed out/i.test(message)) return "timeout";
170
- if (/outside the configured workspace/i.test(message)) return "path_boundary";
171
- if (/disabled|requires .* mode/i.test(message)) return "policy_denied";
172
- if (/not found|ENOENT/i.test(message)) return "not_found";
173
- if (/permission|EACCES|EPERM/i.test(message)) return "permission_denied";
174
- if (/maximum|exceeds|max_bytes|too many/i.test(message)) return "limit_exceeded";
175
- if (/invalid|must|requires|ambiguous|mismatch/i.test(message)) return "invalid_request";
176
- return "execution_failed";
177
- }
178
-
179
- function errorMessage(error) {
180
- return sanitizeLogText(error instanceof Error ? error.message : String(error)).slice(0, 4096) || "tool call failed";
181
- }
182
-
183
- function stderrLogger(verbose) {
184
- const write = (level, message, fields) => {
185
- if (level === "debug" && !verbose) return;
186
- process.stderr.write(`[${level}] stdio: ${sanitizeLogText(message)}${formatFields(fields)}\n`);
187
- };
188
- return {
189
- info(message, fields) { write("info", message, fields); },
190
- warn(message, fields) { write("warn", message, fields); },
191
- error(message, fields) { write("error", message, fields); },
192
- debug(message, fields) { write("debug", message, fields); },
193
- };
194
- }
@@ -1,16 +1,30 @@
1
1
  import catalog from "../shared/tool-catalog.json" with { type: "json" };
2
+ import serverMetadata from "../shared/server-metadata.json" with { type: "json" };
2
3
 
3
- export const MCP_PROTOCOL_VERSION = "2025-11-25";
4
- export const MCP_SUPPORTED_PROTOCOL_VERSIONS = ["2025-11-25", "2025-06-18", "2025-03-26"];
4
+ export const SERVER_NAME = String(serverMetadata.name);
5
+ export const MCP_PROTOCOL_VERSION = String(serverMetadata.protocolVersion);
6
+ export const MCP_SUPPORTED_PROTOCOL_VERSIONS = Object.freeze(serverMetadata.supportedProtocolVersions.map((value) => String(value)));
5
7
 
6
- export const MCP_INSTRUCTIONS = [
7
- "You are connected to a local workspace through machine-bridge-mcp.",
8
- "Remote mode uses a Cloudflare relay; stdio mode runs entirely on the local machine.",
9
- "File and command operations execute on the user's local runtime, not in the Worker.",
10
- "Relative paths use the configured workspace. Direct filesystem tools are workspace-scoped unless unrestricted paths are explicitly enabled.",
11
- "run_process avoids shell parsing but is not an OS sandbox. exec_command is only exposed in shell execution mode and has the local user's authority.",
12
- "Inspect before editing, prefer edit_file or apply_patch over whole-file replacement, and report commands that were run.",
13
- ].join("\n");
8
+ export const MCP_INSTRUCTIONS = Object.freeze(serverMetadata.instructions.map((value) => String(value))).join("\n");
9
+
10
+
11
+ export const DEFAULT_POLICY_PROFILE = "full";
12
+ export const DEFAULT_POLICY_REVISION = 2;
13
+
14
+ export const POLICY_PROFILES = Object.freeze({
15
+ review: Object.freeze({ profile: "review", allowWrite: false, execMode: "off", unrestrictedPaths: false, minimalEnv: true, exposeAbsolutePaths: false }),
16
+ edit: Object.freeze({ profile: "edit", allowWrite: true, execMode: "off", unrestrictedPaths: false, minimalEnv: true, exposeAbsolutePaths: false }),
17
+ agent: Object.freeze({ profile: "agent", allowWrite: true, execMode: "direct", unrestrictedPaths: false, minimalEnv: true, exposeAbsolutePaths: false }),
18
+ full: Object.freeze({ profile: "full", allowWrite: true, execMode: "shell", unrestrictedPaths: true, minimalEnv: false, exposeAbsolutePaths: true }),
19
+ });
20
+
21
+ const POLICY_ORIGINS = new Set(["default", "explicit", "custom", "migrated", "legacy-preserved"]);
22
+
23
+ export function policyProfile(name, origin = "explicit") {
24
+ const profile = String(name || "").trim().toLowerCase();
25
+ if (!POLICY_PROFILES[profile]) throw new Error(`unknown policy profile: ${profile}`);
26
+ return normalizePolicy({ ...POLICY_PROFILES[profile], origin, revision: DEFAULT_POLICY_REVISION });
27
+ }
14
28
 
15
29
  export function normalizePolicy(policy = {}) {
16
30
  const execMode = ["off", "direct", "shell"].includes(policy.execMode)
@@ -18,8 +32,12 @@ export function normalizePolicy(policy = {}) {
18
32
  : policy.allowExec === true
19
33
  ? "shell"
20
34
  : "off";
35
+ const origin = POLICY_ORIGINS.has(policy.origin) ? policy.origin : "custom";
36
+ const revision = Number.isInteger(policy.revision) && policy.revision > 0 ? policy.revision : DEFAULT_POLICY_REVISION;
21
37
  return {
22
38
  profile: typeof policy.profile === "string" && policy.profile ? policy.profile : "custom",
39
+ origin,
40
+ revision,
23
41
  allowWrite: policy.allowWrite === true,
24
42
  allowExec: execMode !== "off",
25
43
  execMode,
@@ -44,9 +62,6 @@ export function allToolNames() {
44
62
  return catalog.map((tool) => tool.name);
45
63
  }
46
64
 
47
- export function findTool(name) {
48
- return catalog.find((tool) => tool.name === name) || null;
49
- }
50
65
 
51
66
  export function toolResult(value, isError = false) {
52
67
  const special = specialMcpResult(value);
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "machine-bridge-mcp",
3
+ "protocolVersion": "2025-11-25",
4
+ "supportedProtocolVersions": [
5
+ "2025-11-25",
6
+ "2025-06-18",
7
+ "2025-03-26"
8
+ ],
9
+ "instructions": [
10
+ "You are connected to a local workspace through machine-bridge-mcp.",
11
+ "Remote mode uses a Cloudflare relay; stdio mode runs on the local machine. File and command operations execute on the user's local runtime, not in the Worker.",
12
+ "Filesystem scope and path display follow the active local policy; the default full profile is unrestricted.",
13
+ "Filename sensitivity is not classified by this server; the MCP host may enforce additional independent rules.",
14
+ "run_process avoids shell parsing but is not an OS sandbox. exec_command is exposed only in shell mode and has the local user's authority.",
15
+ "Inspect before editing, prefer read_file line ranges, edit_file, or apply_patch over whole-file replacement, and report commands that were run."
16
+ ]
17
+ }
@@ -34,7 +34,7 @@
34
34
  {
35
35
  "name": "list_roots",
36
36
  "title": "List workspace roots",
37
- "description": "List filesystem roots exposed by the local runtime. Paths are workspace-relative unless absolute-path output is explicitly enabled.",
37
+ "description": "List filesystem roots exposed by the active local policy. The default full profile exposes local filesystem roots and returns absolute paths.",
38
38
  "availability": "always",
39
39
  "annotations": {
40
40
  "readOnlyHint": true,
@@ -100,7 +100,7 @@
100
100
  {
101
101
  "name": "read_file",
102
102
  "title": "Read text file",
103
- "description": "Read a UTF-8 file or an inclusive 1-based line range. Filesystem paths are workspace-scoped unless unrestricted paths are explicitly enabled.",
103
+ "description": "Read a UTF-8 regular file or an inclusive 1-based line range. The server does not block sensitive-looking filenames; access follows the active local policy and operating-system permissions.",
104
104
  "availability": "always",
105
105
  "annotations": {
106
106
  "readOnlyHint": true,
@@ -1,10 +1,11 @@
1
1
  import { DurableObject } from "cloudflare:workers";
2
2
  import toolCatalog from "../shared/tool-catalog.json";
3
+ import serverMetadata from "../shared/server-metadata.json";
3
4
 
4
- const SERVER_NAME = "machine-bridge-mcp";
5
- const SERVER_VERSION = "0.4.2";
6
- const MCP_PROTOCOL_VERSION = "2025-11-25";
7
- const MCP_SUPPORTED_PROTOCOL_VERSIONS = ["2025-11-25", "2025-06-18", "2025-03-26"] as const;
5
+ const SERVER_NAME = String(serverMetadata.name);
6
+ const SERVER_VERSION = "0.5.0";
7
+ const MCP_PROTOCOL_VERSION = String(serverMetadata.protocolVersion);
8
+ const MCP_SUPPORTED_PROTOCOL_VERSIONS = serverMetadata.supportedProtocolVersions.map((value) => String(value));
8
9
  const JSONRPC_VERSION = "2.0";
9
10
  const DEFAULT_MAX_BODY_BYTES = 8 * 1024 * 1024;
10
11
  const MAX_BODY_BYTES = 16 * 1024 * 1024;
@@ -121,13 +122,8 @@ const allCatalogTools = toolCatalog as ToolDefinition[];
121
122
  const serverInfoTool = publicTool(allCatalogTools.find((tool) => tool.name === "server_info")!);
122
123
  const workspaceTools = allCatalogTools.filter((tool) => tool.name !== "server_info").map(publicTool);
123
124
 
124
- const MCP_INSTRUCTIONS = [
125
- "You are connected to a local workspace through machine-bridge-mcp.",
126
- "The Cloudflare Worker authenticates and relays calls; file and command operations execute on the user's local runtime.",
127
- "Relative paths use the configured workspace. Direct filesystem tools are workspace-scoped unless unrestricted paths are explicitly enabled.",
128
- "run_process avoids shell parsing but is not an OS sandbox. exec_command is only exposed in shell mode and has the local user's authority.",
129
- "Prefer read_file line ranges, edit_file, or apply_patch over whole-file replacement, and report commands that were run.",
130
- ].join("\n");
125
+ const MCP_INSTRUCTIONS = serverMetadata.instructions.map((value) => String(value)).join("\n");
126
+
131
127
 
132
128
  function publicTool(tool: ToolDefinition): ToolDefinition {
133
129
  const { availability: _availability, ...definition } = tool;
@@ -1001,8 +997,14 @@ function daemonPolicyAllows(availability: unknown, policy: Record<string, unknow
1001
997
  function sanitizeDaemonPolicy(value: unknown): Record<string, unknown> {
1002
998
  const policy = asObject(value);
1003
999
  const execMode = policy.execMode === "shell" || policy.execMode === "direct" ? policy.execMode : "off";
1000
+ const origin = sanitizeMetadataText(policy.origin, 32);
1001
+ const revision = Number.isInteger(policy.revision) && Number(policy.revision) > 0
1002
+ ? Math.min(Number(policy.revision), 1_000_000)
1003
+ : 1;
1004
1004
  return {
1005
1005
  profile: sanitizeMetadataText(policy.profile, 32) ?? "custom",
1006
+ origin: ["default", "explicit", "custom", "migrated", "legacy-preserved"].includes(origin ?? "") ? origin : "custom",
1007
+ revision,
1006
1008
  allowWrite: policy.allowWrite === true,
1007
1009
  allowExec: execMode !== "off",
1008
1010
  execMode,