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,97 @@
1
+ /**
2
+ * Ghost / EMBER brain — Ghost Protocol's own local model (DragonSpark's EMBER,
3
+ * served by Ollama) as the agent's reasoning layer.
4
+ *
5
+ * EMBER is a small specialist FINE-TUNED to emit tool calls as a JSON array in the
6
+ * response TEXT — `[{"tool":"Bash","arguments":{…}}]` — NOT via the OpenAI native
7
+ * function-calling protocol. So this adapter, unlike the generic openai-compat brain:
8
+ * 1. does NOT send `tools`/`tool_choice` (EMBER ignores them); instead it appends a
9
+ * terse output contract + the available tool names to the system prompt, matching
10
+ * what EMBER was trained on (DragonSpark configs/fixtures/system_prompt.txt);
11
+ * 2. parses EMBER's JSON-array output back into the loop's normalized ToolCall shape.
12
+ *
13
+ * This format bridge is doubly important: it makes EMBER's calls EXECUTE in the loop
14
+ * AND makes the emitted ~/.dragon/traces training-grade (the flywheel that improves the
15
+ * next EMBER). Wyrm-RAG comes for free — dragon-cli already folds recalled project
16
+ * context into the system prompt (loop.ts buildSystemPrompt `primed`), so EMBER decides
17
+ * WITH memory, which is the whole "Wyrm is the brain" thesis.
18
+ *
19
+ * Copyright 2026 Ghost Protocol (Pvt) Ltd. All Rights Reserved.
20
+ */
21
+ const EMBER_CONTRACT = '\n\n── OUTPUT CONTRACT (EMBER) ──\n' +
22
+ 'Respond with ONLY a JSON array of the tool call(s) to make, no prose:\n' +
23
+ '[{"tool":"<name>","arguments":{…}}]\n' +
24
+ 'If the task is complete and no tool is needed, respond with [].';
25
+ function oaiMessages(system, messages, toolNames) {
26
+ const sys = system + EMBER_CONTRACT + '\nAvailable tools: ' + toolNames.join(', ');
27
+ const out = [{ role: 'system', content: sys }];
28
+ for (const m of messages) {
29
+ if (m.role === 'tool') {
30
+ out.push({ role: 'user', content: `[observed] ${m.toolName ?? 'tool'} → ${m.content}`.slice(0, 1200) });
31
+ }
32
+ else if (m.role === 'assistant') {
33
+ out.push({ role: 'assistant', content: m.toolCalls?.length ? JSON.stringify(m.toolCalls.map((t) => ({ tool: t.name, arguments: t.arguments }))) : m.content });
34
+ }
35
+ else {
36
+ out.push({ role: 'user', content: m.content });
37
+ }
38
+ }
39
+ return out;
40
+ }
41
+ /** Pull EMBER's JSON tool array out of the response text (tolerant of stray prose). */
42
+ export function parseEmberToolCalls(text) {
43
+ const m = text.match(/\[[\s\S]*\]/);
44
+ if (!m)
45
+ return [];
46
+ let arr;
47
+ try {
48
+ arr = JSON.parse(m[0]);
49
+ }
50
+ catch {
51
+ return [];
52
+ }
53
+ if (!Array.isArray(arr))
54
+ return [];
55
+ const calls = [];
56
+ arr.forEach((c, i) => {
57
+ if (c && typeof c === 'object' && typeof c.tool === 'string') {
58
+ const o = c;
59
+ calls.push({ id: `ember_${Date.now()}_${i}`, name: o.tool, arguments: o.arguments ?? {} });
60
+ }
61
+ });
62
+ return calls;
63
+ }
64
+ export function makeGhostEmberBrain(opts) {
65
+ const baseURL = opts.baseURL.replace(/\/+$/, '');
66
+ return {
67
+ id: 'ghost',
68
+ model: opts.model,
69
+ async turn(t) {
70
+ const toolNames = t.tools.map((tl) => tl.name);
71
+ const res = await fetch(`${baseURL}/chat/completions`, {
72
+ method: 'POST',
73
+ headers: { 'content-type': 'application/json', authorization: 'Bearer ghost' },
74
+ body: JSON.stringify({
75
+ model: opts.model,
76
+ messages: oaiMessages(t.system, t.messages, toolNames),
77
+ temperature: 0.1,
78
+ stream: false,
79
+ max_tokens: t.maxTokens ?? 512,
80
+ }),
81
+ signal: t.signal,
82
+ });
83
+ if (!res.ok) {
84
+ const body = await res.text().catch(() => res.statusText);
85
+ throw new Error(`ghost(EMBER) brain HTTP ${res.status}: ${body.slice(0, 300)} — is Ollama serving '${opts.model}'? (dragon-spark serve)`);
86
+ }
87
+ const data = (await res.json());
88
+ const text = data.choices?.[0]?.message?.content ?? '';
89
+ const toolCalls = parseEmberToolCalls(text);
90
+ // EMBER emits JSON, not prose — surface a readable line for the operator/trace.
91
+ const display = toolCalls.length ? toolCalls.map((c) => `→ ${c.name}`).join(' ') : text;
92
+ t.onDelta?.(display);
93
+ return { text: toolCalls.length ? '' : text, toolCalls };
94
+ },
95
+ };
96
+ }
97
+ //# sourceMappingURL=ghost-ember.js.map
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Brain factory — resolves which model drives the agent, from (in priority)
3
+ * an explicit override → env → ~/.dragon/config.json → built-in default
4
+ * ('claude'). Keys come from env first, then config (`dragon config key …`).
5
+ *
6
+ * Copyright 2026 Ghost Protocol (Pvt) Ltd. All Rights Reserved.
7
+ */
8
+ import type { Brain } from './types.js';
9
+ export type { Brain, BrainMessage, ToolSpec, ToolCall, BrainTurn } from './types.js';
10
+ export type Provider = 'claude' | 'openai' | 'local' | 'ghost' | 'worker' | 'custom' | 'router';
11
+ export declare const PROVIDERS: Provider[];
12
+ /** Missing/invalid key → thrown so commands can print a friendly fix. */
13
+ export declare class BrainConfigError extends Error {
14
+ provider: Provider;
15
+ constructor(provider: Provider, message: string);
16
+ }
17
+ export declare function resolveProvider(override?: string): Provider;
18
+ export declare function getBrain(opts?: {
19
+ provider?: string;
20
+ model?: string;
21
+ }): Brain;
22
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/brain/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AAcvC,YAAY,EAAE,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAEpF,MAAM,MAAM,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,OAAO,GAAG,OAAO,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAA;AAC/F,eAAO,MAAM,SAAS,EAAE,QAAQ,EAAyE,CAAA;AAEzG,yEAAyE;AACzE,qBAAa,gBAAiB,SAAQ,KAAK;IACtB,QAAQ,EAAE,QAAQ;gBAAlB,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM;CAIvD;AAED,wBAAgB,eAAe,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,QAAQ,CAI3D;AAED,wBAAgB,QAAQ,CAAC,IAAI,CAAC,EAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,KAAK,CAuD5E"}
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Brain factory — resolves which model drives the agent, from (in priority)
3
+ * an explicit override → env → ~/.dragon/config.json → built-in default
4
+ * ('claude'). Keys come from env first, then config (`dragon config key …`).
5
+ *
6
+ * Copyright 2026 Ghost Protocol (Pvt) Ltd. All Rights Reserved.
7
+ */
8
+ import { execSync } from 'node:child_process';
9
+ import { loadConfig } from '../config.js';
10
+ import { makeClaudeBrain, DEFAULT_CLAUDE_MODEL } from './anthropic.js';
11
+ import { makeClaudeCliBrain } from './claude-cli.js';
12
+ import { makeOpenAICompatBrain } from './openai-compat.js';
13
+ import { makeGhostEmberBrain } from './ghost-ember.js';
14
+ import { makeWorkerBrain } from './worker.js';
15
+ import { makeRouterBrain } from './router/index.js';
16
+ /** Is the local `claude` CLI (Claude Code) on PATH? Lets `--brain claude` reuse the
17
+ * operator's existing Claude Code auth when no API key is configured. */
18
+ function hasClaudeCli() {
19
+ try {
20
+ execSync('command -v claude', { stdio: 'ignore' });
21
+ return true;
22
+ }
23
+ catch {
24
+ return false;
25
+ }
26
+ }
27
+ export const PROVIDERS = ['claude', 'worker', 'local', 'ghost', 'openai', 'custom', 'router'];
28
+ /** Missing/invalid key → thrown so commands can print a friendly fix. */
29
+ export class BrainConfigError extends Error {
30
+ provider;
31
+ constructor(provider, message) {
32
+ super(message);
33
+ this.provider = provider;
34
+ this.name = 'BrainConfigError';
35
+ }
36
+ }
37
+ export function resolveProvider(override) {
38
+ const cfg = loadConfig();
39
+ const want = (override || process.env.DRAGON_BRAIN || cfg.brain?.provider || 'claude').toLowerCase();
40
+ return PROVIDERS.includes(want) ? want : 'claude';
41
+ }
42
+ export function getBrain(opts) {
43
+ const cfg = loadConfig();
44
+ const provider = resolveProvider(opts?.provider);
45
+ if (provider === 'claude') {
46
+ const apiKey = process.env.ANTHROPIC_API_KEY || cfg.brain?.keys?.anthropic;
47
+ const override = opts?.model || process.env.DRAGON_MODEL || cfg.brain?.model;
48
+ if (apiKey)
49
+ return makeClaudeBrain({ apiKey, model: override || DEFAULT_CLAUDE_MODEL });
50
+ // No API key → reuse the local Claude Code CLI's own auth ("use this Claude").
51
+ if (hasClaudeCli())
52
+ return makeClaudeCliBrain({ model: override });
53
+ throw new BrainConfigError('claude', 'no Anthropic key and no `claude` CLI found — set ANTHROPIC_API_KEY, run `dragon config key anthropic <key>`, install Claude Code, or use `--brain local`.');
54
+ }
55
+ if (provider === 'openai') {
56
+ const apiKey = process.env.OPENAI_API_KEY || cfg.brain?.keys?.openai;
57
+ if (!apiKey)
58
+ throw new BrainConfigError('openai', 'no OpenAI key — set OPENAI_API_KEY or run `dragon config key openai <key>`.');
59
+ const model = opts?.model || process.env.DRAGON_MODEL || cfg.brain?.model || 'gpt-4o';
60
+ return makeOpenAICompatBrain({ id: 'openai', baseURL: 'https://api.openai.com/v1', apiKey, model });
61
+ }
62
+ if (provider === 'worker') {
63
+ // Our Cloudflare Workers AI — free, no key, just `dragon login`.
64
+ return makeWorkerBrain();
65
+ }
66
+ if (provider === 'custom') {
67
+ // ANY OpenAI-compatible endpoint — OpenRouter, vLLM, LM Studio, llama.cpp, etc.
68
+ const baseURL = process.env.DRAGON_OPENAI_BASE || cfg.brain?.customBaseURL;
69
+ if (!baseURL)
70
+ throw new BrainConfigError('custom', 'no custom endpoint — set DRAGON_OPENAI_BASE (any OpenAI-compatible base URL) or `dragon config custom-url <url>`.');
71
+ const apiKey = process.env.DRAGON_OPENAI_KEY || cfg.brain?.keys?.openai || 'none';
72
+ const model = opts?.model || process.env.DRAGON_CUSTOM_MODEL || cfg.brain?.customModel || 'default';
73
+ return makeOpenAICompatBrain({ id: 'custom', baseURL, apiKey, model });
74
+ }
75
+ if (provider === 'router') {
76
+ // The Ghost Router — classifies each turn and delegates to the best local
77
+ // model (or escalates to Claude). `resolve` is injected so the router can
78
+ // build sibling brains without a circular import.
79
+ const localBaseURL = process.env.DRAGON_LOCAL_URL || cfg.brain?.localBaseURL || 'http://localhost:11434/v1';
80
+ return makeRouterBrain({ resolve: (p, m) => getBrain({ provider: p, model: m }), localBaseURL });
81
+ }
82
+ if (provider === 'ghost') {
83
+ // DragonSpark's EMBER — Ghost Protocol's own fine-tuned local model, served by
84
+ // Ollama. Uses the EMBER-specific adapter (JSON-array tool format + Wyrm-RAG via
85
+ // the primed system prompt), NOT generic OpenAI function-calling.
86
+ const baseURL = process.env.DRAGON_GHOST_URL || cfg.brain?.ghostBaseURL || 'http://localhost:11434/v1';
87
+ const model = opts?.model || process.env.DRAGON_GHOST_MODEL || cfg.brain?.ghostModel || 'ember';
88
+ return makeGhostEmberBrain({ baseURL, model });
89
+ }
90
+ // local (Ollama / any OpenAI-compatible local server) — sovereign, $0
91
+ const baseURL = process.env.DRAGON_LOCAL_URL || cfg.brain?.localBaseURL || 'http://localhost:11434/v1';
92
+ const model = opts?.model || process.env.DRAGON_LOCAL_MODEL || cfg.brain?.localModel || 'qwen2.5-coder:7b';
93
+ return makeOpenAICompatBrain({ id: 'local', baseURL, apiKey: 'ollama', model });
94
+ }
95
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,21 @@
1
+ /**
2
+ * OpenAI-compatible brain — one implementation, two providers:
3
+ * - 'openai' → https://api.openai.com/v1 (GPT)
4
+ * - 'local' → http://localhost:11434/v1 (Ollama; sovereign, $0)
5
+ * Any other OpenAI-compatible server works too via a custom baseURL.
6
+ *
7
+ * Speaks the Chat Completions streaming protocol with function tools, decoding
8
+ * SSE deltas: `delta.content` → text, `delta.tool_calls[]` → accumulated by
9
+ * index (id + name once, arguments streamed as string chunks → JSON-parsed at
10
+ * the end). No SDK — plain fetch keeps the dep surface small.
11
+ *
12
+ * Copyright 2026 Ghost Protocol (Pvt) Ltd. All Rights Reserved.
13
+ */
14
+ import type { Brain } from './types.js';
15
+ export declare function makeOpenAICompatBrain(opts: {
16
+ id: string;
17
+ baseURL: string;
18
+ apiKey: string;
19
+ model: string;
20
+ }): Brain;
21
+ //# sourceMappingURL=openai-compat.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openai-compat.d.ts","sourceRoot":"","sources":["../../src/brain/openai-compat.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAqC,MAAM,YAAY,CAAA;AA+B1E,wBAAgB,qBAAqB,CAAC,IAAI,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,KAAK,CAqEjH"}
@@ -0,0 +1,119 @@
1
+ /**
2
+ * OpenAI-compatible brain — one implementation, two providers:
3
+ * - 'openai' → https://api.openai.com/v1 (GPT)
4
+ * - 'local' → http://localhost:11434/v1 (Ollama; sovereign, $0)
5
+ * Any other OpenAI-compatible server works too via a custom baseURL.
6
+ *
7
+ * Speaks the Chat Completions streaming protocol with function tools, decoding
8
+ * SSE deltas: `delta.content` → text, `delta.tool_calls[]` → accumulated by
9
+ * index (id + name once, arguments streamed as string chunks → JSON-parsed at
10
+ * the end). No SDK — plain fetch keeps the dep surface small.
11
+ *
12
+ * Copyright 2026 Ghost Protocol (Pvt) Ltd. All Rights Reserved.
13
+ */
14
+ function toOpenAI(system, messages) {
15
+ const out = [{ role: 'system', content: system }];
16
+ for (const m of messages) {
17
+ if (m.role === 'tool') {
18
+ out.push({ role: 'tool', tool_call_id: m.toolCallId ?? '', content: m.content });
19
+ }
20
+ else if (m.role === 'assistant') {
21
+ out.push({
22
+ role: 'assistant',
23
+ content: m.content || null,
24
+ tool_calls: m.toolCalls?.length
25
+ ? m.toolCalls.map((tc) => ({ id: tc.id, type: 'function', function: { name: tc.name, arguments: JSON.stringify(tc.arguments ?? {}) } }))
26
+ : undefined,
27
+ });
28
+ }
29
+ else {
30
+ out.push({ role: 'user', content: m.content });
31
+ }
32
+ }
33
+ return out;
34
+ }
35
+ export function makeOpenAICompatBrain(opts) {
36
+ const baseURL = opts.baseURL.replace(/\/+$/, '');
37
+ return {
38
+ id: opts.id,
39
+ model: opts.model,
40
+ async turn(t) {
41
+ const res = await fetch(`${baseURL}/chat/completions`, {
42
+ method: 'POST',
43
+ headers: { 'content-type': 'application/json', authorization: `Bearer ${opts.apiKey}` },
44
+ body: JSON.stringify({
45
+ model: opts.model,
46
+ messages: toOpenAI(t.system, t.messages),
47
+ tools: t.tools.length ? t.tools.map((tool) => ({ type: 'function', function: { name: tool.name, description: tool.description, parameters: tool.parameters } })) : undefined,
48
+ tool_choice: t.tools.length ? 'auto' : undefined,
49
+ stream: true,
50
+ max_tokens: t.maxTokens ?? 8192,
51
+ }),
52
+ signal: t.signal,
53
+ });
54
+ if (!res.ok || !res.body) {
55
+ const body = await res.text().catch(() => res.statusText);
56
+ throw new Error(`${opts.id} brain HTTP ${res.status}: ${body.slice(0, 400)}`);
57
+ }
58
+ const reader = res.body.getReader();
59
+ const dec = new TextDecoder();
60
+ let buf = '';
61
+ let text = '';
62
+ const acc = new Map();
63
+ for (;;) {
64
+ const { value, done } = await reader.read();
65
+ if (done)
66
+ break;
67
+ buf += dec.decode(value, { stream: true });
68
+ const lines = buf.split('\n');
69
+ buf = lines.pop() ?? '';
70
+ for (const line of lines) {
71
+ if (!line.startsWith('data: '))
72
+ continue;
73
+ const data = line.slice(6).trim();
74
+ if (data === '[DONE]')
75
+ break;
76
+ let obj;
77
+ try {
78
+ obj = JSON.parse(data);
79
+ }
80
+ catch {
81
+ continue;
82
+ }
83
+ const delta = obj.choices?.[0]?.delta;
84
+ if (!delta)
85
+ continue;
86
+ if (typeof delta.content === 'string' && delta.content) {
87
+ text += delta.content;
88
+ t.onDelta?.(delta.content);
89
+ }
90
+ for (const tc of delta.tool_calls ?? []) {
91
+ const idx = tc.index ?? 0;
92
+ const cur = acc.get(idx) ?? { id: '', name: '', args: '' };
93
+ if (tc.id)
94
+ cur.id = tc.id;
95
+ if (tc.function?.name)
96
+ cur.name = tc.function.name;
97
+ if (tc.function?.arguments)
98
+ cur.args += tc.function.arguments;
99
+ acc.set(idx, cur);
100
+ }
101
+ }
102
+ }
103
+ const toolCalls = [...acc.values()]
104
+ .filter((a) => a.name)
105
+ .map((a, i) => {
106
+ let args = {};
107
+ try {
108
+ args = a.args ? JSON.parse(a.args) : {};
109
+ }
110
+ catch {
111
+ args = {};
112
+ }
113
+ return { id: a.id || `call_${i}`, name: a.name, arguments: args };
114
+ });
115
+ return { text, toolCalls };
116
+ },
117
+ };
118
+ }
119
+ //# sourceMappingURL=openai-compat.js.map
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Request classification for the Ghost Router — intent × difficulty × stakes.
3
+ *
4
+ * INTENT is decided semantically: we embed the current user message with the
5
+ * already-resident nomic-embed-text (via Ollama, ~0 extra VRAM) and take the
6
+ * nearest route centroid. A deterministic keyword classifier is the fallback when
7
+ * embeddings are unavailable, so routing degrades gracefully (never throws).
8
+ *
9
+ * DIFFICULTY and STAKES are cheap deterministic heuristics — no model needed.
10
+ *
11
+ * Copyright 2026 Ghost Protocol (Pvt) Ltd. All Rights Reserved.
12
+ */
13
+ import type { BrainMessage } from '../types.js';
14
+ export type Intent = 'reasoning' | 'code' | 'tools' | 'chat' | 'ops_security';
15
+ export type Stakes = 'security' | 'financial' | 'critical' | 'standard' | 'dev';
16
+ export interface Classification {
17
+ intent: Intent;
18
+ difficulty: number;
19
+ stakes: Stakes;
20
+ via: 'embed' | 'keyword';
21
+ }
22
+ export declare function classify(base: string, messages: BrainMessage[], toolCount: number, signal?: AbortSignal): Promise<Classification>;
23
+ //# sourceMappingURL=classify.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"classify.d.ts","sourceRoot":"","sources":["../../../src/brain/router/classify.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAE/C,MAAM,MAAM,MAAM,GAAG,WAAW,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,cAAc,CAAA;AAC7E,MAAM,MAAM,MAAM,GAAG,UAAU,GAAG,WAAW,GAAG,UAAU,GAAG,UAAU,GAAG,KAAK,CAAA;AAE/E,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,MAAM,CAAA;IAClB,MAAM,EAAE,MAAM,CAAA;IACd,GAAG,EAAE,OAAO,GAAG,SAAS,CAAA;CACzB;AAwHD,wBAAsB,QAAQ,CAC5B,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,YAAY,EAAE,EACxB,SAAS,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,cAAc,CAAC,CAkBzB"}
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Request classification for the Ghost Router — intent × difficulty × stakes.
3
+ *
4
+ * INTENT is decided semantically: we embed the current user message with the
5
+ * already-resident nomic-embed-text (via Ollama, ~0 extra VRAM) and take the
6
+ * nearest route centroid. A deterministic keyword classifier is the fallback when
7
+ * embeddings are unavailable, so routing degrades gracefully (never throws).
8
+ *
9
+ * DIFFICULTY and STAKES are cheap deterministic heuristics — no model needed.
10
+ *
11
+ * Copyright 2026 Ghost Protocol (Pvt) Ltd. All Rights Reserved.
12
+ */
13
+ const EMBED_URL = (base) => base.replace(/\/v1\/?$/, '').replace(/\/+$/, '') + '/api/embeddings';
14
+ /** Seed phrases per route — centroids are the mean of their embeddings. */
15
+ const ROUTE_SEEDS = {
16
+ reasoning: [
17
+ 'solve this math problem step by step', 'prove that the statement holds',
18
+ 'what is the time complexity', 'derive the closed-form formula',
19
+ 'solve this logic puzzle', 'crack this cipher / modular arithmetic',
20
+ ],
21
+ code: [
22
+ 'write a function that', 'implement this feature', 'refactor this code',
23
+ 'add a unit test for', 'fix the bug in this code',
24
+ ],
25
+ tools: [
26
+ 'run the command', 'edit the file', 'deploy the service', 'list the files',
27
+ 'grep the codebase for', 'execute and show the output',
28
+ ],
29
+ chat: [
30
+ 'hello', 'what do you think about', 'explain this briefly',
31
+ 'summarize this', 'give me a quick answer',
32
+ ],
33
+ ops_security: [
34
+ 'run a penetration test', 'find the vulnerability', 'audit this for security',
35
+ 'harden this configuration', 'scan the target host',
36
+ ],
37
+ };
38
+ const KEYWORDS = {
39
+ ops_security: /\b(pentest|exploit|vuln|cve|nmap|payload|recon|harden|audit|red.?team|owasp)\b/i,
40
+ reasoning: /\b(prove|theorem|complexity|derive|calculate|compute|equation|algorithm|puzzle|cipher|optimal)\b/i,
41
+ code: /\b(function|refactor|implement|class|compile|unit test|bug|stack trace|typescript|python)\b/i,
42
+ tools: /\b(run|execute|deploy|grep|edit|read the file|list files|install|git )\b/i,
43
+ chat: /.*/, // fallback
44
+ };
45
+ const STAKES_RULES = [
46
+ // NOTE: pentest/exploit are our DOMAIN, not "stakes" — they must stay local
47
+ // (privacy-first). Only genuinely sensitive/irreversible ops escalate.
48
+ ['security', /\b(production|prod\b|credential|secret|api.?key|ssh|rm -rf|drop table|sudo)\b/i],
49
+ ['financial', /\b(payment|invoice|billing|refund|bank|wire|salary|price|charge|stripe|paddle)\b/i],
50
+ ['critical', /\b(migration|database|deploy to prod|delete|irreversible|legal|contract)\b/i],
51
+ ];
52
+ function lastUser(messages) {
53
+ for (let i = messages.length - 1; i >= 0; i--)
54
+ if (messages[i].role === 'user')
55
+ return messages[i].content;
56
+ return messages.length ? messages[messages.length - 1].content : '';
57
+ }
58
+ async function embed(base, text, signal) {
59
+ try {
60
+ const res = await fetch(EMBED_URL(base), {
61
+ method: 'POST',
62
+ headers: { 'content-type': 'application/json' },
63
+ body: JSON.stringify({ model: process.env.DRAGON_ROUTER_EMBED || 'nomic-embed-text', prompt: text }),
64
+ signal,
65
+ });
66
+ if (!res.ok)
67
+ return null;
68
+ const data = (await res.json());
69
+ return Array.isArray(data.embedding) && data.embedding.length ? data.embedding : null;
70
+ }
71
+ catch {
72
+ return null;
73
+ }
74
+ }
75
+ function cosine(a, b) {
76
+ let dot = 0, na = 0, nb = 0;
77
+ for (let i = 0; i < a.length; i++) {
78
+ dot += a[i] * b[i];
79
+ na += a[i] * a[i];
80
+ nb += b[i] * b[i];
81
+ }
82
+ return dot / (Math.sqrt(na) * Math.sqrt(nb) + 1e-9);
83
+ }
84
+ // Route centroids are computed once and cached. If embeddings are unavailable
85
+ // (e.g. Ollama still starting), we keep CENTROIDS null and RETRY after a cooldown
86
+ // rather than disabling semantic routing for the whole process lifetime.
87
+ let CENTROIDS = null;
88
+ let centroidsNextRetry = 0;
89
+ async function ensureCentroids(base, signal) {
90
+ if (CENTROIDS)
91
+ return CENTROIDS;
92
+ if (Date.now() < centroidsNextRetry)
93
+ return null;
94
+ centroidsNextRetry = Date.now() + 30_000; // don't hammer a down Ollama
95
+ const out = {};
96
+ for (const intent of Object.keys(ROUTE_SEEDS)) {
97
+ const vecs = [];
98
+ for (const phrase of ROUTE_SEEDS[intent]) {
99
+ const v = await embed(base, phrase, signal);
100
+ if (v)
101
+ vecs.push(v);
102
+ }
103
+ if (!vecs.length)
104
+ return null; // embeddings unavailable → keyword fallback, retry later
105
+ const dim = vecs[0].length;
106
+ const mean = new Array(dim).fill(0);
107
+ for (const v of vecs)
108
+ for (let i = 0; i < dim; i++)
109
+ mean[i] += v[i] / vecs.length;
110
+ out[intent] = mean;
111
+ }
112
+ return (CENTROIDS = out);
113
+ }
114
+ function keywordIntent(text) {
115
+ for (const intent of ['ops_security', 'reasoning', 'code', 'tools']) {
116
+ if (KEYWORDS[intent].test(text))
117
+ return intent;
118
+ }
119
+ return 'chat';
120
+ }
121
+ function scoreDifficulty(text, toolCount) {
122
+ const len = text.length;
123
+ let d = Math.min(len / 1600, 0.5); // length signal, capped at 0.5
124
+ if (/\b(step by step|prove|derive|complex|multi-step|optimi[sz]e|edge case|why)\b/i.test(text))
125
+ d += 0.2;
126
+ if ((text.match(/\?/g) || []).length >= 2)
127
+ d += 0.1; // multiple questions
128
+ if (/```|\bfunction\b|\bclass\b/.test(text))
129
+ d += 0.1; // code present
130
+ d += Math.min(toolCount / 40, 0.15); // many tools = harder orchestration
131
+ return Math.max(0, Math.min(1, d));
132
+ }
133
+ function classifyStakes(text) {
134
+ for (const [tier, re] of STAKES_RULES)
135
+ if (re.test(text))
136
+ return tier;
137
+ return 'standard';
138
+ }
139
+ export async function classify(base, messages, toolCount, signal) {
140
+ const text = lastUser(messages);
141
+ const stakes = classifyStakes(text);
142
+ const difficulty = scoreDifficulty(text, toolCount);
143
+ const centroids = await ensureCentroids(base, signal);
144
+ if (centroids) {
145
+ const v = await embed(base, text, signal);
146
+ if (v) {
147
+ let best = 'chat', bestScore = -Infinity;
148
+ for (const intent of Object.keys(centroids)) {
149
+ const s = cosine(v, centroids[intent]);
150
+ if (s > bestScore) {
151
+ bestScore = s;
152
+ best = intent;
153
+ }
154
+ }
155
+ return { intent: best, difficulty, stakes, via: 'embed' };
156
+ }
157
+ }
158
+ return { intent: keywordIntent(text), difficulty, stakes, via: 'keyword' };
159
+ }
160
+ //# sourceMappingURL=classify.js.map
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Execution-based verification (ROUTER-BLUEPRINT.md §2) — the highest-value reward
3
+ * for code/security work: actually RUN the candidate and use pass/fail as the signal.
4
+ *
5
+ * SECURITY-FIRST (this is a security company): auto-running model-generated code is
6
+ * dangerous, so it is OFF by default and only ever runs:
7
+ * - when DRAGON_VERIFY_EXEC=1 is explicitly set,
8
+ * - inside a bwrap sandbox with NO network, a tmpfs, and a read-only /usr,
9
+ * - for python3 / node ONLY (never bash/sh),
10
+ * - under a hard timeout.
11
+ * If bwrap is missing we REFUSE to execute (fail closed).
12
+ *
13
+ * Copyright 2026 Ghost Protocol (Pvt) Ltd. All Rights Reserved.
14
+ */
15
+ export interface ExecResult {
16
+ ran: boolean;
17
+ ok?: boolean;
18
+ lang?: string;
19
+ output?: string;
20
+ reason?: string;
21
+ }
22
+ export declare function executeVerify(text: string, _signal?: AbortSignal): Promise<ExecResult | null>;
23
+ //# sourceMappingURL=execute.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"execute.d.ts","sourceRoot":"","sources":["../../../src/brain/router/execute.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAOH,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,OAAO,CAAA;IACZ,EAAE,CAAC,EAAE,OAAO,CAAA;IACZ,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAsBD,wBAAsB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CA+BnG"}
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Execution-based verification (ROUTER-BLUEPRINT.md §2) — the highest-value reward
3
+ * for code/security work: actually RUN the candidate and use pass/fail as the signal.
4
+ *
5
+ * SECURITY-FIRST (this is a security company): auto-running model-generated code is
6
+ * dangerous, so it is OFF by default and only ever runs:
7
+ * - when DRAGON_VERIFY_EXEC=1 is explicitly set,
8
+ * - inside a bwrap sandbox with NO network, a tmpfs, and a read-only /usr,
9
+ * - for python3 / node ONLY (never bash/sh),
10
+ * - under a hard timeout.
11
+ * If bwrap is missing we REFUSE to execute (fail closed).
12
+ *
13
+ * Copyright 2026 Ghost Protocol (Pvt) Ltd. All Rights Reserved.
14
+ */
15
+ import { spawnSync, execSync } from 'node:child_process';
16
+ import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
17
+ import { tmpdir } from 'node:os';
18
+ import { join } from 'node:path';
19
+ const LANGS = {
20
+ python: { file: 'snippet.py', cmd: 'python3' },
21
+ py: { file: 'snippet.py', cmd: 'python3' },
22
+ javascript: { file: 'snippet.js', cmd: 'node' },
23
+ js: { file: 'snippet.js', cmd: 'node' },
24
+ node: { file: 'snippet.js', cmd: 'node' },
25
+ };
26
+ function hasBwrap() {
27
+ try {
28
+ execSync('command -v bwrap', { stdio: 'ignore' });
29
+ return true;
30
+ }
31
+ catch {
32
+ return false;
33
+ }
34
+ }
35
+ /** Pull the last fenced code block + its language. */
36
+ function lastCodeBlock(text) {
37
+ const blocks = [...text.matchAll(/```([a-zA-Z0-9]*)\n([\s\S]*?)```/g)];
38
+ if (!blocks.length)
39
+ return null;
40
+ const b = blocks[blocks.length - 1];
41
+ return { lang: (b[1] || '').toLowerCase(), code: b[2] };
42
+ }
43
+ export async function executeVerify(text, _signal) {
44
+ if (process.env.DRAGON_VERIFY_EXEC !== '1')
45
+ return null; // off by default
46
+ const block = lastCodeBlock(text);
47
+ if (!block)
48
+ return null;
49
+ const spec = LANGS[block.lang];
50
+ if (!spec)
51
+ return { ran: false, reason: `unsupported lang '${block.lang}' (only python/node auto-run)` };
52
+ if (!hasBwrap())
53
+ return { ran: false, reason: 'no bwrap sandbox — refusing to execute (fail-closed)' };
54
+ const dir = mkdtempSync(join(tmpdir(), 'dragon-verify-'));
55
+ try {
56
+ writeFileSync(join(dir, spec.file), block.code, { mode: 0o600 });
57
+ const args = [
58
+ '--unshare-all', '--die-with-parent',
59
+ '--cap-drop', 'ALL', '--new-session', // drop all caps + no controlling tty (defense-in-depth)
60
+ '--ro-bind', '/usr', '/usr',
61
+ '--ro-bind', '/lib', '/lib', '--ro-bind', '/lib64', '/lib64',
62
+ '--proc', '/proc', '--dev', '/dev',
63
+ '--tmpfs', '/tmp',
64
+ '--bind', dir, '/work', '--chdir', '/work',
65
+ '--setenv', 'HOME', '/work', '--setenv', 'PATH', '/usr/bin:/bin',
66
+ spec.cmd, join('/work', spec.file),
67
+ ];
68
+ const r = spawnSync('bwrap', args, { timeout: 8000, encoding: 'utf-8', maxBuffer: 1 << 20 });
69
+ if (r.error)
70
+ return { ran: false, lang: block.lang, reason: String(r.error.message || r.error) };
71
+ const output = ((r.stdout || '') + (r.stderr || '')).slice(0, 2000);
72
+ return { ran: true, ok: r.status === 0, lang: block.lang, output };
73
+ }
74
+ catch (e) {
75
+ return { ran: false, lang: block.lang, reason: e.message };
76
+ }
77
+ finally {
78
+ try {
79
+ rmSync(dir, { recursive: true, force: true });
80
+ }
81
+ catch { /* best-effort */ }
82
+ }
83
+ }
84
+ //# sourceMappingURL=execute.js.map