agentlas 1.0.46 → 1.0.48

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 (51) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +7 -7
  3. package/engine/acp/server.cjs +279 -0
  4. package/engine/agentlas-capabilities.cjs +4 -2
  5. package/engine/agentlas-core-harness.cjs +18 -0
  6. package/engine/agentlas-i18n.cjs +8 -8
  7. package/engine/agentlas-input.cjs +3 -2
  8. package/engine/agentlas-native-host.cjs +130 -11
  9. package/engine/agentlas-onboard.cjs +9 -3
  10. package/engine/agentlas-permissions.cjs +5 -1
  11. package/engine/agentlas-workforce.cjs +81 -24
  12. package/engine/agents/router.cjs +4 -2
  13. package/engine/architecture.data.json +6 -30
  14. package/engine/automation/daemon.cjs +3 -7
  15. package/engine/bootstrap-schema.sql +216 -191
  16. package/engine/browser/cdp.cjs +10 -4
  17. package/engine/cloud-assets/commands.cjs +1 -1
  18. package/engine/cloud-assets/package.cjs +161 -45
  19. package/engine/commands/acp.cjs +45 -0
  20. package/engine/commands/billing.cjs +2 -2
  21. package/engine/commands/call.cjs +4 -0
  22. package/engine/commands/context.cjs +14 -3
  23. package/engine/commands/doctor.cjs +8 -4
  24. package/engine/commands/graph.cjs +46 -54
  25. package/engine/commands/index.cjs +2 -0
  26. package/engine/commands/search.cjs +2 -2
  27. package/engine/commands/workforce.cjs +11 -0
  28. package/engine/core/desktop-core.cjs +93 -1
  29. package/engine/firms/orchestrate.cjs +32 -1
  30. package/engine/graph/interview.cjs +2 -11
  31. package/engine/graph/vocabulary.generated.cjs +1 -1
  32. package/engine/hephaestus/runtime.cjs +2 -6
  33. package/engine/project/memory-context.cjs +20 -7
  34. package/engine/project/seed.cjs +46 -31
  35. package/engine/project/state.cjs +8 -1
  36. package/engine/runtimes/acp-driver.cjs +96 -0
  37. package/engine/runtimes/auth-evidence.cjs +6 -0
  38. package/engine/runtimes/detect.cjs +3 -13
  39. package/engine/runtimes/kinds.cjs +84 -0
  40. package/engine/runtimes/resolve.cjs +67 -14
  41. package/engine/sessions/prompt.cjs +2 -2
  42. package/engine/ui/commands-catalog.cjs +2 -0
  43. package/engine/ui/palette.cjs +2 -1
  44. package/engine/ui/repl.cjs +4 -3
  45. package/engine/ui/shell.cjs +43 -5
  46. package/engine/vendor/desktop-core.manifest.json +5 -5
  47. package/engine/workforce/capture.cjs +55 -10
  48. package/engine/workforce/deps.cjs +2 -2
  49. package/engine/workforce/local-core-transport.cjs +13 -19
  50. package/package.json +2 -1
  51. package/engine/project/super-ontology-seed.json +0 -3288
@@ -5,8 +5,8 @@
5
5
  * v1 monolith "Agentlas Cloud packaging" 절의 충실 이식. 핵심 계약(약화 금지):
6
6
  * - 패키징/보안 리뷰는 전부 로컬에서 돈다. Agent Cloud에는 패키지 데이터·해시·
7
7
  * 로컬 리뷰 증적만 올라간다 (플랫폼 LLM 호출 없음).
8
- * - 시크릿 발견 = blocker = 등록 fetch 0회. (agentlas-secret-patterns 공유 모듈 +
9
- * 파일의 구조적 credential 검사 + 파일명 차단 목록의 3중 게이트)
8
+ * - 정적 finding은 advisory다. 시크릿/위험 패턴/초과 바이트는 원문을 싣지 않고,
9
+ * 경로·사유·원본 SHA-256에 묶인 결정적 omission receipt를 남긴 등록을 계속한다.
10
10
  * - 파일 읽기는 no-follow + 전/후 fstat 대조 — 스캔 중 바꿔치기(symlink swap,
11
11
  * append)는 전부 blocker다. TOCTOU로 패키지에 외부 파일이 새는 것을 막는다.
12
12
  * - .agentlas 로컬 상태(경험 계보 experience-relations.jsonl 계열, CAS 마커,
@@ -15,6 +15,7 @@
15
15
  * 으로만 — 조용한 덮어쓰기 금지 (cas.cjs).
16
16
  */
17
17
  const fs = require("node:fs");
18
+ const crypto = require("node:crypto");
18
19
  const os = require("node:os");
19
20
  const path = require("node:path");
20
21
  const {
@@ -133,14 +134,19 @@ function cloudTextContainsStructuredCredential(text) {
133
134
  }
134
135
 
135
136
  function cloudAddSecretFindingsFromBytes(bytes, relativePath, addFinding) {
137
+ let found = false;
136
138
  const candidates = new Set([bytes.toString("utf8")]);
137
139
  const utf16 = cloudDecodeUtf16CredentialText(bytes);
138
140
  if (utf16) candidates.add(utf16);
139
141
  for (const text of candidates) {
140
142
  for (const [id, re, label] of CLOUD_SECRET_RE) {
141
- if (re.test(text)) addFinding(id, "blocker", "secret", `Possible ${label} found in package content.`, relativePath, "Remove the value and require users to configure their own key.");
143
+ if (re.test(text)) {
144
+ found = true;
145
+ addFinding(id, "blocker", "secret", `Possible ${label} found in package content.`, relativePath, "Remove the value and require users to configure their own key.");
146
+ }
142
147
  }
143
148
  if (cloudTextContainsStructuredCredential(text)) {
149
+ found = true;
144
150
  addFinding("generic-unquoted-secret", "blocker", "secret", "Possible unquoted or URL-embedded credential found in package content.", relativePath, "Replace the value with an environment/BYOK placeholder.");
145
151
  }
146
152
  // 공유 시크릿 패턴(agentlas-secret-patterns)도 같은 게이트에 태운다.
@@ -157,12 +163,14 @@ function cloudAddSecretFindingsFromBytes(bytes, relativePath, addFinding) {
157
163
  const assignmentSplit = matched.match(/^[^:=]{0,40}[:=]\s*(.+)$/s);
158
164
  if (assignmentSplit && !cloudCredentialValueLooksReal(assignmentSplit[1])) continue;
159
165
  addFinding("shared-secret-pattern", "blocker", "secret", "Possible live credential (shared secret-pattern match) found in package content.", relativePath, "Remove the value and require users to configure their own key.");
166
+ found = true;
160
167
  sharedPatternHit = true;
161
168
  break;
162
169
  }
163
170
  if (sharedPatternHit) break;
164
171
  }
165
172
  }
173
+ return found;
166
174
  }
167
175
 
168
176
  // ── 스냅샷 읽기 도우미 ──
@@ -358,6 +366,7 @@ function scanCloudFolder(rootPath) {
358
366
  const files = [];
359
367
  const included = [];
360
368
  const findings = [];
369
+ const omissions = [];
361
370
  const restoredExecutablePaths = cloudReadRestoreExecutablePaths(rootPath);
362
371
  let localPackageMarker = null;
363
372
  let totalBytes = 0;
@@ -366,13 +375,22 @@ function scanCloudFolder(rootPath) {
366
375
  function addFinding(kind, severity, category, message, file, remediation) {
367
376
  findings.push({ id: `${kind}-${sha(file || message).slice(0, 10)}`, severity, category, message, ...(file ? { file } : {}), ...(remediation ? { remediation } : {}) });
368
377
  }
378
+ function addOmission(relativePath, reason, source) {
379
+ omissions.push({
380
+ path: relativePath,
381
+ reason,
382
+ sourceBytes: Number.isSafeInteger(source?.bytes) && source.bytes >= 0 ? source.bytes : 0,
383
+ sourceSha256: String(source?.sha256 || sha(`unavailable:${relativePath}:${reason}`)).toLowerCase(),
384
+ sourceHashKind: source?.hashKind || "unavailable-observation",
385
+ });
386
+ }
369
387
  function insideRoot(candidate) {
370
388
  const relative = path.relative(rootPath, candidate);
371
389
  return relative === "" || (relative && !relative.startsWith("..") && !path.isAbsolute(relative));
372
390
  }
373
391
  // no-follow open + 전/후 fstat/realpath 대조: 읽는 동안 파일이 바뀌면(스왑·append)
374
392
  // 무조건 실패한다. 캡처한 바이트와 디스크 상태가 다르면 패키지에 넣지 않는다.
375
- function readStableFile(file, rel) {
393
+ function readStableFile(file, rel, { capture = true } = {}) {
376
394
  const beforeReal = fs.realpathSync.native(file);
377
395
  if (!insideRoot(beforeReal)) throw new Error("file resolves outside the approved package root");
378
396
  const noFollow = fs.constants.O_NOFOLLOW || 0;
@@ -381,18 +399,19 @@ function scanCloudFolder(rootPath) {
381
399
  try {
382
400
  const before = fs.fstatSync(fd);
383
401
  if (!before.isFile()) throw new Error("package entry is not a regular file");
384
- if (before.size > CLOUD_MAX_FILE_BYTES) throw new Error(`file exceeds ${CLOUD_MAX_FILE_BYTES} bytes`);
402
+ const captureBytes = capture && before.size <= CLOUD_MAX_FILE_BYTES;
385
403
  const chunks = [];
404
+ const hasher = crypto.createHash("sha256");
386
405
  let actualBytes = 0;
387
406
  for (;;) {
388
- const capacity = Math.min(64 * 1024, CLOUD_MAX_FILE_BYTES + 1 - actualBytes);
389
- if (capacity <= 0) throw new Error(`file exceeds ${CLOUD_MAX_FILE_BYTES} bytes`);
407
+ const capacity = 64 * 1024;
390
408
  const chunk = Buffer.allocUnsafe(capacity);
391
409
  const read = fs.readSync(fd, chunk, 0, chunk.length, null);
392
410
  if (read === 0) break;
393
411
  actualBytes += read;
394
- if (actualBytes > CLOUD_MAX_FILE_BYTES) throw new Error(`file exceeds ${CLOUD_MAX_FILE_BYTES} bytes`);
395
- chunks.push(chunk.subarray(0, read));
412
+ const slice = chunk.subarray(0, read);
413
+ hasher.update(slice);
414
+ if (captureBytes) chunks.push(slice);
396
415
  }
397
416
  const after = fs.fstatSync(fd);
398
417
  const afterReal = fs.realpathSync.native(file);
@@ -407,7 +426,9 @@ function scanCloudFolder(rootPath) {
407
426
  throw new Error("package entry changed while it was being read");
408
427
  }
409
428
  return {
410
- bytes: Buffer.concat(chunks, actualBytes),
429
+ bytes: captureBytes ? Buffer.concat(chunks, actualBytes) : null,
430
+ byteLength: actualBytes,
431
+ sha256: hasher.digest("hex"),
411
432
  executable: cloudPortableExecutableForFile(rel, after.mode, restoredExecutablePaths),
412
433
  };
413
434
  } finally {
@@ -430,6 +451,7 @@ function scanCloudFolder(rootPath) {
430
451
  let entries;
431
452
  try {
432
453
  entries = fs.readdirSync(dir, { withFileTypes: true });
454
+ entries.sort((a, b) => cloudCodePointPathOrder({ path: a.name }, { path: b.name }));
433
455
  } catch (error) {
434
456
  addFinding("unsafe-directory", "blocker", "policy", `Package directory could not be read safely: ${error.message || error}`, path.relative(rootPath, dir).split(path.sep).join("/"), "Remove linked or changing directories and retry.");
435
457
  return;
@@ -463,7 +485,11 @@ function scanCloudFolder(rootPath) {
463
485
  }
464
486
  if (entry.isSymbolicLink()) {
465
487
  addFinding("symlink", "blocker", "policy", "Symbolic links are not allowed in cloud agent packages.", rel, "Replace the symlink with an ordinary file or remove it.");
466
- files.push({ path: rel, bytes: 0, sha256: "", kind: "binary", included: false, reason: "symlink-blocked" });
488
+ let linkTarget = "";
489
+ try { linkTarget = fs.readlinkSync(abs); } catch { /* advisory receipt keeps unavailable observation */ }
490
+ const source = { bytes: Buffer.byteLength(linkTarget), sha256: sha(Buffer.from(linkTarget)), hashKind: "link-target" };
491
+ addOmission(rel, "symlink-blocked", source);
492
+ files.push({ path: rel, bytes: source.bytes, sha256: source.sha256, kind: "binary", included: false, reason: "symlink-blocked" });
467
493
  continue;
468
494
  }
469
495
  if (entry.isDirectory()) {
@@ -473,30 +499,49 @@ function scanCloudFolder(rootPath) {
473
499
  }
474
500
  if (!entry.isFile()) {
475
501
  addFinding("unsupported-entry", "blocker", "policy", "Only stable ordinary files and directories are allowed in Cloud packages.", rel, "Remove sockets, FIFOs, devices, and other special filesystem entries.");
476
- files.push({ path: rel, bytes: 0, sha256: "", kind: "binary", included: false, reason: "unsupported-entry" });
502
+ const descriptor = Buffer.from(`special-entry:${entry.name}`);
503
+ const source = { bytes: 0, sha256: sha(descriptor), hashKind: "filesystem-entry" };
504
+ addOmission(rel, "unsupported-entry", source);
505
+ files.push({ path: rel, bytes: 0, sha256: source.sha256, kind: "binary", included: false, reason: "unsupported-entry" });
477
506
  continue;
478
507
  }
479
508
  if (!cloudPortableRelativePath(rel)) {
480
509
  addFinding("unsafe-path", "blocker", "policy", "File path is not portable across supported hosts.", rel, "Rename the file to a Unicode NFC, relative, cross-platform-safe path.");
481
- files.push({ path: rel, bytes: 0, sha256: "", kind: "binary", included: false, reason: "unsafe-path" });
510
+ let source;
511
+ try {
512
+ const stable = readStableFile(abs, rel, { capture: false });
513
+ source = { bytes: stable.byteLength, sha256: stable.sha256, hashKind: "content" };
514
+ } catch { source = null; }
515
+ addOmission(rel, "unsafe-path", source);
516
+ files.push({ path: rel, bytes: source?.bytes || 0, sha256: source?.sha256 || "", kind: "binary", included: false, reason: "unsafe-path" });
482
517
  continue;
483
518
  }
484
519
  count++;
485
- if (count > CLOUD_MAX_FILES) {
486
- addFinding("file-count-limit", "blocker", "size", `Package has more than ${CLOUD_MAX_FILES} files.`, "", "Publish a focused agent/team folder.");
487
- continue;
488
- }
520
+ const exceedsFileCount = count > CLOUD_MAX_FILES;
521
+ if (exceedsFileCount) addFinding("file-count-limit", "blocker", "size", `Package has more than ${CLOUD_MAX_FILES} files.`, rel, "Publish a focused agent/team folder.");
489
522
  if (CLOUD_AGENT_FILES.has(entry.name)) hasDefinition = true;
490
523
  let hint;
491
524
  try { hint = fs.lstatSync(abs); } catch { hint = { size: 0 }; }
492
525
  if (CLOUD_BLOCKED_FILE_RE.some((re) => re.test(entry.name))) {
493
526
  addFinding("blocked-file", "blocker", "secret", "Secret-bearing file names are not allowed in cloud packages.", rel, "Remove credentials and publish only env key names.");
494
- files.push({ path: rel, bytes: Number(hint.size) || 0, sha256: "", kind: "binary", included: false, reason: "secret-file-blocked" });
527
+ let source;
528
+ try {
529
+ const stable = readStableFile(abs, rel, { capture: false });
530
+ source = { bytes: stable.byteLength, sha256: stable.sha256, hashKind: "content" };
531
+ } catch { source = null; }
532
+ addOmission(rel, "secret-file-redacted", source);
533
+ files.push({ path: rel, bytes: source?.bytes ?? (Number(hint.size) || 0), sha256: source?.sha256 || "", kind: "binary", included: false, reason: "secret-file-redacted" });
495
534
  continue;
496
535
  }
497
536
  if (Number(hint.size) > CLOUD_MAX_FILE_BYTES) {
498
537
  addFinding("large-file", "blocker", "size", `File exceeds ${CLOUD_MAX_FILE_BYTES} bytes.`, rel, "Move large assets out of the package.");
499
- files.push({ path: rel, bytes: Number(hint.size), sha256: "", kind: "binary", included: false, reason: "file-too-large" });
538
+ let source;
539
+ try {
540
+ const stable = readStableFile(abs, rel, { capture: false });
541
+ source = { bytes: stable.byteLength, sha256: stable.sha256, hashKind: "content" };
542
+ } catch { source = null; }
543
+ addOmission(rel, "file-too-large", source);
544
+ files.push({ path: rel, bytes: source?.bytes ?? Number(hint.size), sha256: source?.sha256 || "", kind: "binary", included: false, reason: "file-too-large" });
500
545
  continue;
501
546
  }
502
547
  const ext = path.extname(entry.name).toLowerCase();
@@ -506,26 +551,53 @@ function scanCloudFolder(rootPath) {
506
551
  stable = readStableFile(abs, rel);
507
552
  } catch (error) {
508
553
  addFinding("unstable-file", "blocker", "policy", `Package file could not be read safely: ${error.message || error}`, rel, "Remove linked or concurrently changing files and retry.");
554
+ addOmission(rel, "unstable-file", { bytes: Number(hint.size) || 0, sha256: sha(`unstable:${rel}:${Number(hint.size) || 0}`), hashKind: "unstable-observation" });
509
555
  files.push({ path: rel, bytes: Number(hint.size) || 0, sha256: "", kind: isText ? "text" : "binary", included: false, reason: "unstable-file" });
510
556
  continue;
511
557
  }
512
558
  const content = stable.bytes;
513
559
  const executable = stable.executable;
514
- totalBytes += content.length;
515
- const digest = sha(content);
516
- cloudAddSecretFindingsFromBytes(content, rel, addFinding);
560
+ const digest = stable.sha256;
561
+ const source = { bytes: stable.byteLength, sha256: digest, hashKind: "content" };
562
+ if (!content) {
563
+ addFinding("large-file", "blocker", "size", `File exceeds ${CLOUD_MAX_FILE_BYTES} bytes.`, rel, "Move large assets out of the package.");
564
+ addOmission(rel, "file-too-large", source);
565
+ files.push({ path: rel, bytes: stable.byteLength, sha256: digest, kind: isText ? "text" : "binary", executable, included: false, reason: "file-too-large" });
566
+ continue;
567
+ }
568
+ if (exceedsFileCount) {
569
+ addOmission(rel, "file-count-limit", source);
570
+ files.push({ path: rel, bytes: content.length, sha256: digest, kind: isText ? "text" : "binary", executable, included: false, reason: "file-count-limit" });
571
+ continue;
572
+ }
573
+ if (cloudAddSecretFindingsFromBytes(content, rel, addFinding)) {
574
+ addOmission(rel, "secret-content-redacted", source);
575
+ files.push({ path: rel, bytes: content.length, sha256: digest, kind: isText ? "text" : "binary", executable, included: false, reason: "secret-content-redacted" });
576
+ continue;
577
+ }
517
578
  if (isText) {
518
579
  const decoded = cloudDecodeTextAsset(content);
519
580
  if (!decoded.ok) {
520
581
  addFinding("invalid-text-encoding", "blocker", "policy", "A text agent asset is not valid UTF-8 or BOM-marked UTF-16.", rel, "Save the file as UTF-8 or BOM-marked UTF-16 before packaging.");
582
+ addOmission(rel, "invalid-text-encoding", source);
521
583
  files.push({ path: rel, bytes: content.length, sha256: digest, kind: "text", executable, included: false, reason: "invalid-text-encoding" });
522
584
  continue;
523
585
  }
524
586
  const text = decoded.text;
525
587
  if (/(?:curl|wget)[^\n|&;]+[|]\s*(?:sh|bash)/i.test(text)) {
526
588
  addFinding("curl-pipe-shell", "high", "network", "Remote shell install pattern detected.", rel, "Use explicit, reviewable install steps.");
589
+ addOmission(rel, "remote-shell-pattern-redacted", source);
590
+ files.push({ path: rel, bytes: content.length, sha256: digest, kind: "text", executable, included: false, reason: "remote-shell-pattern-redacted" });
591
+ continue;
527
592
  }
528
593
  }
594
+ if (totalBytes + content.length > CLOUD_MAX_TOTAL_BYTES) {
595
+ addFinding("package-size-limit", "blocker", "size", `Including this file would exceed ${CLOUD_MAX_TOTAL_BYTES} package bytes.`, rel, "Publish a smaller agent folder.");
596
+ addOmission(rel, "package-total-bytes-limit", source);
597
+ files.push({ path: rel, bytes: content.length, sha256: digest, kind: isText ? "text" : "binary", executable, included: false, reason: "package-total-bytes-limit" });
598
+ continue;
599
+ }
600
+ totalBytes += content.length;
529
601
  files.push({ path: rel, bytes: content.length, sha256: digest, kind: isText ? "text" : "binary", executable, included: true });
530
602
  included.push({ path: rel, bytes: content.length, sha256: digest, executable, contentBase64: content.toString("base64") });
531
603
  }
@@ -545,15 +617,38 @@ function scanCloudFolder(rootPath) {
545
617
  }
546
618
  }
547
619
  walk(rootPath);
548
- const pathConflict = cloudPortablePathConflict(included.map((file) => file.path));
549
- if (pathConflict) {
550
- addFinding(pathConflict.code, "blocker", "policy", pathConflict.message, "", "Rename aliased paths so every file and ancestor directory has one portable identity.");
620
+ included.sort(cloudCodePointPathOrder);
621
+ const portableIncluded = [];
622
+ for (const file of included) {
623
+ const pathConflict = cloudPortablePathConflict([...portableIncluded.map((row) => row.path), file.path]);
624
+ if (!pathConflict) {
625
+ portableIncluded.push(file);
626
+ continue;
627
+ }
628
+ addFinding(pathConflict.code, "blocker", "policy", pathConflict.message, file.path, "Rename aliased paths so every file and ancestor directory has one portable identity.");
629
+ addOmission(file.path, "portable-path-conflict", { bytes: file.bytes, sha256: file.sha256, hashKind: "content" });
630
+ const record = files.find((row) => row.path === file.path);
631
+ if (record) { record.included = false; record.reason = "portable-path-conflict"; }
551
632
  }
633
+ included.splice(0, included.length, ...portableIncluded);
634
+ totalBytes = included.reduce((sum, file) => sum + file.bytes, 0);
552
635
  if (!hasDefinition) addFinding("missing-agent-definition", "blocker", "structure", "No agent definition file was found.", "", "Add AGENTS.md, CLAUDE.md, GEMINI.md, AGENT.md, or README.md at the package root.");
553
- if (totalBytes > CLOUD_MAX_TOTAL_BYTES) addFinding("package-size-limit", "blocker", "size", `Package exceeds ${CLOUD_MAX_TOTAL_BYTES} bytes.`, "", "Publish a smaller agent folder.");
554
636
  files.sort(cloudCodePointPathOrder);
555
- included.sort(cloudCodePointPathOrder);
556
- return { files, included, findings, totalBytes, localPackageMarker };
637
+ omissions.sort(cloudCodePointPathOrder);
638
+ return { files, included, findings, omissions, totalBytes, localPackageMarker };
639
+ }
640
+
641
+ function cloudOmissionReceipt(omissions) {
642
+ if (!Array.isArray(omissions) || omissions.length === 0) return undefined;
643
+ const entries = omissions.map((entry) => ({
644
+ path: entry.path,
645
+ reason: entry.reason,
646
+ sourceBytes: entry.sourceBytes,
647
+ sourceSha256: entry.sourceSha256,
648
+ sourceHashKind: entry.sourceHashKind,
649
+ })).sort(cloudCodePointPathOrder);
650
+ const payload = { schemaVersion: "agentlas.cloud-package-omission-receipt.v1", entries };
651
+ return { ...payload, digest: `sha256:${sha(Buffer.from(JSON.stringify(payload)))}` };
557
652
  }
558
653
 
559
654
  // ── 라우팅 카드 (공개 Hub 발행 전용 게이트) ──
@@ -736,6 +831,13 @@ function cloudReplacePublicCareerCard(scan, card) {
736
831
  if (!card) {
737
832
  // redact 실패 시 원본 카드는 절대 발행 패키지에 실리지 않는다.
738
833
  if (fileRecord) { fileRecord.included = false; fileRecord.reason = "public-career-card-blocked"; }
834
+ if (existing) scan.omissions.push({
835
+ path: relativePath,
836
+ reason: "public-career-card-redacted",
837
+ sourceBytes: existing.bytes,
838
+ sourceSha256: existing.sha256,
839
+ sourceHashKind: "content",
840
+ });
739
841
  return;
740
842
  }
741
843
  const bytes = Buffer.from(JSON.stringify(card, null, 2) + "\n", "utf8");
@@ -743,6 +845,13 @@ function cloudReplacePublicCareerCard(scan, card) {
743
845
  scan.included.push(replacement);
744
846
  scan.included.sort(cloudCodePointPathOrder);
745
847
  scan.totalBytes += bytes.length - (existing?.bytes || 0);
848
+ if (existing && existing.sha256 !== replacement.sha256) scan.omissions.push({
849
+ path: relativePath,
850
+ reason: "public-career-card-redacted",
851
+ sourceBytes: existing.bytes,
852
+ sourceSha256: existing.sha256,
853
+ sourceHashKind: "content",
854
+ });
746
855
  if (fileRecord) Object.assign(fileRecord, { bytes: bytes.length, sha256: replacement.sha256, kind: "text", executable: false, included: true, reason: undefined });
747
856
  else scan.files.push({ path: relativePath, bytes: bytes.length, sha256: replacement.sha256, kind: "text", executable: false, included: true });
748
857
  }
@@ -758,14 +867,15 @@ function privateCloudSafetyFindings(findings) {
758
867
  }
759
868
 
760
869
  function cloudStaticReview(findings, scope = "hub-public") {
761
- const blockers = findings.filter((f) => f.severity === "blocker").length;
762
- const high = findings.filter((f) => f.severity === "high").length;
870
+ const blockers = findings.filter((f) => (f.riskLevel || f.severity) === "blocker").length;
871
+ const high = findings.filter((f) => (f.riskLevel || f.severity) === "high").length;
763
872
  return {
764
873
  mode: "static-only",
765
- verdict: blockers ? "fail" : high ? "needs-review" : "pass",
874
+ authority: "advisory",
875
+ verdict: "pass",
766
876
  costOwner: "none",
767
877
  summary: blockers || high
768
- ? `${blockers} blocker(s), ${high} high-risk finding(s).`
878
+ ? `${blockers} blocker-level and ${high} high-risk advisory finding(s); unsafe source bytes were omitted or redacted.`
769
879
  : scope === "owner-private"
770
880
  ? "Private Agent Cloud safety checks passed."
771
881
  : "Static public package review passed.",
@@ -775,9 +885,9 @@ function cloudStaticReview(findings, scope = "hub-public") {
775
885
  }
776
886
 
777
887
  function cloudSecuritySummary(findings) {
778
- const blockerCount = findings.filter((f) => f.severity === "blocker").length;
779
- const highCount = findings.filter((f) => f.severity === "high").length;
780
- return { verdict: blockerCount ? "fail" : highCount ? "needs-review" : "pass", blockerCount, highCount, findingCount: findings.length };
888
+ const blockerCount = findings.filter((f) => (f.riskLevel || f.severity) === "blocker").length;
889
+ const highCount = findings.filter((f) => (f.riskLevel || f.severity) === "high").length;
890
+ return { verdict: "pass", authority: "advisory", blockerCount, highCount, findingCount: findings.length };
781
891
  }
782
892
 
783
893
  // ── 메인: 패키지(+등록) ──
@@ -848,13 +958,20 @@ async function packageCloudAgent(db, root, opts = {}) {
848
958
  });
849
959
  }
850
960
  }
851
- const packageFindings = isPublicHubPublish ? scan.findings : privateCloudSafetyFindings(scan.findings);
961
+ const selectedFindings = isPublicHubPublish ? scan.findings : privateCloudSafetyFindings(scan.findings);
962
+ const packageFindings = selectedFindings.map((finding) => ({
963
+ ...finding,
964
+ riskLevel: finding.severity,
965
+ severity: "advisory",
966
+ }));
852
967
  const name = cloudReadName(snapshot, path.basename(rootPath));
853
968
  const slug = cloudSlug(opts.slug || cloudReadStableSlug(snapshot) || name || path.basename(rootPath));
854
969
  const scope = cas.cloudScopeForVisibility(visibility);
855
970
  let baseDescriptor = state.cloudBaseDescriptorForSource(scan.localPackageMarker, rootPath, slug, scope);
856
971
  const packageHashVersion = CLOUD_PACKAGE_HASH_V2;
857
972
  const packageHash = cloudHashPackage(scan.included, packageHashVersion);
973
+ scan.omissions.sort(cloudCodePointPathOrder);
974
+ const omissionReceipt = cloudOmissionReceipt(scan.omissions);
858
975
  const manifest = {
859
976
  version: "0.1",
860
977
  kind: "agentlas-cloud-agent",
@@ -869,13 +986,15 @@ async function packageCloudAgent(db, root, opts = {}) {
869
986
  rootFingerprint: sha(`agentlas-package-root:${packageHash}`),
870
987
  packageHash,
871
988
  packageHashVersion,
872
- fileCount: scan.files.length,
989
+ fileCount: scan.included.length,
990
+ sourceFileCount: scan.files.length,
873
991
  includedFileCount: scan.included.length,
874
992
  totalBytes: scan.included.reduce((sum, file) => sum + file.bytes, 0),
875
993
  createdAt: new Date().toISOString(),
876
994
  billingMode: "static-only",
877
995
  costOwner: "none",
878
996
  security: cloudSecuritySummary(packageFindings),
997
+ ...(omissionReceipt ? { omissionReceipt } : {}),
879
998
  ...(careerGraph ? { careerGraph } : {}),
880
999
  };
881
1000
  if (routingCard.card) manifest.routingCard = routingCard.card;
@@ -887,6 +1006,7 @@ async function packageCloudAgent(db, root, opts = {}) {
887
1006
  manifest,
888
1007
  files: scan.included,
889
1008
  source: { packagedBy: "agentlas-cli", packagedAt: manifest.createdAt, costOwner: manifest.costOwner },
1009
+ ...(omissionReceipt ? { omissionReceipt } : {}),
890
1010
  ...(careerGraph ? { careerGraph } : {}),
891
1011
  };
892
1012
  fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n", "utf8");
@@ -896,10 +1016,9 @@ async function packageCloudAgent(db, root, opts = {}) {
896
1016
  manifest.security = cloudSecuritySummary(allFindings);
897
1017
  fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n", "utf8");
898
1018
  fs.writeFileSync(bundlePath, JSON.stringify({ ...bundle, manifest }, null, 2) + "\n", "utf8");
899
- const blocked = review.verdict === "fail" || allFindings.some((f) => f.severity === "blocker");
900
1019
  let registration = null;
901
- let status = blocked ? "blocked" : opts.dryRun ? "dry-run" : "ready";
902
- if (!blocked && !opts.dryRun) {
1020
+ let status = opts.dryRun ? "dry-run" : "ready";
1021
+ if (!opts.dryRun) {
903
1022
  // 자산의 정체성은 (로그인 계정, slug)다 — 어느 폴더에서 올리는지는 중요하지 않다.
904
1023
  // 쓰기 전에 서버가 들고 있는 내 자산의 현재 버전을 조회해서:
905
1024
  // · 이 폴더에 기록이 없으면(새 PC, 새 클론) 그 버전을 기준으로 업데이트한다.
@@ -970,11 +1089,7 @@ async function packageCloudAgent(db, root, opts = {}) {
970
1089
  ? isPublicHubPublish
971
1090
  ? `Published ${slug} publicly to Agentlas Hub.`
972
1091
  : `Saved ${slug} privately in Agent Cloud.`
973
- : status === "blocked"
974
- ? isPublicHubPublish
975
- ? `Hub publish blocked: ${review.summary}`
976
- : `Private Agent Cloud save blocked: ${review.summary}`
977
- : isPublicHubPublish
1092
+ : isPublicHubPublish
978
1093
  ? `Hub package ready: ${slug}.`
979
1094
  : `Private Agent Cloud package ready: ${slug}.`,
980
1095
  };
@@ -1004,6 +1119,7 @@ module.exports = {
1004
1119
  cloudReadRestoreExecutablePaths,
1005
1120
  cloudPortableExecutableForFile,
1006
1121
  scanCloudFolder,
1122
+ cloudOmissionReceipt,
1007
1123
  readCloudRoutingCard,
1008
1124
  cloudRoutingCardProblem,
1009
1125
  cloudReadPublicCareerCard,
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ /*
3
+ * acp — run Agentlas as an Agent Client Protocol agent on stdio (PRD 2026-08-15 B-3).
4
+ *
5
+ * agentlas acp start the ACP v1 agent server (stdin/stdout are the wire)
6
+ * agentlas acp --info print the registry-style descriptor and exit
7
+ *
8
+ * Register in an ACP client (Zed settings.json example):
9
+ * "agent_servers": { "Agentlas": { "command": "agentlas", "args": ["acp"] } }
10
+ * JetBrains / other clients: same command + args. The client then runs Agentlas'
11
+ * project controller on the runtime you subscribe to — no keys leave your machine.
12
+ */
13
+ const { AcpAgentServer, PROTOCOL_VERSION } = require("../acp/server.cjs");
14
+
15
+ function descriptor() {
16
+ let version = "0.0.0";
17
+ try { version = require("../../package.json").version || version; } catch { /* keep */ }
18
+ return {
19
+ id: "agentlas",
20
+ name: "Agentlas",
21
+ version,
22
+ description: "Agentlas project controller over ACP — runs on the coding runtime you already subscribe to (Claude Code, Codex, Antigravity, ACP agents).",
23
+ protocolVersion: PROTOCOL_VERSION,
24
+ distribution: { npm: { package: `agentlas@${version}`, args: ["acp"] } },
25
+ authMethods: [],
26
+ capabilities: { loadSession: false, promptCapabilities: { image: false, audio: false, embeddedContext: true } },
27
+ };
28
+ }
29
+
30
+ async function run(ctx, args) {
31
+ if (args.includes("--info") || args.includes("--json")) {
32
+ ctx.out(JSON.stringify(descriptor(), null, 2));
33
+ return 0;
34
+ }
35
+ if (args.includes("--help") || args.includes("help")) {
36
+ ctx.out("Usage: agentlas acp [--info]\n Speak the Agent Client Protocol (v1) on stdio so an editor can run Agentlas as its agent.");
37
+ return 0;
38
+ }
39
+ // stdout is the protocol wire from here on: route everything human to stderr.
40
+ const server = new AcpAgentServer({ ctx, input: process.stdin, output: process.stdout });
41
+ await server.start();
42
+ return 0;
43
+ }
44
+
45
+ module.exports = { run, descriptor };
@@ -26,8 +26,8 @@ function usage(ko) {
26
26
  ? " 구독 계좌(A)와 렌트수익 계좌(B) 잔액을 표시합니다."
27
27
  : " Shows the subscription account (A) and rental-earnings account (B) balances.",
28
28
  ko
29
- ? " 크레딧은 Hub 에이전트 호출(공개 에이전트 3·팀 10, 활성 리스는 0)에 쓰입니다."
30
- : " Credits pay for Hub agent calls (public agent 3 · team 10; active leases cost 0).",
29
+ ? " 크레딧은 Hub 에이전트 호출에 작업당 쓰입니다(기본 공개 에이전트 3·팀 10, 크리에이터 가격이 있으면 그 가격). 활성 장기대여 중에는 0."
30
+ : " Credits pay per work order for Hub agent calls (base: public agent 3 · team 10; creator-priced agents charge their price). 0 while a day-lease is active.",
31
31
  ko
32
32
  ? " 참고: 렌트수익(B) → 구독(A) 전송은 Agentlas Desktop 에서만 가능합니다 (터미널 전송 명령 없음)."
33
33
  : " Note: earnings (B) → subscription (A) transfer is Desktop-only (no transfer command in the terminal).",
@@ -18,6 +18,10 @@ async function run(ctx, args) {
18
18
  ctx.err("✖ " + usageFor("call", ctx.lang));
19
19
  return 1;
20
20
  }
21
+ // 과금 사전 고지 — 가격은 서버가 청구 시 확정하므로 숫자를 지어내지 않는다.
22
+ ctx.out(ctx.lang !== "en"
23
+ ? "ℹ 공개 Hub 에이전트·팀 호출은 크레딧이 소모됩니다(활성 장기대여 중에는 0). 잔액 확인: agentlas billing"
24
+ : "ℹ Public Hub agent/team calls consume credits (0 while a day-lease is active). Check balance: agentlas billing");
21
25
  return create(ctx).cmdHep(["hep-call", ...args]);
22
26
  }
23
27
 
@@ -13,6 +13,7 @@ const { projectCwd } = require("../project/paths.cjs");
13
13
  const { ensureTerminalProjectForExecutionCli } = require("../project/state.cjs");
14
14
  const {
15
15
  CONTEXT_MAP_MIN_CORE_VERSION,
16
+ CONTEXT_MAP_V3_RUNTIME_MARKERS,
16
17
  resolveCoreRuntimeRoot,
17
18
  resolvePython,
18
19
  spawnCoreModule,
@@ -25,6 +26,10 @@ function usage(ctx) {
25
26
  return 1;
26
27
  }
27
28
 
29
+ function hasExplicitProjectArg(args) {
30
+ return args.some((arg) => arg === "--project" || String(arg).startsWith("--project="));
31
+ }
32
+
28
33
  async function run(ctx, args) {
29
34
  if (!args[0]) return usage(ctx);
30
35
  if (!CONTEXT_SUBCOMMANDS.has(String(args[0]))) return usage(ctx);
@@ -32,7 +37,11 @@ async function run(ctx, args) {
32
37
  // 수동 검사만 — context 명령은 프로젝트를 초기화하지 않는다 (0.9.10 경계).
33
38
  ensureTerminalProjectForExecutionCli(ctx.db(), cwd, "read", "terminal-context");
34
39
 
35
- const coreRoot = resolveCoreRuntimeRoot(null, [], { minVersion: CONTEXT_MAP_MIN_CORE_VERSION });
40
+ const coreRoot = resolveCoreRuntimeRoot(
41
+ null,
42
+ CONTEXT_MAP_V3_RUNTIME_MARKERS,
43
+ { minVersion: CONTEXT_MAP_MIN_CORE_VERSION },
44
+ );
36
45
  if (!coreRoot) {
37
46
  // 정직 정지: 맵을 지어내지 않는다.
38
47
  ctx.err(ctx.lang === "ko"
@@ -51,7 +60,9 @@ async function run(ctx, args) {
51
60
  }
52
61
 
53
62
  const contextArgs = args.slice();
54
- if (!contextArgs.includes("--project")) contextArgs.push("--project", cwd);
63
+ if (!hasExplicitProjectArg(contextArgs)) {
64
+ contextArgs.push("--project", cwd);
65
+ }
55
66
  const child = spawnCoreModule("agentlas_cloud", ["context", ...contextArgs], { cwd, stdio: "inherit" }, coreRoot);
56
67
  if (!child) {
57
68
  ctx.err("Agentlas Core runtime or Python 3.9+ is unavailable.");
@@ -63,4 +74,4 @@ async function run(ctx, args) {
63
74
  });
64
75
  }
65
76
 
66
- module.exports = { run, CONTEXT_SUBCOMMANDS };
77
+ module.exports = { run, CONTEXT_SUBCOMMANDS, hasExplicitProjectArg };
@@ -9,7 +9,9 @@ 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 { RUNTIME_BIN } = require("../runtimes/kinds.cjs");
12
13
  const { runtimeAuthEvidence } = require("../runtimes/auth-evidence.cjs");
14
+ const { sharedRuntimeKind } = require("../runtimes/resolve.cjs");
13
15
  const { resolvedModelRole } = require("../runtimes/roles.cjs");
14
16
 
15
17
  function roleDetail(selection, role, en) {
@@ -31,11 +33,12 @@ async function run(ctx, args = []) {
31
33
  const db = ctx.db();
32
34
  const clis = listAvailableCliRuntimes().map((c) => ({ kind: c.kind, path: c.path, authEvidence: runtimeAuthEvidence(c.kind).status }));
33
35
  const active = activeRuntimeRow(db);
36
+ const activeKind = sharedRuntimeKind(active);
34
37
  return (() => {
35
38
  ctx.out(JSON.stringify({
36
39
  database: { path: dbPath(), exists: fs.existsSync(dbPath()) },
37
40
  runtimes: clis,
38
- activeRuntime: active ? { ...active, authEvidence: runtimeAuthEvidence(active.kind).status } : null,
41
+ activeRuntime: active ? { ...active, runtimeKind: activeKind, authEvidence: runtimeAuthEvidence(activeKind).status } : null,
39
42
  modelRoles: {
40
43
  orchestrator: resolvedModelRole(db, "orchestrator"),
41
44
  worker: resolvedModelRole(db, "worker"),
@@ -98,12 +101,13 @@ async function run(ctx, args = []) {
98
101
  const db = ctx.db();
99
102
  const active = activeRuntimeRow(db);
100
103
  if (active) {
101
- const detail = `${active.kind}${active.model ? ` (${active.model})` : ""}`;
104
+ const activeKind = sharedRuntimeKind(active);
105
+ const detail = `${activeKind}${active.model ? ` (${active.model})` : ""}`;
102
106
  // 활성 런타임은 모든 실행이 지나는 문이다 — 로그인 흔적이 없으면 all clear
103
107
  // 가 아니라 경고다. 흔적 없음 = 미로그인 "가능성"이므로 단정하지 않는다.
104
- const evidence = runtimeAuthEvidence(active.kind);
108
+ const evidence = runtimeAuthEvidence(activeKind);
105
109
  if (evidence.status === "none") {
106
- const bin = { "claude-code": "claude", codex: "codex", gemini: "gemini" }[active.kind] || active.kind;
110
+ const bin = RUNTIME_BIN[activeKind] || activeKind;
107
111
  warn(
108
112
  en ? "active runtime" : "활성 런타임",
109
113
  en