@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.
Files changed (140) hide show
  1. package/LICENSE +21 -0
  2. package/dist/args.d.ts +15 -0
  3. package/dist/args.js +55 -0
  4. package/dist/attachments/image-header.d.ts +28 -0
  5. package/dist/attachments/image-header.js +148 -0
  6. package/dist/attachments/store.d.ts +56 -0
  7. package/dist/attachments/store.js +148 -0
  8. package/dist/browser/cdp.d.ts +162 -0
  9. package/dist/browser/cdp.js +269 -0
  10. package/dist/browser/launch.d.ts +92 -0
  11. package/dist/browser/launch.js +232 -0
  12. package/dist/browser/memory-guard.d.ts +55 -0
  13. package/dist/browser/memory-guard.js +210 -0
  14. package/dist/browser/session.d.ts +146 -0
  15. package/dist/browser/session.js +371 -0
  16. package/dist/browser/snapshot.d.ts +69 -0
  17. package/dist/browser/snapshot.js +163 -0
  18. package/dist/browser/types.d.ts +51 -0
  19. package/dist/browser/types.js +8 -0
  20. package/dist/builtin.d.ts +54 -0
  21. package/dist/builtin.js +77 -0
  22. package/dist/desc.d.ts +20 -0
  23. package/dist/desc.js +22 -0
  24. package/dist/disclosure.d.ts +113 -0
  25. package/dist/disclosure.js +141 -0
  26. package/dist/env.d.ts +10 -0
  27. package/dist/env.js +33 -0
  28. package/dist/errors.d.ts +25 -0
  29. package/dist/errors.js +60 -0
  30. package/dist/exposure.d.ts +145 -0
  31. package/dist/exposure.js +244 -0
  32. package/dist/fn-tool.d.ts +14 -0
  33. package/dist/fn-tool.js +14 -0
  34. package/dist/fs/file-io.d.ts +78 -0
  35. package/dist/fs/file-io.js +239 -0
  36. package/dist/guard/path-guard.d.ts +144 -0
  37. package/dist/guard/path-guard.js +289 -0
  38. package/dist/guard/paths.d.ts +35 -0
  39. package/dist/guard/paths.js +100 -0
  40. package/dist/http/errors.d.ts +24 -0
  41. package/dist/http/errors.js +64 -0
  42. package/dist/http/headers.d.ts +19 -0
  43. package/dist/http/headers.js +62 -0
  44. package/dist/http/redirects.d.ts +31 -0
  45. package/dist/http/redirects.js +76 -0
  46. package/dist/http/ssrf.d.ts +105 -0
  47. package/dist/http/ssrf.js +272 -0
  48. package/dist/http/transport.d.ts +50 -0
  49. package/dist/http/transport.js +130 -0
  50. package/dist/index.d.ts +114 -0
  51. package/dist/index.js +129 -0
  52. package/dist/memory/log.d.ts +75 -0
  53. package/dist/memory/log.js +157 -0
  54. package/dist/memory/store.d.ts +47 -0
  55. package/dist/memory/store.js +61 -0
  56. package/dist/platform/exec.d.ts +79 -0
  57. package/dist/platform/exec.js +218 -0
  58. package/dist/platform/index.d.ts +12 -0
  59. package/dist/platform/index.js +12 -0
  60. package/dist/platform/paths.d.ts +51 -0
  61. package/dist/platform/paths.js +60 -0
  62. package/dist/platform/quote.d.ts +72 -0
  63. package/dist/platform/quote.js +102 -0
  64. package/dist/plugin.d.ts +96 -0
  65. package/dist/plugin.js +101 -0
  66. package/dist/process/buffers.d.ts +33 -0
  67. package/dist/process/buffers.js +86 -0
  68. package/dist/process/registry.d.ts +98 -0
  69. package/dist/process/registry.js +282 -0
  70. package/dist/registry.d.ts +52 -0
  71. package/dist/registry.js +161 -0
  72. package/dist/run-code/broker.d.ts +68 -0
  73. package/dist/run-code/broker.js +465 -0
  74. package/dist/run-code/limits.d.ts +69 -0
  75. package/dist/run-code/limits.js +88 -0
  76. package/dist/run-code/lines.d.ts +69 -0
  77. package/dist/run-code/lines.js +199 -0
  78. package/dist/run-code/sdk-ts.d.ts +34 -0
  79. package/dist/run-code/sdk-ts.js +276 -0
  80. package/dist/run-code/sdk.d.ts +39 -0
  81. package/dist/run-code/sdk.js +294 -0
  82. package/dist/sandbox/async.d.ts +10 -0
  83. package/dist/sandbox/async.js +26 -0
  84. package/dist/sandbox/bwrap-argv.d.ts +62 -0
  85. package/dist/sandbox/bwrap-argv.js +113 -0
  86. package/dist/sandbox/bwrap.d.ts +94 -0
  87. package/dist/sandbox/bwrap.js +159 -0
  88. package/dist/sandbox/child.d.ts +38 -0
  89. package/dist/sandbox/child.js +98 -0
  90. package/dist/sandbox/config.d.ts +89 -0
  91. package/dist/sandbox/config.js +149 -0
  92. package/dist/sandbox/fake-sandbox.d.ts +57 -0
  93. package/dist/sandbox/fake-sandbox.js +110 -0
  94. package/dist/sandbox/launch.d.ts +51 -0
  95. package/dist/sandbox/launch.js +134 -0
  96. package/dist/sandbox/limits.d.ts +63 -0
  97. package/dist/sandbox/limits.js +113 -0
  98. package/dist/sandbox/probe.d.ts +46 -0
  99. package/dist/sandbox/probe.js +102 -0
  100. package/dist/sandbox/provider.d.ts +83 -0
  101. package/dist/sandbox/provider.js +126 -0
  102. package/dist/sandbox/rlimit.d.ts +60 -0
  103. package/dist/sandbox/rlimit.js +76 -0
  104. package/dist/sandbox/seccomp.d.ts +48 -0
  105. package/dist/sandbox/seccomp.js +115 -0
  106. package/dist/sandbox/userspace.d.ts +65 -0
  107. package/dist/sandbox/userspace.js +107 -0
  108. package/dist/sandbox/workdir.d.ts +13 -0
  109. package/dist/sandbox/workdir.js +44 -0
  110. package/dist/schema.d.ts +20 -0
  111. package/dist/schema.js +135 -0
  112. package/dist/testing/platform-gates.d.ts +54 -0
  113. package/dist/testing/platform-gates.js +62 -0
  114. package/dist/tool-failure.d.ts +13 -0
  115. package/dist/tool-failure.js +19 -0
  116. package/dist/tools/ask-user.d.ts +32 -0
  117. package/dist/tools/ask-user.js +145 -0
  118. package/dist/tools/browser.d.ts +24 -0
  119. package/dist/tools/browser.js +132 -0
  120. package/dist/tools/http-request.d.ts +31 -0
  121. package/dist/tools/http-request.js +117 -0
  122. package/dist/tools/list-dir.d.ts +9 -0
  123. package/dist/tools/list-dir.js +45 -0
  124. package/dist/tools/load-skill.d.ts +37 -0
  125. package/dist/tools/load-skill.js +76 -0
  126. package/dist/tools/memory.d.ts +46 -0
  127. package/dist/tools/memory.js +131 -0
  128. package/dist/tools/process-control.d.ts +14 -0
  129. package/dist/tools/process-control.js +49 -0
  130. package/dist/tools/read-file.d.ts +11 -0
  131. package/dist/tools/read-file.js +81 -0
  132. package/dist/tools/read-image.d.ts +33 -0
  133. package/dist/tools/read-image.js +144 -0
  134. package/dist/tools/run-code.d.ts +48 -0
  135. package/dist/tools/run-code.js +115 -0
  136. package/dist/tools/run-shell.d.ts +22 -0
  137. package/dist/tools/run-shell.js +81 -0
  138. package/dist/tools/write-file.d.ts +8 -0
  139. package/dist/tools/write-file.js +31 -0
  140. 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[];
@@ -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
+ }
@@ -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
+ }