ghost-dragon 4.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (226) hide show
  1. package/.github/workflows/ci.yml +23 -0
  2. package/CHANGELOG.md +96 -0
  3. package/README.md +193 -0
  4. package/bootstrap.ps1 +83 -0
  5. package/bootstrap.sh +71 -0
  6. package/dist/agent/loop.d.ts +68 -0
  7. package/dist/agent/loop.d.ts.map +1 -0
  8. package/dist/agent/loop.js +135 -0
  9. package/dist/agent/mcp.d.ts +33 -0
  10. package/dist/agent/mcp.d.ts.map +1 -0
  11. package/dist/agent/mcp.js +107 -0
  12. package/dist/agent/session.d.ts +16 -0
  13. package/dist/agent/session.d.ts.map +1 -0
  14. package/dist/agent/session.js +55 -0
  15. package/dist/agent/skills.d.ts +36 -0
  16. package/dist/agent/skills.d.ts.map +1 -0
  17. package/dist/agent/skills.js +153 -0
  18. package/dist/agent/stack.d.ts +21 -0
  19. package/dist/agent/stack.d.ts.map +1 -0
  20. package/dist/agent/stack.js +158 -0
  21. package/dist/agent/task.d.ts +21 -0
  22. package/dist/agent/task.d.ts.map +1 -0
  23. package/dist/agent/task.js +45 -0
  24. package/dist/agent/tools.d.ts +44 -0
  25. package/dist/agent/tools.d.ts.map +1 -0
  26. package/dist/agent/tools.js +262 -0
  27. package/dist/agent/trace.d.ts +34 -0
  28. package/dist/agent/trace.d.ts.map +1 -0
  29. package/dist/agent/trace.js +72 -0
  30. package/dist/agent.d.ts +46 -0
  31. package/dist/agent.d.ts.map +1 -0
  32. package/dist/agent.js +103 -0
  33. package/dist/auth.d.ts +74 -0
  34. package/dist/auth.d.ts.map +1 -0
  35. package/dist/auth.js +116 -0
  36. package/dist/brain/anthropic.d.ts +19 -0
  37. package/dist/brain/anthropic.d.ts.map +1 -0
  38. package/dist/brain/anthropic.js +74 -0
  39. package/dist/brain/claude-cli.d.ts +20 -0
  40. package/dist/brain/claude-cli.d.ts.map +1 -0
  41. package/dist/brain/claude-cli.js +79 -0
  42. package/dist/brain/ghost-ember.d.ts +28 -0
  43. package/dist/brain/ghost-ember.d.ts.map +1 -0
  44. package/dist/brain/ghost-ember.js +97 -0
  45. package/dist/brain/index.d.ts +22 -0
  46. package/dist/brain/index.d.ts.map +1 -0
  47. package/dist/brain/index.js +95 -0
  48. package/dist/brain/openai-compat.d.ts +21 -0
  49. package/dist/brain/openai-compat.d.ts.map +1 -0
  50. package/dist/brain/openai-compat.js +119 -0
  51. package/dist/brain/router/classify.d.ts +23 -0
  52. package/dist/brain/router/classify.d.ts.map +1 -0
  53. package/dist/brain/router/classify.js +160 -0
  54. package/dist/brain/router/execute.d.ts +23 -0
  55. package/dist/brain/router/execute.d.ts.map +1 -0
  56. package/dist/brain/router/execute.js +84 -0
  57. package/dist/brain/router/index.d.ts +26 -0
  58. package/dist/brain/router/index.d.ts.map +1 -0
  59. package/dist/brain/router/index.js +118 -0
  60. package/dist/brain/router/routing-memory.d.ts +27 -0
  61. package/dist/brain/router/routing-memory.d.ts.map +1 -0
  62. package/dist/brain/router/routing-memory.js +77 -0
  63. package/dist/brain/router/select.d.ts +32 -0
  64. package/dist/brain/router/select.d.ts.map +1 -0
  65. package/dist/brain/router/select.js +146 -0
  66. package/dist/brain/router/two-hop.d.ts +23 -0
  67. package/dist/brain/router/two-hop.d.ts.map +1 -0
  68. package/dist/brain/router/two-hop.js +39 -0
  69. package/dist/brain/router/verify.d.ts +37 -0
  70. package/dist/brain/router/verify.d.ts.map +1 -0
  71. package/dist/brain/router/verify.js +111 -0
  72. package/dist/brain/types.d.ts +55 -0
  73. package/dist/brain/types.d.ts.map +1 -0
  74. package/dist/brain/types.js +16 -0
  75. package/dist/brain/worker.d.ts +27 -0
  76. package/dist/brain/worker.d.ts.map +1 -0
  77. package/dist/brain/worker.js +71 -0
  78. package/dist/commands/ai.d.ts +24 -0
  79. package/dist/commands/ai.d.ts.map +1 -0
  80. package/dist/commands/ai.js +137 -0
  81. package/dist/commands/alerts.d.ts +19 -0
  82. package/dist/commands/alerts.d.ts.map +1 -0
  83. package/dist/commands/alerts.js +114 -0
  84. package/dist/commands/billing.d.ts +13 -0
  85. package/dist/commands/billing.d.ts.map +1 -0
  86. package/dist/commands/billing.js +55 -0
  87. package/dist/commands/chat.d.ts +22 -0
  88. package/dist/commands/chat.d.ts.map +1 -0
  89. package/dist/commands/chat.js +422 -0
  90. package/dist/commands/config.d.ts +18 -0
  91. package/dist/commands/config.d.ts.map +1 -0
  92. package/dist/commands/config.js +136 -0
  93. package/dist/commands/doctor.d.ts +11 -0
  94. package/dist/commands/doctor.d.ts.map +1 -0
  95. package/dist/commands/doctor.js +73 -0
  96. package/dist/commands/global.d.ts +11 -0
  97. package/dist/commands/global.d.ts.map +1 -0
  98. package/dist/commands/global.js +253 -0
  99. package/dist/commands/keep.d.ts +12 -0
  100. package/dist/commands/keep.d.ts.map +1 -0
  101. package/dist/commands/keep.js +58 -0
  102. package/dist/commands/lifecycle.d.ts +17 -0
  103. package/dist/commands/lifecycle.d.ts.map +1 -0
  104. package/dist/commands/lifecycle.js +267 -0
  105. package/dist/commands/login.d.ts +16 -0
  106. package/dist/commands/login.d.ts.map +1 -0
  107. package/dist/commands/login.js +234 -0
  108. package/dist/commands/maintenance.d.ts +12 -0
  109. package/dist/commands/maintenance.d.ts.map +1 -0
  110. package/dist/commands/maintenance.js +76 -0
  111. package/dist/commands/mcp.d.ts +16 -0
  112. package/dist/commands/mcp.d.ts.map +1 -0
  113. package/dist/commands/mcp.js +56 -0
  114. package/dist/commands/memory.d.ts +13 -0
  115. package/dist/commands/memory.d.ts.map +1 -0
  116. package/dist/commands/memory.js +218 -0
  117. package/dist/commands/osint.d.ts +14 -0
  118. package/dist/commands/osint.d.ts.map +1 -0
  119. package/dist/commands/osint.js +161 -0
  120. package/dist/commands/pentest.d.ts +13 -0
  121. package/dist/commands/pentest.d.ts.map +1 -0
  122. package/dist/commands/pentest.js +131 -0
  123. package/dist/commands/scale.d.ts +14 -0
  124. package/dist/commands/scale.d.ts.map +1 -0
  125. package/dist/commands/scale.js +191 -0
  126. package/dist/commands/serve.d.ts +16 -0
  127. package/dist/commands/serve.d.ts.map +1 -0
  128. package/dist/commands/serve.js +167 -0
  129. package/dist/commands/tui.d.ts +17 -0
  130. package/dist/commands/tui.d.ts.map +1 -0
  131. package/dist/commands/tui.js +138 -0
  132. package/dist/commands/wyrm.d.ts +20 -0
  133. package/dist/commands/wyrm.d.ts.map +1 -0
  134. package/dist/commands/wyrm.js +274 -0
  135. package/dist/config.d.ts +67 -0
  136. package/dist/config.d.ts.map +1 -0
  137. package/dist/config.js +54 -0
  138. package/dist/index.d.ts +16 -0
  139. package/dist/index.d.ts.map +1 -0
  140. package/dist/index.js +85 -0
  141. package/dist/manifest.d.ts +31 -0
  142. package/dist/manifest.d.ts.map +1 -0
  143. package/dist/manifest.js +83 -0
  144. package/dist/ui.d.ts +57 -0
  145. package/dist/ui.d.ts.map +1 -0
  146. package/dist/ui.js +174 -0
  147. package/dist/utils.d.ts +33 -0
  148. package/dist/utils.d.ts.map +1 -0
  149. package/dist/utils.js +155 -0
  150. package/dist/wyrm/mcp.d.ts +37 -0
  151. package/dist/wyrm/mcp.d.ts.map +1 -0
  152. package/dist/wyrm/mcp.js +137 -0
  153. package/docs/SYSTEM-PREMORTEM.md +397 -0
  154. package/dragon-manifest.toml +241 -0
  155. package/dragon.py +177 -0
  156. package/install/launchd/lk.ghosts.dragonkeep.plist +57 -0
  157. package/install/systemd/dragonkeep.service +40 -0
  158. package/media/dragon-silver-lockup.svg +931 -0
  159. package/media/dragon-silver-mark.svg +931 -0
  160. package/media/dragon-silver.png +0 -0
  161. package/package.json +45 -0
  162. package/specs/001-godmode/constitution.md +54 -0
  163. package/specs/001-godmode/plan.md +30 -0
  164. package/specs/001-godmode/spec.md +64 -0
  165. package/specs/001-godmode/tasks.md +35 -0
  166. package/specs/002-premortem-positioning/premortem.md +211 -0
  167. package/src/agent/loop.ts +165 -0
  168. package/src/agent/mcp.ts +92 -0
  169. package/src/agent/session.ts +48 -0
  170. package/src/agent/skills.ts +138 -0
  171. package/src/agent/stack.ts +154 -0
  172. package/src/agent/task.ts +55 -0
  173. package/src/agent/tools.ts +255 -0
  174. package/src/agent/trace.ts +76 -0
  175. package/src/agent.ts +114 -0
  176. package/src/auth.ts +133 -0
  177. package/src/brain/anthropic.ts +83 -0
  178. package/src/brain/claude-cli.ts +78 -0
  179. package/src/brain/ghost-ember.ts +94 -0
  180. package/src/brain/index.ts +99 -0
  181. package/src/brain/openai-compat.ts +115 -0
  182. package/src/brain/router/classify.ts +167 -0
  183. package/src/brain/router/execute.ts +80 -0
  184. package/src/brain/router/index.ts +125 -0
  185. package/src/brain/router/routing-memory.ts +71 -0
  186. package/src/brain/router/select.ts +156 -0
  187. package/src/brain/router/two-hop.ts +62 -0
  188. package/src/brain/router/verify.ts +123 -0
  189. package/src/brain/types.ts +61 -0
  190. package/src/brain/worker.ts +72 -0
  191. package/src/commands/ai.ts +144 -0
  192. package/src/commands/alerts.ts +131 -0
  193. package/src/commands/billing.ts +59 -0
  194. package/src/commands/chat.ts +318 -0
  195. package/src/commands/config.ts +137 -0
  196. package/src/commands/doctor.ts +71 -0
  197. package/src/commands/global.ts +256 -0
  198. package/src/commands/keep.ts +67 -0
  199. package/src/commands/lifecycle.ts +273 -0
  200. package/src/commands/login.ts +184 -0
  201. package/src/commands/maintenance.ts +54 -0
  202. package/src/commands/mcp.ts +57 -0
  203. package/src/commands/memory.ts +229 -0
  204. package/src/commands/osint.ts +171 -0
  205. package/src/commands/pentest.ts +140 -0
  206. package/src/commands/scale.ts +185 -0
  207. package/src/commands/serve.ts +171 -0
  208. package/src/commands/tui.ts +126 -0
  209. package/src/commands/wyrm.ts +269 -0
  210. package/src/config.ts +93 -0
  211. package/src/index.ts +92 -0
  212. package/src/manifest.ts +104 -0
  213. package/src/ui.ts +188 -0
  214. package/src/utils.ts +153 -0
  215. package/src/wyrm/mcp.ts +130 -0
  216. package/test/auth.test.ts +70 -0
  217. package/test/brain.test.ts +39 -0
  218. package/test/security.test.ts +104 -0
  219. package/test/skills.test.ts +38 -0
  220. package/test/ui.test.ts +46 -0
  221. package/tsconfig.json +19 -0
  222. package/worker/package-lock.json +1527 -0
  223. package/worker/package.json +17 -0
  224. package/worker/src/index.ts +76 -0
  225. package/worker/tsconfig.json +15 -0
  226. package/worker/wrangler.toml +26 -0
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Stack-fused tools — the agent IS the operator console. It can read the live Ghost
3
+ * Protocol stack state and drive `dragon` subcommands (scale/wyrm/pentest/keep/net…)
4
+ * as first-class tools, not just guess at bash.
5
+ *
6
+ * Safety: read-only verbs run freely; everything else (pentest, deploy, install…) is
7
+ * `dangerous` → always prompts + shows the full command (never covered by --auto).
8
+ * Interactive/recursive/long-lived verbs (chat/ask/login/serve/up) are refused.
9
+ * Each invocation is a fresh subprocess of THIS CLI, time-bounded + output-capped.
10
+ *
11
+ * Copyright 2026 Ghost Protocol (Pvt) Ltd. All Rights Reserved.
12
+ */
13
+ import { execFile } from 'node:child_process';
14
+ import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs';
15
+ import { join } from 'node:path';
16
+ import { loadConfig } from '../config.js';
17
+ const SELF = process.argv[1] ?? ''; // the dragon CLI entry (dist/index.js)
18
+ const SAFE = new Set(['list', 'doctor', 'status', 'brains', 'whoami']); // read-only → no approval
19
+ const BLOCKED = new Set(['chat', 'ask', 'login', 'logout', 'serve', 'up', 'down', 'init']); // interactive / recursive / long-lived
20
+ const MAX_OUT = 20_000;
21
+ function runDragon(args, cwd, timeout) {
22
+ return new Promise((res) => {
23
+ execFile(process.execPath, [SELF, ...args], { cwd, timeout, maxBuffer: 1 << 20 }, (err, stdout, stderr) => {
24
+ const out = ((stdout || '') + (stderr || '')).slice(0, MAX_OUT);
25
+ const code = err && typeof err.code === 'number' ? err.code : err ? 1 : 0;
26
+ res({ out, code });
27
+ });
28
+ });
29
+ }
30
+ /** Run an arbitrary external process (a stack product), bounded + capped.
31
+ * Keeps stdout/stderr SEPARATE — products print progress to stderr and the
32
+ * machine-readable JSON to stdout, so a parser must read stdout alone. */
33
+ function runProc(cmd, args, cwd, timeout) {
34
+ return new Promise((res) => {
35
+ execFile(cmd, args, { cwd, timeout, maxBuffer: 8 << 20 }, (err, stdout, stderr) => {
36
+ const code = err && typeof err.code === 'number' ? err.code : err ? 1 : 0;
37
+ res({ stdout: stdout || '', stderr: stderr || '', code });
38
+ });
39
+ });
40
+ }
41
+ /** Newest reports/<dir>/findings.json under a PhantomDragon repo (the scan output). */
42
+ function newestFindings(pentestRoot) {
43
+ const dir = join(pentestRoot, 'reports');
44
+ if (!existsSync(dir))
45
+ return null;
46
+ const subs = readdirSync(dir).map((d) => join(dir, d)).filter((p) => { try {
47
+ return statSync(p).isDirectory();
48
+ }
49
+ catch {
50
+ return false;
51
+ } });
52
+ if (!subs.length)
53
+ return null;
54
+ subs.sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs);
55
+ const f = join(subs[0], 'findings.json');
56
+ return existsSync(f) ? f : null;
57
+ }
58
+ export function makeStackTools() {
59
+ const specs = [
60
+ {
61
+ name: 'stack_status',
62
+ description: 'Live state of the Ghost Protocol dragon stack (each product: installed / running / version), as JSON. Read-only — call it to know what is deployed before acting.',
63
+ parameters: { type: 'object', properties: {}, required: [] },
64
+ },
65
+ {
66
+ name: 'stack_run',
67
+ description: 'Run a `dragon` stack command for an ops task — e.g. "list", "doctor", "pentest scan <url>", "wyrm <…>", "keep <…>". Read-only verbs run freely; actions require approval. NOT for chat/ask/login/serve.',
68
+ parameters: { type: 'object', properties: { command: { type: 'string', description: 'the dragon subcommand + args, e.g. "pentest scan https://example.com"' } }, required: ['command'] },
69
+ },
70
+ {
71
+ name: 'stack_pentest',
72
+ description: 'Run a PhantomDragon web security scan against a URL (AUTHORIZED targets only) and return STRUCTURED findings (severity, category, CVSS, confirmed, remediation) + a risk summary. Long-running; writes a report. Requires PhantomDragon configured.',
73
+ parameters: { type: 'object', properties: { url: { type: 'string' }, profile: { type: 'string', enum: ['quick', 'standard', 'full', 'api_only', 'auth_only'] }, mode: { type: 'string', enum: ['safe', 'standard', 'aggressive'] } }, required: ['url'] },
74
+ },
75
+ {
76
+ name: 'stack_keep',
77
+ description: 'Run a DragonKeep system-security scan (READ-ONLY local inspection) and return STRUCTURED findings (severity, CVSS, fix) + a summary. scan_type: full | quick | malware | ransomware | hunt | score.',
78
+ parameters: { type: 'object', properties: { scan_type: { type: 'string', enum: ['full', 'quick', 'malware', 'ransomware', 'hunt', 'score'] }, profile: { type: 'string' } }, required: [] },
79
+ },
80
+ ];
81
+ return {
82
+ specs,
83
+ handles: (name) => ['stack_status', 'stack_run', 'stack_pentest', 'stack_keep'].includes(name),
84
+ async call(name, args, ctx) {
85
+ if (name === 'stack_pentest')
86
+ return runPentest(args, ctx);
87
+ if (name === 'stack_keep')
88
+ return runKeep(args, ctx);
89
+ if (!SELF)
90
+ return 'error: stack tools unavailable (no CLI entry path)';
91
+ if (name === 'stack_status') {
92
+ const r = await runDragon(['list', '--json'], ctx.cwd, 30_000);
93
+ return r.out || '(no output)';
94
+ }
95
+ const command = String(args.command ?? '').trim();
96
+ if (!command)
97
+ return 'error: no command given';
98
+ const parts = command.split(/\s+/);
99
+ const verb = parts[0];
100
+ if (BLOCKED.has(verb))
101
+ return `error: "${verb}" can't be run as a tool (interactive / recursive / long-lived)`;
102
+ if (!SAFE.has(verb) && !(await ctx.approve(`dragon ${command.slice(0, 100)}`, { detail: `dragon ${command}`, dangerous: true }))) {
103
+ return 'error: declined by user';
104
+ }
105
+ const r = await runDragon(parts, ctx.cwd, 180_000);
106
+ return `exit ${r.code}\n${r.out || '(no output)'}`;
107
+ },
108
+ };
109
+ }
110
+ // ── Structured stack actions (typed results, not just passthrough) ──
111
+ async function runPentest(args, ctx) {
112
+ const root = loadConfig().products?.pentest?.path;
113
+ if (!root || !existsSync(root))
114
+ return 'error: PhantomDragon not configured — set products.pentest.path (run `dragon init`).';
115
+ const script = ['phantom_dragon_ai.py', 'phantomdragon.py'].map((s) => join(root, s)).find(existsSync);
116
+ if (!script)
117
+ return `error: PhantomDragon entry script not found in ${root}`;
118
+ const url = String(args.url ?? '').trim();
119
+ if (!url)
120
+ return 'error: url is required';
121
+ const profile = String(args.profile ?? 'standard');
122
+ const mode = String(args.mode ?? 'safe');
123
+ if (!(await ctx.approve(`⚠ pentest scan ${url} (profile ${profile}, mode ${mode}) — authorized targets only`, { dangerous: true })))
124
+ return 'error: declined by user';
125
+ const r = await runProc('python3', [script, '-t', url, '--profile', profile, '--mode', mode], root, 600_000);
126
+ const fj = newestFindings(root);
127
+ if (!fj)
128
+ return `scan exited ${r.code}; no findings.json produced.\n${(r.stderr || r.stdout).slice(-1200)}`;
129
+ try {
130
+ const d = JSON.parse(readFileSync(fj, 'utf-8'));
131
+ const findings = (d.findings ?? []).slice(0, 15).map((f) => ({ id: f.id, title: f.title, severity: f.severity, category: f.category, url: f.url, confirmed: f.confirmed, cvss: f.cvss_score, remediation: (f.remediation ?? '').slice(0, 160) }));
132
+ return JSON.stringify({ report: fj, meta: d.meta, summary: d.summary, findings }).slice(0, MAX_OUT);
133
+ }
134
+ catch {
135
+ return `scan complete but findings.json could not be parsed at ${fj}`;
136
+ }
137
+ }
138
+ async function runKeep(args, ctx) {
139
+ const keepRoot = loadConfig().products?.keep?.path;
140
+ const bin = [keepRoot && join(keepRoot, 'target/release/dragonkeep'), keepRoot && join(keepRoot, 'target/debug/dragonkeep'), '/usr/local/bin/dragonkeep', '/usr/bin/dragonkeep'].filter(Boolean).find((p) => existsSync(p)) ?? 'dragonkeep';
141
+ const scanType = String(args.scan_type ?? 'quick');
142
+ const direct = ['malware', 'ransomware', 'hunt', 'score', 'ai', 'supply', 'anomaly'];
143
+ const cmd = ['--quiet', '--format', 'json'];
144
+ if (direct.includes(scanType))
145
+ cmd.push(scanType);
146
+ else
147
+ cmd.push('scan', '--profile', String(args.profile ?? (scanType === 'full' ? 'standard' : 'quick')));
148
+ const r = await runProc(bin, cmd, ctx.cwd, 300_000);
149
+ try {
150
+ const d = JSON.parse(r.stdout);
151
+ const sections = (d.sections ?? []).map((s) => ({ name: s.name, findings: (s.findings ?? []).filter((f) => f.severity !== 'Pass').slice(0, 8).map((f) => ({ title: f.title, severity: f.severity, cvss: f.cvss, fix: (f.fix ?? '').slice(0, 140) })) }));
152
+ return JSON.stringify({ hostname: d.hostname, summary: d.summary, sections }).slice(0, MAX_OUT);
153
+ }
154
+ catch {
155
+ return `dragonkeep exited ${r.code}${bin === 'dragonkeep' ? ' (binary not found — build it: cargo build --release in DragonKeep, or `dragon install keep`)' : ''}\n${(r.stderr || r.stdout).slice(0, 1500)}`;
156
+ }
157
+ }
158
+ //# sourceMappingURL=stack.js.map
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Sub-agents — the `task` tool. The agent can spawn a focused, READ-ONLY sub-agent
3
+ * to research/analyze a sub-problem (audit a file, summarize how something works,
4
+ * survey a codebase) without polluting the main thread. The sub-agent can read,
5
+ * grep, glob, list, and use skills — but cannot write, edit, or run shell, and
6
+ * cannot itself spawn tasks (no recursion / fork-bombs). It returns its findings.
7
+ *
8
+ * Copyright 2026 Ghost Protocol (Pvt) Ltd. All Rights Reserved.
9
+ */
10
+ import type { Brain, ToolSpec } from '../brain/types.js';
11
+ import type { SkillLibrary } from './skills.js';
12
+ export interface TaskTool {
13
+ spec: ToolSpec;
14
+ call(args: Record<string, unknown>): Promise<string>;
15
+ }
16
+ export declare function makeTaskTool(parent: {
17
+ brain: Brain;
18
+ skills: SkillLibrary | null;
19
+ cwd: string;
20
+ }): TaskTool;
21
+ //# sourceMappingURL=task.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"task.d.ts","sourceRoot":"","sources":["../../src/agent/task.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AACxD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAI/C,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,QAAQ,CAAA;IACd,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;CACrD;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,YAAY,GAAG,IAAI,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,QAAQ,CAkCzG"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Sub-agents — the `task` tool. The agent can spawn a focused, READ-ONLY sub-agent
3
+ * to research/analyze a sub-problem (audit a file, summarize how something works,
4
+ * survey a codebase) without polluting the main thread. The sub-agent can read,
5
+ * grep, glob, list, and use skills — but cannot write, edit, or run shell, and
6
+ * cannot itself spawn tasks (no recursion / fork-bombs). It returns its findings.
7
+ *
8
+ * Copyright 2026 Ghost Protocol (Pvt) Ltd. All Rights Reserved.
9
+ */
10
+ import { runAgent, buildSystemPrompt } from './loop.js';
11
+ export function makeTaskTool(parent) {
12
+ return {
13
+ spec: {
14
+ name: 'task',
15
+ description: 'Spawn a focused READ-ONLY sub-agent to research or analyze a sub-problem in isolation (e.g. "audit src/auth.ts for security bugs", "summarize how the brain adapter works"). It can read/grep/glob/list + use skills, but CANNOT write, edit, or run shell. Returns its findings. Use it to offload big read-heavy investigations and keep your own context clean.',
16
+ parameters: { type: 'object', properties: { task: { type: 'string', description: 'the self-contained subtask to investigate' } }, required: ['task'] },
17
+ },
18
+ async call(args) {
19
+ const prompt = String(args.task ?? '').trim();
20
+ if (!prompt)
21
+ return 'error: no task given';
22
+ const denyAll = { cwd: parent.cwd, approve: async () => false }; // read-only sub-agent
23
+ const subDeps = {
24
+ brain: parent.brain,
25
+ wyrm: null, portal: null, stack: null, mcp: null, task: null, // sub-agent: read + skills only, NO recursion
26
+ skills: parent.skills,
27
+ cwd: parent.cwd,
28
+ system: buildSystemPrompt({ cwd: parent.cwd, wyrm: false, portal: false, brainId: `${parent.brain.id}:${parent.brain.model}`, skills: parent.skills?.count || undefined }) +
29
+ '\n\nYou are a FOCUSED SUB-AGENT spawned for ONE read-only investigation. Use read_file/grep/glob/list_dir/skill_* only — you CANNOT write, edit, or run shell (those are denied). Investigate thoroughly, then return a concise, complete, self-contained answer. Do not ask questions.',
30
+ toolCtx: denyAll,
31
+ messages: [],
32
+ };
33
+ let out = '';
34
+ const quiet = { onAssistantStart() { }, onDelta(s) { out += s; }, onToolStart() { }, onToolEnd() { } };
35
+ try {
36
+ await runAgent(subDeps, prompt, quiet, new AbortController().signal);
37
+ return out.trim() || '(sub-agent returned nothing)';
38
+ }
39
+ catch (e) {
40
+ return `sub-agent error: ${String(e instanceof Error ? e.message : e)}`;
41
+ }
42
+ },
43
+ };
44
+ }
45
+ //# sourceMappingURL=task.js.map
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Local coding tools — the agent's hands. These run on THIS machine (that's the
3
+ * whole point of a CLI coding agent vs the hosted Worker assistant): read/write/
4
+ * edit files, list/glob/grep the tree, run shell commands.
5
+ *
6
+ * Mutating tools (write_file, edit_file, bash) pass through an approval gate so
7
+ * nothing touches the disk or runs a command without a yes — unless the session
8
+ * is in auto-approve. Read-only tools (read_file, list_dir, glob, grep) run free.
9
+ *
10
+ * grep/glob shell out to ripgrep (present on this box) for speed + .gitignore
11
+ * awareness; output is capped so a huge result can't blow the context window.
12
+ *
13
+ * Copyright 2026 Ghost Protocol (Pvt) Ltd. All Rights Reserved.
14
+ */
15
+ import type { ToolSpec } from '../brain/types.js';
16
+ export interface ToolContext {
17
+ cwd: string;
18
+ /** Returns true to proceed. `dangerous` actions (bash, anything outside the
19
+ * working dir) are NEVER covered by --auto — they always prompt. */
20
+ approve: (summary: string, opts?: {
21
+ detail?: string;
22
+ dangerous?: boolean;
23
+ }) => Promise<boolean>;
24
+ /** When true, run bash inside a bwrap sandbox (cwd writable, rest read-only,
25
+ * credential dirs masked). Filesystem confinement only — network is shared. */
26
+ sandbox?: boolean;
27
+ }
28
+ export declare const BWRAP: string | null;
29
+ /** Resolve a path and classify it: inside cwd, outside, or protected (symlink-aware). */
30
+ export declare function guardPath(cwd: string, p: string): {
31
+ abs: string;
32
+ outside: boolean;
33
+ protectedPath: boolean;
34
+ };
35
+ export interface LocalTool {
36
+ spec: ToolSpec;
37
+ mutating: boolean;
38
+ summary(args: Record<string, unknown>): string;
39
+ run(args: Record<string, unknown>, ctx: ToolContext): Promise<string>;
40
+ }
41
+ export declare const LOCAL_TOOLS: LocalTool[];
42
+ export declare function localToolSpecs(): ToolSpec[];
43
+ export declare function getLocalTool(name: string): LocalTool | undefined;
44
+ //# sourceMappingURL=tools.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../../src/agent/tools.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAKH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAKjD,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAA;IACX;yEACqE;IACrE,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;IAC/F;oFACgF;IAChF,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,eAAO,MAAM,KAAK,eAAgF,CAAA;AAqBlG,yFAAyF;AACzF,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,aAAa,EAAE,OAAO,CAAA;CAAE,CAU3G;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,QAAQ,CAAA;IACd,QAAQ,EAAE,OAAO,CAAA;IACjB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAA;IAC9C,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;CACtE;AA6KD,eAAO,MAAM,WAAW,EAAE,SAAS,EAAmE,CAAA;AAGtG,wBAAgB,cAAc,IAAI,QAAQ,EAAE,CAE3C;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,CAEhE"}
@@ -0,0 +1,262 @@
1
+ /**
2
+ * Local coding tools — the agent's hands. These run on THIS machine (that's the
3
+ * whole point of a CLI coding agent vs the hosted Worker assistant): read/write/
4
+ * edit files, list/glob/grep the tree, run shell commands.
5
+ *
6
+ * Mutating tools (write_file, edit_file, bash) pass through an approval gate so
7
+ * nothing touches the disk or runs a command without a yes — unless the session
8
+ * is in auto-approve. Read-only tools (read_file, list_dir, glob, grep) run free.
9
+ *
10
+ * grep/glob shell out to ripgrep (present on this box) for speed + .gitignore
11
+ * awareness; output is capped so a huge result can't blow the context window.
12
+ *
13
+ * Copyright 2026 Ghost Protocol (Pvt) Ltd. All Rights Reserved.
14
+ */
15
+ import { spawn } from 'node:child_process';
16
+ import { readFileSync, writeFileSync, mkdirSync, statSync, readdirSync, existsSync, realpathSync } from 'node:fs';
17
+ import { resolve, dirname, isAbsolute, join, relative } from 'node:path';
18
+ const MAX_OUTPUT = 30_000; // chars fed back to the model per tool result
19
+ const MAX_READ_BYTES = 200_000;
20
+ export const BWRAP = ['/usr/bin/bwrap', '/usr/local/bin/bwrap'].find((p) => existsSync(p)) ?? null;
21
+ /** Build the argv for a shell command, optionally wrapped in a bwrap jail. */
22
+ function bashArgv(command, cwd, sandbox) {
23
+ if (sandbox && BWRAP) {
24
+ const home = process.env.HOME || '';
25
+ const mask = ['.ssh', '.aws', '.gnupg', '.dragon', '.config/gh', '.npmrc'].flatMap((d) => ['--tmpfs', `${home}/${d}`]);
26
+ return {
27
+ cmd: BWRAP,
28
+ args: ['--ro-bind', '/', '/', '--bind', cwd, cwd, '--tmpfs', '/tmp', '--dev', '/dev', '--proc', '/proc', '--unshare-pid', ...mask, '--chdir', cwd, '--', 'bash', '-c', command],
29
+ };
30
+ }
31
+ return { cmd: 'bash', args: ['-c', command] };
32
+ }
33
+ // Paths the agent must never touch — credentials/keys — even with approval.
34
+ const PROTECTED = [/(^|\/)\.ssh(\/|$)/, /(^|\/)\.aws(\/|$)/, /(^|\/)\.gnupg(\/|$)/, /(^|\/)\.dragon(\/|$)/, /(^|\/)\.git\/config$/, /\.env(\.[\w-]+)?$/, /id_(rsa|ed25519|ecdsa)/, /\.(pem|key|p12|keystore)$/, /(^|\/)\.(npmrc|netrc|pgpass)$/];
35
+ // Obviously-catastrophic shell — flagged for a louder confirm (never auto-run).
36
+ const CATASTROPHIC = /\brm\s+-[rf]{1,2}\b[^|]*[\s/~*]|\bmkfs\b|\bdd\s+if=|>\s*\/dev\/(sd|nvme|disk)|:\(\)\s*\{\s*:\s*\|\s*:|(curl|wget)\b[^|]*\|\s*(sudo\s+)?(ba)?sh\b|\bchmod\s+-R?\s*777\s+\//i;
37
+ /** Resolve a path and classify it: inside cwd, outside, or protected (symlink-aware). */
38
+ export function guardPath(cwd, p) {
39
+ const absPath = isAbsolute(p) ? resolve(p) : resolve(cwd, p);
40
+ const protectedPath = PROTECTED.some((re) => re.test(absPath));
41
+ let real = absPath;
42
+ try {
43
+ real = realpathSync(absPath);
44
+ }
45
+ catch { /* may not exist yet (new file) */ }
46
+ let root = resolve(cwd);
47
+ try {
48
+ root = realpathSync(cwd);
49
+ }
50
+ catch { /* keep resolved */ }
51
+ const within = (x) => x === root || x.startsWith(root + '/');
52
+ const outside = !(within(real) || within(absPath));
53
+ return { abs: absPath, outside, protectedPath };
54
+ }
55
+ function clamp(s) {
56
+ return s.length > MAX_OUTPUT ? s.slice(0, MAX_OUTPUT) + `\n… [truncated ${s.length - MAX_OUTPUT} chars]` : s;
57
+ }
58
+ function capture(cmd, args, opts) {
59
+ return new Promise((res) => {
60
+ const child = spawn(cmd, args, { cwd: opts.cwd });
61
+ let stdout = '';
62
+ let stderr = '';
63
+ const timer = setTimeout(() => { try {
64
+ child.kill('SIGKILL');
65
+ }
66
+ catch { } }, opts.timeout ?? 120_000);
67
+ child.stdout.on('data', (d) => { if (stdout.length < MAX_OUTPUT * 2)
68
+ stdout += d; });
69
+ child.stderr.on('data', (d) => { if (stderr.length < MAX_OUTPUT)
70
+ stderr += d; });
71
+ child.on('error', (e) => { clearTimeout(timer); res({ stdout, stderr: stderr + String(e), code: 127 }); });
72
+ child.on('close', (code) => { clearTimeout(timer); res({ stdout, stderr, code: code ?? 0 }); });
73
+ if (opts.input !== undefined) {
74
+ child.stdin.write(opts.input);
75
+ child.stdin.end();
76
+ }
77
+ });
78
+ }
79
+ const read_file = {
80
+ mutating: false,
81
+ summary: (a) => `read ${a.path}`,
82
+ spec: {
83
+ name: 'read_file',
84
+ description: 'Read a UTF-8 text file. Returns the content with 1-based line numbers. Use offset/limit for large files.',
85
+ parameters: { type: 'object', properties: { path: { type: 'string' }, offset: { type: 'number', description: '1-based start line' }, limit: { type: 'number', description: 'max lines' } }, required: ['path'] },
86
+ },
87
+ async run(a, ctx) {
88
+ const g = guardPath(ctx.cwd, String(a.path));
89
+ if (g.protectedPath)
90
+ return `error: ${a.path} is a protected path (credentials/keys) — refused`;
91
+ if (g.outside && !(await ctx.approve(`read OUTSIDE the working dir: ${a.path}`, { dangerous: true })))
92
+ return 'error: read declined (outside working directory)';
93
+ const p = g.abs;
94
+ if (!existsSync(p))
95
+ return `error: no such file: ${a.path}`;
96
+ const st = statSync(p);
97
+ if (st.isDirectory())
98
+ return `error: ${a.path} is a directory (use list_dir)`;
99
+ if (st.size > MAX_READ_BYTES)
100
+ return `error: file too large (${st.size} bytes). Use grep or offset/limit.`;
101
+ const lines = readFileSync(p, 'utf-8').split('\n');
102
+ const start = a.offset ? Math.max(1, Number(a.offset)) : 1;
103
+ const end = a.limit ? start + Number(a.limit) - 1 : lines.length;
104
+ const slice = lines.slice(start - 1, end).map((l, i) => `${String(start + i).padStart(5)}\t${l}`).join('\n');
105
+ return clamp(slice || '(empty file)');
106
+ },
107
+ };
108
+ const write_file = {
109
+ mutating: true,
110
+ summary: (a) => `write ${a.path} (${String(a.content ?? '').length} bytes)`,
111
+ spec: {
112
+ name: 'write_file',
113
+ description: 'Create or overwrite a file with the given content. Creates parent directories. Prefer edit_file for changes to existing files.',
114
+ parameters: { type: 'object', properties: { path: { type: 'string' }, content: { type: 'string' } }, required: ['path', 'content'] },
115
+ },
116
+ async run(a, ctx) {
117
+ const g = guardPath(ctx.cwd, String(a.path));
118
+ if (g.protectedPath)
119
+ return `error: ${a.path} is a protected path — refused`;
120
+ const p = g.abs;
121
+ const content = String(a.content ?? '');
122
+ const existed = existsSync(p);
123
+ if (!(await ctx.approve(`${existed ? 'overwrite' : 'create'} ${relative(ctx.cwd, p) || a.path}${g.outside ? ' (OUTSIDE working dir)' : ''}`, { detail: content.slice(0, 800), dangerous: g.outside })))
124
+ return 'error: write declined by user';
125
+ mkdirSync(dirname(p), { recursive: true });
126
+ writeFileSync(p, content);
127
+ return `wrote ${content.length} bytes to ${a.path}`;
128
+ },
129
+ };
130
+ const edit_file = {
131
+ mutating: true,
132
+ summary: (a) => `edit ${a.path}`,
133
+ spec: {
134
+ name: 'edit_file',
135
+ description: 'Replace an exact string in a file. old_string must match verbatim and be unique unless replace_all is true.',
136
+ parameters: { type: 'object', properties: { path: { type: 'string' }, old_string: { type: 'string' }, new_string: { type: 'string' }, replace_all: { type: 'boolean' } }, required: ['path', 'old_string', 'new_string'] },
137
+ },
138
+ async run(a, ctx) {
139
+ const g = guardPath(ctx.cwd, String(a.path));
140
+ if (g.protectedPath)
141
+ return `error: ${a.path} is a protected path — refused`;
142
+ const p = g.abs;
143
+ if (!existsSync(p))
144
+ return `error: no such file: ${a.path}`;
145
+ const src = readFileSync(p, 'utf-8');
146
+ const oldS = String(a.old_string);
147
+ const count = src.split(oldS).length - 1;
148
+ if (count === 0)
149
+ return `error: old_string not found in ${a.path}`;
150
+ if (count > 1 && !a.replace_all)
151
+ return `error: old_string appears ${count}× — pass replace_all:true or add context to make it unique`;
152
+ if (!(await ctx.approve(`edit ${relative(ctx.cwd, p) || a.path}${g.outside ? ' (OUTSIDE working dir)' : ''}`, { detail: `- ${oldS.slice(0, 300)}\n+ ${String(a.new_string).slice(0, 300)}`, dangerous: g.outside })))
153
+ return 'error: edit declined by user';
154
+ const next = a.replace_all ? src.split(oldS).join(String(a.new_string)) : src.replace(oldS, String(a.new_string));
155
+ writeFileSync(p, next);
156
+ return `edited ${a.path} (${count} replacement${count > 1 ? 's' : ''})`;
157
+ },
158
+ };
159
+ const list_dir = {
160
+ mutating: false,
161
+ summary: (a) => `ls ${a.path ?? '.'}`,
162
+ spec: { name: 'list_dir', description: 'List directory entries with type + size.', parameters: { type: 'object', properties: { path: { type: 'string' } }, required: [] } },
163
+ async run(a, ctx) {
164
+ const g = guardPath(ctx.cwd, String(a.path ?? '.'));
165
+ if (g.protectedPath)
166
+ return `error: protected path — refused`;
167
+ if (g.outside && !(await ctx.approve(`list OUTSIDE the working dir: ${a.path}`, { dangerous: true })))
168
+ return 'error: declined (outside working directory)';
169
+ const p = g.abs;
170
+ if (!existsSync(p))
171
+ return `error: no such directory: ${a.path}`;
172
+ const entries = readdirSync(p, { withFileTypes: true })
173
+ .map((e) => {
174
+ const full = join(p, e.name);
175
+ let size = '';
176
+ try {
177
+ if (e.isFile())
178
+ size = ` ${statSync(full).size}b`;
179
+ }
180
+ catch { }
181
+ return `${e.isDirectory() ? 'd' : '-'} ${e.name}${e.isDirectory() ? '/' : ''}${size}`;
182
+ })
183
+ .sort();
184
+ return clamp(entries.join('\n') || '(empty)');
185
+ },
186
+ };
187
+ const glob = {
188
+ mutating: false,
189
+ summary: (a) => `glob ${a.pattern}`,
190
+ spec: { name: 'glob', description: 'Find files matching a glob (ripgrep, .gitignore-aware). e.g. "src/**/*.ts".', parameters: { type: 'object', properties: { pattern: { type: 'string' }, path: { type: 'string' } }, required: ['pattern'] } },
191
+ async run(a, ctx) {
192
+ const g = guardPath(ctx.cwd, String(a.path ?? '.'));
193
+ if (g.protectedPath)
194
+ return `error: protected path — refused`;
195
+ if (g.outside && !(await ctx.approve(`glob OUTSIDE the working dir: ${a.path}`, { dangerous: true })))
196
+ return 'error: declined (outside working directory)';
197
+ const dir = g.abs;
198
+ const { stdout, stderr, code } = await capture('rg', ['--files', '-g', String(a.pattern), dir], { cwd: ctx.cwd });
199
+ if (code !== 0 && !stdout)
200
+ return stderr.trim() ? `no matches (${stderr.trim().slice(0, 200)})` : 'no matches';
201
+ return clamp(stdout.split('\n').filter(Boolean).map((f) => relative(ctx.cwd, f) || f).join('\n') || 'no matches');
202
+ },
203
+ };
204
+ const grep = {
205
+ mutating: false,
206
+ summary: (a) => `grep "${a.pattern}"`,
207
+ spec: {
208
+ name: 'grep',
209
+ description: 'Search file contents with a regex (ripgrep). Returns file:line:match. Use glob to scope by file type.',
210
+ parameters: { type: 'object', properties: { pattern: { type: 'string' }, path: { type: 'string' }, glob: { type: 'string' }, ignore_case: { type: 'boolean' } }, required: ['pattern'] },
211
+ },
212
+ async run(a, ctx) {
213
+ const g = guardPath(ctx.cwd, String(a.path ?? '.'));
214
+ if (g.protectedPath)
215
+ return `error: protected path — refused`;
216
+ if (g.outside && !(await ctx.approve(`grep OUTSIDE the working dir: ${a.path}`, { dangerous: true })))
217
+ return 'error: declined (outside working directory)';
218
+ const args = ['--line-number', '--no-heading', '--color', 'never'];
219
+ if (a.ignore_case)
220
+ args.push('-i');
221
+ if (a.glob)
222
+ args.push('-g', String(a.glob));
223
+ args.push('--', String(a.pattern), g.abs);
224
+ const { stdout, stderr, code } = await capture('rg', args, { cwd: ctx.cwd });
225
+ if (code === 1)
226
+ return 'no matches';
227
+ if (code > 1)
228
+ return `error: ${stderr.trim().slice(0, 300)}`;
229
+ return clamp(stdout || 'no matches');
230
+ },
231
+ };
232
+ const bash = {
233
+ mutating: true,
234
+ summary: (a) => `bash: ${String(a.command).slice(0, 70)}`,
235
+ spec: {
236
+ name: 'bash',
237
+ description: 'Run a shell command in the working directory and return stdout+stderr+exit code. Use for builds, tests, git, installs. Avoid destructive commands.',
238
+ parameters: { type: 'object', properties: { command: { type: 'string' }, timeout_ms: { type: 'number' } }, required: ['command'] },
239
+ },
240
+ async run(a, ctx) {
241
+ const command = String(a.command);
242
+ const danger = CATASTROPHIC.test(command);
243
+ const summary = danger ? `⚠ DANGEROUS shell — ${command.slice(0, 80)}` : `run: ${command.slice(0, 80)}`;
244
+ // bash is ALWAYS dangerous → never covered by --auto; the full command is shown.
245
+ if (!(await ctx.approve(`${ctx.sandbox && BWRAP ? '[sandboxed] ' : ''}${summary}`, { detail: command, dangerous: true })))
246
+ return 'error: command declined by user';
247
+ const { cmd, args } = bashArgv(command, ctx.cwd, !!ctx.sandbox);
248
+ const { stdout, stderr, code } = await capture(cmd, args, { cwd: ctx.cwd, timeout: a.timeout_ms ? Number(a.timeout_ms) : 120_000 });
249
+ const tag = ctx.sandbox ? (BWRAP ? '[sandboxed] ' : '[sandbox unavailable — bwrap not found] ') : '';
250
+ const body = [stdout && `stdout:\n${stdout}`, stderr && `stderr:\n${stderr}`].filter(Boolean).join('\n');
251
+ return clamp(`${tag}exit ${code}\n${body || '(no output)'}`);
252
+ },
253
+ };
254
+ export const LOCAL_TOOLS = [read_file, write_file, edit_file, list_dir, glob, grep, bash];
255
+ const BY_NAME = new Map(LOCAL_TOOLS.map((t) => [t.spec.name, t]));
256
+ export function localToolSpecs() {
257
+ return LOCAL_TOOLS.map((t) => t.spec);
258
+ }
259
+ export function getLocalTool(name) {
260
+ return BY_NAME.get(name);
261
+ }
262
+ //# sourceMappingURL=tools.js.map
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Flywheel — emit a tool-use trace per completed agent turn to ~/.dragon/traces/.
3
+ *
4
+ * These traces are the training data for DragonSpark (our own nano brain): the agent's
5
+ * real work becomes the dataset that makes its next model better. DragonSpark's data
6
+ * pipeline does a mandatory secret/customer scrub before any training; we also do a
7
+ * light redaction here at emit time as defense-in-depth.
8
+ *
9
+ * On by default; disable with `--no-trace` or DRAGON_NO_TRACE=1. Local-only — nothing
10
+ * is uploaded; the file just sits on the operator's machine for the DragonSpark repo
11
+ * to ingest (src/dragonspark/traces.py).
12
+ *
13
+ * Copyright 2026 Ghost Protocol (Pvt) Ltd. All Rights Reserved.
14
+ */
15
+ import type { BrainMessage } from '../brain/types.js';
16
+ export declare const TRACE_DIR: string;
17
+ /**
18
+ * Strip secrets before anything touches disk. Covers known key prefixes
19
+ * (sk-, sk-ant-, ghp_, xoxb-, AKIA, dgn_, JWT), cookie + authorization values,
20
+ * and any long opaque blob. Defense-in-depth for traces that may carry tool output.
21
+ */
22
+ export declare function redact(s: string): string;
23
+ export declare function traceEnabled(disabledFlag?: boolean): boolean;
24
+ /**
25
+ * Record one settled turn. `added` is the slice of messages appended during the turn
26
+ * (user → [assistant/tool]* → final assistant). Never throws — a trace failure must
27
+ * not break the session.
28
+ */
29
+ export declare function recordTurn(added: BrainMessage[], meta: {
30
+ cwd: string;
31
+ brain: string;
32
+ context?: string | null;
33
+ }): void;
34
+ //# sourceMappingURL=trace.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trace.d.ts","sourceRoot":"","sources":["../../src/agent/trace.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAKH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAErD,eAAO,MAAM,SAAS,QAAuC,CAAA;AAE7D;;;;GAIG;AACH,wBAAgB,MAAM,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAQxC;AAED,wBAAgB,YAAY,CAAC,YAAY,CAAC,EAAE,OAAO,GAAG,OAAO,CAE5D;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,YAAY,EAAE,EAAE,IAAI,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAAG,IAAI,CA6BrH"}
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Flywheel — emit a tool-use trace per completed agent turn to ~/.dragon/traces/.
3
+ *
4
+ * These traces are the training data for DragonSpark (our own nano brain): the agent's
5
+ * real work becomes the dataset that makes its next model better. DragonSpark's data
6
+ * pipeline does a mandatory secret/customer scrub before any training; we also do a
7
+ * light redaction here at emit time as defense-in-depth.
8
+ *
9
+ * On by default; disable with `--no-trace` or DRAGON_NO_TRACE=1. Local-only — nothing
10
+ * is uploaded; the file just sits on the operator's machine for the DragonSpark repo
11
+ * to ingest (src/dragonspark/traces.py).
12
+ *
13
+ * Copyright 2026 Ghost Protocol (Pvt) Ltd. All Rights Reserved.
14
+ */
15
+ import { appendFileSync, mkdirSync, chmodSync } from 'node:fs';
16
+ import { homedir } from 'node:os';
17
+ import { join } from 'node:path';
18
+ export const TRACE_DIR = join(homedir(), '.dragon', 'traces');
19
+ /**
20
+ * Strip secrets before anything touches disk. Covers known key prefixes
21
+ * (sk-, sk-ant-, ghp_, xoxb-, AKIA, dgn_, JWT), cookie + authorization values,
22
+ * and any long opaque blob. Defense-in-depth for traces that may carry tool output.
23
+ */
24
+ export function redact(s) {
25
+ if (!s)
26
+ return s;
27
+ return s
28
+ .replace(/\b(?:sk-ant-[A-Za-z0-9_-]+|sk-[A-Za-z0-9_-]{20,}|ghp_[A-Za-z0-9]+|gho_[A-Za-z0-9]+|xox[baprs]-[A-Za-z0-9-]+|AKIA[A-Z0-9]{16}|dgn_[A-Za-z0-9_-]+|eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+)\b/g, '[redacted]')
29
+ .replace(/(gp_session=)[^;\s"']+/gi, '$1[redacted]')
30
+ .replace(/(authorization["']?\s*[:=]\s*["']?(?:bearer\s+)?)[^\s"',}]+/gi, '$1[redacted]')
31
+ .replace(/\b[A-Za-z0-9_-]{40,}\b/g, '[redacted]')
32
+ .replace(/[A-Za-z0-9+/_-]{40,}={0,2}/g, '[redacted]');
33
+ }
34
+ export function traceEnabled(disabledFlag) {
35
+ return !disabledFlag && !process.env.DRAGON_NO_TRACE;
36
+ }
37
+ /**
38
+ * Record one settled turn. `added` is the slice of messages appended during the turn
39
+ * (user → [assistant/tool]* → final assistant). Never throws — a trace failure must
40
+ * not break the session.
41
+ */
42
+ export function recordTurn(added, meta) {
43
+ try {
44
+ const user = added.find((m) => m.role === 'user')?.content ?? '';
45
+ const tool_calls = added
46
+ .filter((m) => m.role === 'assistant')
47
+ .flatMap((m) => (m.toolCalls ?? []).map((t) => ({ name: t.name, arguments: redact(JSON.stringify(t.arguments ?? {})).slice(0, 2000) })));
48
+ const tool_results = added
49
+ .filter((m) => m.role === 'tool')
50
+ .map((m) => ({ name: m.toolName ?? '', result: redact(m.content).slice(0, 4000) }));
51
+ const finals = added.filter((m) => m.role === 'assistant' && m.content);
52
+ const final_answer = finals.length ? finals[finals.length - 1].content : '';
53
+ const rec = {
54
+ ts: new Date().toISOString(),
55
+ cwd: meta.cwd,
56
+ brain: meta.brain,
57
+ prompt: redact(user).slice(0, 8000),
58
+ retrieved_wyrm_context: meta.context ? redact(meta.context).slice(0, 4000) : undefined,
59
+ tool_calls,
60
+ tool_results,
61
+ final_answer: redact(final_answer).slice(0, 8000),
62
+ };
63
+ mkdirSync(TRACE_DIR, { recursive: true, mode: 0o700 });
64
+ const file = join(TRACE_DIR, `${rec.ts.slice(0, 10)}.jsonl`);
65
+ appendFileSync(file, JSON.stringify(rec) + '\n', { mode: 0o600 });
66
+ chmodSync(file, 0o600); // appendFileSync mode only applies on create — enforce on existing too
67
+ }
68
+ catch {
69
+ /* never break a session over a trace write */
70
+ }
71
+ }
72
+ //# sourceMappingURL=trace.js.map