@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
@@ -0,0 +1,223 @@
1
+ "use strict";
2
+ // ============================================================================
3
+ // BrowserAgent — n2world 웹브라우저 제어 (의미 DOM 지각 #3 + 자동화 구동 #1)
4
+ // ----------------------------------------------------------------------------
5
+ // 설계(분석 결론): 지각=상호작용 요소의 의미 추출(접근성 역할+이름), 구동=Playwright(로컬).
6
+ // 비전(#2)은 폴백, 클라우드(#4)는 기본 배제(헌법 제4조 로컬-only).
7
+ //
8
+ // 비용/CI 안전: Playwright 는 **동적 import(선택 설치)**. 미설치면 정직하게 안내한다
9
+ // (mock 으로 성공 연기 금지 — 제1계명). 순수 로직(요소 포맷·보안 스크린)은 브라우저 없이 검증.
10
+ // 보안: 모든 액션은 screenBrowserAction 을 통과한다. 되돌릴 수 없는/금융/인증 행동은
11
+ // deny-by-default (결제·구매·삭제·제출·OAuth 동의 차단, 비밀번호/카드 입력 차단).
12
+ // ============================================================================
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.BrowserAgent = exports.PlaywrightBrowserController = void 0;
15
+ exports.formatElements = formatElements;
16
+ exports.screenBrowserAction = screenBrowserAction;
17
+ /** 요소 목록을 LLM 친화 라인으로 압축(토큰 효율). 테스트용 export. */
18
+ function formatElements(els, max = 60) {
19
+ if (!els.length)
20
+ return '(상호작용 가능한 요소 없음)';
21
+ const lines = els.slice(0, max).map((e) => `[${e.ref}] ${e.role} "${(e.name || '').replace(/\s+/g, ' ').slice(0, 80)}"`);
22
+ if (els.length > max)
23
+ lines.push(`… 외 ${els.length - max}개`);
24
+ return lines.join('\n');
25
+ }
26
+ // 되돌릴 수 없는/금융/인증 행동 키워드(클릭 차단). 안전 규칙과 정렬.
27
+ const IRREVERSIBLE_RE = /\b(buy|purchase|pay|payment|checkout|order|place\s*order|subscribe|delete|remove|confirm|submit|authorize|consent|allow|grant|sign\s*up|withdraw|transfer)\b|결제|구매|주문|결제하기|삭제|제거|확인|동의|허용|승인|가입|이체|송금|출금|탈퇴/i;
28
+ // 비밀/금융 입력 필드(타이핑 차단 — 자격증명·결제정보 입력 금지).
29
+ const SECRET_FIELD_RE = /\b(password|passwd|pwd|card|cardnumber|cvv|cvc|ssn|secret|otp|pin|account\s*number)\b|비밀번호|카드(번호)?|주민(등록)?번호|보안코드|계좌번호|인증번호/i;
30
+ /**
31
+ * 브라우저 액션 보안 스크린(deny-by-default for 위험).
32
+ * read/back/extract 는 허용, goto 는 http(s)만, click 은 금융/인증/되돌릴수없는 라벨이면 차단,
33
+ * type 은 비밀/결제 필드면 차단(자격증명 입력 금지).
34
+ */
35
+ function screenBrowserAction(action, targetName = '') {
36
+ switch (action.verb) {
37
+ case 'read':
38
+ case 'back':
39
+ case 'extract':
40
+ return { allowed: true, reason: 'ok' };
41
+ case 'goto': {
42
+ const u = (action.url || '').trim();
43
+ if (!/^https?:\/\//i.test(u))
44
+ return { allowed: false, reason: 'goto 는 http(s) URL 만 허용(javascript:/file:/data: 차단)' };
45
+ return { allowed: true, reason: 'ok' };
46
+ }
47
+ case 'click':
48
+ if (IRREVERSIBLE_RE.test(targetName)) {
49
+ return { allowed: false, reason: `되돌릴 수 없는/금융/인증 행동 차단(대상: "${targetName}"). 사용자가 직접 수행해야 함.` };
50
+ }
51
+ return { allowed: true, reason: 'ok' };
52
+ case 'vclick': {
53
+ // 비전 폴백 클릭: 설명(query)을 금융/인증 키워드로 스크린(좌표 클릭도 동일 보안).
54
+ const desc = action.query || targetName;
55
+ if (IRREVERSIBLE_RE.test(desc)) {
56
+ return { allowed: false, reason: `되돌릴 수 없는/금융/인증 행동 차단(설명: "${desc}"). 사용자가 직접 수행해야 함.` };
57
+ }
58
+ return { allowed: true, reason: 'ok' };
59
+ }
60
+ case 'type':
61
+ if (SECRET_FIELD_RE.test(targetName)) {
62
+ return { allowed: false, reason: `비밀번호/결제정보 입력 차단(필드: "${targetName}"). 자격증명은 에이전트가 입력하지 않음.` };
63
+ }
64
+ return { allowed: true, reason: 'ok' };
65
+ default:
66
+ return { allowed: false, reason: `알 수 없는 액션: ${action.verb}` };
67
+ }
68
+ }
69
+ /**
70
+ * Playwright 기반 로컬 컨트롤러(#1 구동). Playwright 는 동적 import(선택 설치).
71
+ * 미설치 시 init() 이 명확히 throw → 상위에서 정직한 안내로 변환.
72
+ */
73
+ class PlaywrightBrowserController {
74
+ headless;
75
+ browser = null;
76
+ page = null;
77
+ constructor(headless = true) {
78
+ this.headless = headless;
79
+ }
80
+ async init() {
81
+ let pw;
82
+ try {
83
+ const spec = 'playwright';
84
+ pw = await import(spec);
85
+ }
86
+ catch {
87
+ throw new Error("Playwright 미설치. 활성화: `npm i -D playwright && npx playwright install chromium` (로컬 전용).");
88
+ }
89
+ this.browser = await pw.chromium.launch({ headless: this.headless });
90
+ this.page = await this.browser.newPage();
91
+ }
92
+ async goto(url) {
93
+ await this.page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
94
+ }
95
+ async readElements() {
96
+ // 페이지 내에서 상호작용 요소를 스캔하고 data-n2w-ref 를 부여(이후 click/type 의 안정적 셀렉터).
97
+ return await this.page.evaluate(() => {
98
+ const sel = 'a,button,input,textarea,select,[role=button],[role=link],[role=textbox],[onclick],[contenteditable=true]';
99
+ const nodes = Array.from(document.querySelectorAll(sel));
100
+ const out = [];
101
+ let i = 0;
102
+ for (const el of nodes) {
103
+ const style = window.getComputedStyle(el);
104
+ if (style.display === 'none' || style.visibility === 'hidden')
105
+ continue;
106
+ const rect = el.getBoundingClientRect();
107
+ if (rect.width === 0 && rect.height === 0)
108
+ continue;
109
+ const ref = 'e' + ++i;
110
+ el.setAttribute('data-n2w-ref', ref);
111
+ const role = el.getAttribute('role') || el.tagName.toLowerCase();
112
+ const name = el.getAttribute('aria-label') ||
113
+ el.placeholder ||
114
+ el.value ||
115
+ (el.innerText || el.textContent || '').trim() ||
116
+ el.getAttribute('name') ||
117
+ '';
118
+ out.push({ ref, role, name: String(name).slice(0, 120) });
119
+ if (i >= 200)
120
+ break;
121
+ }
122
+ return out;
123
+ });
124
+ }
125
+ async click(ref) {
126
+ await this.page.click(`[data-n2w-ref="${ref}"]`, { timeout: 10000 });
127
+ }
128
+ async type(ref, text) {
129
+ await this.page.fill(`[data-n2w-ref="${ref}"]`, text, { timeout: 10000 });
130
+ }
131
+ async extractText() {
132
+ return (await this.page.evaluate(() => document.body?.innerText || '')).slice(0, 8000);
133
+ }
134
+ async back() {
135
+ await this.page.goBack({ waitUntil: 'domcontentloaded' });
136
+ }
137
+ async screenshot() {
138
+ const buf = await this.page.screenshot({ type: 'png' });
139
+ const vp = this.page.viewportSize() || { width: 1280, height: 720 };
140
+ return { base64: buf.toString('base64'), width: vp.width, height: vp.height };
141
+ }
142
+ async clickAt(x, y) {
143
+ await this.page.mouse.click(x, y);
144
+ }
145
+ async close() {
146
+ try {
147
+ await this.browser?.close();
148
+ }
149
+ catch { /* noop */ }
150
+ }
151
+ }
152
+ exports.PlaywrightBrowserController = PlaywrightBrowserController;
153
+ /**
154
+ * BrowserAgent — 지각(read)+구동(click/type/goto)을 보안 스크린으로 감싼 실행기.
155
+ * act() 가 LLM 도구(controlBrowser)의 백엔드가 된다. 모든 액션은 screenBrowserAction 통과.
156
+ */
157
+ class BrowserAgent {
158
+ ctrl;
159
+ vision;
160
+ lastElements = new Map();
161
+ /** 비전 폴백 로케이터(선택). 없으면 vclick 은 안내만. */
162
+ constructor(ctrl, vision) {
163
+ this.ctrl = ctrl;
164
+ this.vision = vision;
165
+ }
166
+ /** 한 액션 실행 → 사람이 읽는/LLM이 읽는 결과 문자열. */
167
+ async act(action) {
168
+ // ref 대상의 이름을 알아내 보안 스크린에 전달(click/type).
169
+ const targetName = action.ref ? this.lastElements.get(action.ref)?.name ?? '' : '';
170
+ const screen = screenBrowserAction(action, targetName);
171
+ if (!screen.allowed)
172
+ return `[차단됨] ${screen.reason}`;
173
+ switch (action.verb) {
174
+ case 'goto':
175
+ await this.ctrl.goto(action.url);
176
+ return await this.refreshAndFormat(`이동: ${action.url}`);
177
+ case 'read':
178
+ return await this.refreshAndFormat('현재 페이지 요소:');
179
+ case 'click':
180
+ if (!action.ref)
181
+ return '[오류] click 에는 ref 가 필요합니다(먼저 read).';
182
+ await this.ctrl.click(action.ref);
183
+ return await this.refreshAndFormat(`클릭: ${action.ref}`);
184
+ case 'type':
185
+ if (!action.ref)
186
+ return '[오류] type 에는 ref 가 필요합니다(먼저 read).';
187
+ await this.ctrl.type(action.ref, action.text ?? '');
188
+ return await this.refreshAndFormat(`입력: ${action.ref}`);
189
+ case 'extract':
190
+ return (await this.ctrl.extractText()).slice(0, 4000);
191
+ case 'back':
192
+ await this.ctrl.back();
193
+ return await this.refreshAndFormat('뒤로 가기');
194
+ case 'vclick': {
195
+ // 비전 폴백(#2): DOM(read)으로 안 잡히는 캔버스/이미지 요소를 설명으로 클릭.
196
+ const desc = (action.query || '').trim();
197
+ if (!desc)
198
+ return '[오류] vclick 에는 query(클릭할 요소 설명)가 필요합니다.';
199
+ if (!this.vision)
200
+ return '[비전 미사용] 비전 로케이터가 없습니다. 가능하면 read→click 을 쓰세요(캔버스 전용일 때만 vclick).';
201
+ const shot = await this.ctrl.screenshot();
202
+ const loc = await this.vision(shot.base64, desc);
203
+ if (!loc)
204
+ return `[비전] "${desc}" 위치를 찾지 못했습니다. 설명을 더 구체적으로 하거나 read 를 시도하세요.`;
205
+ const px = Math.round(Math.max(0, Math.min(1, loc.x)) * shot.width);
206
+ const py = Math.round(Math.max(0, Math.min(1, loc.y)) * shot.height);
207
+ await this.ctrl.clickAt(px, py);
208
+ return await this.refreshAndFormat(`비전 클릭: "${desc}" → (${px}, ${py})`);
209
+ }
210
+ default:
211
+ return `[오류] 알 수 없는 verb: ${action.verb}`;
212
+ }
213
+ }
214
+ async refreshAndFormat(prefix) {
215
+ const els = await this.ctrl.readElements();
216
+ this.lastElements = new Map(els.map((e) => [e.ref, e]));
217
+ return `${prefix}\n${formatElements(els)}`;
218
+ }
219
+ async close() {
220
+ await this.ctrl.close();
221
+ }
222
+ }
223
+ exports.BrowserAgent = BrowserAgent;
@@ -0,0 +1,74 @@
1
+ import { IChannel, ChannelName, InboundMessage, SendResult, FetchFn } from './channels';
2
+ import { ChatTurn } from './llm';
3
+ import { GovernedAgent } from './governed-llm';
4
+ /** 에이전트 함수: 대화 히스토리 → 응답 텍스트. 테스트에서 주입 가능. */
5
+ export type AgentFn = (history: ChatTurn[], sessionId: string) => Promise<string>;
6
+ export interface ChannelStatus {
7
+ name: ChannelName;
8
+ configured: boolean;
9
+ inbound: string;
10
+ }
11
+ export interface InboundResult {
12
+ reply: string;
13
+ /** 자동 전송(폴링·슬랙)일 때의 전송 결과. 웹훅 응답형(디스코드·카카오)은 null. */
14
+ send: SendResult | null;
15
+ /** 허용목록 차단 등으로 에이전트를 호출하지 않았는가. */
16
+ blocked?: boolean;
17
+ }
18
+ /** HTTP 웹훅 응답(서버가 그대로 반환). */
19
+ export interface WebhookResponse {
20
+ status: number;
21
+ body: unknown;
22
+ }
23
+ export declare class ChannelGateway {
24
+ private byName;
25
+ private agent;
26
+ private storeDir;
27
+ private polling;
28
+ private started;
29
+ /** 채널별 발신자 허용목록(설정 시 그 외는 차단). 미설정 채널은 전체 허용(하위 호환). */
30
+ private allowlist;
31
+ /** v2.5.2 거버넌스(opt-in). 설정 시 인입을 Tier 분류→가드→로컬/외부로 강제 라우팅한다. */
32
+ private governed?;
33
+ constructor(opts?: {
34
+ channels?: IChannel[];
35
+ agent?: AgentFn;
36
+ governed?: GovernedAgent;
37
+ storeDir?: string;
38
+ fetchFn?: FetchFn;
39
+ allowlist?: Partial<Record<ChannelName, string[]>>;
40
+ });
41
+ /** 거버넌스 라우터(설정 시) — Tier egress 감사 등 조회용. */
42
+ governedRef(): GovernedAgent | undefined;
43
+ /** 발신자가 허용되는가. 해당 채널 허용목록이 비어 있으면 전체 허용. */
44
+ isAllowed(channel: ChannelName, from: string): boolean;
45
+ /** 채널별 활성/인바운드 상태(정직 보고). */
46
+ status(): ChannelStatus[];
47
+ configuredChannels(): ChannelName[];
48
+ private storeFor;
49
+ /**
50
+ * 인바운드 1건 처리: 세션 대화 복원 → 에이전트 응답 → (autoSend 면) 채널로 전송.
51
+ * 세션 키는 채널:발신자. 대화는 영속된다.
52
+ */
53
+ handleInbound(msg: InboundMessage, autoSend?: boolean): Promise<InboundResult>;
54
+ /**
55
+ * 웹훅 수신 처리(Slack/Discord/Kakao). 플랫폼별 응답 규약을 지킨다:
56
+ * - slack: challenge 응답 또는 200 ack 후 API 로 비동기 전송(autoSend).
57
+ * - discord: PING→PONG(type1), 그 외엔 인터랙션 응답 본문(type4)에 답을 담는다.
58
+ * - kakao: 오픈빌더 스킬 응답(simpleText) 본문에 답을 담는다.
59
+ */
60
+ webhook(channel: ChannelName, payload: any): Promise<WebhookResponse>;
61
+ /**
62
+ * 설정된 모든 채널의 인바운드를 시작한다(공개서버 불요):
63
+ * - Telegram: 롱폴링 · Discord: Gateway WS · Slack: Socket Mode(SLACK_APP_TOKEN 필요).
64
+ * 반환: 전체 중단 함수. 어떤 채널이 시작됐는지 기록은 startedInbound() 로 확인.
65
+ */
66
+ startInbound(intervalMs?: number): Promise<() => void>;
67
+ /** startInbound 로 실제 시작된 채널 목록. */
68
+ startedInbound(): ChannelName[];
69
+ /**
70
+ * 폴링 시작(Telegram). 공개서버 없이 양방향 동작. stop() 반환.
71
+ * 설정된 telegram 채널이 없으면 no-op.
72
+ */
73
+ startPolling(intervalMs?: number): () => void;
74
+ }
@@ -0,0 +1,270 @@
1
+ "use strict";
2
+ // ============================================================================
3
+ // ChannelGateway — 메시징 채널 ↔ n2world 에이전트 라우팅 (Telegram/Slack/Discord/Kakao)
4
+ // ----------------------------------------------------------------------------
5
+ // 인바운드 메시지를 받아 에이전트(askAgentChat)로 라우팅하고, 응답을 채널로 돌려보낸다.
6
+ // 세션별(채널:발신자) 대화는 ChatStore 로 영속·복원된다(CLI/웹/TUI 와 동일 보존소 패턴).
7
+ //
8
+ // 정직(제1계명): 자격증명이 없는 채널은 비활성으로 보고한다. 실제 송수신 성공은 토큰이
9
+ // 있어야만 검증되며(그 전엔 (목표; 미측정)), mock 으로 성공을 연기하지 않는다.
10
+ // ============================================================================
11
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
12
+ if (k2 === undefined) k2 = k;
13
+ var desc = Object.getOwnPropertyDescriptor(m, k);
14
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
15
+ desc = { enumerable: true, get: function() { return m[k]; } };
16
+ }
17
+ Object.defineProperty(o, k2, desc);
18
+ }) : (function(o, m, k, k2) {
19
+ if (k2 === undefined) k2 = k;
20
+ o[k2] = m[k];
21
+ }));
22
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
23
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
24
+ }) : function(o, v) {
25
+ o["default"] = v;
26
+ });
27
+ var __importStar = (this && this.__importStar) || (function () {
28
+ var ownKeys = function(o) {
29
+ ownKeys = Object.getOwnPropertyNames || function (o) {
30
+ var ar = [];
31
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
32
+ return ar;
33
+ };
34
+ return ownKeys(o);
35
+ };
36
+ return function (mod) {
37
+ if (mod && mod.__esModule) return mod;
38
+ var result = {};
39
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
40
+ __setModuleDefault(result, mod);
41
+ return result;
42
+ };
43
+ })();
44
+ Object.defineProperty(exports, "__esModule", { value: true });
45
+ exports.ChannelGateway = void 0;
46
+ const path = __importStar(require("path"));
47
+ const channels_1 = require("./channels");
48
+ const chat_store_1 = require("./chat-store");
49
+ const llm_1 = require("./llm");
50
+ const oracle_1 = require("./oracle");
51
+ function sanitize(s) {
52
+ return s.replace(/[^\w.-]/g, '_').slice(0, 64);
53
+ }
54
+ class ChannelGateway {
55
+ byName = new Map();
56
+ agent;
57
+ storeDir;
58
+ polling = false;
59
+ started = [];
60
+ /** 채널별 발신자 허용목록(설정 시 그 외는 차단). 미설정 채널은 전체 허용(하위 호환). */
61
+ allowlist;
62
+ /** v2.5.2 거버넌스(opt-in). 설정 시 인입을 Tier 분류→가드→로컬/외부로 강제 라우팅한다. */
63
+ governed;
64
+ constructor(opts = {}) {
65
+ const channels = opts.channels ?? (0, channels_1.createDefaultChannels)(opts.fetchFn);
66
+ for (const c of channels)
67
+ this.byName.set(c.name, c);
68
+ this.storeDir = opts.storeDir ?? 'chat-data';
69
+ this.agent = opts.agent ?? defaultAgent();
70
+ this.governed = opts.governed;
71
+ this.allowlist = buildAllowlist(opts.allowlist);
72
+ }
73
+ /** 거버넌스 라우터(설정 시) — Tier egress 감사 등 조회용. */
74
+ governedRef() { return this.governed; }
75
+ /** 발신자가 허용되는가. 해당 채널 허용목록이 비어 있으면 전체 허용. */
76
+ isAllowed(channel, from) {
77
+ const set = this.allowlist[channel];
78
+ if (!set || set.size === 0)
79
+ return true;
80
+ return set.has(from);
81
+ }
82
+ /** 채널별 활성/인바운드 상태(정직 보고). */
83
+ status() {
84
+ return [...this.byName.values()].map((c) => ({ name: c.name, configured: c.isConfigured(), inbound: c.inbound }));
85
+ }
86
+ configuredChannels() {
87
+ return [...this.byName.values()].filter((c) => c.isConfigured()).map((c) => c.name);
88
+ }
89
+ storeFor(channel, from) {
90
+ return new chat_store_1.ChatStore(path.join(this.storeDir, `channel-${channel}-${sanitize(from)}.json`));
91
+ }
92
+ /**
93
+ * 인바운드 1건 처리: 세션 대화 복원 → 에이전트 응답 → (autoSend 면) 채널로 전송.
94
+ * 세션 키는 채널:발신자. 대화는 영속된다.
95
+ */
96
+ async handleInbound(msg, autoSend = true) {
97
+ const ch = this.byName.get(msg.channel);
98
+ if (!ch)
99
+ return { reply: '', send: { ok: false, reason: `unknown channel: ${msg.channel}` } };
100
+ // H1: 발신자 허용목록 — 허용되지 않으면 에이전트 호출 없이 차단(샌드박스 노출 최소화).
101
+ if (!this.isAllowed(msg.channel, msg.from)) {
102
+ return { reply: '', send: null, blocked: true };
103
+ }
104
+ const store = this.storeFor(msg.channel, msg.from);
105
+ store.load();
106
+ store.append({ role: 'user', content: msg.text });
107
+ let reply;
108
+ try {
109
+ if (this.governed) {
110
+ // v2.5.2: Tier 분류 → TierGuard → 로컬/외부 강제 라우팅(실 강제 지점).
111
+ reply = (await this.governed.respond(store.history())).text;
112
+ }
113
+ else {
114
+ reply = await this.agent(store.history(), `${msg.channel}:${msg.from}`);
115
+ }
116
+ }
117
+ catch (e) {
118
+ reply = `[오류] ${e?.message ?? e}`;
119
+ }
120
+ store.append({ role: 'assistant', content: reply });
121
+ if (autoSend) {
122
+ const send = await ch.send(msg.from, reply);
123
+ return { reply, send };
124
+ }
125
+ return { reply, send: null };
126
+ }
127
+ /**
128
+ * 웹훅 수신 처리(Slack/Discord/Kakao). 플랫폼별 응답 규약을 지킨다:
129
+ * - slack: challenge 응답 또는 200 ack 후 API 로 비동기 전송(autoSend).
130
+ * - discord: PING→PONG(type1), 그 외엔 인터랙션 응답 본문(type4)에 답을 담는다.
131
+ * - kakao: 오픈빌더 스킬 응답(simpleText) 본문에 답을 담는다.
132
+ */
133
+ async webhook(channel, payload) {
134
+ if (channel === 'slack') {
135
+ if (payload?.type === 'url_verification')
136
+ return { status: 200, body: { challenge: payload.challenge } };
137
+ const msg = channels_1.SlackChannel.parseInbound(payload);
138
+ if (!msg)
139
+ return { status: 200, body: { ok: true } }; // 무시 대상(봇/비메시지)도 ack
140
+ await this.handleInbound(msg, true); // chat.postMessage 로 전송
141
+ return { status: 200, body: { ok: true } };
142
+ }
143
+ if (channel === 'discord') {
144
+ if (payload?.type === 1)
145
+ return { status: 200, body: { type: 1 } }; // PONG
146
+ const msg = channels_1.DiscordChannel.parseInbound(payload);
147
+ if (!msg)
148
+ return { status: 200, body: { type: 4, data: { content: '(처리할 수 없는 요청)' } } };
149
+ const { reply } = await this.handleInbound(msg, false);
150
+ return { status: 200, body: { type: 4, data: { content: reply } } };
151
+ }
152
+ if (channel === 'kakao') {
153
+ const msg = channels_1.KakaoTalkChannel.parseInbound(payload);
154
+ if (!msg)
155
+ return { status: 200, body: channels_1.KakaoTalkChannel.formatSkillResponse('(발화를 인식하지 못했습니다)') };
156
+ const { reply } = await this.handleInbound(msg, false);
157
+ return { status: 200, body: channels_1.KakaoTalkChannel.formatSkillResponse(reply) };
158
+ }
159
+ return { status: 400, body: { error: `webhook 미지원 채널: ${channel}` } };
160
+ }
161
+ /**
162
+ * 설정된 모든 채널의 인바운드를 시작한다(공개서버 불요):
163
+ * - Telegram: 롱폴링 · Discord: Gateway WS · Slack: Socket Mode(SLACK_APP_TOKEN 필요).
164
+ * 반환: 전체 중단 함수. 어떤 채널이 시작됐는지 기록은 startedInbound() 로 확인.
165
+ */
166
+ async startInbound(intervalMs = 1500) {
167
+ const stops = [];
168
+ this.started = [];
169
+ const tg = this.byName.get('telegram');
170
+ if (tg?.isConfigured()) {
171
+ stops.push(this.startPolling(intervalMs));
172
+ this.started.push('telegram');
173
+ }
174
+ const dc = this.byName.get('discord');
175
+ if (dc?.isConfigured() && typeof dc.connectGateway === 'function') {
176
+ stops.push(dc.connectGateway((m) => void this.handleInbound(m, true)));
177
+ this.started.push('discord');
178
+ }
179
+ const sl = this.byName.get('slack');
180
+ const appToken = (process.env.SLACK_APP_TOKEN || '').trim();
181
+ if (sl?.isConfigured() && appToken && typeof sl.connectSocketMode === 'function') {
182
+ try {
183
+ stops.push(await sl.connectSocketMode(appToken, (m) => void this.handleInbound(m, true)));
184
+ this.started.push('slack');
185
+ }
186
+ catch (e) {
187
+ // 정직: 연결 실패는 숨기지 않는다(토큰/스코프 문제 등).
188
+ // eslint-disable-next-line no-console
189
+ console.error('[slack] Socket Mode 연결 실패:', e?.message ?? e);
190
+ }
191
+ }
192
+ return () => { for (const s of stops) {
193
+ try {
194
+ s();
195
+ }
196
+ catch { /* noop */ }
197
+ } };
198
+ }
199
+ /** startInbound 로 실제 시작된 채널 목록. */
200
+ startedInbound() {
201
+ return [...this.started];
202
+ }
203
+ /**
204
+ * 폴링 시작(Telegram). 공개서버 없이 양방향 동작. stop() 반환.
205
+ * 설정된 telegram 채널이 없으면 no-op.
206
+ */
207
+ startPolling(intervalMs = 1500) {
208
+ const tg = this.byName.get('telegram');
209
+ if (!tg || !tg.isConfigured() || typeof tg.poll !== 'function') {
210
+ return () => { };
211
+ }
212
+ this.polling = true;
213
+ const loop = async () => {
214
+ while (this.polling) {
215
+ try {
216
+ const msgs = await tg.poll(20);
217
+ for (const m of msgs)
218
+ await this.handleInbound(m, true);
219
+ }
220
+ catch {
221
+ /* 일시 오류는 다음 주기에 재시도 */
222
+ }
223
+ await new Promise((r) => setTimeout(r, intervalMs));
224
+ }
225
+ };
226
+ void loop();
227
+ return () => {
228
+ this.polling = false;
229
+ };
230
+ }
231
+ }
232
+ exports.ChannelGateway = ChannelGateway;
233
+ /** 환경변수에서 채널별 허용목록(<CHANNEL>_ALLOWED_IDS, 쉼표 구분)을 만든다. */
234
+ function buildAllowlist(override) {
235
+ const out = {};
236
+ const chans = ['telegram', 'slack', 'discord', 'kakao'];
237
+ for (const c of chans) {
238
+ const ids = override?.[c] ?? (process.env[`${c.toUpperCase()}_ALLOWED_IDS`] || '').split(',').map((s) => s.trim()).filter(Boolean);
239
+ if (ids.length)
240
+ out[c] = new Set(ids);
241
+ }
242
+ return out;
243
+ }
244
+ /**
245
+ * 기본 에이전트: 샌드박스 도구 + askAgentChat. 채널 세션 히스토리를 그대로 사용.
246
+ * H2: 채널 입력은 신뢰 불가하므로, 도구 실행 전에 oracle 규칙 스크린으로 **알려진 파괴적 명령을
247
+ * 거부**한다(rm-rf·fork-bomb·디스크 덮어쓰기 등). 통과한 명령만 샌드박스에서 실행.
248
+ */
249
+ function defaultAgent() {
250
+ let runShell = null;
251
+ const oracle = new oracle_1.CrossValidationOracle();
252
+ return async (history) => {
253
+ if (!runShell) {
254
+ // 지연 초기화(테스트가 agent 를 주입하면 여기 도달하지 않음).
255
+ const { LocalSandbox, WorkerAgent } = await import('./index.js');
256
+ const sandbox = new LocalSandbox();
257
+ await sandbox.init();
258
+ const worker = new WorkerAgent(sandbox);
259
+ runShell = async (cmd) => {
260
+ const screen = oracle.screenDangerousAction(cmd);
261
+ if (screen.flagged) {
262
+ return `[차단됨] 위험 명령으로 거부되었습니다(규칙: ${screen.matched.join(', ')}). 채널 입력은 파괴적 명령을 실행할 수 없습니다.`;
263
+ }
264
+ const res = await worker.executeTask(cmd);
265
+ return `[cmd: ${cmd}]\n${res.stdout}\n${res.stderr}`;
266
+ };
267
+ }
268
+ return (0, llm_1.askAgentChat)(history, runShell);
269
+ };
270
+ }