machine-bridge-mcp 0.8.1 → 0.9.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.
package/src/local/cli.mjs CHANGED
@@ -337,6 +337,10 @@ async function prepareStartMode(args, state, logger) {
337
337
 
338
338
  function reportExistingDaemon(args, state, owner, logger) {
339
339
  const pid = owner?.pid ? `pid ${owner.pid}` : "unknown pid";
340
+ if (isIdempotentDaemonOnlyStart(args)) {
341
+ logger.debug?.("local daemon already running; daemon-only start completed as an idempotent no-op", { owner_pid_known: Boolean(owner?.pid) });
342
+ return;
343
+ }
340
344
  logger.warn(`local daemon already running for this workspace (${pid}); requested changes were not applied`);
341
345
  if (args.json) {
342
346
  printStartJson(state, {
@@ -354,6 +358,24 @@ function reportExistingDaemon(args, state, owner, logger) {
354
358
  });
355
359
  }
356
360
 
361
+ function isIdempotentDaemonOnlyStart(args) {
362
+ if (!args.daemonOnly || args.json) return false;
363
+ return !Boolean(
364
+ args.profile
365
+ || args.execMode
366
+ || args.rotateSecrets
367
+ || args.forceWorker
368
+ || args.workerName
369
+ || args.noWrite
370
+ || args.noExec
371
+ || args.fullEnv
372
+ || args.unrestrictedPaths
373
+ || args.absolutePaths
374
+ || args.printMcpCredentials
375
+ || args.printCredentials
376
+ );
377
+ }
378
+
357
379
  async function startRemoteRuntime({ args, workspace, state, daemonLock, logger }) {
358
380
  let runtime = null;
359
381
  try {
@@ -3,10 +3,13 @@ import { classifyOperationalError } from "./log.mjs";
3
3
 
4
4
  const DEFAULT_HEARTBEAT_INTERVAL_MS = 25_000;
5
5
  const DEFAULT_HEARTBEAT_TIMEOUT_MS = 75_000;
6
+ const DEFAULT_CONNECT_TIMEOUT_MS = 15_000;
6
7
  const DEFAULT_HANDSHAKE_TIMEOUT_MS = 10_000;
7
8
  const DEFAULT_OUTAGE_WARN_AFTER_MS = 10_000;
8
9
  const DEFAULT_OUTAGE_WARN_REPEAT_MS = 60_000;
10
+ const DEFAULT_OUTAGE_WARN_MAX_REPEAT_MS = 15 * 60_000;
9
11
  const MAX_CLOSE_REASON_CHARS = 128;
12
+ const MAX_PROTOCOL_ERROR_CODE_CHARS = 64;
10
13
 
11
14
  const DEFAULT_SCHEDULER = Object.freeze({
12
15
  setTimeout: (callback, delay) => setTimeout(callback, delay),
@@ -35,9 +38,14 @@ export class RelayConnection {
35
38
  this.maxPayload = boundedPositiveInteger(options.maxPayload, 8 * 1024 * 1024);
36
39
  this.heartbeatIntervalMs = boundedPositiveInteger(options.heartbeatIntervalMs, DEFAULT_HEARTBEAT_INTERVAL_MS);
37
40
  this.heartbeatTimeoutMs = boundedPositiveInteger(options.heartbeatTimeoutMs, DEFAULT_HEARTBEAT_TIMEOUT_MS);
41
+ this.connectTimeoutMs = boundedPositiveInteger(options.connectTimeoutMs, DEFAULT_CONNECT_TIMEOUT_MS);
38
42
  this.handshakeTimeoutMs = boundedPositiveInteger(options.handshakeTimeoutMs, DEFAULT_HANDSHAKE_TIMEOUT_MS);
39
43
  this.outageWarnAfterMs = boundedPositiveInteger(options.outageWarnAfterMs, DEFAULT_OUTAGE_WARN_AFTER_MS);
40
44
  this.outageWarnRepeatMs = boundedPositiveInteger(options.outageWarnRepeatMs, DEFAULT_OUTAGE_WARN_REPEAT_MS);
45
+ this.outageWarnMaxRepeatMs = Math.max(
46
+ this.outageWarnRepeatMs,
47
+ boundedPositiveInteger(options.outageWarnMaxRepeatMs, DEFAULT_OUTAGE_WARN_MAX_REPEAT_MS),
48
+ );
41
49
 
42
50
  this.closed = true;
43
51
  this.socket = null;
@@ -47,12 +55,14 @@ export class RelayConnection {
47
55
  this.lastInboundAt = 0;
48
56
  this.reconnectAttempt = 0;
49
57
  this.reconnectTimer = null;
58
+ this.connectTimer = null;
50
59
  this.heartbeatTimer = null;
51
60
  this.handshakeTimer = null;
52
61
  this.outageWarnTimer = null;
53
62
  this.outageStartedAt = 0;
54
63
  this.outageAttempts = 0;
55
64
  this.outageNoticeEmitted = false;
65
+ this.outageWarningCount = 0;
56
66
  this.lastOutageWarnAt = 0;
57
67
  this.lastCloseCategory = "connection_interrupted";
58
68
  this.lastTransportErrorClass = "";
@@ -77,6 +87,7 @@ export class RelayConnection {
77
87
  this.closed = true;
78
88
  this.ready = false;
79
89
  this.clearTimer("heartbeatTimer", "clearInterval");
90
+ this.clearTimer("connectTimer", "clearTimeout");
80
91
  this.clearTimer("handshakeTimer", "clearTimeout");
81
92
  this.clearTimer("reconnectTimer", "clearTimeout");
82
93
  this.clearTimer("outageWarnTimer", "clearTimeout");
@@ -128,7 +139,8 @@ export class RelayConnection {
128
139
  } else if (this.outageStartedAt > 0) {
129
140
  const outageMs = Math.max(0, this.connectedAt - this.outageStartedAt);
130
141
  if (this.outageNoticeEmitted) {
131
- this.logger.info?.("remote relay connection restored", {
142
+ this.logger.info?.(`remote relay connection restored after ${formatDuration(outageMs)} (${formatAttempts(this.outageAttempts)})`);
143
+ this.logger.debug?.("remote relay outage recovery details", {
132
144
  outage_seconds: roundSeconds(outageMs),
133
145
  attempts: this.outageAttempts,
134
146
  });
@@ -150,6 +162,20 @@ export class RelayConnection {
150
162
  return true;
151
163
  }
152
164
 
165
+ handleServerError(message = {}) {
166
+ const errorCode = sanitizeProtocolErrorCode(message?.error);
167
+ this.logger.debug?.("remote relay reported a protocol error", { error_code: errorCode });
168
+ if (errorCode === "daemon_hello_timeout" && !this.ready) {
169
+ const socket = this.socket;
170
+ if (this.closed || !socket) return true;
171
+ this.pendingCloseCategory = "relay_handshake_timeout";
172
+ terminateSocket(socket);
173
+ return true;
174
+ }
175
+ this.failPermanently("relay_protocol_error");
176
+ return true;
177
+ }
178
+
153
179
  connect() {
154
180
  if (this.closed || this.socket) return;
155
181
  const wsUrl = `${this.workerUrl.replace(/^http/i, "ws")}/daemon/ws`;
@@ -167,8 +193,17 @@ export class RelayConnection {
167
193
  return;
168
194
  }
169
195
  this.socket = socket;
196
+ this.clearTimer("connectTimer", "clearTimeout");
197
+ this.connectTimer = this.scheduler.setTimeout(() => {
198
+ if (this.socket !== socket || this.closed || this.isSocketOpen(socket)) return;
199
+ this.logger.debug?.("remote relay transport connection timed out", { timeout_ms: this.connectTimeoutMs });
200
+ this.pendingCloseCategory = "relay_connect_timeout";
201
+ terminateSocket(socket);
202
+ }, this.connectTimeoutMs);
203
+ this.connectTimer?.unref?.();
170
204
 
171
205
  socket.on("open", () => {
206
+ this.clearTimer("connectTimer", "clearTimeout");
172
207
  if (this.socket !== socket || this.closed) {
173
208
  try { socket.close(1000, "stale daemon connection"); } catch {}
174
209
  return;
@@ -204,6 +239,7 @@ export class RelayConnection {
204
239
  const wasReady = this.ready;
205
240
  this.socket = null;
206
241
  this.ready = false;
242
+ this.clearTimer("connectTimer", "clearTimeout");
207
243
  this.clearTimer("heartbeatTimer", "clearInterval");
208
244
  this.clearTimer("handshakeTimer", "clearTimeout");
209
245
  const reasonText = sanitizeCloseReason(reason);
@@ -265,6 +301,7 @@ export class RelayConnection {
265
301
  this.closed = true;
266
302
  this.ready = false;
267
303
  this.socket = null;
304
+ this.clearTimer("connectTimer", "clearTimeout");
268
305
  this.clearTimer("heartbeatTimer", "clearInterval");
269
306
  this.clearTimer("handshakeTimer", "clearTimeout");
270
307
  this.clearTimer("reconnectTimer", "clearTimeout");
@@ -286,7 +323,8 @@ export class RelayConnection {
286
323
  reject(error);
287
324
  return;
288
325
  }
289
- this.logger.error?.(message, { cause: relayCloseUserCause(category) });
326
+ this.logger.error?.(message);
327
+ this.logger.debug?.("remote relay fatal details", { category, cause: relayCloseUserCause(category) });
290
328
  queueMicrotask(() => {
291
329
  try { this.onFatal(error); } catch (callbackError) {
292
330
  this.logger.error?.("relay fatal callback failed", { error_class: classifyOperationalError(callbackError) });
@@ -299,7 +337,6 @@ export class RelayConnection {
299
337
  this.recordOutage(category);
300
338
  const delay = this.reconnectDelay(this.reconnectAttempt++);
301
339
  this.scheduleOutageWarning();
302
- this.maybeRepeatOutageWarning();
303
340
  this.logger.debug?.("scheduling daemon reconnect", { delay_ms: delay, attempt: this.outageAttempts });
304
341
  this.reconnectTimer = this.scheduler.setTimeout(() => {
305
342
  this.reconnectTimer = null;
@@ -352,31 +389,39 @@ export class RelayConnection {
352
389
  }
353
390
 
354
391
  scheduleOutageWarning() {
355
- if (this.outageNoticeEmitted || this.outageWarnTimer || this.outageStartedAt === 0) return;
356
- const elapsed = Math.max(0, this.now() - this.outageStartedAt);
357
- const delay = Math.max(0, this.outageWarnAfterMs - elapsed);
392
+ if (this.outageWarnTimer || this.outageStartedAt === 0 || this.closed || this.ready) return;
393
+ const dueAt = this.outageNoticeEmitted
394
+ ? this.lastOutageWarnAt + this.nextOutageWarningDelay()
395
+ : this.outageStartedAt + this.outageWarnAfterMs;
396
+ const delay = Math.max(0, dueAt - this.now());
358
397
  this.outageWarnTimer = this.scheduler.setTimeout(() => {
359
398
  this.outageWarnTimer = null;
360
399
  if (this.closed || this.ready || this.outageStartedAt === 0) return;
361
400
  this.emitOutageWarning();
401
+ this.scheduleOutageWarning();
362
402
  }, delay);
363
403
  this.outageWarnTimer?.unref?.();
364
404
  }
365
405
 
366
- maybeRepeatOutageWarning() {
367
- if (!this.outageNoticeEmitted || this.lastOutageWarnAt === 0) return;
368
- if (this.now() - this.lastOutageWarnAt < this.outageWarnRepeatMs) return;
369
- this.emitOutageWarning();
406
+ nextOutageWarningDelay() {
407
+ const exponent = Math.max(0, Math.min(this.outageWarningCount - 1, 20));
408
+ return Math.min(this.outageWarnRepeatMs * (2 ** exponent), this.outageWarnMaxRepeatMs);
370
409
  }
371
410
 
372
411
  emitOutageWarning() {
373
412
  const outageMs = Math.max(0, this.now() - this.outageStartedAt);
374
413
  this.outageNoticeEmitted = true;
414
+ this.outageWarningCount += 1;
375
415
  this.lastOutageWarnAt = this.now();
376
- this.logger.warn?.(relayOutageWarningMessage(), {
416
+ const cause = relayCloseUserCause(this.lastCloseCategory);
417
+ const action = outageMs >= 5 * 60_000
418
+ ? " If this persists, check internet access and the deployed Worker."
419
+ : "";
420
+ this.logger.warn?.(`remote relay unavailable for ${formatDuration(outageMs)}; reconnecting automatically (${formatAttempts(this.outageAttempts)}; ${cause}).${action}`);
421
+ this.logger.debug?.("remote relay outage details", {
377
422
  outage_seconds: roundSeconds(outageMs),
378
423
  attempts: this.outageAttempts,
379
- cause: relayCloseUserCause(this.lastCloseCategory),
424
+ cause,
380
425
  ...(this.lastTransportErrorClass ? { error_class: this.lastTransportErrorClass } : {}),
381
426
  });
382
427
  }
@@ -386,6 +431,7 @@ export class RelayConnection {
386
431
  this.outageStartedAt = 0;
387
432
  this.outageAttempts = 0;
388
433
  this.outageNoticeEmitted = false;
434
+ this.outageWarningCount = 0;
389
435
  this.lastOutageWarnAt = 0;
390
436
  this.lastCloseCategory = "connection_interrupted";
391
437
  this.lastTransportErrorClass = "";
@@ -406,10 +452,15 @@ export class RelayConnection {
406
452
 
407
453
  export function relayCloseCategory(code, reason = "") {
408
454
  const numeric = Number(code);
409
- if (isSupersededClose(numeric, reason)) return "superseded";
455
+ const reasonText = String(reason || "");
456
+ if (isSupersededClose(numeric, reasonText)) return "superseded";
457
+ if (numeric === 1008 && reasonText === "daemon hello timeout") return "relay_handshake_timeout";
458
+ if (numeric === 1008 && ["stale daemon candidate", "expired daemon candidate"].includes(reasonText)) return "relay_restarting_or_unavailable";
459
+ if (numeric === 1008 && ["daemon hello required", "missing daemon attachment", "invalid daemon candidate timestamp"].includes(reasonText)) return "relay_protocol_error";
410
460
  if (numeric === 1000) return "normal_close";
411
461
  if (numeric === 1001 || numeric === 1012 || numeric === 1013) return "relay_restarting_or_unavailable";
412
462
  if (numeric === 1006) return "connection_interrupted";
463
+ if (numeric === 1002) return "relay_protocol_error";
413
464
  if (numeric === 1007) return "invalid_transport_payload";
414
465
  if (numeric === 1008) return "relay_policy_rejected";
415
466
  if (numeric === 1009) return "message_too_large";
@@ -421,13 +472,12 @@ function relayFatalMessage(category) {
421
472
  if (category === "relay_protocol_mismatch") {
422
473
  return "remote relay identity or version does not match this daemon; upgrade and redeploy both components";
423
474
  }
475
+ if (category === "relay_protocol_error") {
476
+ return "remote relay protocol error; upgrade and redeploy both components, then restart the daemon";
477
+ }
424
478
  return "remote relay rejected the daemon connection; verify credentials or redeploy the Worker";
425
479
  }
426
480
 
427
- function relayOutageWarningMessage() {
428
- return "remote relay is unavailable; automatic reconnection is still in progress";
429
- }
430
-
431
481
  function relayCloseUserCause(category) {
432
482
  const causes = {
433
483
  connection_interrupted: "connection interrupted",
@@ -436,9 +486,11 @@ function relayCloseUserCause(category) {
436
486
  relay_internal_error: "relay internal error",
437
487
  relay_protocol_mismatch: "relay identity or version mismatch",
438
488
  relay_authentication_failed: "relay authentication failed",
489
+ relay_connect_timeout: "relay connection attempt timed out",
439
490
  relay_handshake_timeout: "relay authentication acknowledgement timed out",
440
491
  relay_heartbeat_timeout: "relay stopped responding",
441
492
  relay_transport_error: "relay transport error",
493
+ relay_protocol_error: "relay protocol error",
442
494
  invalid_transport_payload: "invalid transport payload",
443
495
  message_too_large: "message exceeded the relay limit",
444
496
  normal_close: "connection closed",
@@ -512,6 +564,37 @@ function boundedPositiveInteger(value, fallback) {
512
564
  return Number.isFinite(number) && number > 0 ? Math.floor(number) : fallback;
513
565
  }
514
566
 
567
+ function sanitizeProtocolErrorCode(value) {
568
+ const code = String(value || "unknown_error").replace(/[^A-Za-z0-9_-]/g, "_").slice(0, MAX_PROTOCOL_ERROR_CODE_CHARS);
569
+ return code || "unknown_error";
570
+ }
571
+
572
+ function formatAttempts(value) {
573
+ const attempts = Math.max(1, Math.floor(Number(value) || 1));
574
+ return `${attempts} reconnect attempt${attempts === 1 ? "" : "s"}`;
575
+ }
576
+
577
+ function formatDuration(milliseconds) {
578
+ let seconds = Math.max(1, Math.round(Number(milliseconds) / 1000));
579
+ const units = [
580
+ ["day", 86_400],
581
+ ["hour", 3_600],
582
+ ["minute", 60],
583
+ ["second", 1],
584
+ ];
585
+ const parts = [];
586
+ for (const [label, size] of units) {
587
+ if (seconds < size && parts.length === 0) continue;
588
+ const amount = Math.floor(seconds / size);
589
+ if (amount > 0) {
590
+ parts.push(`${amount} ${label}${amount === 1 ? "" : "s"}`);
591
+ seconds -= amount * size;
592
+ }
593
+ if (parts.length === 2) break;
594
+ }
595
+ return parts.join(" ") || "1 second";
596
+ }
597
+
515
598
  function roundSeconds(milliseconds) {
516
599
  return Math.max(1, Math.round(milliseconds / 1000));
517
600
  }
@@ -14,6 +14,7 @@ import { classifyOperationalError } from "./log.mjs";
14
14
  import { ManagedJobManager } from "./managed-jobs.mjs";
15
15
  import { generateRegisteredSshKey } from "./resource-operations.mjs";
16
16
  import { expandHome } from "./state.mjs";
17
+ import { AgentContextManager } from "./agent-context.mjs";
17
18
 
18
19
  export const MAX_WRITE_BYTES = 5 * 1024 * 1024;
19
20
  const MAX_WS_MESSAGE_BYTES = 8 * 1024 * 1024;
@@ -27,6 +28,11 @@ const SLOW_TOOL_CALL_MS = 30_000;
27
28
  const RUNTIME_TOOL_HANDLERS = Object.freeze({
28
29
  server_info: (runtime) => runtime.runtimeInfo(),
29
30
  project_overview: (runtime, _args, context) => runtime.projectOverview(context),
31
+ agent_context: (runtime, args, context) => runtime.agentContextManager.agentContext(args, context),
32
+ list_local_skills: (runtime, args, context) => runtime.agentContextManager.listLocalSkills(args, context),
33
+ load_local_skill: (runtime, args, context) => runtime.agentContextManager.loadLocalSkill(args, context),
34
+ list_local_commands: (runtime, args, context) => runtime.agentContextManager.listLocalCommands(args, context),
35
+ run_local_command: (runtime, args, context) => runtime.runLocalCommand(args, context),
30
36
  list_roots: (runtime) => runtime.listRoots(),
31
37
  list_dir: (runtime, args, context) => runtime.listDir(args.path || ".", context),
32
38
  list_files: (runtime, args, context) => runtime.listFiles(args.path || ".", clampInt(args.max_files, 1000, 1, 10000), context),
@@ -101,6 +107,13 @@ export class LocalRuntime {
101
107
  displayPath: (value) => this.displayPath(value),
102
108
  throwIfCancelled: (context) => this.throwIfCancelled(context),
103
109
  });
110
+ this.agentContextManager = new AgentContextManager({
111
+ workspace: this.workspace,
112
+ policy: this.policy,
113
+ displayPath: (value) => this.displayPath(value),
114
+ resolveExistingPath: (value) => this.resolveExistingPath(value),
115
+ throwIfCancelled: (context) => this.throwIfCancelled(context),
116
+ });
104
117
  this.relay = createRelayConnection(this, {
105
118
  workerUrl: remoteWorkerUrl,
106
119
  secret: remoteSecret,
@@ -176,12 +189,16 @@ export class LocalRuntime {
176
189
  async handleMessage(raw) {
177
190
  let message;
178
191
  try { message = JSON.parse(raw); } catch {
179
- this.logger.warn?.("invalid websocket JSON");
192
+ this.handleRelayProtocolViolation("invalid_server_json");
193
+ return;
194
+ }
195
+ if (!isPlainRecord(message)) {
196
+ this.handleRelayProtocolViolation("invalid_server_message");
180
197
  return;
181
198
  }
182
199
  if (this.handleRelayControlMessage(message)) return;
183
200
  if (message.type !== "tool_call") {
184
- this.logger.warn?.("unknown websocket message", { type: String(message.type || "") });
201
+ this.handleRelayProtocolViolation("unexpected_server_message_type");
185
202
  return;
186
203
  }
187
204
  await this.handleRelayToolCall(message);
@@ -197,6 +214,10 @@ export class LocalRuntime {
197
214
  return true;
198
215
  }
199
216
  if (message.type === "pong") return true;
217
+ if (message.type === "error") {
218
+ this.relay?.handleServerError(message);
219
+ return true;
220
+ }
200
221
  if (message.type === "cancel_call") {
201
222
  if (typeof message.id === "string") this.cancelCall(message.id, "remote cancellation");
202
223
  return true;
@@ -204,6 +225,14 @@ export class LocalRuntime {
204
225
  return false;
205
226
  }
206
227
 
228
+ handleRelayProtocolViolation(errorCode) {
229
+ if (this.relay) {
230
+ this.relay.handleServerError({ type: "error", error: errorCode });
231
+ return;
232
+ }
233
+ this.logger.error?.("remote relay protocol error; upgrade and redeploy both components, then restart the daemon");
234
+ }
235
+
207
236
  async handleRelayToolCall(message) {
208
237
  const envelope = normalizeRelayToolCall(message);
209
238
  if (!envelope.ok) {
@@ -717,6 +746,25 @@ export class LocalRuntime {
717
746
  return this.runProcess(argv[0], argv.slice(1), clampInt(args.timeout_seconds, 120, 1, 600) * 1000, false, 512 * 1024, context, cwd);
718
747
  }
719
748
 
749
+ async runLocalCommand(args, context = {}) {
750
+ if (this.policy.execMode !== "direct" && this.policy.execMode !== "shell") throw new Error("run_local_command is disabled by daemon policy");
751
+ const command = await this.agentContextManager.resolveLocalCommand(args, context);
752
+ const argv = validateArgv(command.argv);
753
+ const cwd = await this.resolveExistingPath(command.cwd);
754
+ if (!(await stat(cwd)).isDirectory()) throw new Error("registered command cwd is not a directory");
755
+ const requestedTimeout = args.timeout_seconds === undefined
756
+ ? command.timeoutSeconds
757
+ : clampInt(args.timeout_seconds, command.timeoutSeconds, 1, 600);
758
+ const timeoutSeconds = Math.min(requestedTimeout, command.timeoutSeconds);
759
+ const result = await this.runProcess(argv[0], argv.slice(1), timeoutSeconds * 1000, false, 512 * 1024, context, cwd);
760
+ return {
761
+ name: command.name,
762
+ cwd: this.displayPath(cwd),
763
+ timeout_seconds: timeoutSeconds,
764
+ ...result,
765
+ };
766
+ }
767
+
720
768
  async execCommand(command, timeoutSeconds, context = {}) {
721
769
  if (this.policy.execMode !== "shell") throw new Error("exec_command requires shell execution mode");
722
770
  if (!command || typeof command !== "string") throw new Error("command is required");
@@ -1154,7 +1202,7 @@ function createRelayConnection(runtime, { workerUrl, secret, expectedVersion, on
1154
1202
  function handleRelayData(runtime, data) {
1155
1203
  const raw = typeof data === "string" ? data : Buffer.from(data).toString("utf8");
1156
1204
  if (Buffer.byteLength(raw) > MAX_WS_MESSAGE_BYTES) {
1157
- runtime.logger.warn?.("oversized websocket message rejected");
1205
+ runtime.handleRelayProtocolViolation("server_message_too_large");
1158
1206
  return;
1159
1207
  }
1160
1208
  return runtime.handleMessage(raw);
@@ -9,7 +9,7 @@ import { readBoundedRegularFileSync } from "./secure-file.mjs";
9
9
  const LABEL = "dev.machine-bridge-mcp.daemon";
10
10
  const WINDOWS_TASK = "MachineBridgeMCP";
11
11
  const SERVICE_COMMAND_OUTPUT_BYTES = 64 * 1024;
12
- const AUTOSTART_LOG_SCHEMA_VERSION = 2;
12
+ const AUTOSTART_LOG_SCHEMA_VERSION = 3;
13
13
 
14
14
  function serviceRun(command, args) {
15
15
  return run(command, args, {
@@ -8,6 +8,7 @@
8
8
  ],
9
9
  "instructions": [
10
10
  "You are connected to a local workspace through machine-bridge-mcp.",
11
+ "For substantive workspace tasks, call agent_context for the relevant target path before editing or executing. Apply returned instruction files in precedence order, load only relevant local skills, and prefer registered local commands for repeatable workflows.",
11
12
  "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
13
  "Filesystem scope and path display follow the active local policy; the default full profile is unrestricted.",
13
14
  "Filename sensitivity is not classified by this server; the MCP host, connector gateway, local OS, or endpoint-security software may enforce additional independent rules.",
@@ -31,6 +31,178 @@
31
31
  "additionalProperties": false
32
32
  }
33
33
  },
34
+ {
35
+ "name": "agent_context",
36
+ "title": "Load agent context",
37
+ "description": "Discover Codex-compatible global and root-to-target instruction precedence, progressively disclosed local skills, and registered commands for a target path.",
38
+ "availability": "always",
39
+ "annotations": {
40
+ "readOnlyHint": true,
41
+ "destructiveHint": false,
42
+ "idempotentHint": true,
43
+ "openWorldHint": false
44
+ },
45
+ "inputSchema": {
46
+ "type": "object",
47
+ "properties": {
48
+ "path": {
49
+ "type": "string",
50
+ "default": ".",
51
+ "maxLength": 32768
52
+ },
53
+ "include_instruction_content": {
54
+ "type": "boolean",
55
+ "default": true
56
+ },
57
+ "max_skills": {
58
+ "type": "integer",
59
+ "minimum": 1,
60
+ "maximum": 500,
61
+ "default": 100
62
+ }
63
+ },
64
+ "additionalProperties": false
65
+ }
66
+ },
67
+ {
68
+ "name": "list_local_skills",
69
+ "title": "List local skills",
70
+ "description": "Search bounded SKILL.md or skill.md bundles from Codex-compatible and explicitly configured skill roots without executing them.",
71
+ "availability": "always",
72
+ "annotations": {
73
+ "readOnlyHint": true,
74
+ "destructiveHint": false,
75
+ "idempotentHint": true,
76
+ "openWorldHint": false
77
+ },
78
+ "inputSchema": {
79
+ "type": "object",
80
+ "properties": {
81
+ "path": {
82
+ "type": "string",
83
+ "default": ".",
84
+ "maxLength": 32768
85
+ },
86
+ "query": {
87
+ "type": "string",
88
+ "default": "",
89
+ "maxLength": 1000
90
+ },
91
+ "max_results": {
92
+ "type": "integer",
93
+ "minimum": 1,
94
+ "maximum": 500,
95
+ "default": 100
96
+ }
97
+ },
98
+ "additionalProperties": false
99
+ }
100
+ },
101
+ {
102
+ "name": "load_local_skill",
103
+ "title": "Load local skill",
104
+ "description": "Load one discovered local skill entrypoint and a bounded relative file inventory. Loading returns instructions and never executes skill files implicitly.",
105
+ "availability": "always",
106
+ "annotations": {
107
+ "readOnlyHint": true,
108
+ "destructiveHint": false,
109
+ "idempotentHint": true,
110
+ "openWorldHint": false
111
+ },
112
+ "inputSchema": {
113
+ "type": "object",
114
+ "properties": {
115
+ "skill": {
116
+ "type": "string",
117
+ "minLength": 1,
118
+ "maxLength": 32768
119
+ },
120
+ "path": {
121
+ "type": "string",
122
+ "default": ".",
123
+ "maxLength": 32768
124
+ },
125
+ "max_files": {
126
+ "type": "integer",
127
+ "minimum": 1,
128
+ "maximum": 500,
129
+ "default": 200
130
+ }
131
+ },
132
+ "required": [
133
+ "skill"
134
+ ],
135
+ "additionalProperties": false
136
+ }
137
+ },
138
+ {
139
+ "name": "list_local_commands",
140
+ "title": "List registered local commands",
141
+ "description": "List commands registered by hierarchical .machine-bridge/agent.json manifests for the selected target path.",
142
+ "availability": "always",
143
+ "annotations": {
144
+ "readOnlyHint": true,
145
+ "destructiveHint": false,
146
+ "idempotentHint": true,
147
+ "openWorldHint": false
148
+ },
149
+ "inputSchema": {
150
+ "type": "object",
151
+ "properties": {
152
+ "path": {
153
+ "type": "string",
154
+ "default": ".",
155
+ "maxLength": 32768
156
+ }
157
+ },
158
+ "additionalProperties": false
159
+ }
160
+ },
161
+ {
162
+ "name": "run_local_command",
163
+ "title": "Run registered local command",
164
+ "description": "Execute a named command registered in .machine-bridge/agent.json as a direct argv process without shell parsing. Manifest timeout and argument policy remain authoritative.",
165
+ "availability": "direct-exec",
166
+ "annotations": {
167
+ "readOnlyHint": false,
168
+ "destructiveHint": true,
169
+ "idempotentHint": false,
170
+ "openWorldHint": true
171
+ },
172
+ "inputSchema": {
173
+ "type": "object",
174
+ "properties": {
175
+ "name": {
176
+ "type": "string",
177
+ "minLength": 1,
178
+ "maxLength": 64
179
+ },
180
+ "path": {
181
+ "type": "string",
182
+ "default": ".",
183
+ "maxLength": 32768
184
+ },
185
+ "args": {
186
+ "type": "array",
187
+ "items": {
188
+ "type": "string",
189
+ "maxLength": 65536
190
+ },
191
+ "maxItems": 64,
192
+ "default": []
193
+ },
194
+ "timeout_seconds": {
195
+ "type": "integer",
196
+ "minimum": 1,
197
+ "maximum": 600
198
+ }
199
+ },
200
+ "required": [
201
+ "name"
202
+ ],
203
+ "additionalProperties": false
204
+ }
205
+ },
34
206
  {
35
207
  "name": "list_roots",
36
208
  "title": "List workspace roots",