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.
- package/CHANGELOG.md +199 -0
- package/README.md +161 -18
- package/bin/agentlas.cjs +8 -8
- package/engine/agentlas-core-harness.cjs +212 -0
- package/engine/agentlas-desktop-loadout.cjs +527 -0
- package/engine/agentlas-doctor.cjs +1 -1
- package/engine/agentlas-experience-exchange.cjs +835 -85
- package/engine/agentlas-experience-intake.cjs +444 -0
- package/engine/agentlas-experience-mcp.cjs +580 -18
- package/engine/agentlas-i18n.cjs +10 -10
- package/engine/agentlas-input.cjs +5 -4
- package/engine/agentlas-mcp-env.cjs +219 -0
- package/engine/agentlas-mcp-wrapper.cjs +51 -0
- package/engine/agentlas-memory-governance.cjs +1029 -0
- package/engine/agentlas-native-host.cjs +129 -39
- package/engine/agentlas-parity.cjs +339 -154
- package/engine/agentlas-repl.cjs +306 -31
- package/engine/agentlas-workforce.cjs +2991 -0
- package/engine/agentlas-workload-routing.cjs +523 -0
- package/engine/agentlas.cjs +1619 -234
- package/engine/bootstrap-schema.sql +1 -1
- package/engine/experience-taxonomy-v1.json +49 -0
- package/package.json +8 -4
- package/scripts/gen-bootstrap-schema.sh +0 -23
- package/test/bootstrap-race.cjs +0 -47
- package/test/capture-runtime-guard.cjs +0 -122
- package/test/cloud-asset-restore.cjs +0 -423
- package/test/cloud-cas-client.cjs +0 -333
- package/test/cloud-owner-restore.cjs +0 -183
- package/test/cloud-runtime-paths.cjs +0 -40
- package/test/cloud-save-publish.cjs +0 -487
- package/test/credential-env-regression.cjs +0 -52
- package/test/engine-hardening-regression.cjs +0 -74
- package/test/experience-exchange-contract.cjs +0 -569
- package/test/experience-mcp-contract.cjs +0 -391
- package/test/fixtures/portable-experience-bundle-v1-golden.json +0 -124
- package/test/login-loopback-security.cjs +0 -115
- package/test/mcp-config-isolation.cjs +0 -36
- package/test/permission-mapping.cjs +0 -180
- package/test/route-regression.cjs +0 -357
- package/test/run-api-regression.cjs +0 -322
- package/test/runtime-env-protection.cjs +0 -89
- package/test/semver-precedence.cjs +0 -39
- package/test/smoke.sh +0 -93
- package/test/sqlite-driver-probe.cjs +0 -22
- package/test/terminal-ui-regression.cjs +0 -477
- package/test/timeout-regression.cjs +0 -218
- package/test/tool-workspace-boundary.cjs +0 -165
- package/test/update-safety.cjs +0 -376
|
@@ -19,6 +19,7 @@ const os = require("node:os");
|
|
|
19
19
|
const path = require("node:path");
|
|
20
20
|
const permissions = require("./agentlas-permissions.cjs");
|
|
21
21
|
const i18n = require("./agentlas-i18n.cjs");
|
|
22
|
+
const { wrapStdioServer } = require("./agentlas-mcp-env.cjs");
|
|
22
23
|
|
|
23
24
|
function uiText(ui, key, ...args) {
|
|
24
25
|
return ui && typeof ui.t === "function" ? ui.t(key, ...args) : i18n.t("en", key, ...args);
|
|
@@ -152,27 +153,45 @@ function prepareCodexRuntimeEnv(env = process.env) {
|
|
|
152
153
|
return base;
|
|
153
154
|
}
|
|
154
155
|
|
|
155
|
-
function runtimeEnvForKind(kind, env = process.env) {
|
|
156
|
-
|
|
156
|
+
function runtimeEnvForKind(kind, env = process.env, options = {}) {
|
|
157
|
+
if (kind === "codex") return prepareCodexRuntimeEnv(env);
|
|
158
|
+
if (kind === "gemini") return prepareGeminiRuntimeEnv(env, options);
|
|
159
|
+
return env;
|
|
157
160
|
}
|
|
158
161
|
|
|
159
162
|
// MCP 서버 이름 → TOML/JSON 안전 키 (하이픈/공백 → _).
|
|
160
163
|
function mcpKey(s) {
|
|
161
|
-
return String((s && (s.
|
|
164
|
+
return String((s && (s.catalog_id || s.id || s.name)) || "mcp").toLowerCase().replace(/[^a-z0-9_]+/g, "_").replace(/^_+|_+$/g, "") || "mcp";
|
|
162
165
|
}
|
|
163
|
-
function
|
|
164
|
-
|
|
166
|
+
function selectedMcpServers(servers, options = {}) {
|
|
167
|
+
const selected = [];
|
|
168
|
+
for (const server of servers || []) {
|
|
169
|
+
if (!server || server.enabled === 0 || server.transport !== "stdio" || !server.command) continue;
|
|
170
|
+
selected.push(server);
|
|
171
|
+
}
|
|
172
|
+
return selected;
|
|
173
|
+
}
|
|
174
|
+
function wrappedMcpServerMap(servers, options = {}) {
|
|
175
|
+
const result = {};
|
|
176
|
+
for (const server of selectedMcpServers(servers, options)) {
|
|
177
|
+
const wrapped = wrapStdioServer(server, { dataDir: userDataDir(options.env || process.env) });
|
|
178
|
+
const baseKey = mcpKey(server);
|
|
179
|
+
let key = baseKey;
|
|
180
|
+
if (Object.prototype.hasOwnProperty.call(result, key)) {
|
|
181
|
+
const identity = String(server.catalog_id || server.id || server.name || server.command);
|
|
182
|
+
key = `${baseKey}_${crypto.createHash("sha256").update(identity, "utf8").digest("hex").slice(0, 8)}`;
|
|
183
|
+
}
|
|
184
|
+
result[key] = { command: wrapped.command, args: wrapped.args };
|
|
185
|
+
}
|
|
186
|
+
return result;
|
|
165
187
|
}
|
|
166
|
-
// Full-access turns only: claude --mcp-config with
|
|
167
|
-
|
|
168
|
-
|
|
188
|
+
// Full-access turns only: claude --mcp-config with the exact host-authorized
|
|
189
|
+
// stdio servers. Empty means empty; there is no legacy or provider seed.
|
|
190
|
+
function cliMcpConfigPath(servers, options = {}) {
|
|
191
|
+
const dir = path.join(userDataDir(options.env || process.env), "mcp");
|
|
169
192
|
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
170
193
|
try { fs.chmodSync(dir, 0o700); } catch { /* Windows/best-effort */ }
|
|
171
|
-
const mcpServers =
|
|
172
|
-
for (const s of servers || []) {
|
|
173
|
-
if (!s || s.enabled === 0 || s.transport !== "stdio" || !s.command) continue;
|
|
174
|
-
mcpServers[mcpKey(s)] = { command: s.command, args: mcpStdioArgs(s) };
|
|
175
|
-
}
|
|
194
|
+
const mcpServers = wrappedMcpServerMap(servers, options);
|
|
176
195
|
const body = JSON.stringify({ mcpServers }, null, 2);
|
|
177
196
|
// 서로 다른 동시 실행이 하나의 agentlas-cli-mcp.json을 덮어쓰지 않도록 내용 주소 파일을 쓴다.
|
|
178
197
|
const digest = crypto.createHash("sha256").update(body).digest("hex").slice(0, 20);
|
|
@@ -191,17 +210,13 @@ function cliMcpConfigPath(servers) {
|
|
|
191
210
|
try { fs.chmodSync(file, 0o600); } catch { /* Windows/best-effort */ }
|
|
192
211
|
return { file, names: Object.keys(mcpServers) };
|
|
193
212
|
}
|
|
194
|
-
// Full-access turns only: codex -c mcp_servers.* with
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
if (!s || s.enabled === 0 || s.transport !== "stdio" || !s.command) continue;
|
|
202
|
-
const k = mcpKey(s);
|
|
203
|
-
out.push("-c", `mcp_servers.${k}.command=${JSON.stringify(s.command)}`);
|
|
204
|
-
out.push("-c", `mcp_servers.${k}.args=${JSON.stringify(mcpStdioArgs(s))}`);
|
|
213
|
+
// Full-access turns only: codex -c mcp_servers.* with the same exact Build
|
|
214
|
+
// allowlist semantics as Claude.
|
|
215
|
+
function codexMcpArgs(servers, options = {}) {
|
|
216
|
+
const out = [];
|
|
217
|
+
for (const [key, server] of Object.entries(wrappedMcpServerMap(servers, options))) {
|
|
218
|
+
out.push("-c", `mcp_servers.${key}.command=${JSON.stringify(server.command)}`);
|
|
219
|
+
out.push("-c", `mcp_servers.${key}.args=${JSON.stringify(server.args)}`);
|
|
205
220
|
}
|
|
206
221
|
return out;
|
|
207
222
|
}
|
|
@@ -276,7 +291,7 @@ function claudePermissionArgs(permission) {
|
|
|
276
291
|
return ["--permission-mode", "plan"];
|
|
277
292
|
}
|
|
278
293
|
|
|
279
|
-
function claudeArgs({ prompt, systemPrompt, permission, session, model, effort, mcpServers }) {
|
|
294
|
+
function claudeArgs({ prompt, systemPrompt, permission, session, model, effort, mcpServers, mcpAllowlistMode, env }) {
|
|
280
295
|
const level = permissions.normalize(permission);
|
|
281
296
|
const perm = claudePermissionArgs(level);
|
|
282
297
|
// /effort → Claude Code는 think 키워드로 reasoning 예산을 올린다(전용 CLI 플래그 없음).
|
|
@@ -294,8 +309,9 @@ function claudeArgs({ prompt, systemPrompt, permission, session, model, effort,
|
|
|
294
309
|
// MCP tools can mutate state outside the workspace sandbox. Until the desktop schema
|
|
295
310
|
// carries a trustworthy readOnlyHint per tool, only explicit full access may inject them.
|
|
296
311
|
if (level === "full") {
|
|
297
|
-
const mcpCfg = cliMcpConfigPath(mcpServers);
|
|
298
|
-
args.push("--strict-mcp-config", "--mcp-config", mcpCfg.file
|
|
312
|
+
const mcpCfg = cliMcpConfigPath(mcpServers, { exactAllowlist: mcpAllowlistMode === "exact", env });
|
|
313
|
+
args.push("--strict-mcp-config", "--mcp-config", mcpCfg.file);
|
|
314
|
+
if (mcpCfg.names.length) args.push("--allowedTools", mcpCfg.names.map((n) => "mcp__" + n).join(","));
|
|
299
315
|
} else {
|
|
300
316
|
args.push(...claudeMcpIsolationArgs());
|
|
301
317
|
}
|
|
@@ -433,13 +449,14 @@ function codexPermissionArgs(permission) {
|
|
|
433
449
|
return ["--sandbox", level === "write" ? "workspace-write" : "read-only", "-c", 'approval_policy="never"'];
|
|
434
450
|
}
|
|
435
451
|
|
|
436
|
-
function codexArgs({ prompt, systemPrompt, permission, session, cwd, model, effort, mcpServers }) {
|
|
452
|
+
function codexArgs({ prompt, systemPrompt, permission, session, cwd, model, effort, mcpServers, mcpAllowlistMode, env }) {
|
|
437
453
|
const level = permissions.normalize(permission);
|
|
438
454
|
const sandbox = codexPermissionArgs(level);
|
|
439
|
-
const mcp = level === "full" ? codexMcpArgs(mcpServers) : [];
|
|
455
|
+
const mcp = level === "full" ? codexMcpArgs(mcpServers, { exactAllowlist: mcpAllowlistMode === "exact", env }) : [];
|
|
440
456
|
const mdl = model ? ["-m", model] : []; // /model parity
|
|
441
|
-
//
|
|
442
|
-
|
|
457
|
+
// Current Codex model inventory advertises max directly; preserve the user's
|
|
458
|
+
// explicit pin instead of silently weakening it to high.
|
|
459
|
+
const eff = effort ? ["-c", `model_reasoning_effort="${effort}"`] : [];
|
|
443
460
|
const full = systemPrompt && !(session && session.id) ? `[SYSTEM]\n${systemPrompt}\n\n${prompt}` : prompt;
|
|
444
461
|
// -C/--sandbox/--skip-git-repo-check 는 `codex exec` 옵션이라 `resume <id>` 토큰 *앞에* 와야 한다.
|
|
445
462
|
// (codex-cli 0.133: resume 뒤에 두면 `unexpected argument` 로 거부 → 멀티턴 전부 실패. 실측 검증됨.)
|
|
@@ -582,22 +599,84 @@ function truncateLines(s, n) {
|
|
|
582
599
|
|
|
583
600
|
// ── gemini (stream-json 구조화 렌더 — claude/codex와 동일 파리티) ──
|
|
584
601
|
// gemini-cli는 -o stream-json 으로 init/message(delta)/tool_use/tool_result/result 이벤트를 낸다(실측).
|
|
602
|
+
function geminiSystemSettingsSourcePath(env = process.env) {
|
|
603
|
+
if (env.GEMINI_CLI_SYSTEM_SETTINGS_PATH) return path.resolve(env.GEMINI_CLI_SYSTEM_SETTINGS_PATH);
|
|
604
|
+
if (process.platform === "darwin") return "/Library/Application Support/GeminiCli/settings.json";
|
|
605
|
+
if (process.platform === "win32") return "C:\\ProgramData\\gemini-cli\\settings.json";
|
|
606
|
+
return "/etc/gemini-cli/settings.json";
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function geminiSystemDefaultsSourcePath(env = process.env) {
|
|
610
|
+
if (env.GEMINI_CLI_SYSTEM_DEFAULTS_PATH) return path.resolve(env.GEMINI_CLI_SYSTEM_DEFAULTS_PATH);
|
|
611
|
+
return path.join(path.dirname(geminiSystemSettingsSourcePath(env)), "system-defaults.json");
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function geminiMcpIsolationReadiness(env = process.env) {
|
|
615
|
+
const managed = path.resolve(userDataDir(env), "mcp");
|
|
616
|
+
for (const source of [geminiSystemSettingsSourcePath(env), geminiSystemDefaultsSourcePath(env)]) {
|
|
617
|
+
let stat;
|
|
618
|
+
try { stat = fs.lstatSync(source); }
|
|
619
|
+
catch (error) {
|
|
620
|
+
if (error && error.code === "ENOENT") continue;
|
|
621
|
+
return { ready: false, reason: "system-settings-unreadable" };
|
|
622
|
+
}
|
|
623
|
+
const relative = path.relative(managed, path.resolve(source));
|
|
624
|
+
const insideManaged = relative && !path.isAbsolute(relative) && relative !== ".." && !relative.startsWith(`..${path.sep}`);
|
|
625
|
+
if (insideManaged && stat.isFile() && !stat.isSymbolicLink()) continue;
|
|
626
|
+
// Replacing a real organization system policy/defaults file would weaken
|
|
627
|
+
// policy. A Gemini Build therefore degrades to empty-MCP on this host.
|
|
628
|
+
return { ready: false, reason: "system-settings-conflict" };
|
|
629
|
+
}
|
|
630
|
+
return { ready: true, reason: "no-system-settings-conflict" };
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function prepareGeminiRuntimeEnv(env = process.env, options = {}) {
|
|
634
|
+
const base = { ...env };
|
|
635
|
+
const exactAllowlist = options.mcpAllowlistMode === "exact";
|
|
636
|
+
const mcpServers = wrappedMcpServerMap(options.mcpServers, { exactAllowlist, env: base });
|
|
637
|
+
if (!Object.keys(mcpServers).length) return base;
|
|
638
|
+
const readiness = geminiMcpIsolationReadiness(base);
|
|
639
|
+
if (!readiness.ready) {
|
|
640
|
+
const error = new Error("Gemini MCP isolation is unavailable because host system settings must be preserved");
|
|
641
|
+
error.code = "AGENTLAS_GEMINI_MCP_ISOLATION_UNAVAILABLE";
|
|
642
|
+
throw error;
|
|
643
|
+
}
|
|
644
|
+
const dir = path.join(userDataDir(base), "mcp");
|
|
645
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
646
|
+
try { fs.chmodSync(dir, 0o700); } catch { /* Windows/best-effort */ }
|
|
647
|
+
const names = Object.keys(mcpServers);
|
|
648
|
+
const body = JSON.stringify({ mcpServers, mcp: { allowed: names } }, null, 2);
|
|
649
|
+
const digest = crypto.createHash("sha256").update(body).digest("hex").slice(0, 20);
|
|
650
|
+
const file = path.join(dir, `agentlas-gemini-mcp-${digest}.json`);
|
|
651
|
+
let current = null;
|
|
652
|
+
try { current = fs.readFileSync(file, "utf8"); } catch { /* first write */ }
|
|
653
|
+
if (current !== body) writeManagedFile(file, body);
|
|
654
|
+
base.GEMINI_CLI_SYSTEM_SETTINGS_PATH = file;
|
|
655
|
+
return base;
|
|
656
|
+
}
|
|
657
|
+
|
|
585
658
|
function geminiPermissionArgs(permission) {
|
|
586
659
|
const level = permissions.normalize(permission);
|
|
587
660
|
const approvalMode = level === "full" ? "yolo" : level === "write" ? "auto_edit" : "plan";
|
|
588
661
|
return ["--approval-mode", approvalMode];
|
|
589
662
|
}
|
|
590
663
|
|
|
591
|
-
function geminiArgs({ prompt, systemPrompt, permission, model }) {
|
|
664
|
+
function geminiArgs({ prompt, systemPrompt, permission, model, mcpServers, mcpAllowlistMode, env }) {
|
|
592
665
|
const level = permissions.normalize(permission);
|
|
593
666
|
// Gemini CLI 0.50 exposes three matching modes: plan, auto_edit, and yolo.
|
|
594
667
|
const approval = geminiPermissionArgs(level);
|
|
595
668
|
const mdl = model ? ["-m", model] : []; // /model parity
|
|
669
|
+
const allowedMcpNames = level === "full"
|
|
670
|
+
? Object.keys(wrappedMcpServerMap(mcpServers, { exactAllowlist: mcpAllowlistMode === "exact", env }))
|
|
671
|
+
: [];
|
|
672
|
+
const exactMcp = level === "full"
|
|
673
|
+
? ["--allowed-mcp-server-names", allowedMcpNames.join(",") || `__agentlas_no_mcp_${crypto.randomUUID()}__`]
|
|
674
|
+
: [];
|
|
596
675
|
return [
|
|
597
676
|
"--output-format", "stream-json",
|
|
598
677
|
"--skip-trust", // 헤드리스: 이 세션 동안 워크스페이스 신뢰 (untrusted dir exit 55 방지)
|
|
599
678
|
...approval,
|
|
600
|
-
...(level === "full" ?
|
|
679
|
+
...(level === "full" ? exactMcp : geminiMcpIsolationArgs()),
|
|
601
680
|
...mdl,
|
|
602
681
|
"--prompt", systemPrompt ? `[SYSTEM]\n${systemPrompt}\n\n${prompt}` : prompt,
|
|
603
682
|
];
|
|
@@ -691,6 +770,15 @@ function handleGeminiLine(line, st, ui) {
|
|
|
691
770
|
function runNativeTurn(req) {
|
|
692
771
|
const { kind, bin, ui } = req;
|
|
693
772
|
const cwd = req.cwd;
|
|
773
|
+
let launchReq = req;
|
|
774
|
+
if (
|
|
775
|
+
kind === "gemini" && permissions.normalize(req.permission) === "full" &&
|
|
776
|
+
selectedMcpServers(req.mcpServers, { exactAllowlist: req.mcpAllowlistMode === "exact" }).length &&
|
|
777
|
+
!geminiMcpIsolationReadiness(req.env || process.env).ready
|
|
778
|
+
) {
|
|
779
|
+
ui.warn("Gemini system policy settings are present; MCP attachment was isolated to empty mode for this turn.");
|
|
780
|
+
launchReq = { ...req, mcpServers: [], mcpAllowlistMode: "exact" };
|
|
781
|
+
}
|
|
694
782
|
const st = {
|
|
695
783
|
text: "",
|
|
696
784
|
finalText: "",
|
|
@@ -710,13 +798,13 @@ function runNativeTurn(req) {
|
|
|
710
798
|
let plainStream = false;
|
|
711
799
|
try {
|
|
712
800
|
if (kind === "claude-code") {
|
|
713
|
-
args = claudeArgs(
|
|
801
|
+
args = claudeArgs(launchReq);
|
|
714
802
|
lineHandler = (l) => handleClaudeLine(l, st, ui);
|
|
715
803
|
} else if (kind === "codex") {
|
|
716
|
-
args = codexArgs({ ...
|
|
804
|
+
args = codexArgs({ ...launchReq, cwd });
|
|
717
805
|
lineHandler = (l) => handleCodexLine(l, st, ui);
|
|
718
806
|
} else if (kind === "gemini") {
|
|
719
|
-
args = geminiArgs(
|
|
807
|
+
args = geminiArgs(launchReq);
|
|
720
808
|
lineHandler = (l) => handleGeminiLine(l, st, ui);
|
|
721
809
|
} else {
|
|
722
810
|
return Promise.resolve({ text: "", session: st.session, error: `unknown runtime: ${kind}` });
|
|
@@ -733,9 +821,9 @@ function runNativeTurn(req) {
|
|
|
733
821
|
let child;
|
|
734
822
|
try {
|
|
735
823
|
const spawnImpl = req.spawn || spawn;
|
|
736
|
-
const childEnv =
|
|
737
|
-
? (
|
|
738
|
-
: runtimeEnvForKind(kind,
|
|
824
|
+
const childEnv = launchReq.prepareRuntimeEnv === false
|
|
825
|
+
? (launchReq.env || process.env)
|
|
826
|
+
: runtimeEnvForKind(kind, launchReq.env || process.env, launchReq);
|
|
739
827
|
child = spawnImpl(bin, args, {
|
|
740
828
|
cwd,
|
|
741
829
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -902,6 +990,8 @@ module.exports = {
|
|
|
902
990
|
claudeMcpIsolationArgs,
|
|
903
991
|
geminiMcpIsolationArgs,
|
|
904
992
|
prepareCodexRuntimeEnv,
|
|
993
|
+
prepareGeminiRuntimeEnv,
|
|
994
|
+
geminiMcpIsolationReadiness,
|
|
905
995
|
runtimeEnvForKind,
|
|
906
996
|
cliMcpConfigPath,
|
|
907
997
|
codexMcpArgs,
|