mandala-computer-mcp 0.4.0 → 0.6.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.
Files changed (128) hide show
  1. package/README.md +558 -29
  2. package/dist/api.d.ts +59 -1
  3. package/dist/api.d.ts.map +1 -1
  4. package/dist/api.js +404 -33
  5. package/dist/api.js.map +1 -1
  6. package/dist/artifacts.d.ts +63 -0
  7. package/dist/artifacts.d.ts.map +1 -0
  8. package/dist/artifacts.js +81 -0
  9. package/dist/artifacts.js.map +1 -0
  10. package/dist/cli.d.ts +4 -0
  11. package/dist/cli.d.ts.map +1 -1
  12. package/dist/cli.js +52 -21
  13. package/dist/cli.js.map +1 -1
  14. package/dist/credentials.d.ts +33 -0
  15. package/dist/credentials.d.ts.map +1 -0
  16. package/dist/credentials.js +395 -0
  17. package/dist/credentials.js.map +1 -0
  18. package/dist/errors.d.ts +122 -11
  19. package/dist/errors.d.ts.map +1 -1
  20. package/dist/errors.js +237 -41
  21. package/dist/errors.js.map +1 -1
  22. package/dist/executions.d.ts +43 -0
  23. package/dist/executions.d.ts.map +1 -0
  24. package/dist/executions.js +162 -0
  25. package/dist/executions.js.map +1 -0
  26. package/dist/format.d.ts +33 -8
  27. package/dist/format.d.ts.map +1 -1
  28. package/dist/format.js +107 -12
  29. package/dist/format.js.map +1 -1
  30. package/dist/http.d.ts +42 -0
  31. package/dist/http.d.ts.map +1 -1
  32. package/dist/http.js +383 -3
  33. package/dist/http.js.map +1 -1
  34. package/dist/index.d.ts +5 -4
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +3 -3
  37. package/dist/index.js.map +1 -1
  38. package/dist/paths.d.ts +50 -2
  39. package/dist/paths.d.ts.map +1 -1
  40. package/dist/paths.js +65 -5
  41. package/dist/paths.js.map +1 -1
  42. package/dist/results.d.ts +97 -0
  43. package/dist/results.d.ts.map +1 -0
  44. package/dist/results.js +263 -0
  45. package/dist/results.js.map +1 -0
  46. package/dist/secret-errors.d.ts +59 -0
  47. package/dist/secret-errors.d.ts.map +1 -0
  48. package/dist/secret-errors.js +199 -0
  49. package/dist/secret-errors.js.map +1 -0
  50. package/dist/secret-store.d.ts +115 -0
  51. package/dist/secret-store.d.ts.map +1 -0
  52. package/dist/secret-store.js +105 -0
  53. package/dist/secret-store.js.map +1 -0
  54. package/dist/server.d.ts +3 -2
  55. package/dist/server.d.ts.map +1 -1
  56. package/dist/server.js +60 -9
  57. package/dist/server.js.map +1 -1
  58. package/dist/session.d.ts +6 -1
  59. package/dist/session.d.ts.map +1 -1
  60. package/dist/session.js +1 -1
  61. package/dist/session.js.map +1 -1
  62. package/dist/stdio.d.ts +5 -1
  63. package/dist/stdio.d.ts.map +1 -1
  64. package/dist/stdio.js +10 -4
  65. package/dist/stdio.js.map +1 -1
  66. package/dist/tool-filters.d.ts +38 -0
  67. package/dist/tool-filters.d.ts.map +1 -0
  68. package/dist/tool-filters.js +137 -0
  69. package/dist/tool-filters.js.map +1 -0
  70. package/dist/tools/account.d.ts +3 -0
  71. package/dist/tools/account.d.ts.map +1 -0
  72. package/dist/tools/account.js +122 -0
  73. package/dist/tools/account.js.map +1 -0
  74. package/dist/tools/activities.d.ts +3 -0
  75. package/dist/tools/activities.d.ts.map +1 -0
  76. package/dist/tools/activities.js +140 -0
  77. package/dist/tools/activities.js.map +1 -0
  78. package/dist/tools/agent.d.ts.map +1 -1
  79. package/dist/tools/agent.js +21 -9
  80. package/dist/tools/agent.js.map +1 -1
  81. package/dist/tools/artifacts.d.ts +3 -0
  82. package/dist/tools/artifacts.d.ts.map +1 -0
  83. package/dist/tools/artifacts.js +97 -0
  84. package/dist/tools/artifacts.js.map +1 -0
  85. package/dist/tools/chat.d.ts +6 -0
  86. package/dist/tools/chat.d.ts.map +1 -0
  87. package/dist/tools/chat.js +192 -0
  88. package/dist/tools/chat.js.map +1 -0
  89. package/dist/tools/computers.d.ts.map +1 -1
  90. package/dist/tools/computers.js +28 -16
  91. package/dist/tools/computers.js.map +1 -1
  92. package/dist/tools/directory.d.ts +13 -0
  93. package/dist/tools/directory.d.ts.map +1 -0
  94. package/dist/tools/directory.js +88 -0
  95. package/dist/tools/directory.js.map +1 -0
  96. package/dist/tools/executions.d.ts +3 -0
  97. package/dist/tools/executions.d.ts.map +1 -0
  98. package/dist/tools/executions.js +87 -0
  99. package/dist/tools/executions.js.map +1 -0
  100. package/dist/tools/guest.d.ts.map +1 -1
  101. package/dist/tools/guest.js +184 -35
  102. package/dist/tools/guest.js.map +1 -1
  103. package/dist/tools/input.d.ts +2 -0
  104. package/dist/tools/input.d.ts.map +1 -1
  105. package/dist/tools/input.js +31 -5
  106. package/dist/tools/input.js.map +1 -1
  107. package/dist/tools/results.d.ts +30 -0
  108. package/dist/tools/results.d.ts.map +1 -0
  109. package/dist/tools/results.js +106 -0
  110. package/dist/tools/results.js.map +1 -0
  111. package/dist/tools/secrets.d.ts +78 -0
  112. package/dist/tools/secrets.d.ts.map +1 -0
  113. package/dist/tools/secrets.js +448 -0
  114. package/dist/tools/secrets.js.map +1 -0
  115. package/dist/tools/signals.d.ts +3 -0
  116. package/dist/tools/signals.d.ts.map +1 -0
  117. package/dist/tools/signals.js +116 -0
  118. package/dist/tools/signals.js.map +1 -0
  119. package/dist/tools/snapshots.d.ts.map +1 -1
  120. package/dist/tools/snapshots.js +52 -10
  121. package/dist/tools/snapshots.js.map +1 -1
  122. package/dist/tools/ssh.d.ts +3 -0
  123. package/dist/tools/ssh.d.ts.map +1 -0
  124. package/dist/tools/ssh.js +186 -0
  125. package/dist/tools/ssh.js.map +1 -0
  126. package/dist/tools/webhooks.js +1 -1
  127. package/dist/tools/webhooks.js.map +1 -1
  128. package/package.json +1 -1
@@ -0,0 +1,13 @@
1
+ import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
2
+ import { z } from 'zod';
3
+ import type { Registrar } from './types.js';
4
+ export declare const count: z.ZodNumber;
5
+ export declare const label: z.ZodString;
6
+ export declare const exitCode: z.ZodNumber;
7
+ export declare const executionId: z.ZodString;
8
+ /** Strip unknown fields and never print a schema exception containing response values. */
9
+ export declare function metadata<T>(schema: z.ZodType<T>, value: unknown): T;
10
+ /** Shared only by the passive metadata tools. No retry or readiness side effects. */
11
+ export declare function metadataCall(work: () => Promise<CallToolResult>): Promise<CallToolResult>;
12
+ export declare const registerDirectory: Registrar;
13
+ //# sourceMappingURL=directory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"directory.d.ts","sourceRoot":"","sources":["../../src/tools/directory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AACzE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAKxB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C,eAAO,MAAM,KAAK,aAAwC,CAAC;AAC3D,eAAO,MAAM,KAAK,aAAoB,CAAC;AACvC,eAAO,MAAM,QAAQ,aAAoD,CAAC;AAC1E,eAAO,MAAM,WAAW,aAA0C,CAAC;AAEnE,0FAA0F;AAC1F,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,GAAG,CAAC,CAKnE;AAED,qFAAqF;AACrF,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,OAAO,CAAC,cAAc,CAAC,GAAG,OAAO,CAAC,cAAc,CAAC,CAqC/F;AAsBD,eAAO,MAAM,iBAAiB,EAAE,SAwC/B,CAAC"}
@@ -0,0 +1,88 @@
1
+ import { z } from 'zod';
2
+ import { APIError } from '../errors.js';
3
+ import { apiErrorMessage, failed, said } from '../format.js';
4
+ import * as P from '../paths.js';
5
+ import { computerSchema, readAnnotations } from './results.js';
6
+ export const count = z.number().int().nonnegative().safe();
7
+ export const label = z.string().min(1);
8
+ export const exitCode = z.number().int().min(-2147483648).max(2147483647);
9
+ export const executionId = z.string().regex(/^exec_[a-f0-9]{32}$/);
10
+ /** Strip unknown fields and never print a schema exception containing response values. */
11
+ export function metadata(schema, value) {
12
+ const parsed = schema.safeParse(value);
13
+ if (!parsed.success)
14
+ throw new Error('Malformed metadata response; no complete result was established.');
15
+ return parsed.data;
16
+ }
17
+ /** Shared only by the passive metadata tools. No retry or readiness side effects. */
18
+ export async function metadataCall(work) {
19
+ try {
20
+ return await work();
21
+ }
22
+ catch (error) {
23
+ if (!(error instanceof APIError))
24
+ return failed(error);
25
+ const fields = z
26
+ .object({
27
+ code: z.string().optional(),
28
+ reason: z.string().optional(),
29
+ incomplete: z.boolean().optional(),
30
+ })
31
+ .safeParse(error.body);
32
+ const detail = fields.success ? fields.data : {};
33
+ const nested = error.body?.error;
34
+ const message = apiErrorMessage(error);
35
+ const refusal = failed(new APIError(message, error.status, { ...detail, error: message, reason: error.reason }, error.retryAfterMs, error), nested === null || typeof nested !== 'object' || Array.isArray(nested));
36
+ return {
37
+ ...refusal,
38
+ content: [
39
+ ...refusal.content,
40
+ ...said('Refusal metadata:', {
41
+ code: detail.code,
42
+ incomplete: detail.incomplete,
43
+ ...(error.retryAfterMs === undefined ? {} : { retry_after_ms: error.retryAfterMs }),
44
+ }).content,
45
+ ],
46
+ };
47
+ }
48
+ }
49
+ const directorySchema = z.object({
50
+ path: label,
51
+ entries: z
52
+ .array(z
53
+ .object({
54
+ name: label,
55
+ type: z.enum(['file', 'directory', 'symlink', 'special', 'unavailable']),
56
+ size_bytes: count.optional(),
57
+ })
58
+ .refine((entry) => entry.type === 'file' || entry.size_bytes === undefined, 'Only regular files may report size_bytes'))
59
+ .max(512),
60
+ truncated: z.boolean(),
61
+ skipped: count,
62
+ });
63
+ export const registerDirectory = (server, session) => {
64
+ server.registerTool('list_directory', {
65
+ title: 'List a guest directory without waking it',
66
+ description: 'Read one passive, unordered directory listing from an already running computer. No resume or idle extension; ordinary API rate/capacity admission still applies. At most 512 examined entries/128 KiB, with no continuation token. Narrow the path when partial. Symlinks are not followed; a final symlink directory is refused. No file content is read.',
67
+ inputSchema: {
68
+ computer_id: computerSchema,
69
+ path: z
70
+ .string()
71
+ .startsWith('/')
72
+ .refine((path) => ![...path].some((char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127) &&
73
+ !P.hasUnpairedSurrogate(path), 'path must be an exact usable absolute guest path'),
74
+ },
75
+ annotations: readAnnotations,
76
+ }, ({ computer_id, path }, extra) => metadataCall(async () => {
77
+ const data = metadata(directorySchema, await session.api.json('GET', P.directory(session.resolve(computer_id)), {
78
+ query: { path },
79
+ signal: extra.signal,
80
+ }));
81
+ if (data.path !== path)
82
+ throw new Error('Directory response did not match the requested path.');
83
+ return said(data.truncated || data.skipped > 0
84
+ ? 'PARTIAL directory listing: entries or names were omitted. This is an unordered subset with no continuation token; use a narrower path.'
85
+ : 'Directory metadata only. An unavailable entry has no inferred type or size; symlinks are not followed.', data);
86
+ }));
87
+ };
88
+ //# sourceMappingURL=directory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"directory.js","sourceRoot":"","sources":["../../src/tools/directory.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { Registrar } from './types.js';
2
+ export declare const registerExecutions: Registrar;
3
+ //# sourceMappingURL=executions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"executions.d.ts","sourceRoot":"","sources":["../../src/tools/executions.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AA4D5C,eAAO,MAAM,kBAAkB,EAAE,SAkDhC,CAAC"}
@@ -0,0 +1,87 @@
1
+ import { z } from 'zod';
2
+ import { APIError, CancelledError } from '../errors.js';
3
+ import { EXECUTION_OUTPUT_DEFAULT, EXECUTION_OUTPUT_MAX, EXECUTION_RESULT_MAX, ExecutionValueError, executionComputerId, executionMetadata, executionOutput, executionReadQuery, } from '../executions.js';
4
+ import { refused, said, withErrorMetadata } from '../format.js';
5
+ import * as P from '../paths.js';
6
+ const identity = {
7
+ computer_id: z
8
+ .string()
9
+ .min(1)
10
+ .max(256)
11
+ .optional()
12
+ .describe('Computer to read; defaults to use_computer selection.'),
13
+ execution_id: z
14
+ .string()
15
+ .length(37)
16
+ .regex(/^exec_[0-9a-f]{32}$/)
17
+ .describe('Stable execution_id returned by an accepted background exec; never a PID.'),
18
+ };
19
+ const offset = z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER);
20
+ const annotations = { readOnlyHint: true, destructiveHint: false, idempotentHint: true };
21
+ async function readResult(fn, signal) {
22
+ try {
23
+ if (signal.aborted)
24
+ return refused('Execution read cancelled. No command was replayed.');
25
+ const result = await fn();
26
+ if (signal.aborted)
27
+ return refused('Execution read cancelled. No command was replayed.');
28
+ if (Buffer.byteLength(JSON.stringify(result)) > EXECUTION_RESULT_MAX) {
29
+ return refused('Execution response cannot fit the bounded tool result. No output was presented and no command was replayed.');
30
+ }
31
+ return result;
32
+ }
33
+ catch (err) {
34
+ if (signal.aborted || err instanceof CancelledError)
35
+ return refused('Execution read cancelled. No command was replayed.');
36
+ if (err instanceof ExecutionValueError)
37
+ return refused(err.message);
38
+ if (err instanceof APIError) {
39
+ const status = err.status;
40
+ const advice = status === 404
41
+ ? 'This execution is unavailable; do not substitute a PID or start the command again.'
42
+ : status === 409
43
+ ? 'The output is unavailable in the current state; no resume was requested.'
44
+ : status === 401
45
+ ? 'A credential was refused; inspect the supplied classification before another read.'
46
+ : status === 403
47
+ ? 'The current credential does not have permission for this read.'
48
+ : 'The platform refused this read; no fallback was attempted.';
49
+ return withErrorMetadata(refused(`Execution read failed (HTTP ${status}). ${advice} No command was replayed.`), err);
50
+ }
51
+ // Network/JSON errors and redirect locations can contain untrusted text or private URLs.
52
+ return refused('Execution read could not be completed. Check the selected computer and connection; no fallback or command replay was attempted.');
53
+ }
54
+ }
55
+ export const registerExecutions = (server, session) => {
56
+ server.registerTool('get_execution', {
57
+ title: 'Read a stable execution observation',
58
+ description: 'Read one background execution by stable execution_id. Last-observed running, exited or lost; running is not proof the computer is awake, and lost establishes no success or failure. Metadata only: no guest I/O, activity refresh, resume, polling loop, PID fallback or command replay. Handles are volatile and can become unavailable after restart, replacement, expiry or cleanup.',
59
+ inputSchema: identity,
60
+ annotations,
61
+ }, ({ computer_id, execution_id }, extra) => readResult(async () => {
62
+ const id = executionComputerId(session.resolve(computer_id));
63
+ const data = await session.api
64
+ .with(extra.signal)
65
+ .json('GET', P.execution(id, execution_id));
66
+ return said('Last observed execution state; running does not prove the computer is awake, and lost is not an exit result.', executionMetadata(data, id, execution_id));
67
+ }, extra.signal));
68
+ server.registerTool('read_execution_output', {
69
+ title: 'Read an independent execution output chunk',
70
+ description: 'Read volatile, mutable guest output once at explicit independent stdout_offset and stderr_offset byte positions. Performs guest I/O but does not refresh activity, resume, change shared PID cursors, retry, tail, capture or replay a command. Unsuitable for passive Activities/history. Each stream is limited to 4096 bytes by default, at most 16384. Text is lossless UTF-8 (BOM preserved); NUL, binary and split UTF-8 remain exact base64. False more flags mean current EOF, not completion. Diagnostics repeat independently; only the first 4096 diagnostic bytes are displayed, with distinct available/displayed counts and display/daemon truncation flags.',
71
+ inputSchema: {
72
+ ...identity,
73
+ stdout_offset: offset,
74
+ stderr_offset: offset,
75
+ limit: z.number().int().min(1).max(EXECUTION_OUTPUT_MAX).default(EXECUTION_OUTPUT_DEFAULT),
76
+ },
77
+ annotations,
78
+ }, ({ computer_id, execution_id, stdout_offset, stderr_offset, limit }, extra) => readResult(async () => {
79
+ const query = executionReadQuery(stdout_offset, stderr_offset, limit);
80
+ const id = executionComputerId(session.resolve(computer_id));
81
+ const data = await session.api
82
+ .with(extra.signal)
83
+ .json('GET', P.executionOutput(id, execution_id), { query });
84
+ return said('Volatile guest output, not retained artifacts. False more flags mean current EOF only. Diagnostics repeat; a truncated display is only a prefix of the available diagnostic. No shared cursor was consumed.', executionOutput(data, execution_id, query));
85
+ }, extra.signal));
86
+ };
87
+ //# sourceMappingURL=executions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"executions.js","sourceRoot":"","sources":["../../src/tools/executions.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACxD,OAAO,EACL,wBAAwB,EACxB,oBAAoB,EACpB,oBAAoB,EACpB,mBAAmB,EACnB,mBAAmB,EACnB,iBAAiB,EACjB,eAAe,EACf,kBAAkB,GACnB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAChE,OAAO,KAAK,CAAC,MAAM,aAAa,CAAC;AAGjC,MAAM,QAAQ,GAAG;IACf,WAAW,EAAE,CAAC;SACX,MAAM,EAAE;SACR,GAAG,CAAC,CAAC,CAAC;SACN,GAAG,CAAC,GAAG,CAAC;SACR,QAAQ,EAAE;SACV,QAAQ,CAAC,uDAAuD,CAAC;IACpE,YAAY,EAAE,CAAC;SACZ,MAAM,EAAE;SACR,MAAM,CAAC,EAAE,CAAC;SACV,KAAK,CAAC,qBAAqB,CAAC;SAC5B,QAAQ,CAAC,2EAA2E,CAAC;CACzF,CAAC;AACF,MAAM,MAAM,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,gBAAgB,CAAC,CAAC;AAC3E,MAAM,WAAW,GAAG,EAAE,YAAY,EAAE,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,cAAc,EAAE,IAAI,EAAE,CAAC;AAEzF,KAAK,UAAU,UAAU,CACvB,EAAiC,EACjC,MAAmB;IAEnB,IAAI,CAAC;QACH,IAAI,MAAM,CAAC,OAAO;YAAE,OAAO,OAAO,CAAC,oDAAoD,CAAC,CAAC;QACzF,MAAM,MAAM,GAAG,MAAM,EAAE,EAAE,CAAC;QAC1B,IAAI,MAAM,CAAC,OAAO;YAAE,OAAO,OAAO,CAAC,oDAAoD,CAAC,CAAC;QACzF,IAAI,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,GAAG,oBAAoB,EAAE,CAAC;YACrE,OAAO,OAAO,CACZ,6GAA6G,CAC9G,CAAC;QACJ,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,MAAM,CAAC,OAAO,IAAI,GAAG,YAAY,cAAc;YACjD,OAAO,OAAO,CAAC,oDAAoD,CAAC,CAAC;QACvE,IAAI,GAAG,YAAY,mBAAmB;YAAE,OAAO,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACpE,IAAI,GAAG,YAAY,QAAQ,EAAE,CAAC;YAC5B,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC;YAC1B,MAAM,MAAM,GACV,MAAM,KAAK,GAAG;gBACZ,CAAC,CAAC,oFAAoF;gBACtF,CAAC,CAAC,MAAM,KAAK,GAAG;oBACd,CAAC,CAAC,0EAA0E;oBAC5E,CAAC,CAAC,MAAM,KAAK,GAAG;wBACd,CAAC,CAAC,oFAAoF;wBACtF,CAAC,CAAC,MAAM,KAAK,GAAG;4BACd,CAAC,CAAC,gEAAgE;4BAClE,CAAC,CAAC,4DAA4D,CAAC;YACzE,OAAO,iBAAiB,CACtB,OAAO,CAAC,+BAA+B,MAAM,MAAM,MAAM,2BAA2B,CAAC,EACrF,GAAG,CACJ,CAAC;QACJ,CAAC;QACD,yFAAyF;QACzF,OAAO,OAAO,CACZ,iIAAiI,CAClI,CAAC;IACJ,CAAC;AACH,CAAC;AAED,MAAM,CAAC,MAAM,kBAAkB,GAAc,CAAC,MAAM,EAAE,OAAO,EAAE,EAAE;IAC/D,MAAM,CAAC,YAAY,CACjB,eAAe,EACf;QACE,KAAK,EAAE,qCAAqC;QAC5C,WAAW,EACT,0XAA0X;QAC5X,WAAW,EAAE,QAAQ;QACrB,WAAW;KACZ,EACD,CAAC,EAAE,WAAW,EAAE,YAAY,EAAE,EAAE,KAAK,EAAE,EAAE,CACvC,UAAU,CAAC,KAAK,IAAI,EAAE;QACpB,MAAM,EAAE,GAAG,mBAAmB,CAAC,OAAO,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC;QAC7D,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,GAAG;aAC3B,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC;aAClB,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,SAAS,CAAC,EAAE,EAAE,YAAY,CAAC,CAAC,CAAC;QAC9C,OAAO,IAAI,CACT,8GAA8G,EAC9G,iBAAiB,CAAC,IAAI,EAAE,EAAE,EAAE,YAAY,CAAC,CAC1C,CAAC;IACJ,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CACnB,CAAC;IAEF,MAAM,CAAC,YAAY,CACjB,uBAAuB,EACvB;QACE,KAAK,EAAE,4CAA4C;QACnD,WAAW,EACT,4oBAA4oB;QAC9oB,WAAW,EAAE;YACX,GAAG,QAAQ;YACX,aAAa,EAAE,MAAM;YACrB,aAAa,EAAE,MAAM;YACrB,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAC,OAAO,CAAC,wBAAwB,CAAC;SAC3F;QACD,WAAW;KACZ,EACD,CAAC,EAAE,WAAW,EAAE,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,EAAE,CAC5E,UAAU,CAAC,KAAK,IAAI,EAAE;QACpB,MAAM,KAAK,GAAG,kBAAkB,CAAC,aAAa,EAAE,aAAa,EAAE,KAAK,CAAC,CAAC;QACtE,MAAM,EAAE,GAAG,mBAAmB,CAAC,OAAO,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC;QAC7D,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,GAAG;aAC3B,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC;aAClB,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,eAAe,CAAC,EAAE,EAAE,YAAY,CAAC,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;QAC/D,OAAO,IAAI,CACT,6MAA6M,EAC7M,eAAe,CAAC,IAAI,EAAE,YAAY,EAAE,KAAK,CAAC,CAC3C,CAAC;IACJ,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CACnB,CAAC;AACJ,CAAC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"guest.d.ts","sourceRoot":"","sources":["../../src/tools/guest.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAyK5C,eAAO,MAAM,aAAa,EAAE,SAgjB3B,CAAC"}
1
+ {"version":3,"file":"guest.d.ts","sourceRoot":"","sources":["../../src/tools/guest.ts"],"names":[],"mappings":"AA0BA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAgN5C,eAAO,MAAM,aAAa,EAAE,SAyrB3B,CAAC"}
@@ -1,7 +1,9 @@
1
1
  import { z } from 'zod';
2
- import { ConflictError, GatewayTimeoutError, platformSaid, RangeNotSatisfiableError, } from '../errors.js';
3
- import { guarded, image, isInlineImage, json, MAX_INLINE_IMAGE_BYTES, refused, said, text, } from '../format.js';
2
+ import { ConflictError, CreateOnlyConflictError, FileExistsError, GatewayTimeoutError, platformSaid, RangeNotSatisfiableError, } from '../errors.js';
3
+ import { apiErrorMessage, guarded, image, isInlineImage, json, MAX_INLINE_IMAGE_BYTES, refused, said, text, withErrorMetadata, } from '../format.js';
4
4
  import * as P from '../paths.js';
5
+ import { retainIntent, synchronousResultId } from '../results.js';
6
+ import { retentionSchema } from './results.js';
5
7
  const idArg = {
6
8
  computer_id: z
7
9
  .string()
@@ -34,6 +36,33 @@ const MAX_INLINE_BYTES = 256 * 1024;
34
36
  * body is cancelled rather than read.
35
37
  */
36
38
  const MAX_WINDOW_BYTES = MAX_INLINE_IMAGE_BYTES;
39
+ /**
40
+ * `no_wake` on a file transfer (OPL-5026): refuse rather than resume a
41
+ * computer that is not running. Sent as the one value the platform takes.
42
+ */
43
+ const noWakeArg = z
44
+ .boolean()
45
+ .default(false)
46
+ .describe('true: do not resume a computer that is not running — refuse instead (409), so nothing is resumed and nothing is charged for a resume. Use it to look at a suspended computer’s files only if it is already awake. false (the default) resumes a suspended computer to answer, which is charged.');
47
+ /**
48
+ * A 409 on a transfer sent with `no_wake`, read as the refusal it most likely is.
49
+ *
50
+ * The platform refuses a computer that is not running with a 409 that has, so
51
+ * far, carried no `reason` — a change under way gives it `unavailable`. Either
52
+ * way it means the same thing here, so both are answered alike; any other word
53
+ * is left to the ordinary refusal, which says what it means.
54
+ */
55
+ function noWakeRefusal(err, id, what) {
56
+ if (!(err instanceof ConflictError))
57
+ return undefined;
58
+ if (err instanceof FileExistsError)
59
+ return undefined;
60
+ if (err.reason !== undefined && err.reason !== 'unavailable')
61
+ return undefined;
62
+ return withErrorMetadata(refused(`${apiErrorMessage(err)} (HTTP ${err.status})\n\n${id} was not resumed, because no_wake was set: ` +
63
+ `most likely it is not running, and ${what}. start_computer wakes it (a resume is charged), ` +
64
+ 'or call again without no_wake to let this resume it.'), err);
65
+ }
37
66
  const absolutePath = (what) => z
38
67
  .string()
39
68
  .startsWith('/', `${what} must be an absolute path starting with /`)
@@ -72,7 +101,7 @@ const BACKGROUND_SLOTS_FULL = /already has (\d+) background commands? running/i;
72
101
  function backgroundSlotsFull(err) {
73
102
  if (!(err instanceof ConflictError) || err.reason !== undefined)
74
103
  return undefined;
75
- const held = Number(BACKGROUND_SLOTS_FULL.exec(err.message)?.[1]);
104
+ const held = Number(BACKGROUND_SLOTS_FULL.exec(apiErrorMessage(err))?.[1]);
76
105
  return Number.isSafeInteger(held) && held > 0 ? held : undefined;
77
106
  }
78
107
  /**
@@ -93,13 +122,13 @@ function backgroundSlotsFull(err) {
93
122
  * the caller say which one it is in, which is the difference between a next step
94
123
  * and a guess.
95
124
  */
96
- const backgroundFull = (err, held) => refused(`${err.message}\n\nA slot is held for as long as its command runs, and all ${held} are held now. ` +
125
+ const backgroundFull = (err, held) => withErrorMetadata(refused(`${apiErrorMessage(err)}\n\nA slot is held for as long as its command runs, and all ${held} are held now. ` +
97
126
  `Nothing on this side frees one: a command that has already finished is not counted, so there is ` +
98
127
  `nothing to reap, and a poll reads output rather than releasing anything. If any of the ${held} are ` +
99
128
  `servers, they do not exit on their own and another exec with background: true gets this same answer ` +
100
129
  `for as long as they run. The way out is to stop one you no longer need — exec_kill on a pid an ` +
101
130
  `earlier exec returned, with exec_poll to see which are still running. If they are builds or installs ` +
102
- `rather than servers, one of them finishes by itself and its slot comes back a moment later.`);
131
+ `rather than servers, one of them finishes by itself and its slot comes back a moment later.`), err);
103
132
  /**
104
133
  * A window action that timed out on the way back, turned into the next step it
105
134
  * needs — which is a READ, and never this call again (OPL-3910).
@@ -140,7 +169,7 @@ const windowOutcomeMayBeUnknown = (err) => {
140
169
  };
141
170
  const windowOutcomeUnknown = (err, action, windowId) => {
142
171
  const named = platformSaid(err.body);
143
- return refused(`${named ? `${named}\n\n` : ''}The ${action} on ${windowId} did not report a result before the ` +
172
+ return withErrorMetadata(refused(`${named ? `${named}\n\n` : ''}The ${action} on ${windowId} did not report a result before the ` +
144
173
  `deadline (HTTP ${err.status}). That is not a refusal and it is not a report that nothing ` +
145
174
  `happened: the guest may have taken the action and lost the race to say so, so the outcome is ` +
146
175
  `UNKNOWN and the ${action} may already have been applied. Do not send this call again to find ` +
@@ -150,7 +179,7 @@ const windowOutcomeUnknown = (err, action, windowId) => {
150
179
  `window id is not reserved forever — the X server can hand the same id to something else — ` +
151
180
  `so a second close is not safely a no-op on a window that has already gone.`
152
181
  : `A repeated ${action} is untidy rather than destructive, but it still answers with a guess ` +
153
- `where a read answers with the window.`));
182
+ `where a read answers with the window.`)), err);
154
183
  };
155
184
  export const registerGuest = (server, session) => {
156
185
  server.registerTool('exec', {
@@ -159,6 +188,10 @@ export const registerGuest = (server, session) => {
159
188
  inputSchema: {
160
189
  ...idArg,
161
190
  command: z.string().describe('A shell command line.'),
191
+ retain_output: z
192
+ .union([z.boolean(), retentionSchema])
193
+ .optional()
194
+ .describe('Explicit synchronous output retention only. False/absent preserves default behavior. A valid result_id confirms a retained version; optional failure never replays the command.'),
162
195
  timeout_s: z
163
196
  .number()
164
197
  .int()
@@ -169,26 +202,35 @@ export const registerGuest = (server, session) => {
169
202
  desktop: z
170
203
  .boolean()
171
204
  .default(false)
172
- .describe('Run inside the logged-in desktop session instead of as root with no display. Required for anything with a window: the guest agent has no DISPLAY, so a GUI app started without this cannot draw. Linux only.'),
205
+ .describe('Run inside the logged-in desktop session instead of as root with no display. Required for anything with a window: the guest agent has no DISPLAY, so a GUI app started without this cannot draw. Also the way to reach secrets bound to this computer as environment variables (set_computer_secrets): they live in the desktop session, and on an image that supports it a value replaced while the computer runs is seen by new desktop-session commands within seconds. Whether a plain root exec sees bound secrets depends on the platform version, so a command that needs one should use this. Linux only.'),
173
206
  background: z
174
207
  .boolean()
175
208
  .default(false)
176
209
  .describe('Return a handle immediately instead of waiting. Use for builds, installs, test suites and servers, then read output with exec_poll. To learn that it finished, wait_for_event with types ["process.exited"] and the pid this returns — the computer reports the exit, so waiting for it costs one call rather than an exec_poll loop. Strictly better than backgrounding with "&", which throws away the exit code and the output. A computer runs at most sixteen of these at once, and past that this is refused with a 409 saying how many are already running rather than queued. A slot is held until its command exits, which for a server is never, so the way out of that refusal is exec_kill on a pid an earlier exec returned — not another attempt.'),
177
- cwd: absolutePath('cwd').optional(),
210
+ cwd: absolutePath('cwd')
211
+ .optional()
212
+ .describe('Absolute path to run in. If it does not exist the command does not run, and exit_code is 127.'),
178
213
  env: z
179
214
  .record(z.string(), z.string())
180
215
  .optional()
181
216
  .describe('Environment for this command, as {NAME: "value"}. Use it instead of writing FOO=bar in front of the command: a prefix assignment is shell syntax, so a value holding a space, a quote, a newline or a $ has to be quoted correctly by you and is silently truncated or re-parsed when it is not, while this reaches the process whole and unquoted. It also keeps a secret out of the command line, which is world-readable in the guest\'s ps and, for a background command, comes back to you inside every exec_poll answer. The variables are added on top of the guest\'s login profile rather than replacing it, so PATH and the rest are still there, and they apply to this command only — including with desktop: true. Names must not be empty or contain "=".'),
182
217
  },
183
- }, ({ computer_id, command, timeout_s, desktop, background, cwd, env }, extra) => guarded(async () => {
218
+ }, ({ computer_id, command, timeout_s, desktop, background, cwd, env, retain_output }, extra) => guarded(async () => {
184
219
  const id = session.resolve(computer_id);
220
+ const retain = retainIntent(retain_output, background);
221
+ let status;
185
222
  let res;
186
223
  try {
187
- res = await session.api
188
- .with(extra.signal)
189
- .json('POST', P.computerAction(id, 'exec'), {
190
- body: P.execBody({ command, timeout_s, desktop, background, cwd, env }),
191
- });
224
+ const api = session.api.with(extra.signal), body = P.execBody({ command, timeout_s, desktop, background, cwd, env, retain_output });
225
+ if (retain !== undefined) {
226
+ const observed = await api.jsonWithStatus('POST', P.computerAction(id, 'exec'), { body });
227
+ res = observed.value;
228
+ status = observed.status;
229
+ }
230
+ else
231
+ res = await api.json('POST', P.computerAction(id, 'exec'), {
232
+ body,
233
+ });
192
234
  }
193
235
  catch (err) {
194
236
  // The one refusal on this route whose next step is a different tool
@@ -216,10 +258,28 @@ export const registerGuest = (server, session) => {
216
258
  if (!Number.isSafeInteger(res.pid) || res.pid <= 0) {
217
259
  return refused(`The command was accepted but the guest reported no pid that is a usable positive safe integer, so there is no safe handle to poll or kill it with. It may still be running inside the computer — check with exec "ps aux".${note}`, body);
218
260
  }
219
- return said(`Started as pid ${res.pid}. Read its output with exec_poll, stop it with exec_kill.${note}`, body);
261
+ const stable = P.isExecutionId(res.execution_id);
262
+ const acceptedBody = stable && body && typeof body === 'object' && !Array.isArray(body)
263
+ ? { ...body, execution_id: res.execution_id }
264
+ : body;
265
+ const identityNote = stable
266
+ ? ' Use get_execution for its last observation and read_execution_output with explicit offsets for independent reads; these never consume the shared PID cursor.'
267
+ : 'execution_id' in res
268
+ ? ' The command was accepted, but its stable identity is unavailable because the returned execution_id was malformed. Do not replay the command; the valid PID remains available for legacy inspection, subject to PID reuse.'
269
+ : '';
270
+ return said(`Started as pid ${res.pid}. Read its output with exec_poll, stop it with exec_kill.${identityNote}${note}`, acceptedBody);
220
271
  }
221
272
  const { body, note } = decodeExec(res, false);
222
- return said(`${execSummary(res)}${note}`, body);
273
+ const retained = retain !== undefined ? synchronousResultId(res, status) : undefined;
274
+ const presented = retained && body && typeof body === 'object' && !Array.isArray(body)
275
+ ? { ...body, result_id: retained }
276
+ : body;
277
+ const retentionNote = retain === undefined
278
+ ? ''
279
+ : retained
280
+ ? ' Retained result confirmed; use get_result or read_result_output for a separate explicit read.'
281
+ : ' No retrievable retained result ID was confirmed. Do not replay the command to recover it.';
282
+ return said(`${execSummary(res)}${note}${retentionNote}`, presented);
223
283
  }));
224
284
  server.registerTool('exec_poll', {
225
285
  title: 'Read a background command',
@@ -322,15 +382,39 @@ export const registerGuest = (server, session) => {
322
382
  }));
323
383
  server.registerTool('window_action', {
324
384
  title: 'Act on a window',
325
- description: 'Focus, raise, minimize, maximize, unmaximize, close, move or resize one window. The reply is the window afterwards, not an acknowledgement — the window manager places the frame and applications snap to their own grid, so a move to 300,200 routinely lands at 305,229. Believe the response, not the request. Prefer focus over raise: raising without focusing gives a window that is visibly in front and silently not receiving keystrokes. A 504 is neither a refusal nor a report that nothing happened unless its structured response explicitly says the request was not dispatched: an absent or ambiguous explanation leaves the action possibly already applied. An uncertain outcome is not permission to repeat it — the next call is list_windows, which says what the desktop is now, not this one again. Least of all for close, which cannot be undone.',
385
+ description: 'Focus, raise, minimize, maximize, unmaximize, close, move or resize one window. The reply is the window afterwards, not an acknowledgement — the window manager places the frame and applications snap to their own grid, so a move to 300,200 routinely lands at 305,229. Believe the response, not the request. On a Wayland desktop a tiled window cannot be moved or resized — float it first — and an action the compositor refuses is a 400. Prefer focus over raise: raising without focusing gives a window that is visibly in front and silently not receiving keystrokes. A 504 is neither a refusal nor a report that nothing happened unless its structured response explicitly says the request was not dispatched: an absent or ambiguous explanation leaves the action possibly already applied. An uncertain outcome is not permission to repeat it — the next call is list_windows, which says what the desktop is now, not this one again. Least of all for close, which cannot be undone.',
326
386
  inputSchema: {
327
387
  ...idArg,
328
388
  window_id: z.string().describe('From list_windows, e.g. "0x2600003".'),
329
389
  action: z.enum(P.WINDOW_ACTIONS),
330
- x: z.number().int().optional().describe('For move.'),
331
- y: z.number().int().optional().describe('For move.'),
332
- width: z.number().int().optional().describe('For resize.'),
333
- height: z.number().int().optional().describe('For resize.'),
390
+ x: z
391
+ .number()
392
+ .int()
393
+ .min(-32768)
394
+ .max(32767)
395
+ .optional()
396
+ .describe('Required for move, with y: -32768 to 32767.'),
397
+ y: z
398
+ .number()
399
+ .int()
400
+ .min(-32768)
401
+ .max(32767)
402
+ .optional()
403
+ .describe('Required for move, with x: -32768 to 32767.'),
404
+ width: z
405
+ .number()
406
+ .int()
407
+ .min(1)
408
+ .max(32767)
409
+ .optional()
410
+ .describe('Required for resize, with height: 1 to 32767.'),
411
+ height: z
412
+ .number()
413
+ .int()
414
+ .min(1)
415
+ .max(32767)
416
+ .optional()
417
+ .describe('Required for resize, with width: 1 to 32767.'),
334
418
  },
335
419
  }, ({ computer_id, window_id, action, x, y, width, height }, extra) => guarded(async () => {
336
420
  const id = session.resolve(computer_id);
@@ -390,7 +474,7 @@ export const registerGuest = (server, session) => {
390
474
  }));
391
475
  server.registerTool('write_file', {
392
476
  title: 'Put a file into the guest',
393
- description: 'Write a file inside the computer. Paths must be absolute — the guest agent inherits whatever working directory it was started in, so a relative path resolves somewhere you did not name.',
477
+ description: 'Write a file inside the computer. Paths must be absolute — the guest agent inherits whatever working directory it was started in, so a relative path resolves somewhere you did not name. By default a file already at the path is replaced. Pass overwrite: false to create the file only if nothing is there: a path that is taken is refused and that attempt writes nothing — retrying does not change that. Incomplete contents are never published at the path, but a failure while publishing or answering can leave the COMPLETE file there: after any error, read the path before retrying or overwriting, since what is there may be your own upload. A refusal saying this computer\u2019s host cannot do a create-only write, and a 503 on one, mean nothing was sent — but neither means the path is free. overwrite: false is for Linux computers; a Windows computer refuses it. A suspended computer is resumed to take the write, which is charged, unless no_wake is set.',
394
478
  inputSchema: {
395
479
  ...idArg,
396
480
  path: absolutePath('path').describe('Absolute path inside the guest, e.g. /home/user/Desktop/notes.txt.'),
@@ -399,8 +483,13 @@ export const registerGuest = (server, session) => {
399
483
  .enum(['utf8', 'base64'])
400
484
  .default('utf8')
401
485
  .describe('base64 for anything that is not text.'),
486
+ overwrite: z
487
+ .boolean()
488
+ .default(true)
489
+ .describe('true (the default) replaces a file already at the path. false creates the file only if nothing is there, and refuses without writing anything when the path is taken.'),
490
+ no_wake: noWakeArg,
402
491
  },
403
- }, ({ computer_id, path, content, encoding }, extra) => guarded(async () => {
492
+ }, ({ computer_id, path, content, encoding, overwrite, no_wake }, extra) => guarded(async () => {
404
493
  const id = session.resolve(computer_id);
405
494
  // Node's base64 decoder is lenient: it drops characters outside the
406
495
  // alphabet and stops early on bad padding, without ever throwing. A
@@ -423,18 +512,68 @@ export const registerGuest = (server, session) => {
423
512
  return refused(`That content has an unpaired surrogate in it — half of a character, usually from a string cut through the middle of an emoji. It is not valid UTF-8, so ${path} would have been written with a replacement character where your text was, and reported as a success. Nothing was written. Send the whole character, cut the text on a character boundary, or send the exact bytes with encoding: "base64".`);
424
513
  }
425
514
  const bytes = new Uint8Array(Buffer.from(content, encoding === 'base64' ? 'base64' : 'utf8'));
426
- const res = await session.api.with(extra.signal).json('PUT', P.computerAction(id, 'files'),
427
- // The path is a query parameter, and the URL builder encodes it. Doing
428
- // that matters more than it looks: `+` decodes to a space and `&`
429
- // ends the parameter, so an unencoded path with punctuation in it
430
- // writes a DIFFERENT file and nothing reports that, because the
431
- // platform never sees what was meant.
432
- { query: { path }, raw: bytes });
515
+ let res;
516
+ try {
517
+ res = await session.api.with(extra.signal).json('PUT', P.computerAction(id, 'files'),
518
+ // The path is a query parameter, and the URL builder encodes it. Doing
519
+ // that matters more than it looks: `+` decodes to a space and `&`
520
+ // ends the parameter, so an unencoded path with punctuation in it
521
+ // writes a DIFFERENT file and nothing reports that, because the
522
+ // platform never sees what was meant.
523
+ //
524
+ // `overwrite` is sent only to ask for create-only (OPL-4994). Absent
525
+ // means replace on every platform version, so the default request
526
+ // is the one this tool always sent.
527
+ {
528
+ query: {
529
+ path,
530
+ ...(overwrite ? {} : { overwrite: 'false' }),
531
+ ...(no_wake ? { no_wake: '1' } : {}),
532
+ },
533
+ raw: bytes,
534
+ });
535
+ }
536
+ catch (err) {
537
+ // A create-only upload's reasonless 409 is CreateOnlyConflictError
538
+ // whether or not no_wake was sent, and claims nothing about the path
539
+ // or the computer's state — the same answer the Python SDK gives.
540
+ if (no_wake && !(err instanceof CreateOnlyConflictError)) {
541
+ const asleep = noWakeRefusal(err, id, `nothing was written to ${path}`);
542
+ if (asleep)
543
+ return asleep;
544
+ }
545
+ // Only THIS attempt is known to have written nothing. A create-only
546
+ // write whose earlier attempt lost its answer may have written the
547
+ // file itself, and the retry then meets its own file here — so the
548
+ // sentence must not tell a model to go elsewhere or overwrite blind.
549
+ if (err instanceof FileExistsError) {
550
+ return withErrorMetadata(refused(`${apiErrorMessage(err)}\n\nSomething is already at ${path}, and this attempt wrote ` +
551
+ 'nothing: the file there is untouched by it. This does not clear by waiting, so do not ' +
552
+ 'send the same call again. If an earlier attempt\u2019s outcome was unknown, the file may ' +
553
+ 'be yours: read it and compare before choosing another path or overwriting. To replace ' +
554
+ 'it on purpose, call write_file again with overwrite: true (or without overwrite).'), err);
555
+ }
556
+ // A create-only 409 with no usable reason — the body lost, empty, not
557
+ // the platform's JSON, or JSON without a string `reason`. The Api
558
+ // raises it as CreateOnlyConflictError so no caller resends it; the
559
+ // words here claim nothing about the path, nor that this attempt
560
+ // wrote nothing \u2014 without the platform's word, the 409 may come from
561
+ // a hop that had already forwarded the write. Not apiErrorMessage:
562
+ // the body's own text could say "already exists" without the
563
+ // platform's `exists` reason.
564
+ if (err instanceof CreateOnlyConflictError) {
565
+ return withErrorMetadata(refused(`${err.message} (HTTP ${err.status})\n\nThis create-only write to ${path} was refused ` +
566
+ 'as a conflict, reason unknown, and whether this attempt wrote anything is ' +
567
+ 'unconfirmed. Do not send the same call again: it was not said to clear by waiting. ' +
568
+ 'Read the path to see what is there before choosing another path or overwriting.'), err);
569
+ }
570
+ throw err;
571
+ }
433
572
  return said(`Wrote ${res.bytes ?? bytes.length} bytes to ${res.path ?? path}.`, res);
434
573
  }));
435
574
  server.registerTool('read_file', {
436
575
  title: 'Get a file out of the guest',
437
- description: 'Read a file from inside the computer. Text comes back as text and images come back as images; anything else comes back base64. A large file comes back a window at a time rather than filling the conversation: `offset` says where the window starts, and the note under a truncated read gives the exact offset to pass next. There is no size a file can be that makes it unreadable this way — a 2 GB log is pages, not a refusal — but a file you want whole is still better pushed out of the guest than carried through a conversation, and the note says how. This reaches the guest agent to do it, so a suspended computer is resumed to answer and that resume is charged — it can even come back 402. read_clipboard, by contrast, refuses a suspended computer rather than starting it.',
576
+ description: 'Read a file from inside the computer. Text comes back as text and images come back as images; anything else comes back base64. A large file comes back a window at a time rather than filling the conversation: `offset` says where the window starts, and the note under a truncated read gives the exact offset to pass next. There is no size a file can be that makes it unreadable this way — a 2 GB log is pages, not a refusal — but a file you want whole is still better pushed out of the guest than carried through a conversation, and the note says how. This reaches the guest agent to do it, so a suspended computer is resumed to answer and that resume is charged — it can even come back 402 — unless no_wake is set, which refuses instead. read_clipboard, by contrast, refuses a suspended computer rather than starting it.',
438
577
  inputSchema: {
439
578
  ...idArg,
440
579
  path: absolutePath('path'),
@@ -445,6 +584,7 @@ export const registerGuest = (server, session) => {
445
584
  .max(Number.MAX_SAFE_INTEGER)
446
585
  .default(0)
447
586
  .describe('Byte offset to start at. 0 is the beginning of the file. Each call returns a window from here, and the truncation note names the offset of the byte after the last one it returned — pass that to read on. Never assume a window covers what you asked for: read the offset out of the note rather than adding a fixed number.'),
587
+ no_wake: noWakeArg,
448
588
  },
449
589
  // Not readOnlyHint, for the reason cursor_position is not: the platform
450
590
  // marks `GET computers/:id/files` as spending, because reaching the guest
@@ -457,12 +597,12 @@ export const registerGuest = (server, session) => {
457
597
  // reading it twice — a host that gates retry-of-a-timed-out-call on
458
598
  // idempotentHint would refuse to retry a plain file read.
459
599
  annotations: { destructiveHint: false, idempotentHint: true },
460
- }, ({ computer_id, path, offset }, extra) => guarded(async () => {
600
+ }, ({ computer_id, path, offset, no_wake }, extra) => guarded(async () => {
461
601
  const id = session.resolve(computer_id);
462
602
  let file;
463
603
  try {
464
604
  file = await session.api.with(extra.signal).bytes('GET', P.computerAction(id, 'files'), {
465
- query: { path },
605
+ query: { path, ...(no_wake ? { no_wake: '1' } : {}) },
466
606
  // The window this tool asks the platform for, which is not the
467
607
  // same as the window it will return. It is the larger of the two
468
608
  // caps below — the most this tool could ever hand back — because
@@ -482,7 +622,12 @@ export const registerGuest = (server, session) => {
482
622
  }
483
623
  catch (err) {
484
624
  if (err instanceof RangeNotSatisfiableError)
485
- return pastEnd(path, offset, err.size);
625
+ return withErrorMetadata(pastEnd(path, offset, err.size), err);
626
+ if (no_wake) {
627
+ const asleep = noWakeRefusal(err, id, `nothing was read from ${path}`);
628
+ if (asleep)
629
+ return asleep;
630
+ }
486
631
  throw err;
487
632
  }
488
633
  const served = file.window;
@@ -903,6 +1048,10 @@ function decodeExec(res, continued) {
903
1048
  const body = {};
904
1049
  const notes = [];
905
1050
  for (const [key, value] of Object.entries(source)) {
1051
+ // Only the accepted launch can bind stable identity to this command.
1052
+ // A reusable PID poll (or kill) cannot reconstruct that association.
1053
+ if (key === 'execution_id' || key === 'result_id')
1054
+ continue;
906
1055
  if (claimed.has(key))
907
1056
  continue;
908
1057
  const field = EXEC_STREAMS[key];