agentlas 1.0.11 → 1.0.14
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 +67 -0
- package/README.md +4 -2
- package/bin/agentlas.cjs +17 -3
- package/engine/agentlas-config.cjs +25 -18
- package/engine/agentlas-core-harness.cjs +14 -1
- package/engine/agentlas-i18n.cjs +2 -0
- package/engine/agentlas-input.cjs +62 -8
- package/engine/agentlas-memory-governance.cjs +10 -0
- package/engine/agentlas-onboard.cjs +22 -5
- package/engine/agentlas-sqlite-policy.cjs +18 -7
- package/engine/agentlas-workforce.cjs +989 -114
- package/engine/agentlas-workload-routing.cjs +61 -7
- package/engine/agentlas.cjs +8 -0
- package/engine/automation/daemon.cjs +76 -30
- package/engine/automation/schedule.cjs +16 -0
- package/engine/automation/store.cjs +92 -12
- package/engine/bootstrap-schema.sql +788 -29
- package/engine/commands/automation.cjs +8 -0
- package/engine/commands/chats.cjs +6 -1
- package/engine/commands/doctor.cjs +23 -1
- package/engine/commands/firm.cjs +66 -4
- package/engine/commands/help.cjs +31 -7
- package/engine/commands/hep-cloud.cjs +31 -0
- package/engine/commands/hep-hub.cjs +30 -0
- package/engine/commands/hep-local.cjs +32 -0
- package/engine/commands/hep-network.cjs +43 -0
- package/engine/commands/index.cjs +38 -7
- package/engine/commands/list.cjs +25 -1
- package/engine/commands/open.cjs +5 -1
- package/engine/commands/run.cjs +77 -13
- package/engine/commands/setup.cjs +12 -11
- package/engine/commands/storm.cjs +5 -9
- package/engine/commands/swarm.cjs +4 -4
- package/engine/commands/uninstall.cjs +36 -2
- package/engine/commands/version.cjs +29 -0
- package/engine/commands/workforce.cjs +10 -10
- package/engine/core/schema-ensure.cjs +75 -0
- package/engine/experience/variant.cjs +46 -2
- package/engine/firms/orchestrate.cjs +10 -3
- package/engine/hephaestus/runtime.cjs +7 -0
- package/engine/memory-cli/curate.cjs +3 -7
- package/engine/project/memory-context.cjs +3 -7
- package/engine/project/state.cjs +7 -6
- package/engine/runtimes/overrides.cjs +91 -22
- package/engine/runtimes/resolve.cjs +38 -8
- package/engine/runtimes/roles.cjs +162 -0
- package/engine/sessions/orchestrator.cjs +43 -4
- package/engine/sessions/prompt.cjs +4 -7
- package/engine/sessions/session.cjs +88 -20
- package/engine/storm/swarm.cjs +40 -12
- package/engine/ui/palette.cjs +52 -0
- package/engine/ui/renderer.cjs +37 -0
- package/engine/ui/repl.cjs +78 -10
- package/engine/workforce/capture.cjs +199 -14
- package/engine/workforce/concurrency.cjs +41 -0
- package/engine/workforce/deps.cjs +145 -29
- package/package.json +1 -1
|
@@ -89,7 +89,48 @@ function buildArgs(kind, systemPrompt, prompt, permission, runtimeOptions = {})
|
|
|
89
89
|
const model = runtimeOptions.model ? String(runtimeOptions.model) : null;
|
|
90
90
|
const effort = runtimeOptions.effort ? String(runtimeOptions.effort) : null;
|
|
91
91
|
const noAuthority = runtimeOptions.authorityMode === "no-authority";
|
|
92
|
+
// read-only 권한: 워크포스가 tool:file-read 능력만 부여했을 때. plan 모드(쓰기 불가)에
|
|
93
|
+
// 정확-도구 allowlist를 겹쳐 읽기 3종으로 고정한다. 이것이 "증명 가능한 경계"의 실체다 —
|
|
94
|
+
// 예전에는 이 모드가 없어서 도구를 주면 곧바로 acceptEdits(쓰기)+기본 도구 전체가 됐고,
|
|
95
|
+
// 그래서 권한 부여 자체를 항상 거부할 수밖에 없었다(2026-07-27 코드 감사 불가의 근본).
|
|
96
|
+
const readOnlyAuthority = runtimeOptions.authorityMode === "read-only";
|
|
97
|
+
const readOnlyTools = Array.isArray(runtimeOptions.allowedNativeTools) && runtimeOptions.allowedNativeTools.length
|
|
98
|
+
? [...new Set(runtimeOptions.allowedNativeTools.map((t) => String(t)))].sort()
|
|
99
|
+
: [];
|
|
100
|
+
if (readOnlyAuthority && !readOnlyTools.length) {
|
|
101
|
+
throw new Error("read-only capture requires an explicit native tool allowlist");
|
|
102
|
+
}
|
|
103
|
+
// read-only 분기를 가진 런타임은 claude-code뿐이다. 다른 kind로 들어오면 아래에서
|
|
104
|
+
// 일반 권한 분기로 떨어져 조용히 쓰기/셸까지 열린다 — 권한 승격이므로 정직 정지.
|
|
105
|
+
// (역할별 모델 배정으로 워커가 codex/gemini에서 돌 수 있게 되며 실재 경로가 됐다.)
|
|
106
|
+
if (readOnlyAuthority && kind !== "claude-code") {
|
|
107
|
+
throw new Error(`read-only capture is not provable on ${kind}`);
|
|
108
|
+
}
|
|
92
109
|
if (kind === "claude-code") {
|
|
110
|
+
if (readOnlyAuthority) {
|
|
111
|
+
const thinkingRo = effort === "max" || effort === "xhigh" ? "Ultrathink. " : effort === "high" ? "Think hard. " : effort === "medium" ? "Think. " : "";
|
|
112
|
+
const claudeEffortRo = effort === "minimal" ? "low" : effort === "xhigh" ? "max" : effort;
|
|
113
|
+
return [
|
|
114
|
+
"-p", thinkingRo + prompt,
|
|
115
|
+
"--append-system-prompt", systemPrompt,
|
|
116
|
+
...(model ? ["--model", model] : []),
|
|
117
|
+
...(claudeEffortRo && claudeEffortRo !== "none" ? ["--effort", claudeEffortRo] : []),
|
|
118
|
+
// 도구를 쓰는 워커는 최종 답까지 stdout에 한 글자도 내지 않는다 — 무도구
|
|
119
|
+
// 워커에는 없던 문제다. 유휴 타이머는 그 침묵을 "죽었다"로 읽고 10분에
|
|
120
|
+
// 처형한다(2026-07-27 라이브 실측 AGENTLAS_CAPTURE_IDLE_TIMEOUT). 이벤트
|
|
121
|
+
// 스트림으로 받아 도구 호출마다 진행 신호가 흐르게 하면 유휴 판정이 비로소
|
|
122
|
+
// 진짜 정지 신호가 된다. 최종 텍스트는 capturedRuntimeAgentText가 result
|
|
123
|
+
// 이벤트에서 뽑는다(이미 이 프로토콜을 파싱한다). 부분 토큰 델타는 켜지
|
|
124
|
+
// 않는다 — 진행 신호는 도구 호출 단위로 충분하고 출력량이 폭증한다.
|
|
125
|
+
"--output-format", "stream-json",
|
|
126
|
+
"--verbose",
|
|
127
|
+
// plan 모드는 쓰기/실행을 거부한다. allowedTools로 읽기 3종만 남긴다.
|
|
128
|
+
"--permission-mode", "plan",
|
|
129
|
+
"--allowedTools", readOnlyTools.join(","),
|
|
130
|
+
"--disallowedTools", "Write,Edit,MultiEdit,NotebookEdit,Bash,BashOutput,KillShell,WebFetch,WebSearch,Task",
|
|
131
|
+
...native.claudeMcpIsolationArgs(),
|
|
132
|
+
];
|
|
133
|
+
}
|
|
93
134
|
const perm = native.claudePermissionArgs(noAuthority ? "read" : level);
|
|
94
135
|
// 백그라운드/캡처 경로에는 검토된 MCP 서버 목록이 없다. full 권한은 툴 권한이지
|
|
95
136
|
// MCP 동의가 아니므로 이 경로는 항상 정확-공백 MCP 격리를 유지한다.
|
|
@@ -102,6 +143,16 @@ function buildArgs(kind, systemPrompt, prompt, permission, runtimeOptions = {})
|
|
|
102
143
|
"--append-system-prompt", systemPrompt,
|
|
103
144
|
...(model ? ["--model", model] : []),
|
|
104
145
|
...effortArgs,
|
|
146
|
+
// 이 분기도 read-only 분기와 같은 이유로 이벤트 스트림이 필요하다. 위 주석은
|
|
147
|
+
// 침묵 문제를 "도구를 쓰는 워커"의 것으로 한정했지만, 그건 도구 유무가 아니라
|
|
148
|
+
// "최종 답 전에는 stdout에 아무것도 없다"는 성질이고 이 분기도 똑같다.
|
|
149
|
+
// effort가 높으면 "Ultrathink."가 앞에 붙어 사고만 10분을 넘길 수 있고,
|
|
150
|
+
// 유휴 타이머는 그 침묵을 죽음으로 읽는다 — 2026-07-28 라이브에서 무도구
|
|
151
|
+
// 워커 3개가 전부 600초 유휴로 처형됐다. 진행 신호가 흐르는 채널로 재야
|
|
152
|
+
// 유휴 판정이 실제 정지를 뜻한다. 최종 텍스트는
|
|
153
|
+
// capturedRuntimeAgentText가 result 이벤트에서 그대로 뽑는다.
|
|
154
|
+
"--output-format", "stream-json",
|
|
155
|
+
"--verbose",
|
|
105
156
|
...perm,
|
|
106
157
|
...(noAuthority ? ["--tools", ""] : []),
|
|
107
158
|
...mcp,
|
|
@@ -166,6 +217,98 @@ function codexCaptureAgentText(jsonl) {
|
|
|
166
217
|
return [...latest.values()].join("");
|
|
167
218
|
}
|
|
168
219
|
|
|
220
|
+
function nonNegativeTokenCount(value) {
|
|
221
|
+
return Number.isInteger(value) && value >= 0 ? value : null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function usageFromObject(value, inputKeys, outputKeys, inputAdditiveKeys = []) {
|
|
225
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
226
|
+
const firstCount = (keys) => {
|
|
227
|
+
for (const key of keys) {
|
|
228
|
+
const count = nonNegativeTokenCount(value[key]);
|
|
229
|
+
if (count != null) return count;
|
|
230
|
+
}
|
|
231
|
+
return null;
|
|
232
|
+
};
|
|
233
|
+
let inputTokens = firstCount(inputKeys);
|
|
234
|
+
const outputTokens = firstCount(outputKeys);
|
|
235
|
+
if (inputTokens != null) {
|
|
236
|
+
for (const key of inputAdditiveKeys) {
|
|
237
|
+
const count = nonNegativeTokenCount(value[key]);
|
|
238
|
+
if (count != null) inputTokens += count;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
// Do not manufacture a zero for providers that expose only one side.
|
|
242
|
+
return inputTokens == null || outputTokens == null
|
|
243
|
+
? null
|
|
244
|
+
: { inputTokens, outputTokens };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function capturedRuntimeUsage(kind, raw) {
|
|
248
|
+
const events = [];
|
|
249
|
+
for (const line of String(raw || "").split(/\r?\n/)) {
|
|
250
|
+
if (!line.trim()) continue;
|
|
251
|
+
try {
|
|
252
|
+
events.push(JSON.parse(line));
|
|
253
|
+
} catch {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
const genericUsage = (value) =>
|
|
258
|
+
usageFromObject(
|
|
259
|
+
value,
|
|
260
|
+
["inputTokens", "input_tokens", "promptTokens", "prompt_tokens", "promptTokenCount"],
|
|
261
|
+
["outputTokens", "output_tokens", "completionTokens", "completion_tokens", "candidatesTokenCount"],
|
|
262
|
+
);
|
|
263
|
+
for (const event of [...events].reverse()) {
|
|
264
|
+
if (kind === "claude-code") {
|
|
265
|
+
const claudeUsage = usageFromObject(
|
|
266
|
+
event?.usage,
|
|
267
|
+
["input_tokens"],
|
|
268
|
+
["output_tokens"],
|
|
269
|
+
["cache_creation_input_tokens", "cache_read_input_tokens"],
|
|
270
|
+
);
|
|
271
|
+
if (claudeUsage) return claudeUsage;
|
|
272
|
+
const modelUsage = event?.modelUsage;
|
|
273
|
+
if (modelUsage && typeof modelUsage === "object" && !Array.isArray(modelUsage)) {
|
|
274
|
+
let inputTokens = 0;
|
|
275
|
+
let outputTokens = 0;
|
|
276
|
+
let observed = false;
|
|
277
|
+
for (const row of Object.values(modelUsage)) {
|
|
278
|
+
const usage = genericUsage(row);
|
|
279
|
+
if (!usage) continue;
|
|
280
|
+
inputTokens += usage.inputTokens;
|
|
281
|
+
outputTokens += usage.outputTokens;
|
|
282
|
+
observed = true;
|
|
283
|
+
}
|
|
284
|
+
if (observed) return { inputTokens, outputTokens };
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
const direct =
|
|
288
|
+
genericUsage(event?.usage) ||
|
|
289
|
+
genericUsage(event?.usageMetadata) ||
|
|
290
|
+
genericUsage(event?.stats);
|
|
291
|
+
if (direct) return direct;
|
|
292
|
+
if (kind === "gemini") {
|
|
293
|
+
const models = event?.stats?.models;
|
|
294
|
+
if (models && typeof models === "object" && !Array.isArray(models)) {
|
|
295
|
+
let inputTokens = 0;
|
|
296
|
+
let outputTokens = 0;
|
|
297
|
+
let observed = false;
|
|
298
|
+
for (const row of Object.values(models)) {
|
|
299
|
+
const usage = genericUsage(row);
|
|
300
|
+
if (!usage) continue;
|
|
301
|
+
inputTokens += usage.inputTokens;
|
|
302
|
+
outputTokens += usage.outputTokens;
|
|
303
|
+
observed = true;
|
|
304
|
+
}
|
|
305
|
+
if (observed) return { inputTokens, outputTokens };
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
|
|
169
312
|
function capturedRuntimeAgentText(kind, raw) {
|
|
170
313
|
const text = String(raw || "");
|
|
171
314
|
const events = [];
|
|
@@ -219,7 +362,9 @@ function capturedRuntimeAgentText(kind, raw) {
|
|
|
219
362
|
/**
|
|
220
363
|
* 네이티브 CLI 헤드리스 1턴 캡처 — 최종 텍스트를 resolve한다.
|
|
221
364
|
* opts: { cwd, env, permission, model, effort, authorityMode, noToolsPolicyPath,
|
|
222
|
-
* timeoutConfig, outputLimitBytes, signal, spawn(테스트 주입) }
|
|
365
|
+
* envelope, timeoutConfig, outputLimitBytes, signal, spawn(테스트 주입) }
|
|
366
|
+
* envelope=true면 { text, usage }를 반환한다. usage는 양쪽 토큰을 실제로 관측한
|
|
367
|
+
* 경우만 채우고, 불완전한 provider 이벤트를 0으로 위조하지 않는다.
|
|
223
368
|
*/
|
|
224
369
|
function captureRuntime(kind, systemPrompt, prompt, opts) {
|
|
225
370
|
opts = opts || {};
|
|
@@ -247,6 +392,7 @@ function captureRuntime(kind, systemPrompt, prompt, opts) {
|
|
|
247
392
|
effort: opts.effort,
|
|
248
393
|
authorityMode: opts.authorityMode,
|
|
249
394
|
noToolsPolicyPath: opts.noToolsPolicyPath,
|
|
395
|
+
allowedNativeTools: opts.allowedNativeTools,
|
|
250
396
|
}), {
|
|
251
397
|
cwd,
|
|
252
398
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -369,15 +515,17 @@ function captureRuntime(kind, systemPrompt, prompt, opts) {
|
|
|
369
515
|
return;
|
|
370
516
|
}
|
|
371
517
|
const raw = stdout.trim() || stderr.trim();
|
|
518
|
+
let text;
|
|
372
519
|
if (kind === "codex" && opts.authorityMode === "no-authority") {
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
return;
|
|
520
|
+
text = codexCaptureAgentText(raw);
|
|
521
|
+
} else if (kind === "claude-code" || kind === "gemini") {
|
|
522
|
+
text = capturedRuntimeAgentText(kind, raw);
|
|
523
|
+
} else {
|
|
524
|
+
text = raw;
|
|
379
525
|
}
|
|
380
|
-
finishResolve(
|
|
526
|
+
finishResolve(opts.envelope
|
|
527
|
+
? { text, usage: capturedRuntimeUsage(kind, raw) }
|
|
528
|
+
: text);
|
|
381
529
|
};
|
|
382
530
|
onAbort = () => {
|
|
383
531
|
const reason = opts.signal && opts.signal.reason;
|
|
@@ -399,7 +547,7 @@ function captureRuntime(kind, systemPrompt, prompt, opts) {
|
|
|
399
547
|
});
|
|
400
548
|
}
|
|
401
549
|
|
|
402
|
-
// ── API 러너 (BYOK / Ollama) — 비스트리밍, 최종
|
|
550
|
+
// ── API 러너 (BYOK / Ollama) — 비스트리밍, 최종 텍스트/usage 반환 (v1 runApi 포팅) ──
|
|
403
551
|
// engine/agentlas-api-agent.cjs 는 스트리밍+툴 루프(대화형)용이다. 워크포스 워커는
|
|
404
552
|
// zero-tools 원샷이 계약이므로 v1의 비스트리밍 원샷 경로를 그대로 쓴다.
|
|
405
553
|
const DEFAULT_API_MODEL = {
|
|
@@ -483,6 +631,9 @@ function readCustomApiBaseUrl() {
|
|
|
483
631
|
*/
|
|
484
632
|
async function runApi(backend, model, system, prompt, options) {
|
|
485
633
|
options = options || {};
|
|
634
|
+
const finish = (text, usage) => options.envelope
|
|
635
|
+
? { text: String(text || ""), usage: usage || null }
|
|
636
|
+
: String(text || "");
|
|
486
637
|
model = model || DEFAULT_API_MODEL[backend];
|
|
487
638
|
const fetchImpl = options.fetch || globalThis.fetch;
|
|
488
639
|
if (typeof fetchImpl !== "function") throw new Error("fetch is unavailable in this runtime (run through the app runtime).");
|
|
@@ -494,7 +645,10 @@ async function runApi(backend, model, system, prompt, options) {
|
|
|
494
645
|
});
|
|
495
646
|
if (!resp.ok) throw new Error(`Ollama ${resp.status} — run 'ollama serve' and check the model`);
|
|
496
647
|
const j = await resp.json();
|
|
497
|
-
return (
|
|
648
|
+
return finish(
|
|
649
|
+
(j.message && j.message.content) || "",
|
|
650
|
+
usageFromObject(j, ["prompt_eval_count"], ["eval_count"]),
|
|
651
|
+
);
|
|
498
652
|
}
|
|
499
653
|
const supported = backend === "anthropic" || backend === "openai" || backend === "google" ||
|
|
500
654
|
backend === "upstage" || backend === "custom" || !!ANTHROPIC_COMPAT_API[backend];
|
|
@@ -509,14 +663,25 @@ async function runApi(backend, model, system, prompt, options) {
|
|
|
509
663
|
const authHeaders = anthropicCompat
|
|
510
664
|
? { "x-api-key": key, authorization: "Bearer " + key }
|
|
511
665
|
: { "x-api-key": key };
|
|
666
|
+
// Prompt caching: 진짜 Anthropic만 cache_control을 존중한다. 크고 안정된
|
|
667
|
+
// system 프리픽스를 캐시 경계로 표시하면 적중 시 입력이 ~90% 싸게 과금되고,
|
|
668
|
+
// 모델별 최소치(~1024 토큰) 미만이면 서버가 조용히 무시하므로 무해하다.
|
|
669
|
+
// 호환 엔드포인트(GLM/Kimi/DeepSeek)는 문자열 형태를 유지한다 — 서버측
|
|
670
|
+
// 자동 캐시가 있고 추가 필드를 거부할 수 있다(데스크탑 byok.ts와 동일 계약).
|
|
671
|
+
const systemField = backend === "anthropic"
|
|
672
|
+
? [{ type: "text", text: system, cache_control: { type: "ephemeral" } }]
|
|
673
|
+
: system;
|
|
512
674
|
const resp = await fetchImpl(`${base}/v1/messages`, {
|
|
513
675
|
method: "POST",
|
|
514
676
|
headers: { "content-type": "application/json", ...authHeaders, "anthropic-version": "2023-06-01" },
|
|
515
|
-
body: JSON.stringify({ model, max_tokens: 4096, system, messages: [{ role: "user", content: prompt }] }),
|
|
677
|
+
body: JSON.stringify({ model, max_tokens: 4096, system: systemField, messages: [{ role: "user", content: prompt }] }),
|
|
516
678
|
});
|
|
517
679
|
if (!resp.ok) throw new Error(`${label} ${resp.status}: ${(await resp.text().catch(() => "")).slice(0, 200)}`);
|
|
518
680
|
const j = await resp.json();
|
|
519
|
-
return (
|
|
681
|
+
return finish(
|
|
682
|
+
(j.content && j.content[0] && j.content[0].text) || "",
|
|
683
|
+
usageFromObject(j.usage, ["input_tokens"], ["output_tokens"]),
|
|
684
|
+
);
|
|
520
685
|
}
|
|
521
686
|
if (backend === "openai" || backend === "upstage" || backend === "custom") {
|
|
522
687
|
const base = backend === "upstage"
|
|
@@ -534,7 +699,10 @@ async function runApi(backend, model, system, prompt, options) {
|
|
|
534
699
|
});
|
|
535
700
|
if (!resp.ok) throw new Error(`${label} ${resp.status}: ${(await resp.text().catch(() => "")).slice(0, 200)}`);
|
|
536
701
|
const j = await resp.json();
|
|
537
|
-
return (
|
|
702
|
+
return finish(
|
|
703
|
+
(j.choices && j.choices[0] && j.choices[0].message && j.choices[0].message.content) || "",
|
|
704
|
+
usageFromObject(j.usage, ["prompt_tokens"], ["completion_tokens"]),
|
|
705
|
+
);
|
|
538
706
|
}
|
|
539
707
|
if (backend === "google") {
|
|
540
708
|
const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${encodeURIComponent(key)}`;
|
|
@@ -546,7 +714,10 @@ async function runApi(backend, model, system, prompt, options) {
|
|
|
546
714
|
if (!resp.ok) throw new Error(`Google ${resp.status}: ${(await resp.text().catch(() => "")).slice(0, 200)}`);
|
|
547
715
|
const j = await resp.json();
|
|
548
716
|
const c = j.candidates && j.candidates[0];
|
|
549
|
-
return (
|
|
717
|
+
return finish(
|
|
718
|
+
(c && c.content && c.content.parts && c.content.parts[0] && c.content.parts[0].text) || "",
|
|
719
|
+
usageFromObject(j.usageMetadata, ["promptTokenCount"], ["candidatesTokenCount"]),
|
|
720
|
+
);
|
|
550
721
|
}
|
|
551
722
|
throw new Error("Unsupported backend: " + backend);
|
|
552
723
|
}
|
|
@@ -641,6 +812,19 @@ const UNTRUSTED_PROTECTED_ENV_KEYS_CLI = new Set([
|
|
|
641
812
|
"HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "NO_PROXY", "GRPC_PROXY", "NPM_CONFIG_PROXY",
|
|
642
813
|
"AGENTLAS_SESSION", "AGENTLAS_MCP_BASE_URL", "AGENTLAS_WEB_BASE_URL", "AGENTLAS_API_BASE_URL",
|
|
643
814
|
"AGENTLAS_HUB_BASE_URL", "AGENTLAS_CLOUD_BASE_URL", "OLLAMA_HOST",
|
|
815
|
+
// 모델 제공사 엔드포인트/게이트웨이 — Agentlas 자체 base URL과 같은 부류인데 빠져
|
|
816
|
+
// 있었다. 2026-07-27 실측: 클론 레포의 .env 한 줄이 자식 CLI의 ANTHROPIC_BASE_URL과
|
|
817
|
+
// AUTH_TOKEN을 갈아치워, 모든 워커 프롬프트가 공격자 서버로 나가고 그 서버가 쓴
|
|
818
|
+
// 답변이 핸드오프·합성으로 흘러든다. 키 자체(*_API_KEY)는 프로젝트별 BYOK 기능이라
|
|
819
|
+
// 계속 허용한다 — 막는 것은 "데이터가 어디로 가는가"뿐이다.
|
|
820
|
+
"ANTHROPIC_BASE_URL", "ANTHROPIC_API_URL", "ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_CUSTOM_HEADERS",
|
|
821
|
+
"ANTHROPIC_BEDROCK_BASE_URL", "ANTHROPIC_VERTEX_BASE_URL",
|
|
822
|
+
"CLAUDE_CODE_USE_BEDROCK", "CLAUDE_CODE_USE_VERTEX", "CLAUDE_CODE_SKIP_BEDROCK_AUTH",
|
|
823
|
+
"CLAUDE_CODE_SKIP_VERTEX_AUTH", "AWS_ENDPOINT_URL", "AWS_ENDPOINT_URL_BEDROCK",
|
|
824
|
+
"OPENAI_BASE_URL", "OPENAI_API_BASE", "OPENAI_ORGANIZATION", "AZURE_OPENAI_ENDPOINT",
|
|
825
|
+
"OPENAI_PROXY", "CODEX_BASE_URL", "CODEX_API_URL",
|
|
826
|
+
"GOOGLE_GEMINI_BASE_URL", "GOOGLE_VERTEX_BASE_URL", "GEMINI_API_BASE_URL",
|
|
827
|
+
"GOOGLE_CLOUD_PROJECT", "GOOGLE_APPLICATION_CREDENTIALS",
|
|
644
828
|
]);
|
|
645
829
|
function isProtectedChildEnvKeyCli(key, trusted) {
|
|
646
830
|
const k = String(key || "").trim().toUpperCase();
|
|
@@ -690,6 +874,7 @@ module.exports = {
|
|
|
690
874
|
buildArgs,
|
|
691
875
|
codexCaptureAgentText,
|
|
692
876
|
capturedRuntimeAgentText,
|
|
877
|
+
capturedRuntimeUsage,
|
|
693
878
|
captureOutputLimit,
|
|
694
879
|
directCaptureOutputLimit,
|
|
695
880
|
captureRuntime,
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* workforce/concurrency — 워크포스 워커 동시 실행 수(스웜 크기)의 사양 기반 추천값.
|
|
4
|
+
*
|
|
5
|
+
* 워커 1명 = captureRuntime을 통해 스폰되는 실제 CLI 자식 프로세스(engine/workforce/
|
|
6
|
+
* capture.cjs)다. 이전에는 사용자가 --parallel/-n을 안 주면 컴퓨터 사양과 무관하게
|
|
7
|
+
* 고정 3(상한 8)을 썼다 — 코어 2개짜리 저사양 기기에서 3개 CLI 자식이 한꺼번에 뜨면
|
|
8
|
+
* 과다 산정, 32코어 워크스테이션에서도 항상 3으로 저평가되는 양방향 문제였다.
|
|
9
|
+
*
|
|
10
|
+
* 데스크탑(electron/store/concurrency.ts)과 동일한 원리(코어 2개는 OS/자식 자신에게
|
|
11
|
+
* 남기고, 에이전트당 RAM ~2GB + 여유 4GB로 추정)를 쓰되, 상한(HARD_MAX)은 터미널
|
|
12
|
+
* 자체의 기존 안전선(8)을 그대로 유지한다 — 이건 provider가 바뀌어도 변하지 않는
|
|
13
|
+
* Agentlas 자체 정책 상수이지, 하드코딩 금지 대상인 "외부에서 바뀌는 값"이 아니다.
|
|
14
|
+
*/
|
|
15
|
+
const os = require("node:os");
|
|
16
|
+
|
|
17
|
+
const HARD_MAX = 8;
|
|
18
|
+
|
|
19
|
+
function getSystemSpecs() {
|
|
20
|
+
let cores = 4;
|
|
21
|
+
let totalMemGB = 8;
|
|
22
|
+
try {
|
|
23
|
+
cores = Math.max(1, os.cpus().length);
|
|
24
|
+
} catch {
|
|
25
|
+
// fall back
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
totalMemGB = os.totalmem() / 1024 ** 3;
|
|
29
|
+
} catch {
|
|
30
|
+
// fall back
|
|
31
|
+
}
|
|
32
|
+
return { cores, totalMemGB };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function recommendedConcurrency(specs = getSystemSpecs()) {
|
|
36
|
+
const coreBound = Math.max(1, specs.cores - 2);
|
|
37
|
+
const memBound = Math.max(1, Math.floor((specs.totalMemGB - 4) / 2));
|
|
38
|
+
return Math.max(1, Math.min(coreBound, memBound, HARD_MAX));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = { HARD_MAX, getSystemSpecs, recommendedConcurrency };
|
|
@@ -32,49 +32,116 @@ const path = require("node:path");
|
|
|
32
32
|
const { userDataDir } = require("../core/paths.cjs");
|
|
33
33
|
const hubClient = require("../cloud/hub-client.cjs");
|
|
34
34
|
const detect = require("../runtimes/detect.cjs");
|
|
35
|
+
const { resolvedModelRole } = require("../runtimes/roles.cjs");
|
|
35
36
|
const capture = require("./capture.cjs");
|
|
36
37
|
|
|
37
38
|
// ── 런타임 해석 (v1 resolveRuntime의 워크포스 어댑터) ─────────────────────
|
|
38
|
-
// 워크포스 모듈이 기대하는 형태:
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
function
|
|
43
|
-
if (!
|
|
39
|
+
// 워크포스 모듈이 기대하는 형태:
|
|
40
|
+
// {mode:"cli", kind, model?, effort?} | {mode:"api", backend, model, effort?}
|
|
41
|
+
// 반환 정본은 orchestrator 런타임이며 roleRuntimes에 두 역할의 실제 실행 런타임을
|
|
42
|
+
// 동봉한다. stage별 선택은 agentlas-workforce.cjs가 수행한다.
|
|
43
|
+
function runtimeFromSelection(selection) {
|
|
44
|
+
if (!selection || !selection.kind) return null;
|
|
45
|
+
const common = {
|
|
46
|
+
model: selection.model || null,
|
|
47
|
+
effort: selection.effort || null,
|
|
48
|
+
capabilities: ["code", "tools", ...(selection.longContext ? ["long-context"] : [])],
|
|
49
|
+
efforts: [],
|
|
50
|
+
source: selection.sourceLayer || selection.source || null,
|
|
51
|
+
role: selection.role || null,
|
|
52
|
+
};
|
|
53
|
+
if (capture.RUNTIME_BIN[selection.kind]) {
|
|
54
|
+
return { mode: "cli", kind: selection.kind, ...common };
|
|
55
|
+
}
|
|
56
|
+
if (selection.kind === "byok" && selection.backend) {
|
|
57
|
+
return { mode: "api", backend: selection.backend, ...common };
|
|
58
|
+
}
|
|
59
|
+
if (selection.kind === "ollama") {
|
|
60
|
+
return { mode: "api", backend: "ollama", ...common };
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function legacyWorkforceRuntime(db, override) {
|
|
66
|
+
let selectedOverride = override;
|
|
67
|
+
if (!selectedOverride) {
|
|
44
68
|
try {
|
|
45
69
|
const saved = require("../agentlas-config.cjs").loadPrefs(userDataDir()).runtime;
|
|
46
|
-
if (
|
|
70
|
+
if (
|
|
71
|
+
saved &&
|
|
72
|
+
saved !== "auto" &&
|
|
73
|
+
capture.RUNTIME_BIN[saved] &&
|
|
74
|
+
capture.which(capture.RUNTIME_BIN[saved])
|
|
75
|
+
) {
|
|
76
|
+
selectedOverride = saved;
|
|
77
|
+
}
|
|
47
78
|
} catch { /* prefs 없음 — 사다리 계속 */ }
|
|
48
79
|
}
|
|
49
80
|
const ar = db ? detect.activeRuntimeRow(db) : null;
|
|
50
|
-
const
|
|
51
|
-
? {
|
|
52
|
-
|
|
81
|
+
const active = ar
|
|
82
|
+
? runtimeFromSelection({
|
|
83
|
+
role: "orchestrator",
|
|
53
84
|
kind: ar.kind,
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
85
|
+
backend: ar.backend,
|
|
86
|
+
source: ar.source,
|
|
87
|
+
model: ar.model,
|
|
88
|
+
effort: null,
|
|
89
|
+
longContext: Boolean(ar.long_context),
|
|
90
|
+
sourceLayer: "active-runtime",
|
|
91
|
+
})
|
|
58
92
|
: null;
|
|
59
|
-
if (
|
|
60
|
-
if (!capture.RUNTIME_BIN[
|
|
61
|
-
const error = new Error(
|
|
93
|
+
if (selectedOverride) {
|
|
94
|
+
if (!capture.RUNTIME_BIN[selectedOverride]) {
|
|
95
|
+
const error = new Error(
|
|
96
|
+
`unknown workforce runtime: ${selectedOverride} (capture drivers: ${Object.keys(capture.RUNTIME_BIN).join(", ")})`,
|
|
97
|
+
);
|
|
62
98
|
error.code = "no_runtime";
|
|
63
99
|
throw error;
|
|
64
100
|
}
|
|
65
|
-
return
|
|
101
|
+
return active && active.mode === "cli" && active.kind === selectedOverride
|
|
102
|
+
? active
|
|
103
|
+
: { mode: "cli", kind: selectedOverride, model: null, effort: null };
|
|
66
104
|
}
|
|
67
|
-
if (
|
|
68
|
-
if (ar && ar.kind === "byok" && ar.backend) return { mode: "api", backend: ar.backend, model: ar.model };
|
|
69
|
-
if (ar && ar.kind === "ollama") return { mode: "api", backend: "ollama", model: ar.model };
|
|
105
|
+
if (active) return active;
|
|
70
106
|
for (const kind of Object.keys(capture.RUNTIME_BIN)) {
|
|
71
|
-
if (capture.which(capture.RUNTIME_BIN[kind]))
|
|
107
|
+
if (capture.which(capture.RUNTIME_BIN[kind])) {
|
|
108
|
+
return { mode: "cli", kind, model: null, effort: null, source: "detected" };
|
|
109
|
+
}
|
|
72
110
|
}
|
|
73
|
-
const error = new Error(
|
|
111
|
+
const error = new Error(
|
|
112
|
+
"no_runtime: no agent CLI or connected API runtime found (claude / codex / gemini / BYOK / Ollama).",
|
|
113
|
+
);
|
|
74
114
|
error.code = "no_runtime";
|
|
75
115
|
throw error;
|
|
76
116
|
}
|
|
77
117
|
|
|
118
|
+
// 사다리: 명시 override > model_roles[role] > 레거시 prefs/active/PATH.
|
|
119
|
+
// worker가 비어 있거나 inherit=1이면 roles.cjs가 orchestrator로만 승격한다.
|
|
120
|
+
function resolveWorkforceRuntime(db, override) {
|
|
121
|
+
if (override) {
|
|
122
|
+
const exact = legacyWorkforceRuntime(db, override);
|
|
123
|
+
return {
|
|
124
|
+
...exact,
|
|
125
|
+
role: "orchestrator",
|
|
126
|
+
roleRuntimes: {
|
|
127
|
+
orchestrator: { ...exact, role: "orchestrator" },
|
|
128
|
+
worker: { ...exact, role: "worker" },
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
const fallback = legacyWorkforceRuntime(db, null);
|
|
133
|
+
const orchestrator = runtimeFromSelection(resolvedModelRole(db, "orchestrator")) || fallback;
|
|
134
|
+
const worker = runtimeFromSelection(resolvedModelRole(db, "worker")) || orchestrator;
|
|
135
|
+
return {
|
|
136
|
+
...orchestrator,
|
|
137
|
+
role: "orchestrator",
|
|
138
|
+
roleRuntimes: {
|
|
139
|
+
orchestrator: { ...orchestrator, role: "orchestrator" },
|
|
140
|
+
worker: { ...worker, role: "worker" },
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
78
145
|
// ── 영수증 (v1 모놀리스의 JSONL 계약 포팅: userData 하위, 0700/0600) ──────
|
|
79
146
|
function receiptFile() {
|
|
80
147
|
return path.join(userDataDir(), "workforce-execution-receipts.jsonl");
|
|
@@ -112,13 +179,49 @@ function persistBenchmarkArtifact(artifact, executionIdHint) {
|
|
|
112
179
|
// 보존하되 runtimeIds=[] / status="observed-not-executable"로 광고한다 —
|
|
113
180
|
// 필요 capability에 대해 워크포스 모듈이 플래너 전에 fail-closed 하게 된다.
|
|
114
181
|
// 권한을 제조하는 것보다 정직 정지가 계약이다.
|
|
115
|
-
|
|
182
|
+
// 워커가 실제로 부여받을 수 있는 유일한 네이티브 도구 집합 — 읽기 전용.
|
|
183
|
+
// claude-code는 plan 모드(쓰기·실행 거부) + --allowedTools 로 이 경계를 강제할 수 있어
|
|
184
|
+
// "정확 per-tool 부착"이 증명된다. 쓰기·셸·네트워크·MCP는 여전히 증명 불가라 잠긴 채다.
|
|
185
|
+
const READ_ONLY_NATIVE_TOOLS = ["Read", "Grep", "Glob"];
|
|
186
|
+
const READ_ONLY_BUILTIN_TOOL_ID = "builtin:file-read";
|
|
187
|
+
const READ_ONLY_CAPABILITY_IDS = ["tool:file-read"];
|
|
188
|
+
const READ_ONLY_RUNTIME_IDS = ["runtime:claude-code"];
|
|
189
|
+
|
|
190
|
+
function readOnlyBuiltinToolRows(roster, runtimeId) {
|
|
191
|
+
if (!READ_ONLY_RUNTIME_IDS.includes(String(runtimeId || ""))) return [];
|
|
192
|
+
const rows = [];
|
|
193
|
+
for (const pinned of roster || []) {
|
|
194
|
+
// 허브가 이 릴리스에 파일 읽기를 허용했을 때만. deny면 존중하고 부여하지 않는다.
|
|
195
|
+
if (pinned?.permissionPolicy?.fileRead?.mode !== "manifest-allowlist") continue;
|
|
196
|
+
rows.push({
|
|
197
|
+
slotId: pinned.slotId,
|
|
198
|
+
agentReleaseId: pinned.agentReleaseId,
|
|
199
|
+
permissionPolicyDigest: pinned.permissionPolicyDigest,
|
|
200
|
+
provider: "builtin",
|
|
201
|
+
toolId: READ_ONLY_BUILTIN_TOOL_ID,
|
|
202
|
+
// 내장 도구는 MCP 서버가 없다. validateToolInventory는 serverId가 정확히 null이
|
|
203
|
+
// 아니면 거절한다(2026-07-27 라이브: "builtin" 문자열을 넣어 prepare 직후 전량 폐기).
|
|
204
|
+
serverId: null,
|
|
205
|
+
description: "Read-only project file access (Read/Grep/Glob, no write, no shell)",
|
|
206
|
+
inputSchemaDigest: null,
|
|
207
|
+
runtimeIds: [...READ_ONLY_RUNTIME_IDS],
|
|
208
|
+
selectiveEnforcement: "exact-tool-allowlist",
|
|
209
|
+
capabilityIds: [...READ_ONLY_CAPABILITY_IDS],
|
|
210
|
+
status: "ready",
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
return rows;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function listWorkforceTools({ db, roster, cwd, env, timeoutMs, signal, runtimeId }) {
|
|
116
217
|
const mcp = require("../mcp/index.cjs");
|
|
218
|
+
// 읽기 전용 내장 도구는 MCP 서버 유무와 무관하게 항상 제공한다.
|
|
219
|
+
const builtinRows = readOnlyBuiltinToolRows(roster, runtimeId);
|
|
117
220
|
const servers = mcp.readConsentedSystemMcpServers(db, {
|
|
118
221
|
userDataDir: userDataDir(),
|
|
119
222
|
createRuntimeHome: false,
|
|
120
223
|
}).slice(0, 8);
|
|
121
|
-
if (!servers.length) return
|
|
224
|
+
if (!servers.length) return builtinRows;
|
|
122
225
|
const deadline = Date.now() + Math.max(50, Math.min(12_000, Number(timeoutMs) || 12_000));
|
|
123
226
|
const outcomes = new Array(servers.length);
|
|
124
227
|
let cursor = 0;
|
|
@@ -140,7 +243,7 @@ async function listWorkforceTools({ db, roster, cwd, env, timeoutMs, signal }) {
|
|
|
140
243
|
await Promise.all(Array.from({ length: Math.min(3, servers.length) }, () => worker()));
|
|
141
244
|
|
|
142
245
|
const safeId = /^[A-Za-z0-9][A-Za-z0-9_.$:/@+~-]{0,127}$/;
|
|
143
|
-
const rows = [];
|
|
246
|
+
const rows = [...builtinRows];
|
|
144
247
|
for (let index = 0; index < servers.length; index += 1) {
|
|
145
248
|
const server = servers[index];
|
|
146
249
|
const listed = outcomes[index];
|
|
@@ -444,9 +547,19 @@ function buildWorkforceDeps(ctx = {}) {
|
|
|
444
547
|
appendAuditReceipt,
|
|
445
548
|
persistBenchmarkArtifact,
|
|
446
549
|
listWorkforceTools,
|
|
447
|
-
//
|
|
448
|
-
//
|
|
449
|
-
|
|
550
|
+
// 호스트가 자기 도구를 빌려주는 결정은 허브 후보 자격과 무관하다.
|
|
551
|
+
// requiredToolCapabilities는 "이 허브 에이전트가 그 도구를 선언했는가"라는
|
|
552
|
+
// 후보 필터라서, 선언한 에이전트가 사실상 0이라 그걸 쓰면 후보가 0건이 된다
|
|
553
|
+
// (2026-07-27 실측: 그래서 리더가 절대 선언하지 않았고 부여가 영영 발동 안 됨).
|
|
554
|
+
// 읽기 권한 대여는 허브가 그 릴리스에 파일 읽기를 허용했는지만 보면 된다.
|
|
555
|
+
hostReadOnlyGrants: (roster, runtimeId) => readOnlyBuiltinToolRows(roster, runtimeId),
|
|
556
|
+
// 읽기 전용 내장 도구는 claude-code plan 모드 + --allowedTools/--disallowedTools로
|
|
557
|
+
// 정확 경계가 증명된다 → 부여 허용. 그 밖(쓰기·셸·네트워크·MCP)은 여전히 증명
|
|
558
|
+
// 불가라 거부한다. 증명 없이 true를 돌려주면 권한 제조가 된다.
|
|
559
|
+
supportsWorkforceToolAuthority: async ({ grantedToolIds }) =>
|
|
560
|
+
Array.isArray(grantedToolIds)
|
|
561
|
+
&& grantedToolIds.length > 0
|
|
562
|
+
&& grantedToolIds.every((id) => id === READ_ONLY_BUILTIN_TOOL_ID),
|
|
450
563
|
bindWorkforceGoal,
|
|
451
564
|
loadWorkforceGoalRuntime,
|
|
452
565
|
recordWorkforceGoalTurn,
|
|
@@ -472,6 +585,9 @@ module.exports = {
|
|
|
472
585
|
appendAuditReceipt,
|
|
473
586
|
persistBenchmarkArtifact,
|
|
474
587
|
listWorkforceTools,
|
|
588
|
+
readOnlyBuiltinToolRows,
|
|
589
|
+
READ_ONLY_NATIVE_TOOLS,
|
|
590
|
+
READ_ONLY_BUILTIN_TOOL_ID,
|
|
475
591
|
bindWorkforceGoal,
|
|
476
592
|
loadWorkforceGoalRuntime,
|
|
477
593
|
completeWorkforceGoal,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentlas",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.14",
|
|
4
4
|
"description": "Agentlas agent terminal — chat with your installed AI agents and teams from the terminal, Claude Code style. Standalone: no desktop app required.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"agentlas": "bin/agentlas.cjs"
|