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.
- package/CHANGELOG.md +106 -0
- package/README.md +319 -341
- package/bin/agentlas.cjs +32 -9
- package/engine/agentlas-banner.cjs +24 -3
- package/engine/agentlas-composer.cjs +239 -31
- package/engine/agentlas-config.cjs +103 -7
- package/engine/agentlas-core-harness.cjs +48 -4
- package/engine/agentlas-evolution.cjs +3 -4
- package/engine/agentlas-i18n.cjs +88 -32
- package/engine/agentlas-input.cjs +234 -18
- package/engine/agentlas-memory-governance.cjs +3 -5
- package/engine/agentlas-memory-import.cjs +3 -3
- package/engine/agentlas-native-host.cjs +200 -9
- package/engine/agentlas-onboard.cjs +112 -20
- package/engine/agentlas-permissions.cjs +14 -7
- package/engine/agentlas-sqlite-policy.cjs +34 -0
- package/engine/agentlas-ui.cjs +93 -42
- package/engine/agentlas-workforce.cjs +472 -58
- package/engine/agentlas-workload-routing.cjs +8 -3
- package/engine/agentlas.cjs +140 -12367
- package/engine/agents/files.cjs +61 -0
- package/engine/agents/import-local.cjs +237 -0
- package/engine/agents/registry.cjs +158 -0
- package/engine/agents/router.cjs +565 -0
- package/engine/agents/routes.cjs +43 -0
- package/engine/architecture.data.json +2 -2
- package/engine/automation/daemon.cjs +335 -0
- package/engine/automation/schedule.cjs +181 -0
- package/engine/automation/store.cjs +209 -0
- package/engine/cloud/auth.cjs +279 -0
- package/engine/cloud/hub-client.cjs +239 -0
- package/engine/cloud-assets/cargo.cjs +49 -0
- package/engine/cloud-assets/cas.cjs +235 -0
- package/engine/cloud-assets/commands.cjs +273 -0
- package/engine/cloud-assets/package.cjs +936 -0
- package/engine/cloud-assets/restore.cjs +172 -0
- package/engine/cloud-assets/state.cjs +268 -0
- package/engine/commands/automation.cjs +195 -0
- package/engine/commands/billing.cjs +96 -0
- package/engine/commands/browser.cjs +20 -0
- package/engine/commands/build.cjs +33 -0
- package/engine/commands/call.cjs +24 -0
- package/engine/commands/career-graph.cjs +51 -0
- package/engine/commands/cd.cjs +22 -0
- package/engine/commands/chat.cjs +12 -0
- package/engine/commands/chats.cjs +28 -0
- package/engine/commands/cloud.cjs +17 -0
- package/engine/commands/connect.cjs +20 -0
- package/engine/commands/context.cjs +66 -0
- package/engine/commands/creds.cjs +203 -0
- package/engine/commands/doctor.cjs +68 -0
- package/engine/commands/env.cjs +33 -0
- package/engine/commands/evolve.cjs +23 -0
- package/engine/commands/experience.cjs +34 -0
- package/engine/commands/film.cjs +8 -0
- package/engine/commands/firm.cjs +115 -0
- package/engine/commands/help.cjs +65 -0
- package/engine/commands/hep.cjs +23 -0
- package/engine/commands/import.cjs +35 -0
- package/engine/commands/index.cjs +136 -0
- package/engine/commands/install.cjs +27 -0
- package/engine/commands/journal.cjs +31 -0
- package/engine/commands/legacy-network.cjs +29 -0
- package/engine/commands/list.cjs +49 -0
- package/engine/commands/login.cjs +68 -0
- package/engine/commands/logout.cjs +28 -0
- package/engine/commands/mcp.cjs +91 -0
- package/engine/commands/memory.cjs +21 -0
- package/engine/commands/multimodal.cjs +91 -0
- package/engine/commands/native.cjs +34 -0
- package/engine/commands/netadmin.cjs +31 -0
- package/engine/commands/oberon.cjs +70 -0
- package/engine/commands/ontology.cjs +24 -0
- package/engine/commands/open.cjs +48 -0
- package/engine/commands/plugin.cjs +101 -0
- package/engine/commands/project.cjs +47 -0
- package/engine/commands/research.cjs +36 -0
- package/engine/commands/route.cjs +37 -0
- package/engine/commands/run.cjs +149 -0
- package/engine/commands/search.cjs +55 -0
- package/engine/commands/setup.cjs +45 -0
- package/engine/commands/storm.cjs +75 -0
- package/engine/commands/swarm.cjs +75 -0
- package/engine/commands/telegram.cjs +32 -0
- package/engine/commands/uninstall.cjs +68 -0
- package/engine/commands/update.cjs +54 -0
- package/engine/commands/upload.cjs +18 -0
- package/engine/commands/usage.cjs +35 -0
- package/engine/commands/variant.cjs +25 -0
- package/engine/commands/version.cjs +9 -0
- package/engine/commands/whoami.cjs +38 -0
- package/engine/commands/workforce.cjs +103 -0
- package/engine/core/db.cjs +160 -0
- package/engine/core/paths.cjs +35 -0
- package/engine/experience/build.cjs +181 -0
- package/engine/experience/intents.cjs +492 -0
- package/engine/experience/runtime.cjs +242 -0
- package/engine/experience/variant.cjs +196 -0
- package/engine/firms/orchestrate.cjs +333 -0
- package/engine/hephaestus/runtime.cjs +697 -0
- package/engine/hub/install.cjs +872 -0
- package/engine/hub/plugins.cjs +213 -0
- package/engine/mcp/consent.cjs +289 -0
- package/engine/mcp/contract.cjs +202 -0
- package/engine/mcp/index.cjs +43 -0
- package/engine/mcp/inventory.cjs +322 -0
- package/engine/mcp/plan.cjs +286 -0
- package/engine/mcp/probe.cjs +151 -0
- package/engine/memory-cli/curate.cjs +163 -0
- package/engine/oberon/common.cjs +69 -0
- package/engine/oberon/manifest.cjs +164 -0
- package/engine/oberon/outputs.cjs +70 -0
- package/engine/oberon/render.cjs +164 -0
- package/engine/project/career-graph.cjs +249 -0
- package/engine/project/credentials.cjs +262 -0
- package/engine/project/env-file.cjs +46 -0
- package/engine/project/index.cjs +27 -0
- package/engine/project/memory-context.cjs +453 -0
- package/engine/project/ontology.cjs +467 -0
- package/engine/project/paths.cjs +39 -0
- package/engine/project/seed.cjs +200 -0
- package/engine/project/state.cjs +403 -0
- package/engine/project/super-ontology-seed.json +3288 -0
- package/engine/runtimes/detect.cjs +54 -0
- package/engine/runtimes/overrides.cjs +139 -0
- package/engine/runtimes/resolve.cjs +64 -0
- package/engine/sessions/apply-fences.cjs +188 -0
- package/engine/sessions/fences.cjs +362 -0
- package/engine/sessions/orchestrator.cjs +170 -0
- package/engine/sessions/prompt.cjs +212 -0
- package/engine/sessions/session.cjs +245 -0
- package/engine/sessions/sink.cjs +54 -0
- package/engine/sessions/store.cjs +79 -0
- package/engine/storm/deps.cjs +88 -0
- package/engine/storm/storm.cjs +218 -0
- package/engine/storm/swarm.cjs +422 -0
- package/engine/ui/palette.cjs +105 -0
- package/engine/ui/renderer.cjs +85 -0
- package/engine/ui/repl.cjs +444 -0
- package/engine/workforce/capture.cjs +701 -0
- package/engine/workforce/deps.cjs +472 -0
- package/package.json +2 -6
- package/engine/agentlas-experience-mcp.cjs +0 -1709
- package/engine/agentlas-parity.cjs +0 -1499
- package/engine/agentlas-repl.cjs +0 -1780
|
@@ -0,0 +1,701 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* workforce/capture — 헤드리스 러너 2종: CLI 캡처(captureRuntime) + BYOK 원샷(runApi),
|
|
4
|
+
* 그리고 워크포스 워커의 자식 env 빌더(buildChildEnv).
|
|
5
|
+
*
|
|
6
|
+
* v1 모놀리스(engine/agentlas.cjs, legacy-v1-engine-snapshot)의 captureRuntime /
|
|
7
|
+
* runApi / buildChildEnvCli 를 계약 그대로 포팅했다. 계약 자체는
|
|
8
|
+
* test/capture-runtime-guard.cjs 가 고정한다 — 아래 속성을 약화시키면 안 된다:
|
|
9
|
+
*
|
|
10
|
+
* - idle/total 타임아웃 + SIGTERM→SIGKILL 승격 + kill-grace 강제 해제
|
|
11
|
+
* (child가 close를 영영 안 줘도 캡처 슬롯은 반드시 풀린다)
|
|
12
|
+
* - 출력 상한(AGENTLAS_CAPTURE_MAX_OUTPUT_BYTES, 64KB..32MB) 초과 즉시 중단 —
|
|
13
|
+
* 무한 출력 버퍼링 금지
|
|
14
|
+
* - AbortSignal 전파(운영자 취소)
|
|
15
|
+
* - no-authority 캡처: 프롬프트를 argv로만 전달(stdin 에코 없음), MCP는
|
|
16
|
+
* 정확-공백 격리(claudeMcpIsolationArgs/geminiMcpIsolationArgs), 툴 전면 차단
|
|
17
|
+
* - 자식 env는 native-host.runtimeEnvForKind 로 신원 격리(CODEX_HOME 등),
|
|
18
|
+
* 프로젝트 .env가 보호 키(타임아웃/캡처 상한 포함)를 덮지 못한다
|
|
19
|
+
*/
|
|
20
|
+
const fs = require("node:fs");
|
|
21
|
+
const os = require("node:os");
|
|
22
|
+
const path = require("node:path");
|
|
23
|
+
const { spawn } = require("node:child_process");
|
|
24
|
+
const { dbPath, userDataDir } = require("../core/paths.cjs");
|
|
25
|
+
|
|
26
|
+
// 캡처 드라이버가 검증된 런타임만. v2 detect.cjs의 RUNTIME_BIN에는 kimi/grok/cursor도
|
|
27
|
+
// 있지만 buildArgs/텍스트 추출 계약이 없으므로 여기 목록에 절대 조용히 추가하지 않는다.
|
|
28
|
+
const RUNTIME_BIN = {
|
|
29
|
+
"claude-code": "claude",
|
|
30
|
+
codex: "codex",
|
|
31
|
+
gemini: "gemini",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const SERVICE = "com.agentlas.desktop";
|
|
35
|
+
|
|
36
|
+
function readKeytar() {
|
|
37
|
+
try {
|
|
38
|
+
return require("keytar");
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// v1 which 포팅: PATH + 알려진 설치 위치. detect.whichSync(spawn `which`)와 달리
|
|
45
|
+
// 캡처 경로는 프로세스 spawn 없이 결정론적으로 실행 파일을 찾는다.
|
|
46
|
+
function which(cmd) {
|
|
47
|
+
const paths = (process.env.PATH || "").split(path.delimiter);
|
|
48
|
+
const exts = process.platform === "win32" ? [".cmd", ".exe", ""] : [""];
|
|
49
|
+
const extra = [
|
|
50
|
+
path.join(os.homedir(), ".claude/local"),
|
|
51
|
+
path.join(os.homedir(), ".codex/bin"),
|
|
52
|
+
path.join(os.homedir(), ".gemini/bin"),
|
|
53
|
+
"/opt/homebrew/bin",
|
|
54
|
+
"/usr/local/bin",
|
|
55
|
+
];
|
|
56
|
+
for (const dir of [...paths, ...extra]) {
|
|
57
|
+
for (const ext of exts) {
|
|
58
|
+
const full = path.join(dir, cmd + ext);
|
|
59
|
+
try {
|
|
60
|
+
fs.accessSync(full, fs.constants.X_OK);
|
|
61
|
+
return full;
|
|
62
|
+
} catch {
|
|
63
|
+
/* next */
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function finiteTimeoutMs(value, fallback, min, max) {
|
|
71
|
+
const parsed = Number(value);
|
|
72
|
+
if (!Number.isFinite(parsed)) return fallback;
|
|
73
|
+
return Math.min(max, Math.max(min, Math.trunc(parsed)));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const CAPTURE_OUTPUT_DEFAULT_BYTES = 4 * 1024 * 1024;
|
|
77
|
+
function captureOutputLimit(env = process.env) {
|
|
78
|
+
return finiteTimeoutMs(env.AGENTLAS_CAPTURE_MAX_OUTPUT_BYTES, CAPTURE_OUTPUT_DEFAULT_BYTES, 64 * 1024, 32 * 1024 * 1024);
|
|
79
|
+
}
|
|
80
|
+
function directCaptureOutputLimit(value) {
|
|
81
|
+
return finiteTimeoutMs(value, CAPTURE_OUTPUT_DEFAULT_BYTES, 128, 32 * 1024 * 1024);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ── 캡처 인자 빌드 (v1 buildArgs 포팅) ──────────────────────────────────
|
|
85
|
+
// 보안 매핑은 native-host/permissions가 단일 정본이다. 여기서는 조립만 한다.
|
|
86
|
+
function buildArgs(kind, systemPrompt, prompt, permission, runtimeOptions = {}) {
|
|
87
|
+
const native = require("../agentlas-native-host.cjs");
|
|
88
|
+
const level = require("../agentlas-permissions.cjs").normalize(permission);
|
|
89
|
+
const model = runtimeOptions.model ? String(runtimeOptions.model) : null;
|
|
90
|
+
const effort = runtimeOptions.effort ? String(runtimeOptions.effort) : null;
|
|
91
|
+
const noAuthority = runtimeOptions.authorityMode === "no-authority";
|
|
92
|
+
if (kind === "claude-code") {
|
|
93
|
+
const perm = native.claudePermissionArgs(noAuthority ? "read" : level);
|
|
94
|
+
// 백그라운드/캡처 경로에는 검토된 MCP 서버 목록이 없다. full 권한은 툴 권한이지
|
|
95
|
+
// MCP 동의가 아니므로 이 경로는 항상 정확-공백 MCP 격리를 유지한다.
|
|
96
|
+
const mcp = native.claudeMcpIsolationArgs();
|
|
97
|
+
const thinking = effort === "max" || effort === "xhigh" ? "Ultrathink. " : effort === "high" ? "Think hard. " : effort === "medium" ? "Think. " : "";
|
|
98
|
+
const claudeEffort = effort === "minimal" ? "low" : effort === "xhigh" ? "max" : effort;
|
|
99
|
+
const effortArgs = claudeEffort && claudeEffort !== "none" ? ["--effort", claudeEffort] : [];
|
|
100
|
+
return [
|
|
101
|
+
"-p", thinking + prompt,
|
|
102
|
+
"--append-system-prompt", systemPrompt,
|
|
103
|
+
...(model ? ["--model", model] : []),
|
|
104
|
+
...effortArgs,
|
|
105
|
+
...perm,
|
|
106
|
+
...(noAuthority ? ["--tools", ""] : []),
|
|
107
|
+
...mcp,
|
|
108
|
+
];
|
|
109
|
+
}
|
|
110
|
+
if (kind === "codex") {
|
|
111
|
+
const perm = native.codexPermissionArgs(noAuthority ? "read" : level);
|
|
112
|
+
const mcp = [];
|
|
113
|
+
const modelArgs = model ? ["-m", model] : [];
|
|
114
|
+
const effortArgs = effort ? ["-c", `model_reasoning_effort="${effort}"`] : [];
|
|
115
|
+
const noAuthorityArgs = noAuthority ? [
|
|
116
|
+
"--ephemeral",
|
|
117
|
+
"--ignore-user-config",
|
|
118
|
+
"--ignore-rules",
|
|
119
|
+
"--disable", "shell_tool",
|
|
120
|
+
"--disable", "unified_exec",
|
|
121
|
+
"--disable", "apps",
|
|
122
|
+
"--disable", "browser_use",
|
|
123
|
+
"--disable", "computer_use",
|
|
124
|
+
"--disable", "image_generation",
|
|
125
|
+
"--disable", "workspace_dependencies",
|
|
126
|
+
"--disable", "goals",
|
|
127
|
+
"--disable", "memories",
|
|
128
|
+
"--disable", "plugins",
|
|
129
|
+
"--disable", "hooks",
|
|
130
|
+
"--disable", "multi_agent",
|
|
131
|
+
"--disable", "tool_suggest",
|
|
132
|
+
"--json",
|
|
133
|
+
] : [];
|
|
134
|
+
return ["exec", "--skip-git-repo-check", ...noAuthorityArgs, ...modelArgs, ...effortArgs, ...perm, ...mcp, `[SYSTEM]\n${systemPrompt}\n\n${prompt}`];
|
|
135
|
+
}
|
|
136
|
+
if (kind === "gemini") {
|
|
137
|
+
const perm = native.geminiPermissionArgs(noAuthority ? "read" : level);
|
|
138
|
+
if (noAuthority && !runtimeOptions.noToolsPolicyPath) {
|
|
139
|
+
// Gemini는 툴 공백을 증명할 명시적 deny-all 정책 파일 없이는 no-authority 실행 금지.
|
|
140
|
+
throw new Error("Gemini no-authority capture requires an explicit deny-all policy");
|
|
141
|
+
}
|
|
142
|
+
const noAuthorityArgs = noAuthority
|
|
143
|
+
? ["--admin-policy", String(runtimeOptions.noToolsPolicyPath)]
|
|
144
|
+
: [];
|
|
145
|
+
// full 권한이어도 사용자의 전역 Gemini MCP 정의(자격증명 env 동반)를 상속하지 않는다.
|
|
146
|
+
const mcp = native.geminiMcpIsolationArgs();
|
|
147
|
+
return ["--prompt", `[SYSTEM]\n${systemPrompt}\n\n${prompt}`, ...(model ? ["-m", model] : []), ...perm, ...noAuthorityArgs, ...mcp];
|
|
148
|
+
}
|
|
149
|
+
return [prompt];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// codex --json 이벤트 스트림에서 최종 에이전트 텍스트만 추출.
|
|
153
|
+
function codexCaptureAgentText(jsonl) {
|
|
154
|
+
const completed = [];
|
|
155
|
+
const latest = new Map();
|
|
156
|
+
for (const line of String(jsonl || "").split(/\r?\n/)) {
|
|
157
|
+
if (!line.trim()) continue;
|
|
158
|
+
let event;
|
|
159
|
+
try { event = JSON.parse(line); } catch { continue; }
|
|
160
|
+
const item = event?.item;
|
|
161
|
+
if (!item || item.type !== "agent_message" || typeof item.text !== "string") continue;
|
|
162
|
+
if (event.type === "item.completed") completed.push(item.text);
|
|
163
|
+
else if (event.type === "item.started" || event.type === "item.updated") latest.set(String(item.id || latest.size), item.text);
|
|
164
|
+
}
|
|
165
|
+
if (completed.length) return completed.join("");
|
|
166
|
+
return [...latest.values()].join("");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function capturedRuntimeAgentText(kind, raw) {
|
|
170
|
+
const text = String(raw || "");
|
|
171
|
+
const events = [];
|
|
172
|
+
for (const line of text.split(/\r?\n/)) {
|
|
173
|
+
if (!line.trim()) continue;
|
|
174
|
+
try {
|
|
175
|
+
events.push(JSON.parse(line));
|
|
176
|
+
} catch {
|
|
177
|
+
// 네이티브 CLI는 캡처 모드에서 보통 평문을 준다. 한 줄이라도 JSON이 아니면
|
|
178
|
+
// 추측하지 말고 전체 결과를 보존한다.
|
|
179
|
+
return text.trim();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (!events.length) return "";
|
|
183
|
+
|
|
184
|
+
if (kind === "claude-code") {
|
|
185
|
+
const isProtocol = events.some((event) =>
|
|
186
|
+
["system", "stream_event", "result", "rate_limit_event"].includes(event?.type),
|
|
187
|
+
);
|
|
188
|
+
if (!isProtocol) return text.trim();
|
|
189
|
+
const final = [...events].reverse().find(
|
|
190
|
+
(event) => event?.type === "result" && typeof event.result === "string",
|
|
191
|
+
);
|
|
192
|
+
if (final) return final.result;
|
|
193
|
+
return events.map((event) => {
|
|
194
|
+
const delta = event?.type === "stream_event" ? event.event?.delta : null;
|
|
195
|
+
return delta?.type === "text_delta" && typeof delta.text === "string" ? delta.text : "";
|
|
196
|
+
}).join("");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (kind === "gemini") {
|
|
200
|
+
const isProtocol = events.some((event) =>
|
|
201
|
+
["init", "message", "tool_use", "tool_result", "result", "error"].includes(event?.type),
|
|
202
|
+
);
|
|
203
|
+
if (!isProtocol) return text.trim();
|
|
204
|
+
const assistant = events
|
|
205
|
+
.filter((event) => event?.type === "message" && event.role === "assistant")
|
|
206
|
+
.map((event) => typeof event.content === "string" ? event.content : "")
|
|
207
|
+
.join("");
|
|
208
|
+
if (assistant) return assistant;
|
|
209
|
+
const final = [...events].reverse().find((event) =>
|
|
210
|
+
event?.type === "result" &&
|
|
211
|
+
(typeof event.result === "string" || typeof event.response === "string"),
|
|
212
|
+
);
|
|
213
|
+
return final ? String(final.result ?? final.response) : "";
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return text.trim();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* 네이티브 CLI 헤드리스 1턴 캡처 — 최종 텍스트를 resolve한다.
|
|
221
|
+
* opts: { cwd, env, permission, model, effort, authorityMode, noToolsPolicyPath,
|
|
222
|
+
* timeoutConfig, outputLimitBytes, signal, spawn(테스트 주입) }
|
|
223
|
+
*/
|
|
224
|
+
function captureRuntime(kind, systemPrompt, prompt, opts) {
|
|
225
|
+
opts = opts || {};
|
|
226
|
+
const cwd = opts.cwd || runCwd();
|
|
227
|
+
const nativeHost = require("../agentlas-native-host.cjs");
|
|
228
|
+
const timeout = opts.timeoutConfig
|
|
229
|
+
? nativeHost.directNativeTimeoutConfig(opts.timeoutConfig)
|
|
230
|
+
: nativeHost.nativeTimeoutConfig(opts.env || process.env);
|
|
231
|
+
const outputLimit = opts.outputLimitBytes == null
|
|
232
|
+
? captureOutputLimit(opts.env || process.env)
|
|
233
|
+
: directCaptureOutputLimit(opts.outputLimitBytes);
|
|
234
|
+
return new Promise((resolve, reject) => {
|
|
235
|
+
const bin = which(RUNTIME_BIN[kind]) || RUNTIME_BIN[kind];
|
|
236
|
+
let child;
|
|
237
|
+
try {
|
|
238
|
+
const spawnImpl = opts.spawn || spawn;
|
|
239
|
+
const env = nativeHost.runtimeEnvForKind(kind, opts.env || process.env, {
|
|
240
|
+
permission: opts.permission,
|
|
241
|
+
mcpServers: [],
|
|
242
|
+
mcpAllowlistMode: kind === "gemini" ? "exact" : undefined,
|
|
243
|
+
});
|
|
244
|
+
const groupedChild = process.platform !== "win32" && spawnImpl === spawn;
|
|
245
|
+
child = spawnImpl(bin, buildArgs(kind, systemPrompt, prompt, opts.permission, {
|
|
246
|
+
model: opts.model,
|
|
247
|
+
effort: opts.effort,
|
|
248
|
+
authorityMode: opts.authorityMode,
|
|
249
|
+
noToolsPolicyPath: opts.noToolsPolicyPath,
|
|
250
|
+
}), {
|
|
251
|
+
cwd,
|
|
252
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
253
|
+
env,
|
|
254
|
+
detached: groupedChild,
|
|
255
|
+
windowsHide: true,
|
|
256
|
+
});
|
|
257
|
+
child.__agentlasGroupedChild = groupedChild;
|
|
258
|
+
} catch (error) {
|
|
259
|
+
reject(error);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const stdoutChunks = [];
|
|
264
|
+
const stderrChunks = [];
|
|
265
|
+
let capturedBytes = 0;
|
|
266
|
+
let settled = false;
|
|
267
|
+
let terminationError = null;
|
|
268
|
+
let idleTimer = null;
|
|
269
|
+
let totalTimer = null;
|
|
270
|
+
let killTimer = null;
|
|
271
|
+
let forceTimer = null;
|
|
272
|
+
let onStdout = () => {};
|
|
273
|
+
let onStderr = () => {};
|
|
274
|
+
let onError = () => {};
|
|
275
|
+
let onClose = () => {};
|
|
276
|
+
let onAbort = () => {};
|
|
277
|
+
|
|
278
|
+
const clearTimers = () => {
|
|
279
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
280
|
+
if (totalTimer) clearTimeout(totalTimer);
|
|
281
|
+
if (killTimer) clearTimeout(killTimer);
|
|
282
|
+
if (forceTimer) clearTimeout(forceTimer);
|
|
283
|
+
idleTimer = totalTimer = killTimer = forceTimer = null;
|
|
284
|
+
};
|
|
285
|
+
const cleanup = () => {
|
|
286
|
+
clearTimers();
|
|
287
|
+
child.stdout?.removeListener("data", onStdout);
|
|
288
|
+
child.stderr?.removeListener("data", onStderr);
|
|
289
|
+
child.removeListener("error", onError);
|
|
290
|
+
child.removeListener("close", onClose);
|
|
291
|
+
if (opts.signal) opts.signal.removeEventListener?.("abort", onAbort);
|
|
292
|
+
};
|
|
293
|
+
const finishReject = (error) => {
|
|
294
|
+
if (settled) return;
|
|
295
|
+
settled = true;
|
|
296
|
+
cleanup();
|
|
297
|
+
reject(error);
|
|
298
|
+
};
|
|
299
|
+
const finishResolve = (value) => {
|
|
300
|
+
if (settled) return;
|
|
301
|
+
settled = true;
|
|
302
|
+
cleanup();
|
|
303
|
+
resolve(value);
|
|
304
|
+
};
|
|
305
|
+
const requestStop = (error) => {
|
|
306
|
+
if (settled || terminationError) return;
|
|
307
|
+
terminationError = error;
|
|
308
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
309
|
+
if (totalTimer) clearTimeout(totalTimer);
|
|
310
|
+
idleTimer = totalTimer = null;
|
|
311
|
+
nativeHost.terminateNativeChild(child, "SIGTERM");
|
|
312
|
+
if (settled) return;
|
|
313
|
+
killTimer = setTimeout(() => {
|
|
314
|
+
if (settled) return;
|
|
315
|
+
nativeHost.terminateNativeChild(child, "SIGKILL");
|
|
316
|
+
if (settled) return;
|
|
317
|
+
// child가 close를 영영 안 줘도 캡처 슬롯을 강제로 해제한다 (v1 계약).
|
|
318
|
+
forceTimer = setTimeout(() => finishReject(terminationError), Math.max(250, Math.min(1_000, timeout.killGraceMs)));
|
|
319
|
+
}, timeout.killGraceMs);
|
|
320
|
+
};
|
|
321
|
+
const timeoutError = (phase, ms) => {
|
|
322
|
+
const error = new Error(
|
|
323
|
+
phase === "idle"
|
|
324
|
+
? `${kind} capture idle timeout: ${ms}ms 동안 출력이 없습니다.`
|
|
325
|
+
: `${kind} capture total timeout: 전체 실행 시간이 ${ms}ms를 초과했습니다.`,
|
|
326
|
+
);
|
|
327
|
+
error.code = `AGENTLAS_CAPTURE_${phase.toUpperCase()}_TIMEOUT`;
|
|
328
|
+
return error;
|
|
329
|
+
};
|
|
330
|
+
const armIdle = () => {
|
|
331
|
+
if (settled || terminationError) return;
|
|
332
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
333
|
+
idleTimer = setTimeout(() => requestStop(timeoutError("idle", timeout.idleMs)), timeout.idleMs);
|
|
334
|
+
};
|
|
335
|
+
const append = (target, chunk) => {
|
|
336
|
+
armIdle();
|
|
337
|
+
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
338
|
+
const remaining = Math.max(0, outputLimit - capturedBytes);
|
|
339
|
+
if (remaining > 0) {
|
|
340
|
+
const kept = bytes.length > remaining ? bytes.subarray(0, remaining) : bytes;
|
|
341
|
+
target.push(kept);
|
|
342
|
+
capturedBytes += kept.length;
|
|
343
|
+
}
|
|
344
|
+
if (bytes.length > remaining) {
|
|
345
|
+
const error = new Error(`${kind} capture output limit: ${outputLimit} bytes를 초과했습니다.`);
|
|
346
|
+
error.code = "AGENTLAS_CAPTURE_OUTPUT_LIMIT";
|
|
347
|
+
requestStop(error);
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
onStdout = (chunk) => append(stdoutChunks, chunk);
|
|
352
|
+
onStderr = (chunk) => append(stderrChunks, chunk);
|
|
353
|
+
onError = (error) => finishReject(terminationError || error);
|
|
354
|
+
onClose = (code) => {
|
|
355
|
+
if (terminationError) {
|
|
356
|
+
finishReject(terminationError);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
const stdout = Buffer.concat(stdoutChunks).toString("utf8");
|
|
360
|
+
const stderr = Buffer.concat(stderrChunks).toString("utf8");
|
|
361
|
+
if (code && code !== 0) {
|
|
362
|
+
finishReject(new Error(`${kind} exited ${code}: ${stderr.slice(-500)}`));
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
const raw = stdout.trim() || stderr.trim();
|
|
366
|
+
if (kind === "codex" && opts.authorityMode === "no-authority") {
|
|
367
|
+
finishResolve(codexCaptureAgentText(raw));
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
if (kind === "claude-code" || kind === "gemini") {
|
|
371
|
+
finishResolve(capturedRuntimeAgentText(kind, raw));
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
finishResolve(raw);
|
|
375
|
+
};
|
|
376
|
+
onAbort = () => {
|
|
377
|
+
const reason = opts.signal && opts.signal.reason;
|
|
378
|
+
const error = reason instanceof Error ? reason : new Error(`${kind} capture aborted`);
|
|
379
|
+
if (!error.code) error.code = "ABORT_ERR";
|
|
380
|
+
requestStop(error);
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
child.stdout.on("data", onStdout);
|
|
384
|
+
child.stderr.on("data", onStderr);
|
|
385
|
+
child.on("error", onError);
|
|
386
|
+
child.on("close", onClose);
|
|
387
|
+
armIdle();
|
|
388
|
+
totalTimer = setTimeout(() => requestStop(timeoutError("total", timeout.totalMs)), timeout.totalMs);
|
|
389
|
+
if (opts.signal) {
|
|
390
|
+
if (opts.signal.aborted) onAbort();
|
|
391
|
+
else opts.signal.addEventListener("abort", onAbort, { once: true });
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// ── API 러너 (BYOK / Ollama) — 비스트리밍, 최종 텍스트 반환 (v1 runApi 포팅) ──
|
|
397
|
+
// engine/agentlas-api-agent.cjs 는 스트리밍+툴 루프(대화형)용이다. 워크포스 워커는
|
|
398
|
+
// zero-tools 원샷이 계약이므로 v1의 비스트리밍 원샷 경로를 그대로 쓴다.
|
|
399
|
+
const DEFAULT_API_MODEL = {
|
|
400
|
+
anthropic: "claude-sonnet-4-6",
|
|
401
|
+
openai: "gpt-4o-mini",
|
|
402
|
+
google: "gemini-1.5-flash",
|
|
403
|
+
ollama: "llama3.1",
|
|
404
|
+
upstage: "solar-pro2",
|
|
405
|
+
custom: "deepseek-chat",
|
|
406
|
+
glm: "glm-4.6",
|
|
407
|
+
kimi: "kimi-k2-0711-preview",
|
|
408
|
+
deepseek: "deepseek-chat",
|
|
409
|
+
};
|
|
410
|
+
const ANTHROPIC_COMPAT_API = {
|
|
411
|
+
glm: { label: "GLM", baseUrl: "https://api.z.ai/api/anthropic" },
|
|
412
|
+
kimi: { label: "Kimi", baseUrl: "https://api.moonshot.ai/anthropic" },
|
|
413
|
+
deepseek: { label: "DeepSeek", baseUrl: "https://api.deepseek.com/anthropic" },
|
|
414
|
+
};
|
|
415
|
+
const DEFAULT_CUSTOM_API_BASE_URL = "https://api.openai.com/v1";
|
|
416
|
+
|
|
417
|
+
async function apiKey(backend) {
|
|
418
|
+
const keytar = readKeytar();
|
|
419
|
+
if (!keytar) return null;
|
|
420
|
+
// 키체인 접근 거부(서명 안 된 standalone Node)는 "키 없음"으로 조용히 처리.
|
|
421
|
+
return keytar.getPassword(SERVICE, "byok:" + backend).catch(() => null);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Custom BYOK 키가 전송될 origin 재검증: 공개 주소는 HTTPS만, HTTP는 localhost/LAN만.
|
|
425
|
+
function normalizeCustomApiBaseUrl(raw) {
|
|
426
|
+
const value = String(raw || "").trim();
|
|
427
|
+
if (!value) return DEFAULT_CUSTOM_API_BASE_URL;
|
|
428
|
+
let parsed;
|
|
429
|
+
try {
|
|
430
|
+
parsed = new URL(value);
|
|
431
|
+
} catch {
|
|
432
|
+
throw new Error("Custom API base URL is invalid.");
|
|
433
|
+
}
|
|
434
|
+
const host = parsed.hostname.toLowerCase();
|
|
435
|
+
const isLoopback = host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
|
|
436
|
+
const isPrivateLan =
|
|
437
|
+
/^10\./.test(host) || /^192\.168\./.test(host) || /^172\.(1[6-9]|2\d|3[01])\./.test(host);
|
|
438
|
+
if (parsed.protocol !== "https:" && !(parsed.protocol === "http:" && (isLoopback || isPrivateLan))) {
|
|
439
|
+
throw new Error("Custom API base URL must use HTTPS or HTTP on localhost/LAN.");
|
|
440
|
+
}
|
|
441
|
+
return value.replace(/\/+$/, "");
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/** Desktop과 공유하는 SQLite meta에서 Custom OpenAI base URL을 읽는다(읽기 전용). */
|
|
445
|
+
function readCustomApiBaseUrl() {
|
|
446
|
+
const p = dbPath();
|
|
447
|
+
if (!fs.existsSync(p)) return DEFAULT_CUSTOM_API_BASE_URL;
|
|
448
|
+
let db = null;
|
|
449
|
+
let raw = "";
|
|
450
|
+
try {
|
|
451
|
+
try {
|
|
452
|
+
const Database = require("better-sqlite3");
|
|
453
|
+
db = new Database(p, { readonly: true, fileMustExist: true });
|
|
454
|
+
} catch {
|
|
455
|
+
const { DatabaseSync } = require("node:sqlite");
|
|
456
|
+
db = new DatabaseSync(p, { readOnly: true });
|
|
457
|
+
}
|
|
458
|
+
try {
|
|
459
|
+
const row = db.prepare("SELECT value FROM meta WHERE key = 'custom_base_url'").get();
|
|
460
|
+
raw = row && row.value ? row.value : "";
|
|
461
|
+
} catch {
|
|
462
|
+
// 구버전 DB에 meta 테이블/키가 없으면 Desktop과 동일하게 OpenAI 기본 URL.
|
|
463
|
+
raw = "";
|
|
464
|
+
}
|
|
465
|
+
} catch (e) {
|
|
466
|
+
throw new Error(`Could not read the Custom API base URL from the shared database: ${(e && e.message) || e}`);
|
|
467
|
+
} finally {
|
|
468
|
+
try { if (db && typeof db.close === "function") db.close(); } catch { /* ignore close failure */ }
|
|
469
|
+
}
|
|
470
|
+
return normalizeCustomApiBaseUrl(raw);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* BYOK/Ollama 한 턴. 재사용 경로이므로 절대 process.exit하지 않고 오류를 throw해
|
|
475
|
+
* 호출자의 catch/finally가 부분 실패를 처리하게 한다.
|
|
476
|
+
* options는 회귀 테스트의 fetch/키 주입용이며 상용 호출자는 사용하지 않는다.
|
|
477
|
+
*/
|
|
478
|
+
async function runApi(backend, model, system, prompt, options) {
|
|
479
|
+
options = options || {};
|
|
480
|
+
model = model || DEFAULT_API_MODEL[backend];
|
|
481
|
+
const fetchImpl = options.fetch || globalThis.fetch;
|
|
482
|
+
if (typeof fetchImpl !== "function") throw new Error("fetch is unavailable in this runtime (run through the app runtime).");
|
|
483
|
+
if (backend === "ollama") {
|
|
484
|
+
const resp = await fetchImpl("http://127.0.0.1:11434/api/chat", {
|
|
485
|
+
method: "POST",
|
|
486
|
+
headers: { "content-type": "application/json" },
|
|
487
|
+
body: JSON.stringify({ model, stream: false, messages: [{ role: "system", content: system }, { role: "user", content: prompt }] }),
|
|
488
|
+
});
|
|
489
|
+
if (!resp.ok) throw new Error(`Ollama ${resp.status} — run 'ollama serve' and check the model`);
|
|
490
|
+
const j = await resp.json();
|
|
491
|
+
return (j.message && j.message.content) || "";
|
|
492
|
+
}
|
|
493
|
+
const supported = backend === "anthropic" || backend === "openai" || backend === "google" ||
|
|
494
|
+
backend === "upstage" || backend === "custom" || !!ANTHROPIC_COMPAT_API[backend];
|
|
495
|
+
if (!supported) throw new Error("Unsupported backend: " + backend);
|
|
496
|
+
const key = Object.prototype.hasOwnProperty.call(options, "apiKey") ? options.apiKey : await apiKey(backend);
|
|
497
|
+
if (!key) throw new Error(`${backend} API key is missing. Register it in App settings → BYOK.`);
|
|
498
|
+
|
|
499
|
+
const anthropicCompat = ANTHROPIC_COMPAT_API[backend];
|
|
500
|
+
if (backend === "anthropic" || anthropicCompat) {
|
|
501
|
+
const label = anthropicCompat ? anthropicCompat.label : "Anthropic";
|
|
502
|
+
const base = anthropicCompat ? anthropicCompat.baseUrl : "https://api.anthropic.com";
|
|
503
|
+
const authHeaders = anthropicCompat
|
|
504
|
+
? { "x-api-key": key, authorization: "Bearer " + key }
|
|
505
|
+
: { "x-api-key": key };
|
|
506
|
+
const resp = await fetchImpl(`${base}/v1/messages`, {
|
|
507
|
+
method: "POST",
|
|
508
|
+
headers: { "content-type": "application/json", ...authHeaders, "anthropic-version": "2023-06-01" },
|
|
509
|
+
body: JSON.stringify({ model, max_tokens: 4096, system, messages: [{ role: "user", content: prompt }] }),
|
|
510
|
+
});
|
|
511
|
+
if (!resp.ok) throw new Error(`${label} ${resp.status}: ${(await resp.text().catch(() => "")).slice(0, 200)}`);
|
|
512
|
+
const j = await resp.json();
|
|
513
|
+
return (j.content && j.content[0] && j.content[0].text) || "";
|
|
514
|
+
}
|
|
515
|
+
if (backend === "openai" || backend === "upstage" || backend === "custom") {
|
|
516
|
+
const base = backend === "upstage"
|
|
517
|
+
? "https://api.upstage.ai/v1"
|
|
518
|
+
: backend === "custom"
|
|
519
|
+
? normalizeCustomApiBaseUrl(Object.prototype.hasOwnProperty.call(options, "customBaseUrl")
|
|
520
|
+
? options.customBaseUrl
|
|
521
|
+
: readCustomApiBaseUrl())
|
|
522
|
+
: "https://api.openai.com/v1";
|
|
523
|
+
const label = backend === "custom" ? "Custom API" : backend === "upstage" ? "Upstage" : "OpenAI";
|
|
524
|
+
const resp = await fetchImpl(`${base}/chat/completions`, {
|
|
525
|
+
method: "POST",
|
|
526
|
+
headers: { "content-type": "application/json", authorization: "Bearer " + key },
|
|
527
|
+
body: JSON.stringify({ model, messages: [{ role: "system", content: system }, { role: "user", content: prompt }] }),
|
|
528
|
+
});
|
|
529
|
+
if (!resp.ok) throw new Error(`${label} ${resp.status}: ${(await resp.text().catch(() => "")).slice(0, 200)}`);
|
|
530
|
+
const j = await resp.json();
|
|
531
|
+
return (j.choices && j.choices[0] && j.choices[0].message && j.choices[0].message.content) || "";
|
|
532
|
+
}
|
|
533
|
+
if (backend === "google") {
|
|
534
|
+
const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${encodeURIComponent(key)}`;
|
|
535
|
+
const resp = await fetchImpl(url, {
|
|
536
|
+
method: "POST",
|
|
537
|
+
headers: { "content-type": "application/json" },
|
|
538
|
+
body: JSON.stringify({ systemInstruction: { parts: [{ text: system }] }, contents: [{ role: "user", parts: [{ text: prompt }] }] }),
|
|
539
|
+
});
|
|
540
|
+
if (!resp.ok) throw new Error(`Google ${resp.status}: ${(await resp.text().catch(() => "")).slice(0, 200)}`);
|
|
541
|
+
const j = await resp.json();
|
|
542
|
+
const c = j.candidates && j.candidates[0];
|
|
543
|
+
return (c && c.content && c.content.parts && c.content.parts[0] && c.content.parts[0].text) || "";
|
|
544
|
+
}
|
|
545
|
+
throw new Error("Unsupported backend: " + backend);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// ── 작업 폴더 규칙 (v1 runCwd/projectCwd 포팅) ─────────────────────────
|
|
549
|
+
function runCwd() {
|
|
550
|
+
const dir = path.join(userDataDir(), "agent-cwd");
|
|
551
|
+
try {
|
|
552
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
553
|
+
return dir;
|
|
554
|
+
} catch {
|
|
555
|
+
return os.homedir();
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// 에이전트가 실행될 작업 폴더 = 사용자가 명령을 친 현재 디렉터리(= 대상 프로젝트).
|
|
560
|
+
// 단, home/userData/agent-cwd 같은 "프로젝트 아님" 위치면 안전한 전용 폴더로 폴백.
|
|
561
|
+
function projectCwd() {
|
|
562
|
+
try {
|
|
563
|
+
const cwd = process.cwd();
|
|
564
|
+
if (!cwd || cwd === os.homedir() || cwd === userDataDir() || cwd === runCwd()) return runCwd();
|
|
565
|
+
return cwd;
|
|
566
|
+
} catch {
|
|
567
|
+
return runCwd();
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// ── 자식 env 빌더 (v1 buildChildEnvCli 포팅) ───────────────────────────
|
|
572
|
+
function parseDotEnv(text) {
|
|
573
|
+
const out = {};
|
|
574
|
+
for (const raw of String(text || "").split(/\r?\n/)) {
|
|
575
|
+
const line = raw.trim();
|
|
576
|
+
if (!line || line.startsWith("#")) continue;
|
|
577
|
+
const m = line.match(/^(?:export\s+)?([A-Z][A-Z0-9_]{2,})\s*=\s*(.*)$/);
|
|
578
|
+
if (!m) continue;
|
|
579
|
+
let value = m[2].trim();
|
|
580
|
+
const q = value[0];
|
|
581
|
+
if ((q === '"' || q === "'") && value.endsWith(q)) value = value.slice(1, -1);
|
|
582
|
+
out[m[1]] = value;
|
|
583
|
+
}
|
|
584
|
+
return out;
|
|
585
|
+
}
|
|
586
|
+
function readDotEnvFile(file) {
|
|
587
|
+
try {
|
|
588
|
+
const st = fs.statSync(file);
|
|
589
|
+
if (!st.isFile() || st.size > 512 * 1024) return {};
|
|
590
|
+
return parseDotEnv(fs.readFileSync(file, "utf8"));
|
|
591
|
+
} catch {
|
|
592
|
+
return {};
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
function readDotEnvDir(dir) {
|
|
596
|
+
return { ...readDotEnvFile(path.join(dir, ".env")), ...readDotEnvFile(path.join(dir, ".env.local")) };
|
|
597
|
+
}
|
|
598
|
+
function projectEnvId(projectPath) {
|
|
599
|
+
const raw = path.basename(projectPath || runCwd() || "project") || "project";
|
|
600
|
+
return raw.toUpperCase().replace(/[^A-Z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "PROJECT";
|
|
601
|
+
}
|
|
602
|
+
function projectScopedEnvValues(values, projectPath) {
|
|
603
|
+
const prefix = `AGENTLAS_PROJECT_${projectEnvId(projectPath)}_`;
|
|
604
|
+
const result = {};
|
|
605
|
+
for (const [key, value] of Object.entries(values || {})) {
|
|
606
|
+
if (!key.startsWith(prefix)) continue;
|
|
607
|
+
const actualKey = key.slice(prefix.length);
|
|
608
|
+
if (/^[A-Z][A-Z0-9_]*$/.test(actualKey)) result[actualKey] = value;
|
|
609
|
+
}
|
|
610
|
+
return result;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// 프로젝트/에이전트 dotenv는 일반 API 키 우선순위를 유지하되, 호스트 CLI의 신원·설치·
|
|
614
|
+
// 플러그인 탐색 루트는 바꾸지 못한다. Windows env도 대소문자 무관 비교(v1 계약).
|
|
615
|
+
const PROTECTED_CHILD_ENV_KEYS_CLI = new Set([
|
|
616
|
+
"HOME", "PATH", "PATHEXT", "USERPROFILE", "HOMEDRIVE", "HOMEPATH", "APPDATA", "LOCALAPPDATA",
|
|
617
|
+
"XDG_CONFIG_HOME", "XDG_DATA_HOME", "CODEX_HOME", "CLAUDE_CONFIG_DIR", "CLAUDE_CODE_SAFE_MODE",
|
|
618
|
+
"AGENTLAS_CODEX_HOME", "AGENTLAS_USER_DATA_DIR",
|
|
619
|
+
"CLAUDE_CODE_SIMPLE", "CLAUDE_PLUGIN_ROOT", "CLAUDE_PLUGIN_DATA", "CLAUDE_PROJECT_DIR",
|
|
620
|
+
"GEMINI_CLI_HOME", "GEMINI_CLI_SYSTEM_SETTINGS_PATH", "GEMINI_CLI_USER_SETTINGS",
|
|
621
|
+
"GEMINI_CLI_TRUSTED_FOLDERS_PATH", "GEMINI_CLI_TRUST_WORKSPACE", "GEMINI_CLI_EXTENSION_REGISTRY_URI",
|
|
622
|
+
"HEPHAESTUS_RUNTIME_ROOT", "HEPHAESTUS_RUNTIME_BASE", "HEPHAESTUS_PYTHON", "HEPHAESTUS_AUTO_UPDATE",
|
|
623
|
+
"HEPHAESTUS_UPDATE_CHECK", "NPM_CONFIG_PREFIX", "NODE_OPTIONS", "NODE_PATH",
|
|
624
|
+
"PYTHONHOME", "PYTHONPATH", "LD_PRELOAD", "DYLD_INSERT_LIBRARIES", "DYLD_LIBRARY_PATH",
|
|
625
|
+
"AGENTLAS_HUB_CONNECT_TIMEOUT_MS", "AGENTLAS_HUB_IDLE_TIMEOUT_MS", "AGENTLAS_HUB_TOTAL_TIMEOUT_MS",
|
|
626
|
+
"AGENTLAS_NATIVE_IDLE_TIMEOUT_MS", "AGENTLAS_NATIVE_TOTAL_TIMEOUT_MS", "AGENTLAS_NATIVE_KILL_GRACE_MS",
|
|
627
|
+
"AGENTLAS_CAPTURE_MAX_OUTPUT_BYTES",
|
|
628
|
+
]);
|
|
629
|
+
// 네트워크 무결성 키 — TLS 검증·프록시·CA·엔드포인트·세션. 비신뢰 출처(클론 레포의
|
|
630
|
+
// 프로젝트/에이전트 dotenv)로 주입되면 MITM/SSRF/세션 하이재킹. 사용자 본인의 전역
|
|
631
|
+
// credentials.env와 호스트 셸 env는 신뢰하므로 허용.
|
|
632
|
+
const UNTRUSTED_PROTECTED_ENV_KEYS_CLI = new Set([
|
|
633
|
+
"NODE_TLS_REJECT_UNAUTHORIZED", "NODE_EXTRA_CA_CERTS", "SSL_CERT_FILE", "SSL_CERT_DIR",
|
|
634
|
+
"OPENSSL_CONF", "REQUESTS_CA_BUNDLE", "CURL_CA_BUNDLE",
|
|
635
|
+
"HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "NO_PROXY", "GRPC_PROXY", "NPM_CONFIG_PROXY",
|
|
636
|
+
"AGENTLAS_SESSION", "AGENTLAS_MCP_BASE_URL", "AGENTLAS_WEB_BASE_URL", "AGENTLAS_API_BASE_URL",
|
|
637
|
+
"AGENTLAS_HUB_BASE_URL", "AGENTLAS_CLOUD_BASE_URL", "OLLAMA_HOST",
|
|
638
|
+
]);
|
|
639
|
+
function isProtectedChildEnvKeyCli(key, trusted) {
|
|
640
|
+
const k = String(key || "").trim().toUpperCase();
|
|
641
|
+
if (PROTECTED_CHILD_ENV_KEYS_CLI.has(k)) return true; // 호스트 신원/플러그인 루트 — 모든 출처 차단
|
|
642
|
+
if (!trusted && UNTRUSTED_PROTECTED_ENV_KEYS_CLI.has(k)) return true; // 네트워크 무결성 — 비신뢰 출처만 차단
|
|
643
|
+
return false;
|
|
644
|
+
}
|
|
645
|
+
function mergeChildEnvValues(target, values, overwrite, trusted) {
|
|
646
|
+
const injected = [];
|
|
647
|
+
for (const [key, value] of Object.entries(values || {})) {
|
|
648
|
+
if (!value || isProtectedChildEnvKeyCli(key, trusted)) continue;
|
|
649
|
+
if (!overwrite && target[key]) continue;
|
|
650
|
+
target[key] = value;
|
|
651
|
+
injected.push(key);
|
|
652
|
+
}
|
|
653
|
+
return injected;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* 워크포스 자식 env 빌더 (v1 buildChildEnvCli의 v2 포팅).
|
|
658
|
+
* v1의 멀티모달 카탈로그/키체인 볼트/에이전트 폴더 dotenv 소스는 그 서브시스템이
|
|
659
|
+
* 아직 v2로 이식되지 않아 여기 없다 — 워크포스 경로는 ctx.agentId를 절대 넘기지
|
|
660
|
+
* 않으므로(no-authority 원샷) 계약상 영향이 없고, 필요해지면 그 모듈 포팅과 함께
|
|
661
|
+
* 여기에 소스를 추가한다(조용한 근사 금지).
|
|
662
|
+
*/
|
|
663
|
+
async function buildChildEnv(db, ctx) {
|
|
664
|
+
const env = { ...process.env };
|
|
665
|
+
const apply = (values, overwrite, trusted) => {
|
|
666
|
+
mergeChildEnvValues(env, values, overwrite, trusted);
|
|
667
|
+
};
|
|
668
|
+
const globalCredentials = {
|
|
669
|
+
...readDotEnvFile(path.join(userDataDir(), "credentials.env")),
|
|
670
|
+
...readDotEnvFile(path.join(os.homedir(), ".agentlas", "credentials.env")),
|
|
671
|
+
};
|
|
672
|
+
apply(globalCredentials, false, true); // 사용자 본인의 전역 자격 — 신뢰
|
|
673
|
+
if (ctx && ctx.projectPath) apply(projectScopedEnvValues(globalCredentials, ctx.projectPath), true, true);
|
|
674
|
+
if (ctx && ctx.cwd) apply(readDotEnvDir(ctx.cwd), true, false); // 프로젝트 dotenv — 비신뢰
|
|
675
|
+
if (ctx && ctx.projectPath && ctx.projectPath !== ctx.cwd) apply(readDotEnvDir(ctx.projectPath), true, false);
|
|
676
|
+
return env;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
module.exports = {
|
|
680
|
+
RUNTIME_BIN,
|
|
681
|
+
which,
|
|
682
|
+
runCwd,
|
|
683
|
+
projectCwd,
|
|
684
|
+
buildArgs,
|
|
685
|
+
codexCaptureAgentText,
|
|
686
|
+
capturedRuntimeAgentText,
|
|
687
|
+
captureOutputLimit,
|
|
688
|
+
directCaptureOutputLimit,
|
|
689
|
+
captureRuntime,
|
|
690
|
+
apiKey,
|
|
691
|
+
normalizeCustomApiBaseUrl,
|
|
692
|
+
readCustomApiBaseUrl,
|
|
693
|
+
runApi,
|
|
694
|
+
buildChildEnv,
|
|
695
|
+
isProtectedChildEnvKeyCli,
|
|
696
|
+
parseDotEnv,
|
|
697
|
+
mergeChildEnvValues,
|
|
698
|
+
readDotEnvFile,
|
|
699
|
+
readDotEnvDir,
|
|
700
|
+
projectScopedEnvValues,
|
|
701
|
+
};
|