agentlas 0.9.9 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/README.md +319 -342
  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 +318 -8
  19. package/engine/agentlas-workload-routing.cjs +8 -3
  20. package/engine/agentlas.cjs +140 -12336
  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 +3 -7
  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,286 @@
1
+ "use strict";
2
+ /*
3
+ * mcp/plan — 요구사항 해소 + MCP 빌드 플랜 구성 + 빌더 지시문.
4
+ *
5
+ * 계약(v1 그대로):
6
+ * - 플랜은 추천일 뿐이다. 명시적 1회 동의 전에는 어떤 MCP도 attach되지 않는다.
7
+ * - 네트워크 발견/키 프로브/설치는 절대 수행하지 않는다.
8
+ * - 부족(shortage)은 요구사항 단위로 격리된다 — 빌드는 중단되지 않고 degrade만 한다.
9
+ * - 신뢰 레지스트리가 자격증명 매핑을 소유한다: 패키지가 env 메타데이터 선언만으로
10
+ * 무자격 레지스트리 행을 "key present"로 둔갑시킬 수 없다.
11
+ */
12
+ const crypto = require("node:crypto");
13
+ const {
14
+ MAX_APPROVED_MCP_PER_BUILD,
15
+ assertId,
16
+ } = require("./contract.cjs");
17
+
18
+ const MAX_BUILD_DIRECTIVE_CHARS = 1400;
19
+
20
+ function syntheticRequirement(catalogId, required, priority) {
21
+ const suffix = crypto.createHash("sha256").update(catalogId).digest("hex").slice(0, 24);
22
+ return {
23
+ schemaVersion: "agentlas.mcp-requirement.v1",
24
+ kind: "agentlas-mcp-requirement",
25
+ requirementId: `terminal-requirement:${suffix}`,
26
+ catalogId,
27
+ reason: required ? "Explicitly required for this Terminal build" : "Explicitly recommended for this Terminal build",
28
+ capabilities: [`terminal-mcp:${suffix}`],
29
+ required,
30
+ requiresKey: false,
31
+ priority,
32
+ permissions: [],
33
+ alternatives: [],
34
+ unavailablePolicy: {
35
+ build: "degrade",
36
+ rental: required ? "exclude-variant" : "continue-degraded",
37
+ execution: required ? "use-alternative" : "continue-degraded",
38
+ },
39
+ };
40
+ }
41
+
42
+ // 이름 휴리스틱은 "추천 후보 회수"만 담당한다 — 최종 선택/attach 권한이 아니다.
43
+ const HEURISTIC_GROUPS = [
44
+ [/(browser|playwright|chrome|web)/i, /(?:browser|website|web page|웹|브라우저|사이트|페이지|로그인)/i],
45
+ [/(github|gitlab|source)/i, /(?:github|gitlab|repository|pull request|issue|깃허브|레포|저장소)/i],
46
+ [/(figma|design)/i, /(?:figma|mockup|design|ui|ux|피그마|디자인)/i],
47
+ [/(postgres|mysql|sqlite|database|mongo)/i, /(?:database|sql|query|schema|db|데이터베이스|쿼리)/i],
48
+ [/(notion|docs|drive)/i, /(?:notion|document|docs|drive|노션|문서|드라이브)/i],
49
+ [/(slack|teams|discord)/i, /(?:slack|teams|discord|message|슬랙|메시지)/i],
50
+ [/(search|research)/i, /(?:search|research|lookup|검색|리서치|조사)/i],
51
+ ];
52
+
53
+ function inferRequirements(request, inventory) {
54
+ const text = String(request || "");
55
+ const results = [];
56
+ for (const item of inventory) {
57
+ const direct = text.toLowerCase().includes(item.catalogId.toLowerCase()) || text.toLowerCase().includes(item.name.toLowerCase());
58
+ const heuristic = HEURISTIC_GROUPS.some(([nameRe, taskRe]) => nameRe.test(`${item.catalogId} ${item.name}`) && taskRe.test(text));
59
+ if (direct || heuristic) results.push(syntheticRequirement(item.catalogId, false, results.length + 100));
60
+ }
61
+ return results.slice(0, 8);
62
+ }
63
+
64
+ function indexInventory(inventory) {
65
+ return new Map((inventory || []).map((item) => [item.catalogId, item]));
66
+ }
67
+
68
+ function resolveMcpRequirement(requirement, inventoryById) {
69
+ const order = [requirement.catalogId, ...(requirement.alternatives || [])];
70
+ const attempted = [];
71
+ const candidates = [];
72
+ for (const catalogId of order) {
73
+ const item = inventoryById.get(catalogId);
74
+ if (!item) {
75
+ attempted.push({ catalogId, status: "unavailable" });
76
+ continue;
77
+ }
78
+ if (item.transport !== "stdio") {
79
+ attempted.push({ catalogId, status: "runtime-incompatible" });
80
+ continue;
81
+ }
82
+ const keyRequired = requirement.requiresKey || item.keyRequired;
83
+ // The trusted registry owns credential mapping. A package cannot turn an
84
+ // uncredentialed registry row into "key present" merely by declaring env metadata.
85
+ const keyPresent = keyRequired ? (item.keyRequired && item.keyPresent) : true;
86
+ if (!keyPresent) {
87
+ attempted.push({ catalogId, status: "missing-key" });
88
+ continue;
89
+ }
90
+ candidates.push({ item, keyRequired, keyPresent: true });
91
+ }
92
+ if (candidates.length) {
93
+ return {
94
+ selected: candidates[0].item,
95
+ candidates,
96
+ status: "available",
97
+ attempted,
98
+ keyRequired: candidates[0].keyRequired,
99
+ keyPresent: true,
100
+ };
101
+ }
102
+ const primary = inventoryById.get(requirement.catalogId);
103
+ const keyRequired = requirement.requiresKey || Boolean(primary && primary.keyRequired);
104
+ const missingKey = attempted.some((attempt) => attempt.status === "missing-key");
105
+ return { selected: null, candidates: [], status: missingKey ? "missing-key" : "unavailable", attempted, keyRequired, keyPresent: false };
106
+ }
107
+
108
+ function buildMcpPlan(options) {
109
+ const inventory = options.inventory || [];
110
+ const inventoryById = indexInventory(inventory);
111
+ const policyRequirements = options.policy ? options.policy.requirements : [];
112
+ const requirements = [...policyRequirements];
113
+ const known = new Set(requirements.map((requirement) => requirement.catalogId));
114
+ for (const catalogId of options.requiredIds || []) {
115
+ assertId(catalogId, "--require-mcp");
116
+ if (!known.has(catalogId)) { requirements.push(syntheticRequirement(catalogId, true, 1)); known.add(catalogId); }
117
+ }
118
+ for (const catalogId of options.recommendedIds || []) {
119
+ assertId(catalogId, "--recommend-mcp");
120
+ if (!known.has(catalogId)) { requirements.push(syntheticRequirement(catalogId, false, 500)); known.add(catalogId); }
121
+ }
122
+ if (!requirements.length) requirements.push(...inferRequirements(options.request, inventory));
123
+ const entries = requirements
124
+ .map((requirement) => {
125
+ const resolution = resolveMcpRequirement(requirement, inventoryById);
126
+ const entry = {
127
+ requirementId: requirement.requirementId,
128
+ requestedCatalogId: requirement.catalogId,
129
+ resolvedCatalogId: resolution.selected ? resolution.selected.catalogId : null,
130
+ name: resolution.selected ? resolution.selected.name : requirement.catalogId,
131
+ source: resolution.selected ? resolution.selected.source : null,
132
+ required: requirement.required,
133
+ priority: requirement.priority,
134
+ reason: requirement.reason,
135
+ status: resolution.status,
136
+ keyRequired: resolution.keyRequired,
137
+ keyPresent: resolution.keyRequired ? resolution.keyPresent : null,
138
+ permissions: [...(requirement.permissions || [])],
139
+ permissionBasis: "package-declared",
140
+ permissionEnforced: false,
141
+ fallbackCatalogIds: resolution.candidates.slice(1).map((candidate) => candidate.item.catalogId),
142
+ alternativesTried: resolution.attempted.map((attempt) => ({ catalogId: attempt.catalogId, status: attempt.status })),
143
+ unavailableBuildPolicy: "degrade",
144
+ };
145
+ // 레지스트리 행 id/지문은 post-consent 재검증 재료 — 공개 투영에서 제외.
146
+ Object.defineProperty(entry, "registryServerId", {
147
+ value: resolution.selected?.registryServerId || null,
148
+ enumerable: false,
149
+ });
150
+ Object.defineProperty(entry, "credentialKeyFingerprint", {
151
+ value: resolution.selected?.credentialKeyFingerprint || null,
152
+ enumerable: false,
153
+ });
154
+ Object.defineProperty(entry, "runtimeCandidates", {
155
+ value: resolution.candidates.map((candidate) => ({
156
+ resolvedCatalogId: candidate.item.catalogId,
157
+ registryServerId: candidate.item.registryServerId || null,
158
+ credentialKeyFingerprint: candidate.item.credentialKeyFingerprint || null,
159
+ })),
160
+ enumerable: false,
161
+ });
162
+ return entry;
163
+ })
164
+ .sort((a, b) => Number(b.required) - Number(a.required) || a.priority - b.priority || a.requestedCatalogId.localeCompare(b.requestedCatalogId));
165
+ return {
166
+ schemaVersion: "agentlas.terminal-mcp-build-plan.v1",
167
+ planId: crypto.randomUUID(),
168
+ registryStatus: options.registryStatus || inventory.registryStatus || "complete",
169
+ registryResolutionOrder: options.policy ? [...options.policy.registryResolutionOrder] : ["system-global"],
170
+ discoveryNetworkUsed: false,
171
+ consentMode: "one-pass",
172
+ entries,
173
+ availableCatalogIds: [...new Set(entries.flatMap((entry) =>
174
+ entry.status === "available" ? (entry.runtimeCandidates || []).map((candidate) => candidate.resolvedCatalogId) : []
175
+ ))],
176
+ maxApprovedMcp: MAX_APPROVED_MCP_PER_BUILD,
177
+ shortages: entries.filter((entry) => entry.status !== "available").map((entry) => ({
178
+ requirementId: entry.requirementId,
179
+ catalogId: entry.requestedCatalogId,
180
+ required: entry.required,
181
+ status: entry.status,
182
+ effect: "build-degraded-only",
183
+ })),
184
+ };
185
+ }
186
+
187
+ function renderMcpPlan(plan) {
188
+ const lines = [`MCP BUILD PLAN · system-global registry first · registry: ${plan.registryStatus} · no network discovery`];
189
+ if (plan.registryStatus === "unavailable") lines.push("- System-global registry could not be read. Build continues safely in empty-MCP mode; no install/network fallback was attempted.");
190
+ if (!plan.entries.length) lines.push("- No relevant MCP recommended. Build continues in empty-MCP mode.");
191
+ for (const entry of plan.entries) {
192
+ const key = entry.keyRequired ? (entry.keyPresent ? "key: present" : "key: missing") : "key: not needed";
193
+ const requirement = entry.required ? "required" : "optional";
194
+ const permissions = entry.permissions.length ? entry.permissions.join(",") : "none";
195
+ lines.push(`- P${entry.priority} ${entry.name} [${entry.resolvedCatalogId || entry.requestedCatalogId}] · ${requirement} · ${entry.status} · ${key}`);
196
+ lines.push(` ${entry.reason}`);
197
+ if (entry.fallbackCatalogIds?.length) lines.push(` approved fallback order: ${entry.fallbackCatalogIds.join(",")}`);
198
+ lines.push(` permissions: ${permissions} · declared only; host enforcement not yet verified`);
199
+ }
200
+ if (plan.shortages.length) lines.push(`Shortages are isolated: ${plan.shortages.length} requirement(s) degrade only; the build does not abort.`);
201
+ lines.push("Recommendation only: no MCP is attached until one explicit consent; this plan performs no network key probe or install.");
202
+ return lines.join("\n");
203
+ }
204
+
205
+ function fitApprovedMcpIds(plan, requestedIds) {
206
+ const available = new Set(plan.availableCatalogIds || []);
207
+ const requested = [...new Set((requestedIds || []).filter((id) => available.has(id)))].slice(0, MAX_APPROVED_MCP_PER_BUILD);
208
+ const accepted = [];
209
+ const fixedReserve = 520; // frozen instruction + minimum shortage/degrade clause
210
+ for (const id of requested) {
211
+ const clause = `Approved catalog IDs: ${accepted.concat(id).join(",")}.`;
212
+ if (clause.length + fixedReserve > MAX_BUILD_DIRECTIVE_CHARS) break;
213
+ accepted.push(id);
214
+ }
215
+ return accepted;
216
+ }
217
+
218
+ function buildMcpDirective(plan, approvedIds) {
219
+ const approved = fitApprovedMcpIds(plan, approvedIds);
220
+ const shortages = plan.shortages.map((item) => item.catalogId);
221
+ const base = [
222
+ "[AGENTLAS_MCP_BUILD_CONTEXT v1]",
223
+ "Resolve MCP only from the host system-global registry; package IDs/requirements only, never server definitions or credentials.",
224
+ ].join(" ");
225
+ const approvedClause = `Approved catalog IDs: ${approved.length ? approved.join(",") : "none"}.`;
226
+ const shortagePrefix = "Unavailable or missing-key IDs: ";
227
+ const shortageSuffix = "; degrade each capability independently and continue the build.";
228
+ const fittedShortages = [];
229
+ for (const id of shortages.slice(0, 16)) {
230
+ const omitted = shortages.length - fittedShortages.length - 1;
231
+ const proposal = `${shortagePrefix}${fittedShortages.concat(id).join(",")}${omitted > 0 ? ` (+${omitted} more)` : ""}${shortageSuffix}`;
232
+ if (`${base} ${approvedClause} ${proposal}`.length > MAX_BUILD_DIRECTIVE_CHARS) break;
233
+ fittedShortages.push(id);
234
+ }
235
+ const omittedShortages = shortages.length - fittedShortages.length;
236
+ const shortageValue = shortages.length === 0
237
+ ? "none"
238
+ : fittedShortages.length
239
+ ? `${fittedShortages.join(",")}${omittedShortages > 0 ? ` (+${omittedShortages} more)` : ""}`
240
+ : `${shortages.length} unresolved (IDs omitted from prompt; declared policy remains source)`;
241
+ const shortageClause = `${shortagePrefix}${shortageValue}${shortageSuffix}`;
242
+ const line = `${base} ${approvedClause} ${shortageClause}`;
243
+ if (line.length > MAX_BUILD_DIRECTIVE_CHARS) throw new Error("internal MCP builder directive exceeded its context limit");
244
+ return line;
245
+ }
246
+
247
+ function renderBuildMcpResult(plan, approvedIds, runtimeAllowlist = null) {
248
+ const approved = new Set(approvedIds || []);
249
+ const attached = new Set((runtimeAllowlist?.attached || []).map((item) => item.catalogId));
250
+ const failed = new Map((runtimeAllowlist?.failed || []).map((item) => [item.catalogId, item.reason]));
251
+ const lines = ["MCP BUILD RESULT"];
252
+ for (const entry of plan.entries) {
253
+ let status = entry.status;
254
+ if (entry.status === "available") {
255
+ const candidateIds = [entry.resolvedCatalogId, ...(entry.fallbackCatalogIds || [])].filter(Boolean);
256
+ const attachedId = candidateIds.find((id) => attached.has(id));
257
+ const approvedId = candidateIds.find((id) => approved.has(id));
258
+ const failedId = candidateIds.find((id) => failed.has(id));
259
+ status = attachedId
260
+ ? attachedId === entry.resolvedCatalogId ? "connected-and-allowlisted" : `fallback-connected-and-allowlisted:${attachedId}`
261
+ : failedId
262
+ ? `failed-isolated:${failedId}:${failed.get(failedId)}`
263
+ : approvedId
264
+ ? "approved-but-not-attached"
265
+ : "skipped";
266
+ }
267
+ lines.push(`- ${entry.resolvedCatalogId || entry.requestedCatalogId}: ${status}`);
268
+ }
269
+ if (!runtimeAllowlist || runtimeAllowlist.emptyMode) lines.push("- Build continued in empty-MCP mode.");
270
+ if (runtimeAllowlist && runtimeAllowlist.consentPersisted === false) lines.push("- Runtime consent was one-pass only because its local fingerprint receipt could not be saved.");
271
+ lines.push("Only the post-consent host allowlist reached the builder; tool-call success is not implied by connection readiness.");
272
+ return lines.join("\n");
273
+ }
274
+
275
+ module.exports = {
276
+ MAX_BUILD_DIRECTIVE_CHARS,
277
+ syntheticRequirement,
278
+ inferRequirements,
279
+ indexInventory,
280
+ resolveMcpRequirement,
281
+ buildMcpPlan,
282
+ renderMcpPlan,
283
+ fitApprovedMcpIds,
284
+ buildMcpDirective,
285
+ renderBuildMcpResult,
286
+ };
@@ -0,0 +1,151 @@
1
+ "use strict";
2
+ /*
3
+ * mcp/probe — stdio MCP 서버 연결 프리플라이트.
4
+ *
5
+ * initialize → notifications/initialized → tools/list 핸드셰이크만 수행하고
6
+ * 즉시 종료한다. 툴 호출은 절대 하지 않는다("연결됨 ≠ 툴 성공" 계약).
7
+ * 자식 env는 agentlas-mcp-env의 buildMcpChildEnv 경계(agentlas.mcp-child-launch.v1)
8
+ * 를 그대로 재사용한다 — 여기서 env를 따로 구성하면 격리 계약이 깨진다.
9
+ */
10
+ const { spawn } = require("node:child_process");
11
+ const { buildMcpChildEnv, mcpRuntimeHome } = require("../agentlas-mcp-env.cjs");
12
+ const { parseRuntimeServerArgs } = require("./contract.cjs");
13
+
14
+ const MCP_PROBE_CONCURRENCY = 3;
15
+ const MCP_PROBE_PER_SERVER_TIMEOUT_MS = 8_000;
16
+ const MCP_PROBE_TOTAL_TIMEOUT_MS = 12_000;
17
+
18
+ function probeSystemMcpServerConnection(server, options = {}) {
19
+ const requestedTimeout = Number(options.timeoutMs);
20
+ const timeoutMs = Number.isFinite(requestedTimeout)
21
+ ? Math.max(50, Math.min(30_000, Math.trunc(requestedTimeout)))
22
+ : MCP_PROBE_PER_SERVER_TIMEOUT_MS;
23
+ const spawnImpl = options.spawn || spawn;
24
+ return new Promise((resolve) => {
25
+ let child = null;
26
+ let settled = false;
27
+ let buffer = Buffer.alloc(0);
28
+ let totalBytes = 0;
29
+ let initialized = false;
30
+ let abortHandler = null;
31
+ let forceKillTimer = null;
32
+ let childClosed = false;
33
+ const terminateChild = (signal) => {
34
+ const pid = Number(child?.pid);
35
+ // detached 자식은 프로세스 그룹(-pid)째로 종료해야 패키지 매니저가 띄운
36
+ // 손자 프로세스가 고아로 남지 않는다.
37
+ if (process.platform !== "win32" && Number.isInteger(pid) && pid > 1) {
38
+ try { process.kill(-pid, signal); return; } catch { /* fall through */ }
39
+ }
40
+ try { child?.kill(signal); } catch { /* noop */ }
41
+ };
42
+ const finish = (connected, reason, tools = []) => {
43
+ if (settled) return;
44
+ settled = true;
45
+ clearTimeout(timer);
46
+ if (options.signal && abortHandler) options.signal.removeEventListener?.("abort", abortHandler);
47
+ try { child?.stdin?.end(); } catch { /* noop */ }
48
+ if (!childClosed) {
49
+ terminateChild("SIGTERM");
50
+ forceKillTimer = setTimeout(() => terminateChild("SIGKILL"), 250);
51
+ forceKillTimer.unref?.();
52
+ }
53
+ const result = { connected, reason };
54
+ // tools 목록은 프리플라이트 참고 정보일 뿐 공개 투영에 실리면 안 된다.
55
+ Object.defineProperty(result, "tools", {
56
+ value: Array.isArray(tools) ? tools : [],
57
+ enumerable: false,
58
+ });
59
+ resolve(result);
60
+ };
61
+ const onMessage = (message) => {
62
+ if (!message || message.jsonrpc !== "2.0") return;
63
+ if (message.id === 1) {
64
+ if (message.error || !message.result) return finish(false, "initialize_failed");
65
+ initialized = true;
66
+ try {
67
+ child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized", params: {} })}\n`);
68
+ child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id: 2, method: "tools/list", params: {} })}\n`);
69
+ } catch {
70
+ finish(false, "connection_failed");
71
+ }
72
+ } else if (message.id === 2 && initialized) {
73
+ finish(
74
+ !message.error && Boolean(message.result),
75
+ message.error ? "tools_list_failed" : "connected",
76
+ message.result?.tools,
77
+ );
78
+ }
79
+ };
80
+ const drain = () => {
81
+ // 서버가 Content-Length 프레이밍과 개행 구분 JSON 중 무엇을 쓰든 수용한다.
82
+ while (buffer.length) {
83
+ const header = buffer.toString("ascii", 0, Math.min(buffer.length, 64 * 1024)).match(/^Content-Length:\s*(\d+)\r?\n\r?\n/i);
84
+ if (header) {
85
+ const headerBytes = Buffer.byteLength(header[0], "ascii");
86
+ const bodyBytes = Number(header[1]);
87
+ if (!Number.isSafeInteger(bodyBytes) || bodyBytes < 0 || bodyBytes > 1024 * 1024) return finish(false, "invalid_protocol_frame");
88
+ if (buffer.length < headerBytes + bodyBytes) return;
89
+ const body = buffer.subarray(headerBytes, headerBytes + bodyBytes).toString("utf8");
90
+ buffer = buffer.subarray(headerBytes + bodyBytes);
91
+ try { onMessage(JSON.parse(body)); } catch { /* ignore non-JSON noise */ }
92
+ continue;
93
+ }
94
+ const newline = buffer.indexOf(0x0a);
95
+ if (newline < 0) return;
96
+ const line = buffer.subarray(0, newline).toString("utf8").trim();
97
+ buffer = buffer.subarray(newline + 1);
98
+ if (!line || /^Content-Length:/i.test(line)) continue;
99
+ try { onMessage(JSON.parse(line)); } catch { /* ignore banners */ }
100
+ }
101
+ };
102
+ const timer = setTimeout(() => finish(false, "connection_timeout"), timeoutMs);
103
+ try {
104
+ child = spawnImpl(server.command, parseRuntimeServerArgs(server.args_json) || [], {
105
+ cwd: options.cwd || process.cwd(),
106
+ env: buildMcpChildEnv(options.env || process.env, server.credentialKeyNames || [], {
107
+ runtimeHome: server.mcpRuntimeHome || mcpRuntimeHome(options.userDataDir, server.catalog_id || server.id || server.command),
108
+ }),
109
+ detached: process.platform !== "win32",
110
+ stdio: ["pipe", "pipe", "ignore"],
111
+ });
112
+ child.once("error", () => finish(false, "connection_failed"));
113
+ child.once("close", () => {
114
+ childClosed = true;
115
+ if (forceKillTimer) clearTimeout(forceKillTimer);
116
+ forceKillTimer = null;
117
+ finish(false, "connection_closed");
118
+ });
119
+ child.stdout.on("data", (chunk) => {
120
+ totalBytes += chunk.length;
121
+ if (totalBytes > 1024 * 1024) return finish(false, "protocol_output_limit");
122
+ buffer = Buffer.concat([buffer, Buffer.from(chunk)]);
123
+ drain();
124
+ });
125
+ child.stdin.write(`${JSON.stringify({
126
+ jsonrpc: "2.0",
127
+ id: 1,
128
+ method: "initialize",
129
+ params: {
130
+ protocolVersion: "2024-11-05",
131
+ capabilities: {},
132
+ clientInfo: { name: "agentlas-terminal-build", version: "1" },
133
+ },
134
+ })}\n`);
135
+ if (options.signal) {
136
+ abortHandler = () => finish(false, "connection_timeout");
137
+ if (options.signal.aborted) abortHandler();
138
+ else options.signal.addEventListener?.("abort", abortHandler, { once: true });
139
+ }
140
+ } catch {
141
+ finish(false, "connection_failed");
142
+ }
143
+ });
144
+ }
145
+
146
+ module.exports = {
147
+ MCP_PROBE_CONCURRENCY,
148
+ MCP_PROBE_PER_SERVER_TIMEOUT_MS,
149
+ MCP_PROBE_TOTAL_TIMEOUT_MS,
150
+ probeSystemMcpServerConnection,
151
+ };
@@ -0,0 +1,163 @@
1
+ "use strict";
2
+
3
+ /*
4
+ * memory-cli/curate — 어시스턴트 응답의 `## Memory Events` 큐레이션 파서/게이트.
5
+ *
6
+ * v1 모놀리스(engine/agentlas.cjs)의 parseMemoryEventsCli/curateCliReply
7
+ * 슬라이스를 이식했다. 큐레이터(LLM)는 후보를 "제안"만 하고, 여기의 결정적
8
+ * 게이트가 실제 durable 쓰기를 결정한다(제안 ≠ 승인).
9
+ *
10
+ * 불변식(v1 그대로):
11
+ * - permission=read 턴은 어떤 durable 쓰기도 하지 않는다(응답 정리만).
12
+ * - SECRET_RE에 걸리는 후보와 sensitivity=secret 후보는 무조건 버린다.
13
+ * - user_identity 승격은 confidence=high + 허용 kind에서만; 아니면 session 강등.
14
+ * - session/discard는 프로젝트 로그(memory-log.jsonl)로만 남는다.
15
+ * - 중복(scope+kind+content+project) 행은 재삽입하지 않고 기존 행을 재사용한다.
16
+ */
17
+
18
+ const fs = require("node:fs");
19
+ const path = require("node:path");
20
+ const { tableExists, columnExists } = require("../core/db.cjs");
21
+
22
+ // 앱과 동일한 컴파일된 manifest — 빌트인 에이전트 + 메모리 아키텍처 상수.
23
+ let _arch = null;
24
+ function loadArch() {
25
+ if (_arch) return _arch;
26
+ try {
27
+ _arch = require("../architecture.data.json");
28
+ } catch {
29
+ _arch = { version: "0", agents: [], emitterBlock: "", eventsHeading: "## Memory Events", memoryDir: ".agentlas", soulFile: "project-soul-memory.md", sitemapFile: "sitemap.json", logFile: "memory-log.jsonl", kinds: [], scopes: [] };
30
+ }
31
+ return _arch;
32
+ }
33
+
34
+ function ensureMemoryContextColumn(db) {
35
+ try {
36
+ if (tableExists(db, "memory_entries") && !columnExists(db, "memory_entries", "context_json")) {
37
+ db.exec("ALTER TABLE memory_entries ADD COLUMN context_json TEXT NOT NULL DEFAULT '{}'");
38
+ }
39
+ } catch { /* ignore */ }
40
+ }
41
+
42
+ const SECRET_RE = [/\b(?:sk|pk|rk)-[A-Za-z0-9]{16,}/, /AKIA[0-9A-Z]{16}/, /ghp_[A-Za-z0-9]{20,}/, /xox[baprs]-[A-Za-z0-9-]{10,}/, /-----BEGIN [A-Z ]*PRIVATE KEY-----/, /\b(?:password|passwd|secret|api[_-]?key|access[_-]?token|bearer)\b\s*[:=]\s*\S+/i];
43
+
44
+ /*
45
+ * 프로젝트 메모리 로그 한 줄 추가. v1은 ensureProjectMemoryCli(전체 프로젝트
46
+ * 스캐폴딩: soul/sitemap/ontology 등)를 거쳤지만, 그 스캐폴딩은 v2에서 프로젝트
47
+ * 부트스트랩 모듈 소관이다. 큐레이션 관점의 계약(=.agentlas/memory-log.jsonl에
48
+ * 결정 로그가 남는다)만 여기서 보장한다 — 로그 누락은 없고, 스캐폴딩 생성은
49
+ * 이 모듈이 위장하지 않는다.
50
+ */
51
+ function logCli(projectPath, rec) {
52
+ if (!projectPath) return;
53
+ try {
54
+ const arch = loadArch();
55
+ const dir = path.join(projectPath, arch.memoryDir);
56
+ fs.mkdirSync(dir, { recursive: true });
57
+ fs.appendFileSync(path.join(dir, arch.logFile), JSON.stringify(rec) + "\n", "utf8");
58
+ } catch { /* ignore */ }
59
+ }
60
+
61
+ function coerceText(v, max) {
62
+ if (typeof v !== "string") return undefined;
63
+ const s = v.trim();
64
+ return s ? s.slice(0, max) : undefined;
65
+ }
66
+
67
+ function coerceNullableText(v, max) {
68
+ if (v === null) return null;
69
+ return coerceText(v, max);
70
+ }
71
+
72
+ function normalizeRequestContext(ev, ctx, projectPath) {
73
+ const raw = ev && ev.request_context && typeof ev.request_context === "object" ? ev.request_context : {};
74
+ const triggerTerms = Array.isArray(raw.trigger_terms)
75
+ ? [...new Set(raw.trigger_terms.filter((x) => typeof x === "string").map((x) => x.trim()).filter(Boolean))]
76
+ .slice(0, 12)
77
+ .map((x) => x.slice(0, 40))
78
+ : undefined;
79
+ const cwd = coerceNullableText(raw.cwd_at_request, 500) ?? ctx.cwdAtRequest ?? ctx.cwd ?? ctx.projectPath ?? null;
80
+ const targetProject = coerceNullableText(raw.target_project, 120) ?? ctx.projectId ?? null;
81
+ const targetPath = coerceNullableText(raw.target_path, 500) ?? projectPath ?? null;
82
+ const out = {};
83
+ const userIntent = coerceText(raw.user_intent, 240);
84
+ const outcome = coerceNullableText(raw.outcome, 240);
85
+ if (userIntent) out.user_intent = userIntent;
86
+ if (triggerTerms && triggerTerms.length) out.trigger_terms = triggerTerms;
87
+ if (cwd !== undefined) out.cwd_at_request = cwd;
88
+ if (targetProject !== undefined) out.target_project = targetProject;
89
+ if (targetPath !== undefined) out.target_path = targetPath;
90
+ out.cross_context = typeof raw.cross_context === "boolean" ? raw.cross_context : !!(cwd && targetPath && cwd !== targetPath);
91
+ if (outcome !== undefined) out.outcome = outcome;
92
+ if (SECRET_RE.some((re) => re.test(JSON.stringify(out)))) return {};
93
+ return Object.keys(out).length ? out : {};
94
+ }
95
+
96
+ function parseMemoryEventsCli(text) {
97
+ const heading = loadArch().eventsHeading;
98
+ const idx = text.lastIndexOf(heading);
99
+ if (idx < 0) return { events: [], cleaned: text.trim() };
100
+ const after = text.slice(idx + heading.length);
101
+ const fence = after.match(/```(?:json)?\s*([\s\S]*?)```/);
102
+ let events = [];
103
+ if (fence) { try { const d = JSON.parse(fence[1].trim()); if (Array.isArray(d)) events = d; } catch { /* ignore */ } }
104
+ let cut = text.length;
105
+ if (fence && fence.index != null) cut = idx + heading.length + fence.index + fence[0].length;
106
+ const before = text.slice(0, idx).replace(/<!--\s*$/u, "");
107
+ const remainder = text.slice(cut).replace(/^\s*-->/u, "");
108
+ return { events, cleaned: (before + remainder).trim() };
109
+ }
110
+
111
+ function curateCliReply(db, text, ctx) {
112
+ const { events, cleaned } = parseMemoryEventsCli(text);
113
+ const style = require("../agentlas-style.cjs");
114
+ if (ctx && ctx.permission === "read") return style.sanitizeAssistantText(cleaned);
115
+ if (!events.length || !tableExists(db, "memory_entries")) return style.sanitizeAssistantText(cleaned);
116
+ ensureMemoryContextColumn(db);
117
+ const arch = loadArch();
118
+ const { randomUUID } = require("node:crypto");
119
+ const now = new Date().toISOString();
120
+ const rememberCurated = (memory) => {
121
+ if (!ctx || !Array.isArray(ctx.curatedMemories) || !memory) return;
122
+ if (!ctx.curatedMemories.some((item) => item.id === memory.id)) ctx.curatedMemories.push(memory);
123
+ };
124
+ for (const ev of events) {
125
+ const content = ev && typeof ev.content === "string" ? ev.content.trim() : "";
126
+ if (!content) continue;
127
+ if (ev.sensitivity === "secret" || SECRET_RE.some((re) => re.test(content))) continue;
128
+ const kind = arch.kinds.includes(ev.memory_kind) ? ev.memory_kind : "fact";
129
+ let scope = ev.suggested_scope === "agent_team"
130
+ ? "team_memory"
131
+ : arch.scopes.includes(ev.suggested_scope) ? ev.suggested_scope : "session";
132
+ const kindAllowsUserIdentity = ["fact", "decision", "preference", "procedure"].includes(kind);
133
+ if (scope === "user_identity" && (ev.confidence !== "high" || !kindAllowsUserIdentity)) scope = "session";
134
+ if (scope === "discard" || scope === "session") { logCli(ctx.projectPath, { action: scope, kind, content, at: now }); continue; }
135
+ if (scope === "project" && !ctx.projectPath) scope = "team_memory";
136
+ const ppath = scope === "project" ? ctx.projectPath : null;
137
+ const requestContext = normalizeRequestContext(ev, ctx, ppath);
138
+ try {
139
+ const dup = db.prepare("SELECT id,scope,kind,content,confidence,sensitivity,context_json FROM memory_entries WHERE scope=? AND kind=? AND lower(trim(content))=? AND superseded_at IS NULL AND (project_path IS ? OR project_path=?) LIMIT 1").get(scope, kind, content.toLowerCase(), ppath, ppath);
140
+ if (dup) {
141
+ rememberCurated({ ...dup, requestContext });
142
+ continue;
143
+ }
144
+ const memoryId = randomUUID();
145
+ const confidence = ev.confidence || "medium";
146
+ const sensitivity = ev.sensitivity || "internal";
147
+ db.prepare("INSERT INTO memory_entries (id,scope,kind,content,project_id,project_path,agent_id,chat_id,confidence,sensitivity,evidence_json,context_json,superseded_at,created_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,NULL,?)").run(memoryId, scope, kind, content, ctx.projectId || null, ppath, ctx.agentId || null, null, confidence, sensitivity, JSON.stringify(Array.isArray(ev.evidence_refs) ? ev.evidence_refs : []), JSON.stringify(requestContext), now);
148
+ rememberCurated({ id: memoryId, scope, kind, content, confidence, sensitivity, requestContext });
149
+ logCli(ctx.projectPath, { action: "written", scope, kind, content, request_context: requestContext, at: now });
150
+ } catch { /* ignore */ }
151
+ }
152
+ return style.sanitizeAssistantText(cleaned);
153
+ }
154
+
155
+ module.exports = {
156
+ SECRET_RE,
157
+ loadArch,
158
+ ensureMemoryContextColumn,
159
+ logCli,
160
+ normalizeRequestContext,
161
+ parseMemoryEventsCli,
162
+ curateCliReply,
163
+ };
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ /*
3
+ * oberon/common — Oberon 필름 스튜디오 공용 유틸.
4
+ * v1 모놀리스(engine/agentlas.cjs, legacy-v1-engine-snapshot §12213-12580)의
5
+ * oberonParseFlags / oberonBar / oberonBytes / slugifyOberon 충실 포팅.
6
+ *
7
+ * v2 규칙: v1의 fail()은 process.exit(1)로 즉사했지만, v2 명령은 exit code를
8
+ * 반환해야 한다. 그래서 사용자-레벨 실패는 OberonFail throw → 명령 래퍼가
9
+ * "✖ <msg>" + return 1 로 변환한다 (v1 fail의 "✖ " 접두 계약 유지).
10
+ */
11
+
12
+ class OberonFail extends Error {
13
+ constructor(message, code) {
14
+ super(message);
15
+ this.oberonFail = true;
16
+ if (code) this.code = code;
17
+ }
18
+ }
19
+
20
+ function fail(msg, code) {
21
+ throw new OberonFail(String(msg ?? ""), code);
22
+ }
23
+
24
+ // v1 oberonParseFlags 그대로: `--key value` / `--key`(불리언) / 나머지는 위치 인자.
25
+ // 다음 토큰이 `--`로 시작하면 값이 아니라 다른 플래그로 본다.
26
+ function parseFlags(args) {
27
+ const flags = {};
28
+ const rest = [];
29
+ for (let i = 0; i < args.length; i++) {
30
+ const a = args[i];
31
+ if (a.startsWith("--")) {
32
+ const key = a.slice(2);
33
+ const next = args[i + 1];
34
+ if (next === undefined || next.startsWith("--")) flags[key] = true;
35
+ else {
36
+ flags[key] = next;
37
+ i++;
38
+ }
39
+ } else rest.push(a);
40
+ }
41
+ return { flags, rest };
42
+ }
43
+
44
+ // 20-셀 진행률 바 (v1 oberonBar)
45
+ function oberonBar(pct) {
46
+ const n = Math.max(0, Math.min(20, Math.round((pct / 100) * 20)));
47
+ return "█".repeat(n) + "░".repeat(20 - n);
48
+ }
49
+
50
+ // 사람이 읽는 바이트 표기 (v1 oberonBytes)
51
+ function oberonBytes(n) {
52
+ if (n > 1e6) return (n / 1e6).toFixed(1) + "MB";
53
+ if (n > 1e3) return (n / 1e3).toFixed(0) + "KB";
54
+ return n + "B";
55
+ }
56
+
57
+ // 딜리버리 폴더 이름용 슬러그 (v1 slugifyOberon).
58
+ // 한국어 제목이 흔해서 가-힣을 보존한다 — ASCII만 남기면 폴더명이 전부 "_"가 된다.
59
+ function slugifyOberon(value) {
60
+ return (
61
+ String(value || "")
62
+ .trim()
63
+ .replace(/[^\w가-힣-]+/g, "_")
64
+ .replace(/_{2,}/g, "_")
65
+ .slice(0, 48) || "oberon"
66
+ );
67
+ }
68
+
69
+ module.exports = { OberonFail, fail, parseFlags, oberonBar, oberonBytes, slugifyOberon };