agentlas 0.7.0 → 0.9.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 (49) hide show
  1. package/CHANGELOG.md +199 -0
  2. package/README.md +161 -18
  3. package/bin/agentlas.cjs +8 -8
  4. package/engine/agentlas-core-harness.cjs +212 -0
  5. package/engine/agentlas-desktop-loadout.cjs +527 -0
  6. package/engine/agentlas-doctor.cjs +1 -1
  7. package/engine/agentlas-experience-exchange.cjs +835 -85
  8. package/engine/agentlas-experience-intake.cjs +444 -0
  9. package/engine/agentlas-experience-mcp.cjs +580 -18
  10. package/engine/agentlas-i18n.cjs +10 -10
  11. package/engine/agentlas-input.cjs +5 -4
  12. package/engine/agentlas-mcp-env.cjs +219 -0
  13. package/engine/agentlas-mcp-wrapper.cjs +51 -0
  14. package/engine/agentlas-memory-governance.cjs +1029 -0
  15. package/engine/agentlas-native-host.cjs +129 -39
  16. package/engine/agentlas-parity.cjs +339 -154
  17. package/engine/agentlas-repl.cjs +306 -31
  18. package/engine/agentlas-workforce.cjs +2991 -0
  19. package/engine/agentlas-workload-routing.cjs +523 -0
  20. package/engine/agentlas.cjs +1619 -234
  21. package/engine/bootstrap-schema.sql +1 -1
  22. package/engine/experience-taxonomy-v1.json +49 -0
  23. package/package.json +8 -4
  24. package/scripts/gen-bootstrap-schema.sh +0 -23
  25. package/test/bootstrap-race.cjs +0 -47
  26. package/test/capture-runtime-guard.cjs +0 -122
  27. package/test/cloud-asset-restore.cjs +0 -423
  28. package/test/cloud-cas-client.cjs +0 -333
  29. package/test/cloud-owner-restore.cjs +0 -183
  30. package/test/cloud-runtime-paths.cjs +0 -40
  31. package/test/cloud-save-publish.cjs +0 -487
  32. package/test/credential-env-regression.cjs +0 -52
  33. package/test/engine-hardening-regression.cjs +0 -74
  34. package/test/experience-exchange-contract.cjs +0 -569
  35. package/test/experience-mcp-contract.cjs +0 -391
  36. package/test/fixtures/portable-experience-bundle-v1-golden.json +0 -124
  37. package/test/login-loopback-security.cjs +0 -115
  38. package/test/mcp-config-isolation.cjs +0 -36
  39. package/test/permission-mapping.cjs +0 -180
  40. package/test/route-regression.cjs +0 -357
  41. package/test/run-api-regression.cjs +0 -322
  42. package/test/runtime-env-protection.cjs +0 -89
  43. package/test/semver-precedence.cjs +0 -39
  44. package/test/smoke.sh +0 -93
  45. package/test/sqlite-driver-probe.cjs +0 -22
  46. package/test/terminal-ui-regression.cjs +0 -477
  47. package/test/timeout-regression.cjs +0 -218
  48. package/test/tool-workspace-boundary.cjs +0 -165
  49. package/test/update-safety.cjs +0 -376
@@ -60,7 +60,7 @@ const STRINGS = {
60
60
  "modelDefault": "(default)",
61
61
  "effortSet": "effort → %s",
62
62
  "effortCurrent": "current effort: %s",
63
- "effortUsage": "usage: /effort low|medium|high|max|auto",
63
+ "effortUsage": "usage: /effort low|medium|high|max|off|auto",
64
64
  "permSet": "permission → %s",
65
65
  "permUsage": "usage: /permission read|write|full",
66
66
  "permCurrent": "current permission: %s",
@@ -114,11 +114,11 @@ const STRINGS = {
114
114
  "usageBar": "tokens",
115
115
  "config.title": "Engine auto engagement — explicit on/off (default: off)",
116
116
  "config.storm": "Stormbreaker auto-engage on direct-routed real work",
117
- "config.network": "hep-network auto-borrow on direct-routed real work",
117
+ "config.network": "Workforce Ontology auto-staffing on direct-routed real work",
118
118
  "config.usage": "usage: /config storm|network on|off · explicit /storm and /network always work",
119
119
  "config.set": "%s → %s",
120
120
  "config.autoEngage": "auto-engaging %s (opted in via /config — turn off with /config %s off)",
121
- "help.config": "explicit on/off for Stormbreaker/hep-network auto engagement",
121
+ "help.config": "explicit on/off for Stormbreaker/Workforce Ontology auto engagement",
122
122
  "guard.imageWarn": "this agent works with images, but %s can't generate them — try /runtime gemini (or codex), or /team to pin one",
123
123
  "multimodal.title": "Multimodal fallback",
124
124
  "multimodal.set": "multimodal %s → %s",
@@ -139,7 +139,7 @@ const STRINGS = {
139
139
  "help.firms": "list companies / switch to one",
140
140
  "help.runtime": "switch runtime (claude-code|codex|gemini|anthropic|openai|google|ollama|upstage)",
141
141
  "help.model": "set the model (claude/codex/gemini alias or BYOK/Ollama id)",
142
- "help.effort": "set reasoning effort (low|medium|high|max|auto)",
142
+ "help.effort": "set reasoning effort (low|medium|high|max|off|auto)",
143
143
  "help.permission": "what it may do (read|write|full)",
144
144
  "help.permissions": "show permission choices and current level",
145
145
  "help.setup": "run language, runtime, and permission setup again",
@@ -159,7 +159,7 @@ const STRINGS = {
159
159
  "help.research": "run the Research Engine (search/gather/read)",
160
160
  "help.search": "discover agents in the Hub",
161
161
  "help.install": "install an agent from the Hub by slug",
162
- "help.network": "decompose a request into an A2A task force",
162
+ "help.network": "let the host LLM staff and execute an ontology-grounded exact-release task force",
163
163
  "help.browser": "real browser execution hardpoint",
164
164
  "help.connect": "wire Telegram / platforms to an agent team",
165
165
  "help.swarm": "fan out an emergent agent swarm on a goal",
@@ -274,7 +274,7 @@ const STRINGS = {
274
274
  "modelDefault": "(기본값)",
275
275
  "effortSet": "추론 강도 → %s",
276
276
  "effortCurrent": "현재 추론 강도: %s",
277
- "effortUsage": "사용법: /effort low|medium|high|max|auto",
277
+ "effortUsage": "사용법: /effort low|medium|high|max|off|auto",
278
278
  "permSet": "권한 → %s",
279
279
  "permUsage": "사용법: /permission read|write|full",
280
280
  "permCurrent": "현재 권한: %s",
@@ -328,11 +328,11 @@ const STRINGS = {
328
328
  "usageBar": "토큰",
329
329
  "config.title": "엔진 자동 개입 — 명시적 on/off (기본: off)",
330
330
  "config.storm": "직답 라우팅된 실작업에 Stormbreaker 자동 개입",
331
- "config.network": "직답 라우팅된 실작업에 hep-network 자동 빌림",
331
+ "config.network": "직답 라우팅된 실작업에 Workforce Ontology 자동 선발",
332
332
  "config.usage": "사용법: /config storm|network on|off · /storm·/network 명시 실행은 항상 가능",
333
333
  "config.set": "%s → %s",
334
334
  "config.autoEngage": "%s 자동 개입 (/config로 켜져 있음 — 해제: /config %s off)",
335
- "help.config": "Stormbreaker/hep-network 자동 개입 명시적 on/off",
335
+ "help.config": "Stormbreaker/Workforce Ontology 자동 개입 명시적 on/off",
336
336
  "guard.imageWarn": "이 에이전트는 이미지 작업인데 %s는 이미지 생성 불가 — /runtime gemini(또는 codex), 또는 /team으로 고정",
337
337
  "multimodal.title": "멀티모달 fallback",
338
338
  "multimodal.set": "멀티모달 %s → %s",
@@ -352,7 +352,7 @@ const STRINGS = {
352
352
  "help.firms": "회사 목록 / 전환",
353
353
  "help.runtime": "런타임 전환 (claude-code|codex|gemini|anthropic|openai|google|ollama|upstage)",
354
354
  "help.model": "모델 지정 (claude/codex/gemini 별칭 또는 BYOK/Ollama id)",
355
- "help.effort": "추론 강도 지정 (low|medium|high|max|auto)",
355
+ "help.effort": "추론 강도 지정 (low|medium|high|max|off|auto)",
356
356
  "help.permission": "허용 범위 (read|write|full)",
357
357
  "help.permissions": "권한 선택지와 현재 단계 보기",
358
358
  "help.setup": "언어·런타임·권한 설정 다시 실행",
@@ -372,7 +372,7 @@ const STRINGS = {
372
372
  "help.research": "Research Engine 실행 (search/gather/read)",
373
373
  "help.search": "Hub에서 에이전트 발견",
374
374
  "help.install": "slug로 Hub 에이전트 설치",
375
- "help.network": "요청을 A2A 태스크포스로 분해",
375
+ "help.network": "상위 LLM이 온톨로지에서 정확한 릴리스 TF를 선발·실행",
376
376
  "help.browser": "실제 브라우저 실행 하드포인트",
377
377
  "help.connect": "Telegram/플랫폼을 에이전트 팀에 연결",
378
378
  "help.swarm": "목표에 emergent 에이전트 스웜 전개",
@@ -84,12 +84,12 @@ const SLASH_COMMAND_META = [
84
84
  { command: "/firms", description: "List installed companies", category: "Routing", usage: "/firms", detail: "Show company CEOs available in this terminal." },
85
85
  { command: "/firm", description: "Switch to a company CEO", category: "Routing", usage: "/firm <name>", detail: "Switch the current conversation to a company CEO agent." },
86
86
  { command: "/runtime", description: "Switch runtime: claude-code, codex, gemini, BYOK, or Ollama", category: "Settings", usage: "/runtime codex", detail: "Change the engine Agentlas uses for subsequent turns." },
87
- { command: "/model", description: "Set the model for the current runtime", category: "Settings", usage: "/model <id>", detail: "Works for claude/codex/gemini (alias like sonnet/opus, or full id) and BYOK/Ollama." },
88
- { command: "/effort", description: "Set reasoning effort (low/medium/high/max)", category: "Settings", usage: "/effort high", detail: "Higher effort = deeper reasoning. Maps to codex model_reasoning_effort and claude think-depth." },
87
+ { command: "/model", description: "Pin a model or return to auto allocation", category: "Settings", usage: "/model <id|auto>", detail: "An id is an explicit session pin; auto lets the higher-level allocator choose a provider-compatible tier." },
88
+ { command: "/effort", description: "Pin reasoning effort or return to auto", category: "Settings", usage: "/effort low|medium|high|max|off|auto", detail: "A level is an explicit pin; auto lets the higher-level allocator choose per task; off is an explicit no-effort pin." },
89
89
  { command: "/permission", description: "Set read/write/full permission", category: "Settings", usage: "/permission full", detail: "No argument shows what read, write, and full mean.", aliases: ["/perm"] },
90
90
  { command: "/permissions", description: "Show or set current permission", category: "Settings", usage: "/permissions", detail: "Codex-style permission screen for Agentlas read/write/full." },
91
91
  { command: "/setup", description: "Run first-time setup again", category: "Settings", usage: "/setup", detail: "Re-run language, runtime, and default permission setup in-place." },
92
- { command: "/config", description: "Explicit on/off for Stormbreaker and hep-network auto engagement", category: "Settings", usage: "/config [storm|network] [on|off]", detail: "Both engines default to off — no automatic activation. Turn one on to let direct-routed real-work prompts engage it; /storm and /network stay available explicitly either way.", examples: ["/config", "/config storm on", "/config network off"], aliases: ["/toggles"] },
92
+ { command: "/config", description: "Explicit on/off for Stormbreaker and Workforce Ontology auto engagement", category: "Settings", usage: "/config [storm|network] [on|off]", detail: "Both engines default to off — no automatic activation. Turn one on to let direct-routed real-work prompts engage it; /storm and /network stay available explicitly either way.", examples: ["/config", "/config storm on", "/config network off"], aliases: ["/toggles"] },
93
93
  { command: "/cwd", description: "Show or change the working folder", category: "Files", usage: "/cwd <path>", detail: "Change the folder used for tools, file mentions, and local commands." },
94
94
  { command: "/memory", description: "Show the memory injected into this run", category: "Context", usage: "/memory", detail: "Print the project memory that Agentlas adds to agent turns." },
95
95
  { command: "/side", description: "Ask a side question without saving it to chat context", category: "Context", usage: "/side <question>", detail: "Runs a one-off answer using current context, then returns without appending to chat history.", aliases: ["/btw"] },
@@ -111,7 +111,8 @@ const SLASH_COMMAND_META = [
111
111
  { command: "/research", description: "Run the Hephaestus Research Engine", category: "Engine", usage: "/research search \"query\"", detail: "status|gather|search|read|plan — evidence-grade web research from the terminal." },
112
112
  { command: "/search", description: "Discover agents in the Hub", category: "Hub", usage: "/search <what you need>", detail: "Search the Agentlas Hub + local for an agent that fits the task (hep-search)." },
113
113
  { command: "/install", description: "Install an agent from the Hub by slug", category: "Hub", usage: "/install <slug>", detail: "Install a marketplace agent into this terminal (hep-cloud)." },
114
- { command: "/network", description: "Decompose a request into an A2A task force", category: "Engine", usage: "/network <request>", detail: "Runs Hephaestus hep-network splits a composite request across agents.", aliases: ["/taskforce"] },
114
+ { command: "/network", description: "Staff and execute an ontology-grounded task force", category: "Engine", usage: "/network <request> [--benchmark]", detail: "The active top host LLM creates the work order, searches the Hub workforce ontology, selects exact releases, and executes a receipt-backed task force.", aliases: ["/taskforce", "/workforce"] },
115
+ { command: "/legacy-network", description: "Run the compatibility Hephaestus network route", category: "Engine", usage: "/legacy-network <request>", detail: "Explicit compatibility escape hatch; never used as a fallback by /network." },
115
116
  { command: "/browser", description: "Real browser execution hardpoint", category: "Engine", usage: "/browser [sub]", detail: "Runs the Agentlas browser hardpoint (hep-browser)." },
116
117
  { command: "/connect", description: "Wire Telegram / platforms to an agent team", category: "Hub", usage: "/connect", detail: "Runs Hephaestus hep-connect for platform integration." },
117
118
  { command: "/doctor", description: "Check runtimes and local data", category: "Health", usage: "/doctor", detail: "Run local checks for runtimes, data, credentials, and setup." },
@@ -0,0 +1,219 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Environment boundary for stdio MCP children owned by Agentlas Terminal.
5
+ *
6
+ * The native LLM provider keeps its normal environment so its own login can
7
+ * work. The MCP server does not inherit that environment: it receives a small
8
+ * launch environment plus only the credential names declared by the trusted,
9
+ * private system registry row selected after consent.
10
+ */
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
+
17
+ const MCP_LAUNCH_SCHEMA = "agentlas.mcp-child-launch.v1";
18
+ const ENV_NAME_RE = /^[A-Z][A-Z0-9_]*$/;
19
+ const MAX_DESCRIPTOR_BYTES = 64 * 1024;
20
+
21
+ const SAFE_BASE_ENV_KEYS = Object.freeze([
22
+ "PATH",
23
+ "PATHEXT",
24
+ "SystemRoot",
25
+ "WINDIR",
26
+ "ComSpec",
27
+ "LANG",
28
+ "LC_ALL",
29
+ "LC_CTYPE",
30
+ "TZ",
31
+ "TERM",
32
+ "COLORTERM",
33
+ ]);
34
+
35
+ const FORBIDDEN_CREDENTIAL_ENV_KEYS = new Set([
36
+ "HOME", "PATH", "PATHEXT", "USERPROFILE", "HOMEDRIVE", "HOMEPATH",
37
+ "SYSTEMROOT", "WINDIR", "COMSPEC", "TMPDIR", "TMP", "TEMP",
38
+ "NODE_OPTIONS", "NODE_PATH", "PYTHONHOME", "PYTHONPATH", "RUBYOPT",
39
+ "LD_PRELOAD", "LD_LIBRARY_PATH", "DYLD_INSERT_LIBRARIES", "DYLD_LIBRARY_PATH",
40
+ "BASH_ENV", "ENV", "ZDOTDIR", "SHELLOPTS", "ELECTRON_RUN_AS_NODE",
41
+ "SSLKEYLOGFILE", "NODE_TLS_REJECT_UNAUTHORIZED", "NODE_EXTRA_CA_CERTS",
42
+ "SSL_CERT_FILE", "SSL_CERT_DIR", "OPENSSL_CONF", "REQUESTS_CA_BUNDLE", "CURL_CA_BUNDLE",
43
+ "HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "NO_PROXY", "GRPC_PROXY", "NPM_CONFIG_PROXY",
44
+ "AGENTLAS_SESSION", "AGENTLAS_USER_DATA_DIR", "AGENTLAS_MCP_BASE_URL",
45
+ "AGENTLAS_WEB_BASE_URL", "AGENTLAS_API_BASE_URL", "AGENTLAS_HUB_BASE_URL",
46
+ "AGENTLAS_CLOUD_BASE_URL",
47
+ ]);
48
+
49
+ function normalizeCredentialKeyNames(value) {
50
+ if (!Array.isArray(value) || value.length > 64) throw new Error("MCP credential key metadata must be a bounded array");
51
+ const names = [];
52
+ const seen = new Set();
53
+ for (const raw of value) {
54
+ const key = String(raw || "");
55
+ if (!ENV_NAME_RE.test(key)) throw new Error("MCP credential key metadata contains an invalid name");
56
+ if (FORBIDDEN_CREDENTIAL_ENV_KEYS.has(key) || key.startsWith("AGENTLAS_")) {
57
+ throw new Error("MCP credential key metadata requests a protected host variable");
58
+ }
59
+ if (!seen.has(key)) names.push(key), seen.add(key);
60
+ }
61
+ return names;
62
+ }
63
+
64
+ function findEnvValue(source, key) {
65
+ if (Object.prototype.hasOwnProperty.call(source || {}, key)) return source[key];
66
+ if (process.platform !== "win32") return undefined;
67
+ const match = Object.keys(source || {}).find((candidate) => candidate.toUpperCase() === key.toUpperCase());
68
+ return match ? source[match] : undefined;
69
+ }
70
+
71
+ function ensurePrivateDirectory(directory) {
72
+ fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
73
+ const stat = fs.lstatSync(directory);
74
+ if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error("MCP runtime home must be a real directory");
75
+ try { fs.chmodSync(directory, 0o700); } catch { /* Windows/best-effort */ }
76
+ return directory;
77
+ }
78
+
79
+ function mcpRuntimeHome(dataDir, serverIdentity) {
80
+ const root = path.resolve(dataDir || path.join(os.tmpdir(), "agentlas-mcp-runtime"));
81
+ ensurePrivateDirectory(root);
82
+ const parent = path.join(root, "mcp-runtime-homes");
83
+ ensurePrivateDirectory(parent);
84
+ const digest = crypto.createHash("sha256").update(String(serverIdentity || "mcp"), "utf8").digest("hex").slice(0, 32);
85
+ const target = path.join(parent, digest);
86
+ ensurePrivateDirectory(target);
87
+ const relative = path.relative(fs.realpathSync(parent), fs.realpathSync(target));
88
+ if (!relative || path.isAbsolute(relative) || relative === ".." || relative.startsWith(`..${path.sep}`)) {
89
+ throw new Error("MCP runtime home escaped its private root");
90
+ }
91
+ return target;
92
+ }
93
+
94
+ function safeBaseEnvironment(source, options = {}) {
95
+ const runtimeHome = ensurePrivateDirectory(path.resolve(options.runtimeHome || mcpRuntimeHome(null, options.serverIdentity)));
96
+ const result = {};
97
+ for (const key of SAFE_BASE_ENV_KEYS) {
98
+ const value = findEnvValue(source, key);
99
+ if (typeof value === "string" && value && !value.includes("\0")) result[key] = value;
100
+ }
101
+ // Never expose the user's real home through the environment. MCPs that need
102
+ // credentials must use their declared registry key, not ambient dotfiles.
103
+ const runtimeTmp = ensurePrivateDirectory(path.join(runtimeHome, "tmp"));
104
+ result.HOME = runtimeHome;
105
+ result.TMPDIR = runtimeTmp;
106
+ result.TMP = runtimeTmp;
107
+ result.TEMP = runtimeTmp;
108
+ if (process.platform === "win32") {
109
+ result.USERPROFILE = runtimeHome;
110
+ result.HOMEDRIVE = path.parse(runtimeHome).root.replace(/[\\/]$/, "") || "C:";
111
+ result.HOMEPATH = runtimeHome.slice(result.HOMEDRIVE.length) || "\\";
112
+ }
113
+ return result;
114
+ }
115
+
116
+ function buildMcpChildEnv(source, credentialKeyNames, options = {}) {
117
+ const names = normalizeCredentialKeyNames(credentialKeyNames || []);
118
+ const result = safeBaseEnvironment(source || {}, options);
119
+ for (const key of names) {
120
+ const value = findEnvValue(source || {}, key);
121
+ if (typeof value === "string" && value && !value.includes("\0")) result[key] = value;
122
+ }
123
+ return result;
124
+ }
125
+
126
+ function hasSensitiveRuntimeArgument(command, args) {
127
+ const values = [command, ...(args || [])].map((value) => String(value || ""));
128
+ const joined = values.join("\n");
129
+ if (/\b(?:sk-(?:proj-)?|gh[pousr]_|xox[baprs]-)[A-Za-z0-9_-]{16,}\b/i.test(joined)) return true;
130
+ if (/-----BEGIN (?:RSA |EC |OPENSSH |DSA )?PRIVATE KEY-----/i.test(joined)) return true;
131
+ if (/https?:\/\/[^\s/@:]+:[^\s/@]+@/i.test(joined)) return true;
132
+ if (/[?&](?:api[-_]?key|token|secret|password|auth|authorization|cookie)=[^&\s]+/i.test(joined)) return true;
133
+ if (/\bbearer\s+[A-Za-z0-9._~+/=-]{8,}/i.test(joined)) return true;
134
+ for (let index = 0; index < values.length; index += 1) {
135
+ const value = values[index];
136
+ const credentialFlag = /^(?:--?)?(?:api[-_]?key|access[-_]?token|refresh[-_]?token|auth[-_]?token|client[-_]?secret|secret|password|passwd|private[-_]?key|authorization|cookie|credential)(?:=|:).+/i;
137
+ const credentialFlagName = /^(?:--?)?(?:api[-_]?key|access[-_]?token|refresh[-_]?token|auth[-_]?token|client[-_]?secret|secret|password|passwd|private[-_]?key|authorization|cookie|credential)$/i;
138
+ if (credentialFlag.test(value)) return true;
139
+ if (credentialFlagName.test(value) && values[index + 1]) return true;
140
+ }
141
+ return false;
142
+ }
143
+
144
+ function validateLaunchDescriptor(value) {
145
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("invalid MCP launch descriptor");
146
+ const expected = ["schema", "command", "args", "credentialKeyNames", "runtimeHome"];
147
+ if (Object.keys(value).sort().join("\0") !== [...expected].sort().join("\0")) throw new Error("invalid MCP launch descriptor fields");
148
+ if (value.schema !== MCP_LAUNCH_SCHEMA) throw new Error("unsupported MCP launch descriptor");
149
+ if (typeof value.command !== "string" || !value.command.trim() || value.command.length > 4096 || /[\u0000\r\n]/.test(value.command)) {
150
+ throw new Error("invalid MCP command");
151
+ }
152
+ if (!Array.isArray(value.args) || value.args.length > 128 || value.args.some((item) => typeof item !== "string" || item.length > 4096 || /[\u0000\r\n]/.test(item))) {
153
+ throw new Error("invalid MCP arguments");
154
+ }
155
+ const credentialKeyNames = normalizeCredentialKeyNames(value.credentialKeyNames);
156
+ if (hasSensitiveRuntimeArgument(value.command, value.args)) throw new Error("MCP executable metadata contains a credential-like value");
157
+ if (typeof value.runtimeHome !== "string" || !path.isAbsolute(value.runtimeHome) || value.runtimeHome.length > 4096 || /[\u0000\r\n]/.test(value.runtimeHome)) {
158
+ throw new Error("invalid MCP runtime home");
159
+ }
160
+ return {
161
+ schema: MCP_LAUNCH_SCHEMA,
162
+ command: value.command,
163
+ args: [...value.args],
164
+ credentialKeyNames,
165
+ runtimeHome: value.runtimeHome,
166
+ };
167
+ }
168
+
169
+ function encodeLaunchDescriptor(value) {
170
+ const validated = validateLaunchDescriptor(value);
171
+ const json = JSON.stringify(validated);
172
+ if (Buffer.byteLength(json, "utf8") > MAX_DESCRIPTOR_BYTES) throw new Error("MCP launch descriptor is too large");
173
+ return Buffer.from(json, "utf8").toString("base64url");
174
+ }
175
+
176
+ function decodeLaunchDescriptor(value) {
177
+ if (typeof value !== "string" || !value || value.length > Math.ceil(MAX_DESCRIPTOR_BYTES * 4 / 3) + 16 || !/^[A-Za-z0-9_-]+$/.test(value)) {
178
+ throw new Error("invalid MCP launch descriptor encoding");
179
+ }
180
+ let parsed;
181
+ try { parsed = JSON.parse(Buffer.from(value, "base64url").toString("utf8")); }
182
+ catch { throw new Error("invalid MCP launch descriptor encoding"); }
183
+ return validateLaunchDescriptor(parsed);
184
+ }
185
+
186
+ function wrapStdioServer(server, options = {}) {
187
+ const args = Array.isArray(server?.args) ? server.args : (() => {
188
+ try { return JSON.parse(server?.args_json || "[]"); } catch { return null; }
189
+ })();
190
+ if (!server || typeof server.command !== "string" || !Array.isArray(args)) throw new Error("invalid MCP stdio server");
191
+ const identity = String(server.catalog_id || server.id || server.name || "mcp");
192
+ const runtimeHome = server.mcpRuntimeHome || mcpRuntimeHome(options.dataDir, identity);
193
+ const credentialKeyNames = normalizeCredentialKeyNames(server.credentialKeyNames || []);
194
+ const encoded = encodeLaunchDescriptor({
195
+ schema: MCP_LAUNCH_SCHEMA,
196
+ command: server.command,
197
+ args,
198
+ credentialKeyNames,
199
+ runtimeHome,
200
+ });
201
+ return {
202
+ command: options.nodePath || process.execPath,
203
+ args: [path.join(__dirname, "agentlas-mcp-wrapper.cjs"), encoded],
204
+ };
205
+ }
206
+
207
+ module.exports = {
208
+ MCP_LAUNCH_SCHEMA,
209
+ SAFE_BASE_ENV_KEYS,
210
+ normalizeCredentialKeyNames,
211
+ mcpRuntimeHome,
212
+ safeBaseEnvironment,
213
+ buildMcpChildEnv,
214
+ hasSensitiveRuntimeArgument,
215
+ validateLaunchDescriptor,
216
+ encodeLaunchDescriptor,
217
+ decodeLaunchDescriptor,
218
+ wrapStdioServer,
219
+ };
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ // Trusted Agentlas-owned hop between an authenticated provider CLI and an MCP
5
+ // server. Never print the descriptor or inherited environment: either can
6
+ // reveal private local metadata even though credential values are not encoded.
7
+
8
+ const { spawn } = require("node:child_process");
9
+ const { buildMcpChildEnv, decodeLaunchDescriptor } = require("./agentlas-mcp-env.cjs");
10
+
11
+ function fail(code) {
12
+ process.stderr.write(`agentlas MCP wrapper: ${code}\n`);
13
+ process.exitCode = 1;
14
+ }
15
+
16
+ let descriptor;
17
+ try { descriptor = decodeLaunchDescriptor(process.argv[2]); }
18
+ catch { fail("invalid_launch_descriptor"); }
19
+
20
+ if (descriptor) {
21
+ let child;
22
+ try {
23
+ child = spawn(descriptor.command, descriptor.args, {
24
+ cwd: process.cwd(),
25
+ env: buildMcpChildEnv(process.env, descriptor.credentialKeyNames, {
26
+ runtimeHome: descriptor.runtimeHome,
27
+ }),
28
+ stdio: ["inherit", "inherit", "inherit"],
29
+ windowsHide: true,
30
+ });
31
+ } catch {
32
+ fail("child_spawn_failed");
33
+ }
34
+
35
+ if (child) {
36
+ const relay = (signal) => {
37
+ try { child.kill(signal); } catch { /* child already stopped */ }
38
+ };
39
+ for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
40
+ try { process.on(signal, () => relay(signal)); } catch { /* unavailable on this platform */ }
41
+ }
42
+ child.once("error", () => fail("child_spawn_failed"));
43
+ child.once("close", (code, signal) => {
44
+ if (signal) {
45
+ process.exitCode = 1;
46
+ return;
47
+ }
48
+ process.exitCode = Number.isInteger(code) ? code : 1;
49
+ });
50
+ }
51
+ }