bare-agent 0.10.4 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/bin/cli.d.ts +4 -0
  2. package/bin/cli.js +70 -12
  3. package/bin/test-provider.d.ts +2 -0
  4. package/bin/test-provider.js +5 -1
  5. package/index.d.ts +20 -0
  6. package/package.json +44 -10
  7. package/src/bareguard-adapter.d.ts +118 -0
  8. package/src/bareguard-adapter.js +75 -3
  9. package/src/checkpoint.d.ts +61 -0
  10. package/src/checkpoint.js +17 -8
  11. package/src/circuit-breaker.d.ts +70 -0
  12. package/src/circuit-breaker.js +20 -4
  13. package/src/errors.d.ts +106 -0
  14. package/src/errors.js +50 -1
  15. package/src/loop.d.ts +135 -0
  16. package/src/loop.js +80 -18
  17. package/src/mcp-bridge.d.ts +133 -0
  18. package/src/mcp-bridge.js +199 -26
  19. package/src/mcp.d.ts +4 -0
  20. package/src/memory.d.ts +50 -0
  21. package/src/memory.js +22 -2
  22. package/src/planner.d.ts +62 -0
  23. package/src/planner.js +26 -7
  24. package/src/provider-anthropic.d.ts +55 -0
  25. package/src/provider-anthropic.js +34 -10
  26. package/src/provider-clipipe.d.ts +86 -0
  27. package/src/provider-clipipe.js +28 -18
  28. package/src/provider-fallback.d.ts +44 -0
  29. package/src/provider-fallback.js +18 -8
  30. package/src/provider-ollama.d.ts +41 -0
  31. package/src/provider-ollama.js +29 -7
  32. package/src/provider-openai.d.ts +57 -0
  33. package/src/provider-openai.js +34 -7
  34. package/src/providers.d.ts +6 -0
  35. package/src/retry.d.ts +44 -0
  36. package/src/retry.js +15 -1
  37. package/src/run-plan.d.ts +126 -0
  38. package/src/run-plan.js +46 -13
  39. package/src/scheduler.d.ts +102 -0
  40. package/src/scheduler.js +32 -4
  41. package/src/state.d.ts +45 -0
  42. package/src/state.js +18 -2
  43. package/src/store-jsonfile.d.ts +85 -0
  44. package/src/store-jsonfile.js +50 -8
  45. package/src/store-sqlite.d.ts +90 -0
  46. package/src/store-sqlite.js +31 -7
  47. package/src/stores.d.ts +3 -0
  48. package/src/stream.d.ts +79 -0
  49. package/src/stream.js +32 -0
  50. package/src/tools.d.ts +8 -0
  51. package/src/transport-jsonl.d.ts +30 -0
  52. package/src/transport-jsonl.js +13 -0
  53. package/src/transports.d.ts +2 -0
  54. package/tools/browse.d.ts +10 -0
  55. package/tools/browse.js +2 -0
  56. package/tools/defer.d.ts +33 -0
  57. package/tools/defer.js +12 -3
  58. package/tools/mobile.d.ts +34 -0
  59. package/tools/mobile.js +28 -15
  60. package/tools/shell.d.ts +31 -0
  61. package/tools/shell.js +83 -6
  62. package/tools/spawn.d.ts +107 -0
  63. package/tools/spawn.js +24 -5
  64. package/types/index.d.ts +66 -0
  65. package/types/shims.d.ts +16 -0
package/bin/cli.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ export type Provider = import("../types").Provider;
3
+ export type ToolDef = import("../types").ToolDef;
4
+ export type Ctx = import("../types").Ctx;
package/bin/cli.js CHANGED
@@ -23,7 +23,9 @@
23
23
  * "provider": "openai" | "anthropic" | "ollama",
24
24
  * "model": "gpt-4o-mini" (etc),
25
25
  * "tools": ["shell_read", "shell_grep", "spawn", "defer", ...],
26
- * "gate": { ...bareguard config; humanChannel headless-defaults to deny }
26
+ * "gate": { ...bareguard config; humanChannel headless-defaults to deny },
27
+ * "ungoverned": false // omit/false ⇒ a config with no `gate` is refused;
28
+ * // set true to explicitly run without governance (not recommended)
27
29
  * }
28
30
  */
29
31
 
@@ -34,7 +36,12 @@ const { Loop } = require('../src/loop');
34
36
  const { Stream } = require('../src/stream');
35
37
  const { JsonlTransport } = require('../src/transport-jsonl');
36
38
 
39
+ /** @typedef {import('../types').Provider} Provider */
40
+ /** @typedef {import('../types').ToolDef} ToolDef */
41
+ /** @typedef {import('../types').Ctx} Ctx */
42
+
37
43
  const args = process.argv.slice(2);
44
+ /** @param {string} name */
38
45
  const flag = (name) => {
39
46
  const i = args.indexOf(`--${name}`);
40
47
  return i >= 0 ? args[i + 1] : undefined;
@@ -43,7 +50,7 @@ const flag = (name) => {
43
50
  const configPath = flag('config');
44
51
 
45
52
  if (configPath) {
46
- runConfigMode(configPath).catch((err) => {
53
+ runConfigMode(configPath).catch((/** @type {any} */ err) => {
47
54
  process.stdout.write(JSON.stringify({ type: 'loop:error', data: { source: 'cli', error: err.message } }) + '\n');
48
55
  process.exit(1);
49
56
  });
@@ -53,6 +60,7 @@ if (configPath) {
53
60
 
54
61
  // ─── Mode 2: config-driven ────────────────────────────────────────────────
55
62
 
63
+ /** @param {string} cfgPath */
56
64
  async function runConfigMode(cfgPath) {
57
65
  const cfg = readConfig(cfgPath);
58
66
  const stream = new Stream({ transport: new JsonlTransport() });
@@ -66,9 +74,12 @@ async function runConfigMode(cfgPath) {
66
74
  // Bareguard Gate (optional but strongly recommended for spawn children).
67
75
  // Fail-closed: if the config asks for a gate but wiring fails, exit non-zero
68
76
  // rather than run an ungoverned child agent.
69
- let policy = null;
70
- let onLlmResult = null;
71
- let onToolResult = null;
77
+ /** @type {Function | undefined} */
78
+ let policy;
79
+ /** @type {Function | undefined} */
80
+ let onLlmResult;
81
+ /** @type {Function | undefined} */
82
+ let onToolResult;
72
83
  let gatedTools = tools;
73
84
  if (cfg.gate) {
74
85
  try {
@@ -81,12 +92,21 @@ async function runConfigMode(cfgPath) {
81
92
  let humanChannel = cfg.gate.humanChannel;
82
93
  if (typeof humanChannel === 'string') {
83
94
  // Allow `humanChannel: "./my-channel.js"` — load from a file relative to config.
84
- const fnPath = path.resolve(path.dirname(cfgPath), humanChannel);
95
+ // Confine the resolved path to the config directory: a JSON config (data)
96
+ // must not be able to require() arbitrary code elsewhere on disk (e.g.
97
+ // "../../evil.js"), which would execute outside the gate.
98
+ const cfgDir = path.resolve(path.dirname(cfgPath));
99
+ const fnPath = path.resolve(cfgDir, humanChannel);
100
+ if (fnPath !== cfgDir && !fnPath.startsWith(cfgDir + path.sep)) {
101
+ throw new Error(
102
+ `gate.humanChannel must resolve inside the config directory (${cfgDir}); refusing to load ${fnPath}`,
103
+ );
104
+ }
85
105
  humanChannel = require(fnPath);
86
106
  }
87
107
  if (typeof humanChannel !== 'function') {
88
108
  let warned = false;
89
- humanChannel = async (event) => {
109
+ humanChannel = async (/** @type {any} */ event) => {
90
110
  if (!warned) {
91
111
  process.stderr.write(`[cli] no humanChannel configured — ${event.kind} on ${event.rule} auto-denying.\n`);
92
112
  warned = true;
@@ -106,6 +126,23 @@ async function runConfigMode(cfgPath) {
106
126
  process.stderr.write(`[cli] failed to wire bareguard: ${err.message}. Refusing to run ungoverned (cfg.gate set).\n`);
107
127
  process.exit(1);
108
128
  }
129
+ } else if (cfg.ungoverned === true) {
130
+ // Explicit opt-out. A config-driven / spawned agent runs with no policy,
131
+ // budget, depth, or rate limits — every configured tool executes unchecked.
132
+ process.stderr.write(
133
+ '[cli] WARNING: running UNGOVERNED (cfg.ungoverned=true) — no policy/budget/depth/rate limits. ' +
134
+ 'All configured tools run unchecked.\n',
135
+ );
136
+ } else {
137
+ // Fail-closed: a config with no `gate` is rejected rather than silently run
138
+ // ungoverned. This is the path the LLM-callable `spawn` tool drives — without
139
+ // it, a gate-less child config bypasses all governance (and recursive spawn is
140
+ // unbounded, since maxDepth is only enforced by a wired Gate).
141
+ process.stderr.write(
142
+ '[cli] refusing to run: config has no `gate` block. A config-driven / spawned agent must be governed.\n' +
143
+ ' Add a bareguard `gate` config, or set `"ungoverned": true` to explicitly opt out (not recommended).\n',
144
+ );
145
+ process.exit(1);
109
146
  }
110
147
 
111
148
  // Read ONE input record from stdin (JSON or raw string). Treat blank stdin
@@ -120,7 +157,7 @@ async function runConfigMode(cfgPath) {
120
157
  policy,
121
158
  onLlmResult,
122
159
  onToolResult,
123
- onError: (err, meta) => {
160
+ onError: (/** @type {any} */ err, /** @type {any} */ meta) => {
124
161
  process.stderr.write(`[loop:error ${meta.source}] ${err.message}\n`);
125
162
  },
126
163
  });
@@ -130,6 +167,7 @@ async function runConfigMode(cfgPath) {
130
167
  process.exit(0);
131
168
  }
132
169
 
170
+ /** @param {string} cfgPath */
133
171
  function readConfig(cfgPath) {
134
172
  const abs = path.resolve(cfgPath);
135
173
  let raw;
@@ -151,6 +189,10 @@ function readStdin() {
151
189
  });
152
190
  }
153
191
 
192
+ /**
193
+ * @param {any} cfg
194
+ * @param {string} stdin
195
+ */
154
196
  function buildInitialMessage(cfg, stdin) {
155
197
  if (!stdin) {
156
198
  return { role: 'user', content: cfg.defaultPrompt || 'Begin.' };
@@ -167,7 +209,13 @@ function buildInitialMessage(cfg, stdin) {
167
209
  return { role: 'user', content: stdin };
168
210
  }
169
211
 
212
+ /**
213
+ * @param {string[]} names
214
+ * @param {{ stream: InstanceType<typeof Stream> }} ctx
215
+ * @returns {Promise<ToolDef[]>}
216
+ */
170
217
  async function resolveTools(names, ctx) {
218
+ /** @type {ToolDef[]} */
171
219
  const tools = [];
172
220
  for (const name of names) {
173
221
  const resolved = await resolveOneTool(name, ctx);
@@ -176,6 +224,11 @@ async function resolveTools(names, ctx) {
176
224
  return tools;
177
225
  }
178
226
 
227
+ /**
228
+ * @param {string} name
229
+ * @param {{ stream: InstanceType<typeof Stream> }} ctx
230
+ * @returns {Promise<ToolDef | ToolDef[] | null>}
231
+ */
179
232
  async function resolveOneTool(name, ctx) {
180
233
  switch (name) {
181
234
  case 'shell_read':
@@ -187,16 +240,16 @@ async function resolveOneTool(name, ctx) {
187
240
  return tools.find(t => t.name === name) || null;
188
241
  }
189
242
  case 'shell_*': {
190
- const { createShellTools } = require('../tools/shell');
191
- return createShellTools().tools;
243
+ const { createShellTools: createShellToolsAll } = require('../tools/shell');
244
+ return createShellToolsAll().tools;
192
245
  }
193
246
  case 'spawn': {
194
247
  const { createSpawnTool } = require('../tools/spawn');
195
- return createSpawnTool({ stream: ctx.stream }).tool;
248
+ return /** @type {ToolDef} */ (createSpawnTool({ stream: ctx.stream }).tool);
196
249
  }
197
250
  case 'defer': {
198
251
  const { createDeferTool } = require('../tools/defer');
199
- return createDeferTool().tool;
252
+ return /** @type {ToolDef} */ (createDeferTool().tool);
200
253
  }
201
254
  default:
202
255
  process.stderr.write(`[cli] unknown tool name in config: ${name}\n`);
@@ -241,6 +294,11 @@ function runStdioMode() {
241
294
 
242
295
  // ─── Shared: provider construction ────────────────────────────────────────
243
296
 
297
+ /**
298
+ * @param {string} name
299
+ * @param {string} [model]
300
+ * @returns {Provider}
301
+ */
244
302
  function createProvider(name, model) {
245
303
  if (name === 'openai') {
246
304
  const { OpenAIProvider } = require('../src/provider-openai');
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export type Provider = import("../types").Provider;
@@ -1,7 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
 
4
+ /** @typedef {import('../types').Provider} Provider */
5
+
4
6
  const args = process.argv.slice(2);
7
+ /** @param {string} name */
5
8
  const flag = (name) => {
6
9
  const i = args.indexOf(`--${name}`);
7
10
  return i >= 0 ? args[i + 1] : undefined;
@@ -10,6 +13,7 @@ const flag = (name) => {
10
13
  const providerName = flag('provider') || 'openai';
11
14
  const model = flag('model');
12
15
 
16
+ /** @returns {Provider} */
13
17
  function createProvider() {
14
18
  if (providerName === 'openai') {
15
19
  const { OpenAIProvider } = require('../src/provider-openai');
@@ -21,7 +25,7 @@ function createProvider() {
21
25
  if (providerName === 'anthropic') {
22
26
  const { AnthropicProvider } = require('../src/provider-anthropic');
23
27
  return new AnthropicProvider({
24
- apiKey: process.env.ANTHROPIC_API_KEY,
28
+ apiKey: process.env.ANTHROPIC_API_KEY || '',
25
29
  ...(model && { model }),
26
30
  });
27
31
  }
package/index.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ import { Loop } from "./src/loop";
2
+ import { Planner } from "./src/planner";
3
+ import { StateMachine } from "./src/state";
4
+ import { Scheduler } from "./src/scheduler";
5
+ import { Checkpoint } from "./src/checkpoint";
6
+ import { Memory } from "./src/memory";
7
+ import { Stream } from "./src/stream";
8
+ import { Retry } from "./src/retry";
9
+ import { runPlan } from "./src/run-plan";
10
+ import { CircuitBreaker } from "./src/circuit-breaker";
11
+ import { wireGate } from "./src/bareguard-adapter";
12
+ import { defaultActionTranslator } from "./src/bareguard-adapter";
13
+ import { BareAgentError } from "./src/errors";
14
+ import { ProviderError } from "./src/errors";
15
+ import { ToolError } from "./src/errors";
16
+ import { TimeoutError } from "./src/errors";
17
+ import { ValidationError } from "./src/errors";
18
+ import { CircuitOpenError } from "./src/errors";
19
+ import { HaltError } from "./src/errors";
20
+ export { Loop, Planner, StateMachine, Scheduler, Checkpoint, Memory, Stream, Retry, runPlan, CircuitBreaker, wireGate, defaultActionTranslator, BareAgentError, ProviderError, ToolError, TimeoutError, ValidationError, CircuitOpenError, HaltError };
package/package.json CHANGED
@@ -1,11 +1,13 @@
1
1
  {
2
2
  "name": "bare-agent",
3
- "version": "0.10.4",
3
+ "version": "0.12.0",
4
4
  "files": [
5
5
  "index.js",
6
+ "index.d.ts",
6
7
  "src/",
7
8
  "bin/",
8
9
  "tools/",
10
+ "types/",
9
11
  "LICENSE",
10
12
  "NOTICE"
11
13
  ],
@@ -17,18 +19,43 @@
17
19
  "url": "git+https://github.com/hamr0/bareagent.git"
18
20
  },
19
21
  "main": "index.js",
22
+ "types": "./index.d.ts",
20
23
  "bin": {
21
24
  "bare-agent": "bin/cli.js"
22
25
  },
23
26
  "exports": {
24
- ".": "./index.js",
25
- "./errors": "./src/errors.js",
26
- "./providers": "./src/providers.js",
27
- "./stores": "./src/stores.js",
28
- "./transports": "./src/transports.js",
29
- "./tools": "./src/tools.js",
30
- "./mcp": "./src/mcp.js",
31
- "./bareguard": "./src/bareguard-adapter.js",
27
+ ".": {
28
+ "types": "./index.d.ts",
29
+ "default": "./index.js"
30
+ },
31
+ "./errors": {
32
+ "types": "./src/errors.d.ts",
33
+ "default": "./src/errors.js"
34
+ },
35
+ "./providers": {
36
+ "types": "./src/providers.d.ts",
37
+ "default": "./src/providers.js"
38
+ },
39
+ "./stores": {
40
+ "types": "./src/stores.d.ts",
41
+ "default": "./src/stores.js"
42
+ },
43
+ "./transports": {
44
+ "types": "./src/transports.d.ts",
45
+ "default": "./src/transports.js"
46
+ },
47
+ "./tools": {
48
+ "types": "./src/tools.d.ts",
49
+ "default": "./src/tools.js"
50
+ },
51
+ "./mcp": {
52
+ "types": "./src/mcp.d.ts",
53
+ "default": "./src/mcp.js"
54
+ },
55
+ "./bareguard": {
56
+ "types": "./src/bareguard-adapter.d.ts",
57
+ "default": "./src/bareguard-adapter.js"
58
+ },
32
59
  "./package.json": "./package.json"
33
60
  },
34
61
  "engines": {
@@ -62,6 +89,13 @@
62
89
  }
63
90
  },
64
91
  "scripts": {
65
- "test": "node --test --test-force-exit test/**/*.test.js"
92
+ "test": "node --test --test-force-exit test/**/*.test.js",
93
+ "typecheck": "tsc --noEmit",
94
+ "build:types": "tsc",
95
+ "prepublishOnly": "npm run build:types"
96
+ },
97
+ "devDependencies": {
98
+ "@types/node": "^22.19.19",
99
+ "typescript": "^5.7.0"
66
100
  }
67
101
  }
@@ -0,0 +1,118 @@
1
+ export type Ctx = import("../types").Ctx;
2
+ export type ToolDef = import("../types").ToolDef;
3
+ export type Usage = import("../types").Usage;
4
+ /**
5
+ * A bareguard Gate instance. Comes from the ambient `bareguard` module, so its
6
+ * methods are accessed structurally here.
7
+ */
8
+ export type Gate = {
9
+ check: (action: any) => (GateDecision | Promise<GateDecision>);
10
+ record: (action: any, outcome?: any) => any;
11
+ allows?: ((toolName: string) => (boolean | Promise<boolean>)) | undefined;
12
+ };
13
+ /**
14
+ * A decision returned by `gate.check`.
15
+ */
16
+ export type GateDecision = {
17
+ /**
18
+ * - 'allow' when permitted.
19
+ */
20
+ outcome?: string | undefined;
21
+ /**
22
+ * - 'halt' for halt-severity denials.
23
+ */
24
+ severity?: string | undefined;
25
+ /**
26
+ * - The matched rule name.
27
+ */
28
+ rule?: string | undefined;
29
+ /**
30
+ * - Human-readable reason.
31
+ */
32
+ reason?: string | undefined;
33
+ /**
34
+ * - Arbitrary structured context.
35
+ */
36
+ context?: Record<string, any> | undefined;
37
+ };
38
+ /**
39
+ * Wire a bareguard Gate into bareagent's Loop.
40
+ *
41
+ * Returns:
42
+ * - `policy` — async (toolName, args, ctx) closure for `new Loop({ policy })`.
43
+ * Allow → true; deny → tagged reason string; halt → throws HaltError.
44
+ * - `onLlmResult` — callback for `new Loop({ onLlmResult })`. Forwards every
45
+ * provider.generate result to gate.record as a `{type:'llm'}` action
46
+ * so `budget.maxCostUsd` covers token-only workloads.
47
+ * - `onToolResult` — callback for `new Loop({ onToolResult })`. Forwards every
48
+ * tool.execute result to gate.record with ctx in scope.
49
+ * - `filterTools` — async (tools) => filtered. Drops tools denied by gate.allows
50
+ * so the LLM never sees them. No audit, no record. Bulk-only:
51
+ * when MCP tools are exposed via `mcp_discover`+`mcp_invoke`
52
+ * meta-tools, filterTools cannot drop the inner names (they
53
+ * are not in the tool list). Gate those via bareguard's
54
+ * `tools.denyArgPatterns: { mcp_invoke: [/"name":"…"/] }`
55
+ * — see src/mcp-bridge.js (Gov shape).
56
+ * - `wrapTool` / `wrapTools` — DEPRECATED. Pre-BA1 shim that wraps execute() to
57
+ * call gate.record post-hoc. Loses _ctx and never sees LLM cost.
58
+ * Prefer `onToolResult` (and `onLlmResult` for budget correctness).
59
+ *
60
+ * Halt-severity decisions (budget exhausted, limits.maxTurns hit, gate terminated)
61
+ * throw HaltError from the policy closure; Loop catches it and exits cleanly with
62
+ * loop:error{source:'halt'} + loop:done — the deny is NOT fed back to the LLM.
63
+ *
64
+ * @param {Gate} gate - A bareguard Gate instance (must have .check, .record, .allows).
65
+ * @param {object} [options]
66
+ * @param {Function} [options.formatDeny] - (decision, toolName) => string. Transforms
67
+ * the deny string fed to the LLM. The second arg is the bareagent tool name (handy
68
+ * for tool-specific deny copy). Default: "[deny: <rule>] <reason>" or
69
+ * "[deny: <rule>] <toolName> denied" when bareguard omits a reason. Halt bypasses
70
+ * this (HaltError doesn't reach the LLM).
71
+ * @param {Function} [options.actionTranslator] - (toolName, args, ctx) => action.
72
+ * Builds the action object passed to `gate.check` and `gate.record`. Default:
73
+ * `{ type: toolName, args, _ctx: ctx }`. Override when bareguard's primitives
74
+ * need a specific shape — e.g. `bashCheck` requires `{type:'bash', cmd:...}`,
75
+ * `fsCheck` requires `{type:'read'|'write'|'edit', path:...}`. The default shape
76
+ * matches `tools.denylist` / `tools.allowlist` (which read `action.type`) but
77
+ * does NOT activate `bash`/`fs`/`net` primitives — those need their own
78
+ * `action.type` value. Adopters using those primitives must translate.
79
+ * @returns {{policy: Function, onLlmResult: Function, onToolResult: Function, filterTools: Function, wrapTool: Function, wrapTools: Function}}
80
+ *
81
+ * @example
82
+ * const { Gate } = require('bareguard');
83
+ * const { Loop } = require('bare-agent');
84
+ * const { wireGate } = require('bare-agent/bareguard');
85
+ *
86
+ * const gate = new Gate({
87
+ * budget: { maxCostUsd: 0.50 },
88
+ * limits: { maxTurns: 20 },
89
+ * audit: { path: './audit.jsonl' },
90
+ * });
91
+ * await gate.init();
92
+ *
93
+ * const { policy, onLlmResult, onToolResult, filterTools } = wireGate(gate);
94
+ * const loop = new Loop({ provider, policy, onLlmResult, onToolResult });
95
+ * const tools = await filterTools(myTools);
96
+ * await loop.run(messages, tools);
97
+ */
98
+ export function wireGate(gate: Gate, options?: {
99
+ formatDeny?: Function | undefined;
100
+ actionTranslator?: Function | undefined;
101
+ }): {
102
+ policy: Function;
103
+ onLlmResult: Function;
104
+ onToolResult: Function;
105
+ filterTools: Function;
106
+ wrapTool: Function;
107
+ wrapTools: Function;
108
+ };
109
+ /**
110
+ * @param {string} toolName
111
+ * @param {any} args
112
+ * @param {Ctx} ctx
113
+ */
114
+ export function defaultActionTranslator(toolName: string, args: any, ctx: Ctx): {
115
+ type: string;
116
+ args: any;
117
+ _ctx: any;
118
+ };
@@ -2,10 +2,34 @@
2
2
 
3
3
  const { HaltError } = require('./errors');
4
4
 
5
+ /** @typedef {import('../types').Ctx} Ctx */
6
+ /** @typedef {import('../types').ToolDef} ToolDef */
7
+ /** @typedef {import('../types').Usage} Usage */
8
+
9
+ /**
10
+ * A bareguard Gate instance. Comes from the ambient `bareguard` module, so its
11
+ * methods are accessed structurally here.
12
+ * @typedef {object} Gate
13
+ * @property {(action: any) => (GateDecision | Promise<GateDecision>)} check
14
+ * @property {(action: any, outcome?: any) => any} record
15
+ * @property {(toolName: string) => (boolean | Promise<boolean>)} [allows]
16
+ */
17
+
18
+ /**
19
+ * A decision returned by `gate.check`.
20
+ * @typedef {object} GateDecision
21
+ * @property {string} [outcome] - 'allow' when permitted.
22
+ * @property {string} [severity] - 'halt' for halt-severity denials.
23
+ * @property {string} [rule] - The matched rule name.
24
+ * @property {string} [reason] - Human-readable reason.
25
+ * @property {Record<string, any>} [context] - Arbitrary structured context.
26
+ */
27
+
5
28
  // Safe-stringify for tool results: tools can return circular structures or
6
29
  // values that include functions / undefined / bigints. Falling back to String()
7
30
  // keeps gate.record from throwing inside onToolResult (which would surface as a
8
31
  // loop:error{source:'onToolResult'} for what is really a serialization quirk).
32
+ /** @param {any} value */
9
33
  function safeStringify(value) {
10
34
  if (typeof value === 'string') return value;
11
35
  try {
@@ -46,7 +70,7 @@ let warnedWrap = false;
46
70
  * throw HaltError from the policy closure; Loop catches it and exits cleanly with
47
71
  * loop:error{source:'halt'} + loop:done — the deny is NOT fed back to the LLM.
48
72
  *
49
- * @param {object} gate - A bareguard Gate instance (must have .check, .record, .allows).
73
+ * @param {Gate} gate - A bareguard Gate instance (must have .check, .record, .allows).
50
74
  * @param {object} [options]
51
75
  * @param {Function} [options.formatDeny] - (decision, toolName) => string. Transforms
52
76
  * the deny string fed to the LLM. The second arg is the bareagent tool name (handy
@@ -93,6 +117,11 @@ function wireGate(gate, options = {}) {
93
117
  const formatDeny = options.formatDeny || defaultFormatDeny;
94
118
  const translate = options.actionTranslator || defaultActionTranslator;
95
119
 
120
+ /**
121
+ * @param {string} toolName
122
+ * @param {any} args
123
+ * @param {Ctx} ctx
124
+ */
96
125
  const policy = async (toolName, args, ctx) => {
97
126
  const decision = await gate.check(translate(toolName, args, ctx));
98
127
  if (decision.outcome === 'allow') return true;
@@ -105,6 +134,15 @@ function wireGate(gate, options = {}) {
105
134
  return formatDeny(decision, toolName);
106
135
  };
107
136
 
137
+ /**
138
+ * @param {object} arg
139
+ * @param {string|null} [arg.model]
140
+ * @param {string|null} [arg.provider]
141
+ * @param {Usage} [arg.usage]
142
+ * @param {number} [arg.costUsd]
143
+ * @param {number|null} [arg.durationMs]
144
+ * @param {Ctx} [arg.ctx]
145
+ */
108
146
  const onLlmResult = async ({ model, provider, usage, costUsd, durationMs, ctx }) => {
109
147
  // LLM rounds bypass actionTranslator — they always use the canonical
110
148
  // {type:'llm'} action so budget rules can match without translator collusion.
@@ -118,6 +156,15 @@ function wireGate(gate, options = {}) {
118
156
  );
119
157
  };
120
158
 
159
+ /**
160
+ * @param {object} arg
161
+ * @param {string} arg.name
162
+ * @param {any} [arg.args]
163
+ * @param {any} [arg.result]
164
+ * @param {Error|null} [arg.error]
165
+ * @param {number|null} [arg.durationMs]
166
+ * @param {Ctx} [arg.ctx]
167
+ */
121
168
  const onToolResult = async ({ name, args, result, error, durationMs, ctx }) => {
122
169
  const action = translate(name, args, ctx);
123
170
  if (error) {
@@ -133,6 +180,10 @@ function wireGate(gate, options = {}) {
133
180
  }
134
181
  };
135
182
 
183
+ /**
184
+ * @param {ToolDef[]} tools
185
+ * @returns {Promise<ToolDef[]>}
186
+ */
136
187
  const filterTools = async (tools) => {
137
188
  if (!Array.isArray(tools)) {
138
189
  throw new Error('[wireGate.filterTools] expects an array of tools');
@@ -140,13 +191,20 @@ function wireGate(gate, options = {}) {
140
191
  if (typeof gate.allows !== 'function') {
141
192
  throw new Error('[wireGate.filterTools] gate must have .allows (bareguard >= 0.2)');
142
193
  }
194
+ // Bind to the Gate so `this` stays correct (bareguard's allows reads
195
+ // this._initialized) — extracting the method unbound would crash.
196
+ const allows = gate.allows.bind(gate);
143
197
  // Parallel: gate.allows is config-driven and pure, so concurrent calls are
144
198
  // safe. Matters for large MCP catalogs (50+ tools) where sequential awaits
145
199
  // were noticeable overhead on every startup.
146
- const verdicts = await Promise.all(tools.map(t => gate.allows(t.name)));
200
+ const verdicts = await Promise.all(tools.map(t => allows(t.name)));
147
201
  return tools.filter((_, i) => verdicts[i]);
148
202
  };
149
203
 
204
+ /**
205
+ * @param {ToolDef} tool
206
+ * @returns {ToolDef}
207
+ */
150
208
  function wrapTool(tool) {
151
209
  if (!warnedWrap) {
152
210
  warnedWrap = true;
@@ -161,7 +219,7 @@ function wireGate(gate, options = {}) {
161
219
  const original = tool.execute;
162
220
  return {
163
221
  ...tool,
164
- execute: async (args) => {
222
+ execute: async (/** @type {any} */ args) => {
165
223
  const action = { type: tool.name, args };
166
224
  const startedAt = Date.now();
167
225
  try {
@@ -182,6 +240,10 @@ function wireGate(gate, options = {}) {
182
240
  };
183
241
  }
184
242
 
243
+ /**
244
+ * @param {ToolDef[]} tools
245
+ * @returns {ToolDef[]}
246
+ */
185
247
  function wrapTools(tools) {
186
248
  if (!Array.isArray(tools)) {
187
249
  throw new Error('[wireGate.wrapTools] expects an array of tools');
@@ -192,6 +254,11 @@ function wireGate(gate, options = {}) {
192
254
  return { policy, onLlmResult, onToolResult, filterTools, wrapTool, wrapTools };
193
255
  }
194
256
 
257
+ /**
258
+ * @param {GateDecision} decision
259
+ * @param {string} toolName
260
+ * @returns {string}
261
+ */
195
262
  function defaultFormatDeny(decision, toolName) {
196
263
  const tag = `[deny: ${decision.rule}]`;
197
264
  return decision.reason ? `${tag} ${decision.reason}` : `${tag} ${toolName} denied`;
@@ -202,6 +269,11 @@ function defaultFormatDeny(decision, toolName) {
202
269
  // does NOT activate `bash`/`fs`/`net` primitives — those require `action.type`
203
270
  // to be `bash`/`read`/`write`/etc. and read fields like `action.cmd` /
204
271
  // `action.path` at the top level. Override via `wireGate(gate, { actionTranslator })`.
272
+ /**
273
+ * @param {string} toolName
274
+ * @param {any} args
275
+ * @param {Ctx} ctx
276
+ */
205
277
  function defaultActionTranslator(toolName, args, ctx) {
206
278
  return { type: toolName, args, _ctx: ctx ?? null };
207
279
  }
@@ -0,0 +1,61 @@
1
+ export type CheckpointOptions = {
2
+ /**
3
+ * - Tool names that require approval (exact match).
4
+ */
5
+ tools?: string[] | undefined;
6
+ /**
7
+ * - Custom predicate — overrides tools list if set.
8
+ */
9
+ shouldAsk?: ((toolName: string, args: any) => boolean) | undefined;
10
+ /**
11
+ * - Async `(question, context) => void` to deliver the question.
12
+ */
13
+ send?: ((question: string, context: any) => any) | undefined;
14
+ /**
15
+ * - Async `(context) => string` that resolves with the user's reply.
16
+ */
17
+ waitForReply?: ((context: any) => any) | undefined;
18
+ /**
19
+ * - Ms to wait before auto-denying. 0 disables.
20
+ */
21
+ timeout?: number | undefined;
22
+ };
23
+ /**
24
+ * @typedef {object} CheckpointOptions
25
+ * @property {Array<string>} [tools] - Tool names that require approval (exact match).
26
+ * @property {(toolName: string, args: any) => boolean} [shouldAsk] - Custom predicate — overrides tools list if set.
27
+ * @property {(question: string, context: any) => any} [send] - Async `(question, context) => void` to deliver the question.
28
+ * @property {(context: any) => any} [waitForReply] - Async `(context) => string` that resolves with the user's reply.
29
+ * @property {number} [timeout=300000] - Ms to wait before auto-denying. 0 disables.
30
+ */
31
+ export class Checkpoint {
32
+ /**
33
+ * @param {CheckpointOptions} [options={}]
34
+ */
35
+ constructor(options?: CheckpointOptions);
36
+ tools: Set<string>;
37
+ send: ((question: string, context: any) => any) | null;
38
+ waitForReply: ((context: any) => any) | null;
39
+ shouldAskFn: ((toolName: string, args: any) => boolean) | null;
40
+ timeout: number;
41
+ /**
42
+ * @param {string} toolName - Name of the tool being invoked.
43
+ * @param {any} args - Arguments passed to the tool.
44
+ * @returns {boolean} Whether approval should be requested.
45
+ */
46
+ shouldAsk(toolName: string, args: any): boolean;
47
+ /**
48
+ * Send a question and wait for a reply. Rejects with TimeoutError if `timeout` ms elapse
49
+ * without a reply — the Loop catches this, auto-denies the tool call, and routes the
50
+ * error through loop:error + onError. No silent hangs.
51
+ * @param {string} question - The approval question to send.
52
+ * @param {{tool?: string, [key: string]: any}} [context={}] - Context passed to send and waitForReply.
53
+ * @returns {Promise<string|null>} The user's reply, or null.
54
+ * @throws {Error} `[Checkpoint] send and waitForReply callbacks required` — when callbacks are missing.
55
+ * @throws {TimeoutError} When no reply arrives within `timeout` ms.
56
+ */
57
+ ask(question: string, context?: {
58
+ tool?: string;
59
+ [key: string]: any;
60
+ }): Promise<string | null>;
61
+ }