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.
- package/CHANGELOG.md +33 -0
- package/README.md +7 -7
- package/engine/acp/server.cjs +279 -0
- package/engine/agentlas-capabilities.cjs +4 -2
- package/engine/agentlas-core-harness.cjs +18 -0
- package/engine/agentlas-i18n.cjs +8 -8
- package/engine/agentlas-input.cjs +3 -2
- package/engine/agentlas-native-host.cjs +130 -11
- package/engine/agentlas-onboard.cjs +9 -3
- package/engine/agentlas-permissions.cjs +5 -1
- package/engine/agentlas-workforce.cjs +81 -24
- package/engine/agents/router.cjs +4 -2
- package/engine/architecture.data.json +6 -30
- package/engine/automation/daemon.cjs +3 -7
- package/engine/bootstrap-schema.sql +216 -191
- package/engine/browser/cdp.cjs +10 -4
- package/engine/cloud-assets/commands.cjs +1 -1
- package/engine/cloud-assets/package.cjs +161 -45
- package/engine/commands/acp.cjs +45 -0
- package/engine/commands/billing.cjs +2 -2
- package/engine/commands/call.cjs +4 -0
- package/engine/commands/context.cjs +14 -3
- package/engine/commands/doctor.cjs +8 -4
- package/engine/commands/graph.cjs +46 -54
- package/engine/commands/index.cjs +2 -0
- package/engine/commands/search.cjs +2 -2
- package/engine/commands/workforce.cjs +11 -0
- package/engine/core/desktop-core.cjs +93 -1
- package/engine/firms/orchestrate.cjs +32 -1
- package/engine/graph/interview.cjs +2 -11
- package/engine/graph/vocabulary.generated.cjs +1 -1
- package/engine/hephaestus/runtime.cjs +2 -6
- package/engine/project/memory-context.cjs +20 -7
- package/engine/project/seed.cjs +46 -31
- package/engine/project/state.cjs +8 -1
- package/engine/runtimes/acp-driver.cjs +96 -0
- package/engine/runtimes/auth-evidence.cjs +6 -0
- package/engine/runtimes/detect.cjs +3 -13
- package/engine/runtimes/kinds.cjs +84 -0
- package/engine/runtimes/resolve.cjs +67 -14
- package/engine/sessions/prompt.cjs +2 -2
- package/engine/ui/commands-catalog.cjs +2 -0
- package/engine/ui/palette.cjs +2 -1
- package/engine/ui/repl.cjs +4 -3
- package/engine/ui/shell.cjs +43 -5
- package/engine/vendor/desktop-core.manifest.json +5 -5
- package/engine/workforce/capture.cjs +55 -10
- package/engine/workforce/deps.cjs +2 -2
- package/engine/workforce/local-core-transport.cjs +13 -19
- package/package.json +2 -1
- 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
|
-
* -
|
|
9
|
-
*
|
|
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))
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
395
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
486
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
515
|
-
const
|
|
516
|
-
|
|
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
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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
|
-
|
|
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
|
-
|
|
874
|
+
authority: "advisory",
|
|
875
|
+
verdict: "pass",
|
|
766
876
|
costOwner: "none",
|
|
767
877
|
summary: blockers || high
|
|
768
|
-
? `${blockers} blocker
|
|
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:
|
|
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
|
|
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.
|
|
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 =
|
|
902
|
-
if (!
|
|
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
|
-
:
|
|
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 에이전트
|
|
30
|
-
: " Credits pay for Hub agent calls (public agent 3 · team 10;
|
|
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).",
|
package/engine/commands/call.cjs
CHANGED
|
@@ -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(
|
|
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
|
|
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(
|
|
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
|
|
104
|
+
const activeKind = sharedRuntimeKind(active);
|
|
105
|
+
const detail = `${activeKind}${active.model ? ` (${active.model})` : ""}`;
|
|
102
106
|
// 활성 런타임은 모든 실행이 지나는 문이다 — 로그인 흔적이 없으면 all clear
|
|
103
107
|
// 가 아니라 경고다. 흔적 없음 = 미로그인 "가능성"이므로 단정하지 않는다.
|
|
104
|
-
const evidence = runtimeAuthEvidence(
|
|
108
|
+
const evidence = runtimeAuthEvidence(activeKind);
|
|
105
109
|
if (evidence.status === "none") {
|
|
106
|
-
const bin =
|
|
110
|
+
const bin = RUNTIME_BIN[activeKind] || activeKind;
|
|
107
111
|
warn(
|
|
108
112
|
en ? "active runtime" : "활성 런타임",
|
|
109
113
|
en
|