magi-ai 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.ja.md +377 -0
- package/README.md +377 -0
- package/dist/bin/magi-benchmark.d.ts +14 -0
- package/dist/bin/magi-benchmark.js +93 -0
- package/dist/bin/magi-mcp.d.ts +8 -0
- package/dist/bin/magi-mcp.js +28 -0
- package/dist/bin/magi.d.ts +2 -0
- package/dist/bin/magi.js +634 -0
- package/dist/src/adapters/base.d.ts +34 -0
- package/dist/src/adapters/base.js +149 -0
- package/dist/src/adapters/claude.d.ts +29 -0
- package/dist/src/adapters/claude.js +65 -0
- package/dist/src/adapters/codex.d.ts +21 -0
- package/dist/src/adapters/codex.js +41 -0
- package/dist/src/adapters/gemini.d.ts +18 -0
- package/dist/src/adapters/gemini.js +31 -0
- package/dist/src/adapters/registry.d.ts +19 -0
- package/dist/src/adapters/registry.js +59 -0
- package/dist/src/audit/hash-chain.d.ts +21 -0
- package/dist/src/audit/hash-chain.js +70 -0
- package/dist/src/audit/types.d.ts +25 -0
- package/dist/src/audit/types.js +1 -0
- package/dist/src/audit/writer.d.ts +18 -0
- package/dist/src/audit/writer.js +100 -0
- package/dist/src/benchmark/golden-tasks.d.ts +9 -0
- package/dist/src/benchmark/golden-tasks.js +476 -0
- package/dist/src/benchmark/reporter.d.ts +5 -0
- package/dist/src/benchmark/reporter.js +107 -0
- package/dist/src/benchmark/runner.d.ts +30 -0
- package/dist/src/benchmark/runner.js +224 -0
- package/dist/src/benchmark/scorer.d.ts +12 -0
- package/dist/src/benchmark/scorer.js +124 -0
- package/dist/src/benchmark/types.d.ts +54 -0
- package/dist/src/benchmark/types.js +1 -0
- package/dist/src/cache/deliberation-cache.d.ts +49 -0
- package/dist/src/cache/deliberation-cache.js +127 -0
- package/dist/src/cli/commands/config-cmd.d.ts +11 -0
- package/dist/src/cli/commands/config-cmd.js +190 -0
- package/dist/src/cli/commands/demo.d.ts +12 -0
- package/dist/src/cli/commands/demo.js +66 -0
- package/dist/src/cli/commands/setup.d.ts +7 -0
- package/dist/src/cli/commands/setup.js +182 -0
- package/dist/src/cli/i18n.d.ts +89 -0
- package/dist/src/cli/i18n.js +176 -0
- package/dist/src/cli/interactive-select.d.ts +27 -0
- package/dist/src/cli/interactive-select.js +130 -0
- package/dist/src/cli/tui-setup.d.ts +24 -0
- package/dist/src/cli/tui-setup.js +42 -0
- package/dist/src/config/cli-detector.d.ts +37 -0
- package/dist/src/config/cli-detector.js +99 -0
- package/dist/src/config/user-config.d.ts +81 -0
- package/dist/src/config/user-config.js +134 -0
- package/dist/src/context/auto-collector.d.ts +43 -0
- package/dist/src/context/auto-collector.js +337 -0
- package/dist/src/context/manager.d.ts +35 -0
- package/dist/src/context/manager.js +162 -0
- package/dist/src/context/serializer.d.ts +20 -0
- package/dist/src/context/serializer.js +52 -0
- package/dist/src/demo/recorded-deliberation.d.ts +13 -0
- package/dist/src/demo/recorded-deliberation.js +277 -0
- package/dist/src/engine/angel-detector.d.ts +83 -0
- package/dist/src/engine/angel-detector.js +334 -0
- package/dist/src/engine/at-field.d.ts +40 -0
- package/dist/src/engine/at-field.js +195 -0
- package/dist/src/engine/berserk-orchestrator.d.ts +66 -0
- package/dist/src/engine/berserk-orchestrator.js +378 -0
- package/dist/src/engine/change-metrics.d.ts +56 -0
- package/dist/src/engine/change-metrics.js +214 -0
- package/dist/src/engine/consensus.d.ts +20 -0
- package/dist/src/engine/consensus.js +146 -0
- package/dist/src/engine/dead-sea-scrolls.d.ts +132 -0
- package/dist/src/engine/dead-sea-scrolls.js +610 -0
- package/dist/src/engine/drift-detector.d.ts +39 -0
- package/dist/src/engine/drift-detector.js +225 -0
- package/dist/src/engine/dummy-plug.d.ts +44 -0
- package/dist/src/engine/dummy-plug.js +190 -0
- package/dist/src/engine/engram-manager.d.ts +55 -0
- package/dist/src/engine/engram-manager.js +306 -0
- package/dist/src/engine/events.d.ts +130 -0
- package/dist/src/engine/events.js +44 -0
- package/dist/src/engine/gospel.d.ts +30 -0
- package/dist/src/engine/gospel.js +129 -0
- package/dist/src/engine/hallucination-detector.d.ts +33 -0
- package/dist/src/engine/hallucination-detector.js +215 -0
- package/dist/src/engine/human-resolver.d.ts +19 -0
- package/dist/src/engine/human-resolver.js +89 -0
- package/dist/src/engine/instrumentality.d.ts +64 -0
- package/dist/src/engine/instrumentality.js +297 -0
- package/dist/src/engine/iruel-battle.d.ts +79 -0
- package/dist/src/engine/iruel-battle.js +319 -0
- package/dist/src/engine/kernel/deliberation-kernel.d.ts +12 -0
- package/dist/src/engine/kernel/deliberation-kernel.js +303 -0
- package/dist/src/engine/kernel/index.d.ts +8 -0
- package/dist/src/engine/kernel/index.js +7 -0
- package/dist/src/engine/kernel/phase-runner.d.ts +10 -0
- package/dist/src/engine/kernel/phase-runner.js +155 -0
- package/dist/src/engine/kernel/post-processor.d.ts +17 -0
- package/dist/src/engine/kernel/post-processor.js +131 -0
- package/dist/src/engine/kernel/types.d.ts +107 -0
- package/dist/src/engine/kernel/types.js +1 -0
- package/dist/src/engine/kernel/unit-executor.d.ts +6 -0
- package/dist/src/engine/kernel/unit-executor.js +132 -0
- package/dist/src/engine/lcl-manager.d.ts +44 -0
- package/dist/src/engine/lcl-manager.js +143 -0
- package/dist/src/engine/middleware/cache.d.ts +7 -0
- package/dist/src/engine/middleware/cache.js +29 -0
- package/dist/src/engine/middleware/chain.d.ts +18 -0
- package/dist/src/engine/middleware/chain.js +45 -0
- package/dist/src/engine/middleware/firewall.d.ts +8 -0
- package/dist/src/engine/middleware/firewall.js +24 -0
- package/dist/src/engine/middleware/index.d.ts +4 -0
- package/dist/src/engine/middleware/index.js +3 -0
- package/dist/src/engine/middleware/types.d.ts +43 -0
- package/dist/src/engine/middleware/types.js +1 -0
- package/dist/src/engine/nebuchadnezzar-key.d.ts +61 -0
- package/dist/src/engine/nebuchadnezzar-key.js +203 -0
- package/dist/src/engine/neon-genesis.d.ts +52 -0
- package/dist/src/engine/neon-genesis.js +203 -0
- package/dist/src/engine/objective-judge.d.ts +53 -0
- package/dist/src/engine/objective-judge.js +214 -0
- package/dist/src/engine/offline-mode.d.ts +18 -0
- package/dist/src/engine/offline-mode.js +46 -0
- package/dist/src/engine/orchestrator.d.ts +79 -0
- package/dist/src/engine/orchestrator.js +58 -0
- package/dist/src/engine/secret-cipher.d.ts +26 -0
- package/dist/src/engine/secret-cipher.js +114 -0
- package/dist/src/engine/seele-council.d.ts +90 -0
- package/dist/src/engine/seele-council.js +482 -0
- package/dist/src/engine/self-destruct.d.ts +61 -0
- package/dist/src/engine/self-destruct.js +231 -0
- package/dist/src/engine/self-evolution.d.ts +64 -0
- package/dist/src/engine/self-evolution.js +368 -0
- package/dist/src/engine/sync-rate.d.ts +45 -0
- package/dist/src/engine/sync-rate.js +151 -0
- package/dist/src/engine/type666-firewall.d.ts +76 -0
- package/dist/src/engine/type666-firewall.js +343 -0
- package/dist/src/engine/umbilical-cable.d.ts +41 -0
- package/dist/src/engine/umbilical-cable.js +192 -0
- package/dist/src/index.d.ts +106 -0
- package/dist/src/index.js +426 -0
- package/dist/src/mcp/server.d.ts +38 -0
- package/dist/src/mcp/server.js +196 -0
- package/dist/src/metrics/token-tracker.d.ts +38 -0
- package/dist/src/metrics/token-tracker.js +112 -0
- package/dist/src/parsers/json-extractor.d.ts +9 -0
- package/dist/src/parsers/json-extractor.js +239 -0
- package/dist/src/parsers/opinion-schema.d.ts +81 -0
- package/dist/src/parsers/opinion-schema.js +147 -0
- package/dist/src/parsers/unstructured-parser.d.ts +20 -0
- package/dist/src/parsers/unstructured-parser.js +122 -0
- package/dist/src/pipelines/architecture.d.ts +10 -0
- package/dist/src/pipelines/architecture.js +9 -0
- package/dist/src/pipelines/bug-analysis.d.ts +9 -0
- package/dist/src/pipelines/bug-analysis.js +8 -0
- package/dist/src/pipelines/code-review.d.ts +10 -0
- package/dist/src/pipelines/code-review.js +30 -0
- package/dist/src/pipelines/custom.d.ts +14 -0
- package/dist/src/pipelines/custom.js +29 -0
- package/dist/src/pipelines/registry.d.ts +9 -0
- package/dist/src/pipelines/registry.js +20 -0
- package/dist/src/prompts/personas.d.ts +6 -0
- package/dist/src/prompts/personas.js +44 -0
- package/dist/src/prompts/schemas.d.ts +4 -0
- package/dist/src/prompts/schemas.js +24 -0
- package/dist/src/prompts/templates.d.ts +6 -0
- package/dist/src/prompts/templates.js +91 -0
- package/dist/src/repl/accessibility.d.ts +23 -0
- package/dist/src/repl/accessibility.js +46 -0
- package/dist/src/repl/banner.d.ts +4 -0
- package/dist/src/repl/banner.js +28 -0
- package/dist/src/repl/boot-animation.d.ts +13 -0
- package/dist/src/repl/boot-animation.js +143 -0
- package/dist/src/repl/completer.d.ts +21 -0
- package/dist/src/repl/completer.js +168 -0
- package/dist/src/repl/context.d.ts +24 -0
- package/dist/src/repl/context.js +42 -0
- package/dist/src/repl/display-utils.d.ts +13 -0
- package/dist/src/repl/display-utils.js +65 -0
- package/dist/src/repl/event-listener.d.ts +18 -0
- package/dist/src/repl/event-listener.js +112 -0
- package/dist/src/repl/export-formatter.d.ts +8 -0
- package/dist/src/repl/export-formatter.js +73 -0
- package/dist/src/repl/ghost-text.d.ts +31 -0
- package/dist/src/repl/ghost-text.js +119 -0
- package/dist/src/repl/handoff-animation.d.ts +15 -0
- package/dist/src/repl/handoff-animation.js +65 -0
- package/dist/src/repl/history.d.ts +16 -0
- package/dist/src/repl/history.js +130 -0
- package/dist/src/repl/job-registry.d.ts +26 -0
- package/dist/src/repl/job-registry.js +80 -0
- package/dist/src/repl/magi-repl.d.ts +72 -0
- package/dist/src/repl/magi-repl.js +1008 -0
- package/dist/src/repl/multiline-input.d.ts +45 -0
- package/dist/src/repl/multiline-input.js +78 -0
- package/dist/src/repl/prompt-builder.d.ts +19 -0
- package/dist/src/repl/prompt-builder.js +36 -0
- package/dist/src/repl/repl-state.d.ts +5 -0
- package/dist/src/repl/repl-state.js +19 -0
- package/dist/src/repl/result-display.d.ts +8 -0
- package/dist/src/repl/result-display.js +195 -0
- package/dist/src/repl/session-stats.d.ts +26 -0
- package/dist/src/repl/session-stats.js +119 -0
- package/dist/src/repl/slash-commands.d.ts +60 -0
- package/dist/src/repl/slash-commands.js +725 -0
- package/dist/src/repl/terminal-sanitize.d.ts +14 -0
- package/dist/src/repl/terminal-sanitize.js +19 -0
- package/dist/src/reporters/console.d.ts +7 -0
- package/dist/src/reporters/console.js +78 -0
- package/dist/src/reporters/json.d.ts +2 -0
- package/dist/src/reporters/json.js +3 -0
- package/dist/src/reporters/markdown.d.ts +2 -0
- package/dist/src/reporters/markdown.js +65 -0
- package/dist/src/reporters/streaming.d.ts +20 -0
- package/dist/src/reporters/streaming.js +178 -0
- package/dist/src/tui/activity-log.d.ts +23 -0
- package/dist/src/tui/activity-log.js +67 -0
- package/dist/src/tui/animations.d.ts +39 -0
- package/dist/src/tui/animations.js +167 -0
- package/dist/src/tui/ansi.d.ts +28 -0
- package/dist/src/tui/ansi.js +51 -0
- package/dist/src/tui/boot-sequence.d.ts +11 -0
- package/dist/src/tui/boot-sequence.js +98 -0
- package/dist/src/tui/colors.d.ts +101 -0
- package/dist/src/tui/colors.js +71 -0
- package/dist/src/tui/header.d.ts +24 -0
- package/dist/src/tui/header.js +122 -0
- package/dist/src/tui/index.d.ts +3 -0
- package/dist/src/tui/index.js +3 -0
- package/dist/src/tui/keypress.d.ts +25 -0
- package/dist/src/tui/keypress.js +95 -0
- package/dist/src/tui/layout.d.ts +74 -0
- package/dist/src/tui/layout.js +171 -0
- package/dist/src/tui/magi-tui.d.ts +101 -0
- package/dist/src/tui/magi-tui.js +754 -0
- package/dist/src/tui/panel.d.ts +45 -0
- package/dist/src/tui/panel.js +292 -0
- package/dist/src/tui/screen-buffer.d.ts +54 -0
- package/dist/src/tui/screen-buffer.js +262 -0
- package/dist/src/tui/status-bar.d.ts +25 -0
- package/dist/src/tui/status-bar.js +124 -0
- package/dist/src/tui/terminal-detect.d.ts +26 -0
- package/dist/src/tui/terminal-detect.js +44 -0
- package/dist/src/tui/tui-helpers.d.ts +12 -0
- package/dist/src/tui/tui-helpers.js +37 -0
- package/dist/src/types/adapter.d.ts +75 -0
- package/dist/src/types/adapter.js +36 -0
- package/dist/src/types/config.d.ts +108 -0
- package/dist/src/types/config.js +85 -0
- package/dist/src/types/consensus.d.ts +55 -0
- package/dist/src/types/consensus.js +17 -0
- package/dist/src/types/core.d.ts +178 -0
- package/dist/src/types/core.js +85 -0
- package/dist/src/types/magi-api.d.ts +62 -0
- package/dist/src/types/magi-api.js +7 -0
- package/dist/src/types/phase-h.d.ts +142 -0
- package/dist/src/types/phase-h.js +7 -0
- package/dist/src/types/phase-i.d.ts +186 -0
- package/dist/src/types/phase-i.js +6 -0
- package/dist/src/types/phase-k.d.ts +259 -0
- package/dist/src/types/phase-k.js +6 -0
- package/dist/src/types/phase-l.d.ts +199 -0
- package/dist/src/types/phase-l.js +6 -0
- package/dist/src/types/pipeline.d.ts +37 -0
- package/dist/src/types/pipeline.js +2 -0
- package/dist/src/utils/abstain-factory.d.ts +2 -0
- package/dist/src/utils/abstain-factory.js +18 -0
- package/dist/src/utils/errors.d.ts +34 -0
- package/dist/src/utils/errors.js +59 -0
- package/dist/src/utils/file-validator.d.ts +50 -0
- package/dist/src/utils/file-validator.js +124 -0
- package/dist/src/utils/fire-and-forget.d.ts +5 -0
- package/dist/src/utils/fire-and-forget.js +10 -0
- package/dist/src/utils/flag-validator.d.ts +21 -0
- package/dist/src/utils/flag-validator.js +79 -0
- package/dist/src/utils/freeze.d.ts +8 -0
- package/dist/src/utils/freeze.js +16 -0
- package/dist/src/utils/language-detector.d.ts +16 -0
- package/dist/src/utils/language-detector.js +159 -0
- package/dist/src/utils/latency-tracker.d.ts +45 -0
- package/dist/src/utils/latency-tracker.js +100 -0
- package/dist/src/utils/logger.d.ts +33 -0
- package/dist/src/utils/logger.js +112 -0
- package/dist/src/utils/process.d.ts +40 -0
- package/dist/src/utils/process.js +253 -0
- package/dist/src/utils/retry.d.ts +12 -0
- package/dist/src/utils/retry.js +30 -0
- package/dist/src/utils/safe-fs.d.ts +38 -0
- package/dist/src/utils/safe-fs.js +56 -0
- package/dist/src/utils/safe-json-parse.d.ts +15 -0
- package/dist/src/utils/safe-json-parse.js +49 -0
- package/dist/src/utils/sanitize.d.ts +14 -0
- package/dist/src/utils/sanitize.js +186 -0
- package/dist/src/utils/semaphore.d.ts +22 -0
- package/dist/src/utils/semaphore.js +57 -0
- package/dist/src/utils/shutdown.d.ts +6 -0
- package/dist/src/utils/shutdown.js +51 -0
- package/dist/src/utils/tty.d.ts +5 -0
- package/dist/src/utils/tty.js +7 -0
- package/package.json +82 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { AuditLogWriter } from '../audit/writer.js';
|
|
2
|
+
const LOG_LEVELS = {
|
|
3
|
+
debug: 0,
|
|
4
|
+
info: 1,
|
|
5
|
+
warn: 2,
|
|
6
|
+
error: 3,
|
|
7
|
+
};
|
|
8
|
+
let currentLevel = 'info';
|
|
9
|
+
let auditWriter = null;
|
|
10
|
+
let tuiActive = false;
|
|
11
|
+
let tuiBuffer = [];
|
|
12
|
+
/**
|
|
13
|
+
* Suppress console output during TUI mode.
|
|
14
|
+
* Logs are buffered and flushed when TUI mode is disabled.
|
|
15
|
+
*/
|
|
16
|
+
export function setTuiMode(enabled) {
|
|
17
|
+
if (tuiActive && !enabled) {
|
|
18
|
+
for (const entry of tuiBuffer) {
|
|
19
|
+
console.error(`${entry.level} ${entry.message}`, entry.data);
|
|
20
|
+
}
|
|
21
|
+
tuiBuffer = [];
|
|
22
|
+
}
|
|
23
|
+
tuiActive = enabled;
|
|
24
|
+
}
|
|
25
|
+
export function setLogLevel(level) {
|
|
26
|
+
currentLevel = level;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Initialize audit logging. Must be called before logger.audit() will write entries.
|
|
30
|
+
* If not called, audit() calls are silently ignored.
|
|
31
|
+
*/
|
|
32
|
+
export async function initAuditLog(config) {
|
|
33
|
+
auditWriter = new AuditLogWriter(config);
|
|
34
|
+
await auditWriter.initialize();
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Get the current audit writer (for testing purposes).
|
|
38
|
+
*/
|
|
39
|
+
export function getAuditWriter() {
|
|
40
|
+
return auditWriter;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Reset audit writer (for testing teardown).
|
|
44
|
+
*/
|
|
45
|
+
export function resetAuditWriter() {
|
|
46
|
+
auditWriter = null;
|
|
47
|
+
}
|
|
48
|
+
function shouldLog(level) {
|
|
49
|
+
return LOG_LEVELS[level] >= LOG_LEVELS[currentLevel];
|
|
50
|
+
}
|
|
51
|
+
function formatTimestamp() {
|
|
52
|
+
return new Date().toISOString();
|
|
53
|
+
}
|
|
54
|
+
export const logger = {
|
|
55
|
+
debug(message, data) {
|
|
56
|
+
if (!shouldLog('debug'))
|
|
57
|
+
return;
|
|
58
|
+
const prefix = `[${formatTimestamp()}] DEBUG`;
|
|
59
|
+
if (tuiActive) {
|
|
60
|
+
tuiBuffer.push({ level: prefix, message, data: data ? JSON.stringify(data) : '' });
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
console.error(`${prefix} ${message}`, data ?? '');
|
|
64
|
+
},
|
|
65
|
+
info(message, data) {
|
|
66
|
+
if (!shouldLog('info'))
|
|
67
|
+
return;
|
|
68
|
+
const prefix = `[${formatTimestamp()}] INFO `;
|
|
69
|
+
if (tuiActive) {
|
|
70
|
+
tuiBuffer.push({ level: prefix, message, data: data ? JSON.stringify(data) : '' });
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
console.error(`${prefix} ${message}`, data ?? '');
|
|
74
|
+
},
|
|
75
|
+
warn(message, data) {
|
|
76
|
+
if (!shouldLog('warn'))
|
|
77
|
+
return;
|
|
78
|
+
const prefix = `[${formatTimestamp()}] WARN `;
|
|
79
|
+
if (tuiActive) {
|
|
80
|
+
tuiBuffer.push({ level: prefix, message, data: data ? JSON.stringify(data) : '' });
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
console.error(`${prefix} ${message}`, data ?? '');
|
|
84
|
+
},
|
|
85
|
+
error(message, data) {
|
|
86
|
+
if (!shouldLog('error'))
|
|
87
|
+
return;
|
|
88
|
+
const prefix = `[${formatTimestamp()}] ERROR`;
|
|
89
|
+
if (tuiActive) {
|
|
90
|
+
tuiBuffer.push({ level: prefix, message, data: data ? JSON.stringify(data) : '' });
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
console.error(`${prefix} ${message}`, data ?? '');
|
|
94
|
+
},
|
|
95
|
+
/**
|
|
96
|
+
* Write an audit log entry. Fire-and-forget -- never blocks the caller.
|
|
97
|
+
* If audit logging is not initialized, the call is silently ignored.
|
|
98
|
+
*/
|
|
99
|
+
audit(event, deliberationId, data) {
|
|
100
|
+
if (!auditWriter)
|
|
101
|
+
return;
|
|
102
|
+
// Fire and forget -- catch errors to prevent unhandled rejections
|
|
103
|
+
auditWriter.write(event, deliberationId, data).catch((err) => {
|
|
104
|
+
const msg = `[AUDIT ERROR] Failed to write audit event ${event}: ${String(err)}`;
|
|
105
|
+
if (tuiActive) {
|
|
106
|
+
tuiBuffer.push({ level: '[AUDIT]', message: msg, data: '' });
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
console.error(msg);
|
|
110
|
+
});
|
|
111
|
+
},
|
|
112
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export interface SpawnResult {
|
|
2
|
+
stdout: string;
|
|
3
|
+
stderr: string;
|
|
4
|
+
exitCode: number;
|
|
5
|
+
durationMs: number;
|
|
6
|
+
timedOut: boolean;
|
|
7
|
+
stdoutTruncated: boolean;
|
|
8
|
+
stderrTruncated: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface SpawnOptions {
|
|
11
|
+
command: string;
|
|
12
|
+
args: string[];
|
|
13
|
+
stdin?: string;
|
|
14
|
+
timeoutMs?: number;
|
|
15
|
+
cwd?: string;
|
|
16
|
+
env?: Record<string, string>;
|
|
17
|
+
maxBufferSize?: number;
|
|
18
|
+
onStdoutChunk?: (chunk: string) => void;
|
|
19
|
+
onStderrChunk?: (chunk: string) => void;
|
|
20
|
+
signal?: AbortSignal;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Filter environment variables to only allowed keys.
|
|
24
|
+
* Prevents leaking sensitive env vars to child processes.
|
|
25
|
+
*/
|
|
26
|
+
export declare function filterEnvironment(env?: NodeJS.ProcessEnv, extraKeys?: readonly string[]): Record<string, string>;
|
|
27
|
+
/**
|
|
28
|
+
* Validate that a command is in the allowlist.
|
|
29
|
+
* Throws if the command is not allowed.
|
|
30
|
+
*/
|
|
31
|
+
export declare function validateCommand(command: string): void;
|
|
32
|
+
/**
|
|
33
|
+
* Spawns a CLI process with timeout, stdin support, and structured result.
|
|
34
|
+
* Uses stdin for prompt injection to prevent shell injection attacks.
|
|
35
|
+
*/
|
|
36
|
+
export declare function spawnProcess(options: SpawnOptions): Promise<SpawnResult>;
|
|
37
|
+
/**
|
|
38
|
+
* Check if a CLI binary is available in PATH.
|
|
39
|
+
*/
|
|
40
|
+
export declare function isBinaryAvailable(name: string): Promise<boolean>;
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { realpathSync, lstatSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
5
|
+
import { registerProcess } from './shutdown.js';
|
|
6
|
+
import { logger } from './logger.js';
|
|
7
|
+
/** Environment variables allowed to pass through to child processes */
|
|
8
|
+
const ENV_ALLOWLIST = new Set([
|
|
9
|
+
'PATH',
|
|
10
|
+
'HOME',
|
|
11
|
+
'USER',
|
|
12
|
+
'SHELL',
|
|
13
|
+
'LANG',
|
|
14
|
+
'LC_ALL',
|
|
15
|
+
'TERM',
|
|
16
|
+
'TMPDIR',
|
|
17
|
+
'XDG_CONFIG_HOME',
|
|
18
|
+
'XDG_DATA_HOME',
|
|
19
|
+
'NODE_ENV',
|
|
20
|
+
]);
|
|
21
|
+
/** Per-command API key mapping (least-privilege) */
|
|
22
|
+
const COMMAND_API_KEYS = {
|
|
23
|
+
claude: ['ANTHROPIC_API_KEY'],
|
|
24
|
+
codex: ['OPENAI_API_KEY'],
|
|
25
|
+
gemini: ['GOOGLE_API_KEY', 'GEMINI_API_KEY'],
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Filter environment variables to only allowed keys.
|
|
29
|
+
* Prevents leaking sensitive env vars to child processes.
|
|
30
|
+
*/
|
|
31
|
+
export function filterEnvironment(env = process.env, extraKeys) {
|
|
32
|
+
const allowed = new Set([...ENV_ALLOWLIST, ...(extraKeys ?? [])]);
|
|
33
|
+
const filtered = {};
|
|
34
|
+
for (const key of allowed) {
|
|
35
|
+
const value = env[key];
|
|
36
|
+
if (value !== undefined) {
|
|
37
|
+
filtered[key] = value;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return filtered;
|
|
41
|
+
}
|
|
42
|
+
/** Commands allowed to be executed */
|
|
43
|
+
const COMMAND_ALLOWLIST = new Set([
|
|
44
|
+
'claude',
|
|
45
|
+
'codex',
|
|
46
|
+
'gemini',
|
|
47
|
+
'which',
|
|
48
|
+
]);
|
|
49
|
+
/**
|
|
50
|
+
* Validate that a command is in the allowlist.
|
|
51
|
+
* Throws if the command is not allowed.
|
|
52
|
+
*/
|
|
53
|
+
export function validateCommand(command) {
|
|
54
|
+
// Reject null bytes
|
|
55
|
+
if (command.includes('\0')) {
|
|
56
|
+
throw new Error('Command must not contain null bytes');
|
|
57
|
+
}
|
|
58
|
+
// Extract basename (handle full paths like /usr/bin/claude)
|
|
59
|
+
const basename = command.split('/').pop() ?? command;
|
|
60
|
+
if (COMMAND_ALLOWLIST.has(basename)) {
|
|
61
|
+
// Falls through to symlink check below if path has separators
|
|
62
|
+
}
|
|
63
|
+
else if (command.includes('node_modules/.bin/')) {
|
|
64
|
+
// Allow local devDependency binaries (tsc, eslint, vitest, etc.)
|
|
65
|
+
const normalized = path.normalize(command);
|
|
66
|
+
if (normalized.includes('..')) {
|
|
67
|
+
throw new Error(`Path traversal detected in command: ${command}`);
|
|
68
|
+
}
|
|
69
|
+
return; // node_modules/.bin paths don't need symlink check
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
throw new Error(`Command not allowed: ${command}. Allowed: ${[...COMMAND_ALLOWLIST].join(', ')}`);
|
|
73
|
+
}
|
|
74
|
+
// If command contains a path separator, resolve symlinks and re-validate
|
|
75
|
+
if (command.includes('/')) {
|
|
76
|
+
try {
|
|
77
|
+
const stats = lstatSync(command);
|
|
78
|
+
if (stats.isSymbolicLink()) {
|
|
79
|
+
logger.warn('Symlink detected in command path, resolving', { command });
|
|
80
|
+
}
|
|
81
|
+
const resolved = realpathSync(command);
|
|
82
|
+
const resolvedBasename = resolved.split('/').pop() ?? resolved;
|
|
83
|
+
if (!COMMAND_ALLOWLIST.has(resolvedBasename)) {
|
|
84
|
+
throw new Error(`Resolved command not allowed: ${command} -> ${resolved}. Allowed: ${[...COMMAND_ALLOWLIST].join(', ')}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
// ENOENT: binary not found at explicit path — let spawn handle the error
|
|
89
|
+
if (err.code === 'ENOENT') {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
throw err;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Spawns a CLI process with timeout, stdin support, and structured result.
|
|
98
|
+
* Uses stdin for prompt injection to prevent shell injection attacks.
|
|
99
|
+
*/
|
|
100
|
+
export function spawnProcess(options) {
|
|
101
|
+
const { command, args, stdin: stdinData, timeoutMs = 120_000, cwd, env: extraEnv, maxBufferSize = 10 * 1024 * 1024, } = options;
|
|
102
|
+
validateCommand(command);
|
|
103
|
+
// Inject only the API keys needed for this specific command (least-privilege)
|
|
104
|
+
const cmdBasename = command.split('/').pop() ?? command;
|
|
105
|
+
const apiKeys = COMMAND_API_KEYS[cmdBasename] ?? [];
|
|
106
|
+
const filteredEnv = filterEnvironment(undefined, apiKeys);
|
|
107
|
+
const env = extraEnv
|
|
108
|
+
? { ...filteredEnv, ...filterEnvironment(extraEnv) }
|
|
109
|
+
: filteredEnv;
|
|
110
|
+
return new Promise((resolve, reject) => {
|
|
111
|
+
const startTime = Date.now();
|
|
112
|
+
const stdoutChunks = [];
|
|
113
|
+
const stderrChunks = [];
|
|
114
|
+
let stdoutSize = 0;
|
|
115
|
+
let stderrSize = 0;
|
|
116
|
+
let stdoutTruncated = false;
|
|
117
|
+
let stderrTruncated = false;
|
|
118
|
+
let timedOut = false;
|
|
119
|
+
logger.debug(`Spawning: ${command} ${args.join(' ')}`, { cwd, timeoutMs });
|
|
120
|
+
const proc = spawn(command, args, {
|
|
121
|
+
cwd,
|
|
122
|
+
env,
|
|
123
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
124
|
+
});
|
|
125
|
+
registerProcess(proc);
|
|
126
|
+
// External abort support (e.g. from orchestrator phase timeout)
|
|
127
|
+
if (options.signal) {
|
|
128
|
+
const killProc = () => {
|
|
129
|
+
if (!proc.killed) {
|
|
130
|
+
timedOut = true;
|
|
131
|
+
proc.kill('SIGTERM');
|
|
132
|
+
const forceTimer = setTimeout(() => {
|
|
133
|
+
if (!proc.killed)
|
|
134
|
+
proc.kill('SIGKILL');
|
|
135
|
+
}, 5_000);
|
|
136
|
+
forceTimer.unref();
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
if (options.signal.aborted) {
|
|
140
|
+
killProc();
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
options.signal.addEventListener('abort', killProc, { once: true });
|
|
144
|
+
proc.on('close', () => {
|
|
145
|
+
options.signal.removeEventListener('abort', killProc);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// Handle EPIPE: child may exit before reading all stdin
|
|
150
|
+
proc.stdin.on('error', (err) => {
|
|
151
|
+
if (err.code !== 'EPIPE') {
|
|
152
|
+
logger.warn('stdin write error', { error: String(err) });
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
if (stdinData) {
|
|
156
|
+
proc.stdin.write(stdinData);
|
|
157
|
+
proc.stdin.end();
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
proc.stdin.end();
|
|
161
|
+
}
|
|
162
|
+
// Use StringDecoder to safely handle UTF-8 multibyte chars at chunk boundaries
|
|
163
|
+
const stdoutDecoder = new StringDecoder('utf8');
|
|
164
|
+
const stderrDecoder = new StringDecoder('utf8');
|
|
165
|
+
proc.stdout.on('data', (data) => {
|
|
166
|
+
const chunk = stdoutDecoder.write(data);
|
|
167
|
+
stdoutSize += data.length;
|
|
168
|
+
if (stdoutSize <= maxBufferSize) {
|
|
169
|
+
stdoutChunks.push(chunk);
|
|
170
|
+
}
|
|
171
|
+
else if (!stdoutTruncated) {
|
|
172
|
+
stdoutTruncated = true;
|
|
173
|
+
logger.warn(`stdout exceeded ${maxBufferSize} bytes, truncating`);
|
|
174
|
+
}
|
|
175
|
+
options.onStdoutChunk?.(chunk);
|
|
176
|
+
});
|
|
177
|
+
proc.stderr.on('data', (data) => {
|
|
178
|
+
const chunk = stderrDecoder.write(data);
|
|
179
|
+
stderrSize += data.length;
|
|
180
|
+
if (stderrSize <= maxBufferSize) {
|
|
181
|
+
stderrChunks.push(chunk);
|
|
182
|
+
}
|
|
183
|
+
else if (!stderrTruncated) {
|
|
184
|
+
stderrTruncated = true;
|
|
185
|
+
logger.warn(`stderr exceeded ${maxBufferSize} bytes, truncating`);
|
|
186
|
+
}
|
|
187
|
+
options.onStderrChunk?.(chunk);
|
|
188
|
+
});
|
|
189
|
+
const timer = setTimeout(() => {
|
|
190
|
+
timedOut = true;
|
|
191
|
+
proc.kill('SIGTERM');
|
|
192
|
+
// Give 5s grace period then SIGKILL
|
|
193
|
+
const forceKillTimer = setTimeout(() => {
|
|
194
|
+
if (!proc.killed)
|
|
195
|
+
proc.kill('SIGKILL');
|
|
196
|
+
}, 5_000);
|
|
197
|
+
forceKillTimer.unref(); // Don't block Node.js exit
|
|
198
|
+
}, timeoutMs);
|
|
199
|
+
proc.on('close', (exitCode) => {
|
|
200
|
+
clearTimeout(timer);
|
|
201
|
+
// Flush remaining multibyte bytes from decoders
|
|
202
|
+
const stdoutFinal = stdoutDecoder.end();
|
|
203
|
+
if (stdoutFinal && stdoutSize <= maxBufferSize)
|
|
204
|
+
stdoutChunks.push(stdoutFinal);
|
|
205
|
+
const stderrFinal = stderrDecoder.end();
|
|
206
|
+
if (stderrFinal && stderrSize <= maxBufferSize)
|
|
207
|
+
stderrChunks.push(stderrFinal);
|
|
208
|
+
const durationMs = Date.now() - startTime;
|
|
209
|
+
const stdout = stdoutChunks.join('');
|
|
210
|
+
const stderr = stderrChunks.join('');
|
|
211
|
+
logger.debug(`Process exited`, {
|
|
212
|
+
command,
|
|
213
|
+
exitCode: exitCode ?? -1,
|
|
214
|
+
durationMs,
|
|
215
|
+
timedOut,
|
|
216
|
+
stdoutLen: stdout.length,
|
|
217
|
+
stderrLen: stderr.length,
|
|
218
|
+
stdoutTruncated,
|
|
219
|
+
stderrTruncated,
|
|
220
|
+
});
|
|
221
|
+
resolve({
|
|
222
|
+
stdout,
|
|
223
|
+
stderr,
|
|
224
|
+
exitCode: exitCode ?? -1,
|
|
225
|
+
durationMs,
|
|
226
|
+
timedOut,
|
|
227
|
+
stdoutTruncated,
|
|
228
|
+
stderrTruncated,
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
proc.on('error', (err) => {
|
|
232
|
+
clearTimeout(timer);
|
|
233
|
+
reject(err);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Check if a CLI binary is available in PATH.
|
|
239
|
+
*/
|
|
240
|
+
export async function isBinaryAvailable(name) {
|
|
241
|
+
try {
|
|
242
|
+
const result = await spawnProcess({
|
|
243
|
+
command: 'which',
|
|
244
|
+
args: [name],
|
|
245
|
+
timeoutMs: 5_000,
|
|
246
|
+
});
|
|
247
|
+
return result.exitCode === 0;
|
|
248
|
+
}
|
|
249
|
+
catch (err) {
|
|
250
|
+
logger.debug('isBinaryAvailable: check failed', { name, error: String(err) });
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface RetryOptions {
|
|
2
|
+
maxRetries: number;
|
|
3
|
+
baseDelayMs?: number;
|
|
4
|
+
shouldRetry?: (error: unknown) => boolean;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Retry a function with exponential backoff.
|
|
8
|
+
*/
|
|
9
|
+
export declare function withRetry<T>(fn: (attempt: number) => Promise<T>, options: RetryOptions): Promise<{
|
|
10
|
+
result: T;
|
|
11
|
+
attempts: number;
|
|
12
|
+
}>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { logger } from './logger.js';
|
|
2
|
+
/**
|
|
3
|
+
* Retry a function with exponential backoff.
|
|
4
|
+
*/
|
|
5
|
+
export async function withRetry(fn, options) {
|
|
6
|
+
const { maxRetries, baseDelayMs = 1_000, shouldRetry } = options;
|
|
7
|
+
let lastError;
|
|
8
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
9
|
+
try {
|
|
10
|
+
const result = await fn(attempt);
|
|
11
|
+
return { result, attempts: attempt + 1 };
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
lastError = error;
|
|
15
|
+
if (attempt >= maxRetries)
|
|
16
|
+
break;
|
|
17
|
+
if (shouldRetry && !shouldRetry(error))
|
|
18
|
+
break;
|
|
19
|
+
const delay = baseDelayMs * Math.pow(2, attempt);
|
|
20
|
+
logger.warn(`Retry ${attempt + 1}/${maxRetries} after ${delay}ms`, {
|
|
21
|
+
error: String(error),
|
|
22
|
+
});
|
|
23
|
+
await sleep(delay);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
throw lastError;
|
|
27
|
+
}
|
|
28
|
+
function sleep(ms) {
|
|
29
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
30
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* safe-fs --- セキュアなファイル永続化ユーティリティ
|
|
3
|
+
*
|
|
4
|
+
* mkdir + writeFile + chmod の重複パターンを統一し、
|
|
5
|
+
* デフォルトでセキュアなパーミッション (0o700/0o600) を保証する。
|
|
6
|
+
* symlink検出もオプションで提供。
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* セキュアなディレクトリ作成。
|
|
10
|
+
* デフォルトで 0o700 (owner only) パーミッション + symlink検出。
|
|
11
|
+
*/
|
|
12
|
+
export declare function safeMkdir(dirPath: string, options?: {
|
|
13
|
+
mode?: number;
|
|
14
|
+
checkSymlink?: boolean;
|
|
15
|
+
}): Promise<void>;
|
|
16
|
+
/**
|
|
17
|
+
* セキュアなファイル書き込み。
|
|
18
|
+
* デフォルトで 0o600 (owner read/write) パーミッション。
|
|
19
|
+
*/
|
|
20
|
+
export declare function safeWriteFile(filePath: string, content: string, options?: {
|
|
21
|
+
mode?: number;
|
|
22
|
+
}): Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* セキュアなファイル追記。
|
|
25
|
+
* デフォルトで 0o600 (owner read/write) パーミッション。
|
|
26
|
+
*/
|
|
27
|
+
export declare function safeAppendFile(filePath: string, content: string, options?: {
|
|
28
|
+
mode?: number;
|
|
29
|
+
}): Promise<void>;
|
|
30
|
+
/**
|
|
31
|
+
* ディレクトリ作成 + ファイル書き込みを一括で実行。
|
|
32
|
+
* 最もよくあるパターン: mkdir(dirname(filePath)) → writeFile(filePath)
|
|
33
|
+
*/
|
|
34
|
+
export declare function safePersist(filePath: string, content: string, options?: {
|
|
35
|
+
dirMode?: number;
|
|
36
|
+
fileMode?: number;
|
|
37
|
+
checkSymlink?: boolean;
|
|
38
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* safe-fs --- セキュアなファイル永続化ユーティリティ
|
|
3
|
+
*
|
|
4
|
+
* mkdir + writeFile + chmod の重複パターンを統一し、
|
|
5
|
+
* デフォルトでセキュアなパーミッション (0o700/0o600) を保証する。
|
|
6
|
+
* symlink検出もオプションで提供。
|
|
7
|
+
*/
|
|
8
|
+
import { mkdir, writeFile, appendFile, lstat } from 'node:fs/promises';
|
|
9
|
+
import { dirname } from 'node:path';
|
|
10
|
+
import { logger } from './logger.js';
|
|
11
|
+
/**
|
|
12
|
+
* セキュアなディレクトリ作成。
|
|
13
|
+
* デフォルトで 0o700 (owner only) パーミッション + symlink検出。
|
|
14
|
+
*/
|
|
15
|
+
export async function safeMkdir(dirPath, options) {
|
|
16
|
+
const mode = options?.mode ?? 0o700;
|
|
17
|
+
await mkdir(dirPath, { recursive: true, mode });
|
|
18
|
+
if (options?.checkSymlink !== false) {
|
|
19
|
+
try {
|
|
20
|
+
const stats = await lstat(dirPath);
|
|
21
|
+
if (stats.isSymbolicLink()) {
|
|
22
|
+
throw new Error(`Symlink detected at ${dirPath}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
if (err instanceof Error && err.message.startsWith('Symlink detected'))
|
|
27
|
+
throw err;
|
|
28
|
+
logger.debug('safeMkdir: directory stat failed during symlink check', { dirPath, error: String(err) });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* セキュアなファイル書き込み。
|
|
34
|
+
* デフォルトで 0o600 (owner read/write) パーミッション。
|
|
35
|
+
*/
|
|
36
|
+
export async function safeWriteFile(filePath, content, options) {
|
|
37
|
+
await writeFile(filePath, content, { encoding: 'utf-8', mode: options?.mode ?? 0o600 });
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* セキュアなファイル追記。
|
|
41
|
+
* デフォルトで 0o600 (owner read/write) パーミッション。
|
|
42
|
+
*/
|
|
43
|
+
export async function safeAppendFile(filePath, content, options) {
|
|
44
|
+
await appendFile(filePath, content, { encoding: 'utf-8', mode: options?.mode ?? 0o600 });
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* ディレクトリ作成 + ファイル書き込みを一括で実行。
|
|
48
|
+
* 最もよくあるパターン: mkdir(dirname(filePath)) → writeFile(filePath)
|
|
49
|
+
*/
|
|
50
|
+
export async function safePersist(filePath, content, options) {
|
|
51
|
+
await safeMkdir(dirname(filePath), {
|
|
52
|
+
mode: options?.dirMode,
|
|
53
|
+
checkSymlink: options?.checkSymlink,
|
|
54
|
+
});
|
|
55
|
+
await safeWriteFile(filePath, content, { mode: options?.fileMode });
|
|
56
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safe JSON parser with prototype pollution prevention.
|
|
3
|
+
*
|
|
4
|
+
* Rejects objects containing __proto__, constructor, or prototype keys
|
|
5
|
+
* at any nesting depth to prevent prototype pollution attacks.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Parse JSON string with prototype pollution protection.
|
|
9
|
+
* Returns the parsed value, or throws on invalid JSON or prototype pollution.
|
|
10
|
+
*/
|
|
11
|
+
export declare function safeJsonParse<T = unknown>(raw: string): T;
|
|
12
|
+
/**
|
|
13
|
+
* Parse JSON string safely, returning null on any error instead of throwing.
|
|
14
|
+
*/
|
|
15
|
+
export declare function safeJsonParseOrNull<T = unknown>(raw: string): T | null;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safe JSON parser with prototype pollution prevention.
|
|
3
|
+
*
|
|
4
|
+
* Rejects objects containing __proto__, constructor, or prototype keys
|
|
5
|
+
* at any nesting depth to prevent prototype pollution attacks.
|
|
6
|
+
*/
|
|
7
|
+
import { logger } from './logger.js';
|
|
8
|
+
const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
9
|
+
function assertNoPrototypePollution(val) {
|
|
10
|
+
if (val === null || typeof val !== 'object')
|
|
11
|
+
return;
|
|
12
|
+
const stack = [val];
|
|
13
|
+
const visited = new WeakSet();
|
|
14
|
+
while (stack.length > 0) {
|
|
15
|
+
const current = stack.pop();
|
|
16
|
+
if (current === null || typeof current !== 'object')
|
|
17
|
+
continue;
|
|
18
|
+
if (visited.has(current))
|
|
19
|
+
continue;
|
|
20
|
+
visited.add(current);
|
|
21
|
+
for (const key of Object.keys(current)) {
|
|
22
|
+
if (UNSAFE_KEYS.has(key)) {
|
|
23
|
+
throw new Error(`Prototype pollution attempt detected: key "${key}"`);
|
|
24
|
+
}
|
|
25
|
+
stack.push(current[key]);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Parse JSON string with prototype pollution protection.
|
|
31
|
+
* Returns the parsed value, or throws on invalid JSON or prototype pollution.
|
|
32
|
+
*/
|
|
33
|
+
export function safeJsonParse(raw) {
|
|
34
|
+
const parsed = JSON.parse(raw);
|
|
35
|
+
assertNoPrototypePollution(parsed);
|
|
36
|
+
return parsed;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Parse JSON string safely, returning null on any error instead of throwing.
|
|
40
|
+
*/
|
|
41
|
+
export function safeJsonParseOrNull(raw) {
|
|
42
|
+
try {
|
|
43
|
+
return safeJsonParse(raw);
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
logger.debug('safeJsonParse failed', { error: String(err) });
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt injection sanitization utilities for MAGI system.
|
|
3
|
+
*
|
|
4
|
+
* Detects and neutralizes 9 categories of prompt injection / control character
|
|
5
|
+
* abuse before embedding user-supplied content into LLM prompts.
|
|
6
|
+
*/
|
|
7
|
+
export interface SanitizeResult {
|
|
8
|
+
sanitized: string;
|
|
9
|
+
warnings: string[];
|
|
10
|
+
originalLength: number;
|
|
11
|
+
truncated: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare function sanitizeForPromptEmbedding(input: string): SanitizeResult;
|
|
14
|
+
export declare function quoteForEmbedding(input: string, label?: string): string;
|