wholestack 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.
@@ -0,0 +1,1394 @@
1
+ import * as ai from 'ai';
2
+ import { ToolSet, LanguageModel, ModelMessage } from 'ai';
3
+
4
+ /**
5
+ * Two ways to drive the ZETA build pipeline:
6
+ *
7
+ * http — POST /api/zeta/build on a running Next app. Zero local toolchain,
8
+ * but the engine must be up (and reachable).
9
+ * local — spawn the SAME worker the route spawns
10
+ * (scripts/zeta-build-worker.mts via `node --import tsx`), skipping
11
+ * HTTP, Turbopack, and the docker sandbox entirely. No server needed,
12
+ * but you must be inside the monorepo with tsx + build env present.
13
+ *
14
+ * Both speak the identical NDJSON event protocol, so we normalize once.
15
+ */
16
+ interface BuildResult {
17
+ ok: boolean;
18
+ error?: string;
19
+ buildId?: unknown;
20
+ verdict?: "SHIP" | "NO_SHIP";
21
+ themeId?: unknown;
22
+ fileCount?: number;
23
+ spec?: string | null;
24
+ verifyErrors?: string[];
25
+ fileList?: (string | null | undefined)[];
26
+ /** True when the engine returned a preview only (no code) — needs membership. */
27
+ paywalled?: boolean;
28
+ /** Where to subscribe when paywalled. */
29
+ upgradeUrl?: string;
30
+ }
31
+ interface DeliverResult {
32
+ ok: boolean;
33
+ written?: number;
34
+ dir?: string;
35
+ error?: string;
36
+ paywalled?: boolean;
37
+ upgradeUrl?: string;
38
+ }
39
+
40
+ type TodoStatus = "pending" | "in_progress" | "completed";
41
+ interface Todo {
42
+ content: string;
43
+ status: TodoStatus;
44
+ }
45
+
46
+ /**
47
+ * Permission modes — the safety dial:
48
+ *
49
+ * default — ask before every edit and shell command (with an "always"
50
+ * escape hatch per command and a "yes to all edits" toggle).
51
+ * acceptEdits — file edits apply without asking; shell commands still gated.
52
+ * plan — read-only. Edits, writes, builds, and commands are refused so
53
+ * the agent researches and proposes a plan instead of acting.
54
+ * yolo — nothing is gated (the old `--yes`). For trusted, scripted runs.
55
+ */
56
+ type PermissionMode = "default" | "acceptEdits" | "plan" | "yolo";
57
+ declare const PERMISSION_MODES: PermissionMode[];
58
+ declare function isPermissionMode(v: unknown): v is PermissionMode;
59
+ interface ConfirmChoice {
60
+ key: string;
61
+ label: string;
62
+ }
63
+ /** Prompt the user to pick one of `choices`; resolves to the chosen `key`. */
64
+ type ConfirmFn = (prompt: string, choices: ConfirmChoice[]) => Promise<string>;
65
+ type Decision = "allow" | "deny";
66
+ declare class Permissions {
67
+ mode: PermissionMode;
68
+ private readonly confirm?;
69
+ private alwaysCommands;
70
+ private acceptAllEdits;
71
+ constructor(mode: PermissionMode, confirm?: ConfirmFn | undefined);
72
+ isPlan(): boolean;
73
+ setMode(mode: PermissionMode): void;
74
+ /** Message a tool returns when an action is blocked by plan mode. */
75
+ planRefusal(): string;
76
+ /** Gate a file mutation. The caller has already rendered the diff. */
77
+ requestEdit(path: string): Promise<Decision>;
78
+ /** Gate a shell command. `display` is the command line shown to the user. */
79
+ requestCommand(display: string): Promise<Decision>;
80
+ /** Gate a heavier action (e.g. generate_app) — allowed unless plan mode. */
81
+ guardMutation(): Decision;
82
+ }
83
+
84
+ /**
85
+ * Edit checkpoints — the safety net under the agent's hands. Every file mutation
86
+ * (write_file / edit_file / multi_edit) snapshots the prior on-disk state before
87
+ * touching the file, grouped per tool call into one undoable checkpoint. `/undo`
88
+ * restores the previous state (re-creating or deleting files as needed) and pushes
89
+ * the just-undone state onto a redo stack; `/redo` replays it.
90
+ *
91
+ * In-memory, scoped to the running session — the point is fast, trustworthy
92
+ * rollback of agent edits while you work, not a durable VCS (that's git's job).
93
+ */
94
+ interface FileSnap {
95
+ path: string;
96
+ /** Content before the change; null means the file did not exist. */
97
+ before: string | null;
98
+ }
99
+ interface Checkpoint {
100
+ id: number;
101
+ label: string;
102
+ at: number;
103
+ snaps: FileSnap[];
104
+ }
105
+ /** Outcome of an undo/redo, for the REPL to report. */
106
+ interface RestoreResult {
107
+ ok: boolean;
108
+ label?: string;
109
+ restored?: string[];
110
+ deleted?: string[];
111
+ error?: string;
112
+ }
113
+ declare class CheckpointStore {
114
+ private undoStack;
115
+ private redoStack;
116
+ private seq;
117
+ /** Snaps accumulated for the in-flight tool call (multi_edit groups several). */
118
+ private pending;
119
+ /** Begin a new group; call before a mutating tool touches any file. */
120
+ begin(): void;
121
+ /** Record a file's pre-edit state once per path within the current group. */
122
+ capture(path: string, before: string | null): void;
123
+ /** Seal the group into an undoable checkpoint. A redo stack is invalidated by new edits. */
124
+ commit(label: string): void;
125
+ get canUndo(): boolean;
126
+ get canRedo(): boolean;
127
+ list(limit?: number): Checkpoint[];
128
+ /** Restore the most recent checkpoint; the displaced state becomes redoable. */
129
+ undo(): Promise<RestoreResult>;
130
+ /** Re-apply the most recently undone checkpoint. */
131
+ redo(): Promise<RestoreResult>;
132
+ /** Read the current on-disk state of each path so it can be re-applied later. */
133
+ private captureCurrent;
134
+ /** Write each snap's `before` back to disk (or delete when it was absent). */
135
+ private apply;
136
+ }
137
+
138
+ /**
139
+ * The agent's hands. Three families:
140
+ *
141
+ * ZETA verbs — generate_app / verify_contract — the reason this CLI exists.
142
+ * They drive the real engine, not a mock.
143
+ * Code edits — read / write / edit / grep / glob — a real coding agent's
144
+ * toolset, with diff previews and permission gates on writes.
145
+ * Shell — run_command — gated through the permission layer.
146
+ *
147
+ * Every tool keeps its side effects honest: failures come back as
148
+ * { ok:false, error } the model can read and react to, never a silent success.
149
+ */
150
+ interface ToolContext {
151
+ /** Where file ops are rooted + sandboxed. */
152
+ cwd: string;
153
+ /** ZETA build engine base URL (the running Next app). */
154
+ zetaApiUrl: string;
155
+ /** How generate_app drives the pipeline: auto | local | http. */
156
+ buildMode: "auto" | "local" | "http";
157
+ /** The permission gate — diffs, command confirms, plan-mode refusals. */
158
+ permissions: Permissions;
159
+ /** Snapshots taken before each file mutation, for /undo + /redo. */
160
+ checkpoints?: CheckpointStore;
161
+ }
162
+ declare function buildTools(ctx: ToolContext): {
163
+ todo_write: ai.Tool<{
164
+ todos: {
165
+ status: "pending" | "in_progress" | "completed";
166
+ content: string;
167
+ }[];
168
+ }, {
169
+ ok: boolean;
170
+ remaining: number;
171
+ total: number;
172
+ }>;
173
+ todo_read: ai.Tool<{}, {
174
+ ok: boolean;
175
+ todos: Todo[];
176
+ }>;
177
+ generate_app: ai.Tool<{
178
+ idea: string;
179
+ scope: "full" | "static" | "component" | "page";
180
+ buildModel: "zeta-g1" | "zeta-g1-max";
181
+ install: boolean;
182
+ keep: boolean;
183
+ }, BuildResult | {
184
+ ok: boolean;
185
+ liveUrl: string | undefined;
186
+ buildId: string | undefined;
187
+ files: number | undefined;
188
+ loc: number | undefined;
189
+ verdict: string | undefined;
190
+ trust: number | undefined;
191
+ note: string;
192
+ } | {
193
+ delivered: DeliverResult;
194
+ ok: boolean;
195
+ error?: string;
196
+ buildId?: unknown;
197
+ verdict?: "SHIP" | "NO_SHIP";
198
+ themeId?: unknown;
199
+ fileCount?: number;
200
+ spec?: string | null;
201
+ verifyErrors?: string[];
202
+ fileList?: (string | null | undefined)[];
203
+ paywalled?: boolean;
204
+ upgradeUrl?: string;
205
+ liveUrl?: undefined;
206
+ files?: undefined;
207
+ loc?: undefined;
208
+ trust?: undefined;
209
+ note?: undefined;
210
+ }>;
211
+ verify_contract: ai.Tool<{
212
+ projectDir: string;
213
+ contract: string;
214
+ property?: "conservation" | "no-value-extraction" | "reentrancy-safety" | "asset-conservation" | "monotonic-supply" | "access-control" | "initialization-safety" | "pausability" | "supply-cap" | "allowance-correctness" | undefined;
215
+ certPath?: string | undefined;
216
+ address?: string | undefined;
217
+ rpc?: string | undefined;
218
+ }, {
219
+ ok: boolean;
220
+ error: string;
221
+ verified?: undefined;
222
+ property?: undefined;
223
+ exitCode?: undefined;
224
+ output?: undefined;
225
+ } | {
226
+ ok: boolean;
227
+ verified: boolean;
228
+ property: string;
229
+ exitCode: number;
230
+ output: string;
231
+ error?: undefined;
232
+ }>;
233
+ read_file: ai.Tool<{
234
+ path: string;
235
+ offset?: number | undefined;
236
+ limit?: number | undefined;
237
+ }, {
238
+ ok: boolean;
239
+ path: string;
240
+ content: string;
241
+ startLine?: undefined;
242
+ error?: undefined;
243
+ } | {
244
+ ok: boolean;
245
+ path: string;
246
+ content: string;
247
+ startLine: number;
248
+ error?: undefined;
249
+ } | {
250
+ ok: boolean;
251
+ error: string;
252
+ path?: undefined;
253
+ content?: undefined;
254
+ startLine?: undefined;
255
+ }>;
256
+ write_file: ai.Tool<{
257
+ path: string;
258
+ content: string;
259
+ }, {
260
+ ok: boolean;
261
+ declined: boolean;
262
+ error: string;
263
+ path?: undefined;
264
+ bytes?: undefined;
265
+ } | {
266
+ ok: boolean;
267
+ path: string;
268
+ bytes: number;
269
+ declined?: undefined;
270
+ error?: undefined;
271
+ } | {
272
+ ok: boolean;
273
+ error: string;
274
+ declined?: undefined;
275
+ path?: undefined;
276
+ bytes?: undefined;
277
+ }>;
278
+ edit_file: ai.Tool<{
279
+ path: string;
280
+ old_string: string;
281
+ new_string: string;
282
+ replace_all: boolean;
283
+ }, {
284
+ ok: boolean;
285
+ error: string;
286
+ declined?: undefined;
287
+ path?: undefined;
288
+ added?: undefined;
289
+ removed?: undefined;
290
+ } | {
291
+ ok: boolean;
292
+ declined: boolean;
293
+ error: string;
294
+ path?: undefined;
295
+ added?: undefined;
296
+ removed?: undefined;
297
+ } | {
298
+ ok: boolean;
299
+ path: string;
300
+ added: number;
301
+ removed: number;
302
+ error?: undefined;
303
+ declined?: undefined;
304
+ }>;
305
+ multi_edit: ai.Tool<{
306
+ edits: {
307
+ path: string;
308
+ old_string: string;
309
+ new_string: string;
310
+ replace_all: boolean;
311
+ }[];
312
+ }, {
313
+ ok: boolean;
314
+ error: string;
315
+ declined?: undefined;
316
+ files?: undefined;
317
+ added?: undefined;
318
+ removed?: undefined;
319
+ } | {
320
+ ok: boolean;
321
+ declined: boolean;
322
+ error: string;
323
+ files?: undefined;
324
+ added?: undefined;
325
+ removed?: undefined;
326
+ } | {
327
+ ok: boolean;
328
+ files: string[];
329
+ added: number;
330
+ removed: number;
331
+ error?: undefined;
332
+ declined?: undefined;
333
+ }>;
334
+ list_dir: ai.Tool<{
335
+ path: string;
336
+ }, {
337
+ ok: boolean;
338
+ path: string;
339
+ entries: {
340
+ name: string;
341
+ dir: boolean;
342
+ }[];
343
+ error?: undefined;
344
+ } | {
345
+ ok: boolean;
346
+ error: string;
347
+ path?: undefined;
348
+ entries?: undefined;
349
+ }>;
350
+ glob: ai.Tool<{
351
+ path: string;
352
+ pattern: string;
353
+ }, {
354
+ ok: boolean;
355
+ count: number;
356
+ files: string[];
357
+ error?: undefined;
358
+ } | {
359
+ ok: boolean;
360
+ error: string;
361
+ count?: undefined;
362
+ files?: undefined;
363
+ }>;
364
+ grep: ai.Tool<{
365
+ path: string;
366
+ pattern: string;
367
+ ignoreCase: boolean;
368
+ maxResults: number;
369
+ glob?: string | undefined;
370
+ }, {
371
+ ok: boolean;
372
+ count: number;
373
+ matches: {
374
+ file: string;
375
+ line: number;
376
+ text: string;
377
+ }[];
378
+ error?: undefined;
379
+ } | {
380
+ ok: boolean;
381
+ error: string;
382
+ count?: undefined;
383
+ matches?: undefined;
384
+ }>;
385
+ run_command: ai.Tool<{
386
+ args: string[];
387
+ command: string;
388
+ }, {
389
+ ok: boolean;
390
+ declined: boolean;
391
+ error: string;
392
+ exitCode?: undefined;
393
+ output?: undefined;
394
+ } | {
395
+ ok: boolean;
396
+ exitCode: number;
397
+ output: string;
398
+ declined?: undefined;
399
+ error?: undefined;
400
+ }>;
401
+ run_app: ai.Tool<{
402
+ dir: string;
403
+ timeoutSeconds: number;
404
+ script?: string | undefined;
405
+ }, {
406
+ ok: boolean;
407
+ error: string;
408
+ declined?: undefined;
409
+ url?: undefined;
410
+ serving?: undefined;
411
+ log?: undefined;
412
+ } | {
413
+ ok: boolean;
414
+ declined: boolean;
415
+ error: string;
416
+ url?: undefined;
417
+ serving?: undefined;
418
+ log?: undefined;
419
+ } | {
420
+ ok: boolean;
421
+ url: string | undefined;
422
+ serving: boolean;
423
+ log: string;
424
+ error?: undefined;
425
+ declined?: undefined;
426
+ } | {
427
+ ok: boolean;
428
+ error: string | undefined;
429
+ log: string;
430
+ declined?: undefined;
431
+ url?: undefined;
432
+ serving?: undefined;
433
+ }>;
434
+ };
435
+
436
+ /**
437
+ * Hooks — shell commands the user (or a plugin) wires to agent events, the
438
+ * familiar event-hook idea. A PreToolUse hook can BLOCK a tool call (a
439
+ * conscience/guardrail); PostToolUse and UserPromptSubmit hooks inject extra
440
+ * context. Each hook receives a JSON event on stdin and may answer with JSON or
441
+ * an exit code.
442
+ */
443
+ type HookEvent = "PreToolUse" | "PostToolUse" | "UserPromptSubmit" | "Stop";
444
+ interface HookDef {
445
+ /** Regex tested against the tool name (tool events only). */
446
+ matcher?: string;
447
+ command: string;
448
+ source?: string;
449
+ }
450
+ type HookSet = Partial<Record<HookEvent, HookDef[]>>;
451
+ interface HookPayload {
452
+ toolName?: string;
453
+ toolInput?: unknown;
454
+ toolResult?: unknown;
455
+ prompt?: string;
456
+ }
457
+ interface HookOutcome {
458
+ /** Set on a PreToolUse block — the reason fed back to the model. */
459
+ block?: string;
460
+ /** Extra context strings to surface (PostToolUse / UserPromptSubmit). */
461
+ context: string[];
462
+ }
463
+ declare class HookRunner {
464
+ private readonly hooks;
465
+ private readonly cwd;
466
+ constructor(hooks: HookSet, cwd: string);
467
+ any(): boolean;
468
+ has(event: HookEvent): boolean;
469
+ run(event: HookEvent, payload: HookPayload): Promise<HookOutcome>;
470
+ }
471
+ /** Merge hook sets (later wins on nothing — they all concatenate). */
472
+ declare function mergeHookSets(...sets: HookSet[]): HookSet;
473
+ /** Load hooks.json from the global config dir and the project's .zeta-g/. */
474
+ declare function loadHookFiles(cwd: string): HookSet;
475
+ /**
476
+ * Wrap every tool's execute so PreToolUse hooks can block it and PostToolUse
477
+ * hooks can append context. No-op when there are no tool hooks.
478
+ */
479
+ declare function applyHooks(tools: ToolSet, runner: HookRunner): ToolSet;
480
+
481
+ /**
482
+ * Zeta-G's brain — one family, three tiers, all branded Zeta:
483
+ *
484
+ * Zeta-G1.0 Lite — light & cheap
485
+ * Zeta-G1.0 — the default
486
+ * Zeta-G1.0 MAX — the most capable
487
+ *
488
+ * The transport underneath is an implementation detail and never surfaced to
489
+ * the user — only the Zeta tier names are shown. (The ZETA build engine writes
490
+ * ISL on its own fast lane; that's separate from the conversational brain here.)
491
+ * We resolve lazily so the CLI boots even with no key; it errors the moment you
492
+ * actually ask it to think.
493
+ */
494
+ type ModelKey = string;
495
+ declare function resolveModelKey(raw: string | undefined): ModelKey;
496
+ declare function modelLabel(key: ModelKey): string;
497
+ declare function modelId(key: ModelKey): string;
498
+ declare function modelContextWindow(key: ModelKey): number;
499
+ declare function supportsThinking(key: ModelKey): boolean;
500
+ declare function listModels(): {
501
+ key: ModelKey;
502
+ label: string;
503
+ thinking: boolean;
504
+ }[];
505
+ /**
506
+ * Provider-specific reasoning options. Every tier runs on Cerebras' OpenAI-
507
+ * compatible endpoint, which honors `reasoning_effort` (the @ai-sdk/openai
508
+ * provider maps `openai.reasoningEffort` → that body field). We keep effort
509
+ * "low" by default — the measured-safe setting that stops gpt-oss from starving
510
+ * its `content` budget — and only raise it to "high" when the user opts into
511
+ * thinking via --think / /think. Custom (OpenRouter) lanes get no override.
512
+ */
513
+ declare function buildProviderOptions(key: ModelKey, thinkingOn: boolean): Record<string, unknown> | undefined;
514
+ declare function resolveModel(key: ModelKey): LanguageModel;
515
+
516
+ /**
517
+ * Token + cost accounting. The model reports usage after every turn; we keep a
518
+ * running total so `/cost` can answer "what has this session cost me?" honestly.
519
+ *
520
+ * Prices are approximate list rates ($ per 1M tokens). The fast Zeta-g1 lanes
521
+ * are billed on a flat plan, so we surface 0 rather than invent a per-token
522
+ * number. Costs are internal estimates only — the user never sees an upstream id.
523
+ */
524
+ interface TokenUsage {
525
+ inputTokens?: number;
526
+ outputTokens?: number;
527
+ totalTokens?: number;
528
+ cachedInputTokens?: number;
529
+ reasoningTokens?: number;
530
+ }
531
+ declare function estimateCost(modelId: string, u: TokenUsage): number;
532
+ /** Running totals across a session, keyed for a clean `/cost` readout. */
533
+ declare class UsageMeter {
534
+ private input;
535
+ private output;
536
+ private cached;
537
+ private reasoning;
538
+ private turns;
539
+ private cost;
540
+ add(modelId: string, u: TokenUsage): void;
541
+ get totalTokens(): number;
542
+ get totalCost(): number;
543
+ snapshot(): {
544
+ input: number;
545
+ output: number;
546
+ cached: number;
547
+ reasoning: number;
548
+ total: number;
549
+ turns: number;
550
+ cost: number;
551
+ };
552
+ }
553
+
554
+ interface SessionMeta {
555
+ id: string;
556
+ cwd: string;
557
+ model: string;
558
+ startedAt: string;
559
+ }
560
+ interface SessionSummary extends SessionMeta {
561
+ preview: string;
562
+ turns: number;
563
+ updatedAt: number;
564
+ }
565
+ declare class Session {
566
+ readonly meta: SessionMeta;
567
+ private readonly path;
568
+ private constructor();
569
+ /** Start a fresh session and write its meta header. */
570
+ static create(cwd: string, model: string): Session;
571
+ /** Reopen an existing session and replay its messages. */
572
+ static resume(id: string): {
573
+ session: Session;
574
+ messages: ModelMessage[];
575
+ } | null;
576
+ /** The most recent session (optionally scoped to a working directory). */
577
+ static latestId(cwd?: string): string | null;
578
+ /** List sessions newest-first, with a preview of the first user message. */
579
+ static list(limit?: number, cwd?: string): SessionSummary[];
580
+ appendUser(text: string): void;
581
+ appendMessages(messages: ModelMessage[]): void;
582
+ }
583
+
584
+ /**
585
+ * SP-21 — Prompt / Policy Runtime: the shared contract.
586
+ *
587
+ * The single source of truth for the prompt-governance layer. Every module in
588
+ * src/prompts, src/policy, and src/security implements against THESE types so
589
+ * the pieces compose without interface drift.
590
+ *
591
+ * Core idea: a turn's context is split into isolated, authority-stamped
592
+ * CHANNELS. SYSTEM/DEVELOPER/USER carry instruction authority (descending);
593
+ * RETRIEVED/TOOL/MCP/WEB are UNTRUSTED data and can never issue instructions —
594
+ * "ignore previous instructions" buried in a tool result or a fetched page is
595
+ * neutralized and fenced as data before it ever reaches the provider.
596
+ */
597
+ /** Authority tiers — higher binds harder; a lower tier NEVER overrides a higher one. */
598
+ type Authority = "system" | "developer" | "user" | "untrusted";
599
+ /** The isolated channels that make up a turn's context. */
600
+ type ChannelKind = "system" | "developer" | "user" | "retrieved" | "tool" | "mcp" | "memory" | "web";
601
+ interface Channel {
602
+ kind: ChannelKind;
603
+ authority: Authority;
604
+ /** Provenance for linting + display (tool name, url, file path, server key). */
605
+ source?: string;
606
+ content: string;
607
+ /** Untrusted channels are firewall-scanned and fenced as data, never as instructions. */
608
+ trusted: boolean;
609
+ }
610
+ declare function channelAuthority(kind: ChannelKind): Authority;
611
+ declare function isUntrusted(kind: ChannelKind): boolean;
612
+ /** Channels whose content is untrusted external input — derived from the one
613
+ * source of truth (AUTHORITY_OF) so it can never drift from isUntrusted(). */
614
+ declare const UNTRUSTED_CHANNELS: ChannelKind[];
615
+ type FsAccess = "none" | "read" | "write";
616
+ /** What a tool/plugin/MCP server is permitted to touch. */
617
+ interface CapabilitySet {
618
+ fs: FsAccess;
619
+ network: boolean;
620
+ shell: boolean;
621
+ secrets: boolean;
622
+ }
623
+ interface CapabilityManifest {
624
+ /** Tool / plugin / MCP id this manifest governs (e.g. "edit_file", "mcp__github__*"). */
625
+ name: string;
626
+ capabilities: CapabilitySet;
627
+ /** Where it came from: "builtin" | "plugin:<name>" | "mcp:<server>" | "web". */
628
+ origin: string;
629
+ }
630
+ declare const NO_CAPS: CapabilitySet;
631
+ /** True iff `have` covers everything in `need`. */
632
+ declare function capsAllow(have: CapabilitySet, need: Partial<CapabilitySet>): boolean;
633
+ type PolicyAction = "allow" | "warn" | "strip" | "block";
634
+ interface PolicyDecision {
635
+ action: PolicyAction;
636
+ reason?: string;
637
+ /** Sanitized content when action is "strip" (or "allow" with redactions). */
638
+ sanitized?: string;
639
+ }
640
+ type Severity = "none" | "low" | "medium" | "high";
641
+ declare function maxSeverity(a: Severity, b: Severity): Severity;
642
+ declare function severityRank(s: Severity): number;
643
+ interface Detection {
644
+ /** Stable rule id, e.g. "instruction-override". */
645
+ rule: string;
646
+ severity: Severity;
647
+ /** The offending snippet (bounded ≤200 chars). */
648
+ match: string;
649
+ note: string;
650
+ }
651
+ interface ScanResult {
652
+ detections: Detection[];
653
+ /** Max severity across all detections (or "none"). */
654
+ severity: Severity;
655
+ }
656
+ interface FirewallVerdict {
657
+ /** What the policy decided to do with this channel. */
658
+ action: PolicyAction;
659
+ scan: ScanResult;
660
+ /** Content after neutralization (instructions defanged, secrets redacted, fenced). */
661
+ safe: string;
662
+ source?: string;
663
+ kind?: ChannelKind;
664
+ }
665
+ /** A detector inspects untrusted text and returns any detections it finds. */
666
+ type Detector = (text: string, ctx?: {
667
+ source?: string;
668
+ kind?: ChannelKind;
669
+ }) => Detection[];
670
+
671
+ interface AgentOptions {
672
+ model: LanguageModel;
673
+ modelKey: ModelKey;
674
+ ctx: ToolContext;
675
+ /** Web + MCP tools merged alongside the built-in toolset. */
676
+ extraTools?: ToolSet;
677
+ /** Hooks applied to the merged toolset + run on UserPromptSubmit. */
678
+ hooks?: HookRunner;
679
+ /** Project memory + plugin system text, appended to the system prompt. */
680
+ memoryText?: string;
681
+ thinking?: boolean;
682
+ maxSteps?: number;
683
+ contextWindow?: number;
684
+ session?: Session | null;
685
+ usage?: UsageMeter;
686
+ onUsage?: (u: TokenUsage) => void;
687
+ }
688
+ interface TurnResult {
689
+ aborted: boolean;
690
+ usage: TokenUsage | null;
691
+ }
692
+ declare class Agent {
693
+ private readonly opts;
694
+ private history;
695
+ private tools;
696
+ private thinking;
697
+ private modelKey;
698
+ private model;
699
+ private session;
700
+ private contextWindow;
701
+ /** Real input-token count the provider reported last turn (beats the char estimate). */
702
+ private lastInputTokens;
703
+ /** Output throughput of the last turn (tokens/sec, first token → stream end). */
704
+ lastTps: number;
705
+ /** SP-21 prompt/policy runtime: authority preamble + injection firewall + capability model. */
706
+ private readonly policy;
707
+ private readonly caps;
708
+ readonly usage: UsageMeter;
709
+ constructor(opts: AgentOptions);
710
+ get toolNames(): string[];
711
+ /** The declared capability manifests (for /caps + /doctor). */
712
+ capabilities(): CapabilityManifest[];
713
+ get thinkingOn(): boolean;
714
+ setThinking(on: boolean): void;
715
+ setModel(model: LanguageModel, key: ModelKey, contextWindow?: number): void;
716
+ setSession(session: Session | null): void;
717
+ reset(): void;
718
+ replaceHistory(messages: ModelMessage[]): void;
719
+ /** % of the model's context window the current history occupies. */
720
+ contextPct(): number;
721
+ private system;
722
+ /**
723
+ * Auto-compact when the history nears the context window. We budget against
724
+ * the MAX of the provider's last real input-token count and a char estimate
725
+ * that includes the system prompt (tool list, memory) — so tool/JSON-dense
726
+ * histories that the naive estimate under-counts still trigger in time.
727
+ */
728
+ private maybeCompact;
729
+ /** Force or auto compaction; prints a one-line note. Returns true if it ran. */
730
+ runCompaction(announce: boolean): Promise<boolean>;
731
+ /** Run one user turn end-to-end; streams to stdout, updates history. */
732
+ send(userInput: string, signal?: AbortSignal): Promise<TurnResult>;
733
+ }
734
+
735
+ interface MemorySource {
736
+ path: string;
737
+ bytes: number;
738
+ truncated: boolean;
739
+ }
740
+ interface ProjectMemory {
741
+ sources: MemorySource[];
742
+ text: string;
743
+ }
744
+ /**
745
+ * Collect memory files from the global config dir and every directory between
746
+ * the repo root and the cwd (root first, so cwd-local files win on conflict).
747
+ */
748
+ declare function loadProjectMemory(cwd: string): Promise<ProjectMemory>;
749
+
750
+ /**
751
+ * A slim MCP client — the extensibility layer. It reads the standard `.mcp.json`
752
+ * format (so a project's servers Just Work), connects to each,
753
+ * and exposes their tools to the agent namespaced `mcp__<server>__<tool>`.
754
+ *
755
+ * Fail-soft by design: a server that won't start is reported and skipped, never
756
+ * fatal. We support both stdio servers ({command,args,env}) and HTTP servers
757
+ * ({url,headers}). No tier policy here — a CLI on your own machine trusts the
758
+ * servers you configured.
759
+ */
760
+ interface StdioServerConfig {
761
+ command: string;
762
+ args?: string[];
763
+ env?: Record<string, string>;
764
+ }
765
+ interface HttpServerConfig {
766
+ url: string;
767
+ headers?: Record<string, string>;
768
+ }
769
+ type ServerConfig = StdioServerConfig | HttpServerConfig;
770
+ interface LoadedMcp {
771
+ tools: ToolSet;
772
+ mounted: {
773
+ key: string;
774
+ tools: number;
775
+ }[];
776
+ failed: {
777
+ key: string;
778
+ error: string;
779
+ }[];
780
+ close: () => Promise<void>;
781
+ }
782
+ interface LoadMcpOptions {
783
+ cwd: string;
784
+ /** Only mount these server keys (from --mcp). Empty/undefined → all configured. */
785
+ only?: string[];
786
+ /** Skip MCP entirely. */
787
+ disabled?: boolean;
788
+ /** Extra servers (e.g. contributed by plugins). File configs override these. */
789
+ extraServers?: Record<string, ServerConfig>;
790
+ connectTimeoutMs?: number;
791
+ /**
792
+ * Deferred mode (DEFAULT). Instead of binding every MCP tool's full JSON schema
793
+ * to the model on every turn — which can cost tens of thousands of tokens before
794
+ * the user types anything — expose just two tiny tools: `mcp_list_tools` (a
795
+ * compact catalog) and `mcp_call_tool` (the dispatcher). The model discovers and
796
+ * calls MCP tools on demand. Set `defer: false` to bind all schemas eagerly.
797
+ */
798
+ defer?: boolean;
799
+ }
800
+ declare function loadMcpTools(opts: LoadMcpOptions): Promise<LoadedMcp>;
801
+
802
+ /**
803
+ * Slash commands — the in-session control surface. An extensible registry, not
804
+ * a hardcoded if/else: built-ins live here, plugins and ~/.zeta-g/commands/*.md
805
+ * register more. A command returns a CommandResult the REPL acts on, so the
806
+ * registry stays decoupled from the agent it drives.
807
+ */
808
+ type CommandResult = {
809
+ type: "handled";
810
+ } | {
811
+ type: "exit";
812
+ } | {
813
+ type: "clear";
814
+ } | {
815
+ type: "compact";
816
+ } | {
817
+ type: "prompt";
818
+ text: string;
819
+ } | {
820
+ type: "switchModel";
821
+ modelKey: ModelKey;
822
+ } | {
823
+ type: "setMode";
824
+ mode: PermissionMode;
825
+ } | {
826
+ type: "toggleThinking";
827
+ on: boolean;
828
+ } | {
829
+ type: "resume";
830
+ sessionId: string;
831
+ };
832
+ interface CommandContext {
833
+ args: string;
834
+ cwd: string;
835
+ modelKey: ModelKey;
836
+ modelLabel: string;
837
+ thinkingOn: boolean;
838
+ permissions: Permissions;
839
+ usage: UsageMeter;
840
+ memory: ProjectMemory;
841
+ mcp: LoadedMcp;
842
+ toolNames: string[];
843
+ capabilities: CapabilityManifest[];
844
+ checkpoints: CheckpointStore;
845
+ commands: SlashCommand[];
846
+ print: (s?: string) => void;
847
+ }
848
+ interface SlashCommand {
849
+ name: string;
850
+ aliases?: string[];
851
+ summary: string;
852
+ hidden?: boolean;
853
+ /** Where it came from — "builtin", "custom", or a plugin name. */
854
+ source?: string;
855
+ run: (ctx: CommandContext) => Promise<CommandResult> | CommandResult;
856
+ }
857
+ /** Parse every *.md in `dir` into slash commands tagged with `source`. */
858
+ declare function loadMdCommands(dir: string, source: string): SlashCommand[];
859
+ declare class CommandRegistry {
860
+ private map;
861
+ private ordered;
862
+ constructor();
863
+ register(cmd: SlashCommand): void;
864
+ get(name: string): SlashCommand | undefined;
865
+ list(): SlashCommand[];
866
+ names(): string[];
867
+ /** Load *.md commands from ~/.zeta-g/commands and <cwd>/.zeta-g/commands. */
868
+ loadCustom(cwd: string): void;
869
+ dispatch(input: string, base: Omit<CommandContext, "args" | "commands">): Promise<CommandResult | null>;
870
+ }
871
+
872
+ interface LoadedPlugins {
873
+ plugins: {
874
+ name: string;
875
+ description?: string;
876
+ version?: string;
877
+ dir: string;
878
+ }[];
879
+ systemText: string;
880
+ commands: SlashCommand[];
881
+ mcpServers: Record<string, ServerConfig>;
882
+ hooks: HookSet;
883
+ failed: {
884
+ dir: string;
885
+ error: string;
886
+ }[];
887
+ }
888
+ declare function loadPlugins(cwd: string): LoadedPlugins;
889
+
890
+ interface SearchHit {
891
+ title: string;
892
+ url: string;
893
+ snippet: string;
894
+ }
895
+ declare function buildWebTools(): {
896
+ web_fetch: ai.Tool<{
897
+ url: string;
898
+ }, {
899
+ ok: boolean;
900
+ error: string;
901
+ status?: undefined;
902
+ url?: undefined;
903
+ contentType?: undefined;
904
+ content?: undefined;
905
+ truncated?: undefined;
906
+ } | {
907
+ ok: boolean;
908
+ status: number;
909
+ url: string;
910
+ contentType: string;
911
+ content: string;
912
+ truncated: boolean;
913
+ error?: undefined;
914
+ }>;
915
+ web_search: ai.Tool<{
916
+ query: string;
917
+ }, {
918
+ ok: boolean;
919
+ note: string;
920
+ query?: undefined;
921
+ results?: undefined;
922
+ error?: undefined;
923
+ } | {
924
+ ok: boolean;
925
+ query: string;
926
+ results: SearchHit[];
927
+ note?: undefined;
928
+ error?: undefined;
929
+ } | {
930
+ ok: boolean;
931
+ error: string;
932
+ note?: undefined;
933
+ query?: undefined;
934
+ results?: undefined;
935
+ }>;
936
+ };
937
+
938
+ /** Rough token estimate — ~4 chars/token over the serialized messages. */
939
+ declare function estimateTokens(messages: ModelMessage[]): number;
940
+ interface CompactResult {
941
+ messages: ModelMessage[];
942
+ summary: string;
943
+ before: number;
944
+ after: number;
945
+ degraded: boolean;
946
+ }
947
+ interface CompactOptions {
948
+ /** How many trailing user turns to keep verbatim. */
949
+ keepUserTurns?: number;
950
+ /** Max chars of transcript fed to the summarizer. */
951
+ maxTranscript?: number;
952
+ }
953
+ /**
954
+ * Compact `messages`. Returns a new history: [summary, ack, ...recent turns].
955
+ * Falls back to a plain recency window (with `degraded: true`) if the model
956
+ * call to summarize fails — never silently claims success it didn't get.
957
+ */
958
+ declare function compact(messages: ModelMessage[], model: LanguageModel, opts?: CompactOptions): Promise<CompactResult>;
959
+
960
+ /**
961
+ * SP-21 — System-prompt REGISTRY: identity, role layering, and safety overlays.
962
+ *
963
+ * One deterministic place to keep the agent's system prompts. A prompt is
964
+ * assembled from three layers, in a fixed order so the same inputs always
965
+ * yield the same bytes:
966
+ *
967
+ * 1. a BASE system body (versioned per id — register a new version, the old
968
+ * one stays reachable for replay/audit),
969
+ * 2. an optional ROLE layer (e.g. "coder" / "planner") that sharpens behavior,
970
+ * 3. zero or more SAFETY overlays appended last (the safety + verify reflexes),
971
+ * so they read as the final, binding word.
972
+ *
973
+ * Nothing here reaches for wall-clock time or randomness: composition is a pure
974
+ * function of what was registered. The single source of truth for shared shapes
975
+ * lives in ./types.js — this module only adds the registry-specific structures.
976
+ */
977
+ /** A behavior layer for a named role, layered on top of the base system body. */
978
+ interface RoleLayer {
979
+ role: string;
980
+ body: string;
981
+ }
982
+ /** A safety rule appended after the base + role, so it reads as the last word. */
983
+ interface SafetyOverlay {
984
+ id: string;
985
+ body: string;
986
+ }
987
+ /**
988
+ * The registry. Holds versioned system bodies, role layers, and safety
989
+ * overlays, and composes them deterministically. Registration order is
990
+ * preserved (insertion-ordered Maps) so "latest" and "all overlays" are stable.
991
+ */
992
+ declare class PromptRegistry {
993
+ /** id → list of versions, in registration order (last = latest). */
994
+ private readonly systems;
995
+ /** role name → its layer body. */
996
+ private readonly roles;
997
+ /** overlay id → overlay, in registration order. */
998
+ private readonly safety;
999
+ /**
1000
+ * Register a system body under `id`. If `version` is omitted, an auto label
1001
+ * `v<N>` is assigned from the count already stored for that id. Re-registering
1002
+ * the same explicit version replaces that revision in place (it keeps its
1003
+ * original slot); a new version is appended and becomes the latest.
1004
+ */
1005
+ registerSystem(id: string, body: string, version?: string): void;
1006
+ /**
1007
+ * Fetch a system body. With no `version`, returns the latest (last
1008
+ * registered). Throws if the id — or the requested version — is unknown.
1009
+ */
1010
+ getSystem(id: string, version?: string): string;
1011
+ /** The versions stored for `id`, in registration order. Empty if unknown. */
1012
+ versions(id: string): string[];
1013
+ /** Register (or replace) the layer body for a role. */
1014
+ registerRole(layer: RoleLayer): void;
1015
+ /** The body for a role, or undefined if the role is unknown. */
1016
+ getRole(role: string): string | undefined;
1017
+ /** Register (or replace) a safety overlay by its id. */
1018
+ registerOverlay(o: SafetyOverlay): void;
1019
+ /** All registered safety overlays, in registration order. */
1020
+ overlays(): SafetyOverlay[];
1021
+ /**
1022
+ * Compose a full system prompt: base body, then the role layer (when a known
1023
+ * role is given), then the safety overlays — joined by a blank line.
1024
+ *
1025
+ * Overlay selection: if `overlayIds` is provided, those overlays are appended
1026
+ * in the order listed (unknown ids are skipped); if omitted, ALL registered
1027
+ * overlays are appended in registration order.
1028
+ */
1029
+ composeSystem(opts: {
1030
+ id: string;
1031
+ version?: string;
1032
+ role?: string;
1033
+ overlayIds?: string[];
1034
+ }): string;
1035
+ }
1036
+ /**
1037
+ * The default registry, seeded with Zeta-G's core identity, the safety and
1038
+ * verify overlays, and the coder/planner roles. Importers get a ready-to-use
1039
+ * registry; they can register additional versions, roles, or overlays on top.
1040
+ */
1041
+ declare const DEFAULT_REGISTRY: PromptRegistry;
1042
+
1043
+ /**
1044
+ * Deterministic template rendering + token budgeting.
1045
+ *
1046
+ * Two small, pure jobs. First, {{key}} substitution that treats values as
1047
+ * literal text — a value containing "$1" or "$&" must never be reinterpreted
1048
+ * as a regex replacement pattern, so we substitute through a FUNCTION replacer.
1049
+ * Second, fitting a turn's CHANNELS inside a token budget by trimming the
1050
+ * lowest-authority content first: a SYSTEM channel is sacred and is never
1051
+ * touched, untrusted data goes first, then user, then developer.
1052
+ *
1053
+ * Everything here is deterministic — no clock, no randomness — so the same
1054
+ * inputs always produce the same prompt.
1055
+ */
1056
+
1057
+ /**
1058
+ * Replace every {{key}} in `tpl` with `vars[key]`. The replacer is a function,
1059
+ * so any "$" sequences inside a value stay literal rather than being treated as
1060
+ * replacement patterns. Unknown keys: throw under `strict`, else collapse to "".
1061
+ */
1062
+ declare function renderTemplate(tpl: string, vars: Record<string, string>, opts?: {
1063
+ strict?: boolean;
1064
+ }): string;
1065
+ /** Cheap, provider-agnostic token estimate: ~4 chars per token. */
1066
+ declare function estimateTextTokens(text: string): number;
1067
+ interface BudgetInput {
1068
+ channels: Channel[];
1069
+ maxTokens: number;
1070
+ /** Tokens to hold back for the response / scaffolding (default 0). */
1071
+ reserve?: number;
1072
+ }
1073
+ interface BudgetResult {
1074
+ channels: Channel[];
1075
+ /** Per-kind tally of tokens removed by trimming. */
1076
+ trimmed: {
1077
+ kind: ChannelKind;
1078
+ droppedTokens: number;
1079
+ }[];
1080
+ total: number;
1081
+ }
1082
+ /**
1083
+ * Fit `channels` within (maxTokens - reserve) by truncating the lowest-priority
1084
+ * channels first. Truncation keeps the HEAD of the content and appends a trim
1085
+ * marker. Returns fresh channel objects, the per-kind dropped-token counts, and
1086
+ * the final total estimate. Never trims a "system" channel.
1087
+ */
1088
+ declare function budgetChannels(input: BudgetInput): BudgetResult;
1089
+
1090
+ /**
1091
+ * Prompt linting — catch "string soup" mistakes before a turn is sent.
1092
+ *
1093
+ * The channel model (see ./types.ts) keeps authority tiers separated, but it
1094
+ * can't stop a caller from assembling a malformed prompt: an empty system
1095
+ * message, a leaked API key pasted into a developer note, an untrusted tool
1096
+ * result mislabeled as trusted, or a template that never got its variables
1097
+ * filled in. These are cheap to detect and expensive to ship, so we surface
1098
+ * them as plain findings the CLI can print (or refuse on) before the provider
1099
+ * ever sees the text.
1100
+ *
1101
+ * Everything here is pure and deterministic — same channels in, same findings
1102
+ * out, no clock and no randomness.
1103
+ */
1104
+
1105
+ /** How loudly a finding should be surfaced. Mirrors the firewall's tone, not its scale. */
1106
+ type LintSeverity = "error" | "warning" | "info";
1107
+ interface LintFinding {
1108
+ /** Stable rule id, e.g. "empty-body" or "leaked-secret". */
1109
+ rule: string;
1110
+ severity: LintSeverity;
1111
+ message: string;
1112
+ /** Which channel kind the finding belongs to, when it came from a specific channel. */
1113
+ channel?: ChannelKind;
1114
+ }
1115
+ /**
1116
+ * Lint a single system- or developer-class body. These channels carry
1117
+ * instruction authority, so mistakes here are the most damaging: an empty
1118
+ * system prompt silently neuters the agent, and a secret pasted into one rides
1119
+ * straight to the provider with full trust.
1120
+ */
1121
+ declare function lintSystemText(body: string): LintFinding[];
1122
+ /**
1123
+ * Lint a fully assembled prompt — the ordered list of channels for one turn.
1124
+ * Checks the shape of the prompt as a whole (a system channel must exist, the
1125
+ * trust labels must match each channel's kind) and folds in per-body linting
1126
+ * for every instruction-bearing channel.
1127
+ */
1128
+ declare function lintPrompt(channels: Channel[]): LintFinding[];
1129
+
1130
+ /**
1131
+ * The POLICY ENGINE — the layer that turns a firewall SCAN into an ACTION.
1132
+ *
1133
+ * A detector tells us *what* it saw; the policy decides *what to do about it*.
1134
+ * Severity maps to one of four actions (allow / warn / strip / block) per a
1135
+ * small, overridable config, so an operator can tighten or loosen the firewall
1136
+ * without touching detection code. The engine also owns the AUTHORITY PREAMBLE:
1137
+ * the SYSTEM-prompt block that teaches the model the prompt-authority tiers and
1138
+ * the one inviolable rule — text inside fenced UNTRUSTED blocks is DATA, never
1139
+ * an instruction. Pure and deterministic: same scan in, same decision out.
1140
+ */
1141
+
1142
+ /** How the firewall behaves: the per-severity action table plus two hard switches. */
1143
+ interface PolicyConfig {
1144
+ /** Action to take for each injection severity the scanner reports. */
1145
+ onInjection: Record<Severity, PolicyAction>;
1146
+ /** Redact detected secrets/credentials before content reaches the provider. */
1147
+ blockSecrets: boolean;
1148
+ /** Neutralize exfiltration attempts (data-routed-outward) found in untrusted data. */
1149
+ stripExfil: boolean;
1150
+ }
1151
+ /** The safe default: clean passes through, suspicion is stripped, blatant injection is blocked. */
1152
+ declare const DEFAULT_POLICY: PolicyConfig;
1153
+ /**
1154
+ * Maps firewall severity to a policy action and frames the prompt-authority
1155
+ * contract. Stateless aside from its merged config — construct once, reuse.
1156
+ */
1157
+ declare class PolicyEngine {
1158
+ readonly config: PolicyConfig;
1159
+ constructor(config?: Partial<PolicyConfig>);
1160
+ /**
1161
+ * Decide what to do with a scanned channel. The action is read straight off
1162
+ * the severity table; the reason summarizes the detections that drove it so
1163
+ * the choice is legible in logs and to the user.
1164
+ */
1165
+ decideChannel(scan: ScanResult): PolicyDecision;
1166
+ /**
1167
+ * The SYSTEM-prompt block that establishes prompt authority and the
1168
+ * untrusted-data rule. Prepended ahead of any external content so the model
1169
+ * carries the contract into every turn.
1170
+ */
1171
+ authorityPreamble(): string;
1172
+ }
1173
+
1174
+ /**
1175
+ * SP-21 — The anti-prompt-injection FIREWALL.
1176
+ *
1177
+ * Untrusted channels (tool output, RAG hits, MCP results, fetched pages) are
1178
+ * data, not instructions. This module is the chokepoint they pass through
1179
+ * before reaching the provider: every detector runs over the text, the policy
1180
+ * engine decides what to do, and whatever survives is fenced inside a clearly
1181
+ * labelled DATA-ONLY block so the model treats it as content to reason ABOUT,
1182
+ * never as commands to obey.
1183
+ *
1184
+ * Pure and deterministic: same text in, same verdict out. No clock, no random.
1185
+ */
1186
+
1187
+ declare class PromptFirewall {
1188
+ private readonly detectors;
1189
+ constructor(detectors?: Detector[]);
1190
+ /**
1191
+ * Run every detector over the (bounded) text and fold their findings into a
1192
+ * single ScanResult. Severity starts at "none" and climbs via maxSeverity.
1193
+ */
1194
+ scan(text: string, ctx?: {
1195
+ source?: string;
1196
+ kind?: ChannelKind;
1197
+ }): ScanResult;
1198
+ /**
1199
+ * Wrap content in the labelled DATA-ONLY fence. Credential material and the
1200
+ * override imperative itself are ALWAYS scrubbed first via live regex over the
1201
+ * body (redactSensitive) — not by string-matching a lossy clipped snippet — so
1202
+ * collapsed-whitespace or oversized spans can't slip through. `opts` is kept
1203
+ * for API compatibility; redaction is unconditional now.
1204
+ */
1205
+ neutralize(text: string, _scan: ScanResult, _opts?: {
1206
+ redactSecrets?: boolean;
1207
+ }): string;
1208
+ /**
1209
+ * The full pipeline for one untrusted channel: normalize (defang homoglyph /
1210
+ * zero-width evasion), scan, ask the policy what to do, then produce the safe
1211
+ * rendering. We scan AND forward the SAME normalized+bounded body, so nothing
1212
+ * unscanned ever reaches the model.
1213
+ * block → withhold entirely (just a breadcrumb of what was dropped)
1214
+ * strip / warn / allow → fenced + redacted untrusted data
1215
+ */
1216
+ guard(channel: Channel, policy: PolicyEngine): FirewallVerdict;
1217
+ }
1218
+ /** Shared default firewall wired to the full detector suite. */
1219
+ declare const DEFAULT_FIREWALL: PromptFirewall;
1220
+
1221
+ /**
1222
+ * ChannelStack — the assembler that keeps prompt authority tiers isolated.
1223
+ *
1224
+ * You add content as typed channels; on render() the trusted channels
1225
+ * (system/developer/user) are concatenated as authoritative instructions, while
1226
+ * every UNTRUSTED channel (tool/mcp/web/retrieved/memory) is run through the
1227
+ * firewall and emitted as fenced DATA — so "ignore previous instructions" inside
1228
+ * a tool result or a fetched page can't seize control. Optionally budgeted so a
1229
+ * giant retrieved blob can't blow the context window (system is never trimmed).
1230
+ */
1231
+ declare class ChannelStack {
1232
+ private list;
1233
+ add(kind: ChannelKind, content: string, opts?: {
1234
+ source?: string;
1235
+ }): this;
1236
+ /** Convenience for the untrusted families — always firewall-fenced on render. */
1237
+ addUntrusted(kind: Extract<ChannelKind, "retrieved" | "tool" | "mcp" | "web" | "memory">, content: string, source?: string): this;
1238
+ channels(): Channel[];
1239
+ /**
1240
+ * Render the stack into provider-ready text, with hard tier isolation:
1241
+ * · `system` — system + developer channels (authoritative)
1242
+ * · `user` — user channels (the human's literal input)
1243
+ * · `dataBlocks` — every untrusted channel, firewalled + fenced as data
1244
+ * Returns the firewall verdicts so the caller can warn on detections.
1245
+ */
1246
+ render(opts: {
1247
+ policy: PolicyEngine;
1248
+ firewall: PromptFirewall;
1249
+ budget?: number;
1250
+ }): {
1251
+ system: string;
1252
+ user: string;
1253
+ dataBlocks: string;
1254
+ findings: FirewallVerdict[];
1255
+ };
1256
+ }
1257
+
1258
+ /**
1259
+ * SP-21 — Capability security model.
1260
+ *
1261
+ * Every tool, plugin, and MCP server runs with the LEAST authority it needs:
1262
+ * a small, declared set of { fs, network, shell, secrets } capabilities. This
1263
+ * module is the single chokepoint where those grants are declared and checked.
1264
+ *
1265
+ * The flow: on registration a caller `declare`s a manifest (or asks
1266
+ * `defaultManifestFor` to infer one from a known tool name); `check` resolves a
1267
+ * requirement against it. Anything we don't recognise gets NO_CAPS — grant
1268
+ * nothing until someone declares otherwise.
1269
+ *
1270
+ * STATUS: this layer is ADVISORY this pass — manifests are declared and shown
1271
+ * (`/caps`), and runtime side-effects are still gated by the permission layer
1272
+ * (shell/writes/commands) and the prompt firewall. `check()` is the hook a
1273
+ * future sandbox (SP-24) calls to hard-enforce caps before a syscall; it is not
1274
+ * yet on the execute path, so treat the manifest as a declared contract, not an
1275
+ * enforced boundary.
1276
+ */
1277
+
1278
+ /** Infer a conservative capability set from a known tool name + its origin. */
1279
+ declare function defaultManifestFor(toolName: string, origin: string): CapabilityManifest;
1280
+ /**
1281
+ * The central capability registry + gatekeeper. Manifests are declared once;
1282
+ * every privileged action is checked against the resolved grant. Pure and
1283
+ * deterministic — the only state is the declared manifest map.
1284
+ */
1285
+ declare class CapabilityEnforcer {
1286
+ private readonly manifests;
1287
+ /** Register (or overwrite) the manifest governing a tool/plugin/MCP id. */
1288
+ declare(manifest: CapabilityManifest): void;
1289
+ /**
1290
+ * Resolve the manifest for a name. Prefers an exact match; otherwise falls
1291
+ * back to a declared wildcard ("mcp__github__*" governs "mcp__github__x").
1292
+ * Returns undefined when nothing covers the name.
1293
+ */
1294
+ get(name: string): CapabilityManifest | undefined;
1295
+ /**
1296
+ * Decide whether `name` may exercise the requested capabilities. The grant
1297
+ * is the resolved manifest, or NO_CAPS when nothing is declared — so an
1298
+ * unknown tool is denied anything beyond the empty set.
1299
+ */
1300
+ check(name: string, need: Partial<CapabilitySet>): PolicyDecision;
1301
+ /** All declared manifests, in declaration order. */
1302
+ list(): CapabilityManifest[];
1303
+ }
1304
+
1305
+ /**
1306
+ * SP-21 — Prompt-injection DETECTORS.
1307
+ *
1308
+ * A detector reads a slice of UNTRUSTED text (a tool result, a fetched page, a
1309
+ * RAG chunk) and reports anything that looks like it is trying to seize the
1310
+ * conversation, smuggle secrets out, or hijack a tool. Detectors NEVER mutate
1311
+ * the text — they only describe what they saw. The firewall layer decides what
1312
+ * to do (warn / strip / block) with the verdicts these produce.
1313
+ *
1314
+ * Two house rules keep this file safe and predictable:
1315
+ * 1. Every regex is LINEAR-TIME. No nested quantifiers over open text
1316
+ * (`(a+)+`, `(.*)*`), so a hostile input can never trigger ReDoS — the
1317
+ * worst case is a single linear pass. Where we'd want "anything", we use a
1318
+ * bounded character run (`[^\n]{0,80}`) instead of an unbounded `.*`.
1319
+ * 2. Every reported `match` is clamped to ≤200 chars so a giant payload can't
1320
+ * blow up logs or the verdict it rides in.
1321
+ *
1322
+ * Pure and deterministic: no clock, no randomness, no I/O.
1323
+ */
1324
+
1325
+ /** The full battery, in the order the firewall runs them. */
1326
+ declare const ALL_DETECTORS: Detector[];
1327
+
1328
+ declare function isUntrustedTool(name: string): boolean;
1329
+ interface FirewallToolOptions {
1330
+ firewall: PromptFirewall;
1331
+ policy: PolicyEngine;
1332
+ /** Notified for every untrusted output processed (for terminal warnings). */
1333
+ onFinding?: (verdict: FirewallVerdict, tool: string) => void;
1334
+ }
1335
+ declare function applyFirewall(tools: ToolSet, opts: FirewallToolOptions): ToolSet;
1336
+
1337
+ /**
1338
+ * Colored unified-diff rendering — the inline edit preview. We show the
1339
+ * user exactly what an edit changes before it touches disk, so an approval is
1340
+ * an informed one. Built on the `diff` package's line differ, then painted with
1341
+ * our calm ANSI palette: green +, red -, dim context, with collapsed runs of
1342
+ * unchanged lines so a one-line edit doesn't print the whole file.
1343
+ */
1344
+ interface DiffStat {
1345
+ added: number;
1346
+ removed: number;
1347
+ }
1348
+ /** Count added / removed lines between two texts. */
1349
+ declare function diffStat(before: string, after: string): DiffStat;
1350
+ /** Print a colored diff for an edit (before → after) under a path header. */
1351
+ declare function renderEditDiff(path: string, before: string, after: string, opts?: {
1352
+ context?: number;
1353
+ maxLines?: number;
1354
+ }): void;
1355
+ /** Print a new-file preview (first N lines, all green). */
1356
+ declare function renderNewFileDiff(path: string, content: string, opts?: {
1357
+ maxLines?: number;
1358
+ }): void;
1359
+
1360
+ interface InputOptions {
1361
+ /** Live list of slash-command names (without the leading /), for completion. */
1362
+ commandNames: () => string[];
1363
+ /** Called when the user asks to quit (empty line + Ctrl-C, or twice). */
1364
+ onExit: () => void;
1365
+ }
1366
+ declare class InputController {
1367
+ private readonly opts;
1368
+ private rl;
1369
+ constructor(opts: InputOptions);
1370
+ /** Tab completion: /commands when the line starts with /, @paths otherwise. */
1371
+ private complete;
1372
+ /** Read one logical line, joining trailing-backslash continuations. */
1373
+ readLine(promptStr: string): Promise<string>;
1374
+ private saveHistory;
1375
+ /**
1376
+ * Ask a y/n/a-style question; resolves to the chosen choice key. Safe to call
1377
+ * mid-turn: if an interrupt-watch is active (raw mode) we suspend it, ask in
1378
+ * cooked mode, then re-arm — so ESC works before AND after a confirm, and the
1379
+ * prompt itself reads a normal line.
1380
+ */
1381
+ confirm(question: string, choices: ConfirmChoice[]): Promise<string>;
1382
+ private activeWatch;
1383
+ private onKey?;
1384
+ private startWatch;
1385
+ private stopWatch;
1386
+ /**
1387
+ * Run `fn` with an AbortSignal that fires on ESC / Ctrl-C — a streaming turn
1388
+ * can be interrupted cleanly without killing the process.
1389
+ */
1390
+ runInterruptible<T>(fn: (signal: AbortSignal) => Promise<T>): Promise<T>;
1391
+ close(): void;
1392
+ }
1393
+
1394
+ export { ALL_DETECTORS, Agent, type AgentOptions, type Authority, CapabilityEnforcer, type CapabilityManifest, type CapabilitySet, type Channel, type ChannelKind, ChannelStack, type CommandContext, CommandRegistry, type CommandResult, type ConfirmFn, DEFAULT_FIREWALL, DEFAULT_POLICY, DEFAULT_REGISTRY, type Detection, type Detector, type FirewallVerdict, type FsAccess, type HookEvent, HookRunner, type HookSet, InputController, type LintFinding, type LoadedMcp, type LoadedPlugins, type ModelKey, NO_CAPS, PERMISSION_MODES, type PermissionMode, Permissions, type PolicyAction, type PolicyConfig, type PolicyDecision, PolicyEngine, type ProjectMemory, PromptFirewall, PromptRegistry, type RoleLayer, type SafetyOverlay, type ScanResult, type ServerConfig, Session, type SessionMeta, type SessionSummary, type Severity, type SlashCommand, type TokenUsage, type ToolContext, type TurnResult, UNTRUSTED_CHANNELS, UsageMeter, applyFirewall, applyHooks, budgetChannels, buildProviderOptions, buildTools, buildWebTools, capsAllow, channelAuthority, compact, defaultManifestFor, diffStat, estimateCost, estimateTextTokens, estimateTokens, isPermissionMode, isUntrusted, isUntrustedTool, lintPrompt, lintSystemText, listModels, loadHookFiles, loadMcpTools, loadMdCommands, loadPlugins, loadProjectMemory, maxSeverity, mergeHookSets, modelContextWindow, modelId, modelLabel, renderEditDiff, renderNewFileDiff, renderTemplate, resolveModel, resolveModelKey, severityRank, supportsThinking };