@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,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The six builtin tools (`crates/tools/src/builtin.rs`): `read_file`,
|
|
3
|
+
* `write_file`, `list_dir`, `run_shell`, `process_control`, `http_request`.
|
|
4
|
+
*
|
|
5
|
+
* One `ProcessRegistry` and one `Sandbox` are shared by `run_shell` /
|
|
6
|
+
* `process_control` within a tool set, so a background process started in one
|
|
7
|
+
* call is controllable from the next. Embeddings that mount several tool sets
|
|
8
|
+
* pass `builtinTools({ processes, sandbox })` a shared pair.
|
|
9
|
+
*
|
|
10
|
+
* W783: `ask_user_question` is mounted here too when the caller supplies the
|
|
11
|
+
* user-question service. It is NOT one of the six builtins, and it is
|
|
12
|
+
* OPTIONAL: `packages/tools` may only depend on `@celestea/core`, so the service
|
|
13
|
+
* arrives by construction and an embedding that has no human answerer simply
|
|
14
|
+
* leaves `questions` out instead of registering a tool that can never work.
|
|
15
|
+
*/
|
|
16
|
+
import type { Sandbox, Tool, UserQuestionService } from "@celestea/core";
|
|
17
|
+
import { type HttpRequestToolOptions } from "./tools/http-request.js";
|
|
18
|
+
import type { AttachmentStore } from "./attachments/store.js";
|
|
19
|
+
import { ProcessRegistry } from "./process/registry.js";
|
|
20
|
+
export interface BuiltinToolsOptions {
|
|
21
|
+
sandbox?: Sandbox;
|
|
22
|
+
processes?: ProcessRegistry;
|
|
23
|
+
http?: HttpRequestToolOptions;
|
|
24
|
+
/**
|
|
25
|
+
* W783: the host's user-question service. Present = `ask_user_question` is
|
|
26
|
+
* registered (11 tools); absent = it is not (the frozen 10).
|
|
27
|
+
*/
|
|
28
|
+
questions?: UserQuestionService | null;
|
|
29
|
+
/**
|
|
30
|
+
* W804: the session's attachment store. Present = `read_image` is mounted
|
|
31
|
+
* (12 tools); absent = the tool is not offered, so the model is never told it
|
|
32
|
+
* exists (the same "register only what works" rule as ask_user_question).
|
|
33
|
+
*/
|
|
34
|
+
attachments?: AttachmentStore | null;
|
|
35
|
+
/**
|
|
36
|
+
* W804: false ONLY when the target model's input_modalities was explicitly
|
|
37
|
+
* configured without "image" (section 6.6). Absent/true = optimistic default.
|
|
38
|
+
*/
|
|
39
|
+
imageInputAllowed?: boolean;
|
|
40
|
+
/** W804: the model id, for the read_image refusal text. */
|
|
41
|
+
model?: string;
|
|
42
|
+
/**
|
|
43
|
+
* W884: the composing SESSION's workspace root for `load_skill` (W768's
|
|
44
|
+
* `sessionWorkspaceOf` — the single source of truth). `null`/absent = a
|
|
45
|
+
* generation with no workspace (the detached default); the tool is still
|
|
46
|
+
* REGISTERED so every face advertises the same names, and a call fails with a
|
|
47
|
+
* structured `no_workspace` error instead of guessing a path.
|
|
48
|
+
*/
|
|
49
|
+
workspace?: string | null;
|
|
50
|
+
/** W884: environment the CELESTEA_HOME global skill layer resolves under. */
|
|
51
|
+
env?: NodeJS.ProcessEnv;
|
|
52
|
+
}
|
|
53
|
+
/** The six builtins, sharing one sandbox + one process registry. */
|
|
54
|
+
export declare function builtinTools(options?: BuiltinToolsOptions): Tool[];
|
package/dist/builtin.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The six builtin tools (`crates/tools/src/builtin.rs`): `read_file`,
|
|
3
|
+
* `write_file`, `list_dir`, `run_shell`, `process_control`, `http_request`.
|
|
4
|
+
*
|
|
5
|
+
* One `ProcessRegistry` and one `Sandbox` are shared by `run_shell` /
|
|
6
|
+
* `process_control` within a tool set, so a background process started in one
|
|
7
|
+
* call is controllable from the next. Embeddings that mount several tool sets
|
|
8
|
+
* pass `builtinTools({ processes, sandbox })` a shared pair.
|
|
9
|
+
*
|
|
10
|
+
* W783: `ask_user_question` is mounted here too when the caller supplies the
|
|
11
|
+
* user-question service. It is NOT one of the six builtins, and it is
|
|
12
|
+
* OPTIONAL: `packages/tools` may only depend on `@celestea/core`, so the service
|
|
13
|
+
* arrives by construction and an embedding that has no human answerer simply
|
|
14
|
+
* leaves `questions` out instead of registering a tool that can never work.
|
|
15
|
+
*/
|
|
16
|
+
import { askUserTool } from "./tools/ask-user.js";
|
|
17
|
+
import { httpRequestTool } from "./tools/http-request.js";
|
|
18
|
+
import { listDirTool } from "./tools/list-dir.js";
|
|
19
|
+
import { processControlTool } from "./tools/process-control.js";
|
|
20
|
+
import { loadSkillTool } from "./tools/load-skill.js";
|
|
21
|
+
import { forgetTool, rememberTool } from "./tools/memory.js";
|
|
22
|
+
import { readImageTool } from "./tools/read-image.js";
|
|
23
|
+
import { readFileTool } from "./tools/read-file.js";
|
|
24
|
+
import { browserActTool, browserOpenTool } from "./tools/browser.js";
|
|
25
|
+
import { BrowserManager } from "./browser/session.js";
|
|
26
|
+
import { runShellTool } from "./tools/run-shell.js";
|
|
27
|
+
import { writeFileTool } from "./tools/write-file.js";
|
|
28
|
+
import { ProcessRegistry } from "./process/registry.js";
|
|
29
|
+
import { selectSandbox } from "./sandbox/provider.js";
|
|
30
|
+
/** The six builtins, sharing one sandbox + one process registry. */
|
|
31
|
+
export function builtinTools(options = {}) {
|
|
32
|
+
const processes = options.processes ?? new ProcessRegistry();
|
|
33
|
+
const sandbox = options.sandbox ?? selectSandbox();
|
|
34
|
+
const tools = [
|
|
35
|
+
readFileTool(),
|
|
36
|
+
writeFileTool(),
|
|
37
|
+
listDirTool(),
|
|
38
|
+
runShellTool({ sandbox, processes }),
|
|
39
|
+
processControlTool(processes),
|
|
40
|
+
httpRequestTool(options.http ?? {}),
|
|
41
|
+
// W884: the 7th builtin — always mounted so the model face cannot drift
|
|
42
|
+
// between the detached default generation and a real session.
|
|
43
|
+
loadSkillTool({
|
|
44
|
+
workspace: options.workspace ?? null,
|
|
45
|
+
...(options.env === undefined ? {} : { env: options.env }),
|
|
46
|
+
}),
|
|
47
|
+
// B2 (F3 P1): the memory write pair. HOST tools like load_skill: the global
|
|
48
|
+
// memory layer lives OUTSIDE the workspace (so the path guard cannot reach
|
|
49
|
+
// it), and a generation with no workspace still advertises the names and
|
|
50
|
+
// fails closed with no_workspace on a call.
|
|
51
|
+
rememberTool({
|
|
52
|
+
workspace: options.workspace ?? null,
|
|
53
|
+
...(options.env === undefined ? {} : { env: options.env }),
|
|
54
|
+
}),
|
|
55
|
+
forgetTool({
|
|
56
|
+
workspace: options.workspace ?? null,
|
|
57
|
+
...(options.env === undefined ? {} : { env: options.env }),
|
|
58
|
+
}),
|
|
59
|
+
];
|
|
60
|
+
// W783: only when a human answerer actually exists in this host.
|
|
61
|
+
if (options.questions !== undefined && options.questions !== null)
|
|
62
|
+
tools.push(askUserTool({ questions: options.questions }));
|
|
63
|
+
// W804: only when the session has an attachment store to read from / write to.
|
|
64
|
+
if (options.attachments !== undefined && options.attachments !== null) {
|
|
65
|
+
tools.push(readImageTool({
|
|
66
|
+
attachments: options.attachments,
|
|
67
|
+
imageInputAllowed: options.imageInputAllowed ?? true,
|
|
68
|
+
...(options.model === undefined ? {} : { model: options.model }),
|
|
69
|
+
}));
|
|
70
|
+
// F4 step 2b: the browser tools need the SAME session attachment store
|
|
71
|
+
// (the screenshot rides the existing image chain) and share ONE manager, so
|
|
72
|
+
// browser_open and browser_act drive the same page and process.
|
|
73
|
+
const manager = new BrowserManager({ sandbox, processes, attachments: options.attachments });
|
|
74
|
+
tools.push(browserOpenTool({ manager }), browserActTool({ manager }));
|
|
75
|
+
}
|
|
76
|
+
return tools;
|
|
77
|
+
}
|
package/dist/desc.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `desc` UI-label parameter (W779 T1).
|
|
3
|
+
*
|
|
4
|
+
* EVERY tool takes one optional `desc` string: a one-line label describing what
|
|
5
|
+
* this particular call is doing, which the UI shows on the tool card. It is
|
|
6
|
+
* pure presentation:
|
|
7
|
+
* - no executor reads it (nothing but the specs mentions `desc`), so a call
|
|
8
|
+
* behaves exactly the same with or without it;
|
|
9
|
+
* - the schema declares it as a plain optional `string` — deliberately NO
|
|
10
|
+
* `maxLength`, because the 80-character budget is a display concern and the
|
|
11
|
+
* dispatch validator is a frozen subset (`schema.ts`); a long label must
|
|
12
|
+
* never turn a working call into `toolargs: code=schema`.
|
|
13
|
+
*
|
|
14
|
+
* The text is shared so the 7 builtin specs and `contracts/tools.json` cannot
|
|
15
|
+
* drift word by word; `tests/contract-parity.test.ts` compares them all.
|
|
16
|
+
*/
|
|
17
|
+
/** The one-line contract text of the `desc` parameter. */
|
|
18
|
+
export declare const DESC_DESCRIPTION = "Optional one-line label (max 80 chars) describing what this call is doing; shown on the tool card in the UI. Keep it short.";
|
|
19
|
+
/** A fresh `desc` property schema (never share the object: specs are values). */
|
|
20
|
+
export declare function descParam(): Record<string, unknown>;
|
package/dist/desc.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `desc` UI-label parameter (W779 T1).
|
|
3
|
+
*
|
|
4
|
+
* EVERY tool takes one optional `desc` string: a one-line label describing what
|
|
5
|
+
* this particular call is doing, which the UI shows on the tool card. It is
|
|
6
|
+
* pure presentation:
|
|
7
|
+
* - no executor reads it (nothing but the specs mentions `desc`), so a call
|
|
8
|
+
* behaves exactly the same with or without it;
|
|
9
|
+
* - the schema declares it as a plain optional `string` — deliberately NO
|
|
10
|
+
* `maxLength`, because the 80-character budget is a display concern and the
|
|
11
|
+
* dispatch validator is a frozen subset (`schema.ts`); a long label must
|
|
12
|
+
* never turn a working call into `toolargs: code=schema`.
|
|
13
|
+
*
|
|
14
|
+
* The text is shared so the 7 builtin specs and `contracts/tools.json` cannot
|
|
15
|
+
* drift word by word; `tests/contract-parity.test.ts` compares them all.
|
|
16
|
+
*/
|
|
17
|
+
/** The one-line contract text of the `desc` parameter. */
|
|
18
|
+
export const DESC_DESCRIPTION = "Optional one-line label (max 80 chars) describing what this call is doing; shown on the tool card in the UI. Keep it short.";
|
|
19
|
+
/** A fresh `desc` property schema (never share the object: specs are values). */
|
|
20
|
+
export function descParam() {
|
|
21
|
+
return { type: "string", description: DESC_DESCRIPTION };
|
|
22
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* W806 (P0) — dynamic tool disclosure: the cache-safe second hidden layer.
|
|
3
|
+
*
|
|
4
|
+
* `exposedRegistry` already owns the MODE fold (a static keep list). This module
|
|
5
|
+
* adds the layer the design doc (`docs/feature-dynamic-tool-disclosure.md` §7.1)
|
|
6
|
+
* calls for on top of it: a policy that withholds part of the mode's disclosable
|
|
7
|
+
* universe and reveals it ONE TURN AT A TIME.
|
|
8
|
+
*
|
|
9
|
+
* The whole point is the upstream cache. The provider is billed on a strict
|
|
10
|
+
* token BYTE PREFIX (`system -> tools -> messages`), so a change in the middle
|
|
11
|
+
* of the tools array invalidates every cached token after it — including the
|
|
12
|
+
* whole conversation history. These three invariants are therefore policy, not
|
|
13
|
+
* taste (design §3.5):
|
|
14
|
+
*
|
|
15
|
+
* 1. **monotonic** — a name disclosed in this session is never taken back;
|
|
16
|
+
* 2. **tail-append** — a newly disclosed name is appended AFTER every name
|
|
17
|
+
* already on the wire, never inserted into the middle;
|
|
18
|
+
* 3. **turn boundary** — only `beginTurn()` changes the set. A refused call
|
|
19
|
+
* during a turn is only a PROPOSAL; the next turn is the first one that
|
|
20
|
+
* sees it (the design's "被拒后披露" trigger, Q1).
|
|
21
|
+
*
|
|
22
|
+
* Nothing here is authorization: the policy only decides what the model is
|
|
23
|
+
* OFFERED. A withheld tool still runs the same guard chain when reached from a
|
|
24
|
+
* `run_code` program, and a direct call is refused before execution.
|
|
25
|
+
*/
|
|
26
|
+
import { type ExposureOptions } from "./exposure.js";
|
|
27
|
+
/** One immutable view of the policy (diagnostics / assertions). */
|
|
28
|
+
export interface DisclosureSnapshot {
|
|
29
|
+
/** Offered names, in stable disclosure order (baseline ++ first-disclosure). */
|
|
30
|
+
readonly disclosed: readonly string[];
|
|
31
|
+
/** Refused names, in universe order (blocked fold ∪ not-yet-disclosed). */
|
|
32
|
+
readonly hidden: readonly string[];
|
|
33
|
+
}
|
|
34
|
+
export interface DisclosurePolicyOptions {
|
|
35
|
+
/** Every registered tool name, in the baseline wire order. */
|
|
36
|
+
universe: readonly string[];
|
|
37
|
+
/**
|
|
38
|
+
* Names offered from the start. Default: the whole non-blocked universe,
|
|
39
|
+
* which is exactly today's mode baseline (so the default is a no-op).
|
|
40
|
+
*/
|
|
41
|
+
initial?: readonly string[];
|
|
42
|
+
/** Names that must NEVER be disclosed — the static mode fold it may not undo. */
|
|
43
|
+
blocked?: readonly string[];
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The disclosure state of ONE session generation.
|
|
47
|
+
*
|
|
48
|
+
* Mutable by design (the live face has to move at a turn boundary), but every
|
|
49
|
+
* reader gets a fresh array from `disclosed()`/`hidden()`, so a reader can
|
|
50
|
+
* never observe a half-applied change: `beginTurn()` builds the promoted list
|
|
51
|
+
* before it publishes anything.
|
|
52
|
+
*/
|
|
53
|
+
export declare class DisclosurePolicy {
|
|
54
|
+
private readonly universe;
|
|
55
|
+
private readonly universeSet;
|
|
56
|
+
private readonly blocked;
|
|
57
|
+
/** The fixed initial face, in its given order (never reordered afterwards). */
|
|
58
|
+
private readonly baseline;
|
|
59
|
+
private readonly baselineSet;
|
|
60
|
+
/** Names appended by a turn boundary, in first-disclosure order. */
|
|
61
|
+
private readonly added;
|
|
62
|
+
private readonly addedSet;
|
|
63
|
+
/** Refused-but-disclosable names seen since the last turn boundary. */
|
|
64
|
+
private readonly pending;
|
|
65
|
+
private readonly pendingSet;
|
|
66
|
+
constructor(options: DisclosurePolicyOptions);
|
|
67
|
+
/**
|
|
68
|
+
* The names this face offers, in stable disclosure order: the initial
|
|
69
|
+
* baseline first (its own order), then every name a turn boundary appended,
|
|
70
|
+
* in first-disclosure order. Append-only by construction.
|
|
71
|
+
*/
|
|
72
|
+
disclosed(): string[];
|
|
73
|
+
/**
|
|
74
|
+
* The names this face refuses: the mode fold ∪ everything not yet disclosed,
|
|
75
|
+
* in universe order. Reading it is a pure function of the current state.
|
|
76
|
+
*/
|
|
77
|
+
hidden(): string[];
|
|
78
|
+
/** The names a `{{tools}}` rendering may announce (the static universe). */
|
|
79
|
+
universeNames(): string[];
|
|
80
|
+
/** Is `name` part of the static mode fold (never disclosable)? */
|
|
81
|
+
isBlocked(name: string): boolean;
|
|
82
|
+
/** Is anything withheld that could still be disclosed? */
|
|
83
|
+
get active(): boolean;
|
|
84
|
+
/**
|
|
85
|
+
* Record a DIRECT call the face refused because `name` was not disclosed.
|
|
86
|
+
* This is only a proposal: it changes nothing until [beginTurn]. Returns true
|
|
87
|
+
* when the name was accepted as a proposal (known, disclosable, not already
|
|
88
|
+
* offered or pending).
|
|
89
|
+
*/
|
|
90
|
+
propose(name: string): boolean;
|
|
91
|
+
/**
|
|
92
|
+
* The turn boundary: promote every proposal recorded since the previous call.
|
|
93
|
+
* Monotonic (nothing is ever removed) and tail-appending (promotion order is
|
|
94
|
+
* first-proposal order). Returns the names DISCLOSED by this call — empty
|
|
95
|
+
* means the wire array is unchanged and no cache prefix is invalidated.
|
|
96
|
+
*/
|
|
97
|
+
beginTurn(): string[];
|
|
98
|
+
/** One immutable view of this policy. */
|
|
99
|
+
snapshot(): DisclosureSnapshot;
|
|
100
|
+
}
|
|
101
|
+
/** The per-name guidance pair a dynamic face needs (mode fold vs withheld). */
|
|
102
|
+
export interface DisclosureGuidance {
|
|
103
|
+
/** Prose for a name folded by the MODE (default [EXECUTION_GUIDANCE]). */
|
|
104
|
+
folded?: string;
|
|
105
|
+
/** Prose for a name merely not yet disclosed (default [DISCLOSURE_GUIDANCE]). */
|
|
106
|
+
withheld?: string;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* The [ExposureOptions] of a dynamic face: `hidden`/`order` read the policy live,
|
|
110
|
+
* a refused direct call becomes a proposal, and each refusal gets the prose that
|
|
111
|
+
* fits WHY the name is hidden (the mode fold vs merely not-yet-disclosed).
|
|
112
|
+
*/
|
|
113
|
+
export declare function disclosureExposure(policy: DisclosurePolicy, guidance?: DisclosureGuidance): ExposureOptions;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* W806 (P0) — dynamic tool disclosure: the cache-safe second hidden layer.
|
|
3
|
+
*
|
|
4
|
+
* `exposedRegistry` already owns the MODE fold (a static keep list). This module
|
|
5
|
+
* adds the layer the design doc (`docs/feature-dynamic-tool-disclosure.md` §7.1)
|
|
6
|
+
* calls for on top of it: a policy that withholds part of the mode's disclosable
|
|
7
|
+
* universe and reveals it ONE TURN AT A TIME.
|
|
8
|
+
*
|
|
9
|
+
* The whole point is the upstream cache. The provider is billed on a strict
|
|
10
|
+
* token BYTE PREFIX (`system -> tools -> messages`), so a change in the middle
|
|
11
|
+
* of the tools array invalidates every cached token after it — including the
|
|
12
|
+
* whole conversation history. These three invariants are therefore policy, not
|
|
13
|
+
* taste (design §3.5):
|
|
14
|
+
*
|
|
15
|
+
* 1. **monotonic** — a name disclosed in this session is never taken back;
|
|
16
|
+
* 2. **tail-append** — a newly disclosed name is appended AFTER every name
|
|
17
|
+
* already on the wire, never inserted into the middle;
|
|
18
|
+
* 3. **turn boundary** — only `beginTurn()` changes the set. A refused call
|
|
19
|
+
* during a turn is only a PROPOSAL; the next turn is the first one that
|
|
20
|
+
* sees it (the design's "被拒后披露" trigger, Q1).
|
|
21
|
+
*
|
|
22
|
+
* Nothing here is authorization: the policy only decides what the model is
|
|
23
|
+
* OFFERED. A withheld tool still runs the same guard chain when reached from a
|
|
24
|
+
* `run_code` program, and a direct call is refused before execution.
|
|
25
|
+
*/
|
|
26
|
+
import { DISCLOSURE_GUIDANCE, EXECUTION_GUIDANCE } from "./exposure.js";
|
|
27
|
+
/**
|
|
28
|
+
* The disclosure state of ONE session generation.
|
|
29
|
+
*
|
|
30
|
+
* Mutable by design (the live face has to move at a turn boundary), but every
|
|
31
|
+
* reader gets a fresh array from `disclosed()`/`hidden()`, so a reader can
|
|
32
|
+
* never observe a half-applied change: `beginTurn()` builds the promoted list
|
|
33
|
+
* before it publishes anything.
|
|
34
|
+
*/
|
|
35
|
+
export class DisclosurePolicy {
|
|
36
|
+
universe;
|
|
37
|
+
universeSet;
|
|
38
|
+
blocked;
|
|
39
|
+
/** The fixed initial face, in its given order (never reordered afterwards). */
|
|
40
|
+
baseline;
|
|
41
|
+
baselineSet;
|
|
42
|
+
/** Names appended by a turn boundary, in first-disclosure order. */
|
|
43
|
+
added = [];
|
|
44
|
+
addedSet = new Set();
|
|
45
|
+
/** Refused-but-disclosable names seen since the last turn boundary. */
|
|
46
|
+
pending = [];
|
|
47
|
+
pendingSet = new Set();
|
|
48
|
+
constructor(options) {
|
|
49
|
+
this.universe = [...options.universe];
|
|
50
|
+
this.universeSet = new Set(this.universe);
|
|
51
|
+
this.blocked = new Set(options.blocked ?? []);
|
|
52
|
+
const wanted = options.initial ?? this.universe;
|
|
53
|
+
this.baseline = wanted.filter((name) => this.universeSet.has(name) && !this.blocked.has(name));
|
|
54
|
+
this.baselineSet = new Set(this.baseline);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The names this face offers, in stable disclosure order: the initial
|
|
58
|
+
* baseline first (its own order), then every name a turn boundary appended,
|
|
59
|
+
* in first-disclosure order. Append-only by construction.
|
|
60
|
+
*/
|
|
61
|
+
disclosed() {
|
|
62
|
+
return [...this.baseline, ...this.added];
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The names this face refuses: the mode fold ∪ everything not yet disclosed,
|
|
66
|
+
* in universe order. Reading it is a pure function of the current state.
|
|
67
|
+
*/
|
|
68
|
+
hidden() {
|
|
69
|
+
const offered = new Set(this.disclosed());
|
|
70
|
+
return this.universe.filter((name) => this.blocked.has(name) || !offered.has(name));
|
|
71
|
+
}
|
|
72
|
+
/** The names a `{{tools}}` rendering may announce (the static universe). */
|
|
73
|
+
universeNames() {
|
|
74
|
+
return [...this.universe];
|
|
75
|
+
}
|
|
76
|
+
/** Is `name` part of the static mode fold (never disclosable)? */
|
|
77
|
+
isBlocked(name) {
|
|
78
|
+
return this.blocked.has(name);
|
|
79
|
+
}
|
|
80
|
+
/** Is anything withheld that could still be disclosed? */
|
|
81
|
+
get active() {
|
|
82
|
+
return this.hidden().some((name) => !this.blocked.has(name));
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Record a DIRECT call the face refused because `name` was not disclosed.
|
|
86
|
+
* This is only a proposal: it changes nothing until [beginTurn]. Returns true
|
|
87
|
+
* when the name was accepted as a proposal (known, disclosable, not already
|
|
88
|
+
* offered or pending).
|
|
89
|
+
*/
|
|
90
|
+
propose(name) {
|
|
91
|
+
if (!this.universeSet.has(name))
|
|
92
|
+
return false;
|
|
93
|
+
if (this.blocked.has(name))
|
|
94
|
+
return false;
|
|
95
|
+
if (this.baselineSet.has(name) || this.addedSet.has(name))
|
|
96
|
+
return false;
|
|
97
|
+
if (this.pendingSet.has(name))
|
|
98
|
+
return false;
|
|
99
|
+
this.pendingSet.add(name);
|
|
100
|
+
this.pending.push(name);
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* The turn boundary: promote every proposal recorded since the previous call.
|
|
105
|
+
* Monotonic (nothing is ever removed) and tail-appending (promotion order is
|
|
106
|
+
* first-proposal order). Returns the names DISCLOSED by this call — empty
|
|
107
|
+
* means the wire array is unchanged and no cache prefix is invalidated.
|
|
108
|
+
*/
|
|
109
|
+
beginTurn() {
|
|
110
|
+
const promoted = [];
|
|
111
|
+
for (const name of this.pending) {
|
|
112
|
+
if (this.addedSet.has(name))
|
|
113
|
+
continue;
|
|
114
|
+
this.addedSet.add(name);
|
|
115
|
+
this.added.push(name);
|
|
116
|
+
promoted.push(name);
|
|
117
|
+
}
|
|
118
|
+
this.pending.length = 0;
|
|
119
|
+
this.pendingSet.clear();
|
|
120
|
+
return promoted;
|
|
121
|
+
}
|
|
122
|
+
/** One immutable view of this policy. */
|
|
123
|
+
snapshot() {
|
|
124
|
+
return { disclosed: this.disclosed(), hidden: this.hidden() };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* The [ExposureOptions] of a dynamic face: `hidden`/`order` read the policy live,
|
|
129
|
+
* a refused direct call becomes a proposal, and each refusal gets the prose that
|
|
130
|
+
* fits WHY the name is hidden (the mode fold vs merely not-yet-disclosed).
|
|
131
|
+
*/
|
|
132
|
+
export function disclosureExposure(policy, guidance = {}) {
|
|
133
|
+
const folded = guidance.folded ?? EXECUTION_GUIDANCE;
|
|
134
|
+
const withheld = guidance.withheld ?? DISCLOSURE_GUIDANCE;
|
|
135
|
+
return {
|
|
136
|
+
hidden: () => policy.hidden(),
|
|
137
|
+
order: () => policy.disclosed(),
|
|
138
|
+
guidanceFor: (name) => (policy.isBlocked(name) ? folded : withheld),
|
|
139
|
+
onHidden: (name) => void policy.propose(name),
|
|
140
|
+
};
|
|
141
|
+
}
|
package/dist/env.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment helpers shared by the guard, the http policy and the sandbox
|
|
3
|
+
* config. Env is always passed in explicitly (default `process.env`) so every
|
|
4
|
+
* knob is testable without mutating global state.
|
|
5
|
+
*/
|
|
6
|
+
/** One env knob: read a string, a positive integer, or an on/off flag. */
|
|
7
|
+
export declare function envString(env: NodeJS.ProcessEnv, name: string): string | undefined;
|
|
8
|
+
export declare function envInt(env: NodeJS.ProcessEnv, name: string): number | undefined;
|
|
9
|
+
/** On/off flag (`1/true/on/yes` vs `0/false/off/no`); anything else = fallback. */
|
|
10
|
+
export declare function envFlag(value: string | undefined, fallback: boolean): boolean;
|
package/dist/env.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment helpers shared by the guard, the http policy and the sandbox
|
|
3
|
+
* config. Env is always passed in explicitly (default `process.env`) so every
|
|
4
|
+
* knob is testable without mutating global state.
|
|
5
|
+
*/
|
|
6
|
+
/** One env knob: read a string, a positive integer, or an on/off flag. */
|
|
7
|
+
export function envString(env, name) {
|
|
8
|
+
const raw = env[name];
|
|
9
|
+
if (raw === undefined)
|
|
10
|
+
return undefined;
|
|
11
|
+
const trimmed = raw.trim();
|
|
12
|
+
return trimmed === "" ? undefined : trimmed;
|
|
13
|
+
}
|
|
14
|
+
export function envInt(env, name) {
|
|
15
|
+
const raw = envString(env, name);
|
|
16
|
+
if (raw === undefined)
|
|
17
|
+
return undefined;
|
|
18
|
+
const parsed = Number(raw);
|
|
19
|
+
return Number.isFinite(parsed) ? Math.trunc(parsed) : undefined;
|
|
20
|
+
}
|
|
21
|
+
/** On/off flag (`1/true/on/yes` vs `0/false/off/no`); anything else = fallback. */
|
|
22
|
+
export function envFlag(value, fallback) {
|
|
23
|
+
if (value === undefined)
|
|
24
|
+
return fallback;
|
|
25
|
+
const normalized = value.trim().toLowerCase();
|
|
26
|
+
if (normalized === "")
|
|
27
|
+
return fallback;
|
|
28
|
+
if (["1", "true", "on", "yes"].includes(normalized))
|
|
29
|
+
return true;
|
|
30
|
+
if (["0", "false", "off", "no"].includes(normalized))
|
|
31
|
+
return false;
|
|
32
|
+
return fallback;
|
|
33
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured tool-pipeline errors.
|
|
3
|
+
*
|
|
4
|
+
* The pipeline never throws across the `Tool` seam: a tool rejects with a
|
|
5
|
+
* `ToolFailure` whose `message` is the machine-readable contract string
|
|
6
|
+
* `<prefix>: code=<code> msg="<quoted>"` (parity: `toolguard: …`,
|
|
7
|
+
* `run_shell-sandbox: …`, `http_request: code=…`), and the registry captures it
|
|
8
|
+
* into `ToolOutput.error`. Callers branch on `kind`/`code`, never on prose.
|
|
9
|
+
*/
|
|
10
|
+
import { ToolFailure } from "./tool-failure.js";
|
|
11
|
+
export { ToolFailure, isToolFailure } from "./tool-failure.js";
|
|
12
|
+
/** Stable prefix of argument-validation failures (`registry` pipeline stage 1). */
|
|
13
|
+
export declare const TOOLARG_ERROR_PREFIX = "toolargs";
|
|
14
|
+
/** Stable prefix of guard denials (legacy `GUARD_ERROR_PREFIX`). */
|
|
15
|
+
export declare const GUARD_ERROR_PREFIX = "toolguard";
|
|
16
|
+
/** Escape + truncate a message so the one-line error contract stays parseable. */
|
|
17
|
+
export declare function quoteMessage(message: string): string;
|
|
18
|
+
/** `<prefix>: code=<code> msg="<quoted>"` — the tool-side contract error shape. */
|
|
19
|
+
export declare function contractError(prefix: string, code: string, message: string): string;
|
|
20
|
+
/** Build a structured failure with the contract message already formatted. */
|
|
21
|
+
export declare function contractFailure(prefix: string, code: string, message: string): ToolFailure;
|
|
22
|
+
/** Error text for `ToolOutput.error`; non-Error rejections are stringified. */
|
|
23
|
+
export declare function errorText(value: unknown): string;
|
|
24
|
+
/** `run_shell-sandbox: code=timeout msg="…"` → `timeout` (for tests/diagnostics). */
|
|
25
|
+
export declare function errorCode(message: string): string | null;
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured tool-pipeline errors.
|
|
3
|
+
*
|
|
4
|
+
* The pipeline never throws across the `Tool` seam: a tool rejects with a
|
|
5
|
+
* `ToolFailure` whose `message` is the machine-readable contract string
|
|
6
|
+
* `<prefix>: code=<code> msg="<quoted>"` (parity: `toolguard: …`,
|
|
7
|
+
* `run_shell-sandbox: …`, `http_request: code=…`), and the registry captures it
|
|
8
|
+
* into `ToolOutput.error`. Callers branch on `kind`/`code`, never on prose.
|
|
9
|
+
*/
|
|
10
|
+
import { ToolFailure } from "./tool-failure.js";
|
|
11
|
+
export { ToolFailure, isToolFailure } from "./tool-failure.js";
|
|
12
|
+
/** Stable prefix of argument-validation failures (`registry` pipeline stage 1). */
|
|
13
|
+
export const TOOLARG_ERROR_PREFIX = "toolargs";
|
|
14
|
+
/** Stable prefix of guard denials (legacy `GUARD_ERROR_PREFIX`). */
|
|
15
|
+
export const GUARD_ERROR_PREFIX = "toolguard";
|
|
16
|
+
/** Escape + truncate a message so the one-line error contract stays parseable. */
|
|
17
|
+
export function quoteMessage(message) {
|
|
18
|
+
let out = "";
|
|
19
|
+
let count = 0;
|
|
20
|
+
for (const ch of message) {
|
|
21
|
+
if (count >= 512)
|
|
22
|
+
break;
|
|
23
|
+
count += 1;
|
|
24
|
+
const cp = ch.codePointAt(0) ?? 0;
|
|
25
|
+
if (ch === "\\")
|
|
26
|
+
out += "\\\\";
|
|
27
|
+
else if (ch === '"')
|
|
28
|
+
out += '\\"';
|
|
29
|
+
else if (ch === "\n")
|
|
30
|
+
out += "\\n";
|
|
31
|
+
else if (ch === "\r")
|
|
32
|
+
out += "\\r";
|
|
33
|
+
else if (ch === "\t")
|
|
34
|
+
out += "\\t";
|
|
35
|
+
else if (cp < 0x20)
|
|
36
|
+
out += `\\u{${cp.toString(16)}}`;
|
|
37
|
+
else
|
|
38
|
+
out += ch;
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
/** `<prefix>: code=<code> msg="<quoted>"` — the tool-side contract error shape. */
|
|
43
|
+
export function contractError(prefix, code, message) {
|
|
44
|
+
return `${prefix}: code=${code} msg="${quoteMessage(message)}"`;
|
|
45
|
+
}
|
|
46
|
+
/** Build a structured failure with the contract message already formatted. */
|
|
47
|
+
export function contractFailure(prefix, code, message) {
|
|
48
|
+
return new ToolFailure(code, contractError(prefix, code, message));
|
|
49
|
+
}
|
|
50
|
+
/** Error text for `ToolOutput.error`; non-Error rejections are stringified. */
|
|
51
|
+
export function errorText(value) {
|
|
52
|
+
if (value instanceof Error)
|
|
53
|
+
return value.message;
|
|
54
|
+
return String(value);
|
|
55
|
+
}
|
|
56
|
+
/** `run_shell-sandbox: code=timeout msg="…"` → `timeout` (for tests/diagnostics). */
|
|
57
|
+
export function errorCode(message) {
|
|
58
|
+
const match = /(?:^|\s)code=([a-z_]+)/.exec(message);
|
|
59
|
+
return match?.[1] ?? null;
|
|
60
|
+
}
|