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.
- package/bin/cli.d.ts +4 -0
- package/bin/cli.js +70 -12
- 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 +80 -18
- package/src/mcp-bridge.d.ts +133 -0
- package/src/mcp-bridge.js +199 -26
- 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 +34 -10
- 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 +29 -7
- package/src/provider-openai.d.ts +57 -0
- package/src/provider-openai.js +34 -7
- 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 +50 -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 +83 -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
|
@@ -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
|
-
|
|
70
|
-
let
|
|
71
|
-
|
|
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
|
-
|
|
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
|
|
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');
|
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
|
+
}
|