bare-agent 0.11.0 → 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.
- package/bin/cli.d.ts +4 -0
- package/bin/cli.js +40 -10
- package/bin/test-provider.d.ts +2 -0
- package/bin/test-provider.js +5 -1
- package/index.d.ts +20 -0
- package/package.json +44 -10
- package/src/bareguard-adapter.d.ts +118 -0
- package/src/bareguard-adapter.js +75 -3
- package/src/checkpoint.d.ts +61 -0
- package/src/checkpoint.js +17 -8
- package/src/circuit-breaker.d.ts +70 -0
- package/src/circuit-breaker.js +20 -4
- package/src/errors.d.ts +106 -0
- package/src/errors.js +50 -1
- package/src/loop.d.ts +135 -0
- package/src/loop.js +73 -17
- package/src/mcp-bridge.d.ts +133 -0
- package/src/mcp-bridge.js +179 -27
- package/src/mcp.d.ts +4 -0
- package/src/memory.d.ts +50 -0
- package/src/memory.js +22 -2
- package/src/planner.d.ts +62 -0
- package/src/planner.js +26 -7
- package/src/provider-anthropic.d.ts +55 -0
- package/src/provider-anthropic.js +32 -11
- package/src/provider-clipipe.d.ts +86 -0
- package/src/provider-clipipe.js +28 -18
- package/src/provider-fallback.d.ts +44 -0
- package/src/provider-fallback.js +18 -8
- package/src/provider-ollama.d.ts +41 -0
- package/src/provider-ollama.js +27 -7
- package/src/provider-openai.d.ts +57 -0
- package/src/provider-openai.js +31 -16
- package/src/providers.d.ts +6 -0
- package/src/retry.d.ts +44 -0
- package/src/retry.js +15 -1
- package/src/run-plan.d.ts +126 -0
- package/src/run-plan.js +46 -13
- package/src/scheduler.d.ts +102 -0
- package/src/scheduler.js +32 -4
- package/src/state.d.ts +45 -0
- package/src/state.js +18 -2
- package/src/store-jsonfile.d.ts +85 -0
- package/src/store-jsonfile.js +33 -8
- package/src/store-sqlite.d.ts +90 -0
- package/src/store-sqlite.js +31 -7
- package/src/stores.d.ts +3 -0
- package/src/stream.d.ts +79 -0
- package/src/stream.js +32 -0
- package/src/tools.d.ts +8 -0
- package/src/transport-jsonl.d.ts +30 -0
- package/src/transport-jsonl.js +13 -0
- package/src/transports.d.ts +2 -0
- package/tools/browse.d.ts +10 -0
- package/tools/browse.js +2 -0
- package/tools/defer.d.ts +33 -0
- package/tools/defer.js +12 -3
- package/tools/mobile.d.ts +34 -0
- package/tools/mobile.js +28 -15
- package/tools/shell.d.ts +31 -0
- package/tools/shell.js +55 -6
- package/tools/spawn.d.ts +107 -0
- package/tools/spawn.js +24 -5
- package/types/index.d.ts +66 -0
- package/types/shims.d.ts +16 -0
package/bin/cli.d.ts
ADDED
package/bin/cli.js
CHANGED
|
@@ -36,7 +36,12 @@ const { Loop } = require('../src/loop');
|
|
|
36
36
|
const { Stream } = require('../src/stream');
|
|
37
37
|
const { JsonlTransport } = require('../src/transport-jsonl');
|
|
38
38
|
|
|
39
|
+
/** @typedef {import('../types').Provider} Provider */
|
|
40
|
+
/** @typedef {import('../types').ToolDef} ToolDef */
|
|
41
|
+
/** @typedef {import('../types').Ctx} Ctx */
|
|
42
|
+
|
|
39
43
|
const args = process.argv.slice(2);
|
|
44
|
+
/** @param {string} name */
|
|
40
45
|
const flag = (name) => {
|
|
41
46
|
const i = args.indexOf(`--${name}`);
|
|
42
47
|
return i >= 0 ? args[i + 1] : undefined;
|
|
@@ -45,7 +50,7 @@ const flag = (name) => {
|
|
|
45
50
|
const configPath = flag('config');
|
|
46
51
|
|
|
47
52
|
if (configPath) {
|
|
48
|
-
runConfigMode(configPath).catch((err) => {
|
|
53
|
+
runConfigMode(configPath).catch((/** @type {any} */ err) => {
|
|
49
54
|
process.stdout.write(JSON.stringify({ type: 'loop:error', data: { source: 'cli', error: err.message } }) + '\n');
|
|
50
55
|
process.exit(1);
|
|
51
56
|
});
|
|
@@ -55,6 +60,7 @@ if (configPath) {
|
|
|
55
60
|
|
|
56
61
|
// ─── Mode 2: config-driven ────────────────────────────────────────────────
|
|
57
62
|
|
|
63
|
+
/** @param {string} cfgPath */
|
|
58
64
|
async function runConfigMode(cfgPath) {
|
|
59
65
|
const cfg = readConfig(cfgPath);
|
|
60
66
|
const stream = new Stream({ transport: new JsonlTransport() });
|
|
@@ -68,9 +74,12 @@ async function runConfigMode(cfgPath) {
|
|
|
68
74
|
// Bareguard Gate (optional but strongly recommended for spawn children).
|
|
69
75
|
// Fail-closed: if the config asks for a gate but wiring fails, exit non-zero
|
|
70
76
|
// rather than run an ungoverned child agent.
|
|
71
|
-
|
|
72
|
-
let
|
|
73
|
-
|
|
77
|
+
/** @type {Function | undefined} */
|
|
78
|
+
let policy;
|
|
79
|
+
/** @type {Function | undefined} */
|
|
80
|
+
let onLlmResult;
|
|
81
|
+
/** @type {Function | undefined} */
|
|
82
|
+
let onToolResult;
|
|
74
83
|
let gatedTools = tools;
|
|
75
84
|
if (cfg.gate) {
|
|
76
85
|
try {
|
|
@@ -97,7 +106,7 @@ async function runConfigMode(cfgPath) {
|
|
|
97
106
|
}
|
|
98
107
|
if (typeof humanChannel !== 'function') {
|
|
99
108
|
let warned = false;
|
|
100
|
-
humanChannel = async (event) => {
|
|
109
|
+
humanChannel = async (/** @type {any} */ event) => {
|
|
101
110
|
if (!warned) {
|
|
102
111
|
process.stderr.write(`[cli] no humanChannel configured — ${event.kind} on ${event.rule} auto-denying.\n`);
|
|
103
112
|
warned = true;
|
|
@@ -148,7 +157,7 @@ async function runConfigMode(cfgPath) {
|
|
|
148
157
|
policy,
|
|
149
158
|
onLlmResult,
|
|
150
159
|
onToolResult,
|
|
151
|
-
onError: (err, meta) => {
|
|
160
|
+
onError: (/** @type {any} */ err, /** @type {any} */ meta) => {
|
|
152
161
|
process.stderr.write(`[loop:error ${meta.source}] ${err.message}\n`);
|
|
153
162
|
},
|
|
154
163
|
});
|
|
@@ -158,6 +167,7 @@ async function runConfigMode(cfgPath) {
|
|
|
158
167
|
process.exit(0);
|
|
159
168
|
}
|
|
160
169
|
|
|
170
|
+
/** @param {string} cfgPath */
|
|
161
171
|
function readConfig(cfgPath) {
|
|
162
172
|
const abs = path.resolve(cfgPath);
|
|
163
173
|
let raw;
|
|
@@ -179,6 +189,10 @@ function readStdin() {
|
|
|
179
189
|
});
|
|
180
190
|
}
|
|
181
191
|
|
|
192
|
+
/**
|
|
193
|
+
* @param {any} cfg
|
|
194
|
+
* @param {string} stdin
|
|
195
|
+
*/
|
|
182
196
|
function buildInitialMessage(cfg, stdin) {
|
|
183
197
|
if (!stdin) {
|
|
184
198
|
return { role: 'user', content: cfg.defaultPrompt || 'Begin.' };
|
|
@@ -195,7 +209,13 @@ function buildInitialMessage(cfg, stdin) {
|
|
|
195
209
|
return { role: 'user', content: stdin };
|
|
196
210
|
}
|
|
197
211
|
|
|
212
|
+
/**
|
|
213
|
+
* @param {string[]} names
|
|
214
|
+
* @param {{ stream: InstanceType<typeof Stream> }} ctx
|
|
215
|
+
* @returns {Promise<ToolDef[]>}
|
|
216
|
+
*/
|
|
198
217
|
async function resolveTools(names, ctx) {
|
|
218
|
+
/** @type {ToolDef[]} */
|
|
199
219
|
const tools = [];
|
|
200
220
|
for (const name of names) {
|
|
201
221
|
const resolved = await resolveOneTool(name, ctx);
|
|
@@ -204,6 +224,11 @@ async function resolveTools(names, ctx) {
|
|
|
204
224
|
return tools;
|
|
205
225
|
}
|
|
206
226
|
|
|
227
|
+
/**
|
|
228
|
+
* @param {string} name
|
|
229
|
+
* @param {{ stream: InstanceType<typeof Stream> }} ctx
|
|
230
|
+
* @returns {Promise<ToolDef | ToolDef[] | null>}
|
|
231
|
+
*/
|
|
207
232
|
async function resolveOneTool(name, ctx) {
|
|
208
233
|
switch (name) {
|
|
209
234
|
case 'shell_read':
|
|
@@ -215,16 +240,16 @@ async function resolveOneTool(name, ctx) {
|
|
|
215
240
|
return tools.find(t => t.name === name) || null;
|
|
216
241
|
}
|
|
217
242
|
case 'shell_*': {
|
|
218
|
-
const { createShellTools } = require('../tools/shell');
|
|
219
|
-
return
|
|
243
|
+
const { createShellTools: createShellToolsAll } = require('../tools/shell');
|
|
244
|
+
return createShellToolsAll().tools;
|
|
220
245
|
}
|
|
221
246
|
case 'spawn': {
|
|
222
247
|
const { createSpawnTool } = require('../tools/spawn');
|
|
223
|
-
return createSpawnTool({ stream: ctx.stream }).tool;
|
|
248
|
+
return /** @type {ToolDef} */ (createSpawnTool({ stream: ctx.stream }).tool);
|
|
224
249
|
}
|
|
225
250
|
case 'defer': {
|
|
226
251
|
const { createDeferTool } = require('../tools/defer');
|
|
227
|
-
return createDeferTool().tool;
|
|
252
|
+
return /** @type {ToolDef} */ (createDeferTool().tool);
|
|
228
253
|
}
|
|
229
254
|
default:
|
|
230
255
|
process.stderr.write(`[cli] unknown tool name in config: ${name}\n`);
|
|
@@ -269,6 +294,11 @@ function runStdioMode() {
|
|
|
269
294
|
|
|
270
295
|
// ─── Shared: provider construction ────────────────────────────────────────
|
|
271
296
|
|
|
297
|
+
/**
|
|
298
|
+
* @param {string} name
|
|
299
|
+
* @param {string} [model]
|
|
300
|
+
* @returns {Provider}
|
|
301
|
+
*/
|
|
272
302
|
function createProvider(name, model) {
|
|
273
303
|
if (name === 'openai') {
|
|
274
304
|
const { OpenAIProvider } = require('../src/provider-openai');
|
package/bin/test-provider.js
CHANGED
|
@@ -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.
|
|
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
|
-
".":
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
"./
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
+
};
|
package/src/bareguard-adapter.js
CHANGED
|
@@ -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 {
|
|
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 =>
|
|
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
|
+
}
|
package/src/checkpoint.js
CHANGED
|
@@ -4,16 +4,20 @@ const { TimeoutError } = require('./errors');
|
|
|
4
4
|
|
|
5
5
|
const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {object} CheckpointOptions
|
|
9
|
+
* @property {Array<string>} [tools] - Tool names that require approval (exact match).
|
|
10
|
+
* @property {(toolName: string, args: any) => boolean} [shouldAsk] - Custom predicate — overrides tools list if set.
|
|
11
|
+
* @property {(question: string, context: any) => any} [send] - Async `(question, context) => void` to deliver the question.
|
|
12
|
+
* @property {(context: any) => any} [waitForReply] - Async `(context) => string` that resolves with the user's reply.
|
|
13
|
+
* @property {number} [timeout=300000] - Ms to wait before auto-denying. 0 disables.
|
|
14
|
+
*/
|
|
15
|
+
|
|
7
16
|
class Checkpoint {
|
|
8
17
|
/**
|
|
9
|
-
* @param {
|
|
10
|
-
* @param {Array<string>} [options.tools] - Tool names that require approval (exact match).
|
|
11
|
-
* @param {Function} [options.shouldAsk] - Custom predicate `(toolName, args) => bool` — overrides tools list if set.
|
|
12
|
-
* @param {Function} options.send - Async `(question, context) => void` to deliver the question.
|
|
13
|
-
* @param {Function} options.waitForReply - Async `(context) => string` that resolves with the user's reply.
|
|
14
|
-
* @param {number} [options.timeout=300000] - Ms to wait before auto-denying. 0 disables.
|
|
18
|
+
* @param {CheckpointOptions} [options={}]
|
|
15
19
|
*/
|
|
16
|
-
constructor(options = {}) {
|
|
20
|
+
constructor(options = /** @type {CheckpointOptions} */ ({})) {
|
|
17
21
|
this.tools = new Set(options.tools || []);
|
|
18
22
|
this.send = options.send || null;
|
|
19
23
|
this.waitForReply = options.waitForReply || null;
|
|
@@ -21,6 +25,11 @@ class Checkpoint {
|
|
|
21
25
|
this.timeout = options.timeout !== undefined ? options.timeout : DEFAULT_TIMEOUT_MS;
|
|
22
26
|
}
|
|
23
27
|
|
|
28
|
+
/**
|
|
29
|
+
* @param {string} toolName - Name of the tool being invoked.
|
|
30
|
+
* @param {any} args - Arguments passed to the tool.
|
|
31
|
+
* @returns {boolean} Whether approval should be requested.
|
|
32
|
+
*/
|
|
24
33
|
shouldAsk(toolName, args) {
|
|
25
34
|
if (this.shouldAskFn) return this.shouldAskFn(toolName, args);
|
|
26
35
|
return this.tools.has(toolName);
|
|
@@ -31,7 +40,7 @@ class Checkpoint {
|
|
|
31
40
|
* without a reply — the Loop catches this, auto-denies the tool call, and routes the
|
|
32
41
|
* error through loop:error + onError. No silent hangs.
|
|
33
42
|
* @param {string} question - The approval question to send.
|
|
34
|
-
* @param {
|
|
43
|
+
* @param {{tool?: string, [key: string]: any}} [context={}] - Context passed to send and waitForReply.
|
|
35
44
|
* @returns {Promise<string|null>} The user's reply, or null.
|
|
36
45
|
* @throws {Error} `[Checkpoint] send and waitForReply callbacks required` — when callbacks are missing.
|
|
37
46
|
* @throws {TimeoutError} When no reply arrives within `timeout` ms.
|