@n2world/orchestrator 1.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.
Files changed (154) hide show
  1. package/dist/agent-os-rd.d.ts +100 -0
  2. package/dist/agent-os-rd.js +258 -0
  3. package/dist/audit-store.d.ts +14 -0
  4. package/dist/audit-store.js +107 -0
  5. package/dist/beta-runner.d.ts +95 -0
  6. package/dist/beta-runner.js +251 -0
  7. package/dist/beta.d.ts +102 -0
  8. package/dist/beta.js +180 -0
  9. package/dist/browser-agent.d.ts +90 -0
  10. package/dist/browser-agent.js +223 -0
  11. package/dist/channel-gateway.d.ts +74 -0
  12. package/dist/channel-gateway.js +270 -0
  13. package/dist/channels.d.ts +120 -0
  14. package/dist/channels.js +432 -0
  15. package/dist/chat-store.d.ts +29 -0
  16. package/dist/chat-store.js +120 -0
  17. package/dist/cli.d.ts +2 -0
  18. package/dist/cli.js +607 -0
  19. package/dist/command-screen.d.ts +12 -0
  20. package/dist/command-screen.js +44 -0
  21. package/dist/commit-gate.d.ts +98 -0
  22. package/dist/commit-gate.js +258 -0
  23. package/dist/companion-api.d.ts +37 -0
  24. package/dist/companion-api.js +101 -0
  25. package/dist/conversation-graph.d.ts +39 -0
  26. package/dist/conversation-graph.js +92 -0
  27. package/dist/cost-estimator.d.ts +27 -0
  28. package/dist/cost-estimator.js +42 -0
  29. package/dist/cron-runner.d.ts +31 -0
  30. package/dist/cron-runner.js +46 -0
  31. package/dist/dashboard/chat.html +326 -0
  32. package/dist/dashboard/dental.html +58 -0
  33. package/dist/dashboard/freebie.png +0 -0
  34. package/dist/dashboard/icon-192.png +0 -0
  35. package/dist/dashboard/index.html +892 -0
  36. package/dist/dashboard/manifest.json +15 -0
  37. package/dist/dashboard/service-worker.js +28 -0
  38. package/dist/dashboard-server.d.ts +37 -0
  39. package/dist/dashboard-server.js +457 -0
  40. package/dist/dental-intake-service.d.ts +37 -0
  41. package/dist/dental-intake-service.js +61 -0
  42. package/dist/dental-metrics.d.ts +25 -0
  43. package/dist/dental-metrics.js +37 -0
  44. package/dist/docking.d.ts +36 -0
  45. package/dist/docking.js +73 -0
  46. package/dist/finance-mcts-candidate.d.ts +37 -0
  47. package/dist/finance-mcts-candidate.js +106 -0
  48. package/dist/finance-regulation-kr.d.ts +33 -0
  49. package/dist/finance-regulation-kr.js +104 -0
  50. package/dist/finance-workflow.d.ts +135 -0
  51. package/dist/finance-workflow.js +242 -0
  52. package/dist/gateway.d.ts +18 -0
  53. package/dist/gateway.js +123 -0
  54. package/dist/governance.d.ts +39 -0
  55. package/dist/governance.js +48 -0
  56. package/dist/governed-executor.d.ts +31 -0
  57. package/dist/governed-executor.js +63 -0
  58. package/dist/governed-llm.d.ts +41 -0
  59. package/dist/governed-llm.js +83 -0
  60. package/dist/gpu-bridge.d.ts +16 -0
  61. package/dist/gpu-bridge.js +53 -0
  62. package/dist/health.d.ts +47 -0
  63. package/dist/health.js +66 -0
  64. package/dist/identity-link.d.ts +32 -0
  65. package/dist/identity-link.js +98 -0
  66. package/dist/index.d.ts +184 -0
  67. package/dist/index.js +417 -0
  68. package/dist/integrations/emr-adapter.d.ts +41 -0
  69. package/dist/integrations/emr-adapter.js +63 -0
  70. package/dist/kakao-oauth.d.ts +16 -0
  71. package/dist/kakao-oauth.js +87 -0
  72. package/dist/knowledge-graph.d.ts +53 -0
  73. package/dist/knowledge-graph.js +156 -0
  74. package/dist/llm.d.ts +65 -0
  75. package/dist/llm.js +357 -0
  76. package/dist/mcp-client-guard.d.ts +32 -0
  77. package/dist/mcp-client-guard.js +179 -0
  78. package/dist/mcp-macaroon.d.ts +75 -0
  79. package/dist/mcp-macaroon.js +161 -0
  80. package/dist/mcts-kernel-bridge.d.ts +36 -0
  81. package/dist/mcts-kernel-bridge.js +99 -0
  82. package/dist/mcts-prior.d.ts +79 -0
  83. package/dist/mcts-prior.js +170 -0
  84. package/dist/model-router.d.ts +51 -0
  85. package/dist/model-router.js +75 -0
  86. package/dist/multi-axis-lift.d.ts +43 -0
  87. package/dist/multi-axis-lift.js +141 -0
  88. package/dist/net-guard.d.ts +39 -0
  89. package/dist/net-guard.js +141 -0
  90. package/dist/onboarding.d.ts +38 -0
  91. package/dist/onboarding.js +94 -0
  92. package/dist/oracle-anchored-search.d.ts +25 -0
  93. package/dist/oracle-anchored-search.js +50 -0
  94. package/dist/oracle.d.ts +22 -0
  95. package/dist/oracle.js +116 -0
  96. package/dist/p6-governance.d.ts +150 -0
  97. package/dist/p6-governance.js +252 -0
  98. package/dist/pairing.d.ts +22 -0
  99. package/dist/pairing.js +81 -0
  100. package/dist/personalization.d.ts +35 -0
  101. package/dist/personalization.js +73 -0
  102. package/dist/pglite-hnsw-bridge.d.ts +118 -0
  103. package/dist/pglite-hnsw-bridge.js +311 -0
  104. package/dist/pglite-store.d.ts +59 -0
  105. package/dist/pglite-store.js +180 -0
  106. package/dist/playbook.d.ts +79 -0
  107. package/dist/playbook.js +83 -0
  108. package/dist/playbooks/dental-intake.d.ts +20 -0
  109. package/dist/playbooks/dental-intake.js +112 -0
  110. package/dist/predictive-agent.d.ts +157 -0
  111. package/dist/predictive-agent.js +535 -0
  112. package/dist/prompt-optimizer.d.ts +18 -0
  113. package/dist/prompt-optimizer.js +104 -0
  114. package/dist/rate-limiter.d.ts +25 -0
  115. package/dist/rate-limiter.js +75 -0
  116. package/dist/safety-anneal.d.ts +83 -0
  117. package/dist/safety-anneal.js +153 -0
  118. package/dist/sandbox-controller.d.ts +12 -0
  119. package/dist/sandbox-controller.js +95 -0
  120. package/dist/satisfaction-metrics.d.ts +26 -0
  121. package/dist/satisfaction-metrics.js +61 -0
  122. package/dist/sensor-bridge.d.ts +53 -0
  123. package/dist/sensor-bridge.js +133 -0
  124. package/dist/session-repair.d.ts +27 -0
  125. package/dist/session-repair.js +66 -0
  126. package/dist/slack-finance-intake.d.ts +42 -0
  127. package/dist/slack-finance-intake.js +122 -0
  128. package/dist/symbolic-dynamics.d.ts +113 -0
  129. package/dist/symbolic-dynamics.js +420 -0
  130. package/dist/telemetry.d.ts +19 -0
  131. package/dist/telemetry.js +68 -0
  132. package/dist/text-embedding.d.ts +6 -0
  133. package/dist/text-embedding.js +42 -0
  134. package/dist/tier-classifier.d.ts +20 -0
  135. package/dist/tier-classifier.js +58 -0
  136. package/dist/tier-guard.d.ts +36 -0
  137. package/dist/tier-guard.js +56 -0
  138. package/dist/tui.d.ts +9 -0
  139. package/dist/tui.js +214 -0
  140. package/dist/update-security.d.ts +31 -0
  141. package/dist/update-security.js +112 -0
  142. package/dist/v-calibration.d.ts +16 -0
  143. package/dist/v-calibration.js +42 -0
  144. package/dist/value-calibration.d.ts +41 -0
  145. package/dist/value-calibration.js +133 -0
  146. package/dist/value-head.d.ts +20 -0
  147. package/dist/value-head.js +91 -0
  148. package/dist/wal-buffer.d.ts +23 -0
  149. package/dist/wal-buffer.js +144 -0
  150. package/dist/wiki-synthesizer.d.ts +80 -0
  151. package/dist/wiki-synthesizer.js +0 -0
  152. package/dist/worker-agent.d.ts +10 -0
  153. package/dist/worker-agent.js +19 -0
  154. package/package.json +65 -0
package/dist/cli.js ADDED
@@ -0,0 +1,607 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ const dashboard_server_1 = require("./dashboard-server");
38
+ const index_1 = require("./index");
39
+ const beta_runner_1 = require("./beta-runner");
40
+ const llm_1 = require("./llm");
41
+ const governed_llm_1 = require("./governed-llm");
42
+ const chat_store_1 = require("./chat-store");
43
+ const tui_1 = require("./tui");
44
+ const channel_gateway_1 = require("./channel-gateway");
45
+ const browser_agent_1 = require("./browser-agent");
46
+ const llm_2 = require("./llm");
47
+ const kakao_oauth_1 = require("./kakao-oauth");
48
+ const channels_1 = require("./channels");
49
+ const http = __importStar(require("http"));
50
+ const readline = __importStar(require("readline"));
51
+ const fs = __importStar(require("fs/promises"));
52
+ const path = __importStar(require("path"));
53
+ const BETA_SNAPSHOT = path.join('beta-data', 'session.json');
54
+ const args = process.argv.slice(2);
55
+ const command = args[0];
56
+ async function main() {
57
+ if (!command || command === '--help' || command === '-h') {
58
+ console.log(`
59
+ n2world (AnyWorld) CLI - Open Source Version 1.0.0
60
+ Copyright (c) 2026 Park Sung Hoon (MIT License)
61
+
62
+ Usage:
63
+ n2world init Initialize a new project workspace
64
+ n2world dashboard [--port <port>] Start the mobile friendly PWA dashboard server
65
+ n2world chat [--fresh] [--export] Start an interactive conversation (real LLM + sandbox tools). 대화는 자동 저장·복원됨.
66
+ --fresh: 이전 대화 무시하고 새로 시작 · --export: 저장된 대화를 마크다운으로 내보내고 종료
67
+ n2world tui [--fresh] [--export] Rich TUI 대화(둥근 패널·컬러·CJK 줄바꿈). chat 과 같은 대화를 공유
68
+ n2world browse "task" [--show] 에이전트가 웹브라우저를 제어해 과제 수행(의미 DOM + Playwright). --show: 창 표시
69
+ n2world channels [status|start] 메시징 채널 게이트웨이(Telegram/Slack/Discord/KakaoTalk).
70
+ status: 채널별 활성·인바운드 방식 표시 · start: 폴링+웹훅 서버 기동
71
+ n2world channels kakao-login KakaoTalk '나에게 보내기' OAuth 토큰 발급(KAKAO_REST_API_KEY 필요)
72
+ n2world channels kakao-test "메시지" 발급된 KAKAO_ACCESS_TOKEN 으로 나에게 실제 전송 테스트
73
+ n2world run "task description" Run an agent task using MCTS search
74
+ n2world start Start the orchestrator & gateway daemon
75
+ n2world bench Show how to run reproducible benchmarks (no fake numbers)
76
+ n2world beta <sub> Run a closed beta (onboard/run/feedback/report). 접지 단계.
77
+
78
+ 베타 서브커맨드(클로즈드 베타 운영 — 실사용자 라벨 수집, 로컬에만 저장):
79
+ ⚠ PowerShell 주의: 아래는 인자 형식입니다. 값을 직접 입력하고 꺾쇠 < > 는 절대 치지 마세요
80
+ (PowerShell 에서 < > 는 리다이렉션 예약 연산자 → RedirectionNotSupported 에러).
81
+ 형식: n2world beta onboard USERID
82
+ n2world beta run-all USERID (held-out q1~q5 × off/on 일괄 실행 + 워크시트 — 권장)
83
+ n2world beta run USERID TASKID "자연어 과제" [--arm on|off]
84
+ n2world beta feedback USERID TASKID 만족(1-5) 정오(y|n) [--arm on|off]
85
+ n2world beta report
86
+ 예시: n2world beta onboard tester1
87
+ n2world beta run-all tester1
88
+ n2world beta feedback tester1 q1-on 5 y --arm on
89
+ (스냅샷: ${BETA_SNAPSHOT} — 헌법 제4조: 외부 전송 없음)
90
+ `);
91
+ return;
92
+ }
93
+ switch (command) {
94
+ case 'init': {
95
+ console.log('Initializing n2world workspace...');
96
+ await fs.mkdir('skills', { recursive: true });
97
+ const config = {
98
+ name: 'n2world-agent',
99
+ version: '1.0.0',
100
+ port: 49200,
101
+ skillsDir: './skills'
102
+ };
103
+ await fs.writeFile('n2world.config.json', JSON.stringify(config, null, 2), 'utf8');
104
+ console.log('Created skills/ directory and n2world.config.json configuration file successfully.');
105
+ break;
106
+ }
107
+ case 'dashboard': {
108
+ let port = 49200;
109
+ const portIdx = args.indexOf('--port');
110
+ if (portIdx !== -1 && args[portIdx + 1]) {
111
+ port = parseInt(args[portIdx + 1], 10);
112
+ }
113
+ console.log(`Starting n2world PWA Dashboard server on http://localhost:${port}...`);
114
+ const server = new dashboard_server_1.DashboardServer(port);
115
+ await server.start();
116
+ console.log('Dashboard is live. Press Ctrl+C to terminate.');
117
+ // Keep process alive indefinitely
118
+ await new Promise(() => { });
119
+ break;
120
+ }
121
+ case 'run': {
122
+ const task = args[1];
123
+ if (!task) {
124
+ console.error('Error: Please specify a task description. e.g. n2world run "Fix compiler errors"');
125
+ process.exit(1);
126
+ }
127
+ console.log(`Running task: "${task}"...`);
128
+ const sandbox = new index_1.LocalSandbox();
129
+ await sandbox.init();
130
+ console.log('Initializing local process isolation sandbox...');
131
+ const worker = new index_1.WorkerAgent(sandbox);
132
+ console.log('Starting MCTS search rollout...');
133
+ const res = await worker.executeTask('echo "Task completed inside sandbox."');
134
+ console.log('Sandbox execution output:', res.stdout.trim());
135
+ console.log('Task completed successfully!');
136
+ await sandbox.destroy();
137
+ break;
138
+ }
139
+ case 'start': {
140
+ console.log('Starting orchestrator gateway daemon...');
141
+ console.log('IPC gateway listening on port 49152.');
142
+ break;
143
+ }
144
+ case 'bench': {
145
+ // 정직(제1계명): 가짜 하드코딩 수치를 출력하지 않는다. 실측 벤치는 재현 하니스로 돌린다.
146
+ console.log('재현 가능한 벤치마크(가짜 수치 출력 금지 — 제1계명):');
147
+ console.log(' node benchmarks/run.js # MCTS·HNSW·게이팅 실측 → benchmarks/REPORT.md');
148
+ console.log(' npm run bench:pglite -w packages/orchestrator # P1/P1.5 영속·내구성');
149
+ console.log(' npm run bench:p2|p3|p3.5|p4|p4.7|p5|dod -w packages/orchestrator');
150
+ console.log('결과: benchmarks/results/*.json · 요약: benchmarks/REPORT.md');
151
+ break;
152
+ }
153
+ case 'chat': {
154
+ await runChat(args.slice(1));
155
+ break;
156
+ }
157
+ case 'tui': {
158
+ await (0, tui_1.runRichTui)(args.slice(1));
159
+ break;
160
+ }
161
+ case 'browse': {
162
+ await runBrowse(args.slice(1));
163
+ break;
164
+ }
165
+ case 'channels': {
166
+ await runChannels(args.slice(1));
167
+ break;
168
+ }
169
+ case 'beta': {
170
+ await runBetaCommand(args.slice(1));
171
+ break;
172
+ }
173
+ default: {
174
+ console.error(`Unknown command: "${command}". Run "n2world --help" for usage.`);
175
+ process.exit(1);
176
+ }
177
+ }
178
+ }
179
+ // held-out 과제 세트(검증 가능 정답) — beta/held-out-tasks.md 와 동일 소스. 운영 일괄 실행용.
180
+ const HELD_OUT = [
181
+ { id: 'q1', task: '1부터 100까지 정수의 합을 한 숫자로만 답해줘', answer: '5050' },
182
+ { id: 'q2', task: "단어 'racecar'가 회문(palindrome)인지 yes 또는 no로만 답해줘", answer: 'yes' },
183
+ { id: 'q3', task: '물(water)의 화학식을 한 단어로 답해줘', answer: 'H2O' },
184
+ { id: 'q4', task: '2의 10제곱(2^10)을 한 숫자로만 답해줘', answer: '1024' },
185
+ { id: 'q5', task: '대한민국의 수도를 한 단어로 답해줘', answer: '서울' },
186
+ ];
187
+ function getArm(args) {
188
+ const i = args.indexOf('--arm');
189
+ return i !== -1 && args[i + 1] === 'on' ? 'on' : i !== -1 && args[i + 1] === 'off' ? 'off' : 'off';
190
+ }
191
+ async function loadOrNewSession() {
192
+ try {
193
+ await fs.access(BETA_SNAPSHOT);
194
+ return beta_runner_1.BetaSession.load(BETA_SNAPSHOT);
195
+ }
196
+ catch {
197
+ return new beta_runner_1.BetaSession();
198
+ }
199
+ }
200
+ /**
201
+ * 클로즈드 베타 운영 — 비대화식 서브커맨드. 실사용자(운영자가 대리 입력)의 라벨을 수집해
202
+ * 로컬 스냅샷에 누적하고, report 로 B1 리프트를 산출한다. 라벨이 없으면 (목표; 미측정).
203
+ */
204
+ async function runBetaCommand(a) {
205
+ const sub = a[0];
206
+ const session = await loadOrNewSession();
207
+ if (sub === 'onboard') {
208
+ const userId = a[1];
209
+ if (!userId)
210
+ return void console.error('사용법: n2world beta onboard <userId>');
211
+ session.onboard(userId);
212
+ session.save(BETA_SNAPSHOT);
213
+ console.log(`[베타] 참가자 온보딩: ${userId} (동의·signup 기록). 온보딩된 N=${session.onboardedCount()}`);
214
+ return;
215
+ }
216
+ if (sub === 'run') {
217
+ const [, userId, taskId, task] = a;
218
+ if (!userId || !taskId || !task)
219
+ return void console.error('사용법: n2world beta run <userId> <taskId> "<task>" [--arm on|off]');
220
+ const arm = getArm(a);
221
+ const sandbox = new index_1.LocalSandbox();
222
+ await sandbox.init();
223
+ const worker = new index_1.WorkerAgent(sandbox);
224
+ console.log(`[베타] 과제 실행 (arm=${arm}) — 실 LLM 파이프라인...`);
225
+ const r = await session.runTask(userId, taskId, task, arm, async (cmd) => {
226
+ const res = await worker.executeTask(cmd);
227
+ return `[cmd: ${cmd}]\n${res.stdout}\n${res.stderr}`;
228
+ });
229
+ await sandbox.destroy();
230
+ session.save(BETA_SNAPSHOT);
231
+ console.log(`--- 출력 ---\n${r.output}\n------------`);
232
+ console.log(`[베타] oracle/LLM 호출 ${r.oracleCalls}회, ${r.timeMs}ms. 이제 사용자 라벨을 받으세요:`);
233
+ console.log(` n2world beta feedback ${userId} ${taskId} <만족1-5> <정오 y|n> --arm ${arm}`);
234
+ return;
235
+ }
236
+ if (sub === 'run-all') {
237
+ const userId = a[1];
238
+ if (!userId)
239
+ return void console.error('사용법: n2world beta run-all <userId> (held-out q1~q5 를 arm off/on 으로 일괄 실행 + 워크시트 발급)');
240
+ if (!(0, llm_1.activeLlmInfo)().provider)
241
+ return void console.error('[알림] LLM 키 미설정(.env). 실 LLM 파이프라인이 필요합니다.');
242
+ if (session.onboardedCount() === 0 || !session.taskLogFor(userId).length)
243
+ session.onboard(userId);
244
+ const sandbox = new index_1.LocalSandbox();
245
+ await sandbox.init();
246
+ const worker = new index_1.WorkerAgent(sandbox);
247
+ const shell = async (cmd) => { const res = await worker.executeTask(cmd); return `[cmd: ${cmd}]\n${res.stdout}\n${res.stderr}`; };
248
+ console.log(`[베타] ${userId} — held-out ${HELD_OUT.length}과제 × off/on = ${HELD_OUT.length * 2}회 실행(실 LLM)...`);
249
+ for (const t of HELD_OUT) {
250
+ for (const arm of ['off', 'on']) {
251
+ const r = await session.runTask(userId, `${t.id}-${arm}`, t.task, arm, shell);
252
+ session.save(BETA_SNAPSHOT);
253
+ const oneLine = r.output.replace(/\s+/g, ' ').slice(0, 60);
254
+ console.log(` ${t.id} [${arm}] → ${oneLine}`);
255
+ }
256
+ }
257
+ await sandbox.destroy();
258
+ const md = session.worksheet(userId);
259
+ await fs.mkdir('beta-data', { recursive: true });
260
+ const outPath = path.join('beta-data', `worksheet-${userId}.md`);
261
+ await fs.writeFile(outPath, md, 'utf8');
262
+ console.log(`\n[베타] 완료. 워크시트: ${outPath}`);
263
+ console.log(' → 이 파일을 실참가자에게 전달해 정오·만족을 직접 표시·기록하게 하세요(라벨 자체 생성 금지).');
264
+ console.log(' → 참고용 정답: ' + HELD_OUT.map((t) => `${t.id}=${t.answer}`).join(', '));
265
+ return;
266
+ }
267
+ if (sub === 'feedback') {
268
+ const [, userId, taskId, satStr, correctStr] = a;
269
+ if (!userId || !taskId || !satStr || !correctStr)
270
+ return void console.error('사용법: n2world beta feedback <userId> <taskId> <만족1-5> <정오 y|n> [--arm on|off]');
271
+ const arm = getArm(a);
272
+ const sat = parseInt(satStr, 10);
273
+ const correct = /^y(es)?$/i.test(correctStr) ? true : /^n(o)?$/i.test(correctStr) ? false : null;
274
+ session.recordFeedback(userId, taskId, sat, correct, arm);
275
+ session.save(BETA_SNAPSHOT);
276
+ console.log(`[베타] 라벨 기록(인간 입력): user=${userId} task=${taskId} 만족=${sat}/5 정오=${correct} arm=${arm}`);
277
+ return;
278
+ }
279
+ if (sub === 'worksheet') {
280
+ const userId = a[1];
281
+ if (!userId)
282
+ return void console.error('사용법: n2world beta worksheet USERID');
283
+ const md = session.worksheet(userId);
284
+ const outPath = path.join('beta-data', `worksheet-${userId}.md`);
285
+ await fs.mkdir('beta-data', { recursive: true });
286
+ await fs.writeFile(outPath, md, 'utf8');
287
+ console.log(`[베타] 라벨링 워크시트 작성: ${outPath}`);
288
+ console.log(' → 이 파일을 실참가자에게 전달해 정오·만족을 직접 표시·기록하게 하세요(라벨 자체 생성 금지).');
289
+ return;
290
+ }
291
+ if (sub === 'report') {
292
+ const r = session.liftReport();
293
+ console.log(`[베타] 온보딩 N=${r.onboarded}`);
294
+ if (r.status === '(목표; 미측정)') {
295
+ console.log(`[B1 MultiAxisLift] (목표; 미측정)`);
296
+ console.log(` 사유: ${r.reason}`);
297
+ }
298
+ else {
299
+ console.log('[B1 MultiAxisLift] 실데이터 측정:');
300
+ console.log(JSON.stringify(r.report, null, 2));
301
+ }
302
+ return;
303
+ }
304
+ console.error('알 수 없는 beta 서브커맨드. n2world --help 참고(onboard|run-all|run|worksheet|feedback|report).');
305
+ }
306
+ /**
307
+ * 대화형 세션 — 사용자가 n2world(에이전트 'Ethan')와 연속 대화한다. 실 LLM + 샌드박스 도구.
308
+ * 대화는 매 턴 디스크에 영속되어(ChatStore) **다운돼도 사라지지 않고**, 다음 실행 때 자동 복원된다.
309
+ * 플래그: --fresh(이전 대화 무시·초기화), --export(저장된 대화를 마크다운으로 출력 후 종료).
310
+ * 종료: exit/quit/bye 입력 또는 Ctrl+D(EOF)/Ctrl+C.
311
+ */
312
+ async function runChat(flags) {
313
+ const store = new chat_store_1.ChatStore();
314
+ store.load();
315
+ // --export: 저장된 대화를 마크다운으로 내보내고 종료(LLM/샌드박스 불필요).
316
+ if (flags.includes('--export')) {
317
+ if (store.count() === 0) {
318
+ console.log(`저장된 대화가 없습니다(${store.path_()}).`);
319
+ return;
320
+ }
321
+ const outPath = path.join('chat-data', `export-${Date.now()}.md`);
322
+ await fs.mkdir('chat-data', { recursive: true });
323
+ await fs.writeFile(outPath, store.exportMarkdown(), 'utf8');
324
+ console.log(`대화 ${store.count()}턴을 내보냈습니다: ${outPath}`);
325
+ return;
326
+ }
327
+ const info = (0, llm_1.activeLlmInfo)();
328
+ if (!info.provider) {
329
+ console.error('[알림] LLM API 키가 없습니다. 루트 .env 에 GEMINI_API_KEY 또는 ANTHROPIC_API_KEY 를 설정하세요(.env.example 참고).');
330
+ process.exit(1);
331
+ }
332
+ if (flags.includes('--fresh')) {
333
+ store.clear();
334
+ console.log('[새 대화] 이전 대화를 초기화했습니다.');
335
+ }
336
+ const sandbox = new index_1.LocalSandbox();
337
+ await sandbox.init();
338
+ const worker = new index_1.WorkerAgent(sandbox);
339
+ const runShell = async (cmd) => {
340
+ const res = await worker.executeTask(cmd);
341
+ return `[cmd: ${cmd}]\n${res.stdout}\n${res.stderr}`;
342
+ };
343
+ // H2 — Tier 거버넌스 *기본 ON*(보안 하드닝). 인입을 분류→가드→로컬/외부로 강제 라우팅
344
+ // (민감=Tier-0 로컬 강제, 외부 송신 차단). 로컬 운영자 본인 세션이므로 비민감은 consent=true.
345
+ // 명시적 비활성화만 우회: N2WORLD_TIER_GUARD=0|false|off (운영자 책임).
346
+ const guardOff = /^(0|false|off)$/i.test(process.env.N2WORLD_TIER_GUARD || '');
347
+ const gov = guardOff ? null : new governed_llm_1.GovernedAgent((0, governed_llm_1.defaultGovernedBackends)(runShell), { baselineTier: 1 });
348
+ console.log(`n2world 대화형 세션 — 제공자: ${info.provider}/${info.model}`);
349
+ if (gov)
350
+ console.log(`🛡 Tier 거버넌스 ON(기본) — 민감(Tier-0) 입력은 로컬 모델로만 처리(외부 차단). 끄기: N2WORLD_TIER_GUARD=0`);
351
+ console.log(`대화는 자동 저장됩니다: ${store.path_()} (다운돼도 보존)`);
352
+ if (store.count() > 0)
353
+ console.log(`↩ 이전 대화 ${store.count()}턴을 이어갑니다('--fresh' 로 새로 시작).`);
354
+ console.log("무엇이든 한국어로 물어보세요. 종료하려면 'exit' 또는 Ctrl+C.\n");
355
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: 'you> ' });
356
+ rl.prompt();
357
+ for await (const lineRaw of rl) {
358
+ const line = lineRaw.trim();
359
+ if (line === '') {
360
+ rl.prompt();
361
+ continue;
362
+ }
363
+ if (/^(exit|quit|bye)$/i.test(line))
364
+ break;
365
+ store.append({ role: 'user', content: line }); // 입력 즉시 영속(응답 전 다운돼도 질문 보존)
366
+ process.stdout.write('Ethan> (생각 중...)\n');
367
+ let reply;
368
+ if (gov) {
369
+ const r = await gov.respond(store.history(), { consent: true });
370
+ reply = `${r.text}${r.provider !== 'external' ? ` 〔${r.provider}·Tier-${r.tier}〕` : ''}`;
371
+ }
372
+ else {
373
+ reply = await (0, llm_1.askAgentChat)(store.history(), runShell);
374
+ }
375
+ store.append({ role: 'assistant', content: reply }); // 응답도 즉시 영속
376
+ console.log(`Ethan> ${reply}\n`);
377
+ rl.prompt();
378
+ }
379
+ rl.close();
380
+ await sandbox.destroy();
381
+ console.log(`대화를 종료합니다. (${store.count()}턴 저장됨: ${store.path_()})`);
382
+ }
383
+ /**
384
+ * 메시징 채널 게이트웨이. `status`(기본)는 채널별 활성/인바운드 방식을 정직하게 표시하고,
385
+ * `start`는 Telegram 폴링 + Slack/Discord/Kakao 웹훅 HTTP 서버를 기동한다.
386
+ */
387
+ async function runChannels(a) {
388
+ const sub = a[0] || 'status';
389
+ // KakaoTalk OAuth 토큰 발급('나에게 보내기').
390
+ if (sub === 'kakao-login') {
391
+ await runKakaoLogin();
392
+ return;
393
+ }
394
+ // 발급된 토큰으로 실제 전송 테스트(미측정→실측 전환).
395
+ if (sub === 'kakao-test') {
396
+ const msg = a[1] || 'n2world KakaoTalk 연동 테스트 메시지입니다.';
397
+ const ch = new channels_1.KakaoTalkChannel();
398
+ if (!ch.isConfigured()) {
399
+ console.error('KAKAO_ACCESS_TOKEN 미설정. 먼저 `channels kakao-login` 으로 토큰을 발급하세요.');
400
+ process.exit(1);
401
+ }
402
+ const r = await ch.send('self', msg);
403
+ console.log(r.ok ? '✅ 카카오톡 나에게 보내기 성공(실측).' : `❌ 실패: ${r.reason}`);
404
+ if (!r.ok)
405
+ console.log(' 응답:', JSON.stringify(r.raw));
406
+ return;
407
+ }
408
+ const gw = new channel_gateway_1.ChannelGateway();
409
+ const st = gw.status();
410
+ console.log('메시징 채널 게이트웨이 상태:');
411
+ for (const s of st) {
412
+ const mark = s.configured ? '✅ 활성' : '⛔ 미설정';
413
+ const env = s.name === 'telegram' ? 'TELEGRAM_BOT_TOKEN'
414
+ : s.name === 'slack' ? 'SLACK_BOT_TOKEN'
415
+ : s.name === 'discord' ? 'DISCORD_BOT_TOKEN'
416
+ : 'KAKAO_ACCESS_TOKEN';
417
+ console.log(` - ${s.name.padEnd(9)} ${mark} (인바운드: ${s.inbound}${s.configured ? '' : `, ${env} 필요`})`);
418
+ }
419
+ console.log(' 정직 고지: 미설정 채널은 동작하지 않습니다. 실제 송수신은 토큰이 있어야 검증됩니다(그 전엔 미측정).');
420
+ console.log(' 인바운드: telegram=폴링 · discord=Gateway WS · slack=Socket Mode(SLACK_APP_TOKEN 필요) — 모두 공개서버 불요 · kakao=오픈빌더 웹훅(공개 URL 필요)');
421
+ if (sub !== 'start')
422
+ return;
423
+ const configured = gw.configuredChannels();
424
+ if (configured.length === 0) {
425
+ console.error('\n기동 중단: 활성화된 채널이 없습니다. .env 에 해당 토큰을 설정한 뒤 다시 실행하세요.');
426
+ process.exit(1);
427
+ }
428
+ // 모든 설정 채널 인바운드 시작(Telegram 폴링 · Discord Gateway WS · Slack Socket Mode).
429
+ const stopPoll = await gw.startInbound();
430
+ const started = gw.startedInbound();
431
+ console.log('\n인바운드 시작된 채널: ' + (started.length ? started.join(', ') : '(없음)'));
432
+ if (configured.includes('discord') && !started.includes('discord'))
433
+ console.log(' ⚠ discord: 토큰은 있으나 WS 연결 실패(인텐트/토큰 확인).');
434
+ if (configured.includes('slack') && !started.includes('slack'))
435
+ console.log(' ⚠ slack: Socket Mode 에 SLACK_APP_TOKEN(xapp-, connections:write) 이 필요합니다.');
436
+ // Slack/Discord/Kakao 웹훅 HTTP 서버(C1 서명검증 · C2 localhost 바인딩 · M1 본문상한).
437
+ let port = 49260;
438
+ const pi = a.indexOf('--port');
439
+ if (pi !== -1 && a[pi + 1]) {
440
+ const p = parseInt(a[pi + 1], 10);
441
+ if (Number.isInteger(p) && p > 0 && p < 65536)
442
+ port = p;
443
+ }
444
+ // C2: 기본 127.0.0.1(외부 비노출). 공개하려면 --webhook-host 0.0.0.0 (경고).
445
+ let host = '127.0.0.1';
446
+ const hi = a.indexOf('--webhook-host');
447
+ if (hi !== -1 && a[hi + 1])
448
+ host = a[hi + 1];
449
+ const MAX_BODY = 1_000_000; // M1: 1MB 상한
450
+ const verifyWebhook = (channel, raw, headers) => {
451
+ if (channel === 'slack') {
452
+ const secret = (process.env.SLACK_SIGNING_SECRET || '').trim();
453
+ if (!secret)
454
+ return { ok: false, status: 503, msg: 'SLACK_SIGNING_SECRET 미설정 — 서명 검증 불가로 거부' };
455
+ return { ok: channels_1.SlackChannel.verifySignature(raw, headers['x-slack-signature'], headers['x-slack-request-timestamp'], secret) };
456
+ }
457
+ if (channel === 'discord') {
458
+ const pub = (process.env.DISCORD_PUBLIC_KEY || '').trim();
459
+ if (!pub)
460
+ return { ok: false, status: 503, msg: 'DISCORD_PUBLIC_KEY 미설정 — 서명 검증 불가로 거부' };
461
+ return { ok: channels_1.DiscordChannel.verifySignature(raw, headers['x-signature-ed25519'], headers['x-signature-timestamp'], pub) };
462
+ }
463
+ if (channel === 'kakao') {
464
+ const secret = (process.env.KAKAO_WEBHOOK_SECRET || '').trim();
465
+ if (!secret)
466
+ return { ok: false, status: 503, msg: 'KAKAO_WEBHOOK_SECRET 미설정 — 공유 비밀 헤더 검증 불가로 거부' };
467
+ return { ok: channels_1.KakaoTalkChannel.verifyWebhook(headers['x-n2world-secret'], secret) };
468
+ }
469
+ return { ok: false, status: 400, msg: 'unknown channel' };
470
+ };
471
+ const server = http.createServer((req, res) => {
472
+ const m = (req.url || '').match(/^\/webhook\/(slack|discord|kakao)\b/);
473
+ if (req.method === 'POST' && m) {
474
+ let body = '';
475
+ let tooLarge = false;
476
+ req.on('data', (c) => {
477
+ body += c.toString();
478
+ if (body.length > MAX_BODY) {
479
+ tooLarge = true;
480
+ req.destroy();
481
+ } // M1
482
+ });
483
+ req.on('end', async () => {
484
+ if (tooLarge) {
485
+ res.writeHead(413).end('payload too large');
486
+ return;
487
+ }
488
+ // C1: 라우팅 전에 서명 검증(원문 기준).
489
+ const v = verifyWebhook(m[1], body, req.headers);
490
+ if (!v.ok) {
491
+ res.writeHead(v.status ?? 401, { 'Content-Type': 'application/json; charset=utf-8' });
492
+ res.end(JSON.stringify({ error: v.msg ?? 'signature verification failed' }));
493
+ return;
494
+ }
495
+ let payload = {};
496
+ try {
497
+ payload = JSON.parse(body || '{}');
498
+ }
499
+ catch { /* 빈/비JSON */ }
500
+ try {
501
+ const r = await gw.webhook(m[1], payload);
502
+ res.writeHead(r.status, { 'Content-Type': 'application/json; charset=utf-8' });
503
+ res.end(JSON.stringify(r.body));
504
+ }
505
+ catch (e) {
506
+ res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
507
+ res.end(JSON.stringify({ error: e?.message ?? String(e) }));
508
+ }
509
+ });
510
+ return;
511
+ }
512
+ res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
513
+ res.end('Not found. 웹훅 경로: POST /webhook/{slack|discord|kakao}');
514
+ });
515
+ server.listen(port, host, () => {
516
+ console.log(`[webhook] http://${host}:${port}/webhook/{slack|discord|kakao} 수신 대기(서명 검증 필수).`);
517
+ if (host !== '127.0.0.1' && host !== 'localhost') {
518
+ console.log(' ⚠ 외부 인터페이스에 바인딩됨 — 서명 검증 비밀(SLACK_SIGNING_SECRET/DISCORD_PUBLIC_KEY/KAKAO_WEBHOOK_SECRET)이 설정돼야 안전합니다.');
519
+ }
520
+ else {
521
+ console.log(' 로컬 전용 바인딩. 외부 공개가 필요하면 --webhook-host 0.0.0.0 + 서명 비밀 설정 + ngrok 등.');
522
+ }
523
+ console.log('종료: Ctrl+C');
524
+ });
525
+ process.on('SIGINT', () => { stopPoll(); server.close(); process.exit(0); });
526
+ await new Promise(() => { }); // 데몬 유지
527
+ }
528
+ /**
529
+ * KakaoTalk '나에게 보내기' OAuth 토큰 발급 흐름(Authorization Code).
530
+ * 필요: KAKAO_REST_API_KEY(Kakao Developers 앱의 REST 키). redirect_uri 는 앱에 등록돼야 한다.
531
+ */
532
+ async function runKakaoLogin() {
533
+ const restKey = process.env.KAKAO_REST_API_KEY || '';
534
+ const redirectUri = process.env.KAKAO_REDIRECT_URI || 'https://localhost';
535
+ if (!restKey.trim()) {
536
+ console.error('KAKAO_REST_API_KEY 미설정. https://developers.kakao.com 에서 앱을 만들고 REST API 키를 .env 에 넣으세요.');
537
+ console.error("또한 카카오 로그인 활성화 + 동의항목 '카카오톡 메시지 전송(talk_message)' + redirect URI 등록이 필요합니다.");
538
+ process.exit(1);
539
+ }
540
+ const url = (0, kakao_oauth_1.authorizeUrl)(restKey, redirectUri);
541
+ console.log('1) 아래 URL 을 브라우저에서 열어 동의하세요(redirect_uri 는 앱에 등록된 값이어야 함):\n');
542
+ console.log(' ' + url + '\n');
543
+ console.log(`2) 동의 후 ${redirectUri}?code=... 로 이동합니다. 그 주소(또는 code 값)를 붙여넣으세요.`);
544
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
545
+ const answer = await new Promise((resolve) => rl.question('redirect URL 또는 code ▸ ', resolve));
546
+ rl.close();
547
+ const code = (0, kakao_oauth_1.extractCode)(answer);
548
+ if (!code) {
549
+ console.error('code 를 찾지 못했습니다. redirect 주소 전체 또는 code 값을 정확히 붙여넣으세요.');
550
+ process.exit(1);
551
+ }
552
+ let token;
553
+ try {
554
+ token = await (0, kakao_oauth_1.exchangeCode)(restKey, redirectUri, code);
555
+ }
556
+ catch (e) {
557
+ console.error('토큰 발급 실패:', e?.message ?? e);
558
+ process.exit(1);
559
+ }
560
+ console.log('\n✅ 토큰 발급 성공. 아래를 .env 에 설정하세요(따옴표 없이):\n');
561
+ console.log(`KAKAO_ACCESS_TOKEN=${token.accessToken}`);
562
+ if (token.refreshToken)
563
+ console.log(`KAKAO_REFRESH_TOKEN=${token.refreshToken} # 만료 시 갱신용(약 60일)`);
564
+ console.log(`\n(access token 은 약 ${token.expiresIn ?? 21600}초 후 만료됩니다. 만료 시 refresh 로 재발급.)`);
565
+ console.log("설정 후: node packages/orchestrator/dist/cli.js channels kakao-test \"안녕 카카오\"");
566
+ }
567
+ /**
568
+ * 에이전트가 웹브라우저를 제어해 과제를 수행한다(의미 DOM 지각 + Playwright 구동 + 보안 스크린).
569
+ * Playwright 미설치 시 정직하게 안내(설치: npm i -D playwright && npx playwright install chromium).
570
+ */
571
+ async function runBrowse(a) {
572
+ const task = a.find((x) => !x.startsWith('--'));
573
+ if (!task)
574
+ return void console.error('사용법: n2world browse "구글에서 n2world 검색하고 첫 결과 제목 알려줘" [--show]');
575
+ if (!(0, llm_1.activeLlmInfo)().provider) {
576
+ console.error('[알림] LLM API 키가 없습니다(.env 의 GEMINI_API_KEY/ANTHROPIC_API_KEY).');
577
+ process.exit(1);
578
+ }
579
+ const headless = !a.includes('--show');
580
+ const ctrl = new browser_agent_1.PlaywrightBrowserController(headless);
581
+ try {
582
+ await ctrl.init();
583
+ }
584
+ catch (e) {
585
+ console.error('브라우저 시작 실패:', e?.message ?? e);
586
+ process.exit(1);
587
+ }
588
+ // 비전 폴백(#2): DOM 으로 안 잡히는 캔버스 UI 에 한해 멀티모달 좌표 그라운딩.
589
+ const browser = new browser_agent_1.BrowserAgent(ctrl, (b64, desc) => (0, llm_2.locateOnScreen)(b64, desc));
590
+ // 샌드박스 쉘도 제공(과제가 파일 작업 등을 섞을 수 있음).
591
+ const sandbox = new index_1.LocalSandbox();
592
+ await sandbox.init();
593
+ const worker = new index_1.WorkerAgent(sandbox);
594
+ const runShell = async (cmd) => {
595
+ const res = await worker.executeTask(cmd);
596
+ return `[cmd: ${cmd}]\n${res.stdout}\n${res.stderr}`;
597
+ };
598
+ console.log(`[browse] 과제: ${task}\n[browse] 브라우저 제어 중(결제·구매·삭제·로그인 제출은 차단)…`);
599
+ const reply = await (0, llm_1.askAgentChat)([{ role: 'user', content: task }], runShell, { browser: (action) => browser.act(action) });
600
+ console.log(`\n--- 결과 ---\n${reply}`);
601
+ await browser.close();
602
+ await sandbox.destroy();
603
+ }
604
+ main().catch((err) => {
605
+ console.error('Fatal CLI Error:', err);
606
+ process.exit(1);
607
+ });
@@ -0,0 +1,12 @@
1
+ export interface DangerRule {
2
+ name: string;
3
+ re: RegExp;
4
+ }
5
+ /** 알려진 파괴적/되돌릴 수 없는 명령 패턴(POSIX + Windows). */
6
+ export declare const DANGER_RULES: DangerRule[];
7
+ export interface ScreenResult {
8
+ flagged: boolean;
9
+ matched: string[];
10
+ }
11
+ /** 명령을 위험 규칙으로 스크린한다. flagged=true 면 차단 권고. */
12
+ export declare function screenDangerousCommand(command: string): ScreenResult;
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ // ============================================================================
3
+ // 위험명령 스크린 — 단일 출처(H1: 전 실행 경로 통일)
4
+ // ----------------------------------------------------------------------------
5
+ // 보안 점검 보고서 v1.0 H1 대응. 과거엔 channel-gateway 의 defaultAgent 에서만 스크린해
6
+ // 대시보드 /api/run·CLI 직접 실행이 무방비였다. 이제 *샌드박스 실행 직전*(LocalSandbox.
7
+ // runCommand)에서 본 규칙으로 일괄 차단 → 모든 경로가 동일 통제를 받는다.
8
+ //
9
+ // 정직 고지(제1계명): 규칙은 *알려진* 되돌릴 수 없는/파괴적 패턴만 잡는다(denylist).
10
+ // 의미적 위반·OOD·난독화 우회는 못 잡으므로 *충분조건이 아니다*. 격리(샌드박스)·승인·
11
+ // allowlist 와 함께 다층으로 써야 한다. (oracle 의 모델-비평이 의미층을 보완.)
12
+ // ============================================================================
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.DANGER_RULES = void 0;
15
+ exports.screenDangerousCommand = screenDangerousCommand;
16
+ /** 알려진 파괴적/되돌릴 수 없는 명령 패턴(POSIX + Windows). */
17
+ exports.DANGER_RULES = [
18
+ // ── POSIX ──
19
+ { name: 'rm-rf-root', re: /\brm\s+(-\S*[rf]\S*\s+)+(\/|~|\$HOME|\/\*)(\s|;|$)/i },
20
+ { name: 'disk-overwrite', re: /\b(dd|mkfs(\.\w+)?)\b[^\n]*\bof=\/dev\/|>\s*\/dev\/(sd|nvme|disk)/i },
21
+ { name: 'fork-bomb', re: /:\s*\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:/ },
22
+ { name: 'pipe-to-shell', re: /\b(curl|wget)\b[^\n|]*\|\s*(sudo\s+)?(ba)?sh\b/i },
23
+ { name: 'chmod-777-system', re: /\bchmod\s+(-[a-zA-Z]*\s+)*0?777\b[^\n]*\s(\/(etc|bin|usr|boot|sys)\b|\/)/i },
24
+ { name: 'disable-firewall', re: /\b(ufw\s+disable|iptables\s+-F|setenforce\s+0|systemctl\s+stop\s+firewalld)\b/i },
25
+ { name: 'destructive-git', re: /\bgit\s+push\b[^\n]*--force\b[^\n]*\b(main|master|origin)\b|\bgit\s+reset\s+--hard\b/i },
26
+ { name: 'mass-kill', re: /\bkill(all)?\s+-9\s+(-1|\*)\b|\bpkill\s+-9\s+\./i },
27
+ { name: 'shutdown-reboot', re: /\b(shutdown|reboot|halt|poweroff)\b/i },
28
+ { name: 'secret-exfil', re: /\b(curl|wget|Invoke-WebRequest|iwr|curl\.exe)\b[^\n]*\b(AKIA|secret|token|password|api[_-]?key|\.env|id_rsa)\b/i },
29
+ // ── Windows (cmd / PowerShell) ──
30
+ { name: 'win-del-recursive', re: /\bdel\b[^\n]*\s\/[a-z]*s[a-z]*\b[^\n]*\s\/[a-z]*q|\brd\b\s+\/s\b|\brmdir\b\s+\/s\b/i },
31
+ { name: 'win-format', re: /\bformat\b\s+[a-z]:|\bformat\.com\b/i },
32
+ { name: 'win-remove-item-root', re: /\bRemove-Item\b[^\n]*-Recurse\b[^\n]*\b([a-z]:\\?\s*$|[a-z]:\\\*|\\\\)/i },
33
+ { name: 'win-disable-defender', re: /\bSet-MpPreference\b[^\n]*-DisableRealtimeMonitoring\s+\$?true\b/i },
34
+ { name: 'win-bcdedit', re: /\bbcdedit\b[^\n]*\/(delete|deletevalue|set)\b|\bvssadmin\b[^\n]*delete\s+shadows/i },
35
+ ];
36
+ /** 명령을 위험 규칙으로 스크린한다. flagged=true 면 차단 권고. */
37
+ function screenDangerousCommand(command) {
38
+ const matched = [];
39
+ for (const { name, re } of exports.DANGER_RULES) {
40
+ if (re.test(command))
41
+ matched.push(name);
42
+ }
43
+ return { flagged: matched.length > 0, matched };
44
+ }