@celestea/tools 2.7.1
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/LICENSE +21 -0
- package/dist/args.d.ts +15 -0
- package/dist/args.js +55 -0
- package/dist/attachments/image-header.d.ts +28 -0
- package/dist/attachments/image-header.js +148 -0
- package/dist/attachments/store.d.ts +56 -0
- package/dist/attachments/store.js +148 -0
- package/dist/browser/cdp.d.ts +162 -0
- package/dist/browser/cdp.js +269 -0
- package/dist/browser/launch.d.ts +92 -0
- package/dist/browser/launch.js +232 -0
- package/dist/browser/memory-guard.d.ts +55 -0
- package/dist/browser/memory-guard.js +210 -0
- package/dist/browser/session.d.ts +146 -0
- package/dist/browser/session.js +371 -0
- package/dist/browser/snapshot.d.ts +69 -0
- package/dist/browser/snapshot.js +163 -0
- package/dist/browser/types.d.ts +51 -0
- package/dist/browser/types.js +8 -0
- package/dist/builtin.d.ts +54 -0
- package/dist/builtin.js +77 -0
- package/dist/desc.d.ts +20 -0
- package/dist/desc.js +22 -0
- package/dist/disclosure.d.ts +113 -0
- package/dist/disclosure.js +141 -0
- package/dist/env.d.ts +10 -0
- package/dist/env.js +33 -0
- package/dist/errors.d.ts +25 -0
- package/dist/errors.js +60 -0
- package/dist/exposure.d.ts +145 -0
- package/dist/exposure.js +244 -0
- package/dist/fn-tool.d.ts +14 -0
- package/dist/fn-tool.js +14 -0
- package/dist/fs/file-io.d.ts +78 -0
- package/dist/fs/file-io.js +239 -0
- package/dist/guard/path-guard.d.ts +144 -0
- package/dist/guard/path-guard.js +289 -0
- package/dist/guard/paths.d.ts +35 -0
- package/dist/guard/paths.js +100 -0
- package/dist/http/errors.d.ts +24 -0
- package/dist/http/errors.js +64 -0
- package/dist/http/headers.d.ts +19 -0
- package/dist/http/headers.js +62 -0
- package/dist/http/redirects.d.ts +31 -0
- package/dist/http/redirects.js +76 -0
- package/dist/http/ssrf.d.ts +105 -0
- package/dist/http/ssrf.js +272 -0
- package/dist/http/transport.d.ts +50 -0
- package/dist/http/transport.js +130 -0
- package/dist/index.d.ts +114 -0
- package/dist/index.js +129 -0
- package/dist/memory/log.d.ts +75 -0
- package/dist/memory/log.js +157 -0
- package/dist/memory/store.d.ts +47 -0
- package/dist/memory/store.js +61 -0
- package/dist/platform/exec.d.ts +79 -0
- package/dist/platform/exec.js +218 -0
- package/dist/platform/index.d.ts +12 -0
- package/dist/platform/index.js +12 -0
- package/dist/platform/paths.d.ts +51 -0
- package/dist/platform/paths.js +60 -0
- package/dist/platform/quote.d.ts +72 -0
- package/dist/platform/quote.js +102 -0
- package/dist/plugin.d.ts +96 -0
- package/dist/plugin.js +101 -0
- package/dist/process/buffers.d.ts +33 -0
- package/dist/process/buffers.js +86 -0
- package/dist/process/registry.d.ts +98 -0
- package/dist/process/registry.js +282 -0
- package/dist/registry.d.ts +52 -0
- package/dist/registry.js +161 -0
- package/dist/run-code/broker.d.ts +68 -0
- package/dist/run-code/broker.js +465 -0
- package/dist/run-code/limits.d.ts +69 -0
- package/dist/run-code/limits.js +88 -0
- package/dist/run-code/lines.d.ts +69 -0
- package/dist/run-code/lines.js +199 -0
- package/dist/run-code/sdk-ts.d.ts +34 -0
- package/dist/run-code/sdk-ts.js +276 -0
- package/dist/run-code/sdk.d.ts +39 -0
- package/dist/run-code/sdk.js +294 -0
- package/dist/sandbox/async.d.ts +10 -0
- package/dist/sandbox/async.js +26 -0
- package/dist/sandbox/bwrap-argv.d.ts +62 -0
- package/dist/sandbox/bwrap-argv.js +113 -0
- package/dist/sandbox/bwrap.d.ts +94 -0
- package/dist/sandbox/bwrap.js +159 -0
- package/dist/sandbox/child.d.ts +38 -0
- package/dist/sandbox/child.js +98 -0
- package/dist/sandbox/config.d.ts +89 -0
- package/dist/sandbox/config.js +149 -0
- package/dist/sandbox/fake-sandbox.d.ts +57 -0
- package/dist/sandbox/fake-sandbox.js +110 -0
- package/dist/sandbox/launch.d.ts +51 -0
- package/dist/sandbox/launch.js +134 -0
- package/dist/sandbox/limits.d.ts +63 -0
- package/dist/sandbox/limits.js +113 -0
- package/dist/sandbox/probe.d.ts +46 -0
- package/dist/sandbox/probe.js +102 -0
- package/dist/sandbox/provider.d.ts +83 -0
- package/dist/sandbox/provider.js +126 -0
- package/dist/sandbox/rlimit.d.ts +60 -0
- package/dist/sandbox/rlimit.js +76 -0
- package/dist/sandbox/seccomp.d.ts +48 -0
- package/dist/sandbox/seccomp.js +115 -0
- package/dist/sandbox/userspace.d.ts +65 -0
- package/dist/sandbox/userspace.js +107 -0
- package/dist/sandbox/workdir.d.ts +13 -0
- package/dist/sandbox/workdir.js +44 -0
- package/dist/schema.d.ts +20 -0
- package/dist/schema.js +135 -0
- package/dist/testing/platform-gates.d.ts +54 -0
- package/dist/testing/platform-gates.js +62 -0
- package/dist/tool-failure.d.ts +13 -0
- package/dist/tool-failure.js +19 -0
- package/dist/tools/ask-user.d.ts +32 -0
- package/dist/tools/ask-user.js +145 -0
- package/dist/tools/browser.d.ts +24 -0
- package/dist/tools/browser.js +132 -0
- package/dist/tools/http-request.d.ts +31 -0
- package/dist/tools/http-request.js +117 -0
- package/dist/tools/list-dir.d.ts +9 -0
- package/dist/tools/list-dir.js +45 -0
- package/dist/tools/load-skill.d.ts +37 -0
- package/dist/tools/load-skill.js +76 -0
- package/dist/tools/memory.d.ts +46 -0
- package/dist/tools/memory.js +131 -0
- package/dist/tools/process-control.d.ts +14 -0
- package/dist/tools/process-control.js +49 -0
- package/dist/tools/read-file.d.ts +11 -0
- package/dist/tools/read-file.js +81 -0
- package/dist/tools/read-image.d.ts +33 -0
- package/dist/tools/read-image.js +144 -0
- package/dist/tools/run-code.d.ts +48 -0
- package/dist/tools/run-code.js +115 -0
- package/dist/tools/run-shell.d.ts +22 -0
- package/dist/tools/run-shell.js +81 -0
- package/dist/tools/write-file.d.ts +8 -0
- package/dist/tools/write-file.js +31 -0
- package/package.json +28 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `process_control` — poll / stdin / kill a background process
|
|
3
|
+
* (`crates/tools/src/process.rs`).
|
|
4
|
+
*
|
|
5
|
+
* Handles live in the session-scoped registry and survive across turns; a
|
|
6
|
+
* process that exits stays pollable from a bounded tombstone (most recent 32, or
|
|
7
|
+
* 10 minutes), then the handle is unknown. Failures are results
|
|
8
|
+
* (`{ok:false, error}`), not rejections: "unknown handle" is a normal answer to
|
|
9
|
+
* a stale handle, and callers branch on `ok`.
|
|
10
|
+
*/
|
|
11
|
+
import type { ProcessRegistry } from "../process/registry.js";
|
|
12
|
+
import type { Tool, ToolSpec } from "@celestea/core";
|
|
13
|
+
export declare function processControlSpec(): ToolSpec;
|
|
14
|
+
export declare function processControlTool(processes: ProcessRegistry): Tool;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `process_control` — poll / stdin / kill a background process
|
|
3
|
+
* (`crates/tools/src/process.rs`).
|
|
4
|
+
*
|
|
5
|
+
* Handles live in the session-scoped registry and survive across turns; a
|
|
6
|
+
* process that exits stays pollable from a bounded tombstone (most recent 32, or
|
|
7
|
+
* 10 minutes), then the handle is unknown. Failures are results
|
|
8
|
+
* (`{ok:false, error}`), not rejections: "unknown handle" is a normal answer to
|
|
9
|
+
* a stale handle, and callers branch on `ok`.
|
|
10
|
+
*/
|
|
11
|
+
import { optionalStringArg, stringArg } from "../args.js";
|
|
12
|
+
import { descParam } from "../desc.js";
|
|
13
|
+
import { fnTool } from "../fn-tool.js";
|
|
14
|
+
export function processControlSpec() {
|
|
15
|
+
return {
|
|
16
|
+
name: "process_control",
|
|
17
|
+
description: "Control a background process started by run_shell(background=true). Handles live in the session-scoped process registry and survive across turns; a process that exits stays pollable from a bounded tombstone (most recent 32, or 10 minutes), so poll AFTER exit returns {running:false, exit_code, signal, stdout_tail(<=4KB), stderr_tail, cpu_exceeded?} instead of an unknown handle. action=poll reads the current or terminal state; action=kill sends SIGTERM, waits a grace period, then SIGKILL and returns {killed:true}; action=stdin writes one line (content + newline) to the process stdin.",
|
|
18
|
+
parameters: {
|
|
19
|
+
type: "object",
|
|
20
|
+
properties: {
|
|
21
|
+
handle: { type: "string", description: "Process handle returned by run_shell(background=true)." },
|
|
22
|
+
action: { type: "string", enum: ["poll", "kill", "stdin"], description: "poll | kill | stdin (see tool description)." },
|
|
23
|
+
content: { type: "string", description: "Line to write to the process stdin (action=stdin only)." },
|
|
24
|
+
desc: descParam(),
|
|
25
|
+
},
|
|
26
|
+
required: ["handle", "action"],
|
|
27
|
+
additionalProperties: false,
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export function processControlTool(processes) {
|
|
32
|
+
return fnTool(processControlSpec(), async (args) => {
|
|
33
|
+
const handle = stringArg(args, "handle").trim();
|
|
34
|
+
if (handle === "")
|
|
35
|
+
return { ok: false, error: "handle required" };
|
|
36
|
+
const action = optionalStringArg(args, "action") ?? "";
|
|
37
|
+
if (action === "poll")
|
|
38
|
+
return processes.poll(handle);
|
|
39
|
+
if (action === "kill")
|
|
40
|
+
return processes.kill(handle);
|
|
41
|
+
if (action === "stdin") {
|
|
42
|
+
const content = optionalStringArg(args, "content");
|
|
43
|
+
if (content === undefined)
|
|
44
|
+
return { ok: false, error: "content required for action=stdin" };
|
|
45
|
+
return processes.stdinLine(handle, content);
|
|
46
|
+
}
|
|
47
|
+
return { ok: false, error: `unknown action: ${action}` };
|
|
48
|
+
});
|
|
49
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `read_file` — read a UTF-8 text file and return its contents as a string
|
|
3
|
+
* (legacy `builtin.rs::read_file_spec`). A default (no `offset`/`limit`) read
|
|
4
|
+
* keeps the canonical value shape: the value stays the text, the truncation
|
|
5
|
+
* travels in the authored `render`. Passing `offset`/`limit` (W846) returns a
|
|
6
|
+
* line window whose pagination metadata lives on `value` — `render` is not
|
|
7
|
+
* projected to the model, so metadata there would be invisible.
|
|
8
|
+
*/
|
|
9
|
+
import type { Tool, ToolSpec } from "@celestea/core";
|
|
10
|
+
export declare function readFileSpec(): ToolSpec;
|
|
11
|
+
export declare function readFileTool(): Tool;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `read_file` — read a UTF-8 text file and return its contents as a string
|
|
3
|
+
* (legacy `builtin.rs::read_file_spec`). A default (no `offset`/`limit`) read
|
|
4
|
+
* keeps the canonical value shape: the value stays the text, the truncation
|
|
5
|
+
* travels in the authored `render`. Passing `offset`/`limit` (W846) returns a
|
|
6
|
+
* line window whose pagination metadata lives on `value` — `render` is not
|
|
7
|
+
* projected to the model, so metadata there would be invisible.
|
|
8
|
+
*/
|
|
9
|
+
import { optionalIntArg, stringArg } from "../args.js";
|
|
10
|
+
import { descParam } from "../desc.js";
|
|
11
|
+
import { DEFAULT_READ_LIMIT, MAX_READ_BYTES, readTextFile, readTextLines, truncationNote } from "../fs/file-io.js";
|
|
12
|
+
export function readFileSpec() {
|
|
13
|
+
return {
|
|
14
|
+
name: "read_file",
|
|
15
|
+
description: "Read a UTF-8 text file and return its contents as a string. A whole read is capped at 256 KiB; pass offset (0-based line) and/or limit (line count) to page through a larger file — a paged result is an object {text, offset, limit, lineCount, totalLines, hasMore, nextOffset, truncated, totalBytes}.",
|
|
16
|
+
parameters: {
|
|
17
|
+
type: "object",
|
|
18
|
+
properties: {
|
|
19
|
+
path: { type: "string", description: "Filesystem path of the file to read." },
|
|
20
|
+
offset: {
|
|
21
|
+
type: "integer",
|
|
22
|
+
minimum: 0,
|
|
23
|
+
description: "Optional 0-based line index to start from (pagination). Omit to read from the top.",
|
|
24
|
+
},
|
|
25
|
+
limit: {
|
|
26
|
+
type: "integer",
|
|
27
|
+
minimum: 1,
|
|
28
|
+
description: "Optional maximum number of lines to return (pagination). Omit to use the default window.",
|
|
29
|
+
},
|
|
30
|
+
desc: descParam(),
|
|
31
|
+
},
|
|
32
|
+
required: ["path"],
|
|
33
|
+
additionalProperties: false,
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
async function read(args) {
|
|
38
|
+
const path = stringArg(args, "path");
|
|
39
|
+
const offset = optionalIntArg(args, "offset");
|
|
40
|
+
const limit = optionalIntArg(args, "limit");
|
|
41
|
+
// No pagination params: the legacy byte read, value byte-for-byte unchanged.
|
|
42
|
+
if (offset === undefined && limit === undefined) {
|
|
43
|
+
const result = await readTextFile(path);
|
|
44
|
+
if (!result.truncated)
|
|
45
|
+
return { value: result.text, render: null };
|
|
46
|
+
const note = truncationNote(`'${path}'`, MAX_READ_BYTES, result.totalBytes, "bytes", 'read the rest with offset/limit, or run_shell on the same path (head -c / tail -c / sed -n)');
|
|
47
|
+
// W855 (B6): the note must reach the MODEL too — `render` is display-only
|
|
48
|
+
// (never projected), so it rides as a surface descriptor on the log row.
|
|
49
|
+
return { value: result.text, render: note, surface: { kind: "truncation", note } };
|
|
50
|
+
}
|
|
51
|
+
// W846 pagination: line window; metadata on `value` (render is not projected).
|
|
52
|
+
const window = await readTextLines(path, offset ?? 0, limit ?? DEFAULT_READ_LIMIT);
|
|
53
|
+
const lastLine = window.offset + window.lineCount - 1;
|
|
54
|
+
const more = window.hasMore
|
|
55
|
+
? `; ${window.totalLines - window.offset - window.lineCount} more at offset=${window.nextOffset}`
|
|
56
|
+
: "";
|
|
57
|
+
return {
|
|
58
|
+
value: {
|
|
59
|
+
text: window.text,
|
|
60
|
+
offset: window.offset,
|
|
61
|
+
limit: window.limit,
|
|
62
|
+
lineCount: window.lineCount,
|
|
63
|
+
totalLines: window.totalLines,
|
|
64
|
+
hasMore: window.hasMore,
|
|
65
|
+
nextOffset: window.nextOffset,
|
|
66
|
+
truncated: window.truncated,
|
|
67
|
+
totalBytes: window.totalBytes,
|
|
68
|
+
},
|
|
69
|
+
render: window.hasMore || window.truncated
|
|
70
|
+
? `read_file: lines ${window.offset}-${lastLine} of ${window.totalLines}${more}${window.truncated ? " (window clipped at 256 KiB)" : ""}`
|
|
71
|
+
: null,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
export function readFileTool() {
|
|
75
|
+
const spec = readFileSpec();
|
|
76
|
+
return {
|
|
77
|
+
spec: () => spec,
|
|
78
|
+
execute: async (args) => (await read(args)).value,
|
|
79
|
+
executeWith: async (input) => read(input.args),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* read_image (W804, multimodal P0 section 6) — read an image from the session's
|
|
3
|
+
* attachments or from the sandbox roots and hand it to a vision-capable model.
|
|
4
|
+
*
|
|
5
|
+
* Two channels (section 6.3):
|
|
6
|
+
* 1. the tool VALUE (persisted, logged): metadata plus value.attachments — a
|
|
7
|
+
* list of [ImageRef]s. It NEVER contains bytes.
|
|
8
|
+
* 2. the model-visible image block: deriveMessagesFrom reads value.attachments
|
|
9
|
+
* and projects an ImageContent onto the same tool message; the wire layer
|
|
10
|
+
* then delivers it in a following user message (shape B, section 3.3).
|
|
11
|
+
*
|
|
12
|
+
* The capability gate (section 6.6) is OPTIMISTIC: the host passes
|
|
13
|
+
* imageInputAllowed = false only when the target model was EXPLICITLY configured
|
|
14
|
+
* with input_modalities excluding "image". An optimistic default that the
|
|
15
|
+
* upstream rejects is handled by the section 7.6 downgrade, not here.
|
|
16
|
+
*/
|
|
17
|
+
import type { Tool, ToolSpec } from "@celestea/core";
|
|
18
|
+
import { type AttachmentStore } from "../attachments/store.js";
|
|
19
|
+
/** The frozen contract description; mirrored by contracts/tools.json. */
|
|
20
|
+
export declare const READ_IMAGE_DESCRIPTION = "Read an image (PNG/JPEG/WebP/GIF) and attach it to the conversation so a vision-capable model can see it. Returns metadata; the image content block is delivered with the tool result. Fails on models without image input.";
|
|
21
|
+
export interface ReadImageToolOptions {
|
|
22
|
+
/** The session's content-addressed store; the tool cannot exist without it. */
|
|
23
|
+
attachments: AttachmentStore;
|
|
24
|
+
/**
|
|
25
|
+
* false = the target model's input_modalities was EXPLICITLY configured without
|
|
26
|
+
* "image"; the tool then refuses before touching any file (section 6.6).
|
|
27
|
+
*/
|
|
28
|
+
imageInputAllowed?: boolean;
|
|
29
|
+
/** The model id, for the refusal text (the user must be able to switch). */
|
|
30
|
+
model?: string;
|
|
31
|
+
}
|
|
32
|
+
export declare function readImageSpec(): ToolSpec;
|
|
33
|
+
export declare function readImageTool(options: ReadImageToolOptions): Tool;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* read_image (W804, multimodal P0 section 6) — read an image from the session's
|
|
3
|
+
* attachments or from the sandbox roots and hand it to a vision-capable model.
|
|
4
|
+
*
|
|
5
|
+
* Two channels (section 6.3):
|
|
6
|
+
* 1. the tool VALUE (persisted, logged): metadata plus value.attachments — a
|
|
7
|
+
* list of [ImageRef]s. It NEVER contains bytes.
|
|
8
|
+
* 2. the model-visible image block: deriveMessagesFrom reads value.attachments
|
|
9
|
+
* and projects an ImageContent onto the same tool message; the wire layer
|
|
10
|
+
* then delivers it in a following user message (shape B, section 3.3).
|
|
11
|
+
*
|
|
12
|
+
* The capability gate (section 6.6) is OPTIMISTIC: the host passes
|
|
13
|
+
* imageInputAllowed = false only when the target model was EXPLICITLY configured
|
|
14
|
+
* with input_modalities excluding "image". An optimistic default that the
|
|
15
|
+
* upstream rejects is handled by the section 7.6 downgrade, not here.
|
|
16
|
+
*/
|
|
17
|
+
import { open } from "node:fs/promises";
|
|
18
|
+
import { basename } from "node:path";
|
|
19
|
+
import { optionalStringArg } from "../args.js";
|
|
20
|
+
import { descParam } from "../desc.js";
|
|
21
|
+
import { contractFailure, isToolFailure } from "../errors.js";
|
|
22
|
+
import { ATTACHMENT_MAX_BYTES, AttachmentError } from "../attachments/store.js";
|
|
23
|
+
/** The frozen contract description; mirrored by contracts/tools.json. */
|
|
24
|
+
export const READ_IMAGE_DESCRIPTION = "Read an image (PNG/JPEG/WebP/GIF) and attach it to the conversation so a vision-capable model can see it. Returns metadata; the image content block is delivered with the tool result. Fails on models without image input.";
|
|
25
|
+
export function readImageSpec() {
|
|
26
|
+
return {
|
|
27
|
+
name: "read_image",
|
|
28
|
+
description: READ_IMAGE_DESCRIPTION,
|
|
29
|
+
parameters: {
|
|
30
|
+
type: "object",
|
|
31
|
+
properties: {
|
|
32
|
+
path: {
|
|
33
|
+
type: "string",
|
|
34
|
+
description: "Filesystem path of the image to read (PNG/JPEG/WebP/GIF; format detected by content, not extension).",
|
|
35
|
+
},
|
|
36
|
+
attachment_id: {
|
|
37
|
+
type: "string",
|
|
38
|
+
description: "Content-addressed id of an already-uploaded attachment (from a user message). Use instead of path.",
|
|
39
|
+
},
|
|
40
|
+
desc: descParam(),
|
|
41
|
+
},
|
|
42
|
+
required: [],
|
|
43
|
+
additionalProperties: false,
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
async function run(args, options) {
|
|
48
|
+
if (options.imageInputAllowed === false) {
|
|
49
|
+
const model = options.model !== undefined && options.model !== "" ? options.model : "unknown";
|
|
50
|
+
throw contractFailure("read_image", "unsupported_modality", `当前模型 "${model}" 的 input_modalities 未包含 image(按配置显式排除),read_image 未执行。\n请改用文本工具,或在该模型的 provider 设置里打开 input_modalities(加入 "image")。`);
|
|
51
|
+
}
|
|
52
|
+
const path = optionalStringArg(args, "path");
|
|
53
|
+
const attachmentId = optionalStringArg(args, "attachment_id");
|
|
54
|
+
if ((path === undefined) === (attachmentId === undefined)) {
|
|
55
|
+
throw contractFailure("read_image", "invalid_arg", "exactly one of 'path' or 'attachment_id' is required");
|
|
56
|
+
}
|
|
57
|
+
let bytes;
|
|
58
|
+
let name;
|
|
59
|
+
let sourcePath = null;
|
|
60
|
+
if (path !== undefined) {
|
|
61
|
+
bytes = await readBoundedImage(path);
|
|
62
|
+
name = basename(path);
|
|
63
|
+
sourcePath = path;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
const found = await options.attachments.readById(attachmentId);
|
|
67
|
+
if (found === null) {
|
|
68
|
+
throw contractFailure("read_image", "not_found", `attachment '${attachmentId}' was not found in this session`);
|
|
69
|
+
}
|
|
70
|
+
bytes = found.bytes;
|
|
71
|
+
name = found.ref.name;
|
|
72
|
+
}
|
|
73
|
+
let ref;
|
|
74
|
+
try {
|
|
75
|
+
ref = await options.attachments.put({ bytes, ...(name === undefined || name === "" ? {} : { name }) });
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
if (e instanceof AttachmentError)
|
|
79
|
+
throw contractFailure("read_image", e.code, e.message);
|
|
80
|
+
throw e;
|
|
81
|
+
}
|
|
82
|
+
const value = {
|
|
83
|
+
ok: true,
|
|
84
|
+
...(sourcePath === null ? {} : { path: sourcePath }),
|
|
85
|
+
media_type: ref.media_type,
|
|
86
|
+
bytes: bytes.length,
|
|
87
|
+
width: ref.width,
|
|
88
|
+
height: ref.height,
|
|
89
|
+
sha256: ref.attachment_id,
|
|
90
|
+
attachment_id: ref.attachment_id,
|
|
91
|
+
attachments: [ref],
|
|
92
|
+
};
|
|
93
|
+
const render = `read image ${ref.media_type} ${ref.width}x${ref.height} (${bytes.length} bytes)`;
|
|
94
|
+
return { value, render };
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* B2 / W812 P1-3 (R3): read the image with a HARD byte ceiling. The attachment
|
|
98
|
+
* store already rejects >4 MiB, but only AFTER the whole file was buffered, so a
|
|
99
|
+
* huge path would spike RSS (or OOM) before that check ran. Open + stat + read
|
|
100
|
+
* at most the cap, so the oversize case never allocates the file at all.
|
|
101
|
+
*/
|
|
102
|
+
async function readBoundedImage(path) {
|
|
103
|
+
let handle;
|
|
104
|
+
try {
|
|
105
|
+
handle = await open(path, "r");
|
|
106
|
+
}
|
|
107
|
+
catch (e) {
|
|
108
|
+
throw contractFailure("read_image", "io", `cannot read '${path}': ${e instanceof Error ? e.message : String(e)}`);
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
const info = await handle.stat();
|
|
112
|
+
if (info.isDirectory())
|
|
113
|
+
throw contractFailure("read_image", "io", `'${path}' is a directory, not a file`);
|
|
114
|
+
if (info.size > ATTACHMENT_MAX_BYTES) {
|
|
115
|
+
throw contractFailure("read_image", "too_large", `image is ${info.size} bytes, over the ${ATTACHMENT_MAX_BYTES}-byte limit`);
|
|
116
|
+
}
|
|
117
|
+
const size = Number(info.size);
|
|
118
|
+
const buffer = Buffer.allocUnsafe(size);
|
|
119
|
+
let offset = 0;
|
|
120
|
+
while (offset < size) {
|
|
121
|
+
const { bytesRead } = await handle.read(buffer, offset, size - offset, offset);
|
|
122
|
+
if (bytesRead === 0)
|
|
123
|
+
break;
|
|
124
|
+
offset += bytesRead;
|
|
125
|
+
}
|
|
126
|
+
return buffer.subarray(0, offset);
|
|
127
|
+
}
|
|
128
|
+
catch (e) {
|
|
129
|
+
if (isToolFailure(e))
|
|
130
|
+
throw e;
|
|
131
|
+
throw contractFailure("read_image", "io", `cannot read '${path}': ${e instanceof Error ? e.message : String(e)}`);
|
|
132
|
+
}
|
|
133
|
+
finally {
|
|
134
|
+
await handle.close().catch(() => undefined);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
export function readImageTool(options) {
|
|
138
|
+
const spec = readImageSpec();
|
|
139
|
+
return {
|
|
140
|
+
spec: () => spec,
|
|
141
|
+
execute: async (args) => (await run(args, options)).value,
|
|
142
|
+
executeWith: async (input) => run(input.args, options),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `run_code` — programmatic tool execution with a parent broker (W255;
|
|
3
|
+
* TypeScript since W774, `crates/tools/src/run_code.rs`).
|
|
4
|
+
*
|
|
5
|
+
* One call = one round trip: the program runs under the *same* sandbox as
|
|
6
|
+
* `run_shell`, its `tools.<name>(...)` bridge calls travel to this process as
|
|
7
|
+
* one-line JSON on stdout, and each one is dispatched through the identical
|
|
8
|
+
* `ToolRegistry` pipeline (schema → guards → execute) before the reply goes
|
|
9
|
+
* back on stdin. The model sees only `main()`'s return value; the intermediate
|
|
10
|
+
* rows stay in the session log as nested `ToolCall`/`ToolResult` events
|
|
11
|
+
* (`id = "<parent>:c<n>"`, `parent_id = <parent>`), which `deriveMessages`
|
|
12
|
+
* skips.
|
|
13
|
+
*
|
|
14
|
+
* Wiring is late-bound on purpose ([`RegistryHandle`]): the tool must be
|
|
15
|
+
* registered *into* the registry it will dispatch through, so the handle is
|
|
16
|
+
* bound right after registration (the `RegistryHandle` / `run_code_tool_with_handle`).
|
|
17
|
+
*/
|
|
18
|
+
import type { Sandbox, Tool, ToolRegistry, ToolSpec } from "@celestea/core";
|
|
19
|
+
import { type RunCodeEventSink } from "../run-code/broker.js";
|
|
20
|
+
import { type RunCodeConfig } from "../run-code/limits.js";
|
|
21
|
+
/**
|
|
22
|
+
* Late-bound handle to the composed registry: `run_code` is registered before
|
|
23
|
+
* the registry can be handed to it, so the assembly binds this afterwards and
|
|
24
|
+
* dispatch resolves at execution time. The FIRST binding wins (legacy
|
|
25
|
+
* `OnceLock<Weak<dyn ToolRegistry>>`).
|
|
26
|
+
*/
|
|
27
|
+
export declare class RegistryHandle {
|
|
28
|
+
private registry;
|
|
29
|
+
set(registry: ToolRegistry): void;
|
|
30
|
+
resolve(): ToolRegistry | null;
|
|
31
|
+
}
|
|
32
|
+
export interface RunCodeToolOptions {
|
|
33
|
+
/** The execution boundary — the same provider `run_shell` runs under. */
|
|
34
|
+
sandbox: Sandbox;
|
|
35
|
+
/** Late-bound registry: sub-calls ride its guard + schema pipeline. */
|
|
36
|
+
handle: RegistryHandle;
|
|
37
|
+
/** Session-log sink for nested sub-call rows (optional). */
|
|
38
|
+
events?: RunCodeEventSink;
|
|
39
|
+
/** Tuning knobs; default = [`runCodeConfigFromEnv`]. */
|
|
40
|
+
config?: RunCodeConfig;
|
|
41
|
+
}
|
|
42
|
+
/** Build the tool plus the handle the assembly must bind after registration. */
|
|
43
|
+
export declare function runCodeToolWithHandle(options: Omit<RunCodeToolOptions, "handle">): {
|
|
44
|
+
tool: Tool;
|
|
45
|
+
handle: RegistryHandle;
|
|
46
|
+
};
|
|
47
|
+
export declare function runCodeTool(options: RunCodeToolOptions): Tool;
|
|
48
|
+
export declare function runCodeSpec(): ToolSpec;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `run_code` — programmatic tool execution with a parent broker (W255;
|
|
3
|
+
* TypeScript since W774, `crates/tools/src/run_code.rs`).
|
|
4
|
+
*
|
|
5
|
+
* One call = one round trip: the program runs under the *same* sandbox as
|
|
6
|
+
* `run_shell`, its `tools.<name>(...)` bridge calls travel to this process as
|
|
7
|
+
* one-line JSON on stdout, and each one is dispatched through the identical
|
|
8
|
+
* `ToolRegistry` pipeline (schema → guards → execute) before the reply goes
|
|
9
|
+
* back on stdin. The model sees only `main()`'s return value; the intermediate
|
|
10
|
+
* rows stay in the session log as nested `ToolCall`/`ToolResult` events
|
|
11
|
+
* (`id = "<parent>:c<n>"`, `parent_id = <parent>`), which `deriveMessages`
|
|
12
|
+
* skips.
|
|
13
|
+
*
|
|
14
|
+
* Wiring is late-bound on purpose ([`RegistryHandle`]): the tool must be
|
|
15
|
+
* registered *into* the registry it will dispatch through, so the handle is
|
|
16
|
+
* bound right after registration (the `RegistryHandle` / `run_code_tool_with_handle`).
|
|
17
|
+
*/
|
|
18
|
+
import { descParam } from "../desc.js";
|
|
19
|
+
import { ToolFailure } from "../tool-failure.js";
|
|
20
|
+
import { brokerRun } from "../run-code/broker.js";
|
|
21
|
+
import { DEFAULT_TIMEOUT_MS, MAX_LOG_BYTES, MAX_SUB_CALLS, MAX_SUB_OUTPUT_BYTES, MAX_TIMEOUT_MS, RUN_CODE_ERROR_PREFIX, runCodeConfigFromEnv, } from "../run-code/limits.js";
|
|
22
|
+
/**
|
|
23
|
+
* Late-bound handle to the composed registry: `run_code` is registered before
|
|
24
|
+
* the registry can be handed to it, so the assembly binds this afterwards and
|
|
25
|
+
* dispatch resolves at execution time. The FIRST binding wins (legacy
|
|
26
|
+
* `OnceLock<Weak<dyn ToolRegistry>>`).
|
|
27
|
+
*/
|
|
28
|
+
export class RegistryHandle {
|
|
29
|
+
registry = null;
|
|
30
|
+
set(registry) {
|
|
31
|
+
if (this.registry === null)
|
|
32
|
+
this.registry = registry;
|
|
33
|
+
}
|
|
34
|
+
resolve() {
|
|
35
|
+
return this.registry;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** Build the tool plus the handle the assembly must bind after registration. */
|
|
39
|
+
export function runCodeToolWithHandle(options) {
|
|
40
|
+
const handle = new RegistryHandle();
|
|
41
|
+
return { tool: runCodeTool({ ...options, handle }), handle };
|
|
42
|
+
}
|
|
43
|
+
export function runCodeTool(options) {
|
|
44
|
+
const spec = runCodeSpec();
|
|
45
|
+
const config = options.config ?? runCodeConfigFromEnv();
|
|
46
|
+
return {
|
|
47
|
+
spec: () => spec,
|
|
48
|
+
execute: async () => {
|
|
49
|
+
// The broker needs the caller-assigned call id (sub-call ids embed it).
|
|
50
|
+
throw new ToolFailure(RUN_CODE_ERROR_PREFIX, `${RUN_CODE_ERROR_PREFIX}: dispatched without a call id`);
|
|
51
|
+
},
|
|
52
|
+
executeWith: async (input) => {
|
|
53
|
+
const registry = options.handle.resolve();
|
|
54
|
+
if (registry === null) {
|
|
55
|
+
throw new ToolFailure("registry", `${RUN_CODE_ERROR_PREFIX}: code=registry msg="the tool registry is not bound"`);
|
|
56
|
+
}
|
|
57
|
+
const ctx = {
|
|
58
|
+
sandbox: options.sandbox,
|
|
59
|
+
registry,
|
|
60
|
+
config,
|
|
61
|
+
parentId: input.call_id,
|
|
62
|
+
...(options.events === undefined ? {} : { events: options.events }),
|
|
63
|
+
};
|
|
64
|
+
return brokerRun(ctx, input.args);
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The human/model-facing contract text. Kept as one block because it is diffed
|
|
70
|
+
* byte-for-byte against `contracts/tools.json` (`sdk.test.ts`).
|
|
71
|
+
*/
|
|
72
|
+
const DESC = "Execute a program in the sandbox and get its final value in ONE round trip (parent-broker). " +
|
|
73
|
+
"DEFAULT LANGUAGE: TypeScript, run by Node with native type stripping (no build step) — only ERASABLE TypeScript is allowed: " +
|
|
74
|
+
"no `enum`, no `namespace`, no parameter properties, no `declare`; plain JavaScript always works. " +
|
|
75
|
+
'Pass language: "python" for Python instead. ' +
|
|
76
|
+
"Write the program as a `function main()` body (an indented body is wrapped for you), or as a complete script that defines main; " +
|
|
77
|
+
"main() MAY be async and its resolved value (lossless JSON) is the final result. " +
|
|
78
|
+
"Inside the program the SDK exposes four synchronous bridges dispatched through the normal guarded tool pipeline: " +
|
|
79
|
+
"tools.read_file({path}) / tools.write_file({path, content}) / tools.list_dir({path}) / tools.run_shell({command}) " +
|
|
80
|
+
"(Python: tools.read_file(path=...) etc.); a denied or failed sub-call raises ToolCallError (catch it and continue). " +
|
|
81
|
+
"Bridge tools resolve relative paths against the TOOL workdir, not the program's cwd — pass absolute paths from inside the program. " +
|
|
82
|
+
"Only log what the model needs: a non-protocol stdout line becomes a log line (≤" + MAX_LOG_BYTES + " bytes, UI render only); " +
|
|
83
|
+
"intermediate sub-call results are recorded in the session log but context-retained (the model sees only the final value). " +
|
|
84
|
+
"Hard limits: ≤" + MAX_SUB_CALLS + " sub-calls (the next one errors), wall clock ≤" + MAX_TIMEOUT_MS + "ms (timeout_ms, default " + DEFAULT_TIMEOUT_MS + "), " +
|
|
85
|
+
"sub-call output ledger ≤" + MAX_SUB_OUTPUT_BYTES + " bytes (truncated with a warning). " +
|
|
86
|
+
"No network; the same sandbox as run_shell (bwrap/raw/userspace + rlimits).";
|
|
87
|
+
export function runCodeSpec() {
|
|
88
|
+
return {
|
|
89
|
+
name: "run_code",
|
|
90
|
+
description: DESC,
|
|
91
|
+
parameters: {
|
|
92
|
+
type: "object",
|
|
93
|
+
properties: {
|
|
94
|
+
language: {
|
|
95
|
+
type: "string",
|
|
96
|
+
enum: ["typescript", "python"],
|
|
97
|
+
description: "Program language; default \"typescript\". TypeScript runs under Node's native type stripping (erasable syntax only: no enum/namespace/parameter properties) and needs no build; \"python\" runs under python3 -uB.",
|
|
98
|
+
},
|
|
99
|
+
code: {
|
|
100
|
+
type: "string",
|
|
101
|
+
description: "Program source in the chosen language: a \`function main()\` body (an indented body is wrapped for you) or a complete script that defines main. The engine injects the SDK preamble (tools bridge + protocol).",
|
|
102
|
+
},
|
|
103
|
+
timeout_ms: {
|
|
104
|
+
type: "integer",
|
|
105
|
+
minimum: 1,
|
|
106
|
+
maximum: 120000,
|
|
107
|
+
description: "Optional whole-run wall clock in ms. Default 120000; hard cap 120000 (CELAESTEA_RUN_CODE_TIMEOUT_MS tunes the default, never the cap).",
|
|
108
|
+
},
|
|
109
|
+
desc: descParam(),
|
|
110
|
+
},
|
|
111
|
+
required: ["code"],
|
|
112
|
+
additionalProperties: false,
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `run_shell` — **orchestration only** (`crates/tools/src/builtin.rs`).
|
|
3
|
+
*
|
|
4
|
+
* This tool never spawns anything itself: it validates arguments, delegates
|
|
5
|
+
* execution to the injected `Sandbox` seam, and — for `background: true` —
|
|
6
|
+
* hands the detached child to the session process registry so
|
|
7
|
+
* `process_control` can drive it across turns. Consequences:
|
|
8
|
+
* - swapping in the P2c OS-isolated sandbox changes no line here;
|
|
9
|
+
* - a timeout/kill/workdir failure arrives as a structured `SandboxError`
|
|
10
|
+
* (`run_shell-sandbox: code=…`), never as prose;
|
|
11
|
+
* - the effective isolation mode is reported back inside `sandbox`.
|
|
12
|
+
*/
|
|
13
|
+
import type { Sandbox, Tool, ToolSpec } from "@celestea/core";
|
|
14
|
+
import type { ProcessRegistry } from "../process/registry.js";
|
|
15
|
+
export interface RunShellToolOptions {
|
|
16
|
+
/** The execution boundary (userspace-lite in P2b, OS-isolated in P2c). */
|
|
17
|
+
sandbox: Sandbox;
|
|
18
|
+
/** Session-scoped registry that owns background children. */
|
|
19
|
+
processes: ProcessRegistry;
|
|
20
|
+
}
|
|
21
|
+
export declare function runShellSpec(): ToolSpec;
|
|
22
|
+
export declare function runShellTool(options: RunShellToolOptions): Tool;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `run_shell` — **orchestration only** (`crates/tools/src/builtin.rs`).
|
|
3
|
+
*
|
|
4
|
+
* This tool never spawns anything itself: it validates arguments, delegates
|
|
5
|
+
* execution to the injected `Sandbox` seam, and — for `background: true` —
|
|
6
|
+
* hands the detached child to the session process registry so
|
|
7
|
+
* `process_control` can drive it across turns. Consequences:
|
|
8
|
+
* - swapping in the P2c OS-isolated sandbox changes no line here;
|
|
9
|
+
* - a timeout/kill/workdir failure arrives as a structured `SandboxError`
|
|
10
|
+
* (`run_shell-sandbox: code=…`), never as prose;
|
|
11
|
+
* - the effective isolation mode is reported back inside `sandbox`.
|
|
12
|
+
*/
|
|
13
|
+
import { boolArg, optionalIntArg, optionalStringArg, stringArg } from "../args.js";
|
|
14
|
+
import { descParam } from "../desc.js";
|
|
15
|
+
import { fnTool } from "../fn-tool.js";
|
|
16
|
+
export function runShellSpec() {
|
|
17
|
+
return {
|
|
18
|
+
name: "run_shell",
|
|
19
|
+
description: "Run a shell command inside the sandbox (v2 OS isolation when available: namespaces + read-only root + resource limits, else the v1 userspace path; fixed workdir, sanitized env, bounded timeout and output) and return stdout, stderr, exit code, and a `sandbox` object {provider: bwrap|raw|userspace, net_isolated, tmp_private, seccomp} reporting the effective isolation (W249: network isolated and /tmp a private tmpfs by default; CELESTEA_SANDBOX_NET=1 / CELESTEA_SANDBOX_SHARE_TMP=1 restore the shared host net/tmp; CELESTEA_SANDBOX_SECCOMP=1 enables the seccomp whitelist). With background:true the command is spawned detached (no call-level timeout; resource limits still apply) and returns {background, handle, pid} immediately — control it with the process_control tool (poll / stdin / kill); background processes live in the session process registry and survive across turns. Default timeout is 30s; raise it with timeout_ms up to the cap configured by CELESTEA_SHELL_MAX_TIMEOUT_MS (default 300000ms). Optional cpu_sec overrides RLIMIT_CPU for THIS process (default 20s, foreground and background alike); values above the cap from CELESTEA_SHELL_MAX_CPU_SEC (default 600) are CLAMPED to it, and a CPU-cap kill is reported as cpu_exceeded.",
|
|
20
|
+
parameters: {
|
|
21
|
+
type: "object",
|
|
22
|
+
properties: {
|
|
23
|
+
command: { type: "string", description: "The command line to execute." },
|
|
24
|
+
workdir: {
|
|
25
|
+
type: "string",
|
|
26
|
+
description: "Optional working directory. Must already exist inside the sandbox root; relative paths resolve against the sandbox workdir.",
|
|
27
|
+
},
|
|
28
|
+
timeout_ms: {
|
|
29
|
+
type: "integer",
|
|
30
|
+
minimum: 1,
|
|
31
|
+
description: "Optional per-call timeout in milliseconds. Default 30000; can be raised up to the cap from CELESTEA_SHELL_MAX_TIMEOUT_MS (default 300000). Ignored when background:true.",
|
|
32
|
+
},
|
|
33
|
+
cpu_sec: {
|
|
34
|
+
type: "integer",
|
|
35
|
+
minimum: 1,
|
|
36
|
+
description: "Optional per-call CPU time limit in seconds (RLIMIT_CPU) for THIS process, foreground or background. Default 20; values above CELESTEA_SHELL_MAX_CPU_SEC (default 600) are clamped to that cap. A process killed by the CPU limit reports cpu_exceeded:true with a message naming the limit.",
|
|
37
|
+
},
|
|
38
|
+
background: {
|
|
39
|
+
type: "boolean",
|
|
40
|
+
description: "Optional, default false. When true, spawn the command detached (no call-level timeout; rlimits including cpu_sec still apply) and return {background:true, handle, pid} immediately; control the process with process_control (poll / stdin / kill). A process that exits stays pollable from a bounded tombstone (most recent 32, or 10 minutes), so poll after exit still returns exit_code/signal and the tails.",
|
|
41
|
+
},
|
|
42
|
+
notify: {
|
|
43
|
+
type: "boolean",
|
|
44
|
+
description: "Optional, default true. When background:true, whether a NATURAL exit may be offered to a host completion sink. No sink is wired in this deployment, so read the terminal state with process_control(action=poll); the flag is kept for hosts that install one.",
|
|
45
|
+
},
|
|
46
|
+
desc: descParam(),
|
|
47
|
+
},
|
|
48
|
+
required: ["command"],
|
|
49
|
+
additionalProperties: false,
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export function runShellTool(options) {
|
|
54
|
+
return fnTool(runShellSpec(), async (args) => {
|
|
55
|
+
const command = stringArg(args, "command");
|
|
56
|
+
const workdir = optionalStringArg(args, "workdir");
|
|
57
|
+
const cpuSec = optionalIntArg(args, "cpu_sec");
|
|
58
|
+
const cpu = cpuSec === undefined ? {} : { cpuSec };
|
|
59
|
+
if (boolArg(args, "background", false)) {
|
|
60
|
+
const spawned = await options.sandbox.spawn({ command, workdir, ...cpu });
|
|
61
|
+
const handle = options.processes.insert(spawned.child, boolArg(args, "notify", true), {
|
|
62
|
+
cpuSec: spawned.sandbox.cpu_sec ?? cpuSec ?? null,
|
|
63
|
+
});
|
|
64
|
+
return { background: true, handle: handle.handle, pid: handle.pid, sandbox: spawned.sandbox };
|
|
65
|
+
}
|
|
66
|
+
const run = await options.sandbox.run({ command, workdir, timeoutMs: optionalIntArg(args, "timeout_ms"), ...cpu });
|
|
67
|
+
// W6: an RLIMIT_CPU kill (SIGXCPU, or the SIGKILL that follows) must not read
|
|
68
|
+
// as a bare death; mark it when the provider reported a CPU cap in force.
|
|
69
|
+
const cpuExceeded = run.exit_code === null && run.sandbox.cpu_sec !== undefined && (run.signal === "SIGXCPU" || run.signal === "SIGKILL");
|
|
70
|
+
return {
|
|
71
|
+
stdout: run.stdout,
|
|
72
|
+
stderr: run.stderr,
|
|
73
|
+
exit_code: run.exit_code,
|
|
74
|
+
...(run.signal === null || run.signal === undefined ? {} : { signal: run.signal }),
|
|
75
|
+
...(cpuExceeded ? { cpu_exceeded: true, message: `CPU time limit ${run.sandbox.cpu_sec}s exceeded` } : {}),
|
|
76
|
+
stdout_truncated: run.stdout_truncated,
|
|
77
|
+
stderr_truncated: run.stderr_truncated,
|
|
78
|
+
sandbox: run.sandbox,
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `write_file` — create or overwrite a text file, answering `"ok"` like the
|
|
3
|
+
* legacy `builtin.rs::write_file`. The path policy (workspace-only writes) is
|
|
4
|
+
* enforced by the guard chain, not here.
|
|
5
|
+
*/
|
|
6
|
+
import type { Tool, ToolSpec } from "@celestea/core";
|
|
7
|
+
export declare function writeFileSpec(): ToolSpec;
|
|
8
|
+
export declare function writeFileTool(): Tool;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `write_file` — create or overwrite a text file, answering `"ok"` like the
|
|
3
|
+
* legacy `builtin.rs::write_file`. The path policy (workspace-only writes) is
|
|
4
|
+
* enforced by the guard chain, not here.
|
|
5
|
+
*/
|
|
6
|
+
import { stringArg } from "../args.js";
|
|
7
|
+
import { descParam } from "../desc.js";
|
|
8
|
+
import { writeTextFile } from "../fs/file-io.js";
|
|
9
|
+
import { fnTool } from "../fn-tool.js";
|
|
10
|
+
export function writeFileSpec() {
|
|
11
|
+
return {
|
|
12
|
+
name: "write_file",
|
|
13
|
+
description: "Write text content to a file, creating or overwriting it.",
|
|
14
|
+
parameters: {
|
|
15
|
+
type: "object",
|
|
16
|
+
properties: {
|
|
17
|
+
path: { type: "string", description: "Filesystem path of the file to write." },
|
|
18
|
+
content: { type: "string", description: "Text content to write." },
|
|
19
|
+
desc: descParam(),
|
|
20
|
+
},
|
|
21
|
+
required: ["path", "content"],
|
|
22
|
+
additionalProperties: false,
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export function writeFileTool() {
|
|
27
|
+
return fnTool(writeFileSpec(), async (args) => {
|
|
28
|
+
await writeTextFile(stringArg(args, "path"), stringArg(args, "content"));
|
|
29
|
+
return "ok";
|
|
30
|
+
});
|
|
31
|
+
}
|