agentlas 1.0.10 → 1.0.12
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 +48 -0
- package/engine/agentlas-input.cjs +121 -20
- package/engine/agentlas-workforce.cjs +859 -101
- package/engine/agentlas-workload-routing.cjs +29 -3
- package/engine/automation/daemon.cjs +9 -0
- package/engine/automation/store.cjs +22 -0
- package/engine/bootstrap-schema.sql +24 -0
- package/engine/commands/doctor.cjs +23 -1
- package/engine/commands/firm.cjs +59 -4
- package/engine/commands/help.cjs +8 -5
- package/engine/commands/list.cjs +25 -1
- package/engine/commands/run.cjs +65 -11
- package/engine/firms/orchestrate.cjs +10 -3
- 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 +2 -1
- package/engine/sessions/session.cjs +56 -20
- package/engine/storm/swarm.cjs +28 -12
- package/engine/ui/palette.cjs +40 -0
- package/engine/ui/repl.cjs +83 -17
- package/engine/workforce/capture.cjs +199 -14
- package/engine/workforce/deps.cjs +145 -29
- package/package.json +1 -1
package/engine/ui/repl.cjs
CHANGED
|
@@ -20,7 +20,8 @@ const { renderBanner, readVersion } = require("../agentlas-banner.cjs");
|
|
|
20
20
|
const { Orchestrator, maxParallel } = require("../sessions/orchestrator.cjs");
|
|
21
21
|
const { Renderer } = require("./renderer.cjs");
|
|
22
22
|
const { findAgent, listAgents } = require("../agents/registry.cjs");
|
|
23
|
-
const {
|
|
23
|
+
const { resolveRuntimeForAgent } = require("../runtimes/overrides.cjs");
|
|
24
|
+
const { EFFORTS } = require("../agentlas-workload-routing.cjs");
|
|
24
25
|
const permissions = require("../agentlas-permissions.cjs");
|
|
25
26
|
const i18n = require("../agentlas-i18n.cjs");
|
|
26
27
|
const { tokenizeCommandLine } = require("../agentlas-input.cjs");
|
|
@@ -133,8 +134,18 @@ async function startRepl(ctx, opts = {}) {
|
|
|
133
134
|
const renderer = new Renderer(ui);
|
|
134
135
|
let permission = permissions.normalize(opts.permission || (ctx.prefs && ctx.prefs.permission) || "write");
|
|
135
136
|
let runtimeOverride = opts.runtime || null;
|
|
136
|
-
|
|
137
|
-
|
|
137
|
+
let modelOverride = opts.model || null;
|
|
138
|
+
let effortOverride = opts.effort || null;
|
|
139
|
+
|
|
140
|
+
const resolveRt = (agentId = null) => resolveRuntimeForAgent({
|
|
141
|
+
db,
|
|
142
|
+
prefs: ctx.prefs,
|
|
143
|
+
explicit: runtimeOverride,
|
|
144
|
+
model: modelOverride,
|
|
145
|
+
effort: effortOverride,
|
|
146
|
+
role: "orchestrator",
|
|
147
|
+
agentId,
|
|
148
|
+
});
|
|
138
149
|
|
|
139
150
|
let resumeChatId = opts.chatId || null;
|
|
140
151
|
const ensureMainSession = (agentToken) => {
|
|
@@ -146,7 +157,14 @@ async function startRepl(ctx, opts = {}) {
|
|
|
146
157
|
}
|
|
147
158
|
const active = orch.active();
|
|
148
159
|
if (active && active.agent.id === agent.id) return active;
|
|
149
|
-
const session = orch.spawn({
|
|
160
|
+
const session = orch.spawn({
|
|
161
|
+
agent,
|
|
162
|
+
runtime: resolveRt(agent.id),
|
|
163
|
+
permission,
|
|
164
|
+
cwd: process.cwd(),
|
|
165
|
+
activate: true,
|
|
166
|
+
chatId: resumeChatId,
|
|
167
|
+
});
|
|
150
168
|
resumeChatId = null; // 재개는 첫 세션에만 적용
|
|
151
169
|
renderer.attach(session, { replay: false });
|
|
152
170
|
return session;
|
|
@@ -318,7 +336,18 @@ async function startRepl(ctx, opts = {}) {
|
|
|
318
336
|
|
|
319
337
|
if (input.startsWith("/")) {
|
|
320
338
|
try {
|
|
321
|
-
const quit = handleSlash(ctx, input.slice(1), {
|
|
339
|
+
const quit = handleSlash(ctx, input.slice(1), {
|
|
340
|
+
orch,
|
|
341
|
+
renderer,
|
|
342
|
+
ensureMainSession,
|
|
343
|
+
resolveRt,
|
|
344
|
+
track: trackCommand,
|
|
345
|
+
setPermission: (p) => { permission = p; },
|
|
346
|
+
getPermission: () => permission,
|
|
347
|
+
setRuntime: (r) => { runtimeOverride = r; },
|
|
348
|
+
setModel: (model) => { modelOverride = model; },
|
|
349
|
+
setEffort: (effort) => { effortOverride = effort; },
|
|
350
|
+
});
|
|
322
351
|
if (quit === "quit") { rl.close(); return; }
|
|
323
352
|
} catch (e) {
|
|
324
353
|
ui.error(String((e && e.message) || e));
|
|
@@ -369,15 +398,19 @@ async function startRepl(ctx, opts = {}) {
|
|
|
369
398
|
*/
|
|
370
399
|
const finish = () => { orch.shutdown(); ui.ensureNl(); resolve(0); };
|
|
371
400
|
if (!pendingCommands.size) { finish(); return; }
|
|
372
|
-
ui.ensureNl();
|
|
373
|
-
ui.line(ui.c.dim(en
|
|
374
|
-
? `finishing ${pendingCommands.size} command(s)…`
|
|
375
|
-
: `실행 중인 명령 ${pendingCommands.size}개를 마무리하는 중…`));
|
|
376
401
|
let settled = false;
|
|
377
|
-
const once = () => { if (settled) return; settled = true; finish(); };
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
402
|
+
const once = () => { if (settled) return; settled = true; clearTimeout(notice); finish(); };
|
|
403
|
+
// 곧 끝나는 명령까지 매번 고지하면 종료 화면이 시끄러워진다 — 실제로 기다릴 때만 알린다.
|
|
404
|
+
const notice = setTimeout(() => {
|
|
405
|
+
ui.ensureNl();
|
|
406
|
+
ui.line(ui.c.dim(en
|
|
407
|
+
? `finishing ${pendingCommands.size} command(s)…`
|
|
408
|
+
: `실행 중인 명령 ${pendingCommands.size}개를 마무리하는 중…`));
|
|
409
|
+
}, 400);
|
|
410
|
+
if (notice.unref) notice.unref();
|
|
411
|
+
const cap = setTimeout(once, 30_000);
|
|
412
|
+
if (cap.unref) cap.unref();
|
|
413
|
+
Promise.allSettled([...pendingCommands]).then(() => { clearTimeout(cap); once(); }, once);
|
|
381
414
|
});
|
|
382
415
|
|
|
383
416
|
prompt();
|
|
@@ -478,6 +511,18 @@ function printSessions(ctx, orch) {
|
|
|
478
511
|
}
|
|
479
512
|
}
|
|
480
513
|
|
|
514
|
+
/*
|
|
515
|
+
* 세션 키 파싱. 인자가 없으면 사용법을 낸다 — 예전에는 `s${undefined}` 가 그대로
|
|
516
|
+
* 조립돼 `/kill` 이 "no such session: sundefined" 를, `/steer` 는 rest[0].length 에서
|
|
517
|
+
* 날 TypeError 를 냈다. 팔레트가 `/steer <n> <msg>` 라고 안내하므로 인자 없이 Enter 를
|
|
518
|
+
* 눌러 사용법을 보려는 것은 정상적인 탐색이다.
|
|
519
|
+
*/
|
|
520
|
+
function sessionKeyArg(rest, usage) {
|
|
521
|
+
const token = rest[0];
|
|
522
|
+
if (!token) throw new Error(usage);
|
|
523
|
+
return String(token).startsWith("s") ? token : `s${token}`;
|
|
524
|
+
}
|
|
525
|
+
|
|
481
526
|
function handleSlash(ctx, cmdline, api) {
|
|
482
527
|
const en = ctx.lang === "en";
|
|
483
528
|
const ui = ctx.uiInstance;
|
|
@@ -520,7 +565,7 @@ function handleSlash(ctx, cmdline, api) {
|
|
|
520
565
|
case "sessions": case "tree": printSessions(ctx, orch); return;
|
|
521
566
|
|
|
522
567
|
case "s": case "switch": {
|
|
523
|
-
const key =
|
|
568
|
+
const key = sessionKeyArg(rest, `Usage: /${cmd} <n>`);
|
|
524
569
|
const session = orch.setActive(key);
|
|
525
570
|
renderer.attach(session, { replay: true });
|
|
526
571
|
return;
|
|
@@ -548,7 +593,7 @@ function handleSlash(ctx, cmdline, api) {
|
|
|
548
593
|
}
|
|
549
594
|
|
|
550
595
|
case "steer": {
|
|
551
|
-
const key =
|
|
596
|
+
const key = sessionKeyArg(rest, `Usage: /${cmd} <n> <message>`);
|
|
552
597
|
const msg = restStr.slice(rest[0].length).trim();
|
|
553
598
|
if (!msg) throw new Error("Usage: /steer <n> <message>");
|
|
554
599
|
orch.sendTo(key, msg);
|
|
@@ -557,12 +602,12 @@ function handleSlash(ctx, cmdline, api) {
|
|
|
557
602
|
}
|
|
558
603
|
|
|
559
604
|
case "kill": {
|
|
560
|
-
const key =
|
|
605
|
+
const key = sessionKeyArg(rest, `Usage: /${cmd} <n>`);
|
|
561
606
|
orch.kill(key);
|
|
562
607
|
return;
|
|
563
608
|
}
|
|
564
609
|
case "rm": {
|
|
565
|
-
const key =
|
|
610
|
+
const key = sessionKeyArg(rest, `Usage: /${cmd} <n>`);
|
|
566
611
|
orch.remove(key);
|
|
567
612
|
const active = orch.active();
|
|
568
613
|
if (active) renderer.attach(active, { replay: false });
|
|
@@ -590,6 +635,27 @@ function handleSlash(ctx, cmdline, api) {
|
|
|
590
635
|
ctx.out(ui.c.dim(`runtime: ${rest[0]} (${en ? "applies to new sessions" : "새 세션부터 적용"})`));
|
|
591
636
|
return;
|
|
592
637
|
}
|
|
638
|
+
case "model": {
|
|
639
|
+
const model = String(rest[0] || "").trim();
|
|
640
|
+
if (!model) throw new Error("Usage: /model <provider-model-id|default>");
|
|
641
|
+
const next = ["default", "inherit"].includes(model.toLowerCase()) ? null : model;
|
|
642
|
+
api.setModel(next);
|
|
643
|
+
ctx.out(ui.c.dim(
|
|
644
|
+
`model: ${next || "default"} (${en ? "applies to new sessions" : "새 세션부터 적용"})`,
|
|
645
|
+
));
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
case "effort": {
|
|
649
|
+
const effort = String(rest[0] || "").trim().toLowerCase();
|
|
650
|
+
if (!EFFORTS.includes(effort)) {
|
|
651
|
+
throw new Error(`Usage: /effort ${EFFORTS.join("|")}`);
|
|
652
|
+
}
|
|
653
|
+
api.setEffort(effort === "none" ? null : effort);
|
|
654
|
+
ctx.out(ui.c.dim(
|
|
655
|
+
`effort: ${effort} (${en ? "applies to new sessions" : "새 세션부터 적용"})`,
|
|
656
|
+
));
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
593
659
|
case "permission": {
|
|
594
660
|
if (!["read", "write", "full"].includes(String(rest[0] || ""))) {
|
|
595
661
|
throw new Error("Usage: /permission read|write|full");
|
|
@@ -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,
|