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.
- package/README.md +558 -29
- package/dist/api.d.ts +59 -1
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +404 -33
- package/dist/api.js.map +1 -1
- package/dist/artifacts.d.ts +63 -0
- package/dist/artifacts.d.ts.map +1 -0
- package/dist/artifacts.js +81 -0
- package/dist/artifacts.js.map +1 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +52 -21
- package/dist/cli.js.map +1 -1
- package/dist/credentials.d.ts +33 -0
- package/dist/credentials.d.ts.map +1 -0
- package/dist/credentials.js +395 -0
- package/dist/credentials.js.map +1 -0
- package/dist/errors.d.ts +122 -11
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +237 -41
- package/dist/errors.js.map +1 -1
- package/dist/executions.d.ts +43 -0
- package/dist/executions.d.ts.map +1 -0
- package/dist/executions.js +162 -0
- package/dist/executions.js.map +1 -0
- package/dist/format.d.ts +33 -8
- package/dist/format.d.ts.map +1 -1
- package/dist/format.js +107 -12
- package/dist/format.js.map +1 -1
- package/dist/http.d.ts +42 -0
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +383 -3
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/paths.d.ts +50 -2
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +65 -5
- package/dist/paths.js.map +1 -1
- package/dist/results.d.ts +97 -0
- package/dist/results.d.ts.map +1 -0
- package/dist/results.js +263 -0
- package/dist/results.js.map +1 -0
- package/dist/secret-errors.d.ts +59 -0
- package/dist/secret-errors.d.ts.map +1 -0
- package/dist/secret-errors.js +199 -0
- package/dist/secret-errors.js.map +1 -0
- package/dist/secret-store.d.ts +115 -0
- package/dist/secret-store.d.ts.map +1 -0
- package/dist/secret-store.js +105 -0
- package/dist/secret-store.js.map +1 -0
- package/dist/server.d.ts +3 -2
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +60 -9
- package/dist/server.js.map +1 -1
- package/dist/session.d.ts +6 -1
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +1 -1
- package/dist/session.js.map +1 -1
- package/dist/stdio.d.ts +5 -1
- package/dist/stdio.d.ts.map +1 -1
- package/dist/stdio.js +10 -4
- package/dist/stdio.js.map +1 -1
- package/dist/tool-filters.d.ts +38 -0
- package/dist/tool-filters.d.ts.map +1 -0
- package/dist/tool-filters.js +137 -0
- package/dist/tool-filters.js.map +1 -0
- package/dist/tools/account.d.ts +3 -0
- package/dist/tools/account.d.ts.map +1 -0
- package/dist/tools/account.js +122 -0
- package/dist/tools/account.js.map +1 -0
- package/dist/tools/activities.d.ts +3 -0
- package/dist/tools/activities.d.ts.map +1 -0
- package/dist/tools/activities.js +140 -0
- package/dist/tools/activities.js.map +1 -0
- package/dist/tools/agent.d.ts.map +1 -1
- package/dist/tools/agent.js +21 -9
- package/dist/tools/agent.js.map +1 -1
- package/dist/tools/artifacts.d.ts +3 -0
- package/dist/tools/artifacts.d.ts.map +1 -0
- package/dist/tools/artifacts.js +97 -0
- package/dist/tools/artifacts.js.map +1 -0
- package/dist/tools/chat.d.ts +6 -0
- package/dist/tools/chat.d.ts.map +1 -0
- package/dist/tools/chat.js +192 -0
- package/dist/tools/chat.js.map +1 -0
- package/dist/tools/computers.d.ts.map +1 -1
- package/dist/tools/computers.js +28 -16
- package/dist/tools/computers.js.map +1 -1
- package/dist/tools/directory.d.ts +13 -0
- package/dist/tools/directory.d.ts.map +1 -0
- package/dist/tools/directory.js +88 -0
- package/dist/tools/directory.js.map +1 -0
- package/dist/tools/executions.d.ts +3 -0
- package/dist/tools/executions.d.ts.map +1 -0
- package/dist/tools/executions.js +87 -0
- package/dist/tools/executions.js.map +1 -0
- package/dist/tools/guest.d.ts.map +1 -1
- package/dist/tools/guest.js +184 -35
- package/dist/tools/guest.js.map +1 -1
- package/dist/tools/input.d.ts +2 -0
- package/dist/tools/input.d.ts.map +1 -1
- package/dist/tools/input.js +31 -5
- package/dist/tools/input.js.map +1 -1
- package/dist/tools/results.d.ts +30 -0
- package/dist/tools/results.d.ts.map +1 -0
- package/dist/tools/results.js +106 -0
- package/dist/tools/results.js.map +1 -0
- package/dist/tools/secrets.d.ts +78 -0
- package/dist/tools/secrets.d.ts.map +1 -0
- package/dist/tools/secrets.js +448 -0
- package/dist/tools/secrets.js.map +1 -0
- package/dist/tools/signals.d.ts +3 -0
- package/dist/tools/signals.d.ts.map +1 -0
- package/dist/tools/signals.js +116 -0
- package/dist/tools/signals.js.map +1 -0
- package/dist/tools/snapshots.d.ts.map +1 -1
- package/dist/tools/snapshots.js +52 -10
- package/dist/tools/snapshots.js.map +1 -1
- package/dist/tools/ssh.d.ts +3 -0
- package/dist/tools/ssh.d.ts.map +1 -0
- package/dist/tools/ssh.js +186 -0
- package/dist/tools/ssh.js.map +1 -0
- package/dist/tools/webhooks.js +1 -1
- package/dist/tools/webhooks.js.map +1 -1
- 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 @@
|
|
|
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":"
|
|
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"}
|
package/dist/tools/guest.js
CHANGED
|
@@ -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
|
|
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
|
|
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')
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
.
|
|
190
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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];
|