agentlas 0.9.10 → 1.0.2

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 (145) hide show
  1. package/CHANGELOG.md +106 -0
  2. package/README.md +319 -341
  3. package/bin/agentlas.cjs +32 -9
  4. package/engine/agentlas-banner.cjs +24 -3
  5. package/engine/agentlas-composer.cjs +239 -31
  6. package/engine/agentlas-config.cjs +103 -7
  7. package/engine/agentlas-core-harness.cjs +48 -4
  8. package/engine/agentlas-evolution.cjs +3 -4
  9. package/engine/agentlas-i18n.cjs +88 -32
  10. package/engine/agentlas-input.cjs +234 -18
  11. package/engine/agentlas-memory-governance.cjs +3 -5
  12. package/engine/agentlas-memory-import.cjs +3 -3
  13. package/engine/agentlas-native-host.cjs +200 -9
  14. package/engine/agentlas-onboard.cjs +112 -20
  15. package/engine/agentlas-permissions.cjs +14 -7
  16. package/engine/agentlas-sqlite-policy.cjs +34 -0
  17. package/engine/agentlas-ui.cjs +93 -42
  18. package/engine/agentlas-workforce.cjs +472 -58
  19. package/engine/agentlas-workload-routing.cjs +8 -3
  20. package/engine/agentlas.cjs +140 -12367
  21. package/engine/agents/files.cjs +61 -0
  22. package/engine/agents/import-local.cjs +237 -0
  23. package/engine/agents/registry.cjs +158 -0
  24. package/engine/agents/router.cjs +565 -0
  25. package/engine/agents/routes.cjs +43 -0
  26. package/engine/architecture.data.json +2 -2
  27. package/engine/automation/daemon.cjs +335 -0
  28. package/engine/automation/schedule.cjs +181 -0
  29. package/engine/automation/store.cjs +209 -0
  30. package/engine/cloud/auth.cjs +279 -0
  31. package/engine/cloud/hub-client.cjs +239 -0
  32. package/engine/cloud-assets/cargo.cjs +49 -0
  33. package/engine/cloud-assets/cas.cjs +235 -0
  34. package/engine/cloud-assets/commands.cjs +273 -0
  35. package/engine/cloud-assets/package.cjs +936 -0
  36. package/engine/cloud-assets/restore.cjs +172 -0
  37. package/engine/cloud-assets/state.cjs +268 -0
  38. package/engine/commands/automation.cjs +195 -0
  39. package/engine/commands/billing.cjs +96 -0
  40. package/engine/commands/browser.cjs +20 -0
  41. package/engine/commands/build.cjs +33 -0
  42. package/engine/commands/call.cjs +24 -0
  43. package/engine/commands/career-graph.cjs +51 -0
  44. package/engine/commands/cd.cjs +22 -0
  45. package/engine/commands/chat.cjs +12 -0
  46. package/engine/commands/chats.cjs +28 -0
  47. package/engine/commands/cloud.cjs +17 -0
  48. package/engine/commands/connect.cjs +20 -0
  49. package/engine/commands/context.cjs +66 -0
  50. package/engine/commands/creds.cjs +203 -0
  51. package/engine/commands/doctor.cjs +68 -0
  52. package/engine/commands/env.cjs +33 -0
  53. package/engine/commands/evolve.cjs +23 -0
  54. package/engine/commands/experience.cjs +34 -0
  55. package/engine/commands/film.cjs +8 -0
  56. package/engine/commands/firm.cjs +115 -0
  57. package/engine/commands/help.cjs +65 -0
  58. package/engine/commands/hep.cjs +23 -0
  59. package/engine/commands/import.cjs +35 -0
  60. package/engine/commands/index.cjs +136 -0
  61. package/engine/commands/install.cjs +27 -0
  62. package/engine/commands/journal.cjs +31 -0
  63. package/engine/commands/legacy-network.cjs +29 -0
  64. package/engine/commands/list.cjs +49 -0
  65. package/engine/commands/login.cjs +68 -0
  66. package/engine/commands/logout.cjs +28 -0
  67. package/engine/commands/mcp.cjs +91 -0
  68. package/engine/commands/memory.cjs +21 -0
  69. package/engine/commands/multimodal.cjs +91 -0
  70. package/engine/commands/native.cjs +34 -0
  71. package/engine/commands/netadmin.cjs +31 -0
  72. package/engine/commands/oberon.cjs +70 -0
  73. package/engine/commands/ontology.cjs +24 -0
  74. package/engine/commands/open.cjs +48 -0
  75. package/engine/commands/plugin.cjs +101 -0
  76. package/engine/commands/project.cjs +47 -0
  77. package/engine/commands/research.cjs +36 -0
  78. package/engine/commands/route.cjs +37 -0
  79. package/engine/commands/run.cjs +149 -0
  80. package/engine/commands/search.cjs +55 -0
  81. package/engine/commands/setup.cjs +45 -0
  82. package/engine/commands/storm.cjs +75 -0
  83. package/engine/commands/swarm.cjs +75 -0
  84. package/engine/commands/telegram.cjs +32 -0
  85. package/engine/commands/uninstall.cjs +68 -0
  86. package/engine/commands/update.cjs +54 -0
  87. package/engine/commands/upload.cjs +18 -0
  88. package/engine/commands/usage.cjs +35 -0
  89. package/engine/commands/variant.cjs +25 -0
  90. package/engine/commands/version.cjs +9 -0
  91. package/engine/commands/whoami.cjs +38 -0
  92. package/engine/commands/workforce.cjs +103 -0
  93. package/engine/core/db.cjs +160 -0
  94. package/engine/core/paths.cjs +35 -0
  95. package/engine/experience/build.cjs +181 -0
  96. package/engine/experience/intents.cjs +492 -0
  97. package/engine/experience/runtime.cjs +242 -0
  98. package/engine/experience/variant.cjs +196 -0
  99. package/engine/firms/orchestrate.cjs +333 -0
  100. package/engine/hephaestus/runtime.cjs +697 -0
  101. package/engine/hub/install.cjs +872 -0
  102. package/engine/hub/plugins.cjs +213 -0
  103. package/engine/mcp/consent.cjs +289 -0
  104. package/engine/mcp/contract.cjs +202 -0
  105. package/engine/mcp/index.cjs +43 -0
  106. package/engine/mcp/inventory.cjs +322 -0
  107. package/engine/mcp/plan.cjs +286 -0
  108. package/engine/mcp/probe.cjs +151 -0
  109. package/engine/memory-cli/curate.cjs +163 -0
  110. package/engine/oberon/common.cjs +69 -0
  111. package/engine/oberon/manifest.cjs +164 -0
  112. package/engine/oberon/outputs.cjs +70 -0
  113. package/engine/oberon/render.cjs +164 -0
  114. package/engine/project/career-graph.cjs +249 -0
  115. package/engine/project/credentials.cjs +262 -0
  116. package/engine/project/env-file.cjs +46 -0
  117. package/engine/project/index.cjs +27 -0
  118. package/engine/project/memory-context.cjs +453 -0
  119. package/engine/project/ontology.cjs +467 -0
  120. package/engine/project/paths.cjs +39 -0
  121. package/engine/project/seed.cjs +200 -0
  122. package/engine/project/state.cjs +403 -0
  123. package/engine/project/super-ontology-seed.json +3288 -0
  124. package/engine/runtimes/detect.cjs +54 -0
  125. package/engine/runtimes/overrides.cjs +139 -0
  126. package/engine/runtimes/resolve.cjs +64 -0
  127. package/engine/sessions/apply-fences.cjs +188 -0
  128. package/engine/sessions/fences.cjs +362 -0
  129. package/engine/sessions/orchestrator.cjs +170 -0
  130. package/engine/sessions/prompt.cjs +212 -0
  131. package/engine/sessions/session.cjs +245 -0
  132. package/engine/sessions/sink.cjs +54 -0
  133. package/engine/sessions/store.cjs +79 -0
  134. package/engine/storm/deps.cjs +88 -0
  135. package/engine/storm/storm.cjs +218 -0
  136. package/engine/storm/swarm.cjs +422 -0
  137. package/engine/ui/palette.cjs +105 -0
  138. package/engine/ui/renderer.cjs +85 -0
  139. package/engine/ui/repl.cjs +444 -0
  140. package/engine/workforce/capture.cjs +701 -0
  141. package/engine/workforce/deps.cjs +472 -0
  142. package/package.json +2 -6
  143. package/engine/agentlas-experience-mcp.cjs +0 -1709
  144. package/engine/agentlas-parity.cjs +0 -1499
  145. package/engine/agentlas-repl.cjs +0 -1780
@@ -0,0 +1,202 @@
1
+ "use strict";
2
+ /*
3
+ * mcp/contract — MCP 서브시스템 공용 저수준 계약(검증·안전텍스트·프라이빗 JSON IO·락).
4
+ *
5
+ * v1 engine/agentlas-experience-mcp.cjs에서 MCP 관심사만 추출한 v2 모듈군의 바닥층.
6
+ * 여기에는 기능 로직이 없다: 상위(inventory/probe/plan/consent)가 공유하는
7
+ * 불변 계약만 둔다. 검증 규칙 하나라도 느슨해지면 공개 투영/동의 영수증의
8
+ * 보안 경계가 같이 무너지므로, v1 동작을 토씨까지 보존한다.
9
+ */
10
+ const crypto = require("node:crypto");
11
+ const fs = require("node:fs");
12
+ const path = require("node:path");
13
+
14
+ const ID_RE = /^[A-Za-z0-9][A-Za-z0-9._:/@-]{2,255}$/;
15
+ const HASH_RE = /^sha256:[0-9a-f]{64}$/;
16
+ const ENV_RE = /^[A-Z][A-Z0-9_]*$/;
17
+ const MAX_JSON_BYTES = 2 * 1024 * 1024;
18
+ const MAX_APPROVED_MCP_PER_BUILD = 8;
19
+ const STATE_LOCK_STALE_MS = 30_000;
20
+ const STATE_LOCK_WAIT_MS = 2_000;
21
+
22
+ // Public contract text must be compact, value-free, and instruction-safe.
23
+ const UNSAFE_TEXT_PATTERNS = [
24
+ { code: "openai-secret", re: /\bsk-[A-Za-z0-9_-]{20,}\b/ },
25
+ { code: "github-secret", re: /\bgh[pousr]_[A-Za-z0-9_]{20,}\b/ },
26
+ { code: "aws-secret", re: /\bAKIA[0-9A-Z]{16}\b/ },
27
+ { code: "private-key", re: /-----BEGIN (?:RSA |EC |OPENSSH |DSA )?PRIVATE KEY-----/i },
28
+ { code: "credential", re: /(?:api[_ -]?key|access[_ -]?token|refresh[_ -]?token|password|private[_ -]?key|authorization)\s*[:=]\s*\S+/i },
29
+ { code: "bearer", re: /\bbearer\s+[A-Za-z0-9._~+/=-]{8,}/i },
30
+ { code: "private-path", re: /(?:file:\/\/|(?:^|[\s"'`()\[\]{}=:,;])(?:\.\.[/\\]|~[/\\]|\/(?!\/|\s)(?:[^/\s"'`<>]+\/)*[^/\s"'`<>]+|[A-Za-z]:[/\\]\S+|\\\\[^\\/\s]+[\\/][^\\/\s]+))/i },
31
+ { code: "email", re: /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/i },
32
+ { code: "phone", re: /(?:\+?\d[\d .()-]{8,}\d)/ },
33
+ { code: "account-id", re: /\b(?:account|customer|client|user)[ _-]?(?:id|number|no)\s*[:=#]?\s*[A-Za-z0-9_-]{4,}\b|(?:계정|고객|사용자)[ _-]?(?:id|아이디|번호)\s*[:=#]?\s*[A-Za-z0-9_-]{4,}/i },
34
+ { code: "raw-prompt", re: /(?:raw[_ -]?prompt|full[_ -]?transcript|conversation[_ -]?dump|system[_ -]?prompt|(?:^|\n)\s*(?:system|assistant|user|tool)\s*:|(?:AGENTS|CLAUDE|GEMINI)\.md|\.agentlas[\\/])/i },
35
+ { code: "prompt-injection", re: /(?:ignore|disregard|override)[\s_-]+(?:all[\s_-]+)?(?:previous|prior|system|developer|hidden)[\s_-]+(?:instructions?|prompts?|rules?|directives?)/i },
36
+ { code: "exfiltration", re: /(?:reveal|show|print|dump|expose|leak|send|upload|exfiltrate|steal)[\s_-]+(?:(?:the|all)[\s_-]+)?(?:secret|credential|token|cookie|password|private[\s_-]?key|api[\s_-]?key)/i },
37
+ { code: "safety-bypass", re: /(?:disable|bypass|skip|remove|turn[\s_-]+off)[\s_-]+(?:(?:the|all)[\s_-]+)?(?:safety|guardrails?|approval|consent|permission[\s_-]?checks?|security[\s_-]?checks?)/i },
38
+ { code: "opaque-blob", re: /\b(?:[A-Fa-f0-9]{128,}|[A-Za-z0-9+/]{124,}={0,2})\b/ },
39
+ ];
40
+
41
+ function assertObject(value, label) {
42
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object`);
43
+ return value;
44
+ }
45
+
46
+ function assertExactKeys(value, allowed, required, label) {
47
+ assertObject(value, label);
48
+ for (const key of Object.keys(value)) {
49
+ if (!allowed.has(key)) throw new Error(`${label} has an unsupported field: ${key}`);
50
+ }
51
+ for (const key of required) {
52
+ if (!Object.prototype.hasOwnProperty.call(value, key)) throw new Error(`${label} is missing: ${key}`);
53
+ }
54
+ }
55
+
56
+ function assertId(value, label) {
57
+ if (!ID_RE.test(String(value || ""))) throw new Error(`${label} is not a valid Agentlas id`);
58
+ return String(value);
59
+ }
60
+
61
+ function assertUniqueIds(value, label, options = {}) {
62
+ if (!Array.isArray(value)) throw new Error(`${label} must be an array`);
63
+ if (options.min && value.length < options.min) throw new Error(`${label} must have at least ${options.min} item(s)`);
64
+ if (options.max && value.length > options.max) throw new Error(`${label} has too many items`);
65
+ const items = value.map((item, index) => assertId(item, `${label}[${index}]`));
66
+ if (new Set(items).size !== items.length) throw new Error(`${label} must be unique`);
67
+ return items;
68
+ }
69
+
70
+ function assertSafeText(value, label, max = 300) {
71
+ const text = String(value || "").trim();
72
+ if (!text || text.length > max || /[\u0000-\u001f\u007f\u202a-\u202e\u2066-\u2069]/i.test(text)) throw new Error(`${label} must be compact single-line text`);
73
+ const unsafe = UNSAFE_TEXT_PATTERNS.find((pattern) => pattern.re.test(text));
74
+ if (unsafe) throw new Error(`${label} is not public-safe (${unsafe.code})`);
75
+ return text;
76
+ }
77
+
78
+ function assertIsoDateOrNull(value, label) {
79
+ if (value == null) return null;
80
+ if (typeof value !== "string" || !value || !Number.isFinite(Date.parse(value))) throw new Error(`${label} must be an ISO date-time or null`);
81
+ return value;
82
+ }
83
+
84
+ function safeCatalogId(value) {
85
+ const text = String(value || "").trim();
86
+ return ID_RE.test(text) && !UNSAFE_TEXT_PATTERNS.some((pattern) => pattern.re.test(text)) ? text : null;
87
+ }
88
+
89
+ function safeDisplayName(value, fallback) {
90
+ const text = String(value || "").replace(/[\u0000-\u001f\u007f\u202a-\u202e\u2066-\u2069]/g, " ").replace(/\s+/g, " ").trim().slice(0, 80);
91
+ if (!text || UNSAFE_TEXT_PATTERNS.some((pattern) => pattern.re.test(text))) return fallback;
92
+ return text;
93
+ }
94
+
95
+ function readJsonFile(filePath, label) {
96
+ const absolute = path.resolve(filePath);
97
+ const stat = fs.lstatSync(absolute);
98
+ if (!stat.isFile() || stat.isSymbolicLink()) throw new Error(`${label} must be a regular file (symlinks are not accepted)`);
99
+ if (stat.size <= 0 || stat.size > MAX_JSON_BYTES) throw new Error(`${label} has an invalid size`);
100
+ let value;
101
+ try { value = JSON.parse(fs.readFileSync(absolute, "utf8")); } catch (error) { throw new Error(`${label} is not valid JSON: ${error.message}`); }
102
+ return { absolute, value };
103
+ }
104
+
105
+ function writePrivateJsonAtomic(filePath, value) {
106
+ const dir = path.dirname(filePath);
107
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
108
+ try { fs.chmodSync(dir, 0o700); } catch { /* best effort */ }
109
+ const temp = path.join(dir, `.${path.basename(filePath)}.${process.pid}.${crypto.randomBytes(6).toString("hex")}.tmp`);
110
+ try {
111
+ fs.writeFileSync(temp, JSON.stringify(value, null, 2) + "\n", { encoding: "utf8", mode: 0o600, flag: "wx" });
112
+ fs.renameSync(temp, filePath);
113
+ try { fs.chmodSync(filePath, 0o600); } catch { /* best effort */ }
114
+ } finally {
115
+ try { fs.rmSync(temp, { force: true }); } catch { /* noop */ }
116
+ }
117
+ }
118
+
119
+ function waitSync(milliseconds) {
120
+ // Atomics.wait은 지원 Node 20+에서 스핀 없이 대기하는 유일한 동기 sleep이다.
121
+ const signal = new Int32Array(new SharedArrayBuffer(4));
122
+ Atomics.wait(signal, 0, 0, milliseconds);
123
+ }
124
+
125
+ /**
126
+ * <stateFile>.lock 파일 기반 상호배제. 크래시 잔류 락은 STALE_MS 이후 회수한다.
127
+ * busyMessage/unsafeMessage를 호출자가 주어 사용자 문구를 상태별로 유지한다.
128
+ */
129
+ function withPrivateStateLock(stateFile, labels, action) {
130
+ const dir = path.dirname(stateFile);
131
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
132
+ try { fs.chmodSync(dir, 0o700); } catch { /* best effort */ }
133
+ const lockFile = `${stateFile}.lock`;
134
+ const deadline = Date.now() + STATE_LOCK_WAIT_MS;
135
+ let descriptor = null;
136
+ while (descriptor == null) {
137
+ try {
138
+ descriptor = fs.openSync(lockFile, "wx", 0o600);
139
+ fs.writeFileSync(descriptor, `${process.pid}\n${new Date().toISOString()}\n`, "utf8");
140
+ } catch (error) {
141
+ if (!error || error.code !== "EEXIST") throw error;
142
+ try {
143
+ const stat = fs.lstatSync(lockFile);
144
+ if (!stat.isFile() || stat.isSymbolicLink()) throw new Error(labels.unsafe);
145
+ if (Date.now() - stat.mtimeMs > STATE_LOCK_STALE_MS) {
146
+ fs.unlinkSync(lockFile);
147
+ continue;
148
+ }
149
+ } catch (statError) {
150
+ if (statError && statError.code === "ENOENT") continue;
151
+ throw statError;
152
+ }
153
+ if (Date.now() >= deadline) throw new Error(labels.busy);
154
+ waitSync(25);
155
+ }
156
+ }
157
+ try {
158
+ return action();
159
+ } finally {
160
+ try { fs.closeSync(descriptor); } catch { /* noop */ }
161
+ try { fs.unlinkSync(lockFile); } catch { /* crash recovery handles leftovers */ }
162
+ }
163
+ }
164
+
165
+ function parseIdList(value) {
166
+ if (!value || value === true) return [];
167
+ return [...new Set(String(value).split(",").map((item) => item.trim()).filter(Boolean))];
168
+ }
169
+
170
+ function parseRuntimeServerArgs(value) {
171
+ if (typeof value !== "string" || value.length > 64 * 1024) return null;
172
+ try {
173
+ const parsed = JSON.parse(value || "[]");
174
+ if (!Array.isArray(parsed) || parsed.length > 128 || parsed.some((item) => typeof item !== "string" || item.length > 4096 || /[\u0000\r\n]/.test(item))) return null;
175
+ return parsed;
176
+ } catch {
177
+ return null;
178
+ }
179
+ }
180
+
181
+ module.exports = {
182
+ ID_RE,
183
+ HASH_RE,
184
+ ENV_RE,
185
+ MAX_JSON_BYTES,
186
+ MAX_APPROVED_MCP_PER_BUILD,
187
+ UNSAFE_TEXT_PATTERNS,
188
+ assertObject,
189
+ assertExactKeys,
190
+ assertId,
191
+ assertUniqueIds,
192
+ assertSafeText,
193
+ assertIsoDateOrNull,
194
+ safeCatalogId,
195
+ safeDisplayName,
196
+ readJsonFile,
197
+ writePrivateJsonAtomic,
198
+ waitSync,
199
+ withPrivateStateLock,
200
+ parseIdList,
201
+ parseRuntimeServerArgs,
202
+ };
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ /*
3
+ * mcp/index — v1 engine/agentlas-experience-mcp.cjs의 MCP 서브시스템 후계 표면.
4
+ *
5
+ * v1 모놀리스가 export하던 MCP 관련 이름/동작을 그대로 재수출한다(계약 테스트가
6
+ * 이 이름에 의존한다). 경험(Experience) 인텐트·빌드 인자 파싱·variant 해소는
7
+ * MCP 관심사가 아니므로 여기 없다 — 각자의 v2 모듈로 이관된다.
8
+ */
9
+ const inventory = require("./inventory.cjs");
10
+ const probe = require("./probe.cjs");
11
+ const plan = require("./plan.cjs");
12
+ const consent = require("./consent.cjs");
13
+
14
+ module.exports = {
15
+ // inventory — 시스템 전역 레지스트리 + 프로젝트 정책
16
+ TOKEN_BUDGET: inventory.TOKEN_BUDGET,
17
+ validateMcpRequirement: inventory.validateMcpRequirement,
18
+ validateMcpPolicy: inventory.validateMcpPolicy,
19
+ loadProjectMcpPolicy: inventory.loadProjectMcpPolicy,
20
+ collectSystemMcpInventory: inventory.collectSystemMcpInventory,
21
+ materializeTrustedSystemMcpServer: inventory.materializeTrustedSystemMcpServer,
22
+ readApprovedSystemMcpServer: inventory.readApprovedSystemMcpServer,
23
+ // probe — stdio 연결 프리플라이트
24
+ MCP_PROBE_CONCURRENCY: probe.MCP_PROBE_CONCURRENCY,
25
+ MCP_PROBE_PER_SERVER_TIMEOUT_MS: probe.MCP_PROBE_PER_SERVER_TIMEOUT_MS,
26
+ MCP_PROBE_TOTAL_TIMEOUT_MS: probe.MCP_PROBE_TOTAL_TIMEOUT_MS,
27
+ probeSystemMcpServerConnection: probe.probeSystemMcpServerConnection,
28
+ // plan — 요구사항 해소 + 빌드 플랜 + 빌더 지시문
29
+ resolveMcpRequirement: plan.resolveMcpRequirement,
30
+ buildMcpPlan: plan.buildMcpPlan,
31
+ renderMcpPlan: plan.renderMcpPlan,
32
+ fitApprovedMcpIds: plan.fitApprovedMcpIds,
33
+ buildMcpDirective: plan.buildMcpDirective,
34
+ renderBuildMcpResult: plan.renderBuildMcpResult,
35
+ // consent — 1회 동의 + 영수증 + 런타임 allowlist
36
+ mcpConsentStatePath: consent.mcpConsentStatePath,
37
+ loadMcpConsentState: consent.loadMcpConsentState,
38
+ persistMcpConsentReceipts: consent.persistMcpConsentReceipts,
39
+ readConsentedSystemMcpServers: consent.readConsentedSystemMcpServers,
40
+ normalizeConsentAnswer: consent.normalizeConsentAnswer,
41
+ askMcpConsentOnce: consent.askMcpConsentOnce,
42
+ resolveApprovedMcpRuntimeAllowlist: consent.resolveApprovedMcpRuntimeAllowlist,
43
+ };
@@ -0,0 +1,322 @@
1
+ "use strict";
2
+ /*
3
+ * mcp/inventory — 시스템 전역 MCP 레지스트리 인벤토리 + 프로젝트 MCP 정책.
4
+ *
5
+ * 경계(v1 계약 그대로):
6
+ * - 발견은 Agentlas가 신뢰하는 시스템 전역 레지스트리(mcp_servers) 메타데이터만
7
+ * 읽는다. 실행/연결/다운로드/서버정의 복사는 절대 하지 않는다.
8
+ * - 자격증명 "값"은 결코 다루지 않는다 — 키 이름 존재 여부만 관찰한다.
9
+ * - 패키지/카탈로그 콘텐츠는 실행 재료를 공급할 수 없다: 실행 필드는 항상
10
+ * 신뢰 레지스트리 행에서 다시 읽는다(materialize/readApproved).
11
+ */
12
+ const crypto = require("node:crypto");
13
+ const fs = require("node:fs");
14
+ const os = require("node:os");
15
+ const path = require("node:path");
16
+ const {
17
+ hasSensitiveRuntimeArgument,
18
+ mcpRuntimeHome,
19
+ normalizeCredentialKeyNames,
20
+ } = require("../agentlas-mcp-env.cjs");
21
+ const {
22
+ ID_RE,
23
+ ENV_RE,
24
+ UNSAFE_TEXT_PATTERNS,
25
+ assertObject,
26
+ assertExactKeys,
27
+ assertId,
28
+ assertUniqueIds,
29
+ assertSafeText,
30
+ safeCatalogId,
31
+ safeDisplayName,
32
+ readJsonFile,
33
+ parseRuntimeServerArgs,
34
+ } = require("./contract.cjs");
35
+
36
+ // MCP 정책의 contextBudget 상한(동결). 정책 파일이 이보다 큰 예산을 선언하면 거부.
37
+ const TOKEN_BUDGET = Object.freeze({
38
+ coreMemoryMaxTokens: 150,
39
+ experienceRetrievalMaxTokens: 800,
40
+ experienceRetrievalMaxItems: 8,
41
+ });
42
+
43
+ const MCP_REQUIREMENT_REQUIRED = [
44
+ "schemaVersion", "kind", "requirementId", "catalogId", "reason", "capabilities",
45
+ "required", "requiresKey", "priority", "permissions", "alternatives", "unavailablePolicy",
46
+ ];
47
+ const MCP_REQUIREMENT_ALLOWED = new Set([...MCP_REQUIREMENT_REQUIRED, "credentialMetadata"]);
48
+
49
+ function validateCredentialMetadata(value, label) {
50
+ const allowed = new Set(["provider", "env", "allowedHosts", "scopes", "setupUrl", "brokerMode"]);
51
+ assertExactKeys(value, allowed, ["provider", "env"], label);
52
+ assertId(value.provider, `${label}.provider`);
53
+ if (!Array.isArray(value.env) || !value.env.length || new Set(value.env).size !== value.env.length || value.env.some((key) => !ENV_RE.test(String(key)))) {
54
+ throw new Error(`${label}.env must contain unique uppercase environment names`);
55
+ }
56
+ assertSafeText(value.provider, `${label}.provider`, 255);
57
+ value.env.forEach((key, index) => assertSafeText(key, `${label}.env[${index}]`, 255));
58
+ if (value.allowedHosts != null) {
59
+ if (!Array.isArray(value.allowedHosts) || !value.allowedHosts.length || new Set(value.allowedHosts).size !== value.allowedHosts.length) {
60
+ throw new Error(`${label}.allowedHosts must be a non-empty unique list`);
61
+ }
62
+ const hostRe = /^(?:\*\.)?[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$/;
63
+ for (const host of value.allowedHosts) {
64
+ if (typeof host !== "string" || host.length > 255 || !hostRe.test(host)) throw new Error(`${label}.allowedHosts contains an invalid host`);
65
+ assertSafeText(host, `${label}.allowedHosts`, 255);
66
+ }
67
+ }
68
+ if (value.scopes != null) {
69
+ if (!Array.isArray(value.scopes) || !value.scopes.length || new Set(value.scopes).size !== value.scopes.length) {
70
+ throw new Error(`${label}.scopes must be a non-empty unique list`);
71
+ }
72
+ for (const scope of value.scopes) {
73
+ if (!/^[A-Za-z0-9][A-Za-z0-9._:/@-]{0,127}$/.test(String(scope))) throw new Error(`${label}.scopes contains an invalid scope`);
74
+ assertSafeText(scope, `${label}.scopes`, 128);
75
+ }
76
+ }
77
+ if (value.setupUrl != null) {
78
+ let parsed;
79
+ try { parsed = new URL(value.setupUrl); } catch { throw new Error(`${label}.setupUrl must be a safe HTTPS provider page`); }
80
+ if (parsed.protocol !== "https:" || parsed.username || parsed.password || parsed.port || parsed.search || parsed.hash) {
81
+ throw new Error(`${label}.setupUrl must be HTTPS without userinfo, custom port, query, or fragment`);
82
+ }
83
+ if (!/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$/.test(parsed.hostname)) {
84
+ throw new Error(`${label}.setupUrl hostname is invalid`);
85
+ }
86
+ assertSafeText(value.setupUrl, `${label}.setupUrl`, 2048);
87
+ }
88
+ if (value.brokerMode != null && !["host-bound-broker", "runtime-env-injection", "provider-managed-oauth", "manual-provider-page"].includes(value.brokerMode)) {
89
+ throw new Error(`${label}.brokerMode is invalid`);
90
+ }
91
+ return value;
92
+ }
93
+
94
+ function validateMcpRequirement(value, label = "mcpRequirement") {
95
+ assertExactKeys(value, MCP_REQUIREMENT_ALLOWED, MCP_REQUIREMENT_REQUIRED, label);
96
+ if (value.schemaVersion !== "agentlas.mcp-requirement.v1" || value.kind !== "agentlas-mcp-requirement") {
97
+ throw new Error(`${label} has an unsupported schema`);
98
+ }
99
+ assertId(value.requirementId, `${label}.requirementId`);
100
+ assertId(value.catalogId, `${label}.catalogId`);
101
+ assertSafeText(value.reason, `${label}.reason`, 300);
102
+ const capabilities = assertUniqueIds(value.capabilities, `${label}.capabilities`, { min: 1 });
103
+ if (typeof value.required !== "boolean" || typeof value.requiresKey !== "boolean") throw new Error(`${label} required/requiresKey must be boolean`);
104
+ if (!Number.isInteger(value.priority) || value.priority < 1 || value.priority > 1000) throw new Error(`${label}.priority is invalid`);
105
+ const permissions = assertUniqueIds(value.permissions, `${label}.permissions`);
106
+ const alternatives = assertUniqueIds(value.alternatives, `${label}.alternatives`);
107
+ if (alternatives.includes(value.catalogId)) throw new Error(`${label}.alternatives must not contain the primary catalogId`);
108
+ [...capabilities, ...permissions, ...alternatives].forEach((text, index) => assertSafeText(text, `${label}.publicText[${index}]`, 255));
109
+ const unavailable = assertObject(value.unavailablePolicy, `${label}.unavailablePolicy`);
110
+ assertExactKeys(unavailable, new Set(["build", "rental", "execution"]), ["build", "rental", "execution"], `${label}.unavailablePolicy`);
111
+ if (unavailable.build !== "degrade") throw new Error(`${label} must degrade rather than abort a build`);
112
+ const expectedRental = value.required ? "exclude-variant" : "continue-degraded";
113
+ if (unavailable.rental !== expectedRental) throw new Error(`${label} rental policy must be ${expectedRental}`);
114
+ if (!["use-alternative", "disable-capability", "continue-degraded"].includes(unavailable.execution)) throw new Error(`${label} execution policy is invalid`);
115
+ if (value.credentialMetadata != null) validateCredentialMetadata(value.credentialMetadata, `${label}.credentialMetadata`);
116
+ if (value.requiresKey && value.credentialMetadata == null) throw new Error(`${label} requires credential metadata`);
117
+ return value;
118
+ }
119
+
120
+ function validateMcpPolicy(value) {
121
+ const required = [
122
+ "schemaVersion", "kind", "registryResolutionOrder", "consentMode", "serverDefinitionsFromPackage",
123
+ "credentialValuesAllowed", "failureIsolation", "permissionWidening", "toolSchemaLoading", "skillLoading",
124
+ "contextBudget", "requirements",
125
+ ];
126
+ assertExactKeys(value, new Set(required), required, "MCP policy");
127
+ if (value.schemaVersion !== "agentlas.mcp-policy.v1" || value.kind !== "agentlas-mcp-policy") throw new Error("MCP policy schema is invalid");
128
+ if (!Array.isArray(value.registryResolutionOrder) || value.registryResolutionOrder[0] !== "system-global") throw new Error("MCP policy must resolve system-global inventory first");
129
+ const allowedLayers = new Set(["system-global", "project-local", "catalog-recommendation"]);
130
+ if (new Set(value.registryResolutionOrder).size !== value.registryResolutionOrder.length || value.registryResolutionOrder.some((layer) => !allowedLayers.has(layer))) throw new Error("MCP policy registry order is invalid");
131
+ if (value.consentMode !== "one-pass" || value.serverDefinitionsFromPackage !== false || value.credentialValuesAllowed !== false || value.failureIsolation !== "per-requirement") throw new Error("MCP policy weakens the frozen safety boundary");
132
+ if (value.permissionWidening !== "ask" || value.toolSchemaLoading !== "selected-tools-only" || value.skillLoading !== "triggered-only") throw new Error("MCP policy loading/permission mode is invalid");
133
+ const budget = assertObject(value.contextBudget, "MCP policy.contextBudget");
134
+ assertExactKeys(budget, new Set(Object.keys(TOKEN_BUDGET)), Object.keys(TOKEN_BUDGET), "MCP policy.contextBudget");
135
+ for (const [key, max] of Object.entries(TOKEN_BUDGET)) {
136
+ if (!Number.isInteger(budget[key]) || budget[key] < 0 || budget[key] > max) throw new Error(`MCP policy.contextBudget.${key} exceeds the frozen maximum`);
137
+ }
138
+ if (!Array.isArray(value.requirements) || value.requirements.length > 64) throw new Error("MCP policy requirements are invalid");
139
+ value.requirements.forEach((requirement, index) => validateMcpRequirement(requirement, `MCP policy.requirements[${index}]`));
140
+ const requirementIds = value.requirements.map((requirement) => requirement.requirementId);
141
+ if (new Set(requirementIds).size !== requirementIds.length) throw new Error("MCP policy requirementId values must be unique");
142
+ return value;
143
+ }
144
+
145
+ function loadProjectMcpPolicy(cwd) {
146
+ const file = path.join(cwd || process.cwd(), ".agentlas", "mcp-policy.json");
147
+ if (!fs.existsSync(file)) return null;
148
+ const { value } = readJsonFile(file, "MCP policy");
149
+ return validateMcpPolicy(value);
150
+ }
151
+
152
+ /**
153
+ * 관찰 가능한 자격증명 "키 이름"만 수집한다(값은 절대 읽어 반환하지 않는다).
154
+ * 소스: 프로세스 env + userData/credentials.env + ~/.agentlas/credentials.env.
155
+ */
156
+ function readCredentialNames(userDataDir, env = process.env) {
157
+ const names = new Set(Object.keys(env || {}).filter((key) => ENV_RE.test(key) && env[key]));
158
+ const files = [
159
+ path.join(userDataDir, "credentials.env"),
160
+ path.join(os.homedir(), ".agentlas", "credentials.env"),
161
+ ];
162
+ for (const file of files) {
163
+ try {
164
+ const stat = fs.statSync(file);
165
+ if (!stat.isFile() || stat.size > 512 * 1024) continue;
166
+ for (const line of fs.readFileSync(file, "utf8").split(/\r?\n/)) {
167
+ const match = line.match(/^\s*(?:export\s+)?([A-Z][A-Z0-9_]*)\s*=\s*(.*?)\s*$/);
168
+ if (!match) continue;
169
+ let observed = match[2];
170
+ if ((observed.startsWith('"') && observed.endsWith('"')) || (observed.startsWith("'") && observed.endsWith("'"))) observed = observed.slice(1, -1);
171
+ if (observed) names.add(match[1]);
172
+ }
173
+ } catch { /* absent/unreadable means no observed key */ }
174
+ }
175
+ return names;
176
+ }
177
+
178
+ function collectSystemMcpInventory(db, options = {}) {
179
+ let rows = [];
180
+ let registryStatus = "complete";
181
+ try {
182
+ rows = db.prepare("SELECT id, catalog_id, name, name_en, transport, env_keys_json, enabled FROM mcp_servers ORDER BY installed_at ASC LIMIT 1025").all();
183
+ } catch {
184
+ // 읽을 수 없는 레지스트리는 "읽었더니 비어 있음"과 다른 사실이다.
185
+ // 둘 다 empty-MCP로 fail-closed하지만, 사용자 플랜에는 원인을 보존한다.
186
+ registryStatus = "unavailable";
187
+ }
188
+ if (!Array.isArray(rows)) {
189
+ rows = [];
190
+ registryStatus = "unavailable";
191
+ }
192
+ if (rows.length > 1024) {
193
+ rows = [];
194
+ registryStatus = "unavailable";
195
+ }
196
+ const credentialNames = readCredentialNames(options.userDataDir || "", options.env || process.env);
197
+ const inventory = [];
198
+ const seen = new Set();
199
+ for (const row of rows) {
200
+ if (!row || Number(row.enabled) === 0) continue;
201
+ const catalogId = safeCatalogId(row.catalog_id) || safeCatalogId(row.id);
202
+ if (!catalogId || seen.has(catalogId)) continue;
203
+ seen.add(catalogId);
204
+ let keyNames = [];
205
+ let credentialMetadataStatus = "complete";
206
+ try {
207
+ if (String(row.env_keys_json || "[]").length > 64 * 1024) throw new Error("credential metadata too large");
208
+ const parsed = JSON.parse(row.env_keys_json || "[]");
209
+ keyNames = normalizeCredentialKeyNames(parsed);
210
+ } catch { credentialMetadataStatus = "unavailable"; }
211
+ const item = {
212
+ catalogId,
213
+ name: safeDisplayName(row.name || row.name_en, catalogId),
214
+ source: "system-global",
215
+ enabled: true,
216
+ keyRequired: credentialMetadataStatus !== "complete" || keyNames.length > 0,
217
+ keyPresent: credentialMetadataStatus === "complete" && (keyNames.length === 0 || keyNames.every((key) => credentialNames.has(key))),
218
+ credentialMetadataStatus,
219
+ };
220
+ // 실행/지문 재료는 공개 투영(JSON.stringify)에 절대 실리지 않도록 non-enumerable.
221
+ Object.defineProperty(item, "registryServerId", { value: String(row.id), enumerable: false });
222
+ Object.defineProperty(item, "transport", { value: String(row.transport || ""), enumerable: false });
223
+ Object.defineProperty(item, "credentialKeyNames", { value: keyNames, enumerable: false });
224
+ Object.defineProperty(item, "credentialKeyFingerprint", {
225
+ value: crypto.createHash("sha256").update(JSON.stringify(keyNames), "utf8").digest("hex"),
226
+ enumerable: false,
227
+ });
228
+ inventory.push(item);
229
+ }
230
+ Object.defineProperty(inventory, "registryStatus", { value: registryStatus, enumerable: false });
231
+ return inventory;
232
+ }
233
+
234
+ /**
235
+ * 신뢰 시스템 전역 레지스트리 행 → 실행 가능한 서버 오브젝트.
236
+ * 안전하지 않은 행(비-stdio, 민감 인자, 잘못된 자격 메타데이터)은 null로 fail-closed.
237
+ * consentFingerprint는 "무엇에 동의했는가"의 정확한 지문 — 명령/인자/키이름이
238
+ * 하나라도 바뀌면 기존 동의가 무효가 되는 근거다.
239
+ */
240
+ function materializeTrustedSystemMcpServer(row, options = {}) {
241
+ const catalogId = safeCatalogId(row?.catalog_id) || safeCatalogId(row?.id);
242
+ const registryServerId = String(row?.id || "");
243
+ const args = parseRuntimeServerArgs(row?.args_json || "[]");
244
+ let credentialKeyNames = null;
245
+ try {
246
+ if (String(row?.env_keys_json || "[]").length > 64 * 1024) throw new Error("credential metadata too large");
247
+ credentialKeyNames = normalizeCredentialKeyNames(JSON.parse(row?.env_keys_json || "[]"));
248
+ } catch { /* fail closed below */ }
249
+ if (
250
+ !row || !catalogId || !ID_RE.test(registryServerId) || Number(row.enabled) === 0 || row.transport !== "stdio" || typeof row.command !== "string" ||
251
+ !row.command.trim() || row.command.length > 4096 || /[\u0000\r\n]/.test(row.command) || !args ||
252
+ !credentialKeyNames || hasSensitiveRuntimeArgument(row.command, args)
253
+ ) return null;
254
+ const server = {
255
+ id: registryServerId,
256
+ catalog_id: catalogId,
257
+ name: catalogId,
258
+ transport: "stdio",
259
+ command: row.command,
260
+ args_json: JSON.stringify(args),
261
+ enabled: 1,
262
+ };
263
+ Object.defineProperty(server, "credentialKeyNames", { value: credentialKeyNames, enumerable: false });
264
+ Object.defineProperty(server, "credentialKeyFingerprint", {
265
+ value: crypto.createHash("sha256").update(JSON.stringify(credentialKeyNames), "utf8").digest("hex"),
266
+ enumerable: false,
267
+ });
268
+ Object.defineProperty(server, "consentFingerprint", {
269
+ value: crypto.createHash("sha256").update(JSON.stringify({
270
+ schemaVersion: "agentlas.terminal-mcp-consent-fingerprint.v1",
271
+ registryServerId,
272
+ catalogId,
273
+ transport: "stdio",
274
+ command: row.command,
275
+ args,
276
+ credentialKeyNames,
277
+ }), "utf8").digest("hex"),
278
+ enumerable: false,
279
+ });
280
+ if (options.createRuntimeHome !== false) {
281
+ Object.defineProperty(server, "mcpRuntimeHome", {
282
+ value: mcpRuntimeHome(options.userDataDir, `${catalogId}\u0000${row.id}`),
283
+ enumerable: false,
284
+ });
285
+ }
286
+ return server;
287
+ }
288
+
289
+ /**
290
+ * Post-consent only. 정확한 신뢰 시스템 전역 행을 실행 필드까지 다시 읽는다.
291
+ * 패키지/카탈로그 콘텐츠는 이 재료를 공급할 수 없다. 동의 후 자격 메타데이터가
292
+ * 넓어졌다면(지문 불일치) 실행 전에 실패해야 한다.
293
+ */
294
+ function readApprovedSystemMcpServer(db, entry, options = {}) {
295
+ if (!entry?.registryServerId || !entry.resolvedCatalogId) return null;
296
+ let row = null;
297
+ try {
298
+ row = db.prepare(
299
+ "SELECT id, catalog_id, name, name_en, transport, command, args_json, env_keys_json, enabled FROM mcp_servers WHERE id=? LIMIT 1",
300
+ ).get(entry.registryServerId);
301
+ } catch {
302
+ return null;
303
+ }
304
+ const server = materializeTrustedSystemMcpServer(row, options);
305
+ if (
306
+ !server || String(row.id) !== entry.registryServerId || server.catalog_id !== entry.resolvedCatalogId ||
307
+ !entry.credentialKeyFingerprint || server.credentialKeyFingerprint !== entry.credentialKeyFingerprint
308
+ ) return null;
309
+ return server;
310
+ }
311
+
312
+ module.exports = {
313
+ TOKEN_BUDGET,
314
+ validateCredentialMetadata,
315
+ validateMcpRequirement,
316
+ validateMcpPolicy,
317
+ loadProjectMcpPolicy,
318
+ readCredentialNames,
319
+ collectSystemMcpInventory,
320
+ materializeTrustedSystemMcpServer,
321
+ readApprovedSystemMcpServer,
322
+ };