agentlas 1.0.28 → 1.0.29

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.
Files changed (39) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +5 -2
  3. package/engine/agentlas-i18n.cjs +4 -4
  4. package/engine/agentlas-input.cjs +0 -2
  5. package/engine/agentlas-onboard.cjs +20 -0
  6. package/engine/agentlas-workforce.cjs +41 -8
  7. package/engine/agentlas.cjs +7 -1
  8. package/engine/commands/billing.cjs +3 -0
  9. package/engine/commands/creds.cjs +49 -1
  10. package/engine/commands/doctor.cjs +46 -3
  11. package/engine/commands/graph.cjs +1150 -0
  12. package/engine/commands/help.cjs +82 -6
  13. package/engine/commands/hep-cloud.cjs +9 -23
  14. package/engine/commands/hep-hub.cjs +9 -22
  15. package/engine/commands/hep-local.cjs +9 -24
  16. package/engine/commands/hep-network.cjs +9 -35
  17. package/engine/commands/index.cjs +49 -25
  18. package/engine/commands/mcp.cjs +6 -2
  19. package/engine/commands/native.cjs +18 -2
  20. package/engine/commands/plugin.cjs +22 -0
  21. package/engine/commands/roles.cjs +202 -0
  22. package/engine/commands/workforce.cjs +63 -12
  23. package/engine/graph/ask-model.cjs +131 -0
  24. package/engine/graph/interview.cjs +875 -0
  25. package/engine/graph/layout.cjs +137 -0
  26. package/engine/graph/package.cjs +223 -0
  27. package/engine/graph/vocabulary.generated.cjs +30 -0
  28. package/engine/hephaestus/local-core.cjs +159 -0
  29. package/engine/hephaestus/runtime.cjs +4 -8
  30. package/engine/runtimes/auth-evidence.cjs +78 -0
  31. package/engine/sessions/prompt.cjs +16 -0
  32. package/engine/sessions/session.cjs +9 -0
  33. package/engine/tools/access-notice.cjs +86 -0
  34. package/engine/ui/palette.cjs +6 -3
  35. package/engine/ui/repl.cjs +8 -2
  36. package/engine/workforce/deps.cjs +13 -0
  37. package/engine/workforce/local-core-transport.cjs +298 -0
  38. package/package.json +4 -3
  39. package/engine/commands/legacy-network.cjs +0 -29
package/CHANGELOG.md CHANGED
@@ -1,5 +1,38 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.29 — 2026-08-05
4
+
5
+ Terminal-wide audit release. Every command was executed for real; what follows fixes what that audit found.
6
+
7
+ - Native federated staffing: `hep-network` / `hep-local` / `hep-cloud` / `hep-hub` now run this
8
+ terminal's own workforce loop with the local Agentlas-OS core federating the declared source scope.
9
+ (They previously passed through to an external CLI stub that always answered exit 3.)
10
+ Honest stop with install guidance when the local core is missing — no silent fallback to the public Hub.
11
+ - `workforce` now declares its sourceScope ("hub") explicitly instead of relying on the server default.
12
+ - `doctor` distinguishes installed from signed-in: local sign-in evidence per runtime, a warning
13
+ (not "all clear") when the active runtime has none.
14
+ - New `roles` command: view and set orchestrator/worker model roles from the terminal
15
+ (`roles set <role> <runtime> [--model id] [--effort lv]`, `roles set worker --inherit`).
16
+ REPL `/model`·`/runtime` now say they are session-scoped and point to `agentlas roles`.
17
+ - Setup wizard prints install/sign-in guidance when the chosen runtime is missing or unauthenticated.
18
+ - `creds list` (names and stores only — values never printed), `mcp list`, Korean `help` body.
19
+ - The graph command group ships in the npm package for the first time (it landed after 1.0.28 was published).
20
+ - New gates: user-scenarios (93 real CLI invocations), local-core transport wire contract,
21
+ doctor auth-evidence, roles round-trip, onboarding guidance, EN/KO help sync.
22
+ - `agentlas <command> --help` reaches the command's own help. It printed a two-line stub
23
+ scraped from the help table, so `graph --help` never showed the eight lines `graph help` has —
24
+ and that is the spelling people try first. (The gate had pinned the stub as correct; its
25
+ contract now asks whether real help was shown.)
26
+ - `graph install --name "<new name>"` works. The documented flag had never worked: its value was
27
+ appended to the file path, so the install died with "no such file".
28
+ - `graph show` stops indenting a straight chain deeper at every step — fourteen steps meant
29
+ twenty-eight columns. Depth now marks real branches (a fork, a failure exit).
30
+ - Building a graph follows your language setting. One Korean character anywhere in the request
31
+ forced the interview to Korean while `graph show`, the list, and errors stayed English.
32
+ - Graph steps written as code declare the pip packages they import, and a verification step is
33
+ now required whenever a step that changes something outside sends out a value an earlier step
34
+ computed — an unattended run must not ship an empty or invented result.
35
+
3
36
  ## 1.0.26 — 2026-08-03
4
37
 
5
38
  - **`agentlas list` stops letting the terminal cut its own output.** Slugs were
package/README.md CHANGED
@@ -94,10 +94,13 @@ agentlas variant resolve --base-release <id> # Preview local variant compatibili
94
94
  ```sh
95
95
  agentlas storm "<goal>" # Goal + UltraCode harness: plan -> assign -> execute -> verify [--research]
96
96
  agentlas swarm "<goal>" # Emergent multi-agent swarm [--parallel N]
97
- agentlas workforce "<prompt>" # Route through Agent Workforce Ontology
97
+ agentlas workforce "<prompt>" # Route through Agent Workforce Ontology (public Hub menu)
98
98
  agentlas network "<prompt>" # Alias for workforce
99
99
  agentlas taskforce "<prompt>" # Alias for workforce
100
- agentlas legacy-network "<prompt>" # Legacy Hephaestus solver (explicit invocation only)
100
+ agentlas hep-network "<prompt>" # Staff across Local + owner Cloud + public Hub (local Core federation)
101
+ agentlas hep-local "<prompt>" # Same staffing, registered Local agents only
102
+ agentlas hep-cloud "<prompt>" # Same staffing, owner Agent Cloud only
103
+ agentlas hep-hub "<prompt>" # Same staffing, public Hub only
101
104
  agentlas call "a,b" "<context>" # Call explicitly named Hub or Cloud agents
102
105
  agentlas browser [...] # Real browser hardpoint launcher
103
106
  agentlas route "<prompt>" [--json] # Preview routing decisions without execution
@@ -74,7 +74,6 @@ const STRINGS = {
74
74
  "effortUsage": "usage: /effort low|medium|high|max|off|auto",
75
75
  "permSet": "permission → %s",
76
76
  "permUsage": "usage: /permission read|write|full",
77
- "legacyNetworkUsage": "usage: /legacy-network <request>",
78
77
  "stormUsage": "usage: /storm <goal> [--research]",
79
78
  "buildUsage": "usage: /build <agent-or-team request>",
80
79
  "routeUsage": "usage: /route <request> [--json]",
@@ -190,7 +189,6 @@ const STRINGS = {
190
189
  "help.search": "discover agents in the Hub",
191
190
  "help.install": "install an agent from the Hub by slug",
192
191
  "help.network": "let the host LLM staff and execute an ontology-grounded exact-release task force",
193
- "help.legacyNetwork": "run the explicit compatibility-only Hephaestus network route",
194
192
  "help.browser": "real browser execution hardpoint",
195
193
  "help.connect": "wire Telegram / platforms to an agent team",
196
194
  "help.swarm": "fan out an emergent agent swarm on a goal",
@@ -251,6 +249,8 @@ const STRINGS = {
251
249
  "wiz.runtimeAuto": "Use the app's active runtime",
252
250
  "wiz.runtimeInstalled": "installed",
253
251
  "wiz.runtimeMissing": "not installed",
252
+ "wiz.runtimeInstallHint": "This runtime is not installed yet. Install it first: %s",
253
+ "wiz.runtimeLoginHint": "No local sign-in evidence for this runtime. Sign in before your first run (run: %s, then follow its login flow).",
254
254
  "wiz.permQ": "How much should agents be allowed to do by default?",
255
255
  "wiz.permRead": "read — inspect only; workspace writes blocked",
256
256
  "wiz.permWrite": "write — workspace + runtime temp writes; external MCP off (recommended)",
@@ -323,7 +323,6 @@ const STRINGS = {
323
323
  "effortUsage": "사용법: /effort low|medium|high|max|off|auto",
324
324
  "permSet": "권한 → %s",
325
325
  "permUsage": "사용법: /permission read|write|full",
326
- "legacyNetworkUsage": "사용법: /legacy-network <요청>",
327
326
  "stormUsage": "사용법: /storm <목표> [--research]",
328
327
  "buildUsage": "사용법: /build <만들 에이전트·팀 설명>",
329
328
  "routeUsage": "사용법: /route <요청> [--json]",
@@ -438,7 +437,6 @@ const STRINGS = {
438
437
  "help.search": "Hub에서 에이전트 발견",
439
438
  "help.install": "식별자로 Hub 에이전트 설치",
440
439
  "help.network": "상위 LLM이 온톨로지에서 정확한 릴리스 TF를 선발·실행",
441
- "help.legacyNetwork": "호환성 전용 Hephaestus 네트워크 경로를 명시적으로 실행",
442
440
  "help.browser": "실제 브라우저 실행 하드포인트",
443
441
  "help.connect": "Telegram/플랫폼을 에이전트 팀에 연결",
444
442
  "help.swarm": "목표에 창발형 에이전트 스웜 전개",
@@ -497,6 +495,8 @@ const STRINGS = {
497
495
  "wiz.runtimeAuto": "앱의 활성 런타임 사용",
498
496
  "wiz.runtimeInstalled": "설치됨",
499
497
  "wiz.runtimeMissing": "미설치",
498
+ "wiz.runtimeInstallHint": "이 런타임은 아직 설치되어 있지 않습니다. 먼저 설치하세요: %s",
499
+ "wiz.runtimeLoginHint": "이 런타임의 로그인 흔적이 없습니다. 첫 실행 전에 로그인하세요 (%s 실행 후 로그인 절차 진행).",
500
500
  "wiz.permQ": "에이전트가 기본적으로 어디까지 할 수 있게 할까요?",
501
501
  "wiz.permRead": "read — 조회만; 작업 공간 쓰기 차단",
502
502
  "wiz.permWrite": "write — 작업 공간·런타임 임시 경로 쓰기; 외부 MCP 꺼짐 (권장)",
@@ -300,7 +300,6 @@ const SLASH_COMMAND_META = [
300
300
  { command: "/search", description: "Discover agents in the Hub", category: "Hub", usage: "/search <what you need>", detail: "Search the Agentlas Hub + local for an agent that fits the task (hep-search)." },
301
301
  { command: "/install", description: "Install an agent from the Hub by slug", category: "Hub", usage: "/install <slug>", detail: "Install a marketplace agent into this terminal." },
302
302
  { command: "/network", description: "Staff and execute an ontology-grounded task force", category: "Engine", usage: "/network <request> [--benchmark]", detail: "The active top host LLM creates the work order, searches the Hub workforce ontology, selects exact releases, and executes a receipt-backed task force.", aliases: ["/taskforce", "/workforce"] },
303
- { command: "/legacy-network", description: "Run the compatibility Hephaestus network route", category: "Engine", usage: "/legacy-network <request>", detail: "Explicit compatibility escape hatch; never used as a fallback by /network." },
304
303
  { command: "/browser", description: "Real browser execution hardpoint", category: "Engine", usage: "/browser [sub]", detail: "Runs the Agentlas browser hardpoint (hep-browser)." },
305
304
  { command: "/connect", description: "Wire Telegram / platforms to an agent team", category: "Hub", usage: "/connect", detail: "Runs Hephaestus hep-connect for platform integration." },
306
305
  { command: "/doctor", description: "Check runtimes and local data", category: "Health", usage: "/doctor", detail: "Run local checks for runtimes, data, credentials, and setup." },
@@ -351,7 +350,6 @@ const HELP_KEY_BY_COMMAND = {
351
350
  "/research": "help.research",
352
351
  "/search": "help.search",
353
352
  "/network": "help.network",
354
- "/legacy-network": "help.legacyNetwork",
355
353
  "/browser": "help.browser",
356
354
  "/connect": "help.connect",
357
355
  "/doctor": "help.doctor",
@@ -142,6 +142,26 @@ async function runOnboard({ ui, rl, helpers, persist }) {
142
142
  rtOpts.forEach((o, i) => optionLine(i + 1, o.label, i === 0));
143
143
  const ri = await pickNum(rtOpts.length);
144
144
  const runtime = rtOpts[ri - 1].value;
145
+ // missing인 런타임을 골라도 저장은 한다(미리 설정) — 하지만 조용히 저장하면
146
+ // 첫 실행 실패가 배신이 된다(2026-08-05 감사 결함 A). 설치 명령을 그 자리에서
147
+ // 알려주고, 로그인 흔적이 없으면 로그인 절차까지 말한다.
148
+ if (runtime !== "auto") {
149
+ const installHint = {
150
+ "claude-code": "npm i -g @anthropic-ai/claude-code",
151
+ codex: "npm i -g @openai/codex",
152
+ gemini: "npm i -g @google/gemini-cli",
153
+ }[runtime];
154
+ if (!H.which(H.RUNTIME_BIN[runtime])) {
155
+ if (installHint) printIndented(ui.t("wiz.runtimeInstallHint", installHint), c.dim);
156
+ } else {
157
+ try {
158
+ const { runtimeAuthEvidence } = require("./runtimes/auth-evidence.cjs");
159
+ if (runtimeAuthEvidence(runtime).status === "none") {
160
+ printIndented(ui.t("wiz.runtimeLoginHint", H.RUNTIME_BIN[runtime]), c.dim);
161
+ }
162
+ } catch { /* evidence unavailable — say nothing rather than guess */ }
163
+ }
164
+ }
145
165
 
146
166
  // Step 3 — default permission
147
167
  ui.line("");
@@ -1097,10 +1097,15 @@ function validateWorkOrder(value) {
1097
1097
  function validateCandidateSet(value, workOrder, now = new Date(), options = {}) {
1098
1098
  const set = assertObject(value, "candidateSet");
1099
1099
  assertNoForbiddenFitSignals(set);
1100
- assertExactKeys(set, [
1100
+ // projection은 로컬 Core(연합) 응답에만 있는 메뉴 투영 메타데이터다(실측
1101
+ // 2026-08-05, reference-first: fullDossier=false). 원격 서버는 보내지 않는다.
1102
+ // 없애고 되돌려 보내면 Core 쪽 다이제스트 대조가 위험하므로 선택 키로 허용한다.
1103
+ const exactKeys = [
1101
1104
  "schemaVersion", "selectionSessionId", "workOrderId", "ontologyVersion",
1102
1105
  "candidateSetDigest", "decisionOwner", "historyInfluence", "slots", "issuedAt", "expiresAt",
1103
- ], "candidateSet", "candidate_set_invalid");
1106
+ ];
1107
+ if (Object.prototype.hasOwnProperty.call(set, "projection")) exactKeys.push("projection");
1108
+ assertExactKeys(set, exactKeys, "candidateSet", "candidate_set_invalid");
1104
1109
  if (set.schemaVersion !== "agentlas.workforce-candidate-set.v1") fail("candidate_set_invalid", "unsupported candidate set schema");
1105
1110
  assertId(set.selectionSessionId, "candidateSet.selectionSessionId");
1106
1111
  if (set.workOrderId !== workOrder.workOrderId) fail("candidate_set_invalid", "candidate set workOrderId mismatch");
@@ -1126,11 +1131,15 @@ function validateCandidateSet(value, workOrder, now = new Date(), options = {})
1126
1131
  const releases = new Set();
1127
1132
  for (const candidate of assertArray(slotResult.candidates, `candidateSet.${slotId}.candidates`, 100)) {
1128
1133
  assertObject(candidate, "candidate");
1129
- assertExactKeys(candidate, [
1134
+ // missingMandatory는 로컬 Core(연합) 응답에만 있는 미충족 필수 표식이다
1135
+ // (실측 2026-08-05, fullDossier=true에도 동봉). 원격 서버는 보내지 않는다.
1136
+ const candidateKeys = [
1130
1137
  "agentDefinitionId", "agentReleaseId", "releaseVersion", "packageHash", "contentDigest",
1131
1138
  "entityKind", "name", "communities", "fitEvidence", "qualificationEvidence", "optionalGaps",
1132
1139
  "semanticSnapshot", "operational",
1133
- ], "candidate", "candidate_set_invalid");
1140
+ ];
1141
+ if (Object.prototype.hasOwnProperty.call(candidate, "missingMandatory")) candidateKeys.push("missingMandatory");
1142
+ assertExactKeys(candidate, candidateKeys, "candidate", "candidate_set_invalid");
1134
1143
  assertId(candidate.agentDefinitionId, "candidate.agentDefinitionId");
1135
1144
  const releaseId = assertId(candidate.agentReleaseId, "candidate.agentReleaseId");
1136
1145
  if (releases.has(releaseId)) fail("candidate_set_invalid", `duplicate release ${releaseId} in ${slotId}`);
@@ -1151,10 +1160,19 @@ function validateCandidateSet(value, workOrder, now = new Date(), options = {})
1151
1160
  if (typeof operational.callable !== "boolean" || typeof operational.installable !== "boolean") fail("candidate_set_invalid", "candidate operational flags are invalid");
1152
1161
  assertIds(operational.unavailableReasons || [], "candidate.operational.unavailableReasons");
1153
1162
  const semantic = assertObject(candidate.semanticSnapshot, "candidate.semanticSnapshot");
1154
- assertExactKeys(semantic, [
1163
+ // knowledge·modalities는 로컬 Core 스냅샷에만 있는 확장 어휘다(실측 2026-08-05).
1164
+ // 원격 서버는 보내지 않는다 — missingMandatory·projection과 같은 규칙으로
1165
+ // "있으면 검증하고 허용", 원격 계약의 exact-keys는 그대로 둔다.
1166
+ const semanticKeys = [
1155
1167
  "summaries", "roles", "skills", "toolCapabilities", "consumes", "produces",
1156
1168
  "authorities", "runtimes", "languages",
1157
- ], "candidate.semanticSnapshot", "candidate_set_invalid");
1169
+ ];
1170
+ for (const optional of ["knowledge", "modalities"]) {
1171
+ if (Object.prototype.hasOwnProperty.call(semantic, optional)) semanticKeys.push(optional);
1172
+ }
1173
+ assertExactKeys(semantic, semanticKeys, "candidate.semanticSnapshot", "candidate_set_invalid");
1174
+ if (semantic.knowledge !== undefined) assertIds(semantic.knowledge, "candidate.semanticSnapshot.knowledge");
1175
+ if (semantic.modalities !== undefined) assertStrings(semantic.modalities, "candidate.semanticSnapshot.modalities");
1158
1176
  assertStrings(semantic.summaries, "candidate.semanticSnapshot.summaries");
1159
1177
  assertIds(semantic.roles, "candidate.semanticSnapshot.roles");
1160
1178
  assertLeveledConcepts(semantic.skills, "candidate.semanticSnapshot.skills");
@@ -2393,6 +2411,16 @@ function create(deps = {}) {
2393
2411
 
2394
2412
  async function workforceRun(db, rawTask, ctx = {}) {
2395
2413
  const task = assertString(rawTask, "task", 20_000);
2414
+ // 이 표면이 보는 후보 메뉴의 소스 스코프. 원격 MCP 경로의 정직한 기본은 "hub"
2415
+ // (터미널 workforce = 공개 Hub 메뉴 스태핑). 다른 값은 로컬 Core 전송을 가진
2416
+ // 호출자만 넘길 수 있다 — 여기서 조용히 넓히지 않는다.
2417
+ const sourceScope = (() => {
2418
+ const value = ctx.sourceScope === undefined ? "hub" : ctx.sourceScope;
2419
+ if (!["network", "local", "cloud", "hub"].includes(value)) {
2420
+ fail("source_scope_invalid", `sourceScope must be network|local|cloud|hub, got: ${String(value)}`);
2421
+ }
2422
+ return value;
2423
+ })();
2396
2424
  const ui = ctx.ui || newUi();
2397
2425
  const runtime = ctx.runtime || D.resolveRuntime(db, ctx.runtimeOverride);
2398
2426
  const cwd = ctx.cwd || (typeof D.projectCwd === "function" ? D.projectCwd() : process.cwd());
@@ -2744,7 +2772,9 @@ function create(deps = {}) {
2744
2772
  };
2745
2773
 
2746
2774
  const supersedeCandidateSearch = (workOrder, refinementNumber, triggerKind) => {
2747
- const requestDigest = sha256({ workOrder });
2775
+ // hubStage가 저장한 requestDigest 같은 인자 모양이어야 행을 찾는다 —
2776
+ // search 인자에 sourceScope가 실리므로(2026-08-05) 여기서도 함께 계산한다.
2777
+ const requestDigest = sha256({ workOrder, sourceScope });
2748
2778
  for (const row of receipt.hubTools) {
2749
2779
  if (row.tool !== "workforce.search_candidates" || row.requestDigest !== requestDigest || row.authoritativeChain !== true) continue;
2750
2780
  row.authoritativeChain = false;
@@ -3065,7 +3095,10 @@ function create(deps = {}) {
3065
3095
 
3066
3096
  let refinementsUsed = 0;
3067
3097
  const searchCurrentWorkOrder = async () => {
3068
- const candidateRaw = await hubStage("workforce.search_candidates", { workOrder });
3098
+ // sourceScope는 MCP 스키마상 required다. 예전에는 싣지 않아 서버 기본값
3099
+ // ("hub")에 의존했다 — 기본값이 바뀌면 이 표면의 실제 스코프가 조용히
3100
+ // 넓어지거나 좁아진다. 이 표면이 보는 메뉴를 스스로 선언한다.
3101
+ const candidateRaw = await hubStage("workforce.search_candidates", { workOrder, sourceScope });
3069
3102
  candidateSet = validateCandidateSet(
3070
3103
  candidateRaw,
3071
3104
  workOrder,
@@ -107,7 +107,13 @@ function main() {
107
107
  if (helpRequested && helpCommand) {
108
108
  const ctx = buildCtx();
109
109
  const command = commands.resolveCommandName(helpCommand);
110
- const code = require("./commands/help.cjs").runForCommand(ctx, command);
110
+ // ★명령이 자기 도움말을 갖고 있으면 그것을 보여준다.
111
+ // 예전에는 무조건 표 한 줄을 긁어(`runForCommand`) "Usage: agentlas graph [options]"
112
+ // 두 줄만 나왔다 — `graph help`에는 8줄짜리 제대로 된 안내가 있는데도 `--help`로는
113
+ // 영원히 닿지 못했다(사용자가 가장 먼저 치는 것이 `--help`다).
114
+ const code = commands.SELF_HELP_COMMANDS.has(command)
115
+ ? commands.dispatch(ctx, [command, "help"])
116
+ : require("./commands/help.cjs").runForCommand(ctx, command);
111
117
  process.exit(typeof code === "number" ? code : 0);
112
118
  }
113
119
  // 옵션 정규화: -h/--help/-V/--version 은 하위 명령으로 변환
@@ -25,6 +25,9 @@ function usage(ko) {
25
25
  ko
26
26
  ? " 구독 계좌(A)와 렌트수익 계좌(B) 잔액을 표시합니다."
27
27
  : " Shows the subscription account (A) and rental-earnings account (B) balances.",
28
+ ko
29
+ ? " 크레딧은 Hub 에이전트 호출(공개 에이전트 3·팀 10, 활성 리스는 0)에 쓰입니다."
30
+ : " Credits pay for Hub agent calls (public agent 3 · team 10; active leases cost 0).",
28
31
  ko
29
32
  ? " 참고: 렌트수익(B) → 구독(A) 전송은 Agentlas Desktop 에서만 가능합니다 (터미널 전송 명령 없음)."
30
33
  : " Note: earnings (B) → subscription (A) transfer is Desktop-only (no transfer command in the terminal).",
@@ -134,12 +134,60 @@ async function cmdCredsFile(ctx, args) {
134
134
  return 0;
135
135
  }
136
136
 
137
+ /** KEY=value 파일에서 KEY 이름만 뽑는다 — 값은 절대 읽어 돌려주지 않는다. */
138
+ function envKeyNames(filePath) {
139
+ try {
140
+ return fs.readFileSync(filePath, "utf8")
141
+ .split(/\r?\n/)
142
+ .map((line) => line.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/))
143
+ .filter(Boolean)
144
+ .map((m) => m[1]);
145
+ } catch {
146
+ return null; // 파일 없음/읽기 불가 — 목록에서 침묵이 아니라 부재로 표시
147
+ }
148
+ }
149
+
150
+ /*
151
+ * creds list — 어떤 자격증명 "이름"이 어디에 저장돼 있는지.
152
+ * 배경(2026-08-05 감사, 결함 H): save/file만 있어 저장한 것을 확인할 방법이
153
+ * 없었다. 계약은 상단과 동일 — 값은 어떤 경로로도 출력하지 않는다. 키 이름과
154
+ * 저장 위치까지만. (키체인 vault는 standalone에서 keytar를 부르면 무한 대기하는
155
+ * v1 실사고가 있어 조회조차 하지 않는다 — 부재가 아니라 "미조회"로 표시.)
156
+ */
157
+ function cmdCredsList(ctx) {
158
+ const ko = ctx.lang !== "en";
159
+ const project = activeProjectPath(ctx.db());
160
+ const stores = [];
161
+ if (project) stores.push({ label: (ko ? "프로젝트 .env — " : "project .env — ") + path.join(project, ".env"), keys: envKeyNames(path.join(project, ".env")) });
162
+ stores.push({ label: (ko ? "전역 credentials.env — " : "global credentials.env — ") + path.join(userDataDir(), "credentials.env"), keys: envKeyNames(path.join(userDataDir(), "credentials.env")) });
163
+ stores.push({ label: "~/.agentlas/credentials.env", keys: envKeyNames(path.join(os.homedir(), ".agentlas", "credentials.env")) });
164
+
165
+ let any = false;
166
+ for (const store of stores) {
167
+ if (store.keys === null) continue; // 파일 자체가 없으면 줄을 만들지 않는다
168
+ any = true;
169
+ ctx.out(ctx.ui.bold(store.label));
170
+ if (!store.keys.length) ctx.out(ctx.ui.dim(ko ? " (비어 있음)" : " (empty)"));
171
+ for (const key of store.keys) ctx.out(` ${key}${ctx.ui.dim("=•••")}`);
172
+ }
173
+ if (isElectronRuntime()) {
174
+ ctx.out(ctx.ui.dim(ko ? "키체인 vault: 데스크탑에서 확인" : "keychain vault: inspect in Desktop"));
175
+ } else {
176
+ ctx.out(ctx.ui.dim(ko
177
+ ? "키체인 vault는 standalone 터미널에서 조회하지 않습니다(서명 없는 keytar 호출은 키체인에 막혀 멈춥니다)."
178
+ : "The keychain vault is not queried from the standalone terminal (unsigned keytar calls hang on the OS keychain)."));
179
+ }
180
+ if (!any) ctx.out(ko ? "저장된 자격증명 파일이 없습니다. agentlas creds save 로 추가하세요." : "No credential files yet. Add one with: agentlas creds save");
181
+ return 0;
182
+ }
183
+
137
184
  async function run(ctx, args) {
138
185
  const fail = (msg) => { ctx.err(msg); return 1; };
139
186
  const sub = args[0];
140
187
  if (sub === "file") return cmdCredsFile(ctx, args.slice(1));
188
+ if (sub === "list" || sub === "ls") return cmdCredsList(ctx);
141
189
  if (sub !== "save") {
142
- return fail('usage: agentlas creds save --provider <name> --key <ENV_NAME> --value <value> [--project <path>] OR agentlas creds file --source <path> [--env <ENV_NAME>]');
190
+ return fail('usage: agentlas creds save --provider <name> --key <ENV_NAME> --value <value> [--project <path>] OR agentlas creds file --source <path> [--env <ENV_NAME>] OR agentlas creds list');
143
191
  }
144
192
  const f = parseCredFlags(args.slice(1));
145
193
  const key = typeof f.key === "string" ? f.key.trim() : "";
@@ -9,6 +9,7 @@ const fs = require("node:fs");
9
9
  const path = require("node:path");
10
10
  const { dbPath, userDataDir } = require("../core/paths.cjs");
11
11
  const { listAvailableCliRuntimes, activeRuntimeRow } = require("../runtimes/detect.cjs");
12
+ const { runtimeAuthEvidence } = require("../runtimes/auth-evidence.cjs");
12
13
  const { resolvedModelRole } = require("../runtimes/roles.cjs");
13
14
 
14
15
  function roleDetail(selection, role, en) {
@@ -26,8 +27,10 @@ function roleDetail(selection, role, en) {
26
27
  function run(ctx) {
27
28
  const en = ctx.lang === "en";
28
29
  let failures = 0;
30
+ let warnings = 0;
29
31
  const ok = (label, detail) => ctx.out(` ${ctx.ui.green("✓")} ${label}${detail ? ctx.ui.dim(" — " + detail) : ""}`);
30
32
  const bad = (label, detail) => { failures += 1; ctx.out(` ${ctx.ui.red("✗")} ${label}${detail ? ctx.ui.dim(" — " + detail) : ""}`); };
33
+ const warn = (label, detail) => { warnings += 1; ctx.out(` ${ctx.ui.yellow ? ctx.ui.yellow("!") : "!"} ${label}${detail ? ctx.ui.dim(" — " + detail) : ""}`); };
31
34
 
32
35
  // 1) 데이터
33
36
  const p = dbPath();
@@ -43,10 +46,28 @@ function run(ctx) {
43
46
  bad(en ? "database" : "데이터베이스", (en ? "missing: " : "없음: ") + p);
44
47
  }
45
48
 
46
- // 2) 런타임
49
+ // 2) 런타임 — 설치 여부와 인증 흔적은 다른 축이다. which()만 보면 로그아웃
50
+ // 상태에서도 all clear가 나간다(2026-08-05 실측). 각 CLI가 로그인 시 남기는
51
+ // 로컬 산출물을 증거로 관측하고, 증거≠증명이므로 문구도 단정하지 않는다.
47
52
  const clis = listAvailableCliRuntimes();
48
53
  if (clis.length) {
49
- ok(en ? "runtimes" : "런타임", clis.map((c) => `${c.kind}`).join(", "));
54
+ const evidences = clis.map((c) => ({ kind: c.kind, evidence: runtimeAuthEvidence(c.kind) }));
55
+ const annotated = evidences.map(({ kind, evidence }) => {
56
+ if (evidence.status === "none") return `${kind}(${en ? "no sign-in evidence" : "로그인 흔적 없음"})`;
57
+ return kind; // evidence 또는 unknown — 검사법이 없는 런타임을 미로그인으로 표시하지 않는다
58
+ });
59
+ ok(en ? "runtimes" : "런타임", annotated.join(", "));
60
+ // 신규 설치 상태: 검사 가능한 런타임 전부가 흔적 없음이면 첫 실행이 거의
61
+ // 확실히 실패한다 — 활성 런타임 행이 아직 없어도 여기서 경고한다.
62
+ const checkable = evidences.filter(({ evidence }) => evidence.status !== "unknown");
63
+ if (checkable.length && checkable.every(({ evidence }) => evidence.status === "none")) {
64
+ warn(
65
+ en ? "runtime sign-in" : "런타임 로그인",
66
+ en
67
+ ? "no detected runtime has local sign-in evidence — sign in to one before running (e.g. claude / codex login / gemini)"
68
+ : "감지된 어떤 런타임에도 로그인 흔적이 없습니다 — 실행 전에 하나는 로그인하세요 (예: claude / codex login / gemini)",
69
+ );
70
+ }
50
71
  } else {
51
72
  bad(en ? "runtimes" : "런타임", en
52
73
  ? "no agent CLI on PATH (claude / codex / gemini / kimi / grok / cursor-agent)"
@@ -57,7 +78,23 @@ function run(ctx) {
57
78
  try {
58
79
  const db = ctx.db();
59
80
  const active = activeRuntimeRow(db);
60
- if (active) ok(en ? "active runtime" : "활성 런타임", `${active.kind}${active.model ? ` (${active.model})` : ""}`);
81
+ if (active) {
82
+ const detail = `${active.kind}${active.model ? ` (${active.model})` : ""}`;
83
+ // 활성 런타임은 모든 실행이 지나는 문이다 — 로그인 흔적이 없으면 all clear
84
+ // 가 아니라 경고다. 흔적 없음 = 미로그인 "가능성"이므로 단정하지 않는다.
85
+ const evidence = runtimeAuthEvidence(active.kind);
86
+ if (evidence.status === "none") {
87
+ const bin = { "claude-code": "claude", codex: "codex", gemini: "gemini" }[active.kind] || active.kind;
88
+ warn(
89
+ en ? "active runtime" : "활성 런타임",
90
+ en
91
+ ? `${detail} — no local sign-in evidence; runs may fail until you sign in (try: ${bin} login)`
92
+ : `${detail} — 로그인 흔적이 없습니다. 로그인 전에는 실행이 실패할 수 있습니다 (시도: ${bin} login)`,
93
+ );
94
+ } else {
95
+ ok(en ? "active runtime" : "활성 런타임", detail);
96
+ }
97
+ }
61
98
  const orchestrator = resolvedModelRole(db, "orchestrator");
62
99
  const worker = resolvedModelRole(db, "worker");
63
100
  if (orchestrator && worker) {
@@ -83,6 +120,12 @@ function run(ctx) {
83
120
  ctx.out(en ? `doctor: ${failures} problem(s) found` : `doctor: 문제 ${failures}건`);
84
121
  return 1;
85
122
  }
123
+ if (warnings) {
124
+ // 경고가 있으면 "이상 없음"이라고 말하지 않는다 — 그 문구가 첫 실행 실패를
125
+ // 배신으로 만든다. 경고는 실행을 막지 않으므로 exit 0.
126
+ ctx.out(en ? `doctor: ${warnings} warning(s) — runnable, but check the lines above` : `doctor: 경고 ${warnings}건 — 실행은 가능하나 위 항목을 확인하세요`);
127
+ return 0;
128
+ }
86
129
  ctx.out(en ? "doctor: all clear" : "doctor: 이상 없음");
87
130
  return 0;
88
131
  }