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.
@@ -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
- // 워크포스 모듈이 기대하는 형태: {mode:"cli", kind, model?} | {mode:"api", backend, model}.
39
- // v2 runtimes/resolve.cjs는 {kind, bin, source}만 주고 BYOK/Ollama를 모르므로,
40
- // v1의 사다리(명시 override > prefs 저장값 > 공유 DB active_runtime(byok/ollama 포함)
41
- // > PATH 탐지)를 여기서 복원한다. 아무것도 없으면 no_runtime 정직 정지 — 폴백 금지.
42
- function resolveWorkforceRuntime(db, override) {
43
- if (!override) {
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 (saved && saved !== "auto" && capture.RUNTIME_BIN[saved] && capture.which(capture.RUNTIME_BIN[saved])) override = saved;
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 activeCli = ar && capture.RUNTIME_BIN[ar.kind]
51
- ? {
52
- mode: "cli",
81
+ const active = ar
82
+ ? runtimeFromSelection({
83
+ role: "orchestrator",
53
84
  kind: ar.kind,
54
- model: ar.model || null,
55
- capabilities: ["code", "tools", ...(ar.long_context ? ["long-context"] : [])],
56
- efforts: [],
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 (override) {
60
- if (!capture.RUNTIME_BIN[override]) {
61
- const error = new Error(`unknown workforce runtime: ${override} (capture drivers: ${Object.keys(capture.RUNTIME_BIN).join(", ")})`);
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 activeCli && activeCli.kind === override ? activeCli : { mode: "cli", kind: override };
101
+ return active && active.mode === "cli" && active.kind === selectedOverride
102
+ ? active
103
+ : { mode: "cli", kind: selectedOverride, model: null, effort: null };
66
104
  }
67
- if (activeCli) return activeCli;
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])) return { mode: "cli", 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("no_runtime: no agent CLI or connected API runtime found (claude / codex / gemini / BYOK / Ollama).");
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
- async function listWorkforceTools({ db, roster, cwd, env, timeoutMs, signal }) {
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
- // Terminal 원샷 러너는 네이티브 per-tool 권한 부여 경계를 아직 증명하지 못했다.
448
- // 증명 없이 true를 돌려주면 권한 제조가 된다 — v1과 동일하게 항상 false.
449
- supportsWorkforceToolAuthority: async () => false,
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.10",
3
+ "version": "1.0.12",
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"