@addai/node 0.4.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/README.md +96 -0
- package/dist/agent-auth.d.ts +9 -0
- package/dist/agent-auth.js +96 -0
- package/dist/attachments.d.ts +20 -0
- package/dist/attachments.js +164 -0
- package/dist/capabilities.d.ts +64 -0
- package/dist/capabilities.js +406 -0
- package/dist/claude-binary.d.ts +16 -0
- package/dist/claude-binary.js +145 -0
- package/dist/claude-config.d.ts +11 -0
- package/dist/claude-config.js +144 -0
- package/dist/claude-print.d.ts +35 -0
- package/dist/claude-print.js +312 -0
- package/dist/claude-spawn.d.ts +40 -0
- package/dist/claude-spawn.js +220 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +137 -0
- package/dist/codex-binary.d.ts +4 -0
- package/dist/codex-binary.js +70 -0
- package/dist/codex-spawn.d.ts +47 -0
- package/dist/codex-spawn.js +320 -0
- package/dist/command-runner.d.ts +9 -0
- package/dist/command-runner.js +592 -0
- package/dist/config.d.ts +7 -0
- package/dist/config.js +38 -0
- package/dist/control-server.d.ts +5 -0
- package/dist/control-server.js +292 -0
- package/dist/diskguard.d.ts +96 -0
- package/dist/diskguard.js +435 -0
- package/dist/error-classify.d.ts +20 -0
- package/dist/error-classify.js +159 -0
- package/dist/events.d.ts +26 -0
- package/dist/events.js +97 -0
- package/dist/gemini-binary.d.ts +4 -0
- package/dist/gemini-binary.js +64 -0
- package/dist/gemini-spawn.d.ts +52 -0
- package/dist/gemini-spawn.js +369 -0
- package/dist/git-identity.d.ts +18 -0
- package/dist/git-identity.js +104 -0
- package/dist/grok-binary.d.ts +4 -0
- package/dist/grok-binary.js +70 -0
- package/dist/grok-spawn.d.ts +41 -0
- package/dist/grok-spawn.js +495 -0
- package/dist/guardrails.d.ts +32 -0
- package/dist/guardrails.js +125 -0
- package/dist/hang-watchdog.d.ts +17 -0
- package/dist/hang-watchdog.js +48 -0
- package/dist/harness-registry.d.ts +55 -0
- package/dist/harness-registry.js +180 -0
- package/dist/heartbeat.d.ts +13 -0
- package/dist/heartbeat.js +95 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +440 -0
- package/dist/install.d.ts +63 -0
- package/dist/install.js +467 -0
- package/dist/jsonl-tail.d.ts +20 -0
- package/dist/jsonl-tail.js +192 -0
- package/dist/kimi-binary.d.ts +4 -0
- package/dist/kimi-binary.js +68 -0
- package/dist/kimi-spawn.d.ts +47 -0
- package/dist/kimi-spawn.js +319 -0
- package/dist/lockfile.d.ts +30 -0
- package/dist/lockfile.js +169 -0
- package/dist/mcp-config.d.ts +23 -0
- package/dist/mcp-config.js +133 -0
- package/dist/memory-capture.d.ts +20 -0
- package/dist/memory-capture.js +45 -0
- package/dist/memory-pack.d.ts +34 -0
- package/dist/memory-pack.js +171 -0
- package/dist/model-filter.d.ts +4 -0
- package/dist/model-filter.js +34 -0
- package/dist/node-health.d.ts +18 -0
- package/dist/node-health.js +74 -0
- package/dist/pairing.d.ts +20 -0
- package/dist/pairing.js +129 -0
- package/dist/paths.d.ts +9 -0
- package/dist/paths.js +48 -0
- package/dist/precompact-hook.d.ts +27 -0
- package/dist/precompact-hook.js +136 -0
- package/dist/projects.d.ts +25 -0
- package/dist/projects.js +255 -0
- package/dist/prompt-keystrokes.d.ts +3 -0
- package/dist/prompt-keystrokes.js +56 -0
- package/dist/pty-helper.d.ts +26 -0
- package/dist/pty-helper.js +156 -0
- package/dist/request-pump.d.ts +12 -0
- package/dist/request-pump.js +249 -0
- package/dist/run-retry.d.ts +28 -0
- package/dist/run-retry.js +81 -0
- package/dist/self-update.d.ts +75 -0
- package/dist/self-update.js +192 -0
- package/dist/session-runner.d.ts +130 -0
- package/dist/session-runner.js +2271 -0
- package/dist/sleep-detector.d.ts +11 -0
- package/dist/sleep-detector.js +32 -0
- package/dist/state-migrate.d.ts +8 -0
- package/dist/state-migrate.js +91 -0
- package/dist/store.d.ts +5 -0
- package/dist/store.js +82 -0
- package/dist/supabase-client.d.ts +15 -0
- package/dist/supabase-client.js +119 -0
- package/dist/time-context.d.ts +10 -0
- package/dist/time-context.js +71 -0
- package/dist/tokens.d.ts +4 -0
- package/dist/tokens.js +33 -0
- package/dist/tui/app.d.ts +35 -0
- package/dist/tui/app.js +82 -0
- package/dist/tui/console-capture.d.ts +13 -0
- package/dist/tui/console-capture.js +62 -0
- package/dist/tui/dashboard.d.ts +42 -0
- package/dist/tui/dashboard.js +205 -0
- package/dist/tui/data.d.ts +57 -0
- package/dist/tui/data.js +62 -0
- package/dist/tui/harnesses.d.ts +16 -0
- package/dist/tui/harnesses.js +354 -0
- package/dist/tui/render.d.ts +21 -0
- package/dist/tui/render.js +116 -0
- package/dist/tui/requests.d.ts +18 -0
- package/dist/tui/requests.js +127 -0
- package/dist/tui/run.d.ts +13 -0
- package/dist/tui/run.js +178 -0
- package/dist/tui/transcript.d.ts +39 -0
- package/dist/tui/transcript.js +234 -0
- package/dist/tui.d.ts +1 -0
- package/dist/tui.js +314 -0
- package/dist/types.d.ts +65 -0
- package/dist/types.js +3 -0
- package/dist/win.d.ts +67 -0
- package/dist/win.js +317 -0
- package/package.json +60 -0
- package/scripts/fix-pty-helper.js +28 -0
- package/scripts/precompact-capture.js +292 -0
- package/scripts/smoke-test.sh +74 -0
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// grok -p --output-format streaming-json adapter.
|
|
3
|
+
//
|
|
4
|
+
// Mirrors claude-print.ts / kimi-spawn.ts: spawns `grok` non-interactively
|
|
5
|
+
// with a JSONL output stream, parses each line into our normalised
|
|
6
|
+
// RuntimeEvent shape, and resolves on exit.
|
|
7
|
+
//
|
|
8
|
+
// Two things make grok different from the other agents and drive the
|
|
9
|
+
// design here:
|
|
10
|
+
//
|
|
11
|
+
// 1. No `--mcp-config` flag. Grok discovers MCP servers from
|
|
12
|
+
// `config.toml` (user + project scope), `~/.claude.json`, and
|
|
13
|
+
// `.mcp.json` — the Claude-Code compatibility layer. To give an
|
|
14
|
+
// entity ONLY its own servers we (a) write the entity's servers to a
|
|
15
|
+
// per-session project `<cwd>/.grok/config.toml`, and (b) run grok
|
|
16
|
+
// under a sandbox $HOME whose `.claude.json` is empty, so the host's
|
|
17
|
+
// global Claude MCPs don't leak in. The sandbox HOME symlinks every
|
|
18
|
+
// other real-home entry so git / npm / ssh still find their configs
|
|
19
|
+
// (the lesson from kimi: never point HOME at an empty dir).
|
|
20
|
+
//
|
|
21
|
+
// 2. The account lives in `~/.grok/auth.json` and its OAuth token is
|
|
22
|
+
// refreshed in place. So GROK_HOME stays pointed at the REAL
|
|
23
|
+
// `~/.grok` — the entity uses the same logged-in account the user
|
|
24
|
+
// set up, and token refresh keeps working — while only $HOME is
|
|
25
|
+
// sandboxed.
|
|
26
|
+
//
|
|
27
|
+
// grok's streaming-json emits `thought` (reasoning), `text` (assistant
|
|
28
|
+
// output) and a final `end` (usage + sessionId). It does not surface
|
|
29
|
+
// per-tool events, so unlike claude we can't render individual tool
|
|
30
|
+
// bubbles; unknown types are forwarded as `grok:<type>` for forward-compat.
|
|
31
|
+
//
|
|
32
|
+
// Reference: grok --help, ~/.grok/README.md (Claude Code Compatibility).
|
|
33
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
34
|
+
if (k2 === undefined) k2 = k;
|
|
35
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
36
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
37
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
38
|
+
}
|
|
39
|
+
Object.defineProperty(o, k2, desc);
|
|
40
|
+
}) : (function(o, m, k, k2) {
|
|
41
|
+
if (k2 === undefined) k2 = k;
|
|
42
|
+
o[k2] = m[k];
|
|
43
|
+
}));
|
|
44
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
45
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
46
|
+
}) : function(o, v) {
|
|
47
|
+
o["default"] = v;
|
|
48
|
+
});
|
|
49
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
50
|
+
var ownKeys = function(o) {
|
|
51
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
52
|
+
var ar = [];
|
|
53
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
54
|
+
return ar;
|
|
55
|
+
};
|
|
56
|
+
return ownKeys(o);
|
|
57
|
+
};
|
|
58
|
+
return function (mod) {
|
|
59
|
+
if (mod && mod.__esModule) return mod;
|
|
60
|
+
var result = {};
|
|
61
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
62
|
+
__setModuleDefault(result, mod);
|
|
63
|
+
return result;
|
|
64
|
+
};
|
|
65
|
+
})();
|
|
66
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
67
|
+
exports.spawnGrok = spawnGrok;
|
|
68
|
+
const child_process_1 = require("child_process");
|
|
69
|
+
const fs = __importStar(require("fs"));
|
|
70
|
+
const os = __importStar(require("os"));
|
|
71
|
+
const path = __importStar(require("path"));
|
|
72
|
+
const grok_binary_1 = require("./grok-binary");
|
|
73
|
+
const win_1 = require("./win");
|
|
74
|
+
const events_1 = require("./events");
|
|
75
|
+
/** Real config dir for the logged-in account. GROK_HOME points here so the
|
|
76
|
+
* entity uses the same account and token refresh works. */
|
|
77
|
+
function realGrokHome() {
|
|
78
|
+
return path.join(os.homedir(), '.grok');
|
|
79
|
+
}
|
|
80
|
+
// Home entries that are MCP / harness discovery sources for grok's
|
|
81
|
+
// Claude-Code compatibility layer. Masked in the sandbox so an entity only
|
|
82
|
+
// sees its own servers, never the host's global Claude/Cursor MCPs.
|
|
83
|
+
const MASKED_HOME_ENTRIES = new Set(['.claude.json', '.claude', '.mcp.json', '.cursor']);
|
|
84
|
+
/** Build a sandbox $HOME: bridge every real-home entry (so git/npm/ssh
|
|
85
|
+
* keep working) EXCEPT the Claude/Cursor MCP sources, then drop an empty
|
|
86
|
+
* `.claude.json` so host MCPs resolve to nothing. Caller cleans it up.
|
|
87
|
+
*
|
|
88
|
+
* POSIX: plain symlinks. win32: directory junctions (work unprivileged)
|
|
89
|
+
* and symlink-or-copy for files (file symlinks need admin/Dev Mode). */
|
|
90
|
+
function writeGrokSandboxHome() {
|
|
91
|
+
const home = os.homedir();
|
|
92
|
+
const sandbox = fs.mkdtempSync(path.join(os.tmpdir(), 'entities-grok-home-'));
|
|
93
|
+
let entries = [];
|
|
94
|
+
try {
|
|
95
|
+
entries = fs.readdirSync(home);
|
|
96
|
+
}
|
|
97
|
+
catch { /* best-effort */ }
|
|
98
|
+
for (const name of entries) {
|
|
99
|
+
if (MASKED_HOME_ENTRIES.has(name))
|
|
100
|
+
continue;
|
|
101
|
+
const real = path.join(home, name);
|
|
102
|
+
const dest = path.join(sandbox, name);
|
|
103
|
+
if (!win_1.IS_WINDOWS) {
|
|
104
|
+
try {
|
|
105
|
+
fs.symlinkSync(real, dest);
|
|
106
|
+
}
|
|
107
|
+
catch { /* skip */ }
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
if (fs.statSync(real).isDirectory())
|
|
112
|
+
(0, win_1.linkDir)(real, dest);
|
|
113
|
+
else
|
|
114
|
+
(0, win_1.linkOrCopyFile)(real, dest);
|
|
115
|
+
}
|
|
116
|
+
catch { /* unreadable entry — skip */ }
|
|
117
|
+
}
|
|
118
|
+
// Empty Claude config → no host MCPs leak through the compat layer.
|
|
119
|
+
try {
|
|
120
|
+
fs.writeFileSync(path.join(sandbox, '.claude.json'), JSON.stringify({ mcpServers: {} }), { mode: 0o600 });
|
|
121
|
+
}
|
|
122
|
+
catch { /* non-fatal */ }
|
|
123
|
+
return sandbox;
|
|
124
|
+
}
|
|
125
|
+
/** Env overrides pointing the child's "home" at the sandbox. POSIX tools
|
|
126
|
+
* read HOME; on Windows the profile is USERPROFILE (+ HOMEDRIVE/HOMEPATH),
|
|
127
|
+
* and HOME alone would change nothing. */
|
|
128
|
+
function sandboxHomeEnv(sandboxHome) {
|
|
129
|
+
const env = { HOME: sandboxHome };
|
|
130
|
+
if (win_1.IS_WINDOWS) {
|
|
131
|
+
env.USERPROFILE = sandboxHome;
|
|
132
|
+
const parsed = path.parse(sandboxHome);
|
|
133
|
+
if (parsed.root) {
|
|
134
|
+
env.HOMEDRIVE = parsed.root.replace(/[\\/]+$/, '');
|
|
135
|
+
env.HOMEPATH = sandboxHome.slice(env.HOMEDRIVE.length) || '\\';
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return env;
|
|
139
|
+
}
|
|
140
|
+
/** Escape a string for a TOML double-quoted value. */
|
|
141
|
+
function tomlStr(s) {
|
|
142
|
+
return '"' + s.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n') + '"';
|
|
143
|
+
}
|
|
144
|
+
/** Write the entity's MCP servers as a project-scoped `<cwd>/.grok/config.toml`.
|
|
145
|
+
* Project scope only supports `[mcp_servers]`, which is exactly what we need.
|
|
146
|
+
* Returns the config path (for logging) or null when there are no servers. */
|
|
147
|
+
function writeProjectMcpConfig(workingDirectory, servers) {
|
|
148
|
+
const usable = servers.filter(s => s.slug && s.command);
|
|
149
|
+
if (usable.length === 0) {
|
|
150
|
+
// No usable servers: remove any stale config.toml from a prior turn.
|
|
151
|
+
// Returning null WITHOUT deleting leaves grok loading the old servers
|
|
152
|
+
// (project-scoped config persists in the cwd across turns).
|
|
153
|
+
try {
|
|
154
|
+
fs.rmSync(path.join(workingDirectory, '.grok', 'config.toml'), { force: true });
|
|
155
|
+
}
|
|
156
|
+
catch { /* nothing to remove */ }
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
const lines = [];
|
|
160
|
+
for (const s of usable) {
|
|
161
|
+
// `cmd /c` wrapper for npm-shim commands on native Windows (no-op on POSIX).
|
|
162
|
+
const wrapped = (0, win_1.wrapMcpCommandForPlatform)(s.command, Array.isArray(s.args) ? s.args.map(String) : []);
|
|
163
|
+
lines.push(`[mcp_servers.${tomlStr(s.slug)}]`);
|
|
164
|
+
lines.push(`command = ${tomlStr(wrapped.command)}`);
|
|
165
|
+
if (wrapped.args.length > 0) {
|
|
166
|
+
lines.push(`args = [${wrapped.args.map(a => tomlStr(a)).join(', ')}]`);
|
|
167
|
+
}
|
|
168
|
+
if (s.env && Object.keys(s.env).length > 0) {
|
|
169
|
+
const kv = Object.entries(s.env)
|
|
170
|
+
.filter(([, v]) => typeof v === 'string' && v.length > 0)
|
|
171
|
+
.map(([k, v]) => `${/^[A-Za-z0-9_]+$/.test(k) ? k : tomlStr(k)} = ${tomlStr(String(v))}`)
|
|
172
|
+
.join(', ');
|
|
173
|
+
lines.push(`env = { ${kv} }`);
|
|
174
|
+
}
|
|
175
|
+
lines.push('enabled = true');
|
|
176
|
+
// Cap MCP startup so a slow/broken server fails the tool instead of
|
|
177
|
+
// hanging the whole agent (the "thinking forever" class of bug), but
|
|
178
|
+
// allow enough for a cold `npx` fetch of the server package.
|
|
179
|
+
lines.push('startup_timeout_sec = 30');
|
|
180
|
+
lines.push('');
|
|
181
|
+
}
|
|
182
|
+
const dir = path.join(workingDirectory, '.grok');
|
|
183
|
+
try {
|
|
184
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
185
|
+
const target = path.join(dir, 'config.toml');
|
|
186
|
+
fs.writeFileSync(target, lines.join('\n'), { mode: 0o600 });
|
|
187
|
+
return target;
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/** grok issues its own session ids as UUIDv7 (time-ordered, version nibble
|
|
194
|
+
* '7', e.g. 019f60ef-6aa4-7fd2-...). A conversation's stored session id may
|
|
195
|
+
* come from a DIFFERENT agent (claude etc. use UUIDv4) or the chat framework.
|
|
196
|
+
* Passing such an id to `grok --resume` makes grok try to restore it from its
|
|
197
|
+
* remote registry, get a 404, and exit 1 — which is why grok failed on
|
|
198
|
+
* chatflows continuation turns. Only resume when the id is plausibly a grok
|
|
199
|
+
* session; otherwise start fresh (grok couldn't resume a foreign session
|
|
200
|
+
* anyway). Degrades safely: an unrecognised id just means a new session. */
|
|
201
|
+
function isGrokSessionId(id) {
|
|
202
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id);
|
|
203
|
+
}
|
|
204
|
+
/** Map our effort levels to grok's `--reasoning-effort` accepted values. */
|
|
205
|
+
function grokEffort(level) {
|
|
206
|
+
switch (level) {
|
|
207
|
+
case 'low': return 'low';
|
|
208
|
+
case 'medium': return 'medium';
|
|
209
|
+
case 'high':
|
|
210
|
+
case 'xhigh':
|
|
211
|
+
case 'max': return 'high';
|
|
212
|
+
default: return null;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/** Parse one streaming-json line into 0+ RuntimeEvents. grok emits
|
|
216
|
+
* `thought` (reasoning tokens), `text` (assistant output deltas), a final
|
|
217
|
+
* `end` (usage + sessionId), and `error`. Tool calls happen internally and
|
|
218
|
+
* are not surfaced as discrete events. Anything unrecognised is forwarded
|
|
219
|
+
* as `grok:<type>`. */
|
|
220
|
+
function lineToEvents(line) {
|
|
221
|
+
const type = typeof line.type === 'string' ? line.type : '';
|
|
222
|
+
const data = typeof line.data === 'string' ? line.data : '';
|
|
223
|
+
// Assistant output deltas.
|
|
224
|
+
if (type === 'text') {
|
|
225
|
+
return data ? [{ type: 'assistant_text', delta: data }] : [];
|
|
226
|
+
}
|
|
227
|
+
// Reasoning tokens — normalized 'thinking' deltas (coalesced below like
|
|
228
|
+
// text). Kept out of the visible answer; the chat relay aggregates them
|
|
229
|
+
// into a collapsible "thinking" attachment. The old per-token
|
|
230
|
+
// `grok:thought` passthrough flooded the events table (~1 row/token).
|
|
231
|
+
if (type === 'thought') {
|
|
232
|
+
return data ? [{ type: 'thinking', delta: data }] : [];
|
|
233
|
+
}
|
|
234
|
+
// Forward-compat: if a future grok build surfaces tool events, map them.
|
|
235
|
+
if (type === 'tool_use' || type === 'tool_call') {
|
|
236
|
+
return [{
|
|
237
|
+
type: 'tool_use',
|
|
238
|
+
id: String(line.id ?? line.toolCallId ?? ''),
|
|
239
|
+
name: String(line.name ?? line.tool ?? ''),
|
|
240
|
+
input: line.input ?? line.args ?? line.arguments,
|
|
241
|
+
}];
|
|
242
|
+
}
|
|
243
|
+
if (type === 'tool_result') {
|
|
244
|
+
return [{
|
|
245
|
+
type: 'tool_result',
|
|
246
|
+
id: String(line.id ?? line.toolCallId ?? ''),
|
|
247
|
+
content: line.content ?? line.result ?? line.output,
|
|
248
|
+
isError: line.isError === true || line.is_error === true,
|
|
249
|
+
}];
|
|
250
|
+
}
|
|
251
|
+
// Turn / session end.
|
|
252
|
+
if (type === 'end' || type === 'result' || type === 'turn_complete') {
|
|
253
|
+
return [{
|
|
254
|
+
type: 'turn_complete',
|
|
255
|
+
usage: line.usage ?? undefined,
|
|
256
|
+
stopReason: typeof line.stopReason === 'string'
|
|
257
|
+
? line.stopReason
|
|
258
|
+
: (typeof line.stop_reason === 'string' ? line.stop_reason : undefined),
|
|
259
|
+
}];
|
|
260
|
+
}
|
|
261
|
+
// Errors.
|
|
262
|
+
if (type === 'error' || type === 'turn_failed') {
|
|
263
|
+
const message = line.error?.message
|
|
264
|
+
?? (typeof line.message === 'string' ? line.message : 'grok error');
|
|
265
|
+
return [
|
|
266
|
+
{ type: 'error', code: 'grok_error', message },
|
|
267
|
+
{ type: 'turn_complete', stopReason: 'failed' },
|
|
268
|
+
];
|
|
269
|
+
}
|
|
270
|
+
return [{ type: `grok:${type || 'unknown'}`, raw: line }];
|
|
271
|
+
}
|
|
272
|
+
function spawnGrok(input) {
|
|
273
|
+
const bin = (0, grok_binary_1.findGrokBinary)();
|
|
274
|
+
if (!bin) {
|
|
275
|
+
// Surface a "grok not installed" error via the event stream so the
|
|
276
|
+
// chat fails cleanly instead of hanging. bufferedEvents delivers even
|
|
277
|
+
// though the runner attaches its listener after an await.
|
|
278
|
+
const events = (0, events_1.bufferedEvents)([
|
|
279
|
+
{ type: 'error', code: 'grok_not_installed', message: 'grok CLI is not installed on this runtime. Install it from https://grok.com and run `grok login`.' },
|
|
280
|
+
{ type: 'turn_complete', stopReason: 'failed' },
|
|
281
|
+
]);
|
|
282
|
+
return {
|
|
283
|
+
pid: undefined,
|
|
284
|
+
sessionId: null,
|
|
285
|
+
kill: () => { },
|
|
286
|
+
onEvent: events.onEvent,
|
|
287
|
+
onActivity: () => { },
|
|
288
|
+
done: Promise.resolve(127),
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
const sandboxHome = writeGrokSandboxHome();
|
|
292
|
+
writeProjectMcpConfig(input.workingDirectory, input.mcpServers ?? []);
|
|
293
|
+
const args = [
|
|
294
|
+
'-p', input.prompt,
|
|
295
|
+
'--output-format', 'streaming-json',
|
|
296
|
+
'--cwd', input.workingDirectory,
|
|
297
|
+
// Autonomous entity: no human to answer approvals in headless mode.
|
|
298
|
+
'--always-approve',
|
|
299
|
+
// Trust the working folder so its repo-local MCP servers (the entity's
|
|
300
|
+
// <cwd>/.grok/config.toml) actually START. Without this, grok's
|
|
301
|
+
// folder-trust gate silently skips repo-local MCP servers in a headless
|
|
302
|
+
// untrusted dir, and the agent can't call its tools.
|
|
303
|
+
'--trust',
|
|
304
|
+
];
|
|
305
|
+
// Only resume genuine grok sessions — a foreign (e.g. claude UUIDv4) id
|
|
306
|
+
// would make grok 404 on remote restore and exit 1.
|
|
307
|
+
if (input.resumeSessionId && isGrokSessionId(input.resumeSessionId)) {
|
|
308
|
+
args.push('--resume', input.resumeSessionId);
|
|
309
|
+
}
|
|
310
|
+
if (input.model) {
|
|
311
|
+
args.push('--model', input.model);
|
|
312
|
+
}
|
|
313
|
+
const effort = grokEffort(input.effortLevel);
|
|
314
|
+
if (effort) {
|
|
315
|
+
args.push('--reasoning-effort', effort);
|
|
316
|
+
}
|
|
317
|
+
// grok appends `--rules` on top of its base system prompt — same
|
|
318
|
+
// semantics as claude `--append-system-prompt`.
|
|
319
|
+
if (input.appendSystemPrompt && input.appendSystemPrompt.trim().length > 0) {
|
|
320
|
+
args.push('--rules', input.appendSystemPrompt.trim());
|
|
321
|
+
}
|
|
322
|
+
const childEnv = {
|
|
323
|
+
...process.env,
|
|
324
|
+
...sandboxHomeEnv(sandboxHome), // masks host ~/.claude.json MCPs (HOME; +USERPROFILE on win32)
|
|
325
|
+
GROK_HOME: realGrokHome(), // real account + live token refresh
|
|
326
|
+
TERM: 'dumb',
|
|
327
|
+
};
|
|
328
|
+
let proc;
|
|
329
|
+
try {
|
|
330
|
+
// npm .cmd shim → node script (win32); identity on POSIX. Both this
|
|
331
|
+
// resolution and spawn() itself can throw synchronously on Windows —
|
|
332
|
+
// fail the request cleanly instead of crashing the daemon.
|
|
333
|
+
const inv = (0, win_1.resolveCliInvocation)(bin, args);
|
|
334
|
+
proc = (0, child_process_1.spawn)(inv.file, inv.args, {
|
|
335
|
+
cwd: input.workingDirectory,
|
|
336
|
+
env: childEnv,
|
|
337
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
catch (err) {
|
|
341
|
+
const msg = err.message || String(err);
|
|
342
|
+
try {
|
|
343
|
+
fs.rmSync(sandboxHome, { recursive: true, force: true });
|
|
344
|
+
}
|
|
345
|
+
catch { }
|
|
346
|
+
const events = (0, events_1.bufferedEvents)([
|
|
347
|
+
{ type: 'error', code: 'grok_spawn_failed', message: msg },
|
|
348
|
+
{ type: 'turn_complete', stopReason: 'failed' },
|
|
349
|
+
]);
|
|
350
|
+
return {
|
|
351
|
+
pid: undefined,
|
|
352
|
+
sessionId: null,
|
|
353
|
+
kill: () => { },
|
|
354
|
+
onEvent: events.onEvent,
|
|
355
|
+
onActivity: () => { },
|
|
356
|
+
done: Promise.resolve(-1),
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
const listeners = [];
|
|
360
|
+
const emit = (e) => { for (const l of listeners)
|
|
361
|
+
try {
|
|
362
|
+
l(e);
|
|
363
|
+
}
|
|
364
|
+
catch { } };
|
|
365
|
+
let sessionId = null;
|
|
366
|
+
let buffer = '';
|
|
367
|
+
// Coalesce assistant text. grok streams one `text` event PER TOKEN — a
|
|
368
|
+
// short reply is 30-40 events. Emitting each one as its own row floods
|
|
369
|
+
// the event table AND, because the daemon inserts events fire-and-forget,
|
|
370
|
+
// rapid per-token rows race and land out of order (the reply renders
|
|
371
|
+
// scrambled). Buffering deltas into a handful of well-spaced chunks fixes
|
|
372
|
+
// both: fewer rows, and enough spacing that inserts don't overlap.
|
|
373
|
+
const FLUSH_MS = 90;
|
|
374
|
+
const MAX_BUF = 320;
|
|
375
|
+
// Thinking flushes lazily (bigger chunks, longer window) — it's collapsed
|
|
376
|
+
// in the UI, so latency doesn't matter; fewer rows do.
|
|
377
|
+
const THINK_FLUSH_MS = 700;
|
|
378
|
+
const THINK_MAX_BUF = 1500;
|
|
379
|
+
let textBuf = '';
|
|
380
|
+
let thinkBuf = '';
|
|
381
|
+
let flushTimer = null;
|
|
382
|
+
let thinkTimer = null;
|
|
383
|
+
const flushThinking = () => {
|
|
384
|
+
if (thinkTimer) {
|
|
385
|
+
clearTimeout(thinkTimer);
|
|
386
|
+
thinkTimer = null;
|
|
387
|
+
}
|
|
388
|
+
if (thinkBuf) {
|
|
389
|
+
const delta = thinkBuf;
|
|
390
|
+
thinkBuf = '';
|
|
391
|
+
emit({ type: 'thinking', delta });
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
const flushText = () => {
|
|
395
|
+
if (flushTimer) {
|
|
396
|
+
clearTimeout(flushTimer);
|
|
397
|
+
flushTimer = null;
|
|
398
|
+
}
|
|
399
|
+
if (textBuf) {
|
|
400
|
+
const delta = textBuf;
|
|
401
|
+
textBuf = '';
|
|
402
|
+
emit({ type: 'assistant_text', delta });
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
const emitCoalesced = (e) => {
|
|
406
|
+
if (e.type === 'assistant_text') {
|
|
407
|
+
// Thoughts precede the text they produced — flush them first.
|
|
408
|
+
flushThinking();
|
|
409
|
+
textBuf += e.delta;
|
|
410
|
+
if (textBuf.length >= MAX_BUF)
|
|
411
|
+
flushText();
|
|
412
|
+
else if (!flushTimer)
|
|
413
|
+
flushTimer = setTimeout(flushText, FLUSH_MS);
|
|
414
|
+
}
|
|
415
|
+
else if (e.type === 'thinking') {
|
|
416
|
+
thinkBuf += e.delta;
|
|
417
|
+
if (thinkBuf.length >= THINK_MAX_BUF)
|
|
418
|
+
flushThinking();
|
|
419
|
+
else if (!thinkTimer)
|
|
420
|
+
thinkTimer = setTimeout(flushThinking, THINK_FLUSH_MS);
|
|
421
|
+
}
|
|
422
|
+
else {
|
|
423
|
+
// Any other event must appear AFTER the stream so far — flush both.
|
|
424
|
+
flushThinking();
|
|
425
|
+
flushText();
|
|
426
|
+
emit(e);
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
const activityListeners = [];
|
|
430
|
+
const touchActivity = () => { for (const l of activityListeners)
|
|
431
|
+
try {
|
|
432
|
+
l();
|
|
433
|
+
}
|
|
434
|
+
catch { } };
|
|
435
|
+
proc.stdout?.on('data', (chunk) => {
|
|
436
|
+
touchActivity();
|
|
437
|
+
buffer += chunk.toString('utf8');
|
|
438
|
+
let nl;
|
|
439
|
+
while ((nl = buffer.indexOf('\n')) !== -1) {
|
|
440
|
+
const raw = buffer.slice(0, nl).trim();
|
|
441
|
+
buffer = buffer.slice(nl + 1);
|
|
442
|
+
if (!raw)
|
|
443
|
+
continue;
|
|
444
|
+
let parsed;
|
|
445
|
+
try {
|
|
446
|
+
parsed = JSON.parse(raw);
|
|
447
|
+
}
|
|
448
|
+
catch {
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
if (!sessionId) {
|
|
452
|
+
const sid = parsed.sessionId ?? parsed.session_id;
|
|
453
|
+
if (typeof sid === 'string')
|
|
454
|
+
sessionId = sid;
|
|
455
|
+
}
|
|
456
|
+
for (const ev of lineToEvents(parsed))
|
|
457
|
+
emitCoalesced(ev);
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
proc.stderr?.on('data', (chunk) => {
|
|
461
|
+
touchActivity();
|
|
462
|
+
const txt = chunk.toString('utf8').trim();
|
|
463
|
+
if (txt)
|
|
464
|
+
emit({ type: 'error', code: 'grok_stderr', message: txt.slice(0, 4096) });
|
|
465
|
+
});
|
|
466
|
+
const done = new Promise((resolve, reject) => {
|
|
467
|
+
proc.once('error', (err) => {
|
|
468
|
+
// Async spawn failure (ENOENT/EACCES from a stale cached binary path):
|
|
469
|
+
// the exit handler never fires, so clean up the sandbox HOME here too —
|
|
470
|
+
// otherwise it leaks (on Windows we copy files up to 8MB into it).
|
|
471
|
+
try {
|
|
472
|
+
fs.rmSync(sandboxHome, { recursive: true, force: true });
|
|
473
|
+
}
|
|
474
|
+
catch { }
|
|
475
|
+
reject(err);
|
|
476
|
+
});
|
|
477
|
+
proc.once('exit', (code) => {
|
|
478
|
+
flushThinking(); // emit buffered tails before we resolve
|
|
479
|
+
flushText();
|
|
480
|
+
try {
|
|
481
|
+
fs.rmSync(sandboxHome, { recursive: true, force: true });
|
|
482
|
+
}
|
|
483
|
+
catch { }
|
|
484
|
+
resolve(code ?? -1);
|
|
485
|
+
});
|
|
486
|
+
});
|
|
487
|
+
return {
|
|
488
|
+
pid: proc.pid,
|
|
489
|
+
get sessionId() { return sessionId; },
|
|
490
|
+
kill: () => { (0, win_1.killProcessTree)(proc); },
|
|
491
|
+
onEvent: (cb) => { listeners.push(cb); },
|
|
492
|
+
onActivity: (cb) => { activityListeners.push(cb); },
|
|
493
|
+
done,
|
|
494
|
+
};
|
|
495
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface GuardrailDef {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
rule: string;
|
|
5
|
+
mode: 'fast' | 'runtime';
|
|
6
|
+
action: 'retry' | 'block' | 'flag';
|
|
7
|
+
max_retries: number;
|
|
8
|
+
}
|
|
9
|
+
export interface GuardrailVerdict {
|
|
10
|
+
id: string;
|
|
11
|
+
verdict: 'pass' | 'fail';
|
|
12
|
+
reasoning: string;
|
|
13
|
+
checker_error?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface GuardrailIo {
|
|
16
|
+
loadGuardrails(): Promise<GuardrailDef[]>;
|
|
17
|
+
fetchOutput(): Promise<string | null>;
|
|
18
|
+
checkFast(output: string, defs: GuardrailDef[]): Promise<GuardrailVerdict[]>;
|
|
19
|
+
checkRuntime(output: string, def: GuardrailDef): Promise<GuardrailVerdict>;
|
|
20
|
+
/** Continuation turn on the same session. null = retry unavailable. */
|
|
21
|
+
runRetry(feedback: string): Promise<{
|
|
22
|
+
ok: boolean;
|
|
23
|
+
text: string;
|
|
24
|
+
} | null>;
|
|
25
|
+
setResult(meta: Record<string, unknown>, events: Array<Record<string, unknown>>): Promise<void>;
|
|
26
|
+
emit(type: string, payload: unknown): Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
export declare function buildRetryFeedback(failures: Array<{
|
|
29
|
+
def: GuardrailDef;
|
|
30
|
+
verdict: GuardrailVerdict;
|
|
31
|
+
}>): string;
|
|
32
|
+
export declare function enforceGuardrails(io: GuardrailIo): Promise<void>;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Guardrails — post-request checks an entity's output passes through before
|
|
3
|
+
// the daemon writes the terminal `completed` status. Holding the terminal
|
|
4
|
+
// status is the whole enforcement mechanism: every delivery path (bot-egress,
|
|
5
|
+
// flows resume, chatflows queue promotion) triggers on terminal status, so
|
|
6
|
+
// nothing is delivered until enforceGuardrails resolves.
|
|
7
|
+
//
|
|
8
|
+
// The state machine is pure over an injected IO surface (GuardrailIo) so it
|
|
9
|
+
// is unit-testable without a network; session-runner.ts supplies the real IO
|
|
10
|
+
// (RPCs + edge fn + retry spawn). Contract: enforceGuardrails NEVER throws
|
|
11
|
+
// and fails OPEN — a broken checker degrades to `flag`, never to a blocked
|
|
12
|
+
// or wedged reply.
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.buildRetryFeedback = buildRetryFeedback;
|
|
15
|
+
exports.enforceGuardrails = enforceGuardrails;
|
|
16
|
+
// Global ceiling on revision rounds regardless of per-guardrail max_retries —
|
|
17
|
+
// bounds worst-case latency added before delivery.
|
|
18
|
+
const GLOBAL_MAX_RETRY_ROUNDS = 3;
|
|
19
|
+
function buildRetryFeedback(failures) {
|
|
20
|
+
const lines = failures.map(f => `- "${f.def.name}": ${f.def.rule}${f.verdict.reasoning ? ` (violation: ${f.verdict.reasoning})` : ''}`);
|
|
21
|
+
return [
|
|
22
|
+
`Your previous response violated ${failures.length === 1 ? 'a guardrail' : `${failures.length} guardrails`}:`,
|
|
23
|
+
...lines,
|
|
24
|
+
'',
|
|
25
|
+
'Revise your response so it fully complies with every guardrail above.',
|
|
26
|
+
'Reply ONLY with the corrected response — do not mention the guardrails, this revision process, or apologize for the previous version.',
|
|
27
|
+
].join('\n');
|
|
28
|
+
}
|
|
29
|
+
async function judgeAll(io, output, defs) {
|
|
30
|
+
const fastDefs = defs.filter(d => d.mode === 'fast');
|
|
31
|
+
const runtimeDefs = defs.filter(d => d.mode === 'runtime');
|
|
32
|
+
const failOpen = (d, why) => ({ id: d.id, verdict: 'pass', reasoning: `checker_error: ${why}`, checker_error: true });
|
|
33
|
+
const fastP = fastDefs.length === 0 ? Promise.resolve([]) :
|
|
34
|
+
io.checkFast(output, fastDefs).then(vs => fastDefs.map(d => ({ def: d, verdict: vs.find(v => v.id === d.id) ?? failOpen(d, 'missing verdict') })), err => fastDefs.map(d => ({ def: d, verdict: failOpen(d, err.message) })));
|
|
35
|
+
const runtimeP = Promise.all(runtimeDefs.map(d => io.checkRuntime(output, d).then(v => ({ def: d, verdict: v }), err => ({ def: d, verdict: failOpen(d, err.message) }))));
|
|
36
|
+
const [fast, rt] = await Promise.all([fastP, runtimeP]);
|
|
37
|
+
return [...fast, ...rt];
|
|
38
|
+
}
|
|
39
|
+
async function enforceGuardrails(io) {
|
|
40
|
+
try {
|
|
41
|
+
const defs = await io.loadGuardrails();
|
|
42
|
+
if (!defs || defs.length === 0)
|
|
43
|
+
return;
|
|
44
|
+
void io.emit('guardrails_checking', { count: defs.length });
|
|
45
|
+
let output = (await io.fetchOutput())?.trim() ?? '';
|
|
46
|
+
if (!output)
|
|
47
|
+
return; // nothing produced → nothing to judge
|
|
48
|
+
const events = [];
|
|
49
|
+
let attempt = 0;
|
|
50
|
+
let retried = false;
|
|
51
|
+
let judged = await judgeAll(io, output, defs);
|
|
52
|
+
// Retry loop: only rules with action='retry' trigger a revision round.
|
|
53
|
+
for (;;) {
|
|
54
|
+
const retryable = judged.filter(j => j.verdict.verdict === 'fail' && !j.verdict.checker_error &&
|
|
55
|
+
j.def.action === 'retry' && attempt < j.def.max_retries);
|
|
56
|
+
if (retryable.length === 0 || attempt >= GLOBAL_MAX_RETRY_ROUNDS)
|
|
57
|
+
break;
|
|
58
|
+
for (const j of retryable) {
|
|
59
|
+
events.push({ guardrail_id: j.def.id, verdict: 'fail', action_taken: 'retried', attempt, reasoning: j.verdict.reasoning });
|
|
60
|
+
}
|
|
61
|
+
void io.emit('guardrail_retry', { attempt, guardrails: retryable.map(j => j.def.name) });
|
|
62
|
+
const res = await io.runRetry(buildRetryFeedback(retryable)).catch(() => null);
|
|
63
|
+
if (!res || !res.ok || !res.text.trim())
|
|
64
|
+
break; // retry unavailable/failed → resolve with what we have
|
|
65
|
+
output = res.text.trim();
|
|
66
|
+
retried = true;
|
|
67
|
+
attempt += 1;
|
|
68
|
+
judged = await judgeAll(io, output, defs);
|
|
69
|
+
}
|
|
70
|
+
// Final resolution over the last round of verdicts.
|
|
71
|
+
const blocked = [];
|
|
72
|
+
const flagged = [];
|
|
73
|
+
for (const j of judged) {
|
|
74
|
+
if (j.verdict.checker_error) {
|
|
75
|
+
flagged.push(j);
|
|
76
|
+
events.push({ guardrail_id: j.def.id, verdict: 'fail', action_taken: 'flagged', attempt, reasoning: j.verdict.reasoning });
|
|
77
|
+
}
|
|
78
|
+
else if (j.verdict.verdict === 'fail' && j.def.action === 'block') {
|
|
79
|
+
blocked.push(j);
|
|
80
|
+
events.push({ guardrail_id: j.def.id, verdict: 'fail', action_taken: 'blocked', attempt, reasoning: j.verdict.reasoning });
|
|
81
|
+
}
|
|
82
|
+
else if (j.verdict.verdict === 'fail') {
|
|
83
|
+
// action 'flag', or 'retry' with retries exhausted → deliver + warn
|
|
84
|
+
flagged.push(j);
|
|
85
|
+
events.push({ guardrail_id: j.def.id, verdict: 'fail', action_taken: 'flagged', attempt, reasoning: j.verdict.reasoning });
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
events.push({ guardrail_id: j.def.id, verdict: 'pass', action_taken: 'passed', attempt, reasoning: j.verdict.reasoning || null });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const meta = {
|
|
92
|
+
checked: defs.length,
|
|
93
|
+
passed: defs.length - blocked.length - flagged.length,
|
|
94
|
+
attempt,
|
|
95
|
+
};
|
|
96
|
+
if (flagged.length)
|
|
97
|
+
meta.flags = flagged.map(j => j.def.name);
|
|
98
|
+
if (blocked.length) {
|
|
99
|
+
meta.blocked_by = blocked.map(j => j.def.name);
|
|
100
|
+
meta.original_text = output;
|
|
101
|
+
meta.final_text = `⛔ This response was blocked by the guardrail “${blocked[0].def.name}”.`;
|
|
102
|
+
}
|
|
103
|
+
else if (retried) {
|
|
104
|
+
// Aggregation would otherwise concatenate original + revision deltas —
|
|
105
|
+
// pin assistant_text to the revision the checks actually passed on.
|
|
106
|
+
meta.final_text = output;
|
|
107
|
+
}
|
|
108
|
+
await io.setResult(meta, events);
|
|
109
|
+
void io.emit('guardrails_result', {
|
|
110
|
+
checked: defs.length,
|
|
111
|
+
passed: meta.passed,
|
|
112
|
+
flagged: flagged.map(j => j.def.name),
|
|
113
|
+
blocked: blocked.map(j => j.def.name),
|
|
114
|
+
attempt,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
// Fail open — guardrails must never prevent completion/delivery.
|
|
119
|
+
try {
|
|
120
|
+
void io.emit('guardrails_result', { error: err.message });
|
|
121
|
+
}
|
|
122
|
+
catch { /* ignore */ }
|
|
123
|
+
console.error(`[guardrails] enforce failed (failing open): ${err.message}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface HangWatchdogOpts {
|
|
2
|
+
idleMs: number;
|
|
3
|
+
/** When provided, consulted on every (re)arm — lets the caller widen the
|
|
4
|
+
* window mid-run (e.g. grok: once output has been seen, silent tool work
|
|
5
|
+
* is expected and gets a longer leash than a dead-silent spawn). */
|
|
6
|
+
idleMsFn?: () => number;
|
|
7
|
+
onHang: () => void;
|
|
8
|
+
setTimer?: (fn: () => void, ms: number) => unknown;
|
|
9
|
+
clearTimer?: (handle: unknown) => void;
|
|
10
|
+
}
|
|
11
|
+
export interface HangWatchdog {
|
|
12
|
+
/** Call on every agent event — rearms the idle timer. */
|
|
13
|
+
touch(): void;
|
|
14
|
+
/** Disarm permanently (run finished / terminal). */
|
|
15
|
+
stop(): void;
|
|
16
|
+
}
|
|
17
|
+
export declare function createHangWatchdog(opts: HangWatchdogOpts): HangWatchdog;
|