agentlas 1.0.60 → 1.0.61
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/engine/agentlas-memory-governance.cjs +88 -10
- package/engine/agentlas-permissions.cjs +95 -1
- package/engine/agentlas-tools.cjs +45 -2
- package/engine/agents/builder.cjs +4 -0
- package/engine/architecture.data.json +1 -1
- package/engine/bootstrap-schema.sql +188 -24
- package/engine/cloud-assets/cas.cjs +27 -1
- package/engine/cloud-assets/package.cjs +126 -0
- package/engine/cloud-assets/upload-scan-catalog.generated.cjs +13 -0
- package/engine/commands/career-graph.cjs +1 -1
- package/engine/commands/graph.cjs +6 -2
- package/engine/commands/index.cjs +4 -1
- package/engine/commands/one.cjs +307 -0
- package/engine/commands/ontology.cjs +2 -2
- package/engine/commands/plugin.cjs +61 -16
- package/engine/commands/uninstall.cjs +43 -13
- package/engine/core/capability-grants.cjs +204 -0
- package/engine/core/desktop-core.cjs +22 -0
- package/engine/experience/build.cjs +8 -0
- package/engine/graph/node-effect.cjs +56 -0
- package/engine/graph/package.cjs +6 -1
- package/engine/graph/vocabulary.generated.cjs +1 -1
- package/engine/hub/install.cjs +7 -3
- package/engine/hub/plugins.cjs +165 -0
- package/engine/mcp/consent.cjs +140 -12
- package/engine/mcp/index.cjs +1 -0
- package/engine/mcp/plan.cjs +63 -8
- package/engine/project/career-graph.cjs +5 -10
- package/engine/project/ontology.cjs +71 -29
- package/engine/sessions/memory-turn.cjs +39 -0
- package/engine/sessions/orchestrator.cjs +53 -4
- package/engine/sessions/session.cjs +10 -2
- package/engine/sessions/store.cjs +48 -12
- package/engine/ui/commands-catalog.cjs +1 -0
- package/engine/ui/repl.cjs +3 -1
- package/engine/vendor/desktop-core.manifest.json +5 -5
- package/package.json +4 -3
|
@@ -68,6 +68,22 @@ function cloudCasResponseError(response, label) {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
/** 등록(생성/갱신). 성공 시 검증된 리비전 디스크립터(+operation/url)를 반환한다. */
|
|
71
|
+
// 서버는 받은 것보다 적게 저장할 수 있고, 그건 증명 실패가 아니다.
|
|
72
|
+
//
|
|
73
|
+
// 등록은 제출 해시를 검증한 뒤, 자체 스캔이 자격증명으로 판정한 파일을 빼고
|
|
74
|
+
// 나머지를 새 해시로 저장한다(uploadReceipt.omissions가 뺀 경로를 전부 적는다).
|
|
75
|
+
// 저장 해시만 대조하면, 이미 허브에 게시가 끝난 뒤에 "잘못된 등록 영수증"으로
|
|
76
|
+
// 실패하게 된다 — 에이전트는 검색·호출되는데 올린 사람은 실패로 듣는다.
|
|
77
|
+
// 증명의 목적은 "서버가 바로 이 패키지를 봤다"이고 submittedPackageHash가 그
|
|
78
|
+
// 증거이므로, 둘 중 하나가 우리 해시와 같으면 통과시킨다. 둘 다 아니면 그대로 실패.
|
|
79
|
+
function registrationSawOurPackage(json, manifest) {
|
|
80
|
+
const ours = String(manifest.packageHash || "").toLowerCase();
|
|
81
|
+
if (String(json.packageHash || "").toLowerCase() === ours) return true;
|
|
82
|
+
const receipt = json && typeof json === "object" ? json.uploadReceipt : null;
|
|
83
|
+
if (!receipt || typeof receipt !== "object") return false;
|
|
84
|
+
return String(receipt.submittedPackageHash || "").toLowerCase() === ours;
|
|
85
|
+
}
|
|
86
|
+
|
|
71
87
|
async function registerCloudAgent(manifest, bundlePath, review, visibility, options = {}) {
|
|
72
88
|
const cookie = await cloudSessionCookie();
|
|
73
89
|
if (!cookie) throw new Error("Agent Cloud sign-in is required. Sign in through Desktop or set AGENTLAS_SESSION.");
|
|
@@ -119,7 +135,7 @@ async function registerCloudAgent(manifest, bundlePath, review, visibility, opti
|
|
|
119
135
|
json.dryRun !== false ||
|
|
120
136
|
typeof json.cloudId !== "string" || !json.cloudId.trim() ||
|
|
121
137
|
json.slug !== manifest.slug ||
|
|
122
|
-
json
|
|
138
|
+
!registrationSawOurPackage(json, manifest) ||
|
|
123
139
|
json.packageHashVersion !== manifest.packageHashVersion ||
|
|
124
140
|
typeof json.revision !== "string" || etag !== cloudRevisionEtag(json.revision) ||
|
|
125
141
|
typeof json.registeredAt !== "string" || !Number.isFinite(Date.parse(json.registeredAt)) ||
|
|
@@ -138,11 +154,21 @@ async function registerCloudAgent(manifest, bundlePath, review, visibility, opti
|
|
|
138
154
|
etag,
|
|
139
155
|
updatedAt: json.savedAt || json.registeredAt,
|
|
140
156
|
}, "registration receipt");
|
|
157
|
+
// 서버가 자기 스캔으로 뺀 파일은 사용자에게 보여야 한다. 영수증 안에만 있으면
|
|
158
|
+
// "내 파일이 안 올라갔다"는 사실을 아무도 읽지 않는다.
|
|
159
|
+
const serverWithheld = (() => {
|
|
160
|
+
const receipt = json && typeof json === "object" ? json.uploadReceipt : null;
|
|
161
|
+
const omissions = receipt && Array.isArray(receipt.omissions) ? receipt.omissions : [];
|
|
162
|
+
return omissions
|
|
163
|
+
.map((entry) => (entry && typeof entry.path === "string" ? entry.path : ""))
|
|
164
|
+
.filter(Boolean);
|
|
165
|
+
})();
|
|
141
166
|
return {
|
|
142
167
|
...descriptor,
|
|
143
168
|
operation: json.operation,
|
|
144
169
|
...(typeof json.url === "string" ? { url: json.url } : {}),
|
|
145
170
|
...(typeof json.marketplaceUrl === "string" ? { marketplaceUrl: json.marketplaceUrl } : {}),
|
|
171
|
+
...(serverWithheld.length ? { serverWithheld } : {}),
|
|
146
172
|
registeredAt: json.registeredAt,
|
|
147
173
|
dryRun: false,
|
|
148
174
|
};
|
|
@@ -368,6 +368,34 @@ function cloudPortableExecutableForFile(relativePath, statMode, restoredExecutab
|
|
|
368
368
|
return Boolean(statMode & 0o111);
|
|
369
369
|
}
|
|
370
370
|
|
|
371
|
+
// 잘라내기는 에이전트의 능력을 대가로 하면 안 된다 (오너 결정 2026-08-18).
|
|
372
|
+
//
|
|
373
|
+
// 한도에 닿으면 폴더에서 나오는 순서대로 잘렸다. 그 순서엔 의미가 없어서,
|
|
374
|
+
// 벤치마크 파일이 살고 skills/ 가 통째로 빠지는 일이 실제로 일어난다(데스크탑에서
|
|
375
|
+
// 실측: 벤치마크 13개 실림, skills 3개 전멸). 순위 0은 에이전트 자신이므로 절대
|
|
376
|
+
// 버리지 않고, 자리가 없으면 이미 넣은 낮은 순위 중 가장 큰 것을 대신 빼서 만든다.
|
|
377
|
+
// 뺀 파일은 전부 영수증(trimmed-for-package-limits)을 남긴다.
|
|
378
|
+
// 엔진(agentlas_cloud/upload.py)·데스크탑(cloud-agents/package.ts)의 rankOf와 같은 축.
|
|
379
|
+
const CLOUD_CAPABILITY_DIR_RE = /(^|\/)(knowledge|skills?|prompts?|presets?|agents|workers|contracts|shotplans|playbooks|templates)\//;
|
|
380
|
+
const CLOUD_BUILD_OUTPUT_RE = /(^|\/)(node_modules|dist|build|out|coverage|\.next|\.venv|__pycache__|\.git)\//;
|
|
381
|
+
const CLOUD_MEDIA_RE = /\.(png|jpe?g|gif|webp|svg|mp4|mov|mp3|wav|pdf|zip|tar|gz|tgz|bin|so|dylib|dll|wasm|sqlite|db)$/;
|
|
382
|
+
const CLOUD_SIDE_MATERIAL_RE = /(^|\/)(tests?|__tests__|fixtures?|benchmarks?|logs?|examples?)\//;
|
|
383
|
+
const CLOUD_BULK_DATA_RE = /\.(log|jsonl|csv|tsv|lock)$/;
|
|
384
|
+
|
|
385
|
+
function cloudTrimRank(rel, bytes) {
|
|
386
|
+
const lower = String(rel || "").toLowerCase();
|
|
387
|
+
const base = lower.split("/").pop() || lower;
|
|
388
|
+
if (CLOUD_AGENT_FILES.has(base)) return 0;
|
|
389
|
+
if (lower.startsWith(".agentlas/")) return 0;
|
|
390
|
+
if (base === "agentlas.json" || base === "manifest.json" || base === "package.json") return 0;
|
|
391
|
+
if (CLOUD_CAPABILITY_DIR_RE.test(lower)) return 0;
|
|
392
|
+
if (CLOUD_BUILD_OUTPUT_RE.test(lower)) return 1;
|
|
393
|
+
if (CLOUD_MEDIA_RE.test(lower)) return 2;
|
|
394
|
+
if (CLOUD_SIDE_MATERIAL_RE.test(lower)) return 3;
|
|
395
|
+
if (CLOUD_BULK_DATA_RE.test(lower)) return 4;
|
|
396
|
+
return bytes > 64 * 1024 ? 5 : 6;
|
|
397
|
+
}
|
|
398
|
+
|
|
371
399
|
// ── 폴더 스캔 (TOCTOU-안전) ──
|
|
372
400
|
|
|
373
401
|
function scanCloudFolder(rootPath) {
|
|
@@ -392,6 +420,37 @@ function scanCloudFolder(rootPath) {
|
|
|
392
420
|
sourceHashKind: source?.hashKind || "unavailable-observation",
|
|
393
421
|
});
|
|
394
422
|
}
|
|
423
|
+
/** 순위 0 파일이 들어갈 자리를, 이미 넣은 낮은 순위 중 큰 것부터 빼서 만든다.
|
|
424
|
+
* 뺀 바이트 수를 돌려준다(0이면 뺄 것이 없었다는 뜻). */
|
|
425
|
+
function evictLowestRankToFit(rel, needBytes) {
|
|
426
|
+
if (cloudTrimRank(rel, needBytes) !== 0) return 0;
|
|
427
|
+
let freed = 0;
|
|
428
|
+
while (totalBytes - freed + needBytes > CLOUD_MAX_TOTAL_BYTES) {
|
|
429
|
+
let victimIndex = -1;
|
|
430
|
+
let victimRank = 0;
|
|
431
|
+
let victimBytes = -1;
|
|
432
|
+
for (let index = 0; index < included.length; index += 1) {
|
|
433
|
+
const candidate = included[index];
|
|
434
|
+
const rank = cloudTrimRank(candidate.path, candidate.bytes);
|
|
435
|
+
if (rank === 0) continue;
|
|
436
|
+
if (rank > victimRank || (rank === victimRank && candidate.bytes > victimBytes)) {
|
|
437
|
+
victimIndex = index;
|
|
438
|
+
victimRank = rank;
|
|
439
|
+
victimBytes = candidate.bytes;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
if (victimIndex < 0) return freed;
|
|
443
|
+
const victim = included.splice(victimIndex, 1)[0];
|
|
444
|
+
freed += victim.bytes;
|
|
445
|
+
addOmission(victim.path, "trimmed-for-package-limits", { bytes: victim.bytes, sha256: victim.sha256, hashKind: "content" });
|
|
446
|
+
const record = files.find((item) => item.path === victim.path && item.included);
|
|
447
|
+
if (record) {
|
|
448
|
+
record.included = false;
|
|
449
|
+
record.reason = "trimmed-for-package-limits";
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return freed;
|
|
453
|
+
}
|
|
395
454
|
function insideRoot(candidate) {
|
|
396
455
|
const relative = path.relative(rootPath, candidate);
|
|
397
456
|
return relative === "" || (relative && !relative.startsWith("..") && !path.isAbsolute(relative));
|
|
@@ -599,6 +658,10 @@ function scanCloudFolder(rootPath) {
|
|
|
599
658
|
continue;
|
|
600
659
|
}
|
|
601
660
|
}
|
|
661
|
+
if (totalBytes + content.length > CLOUD_MAX_TOTAL_BYTES) {
|
|
662
|
+
// 순위 0(에이전트 정의·카드·skills·knowledge …)이면 자리를 만든다.
|
|
663
|
+
totalBytes -= evictLowestRankToFit(rel, content.length);
|
|
664
|
+
}
|
|
602
665
|
if (totalBytes + content.length > CLOUD_MAX_TOTAL_BYTES) {
|
|
603
666
|
addFinding("package-size-limit", "blocker", "size", `Including this file would exceed ${CLOUD_MAX_TOTAL_BYTES} package bytes.`, rel, "Publish a smaller agent folder.");
|
|
604
667
|
addOmission(rel, "package-total-bytes-limit", source);
|
|
@@ -713,6 +776,18 @@ function cloudRoutingCardProblem(card) {
|
|
|
713
776
|
values.some((value) => typeof value !== "string" || !pattern.test(value))) {
|
|
714
777
|
return `workforce.${field} contains an invalid or duplicate semantic ID`;
|
|
715
778
|
}
|
|
779
|
+
// Scaffold slots that were never filled in. `{{SKILL_ID_1}}` slugifies into
|
|
780
|
+
// `skill:skill-id-1` — a well-formed id that names nothing and matches no
|
|
781
|
+
// work. A skill is named by its <host>/skills/<name>/SKILL.md folder, so an
|
|
782
|
+
// unfilled slot means "not declared", never "declared as skill-id-1".
|
|
783
|
+
// 접두는 필드명이 아니라 패턴에서 뽑는다 — communities 는 community 이지 communitie 가 아니다.
|
|
784
|
+
const prefix = (/\^([a-z]+):/.exec(pattern.source) || [, field])[1];
|
|
785
|
+
const unfilled = values.filter((value) =>
|
|
786
|
+
/\{\{.*?\}\}/.test(String(value)) ||
|
|
787
|
+
new RegExp(`^${prefix}:${prefix}-id-\\d+$`).test(String(value)));
|
|
788
|
+
if (unfilled.length) {
|
|
789
|
+
return `workforce.${field} still carries unfilled scaffold slots: ${unfilled.slice(0, 3).join(", ")}`;
|
|
790
|
+
}
|
|
716
791
|
}
|
|
717
792
|
for (const field of ["languages", "modalities"]) {
|
|
718
793
|
const values = workforce[field];
|
|
@@ -830,6 +905,54 @@ function cloudReadPublicCareerCard(snapshot, findings) {
|
|
|
830
905
|
return cloudSanitizePublicCareerCard(parsed);
|
|
831
906
|
}
|
|
832
907
|
|
|
908
|
+
/**
|
|
909
|
+
* 업로드 사본의 `agentlas.json` 에 불변 신원을 채운다 — **사용자 폴더는 안 건드린다.**
|
|
910
|
+
*
|
|
911
|
+
* 터미널은 엔진(`upload.py`)을 부르지 않고 자체 포장기를 쓴다. 그래서 여기로 올린
|
|
912
|
+
* 패키지에는 지금까지 `agentId` 가 실린 적이 없고, 서버는 그 패키지의 신원을 바뀌는
|
|
913
|
+
* 문자열 다섯 개의 지문으로만 정할 수 있었다 — 같은 폴더를 `hep upload` 로 올리면
|
|
914
|
+
* 신원이 실리고 `agentlas upload` 로 올리면 안 실려 정의가 둘로 갈렸다.
|
|
915
|
+
*
|
|
916
|
+
* 엔진과 같은 규칙을 쓴다: 팀은 `agt_team_…`, 단일은 `agt_…`, 이름 해시로 결정론적.
|
|
917
|
+
* 이미 선언돼 있으면 절대 덮어쓰지 않는다.
|
|
918
|
+
*/
|
|
919
|
+
function cloudEnsureAgentIdentity(scan, snapshot, fallbackName) {
|
|
920
|
+
const relativePath = "agentlas.json";
|
|
921
|
+
const manifest = cloudReadSnapshotJson(snapshot, relativePath);
|
|
922
|
+
const declared = typeof manifest.agentId === "string" ? manifest.agentId.trim() : "";
|
|
923
|
+
if (declared) return declared;
|
|
924
|
+
|
|
925
|
+
const isTeam = scan.included.some((file) => /^agents\/[^/]+\/agent\.md$/.test(file.path));
|
|
926
|
+
const seed = String(
|
|
927
|
+
manifest.slug || manifest.name || fallbackName || "",
|
|
928
|
+
).trim().toLowerCase();
|
|
929
|
+
if (!seed) return "";
|
|
930
|
+
const digest = crypto.createHash("sha256")
|
|
931
|
+
.update(`agentlas-upload-agent-id-v1\0${seed}`, "utf8")
|
|
932
|
+
.digest("hex");
|
|
933
|
+
const agentId = `agt_${isTeam ? "team_" : ""}${digest.slice(0, 32)}`;
|
|
934
|
+
|
|
935
|
+
const next = { ...manifest, agentId };
|
|
936
|
+
const bytes = Buffer.from(JSON.stringify(next, null, 2) + "\n", "utf8");
|
|
937
|
+
const replacement = {
|
|
938
|
+
path: relativePath,
|
|
939
|
+
bytes: bytes.length,
|
|
940
|
+
sha256: sha(bytes),
|
|
941
|
+
contentBase64: bytes.toString("base64"),
|
|
942
|
+
executable: false,
|
|
943
|
+
};
|
|
944
|
+
const index = scan.included.findIndex((file) => file.path === relativePath);
|
|
945
|
+
const existing = index >= 0 ? scan.included[index] : null;
|
|
946
|
+
if (index >= 0) scan.included.splice(index, 1);
|
|
947
|
+
scan.included.push(replacement);
|
|
948
|
+
scan.included.sort(cloudCodePointPathOrder);
|
|
949
|
+
scan.totalBytes += bytes.length - (existing?.bytes || 0);
|
|
950
|
+
const fileRecord = scan.files.find((file) => file.path === relativePath);
|
|
951
|
+
if (fileRecord) Object.assign(fileRecord, { bytes: bytes.length, sha256: replacement.sha256, kind: "text", executable: false, included: true, reason: undefined });
|
|
952
|
+
else scan.files.push({ path: relativePath, bytes: bytes.length, sha256: replacement.sha256, kind: "text", executable: false, included: true });
|
|
953
|
+
return agentId;
|
|
954
|
+
}
|
|
955
|
+
|
|
833
956
|
function cloudReplacePublicCareerCard(scan, card) {
|
|
834
957
|
const relativePath = ".agentlas/public-career-card.json";
|
|
835
958
|
const includedIndex = scan.included.findIndex((file) => file.path === relativePath);
|
|
@@ -949,6 +1072,9 @@ async function packageCloudAgent(db, root, opts = {}) {
|
|
|
949
1072
|
cloudReplacePublicCareerCard(scan, careerGraph);
|
|
950
1073
|
snapshot = cloudPackageSnapshot(scan.included);
|
|
951
1074
|
}
|
|
1075
|
+
// 신원은 해시 계산 전에 채운다 — 클라우드 해시는 agentlas.json 을 포함한다.
|
|
1076
|
+
cloudEnsureAgentIdentity(scan, snapshot, rootPath.split("/").pop());
|
|
1077
|
+
snapshot = cloudPackageSnapshot(scan.included);
|
|
952
1078
|
const routingCard = isPublicHubPublish ? readCloudRoutingCard(snapshot) : {};
|
|
953
1079
|
if (routingCard.finding) scan.findings.push(routingCard.finding);
|
|
954
1080
|
if (isPublicHubPublish) {
|
|
@@ -93,10 +93,23 @@ const FOLDER_SCAN_AGENT_DEFINITION_FILES = Object.freeze([
|
|
|
93
93
|
"persona.md",
|
|
94
94
|
]);
|
|
95
95
|
|
|
96
|
+
const PACKAGE_MAX_TOTAL_BYTES = 31457280;
|
|
97
|
+
const PACKAGE_MAX_FILE_BYTES = 6291456;
|
|
98
|
+
const PACKAGE_MAX_UNCOMPRESSED_TOTAL_BYTES = 125829120;
|
|
99
|
+
const PACKAGE_MAX_UNCOMPRESSED_FILE_BYTES = 25165824;
|
|
100
|
+
const PACKAGE_MAX_FILES = 400;
|
|
101
|
+
const PACKAGE_MAX_REQUEST_BYTES = 47185920;
|
|
102
|
+
|
|
96
103
|
module.exports = {
|
|
97
104
|
SECRET_SCAN_TEXT_EXTENSIONS,
|
|
98
105
|
UPLOAD_SKIP_DIRECTORIES,
|
|
99
106
|
AGENT_DEFINITION_FILES,
|
|
100
107
|
UPLOAD_AGENT_DEFINITION_FILES,
|
|
101
108
|
FOLDER_SCAN_AGENT_DEFINITION_FILES,
|
|
109
|
+
PACKAGE_MAX_TOTAL_BYTES,
|
|
110
|
+
PACKAGE_MAX_FILE_BYTES,
|
|
111
|
+
PACKAGE_MAX_UNCOMPRESSED_TOTAL_BYTES,
|
|
112
|
+
PACKAGE_MAX_UNCOMPRESSED_FILE_BYTES,
|
|
113
|
+
PACKAGE_MAX_FILES,
|
|
114
|
+
PACKAGE_MAX_REQUEST_BYTES,
|
|
102
115
|
};
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
|
|
3
|
+
const { nodeDeclaresOutwardEffect: reachesOutside } = require("../graph/node-effect.cjs");
|
|
2
4
|
/*
|
|
3
5
|
* graph — 저장된 자동화 그래프를 터미널에서 보고 vendored Desktop Core로 직접 실행한다.
|
|
4
6
|
*
|
|
@@ -14,6 +16,8 @@ const path = require("node:path");
|
|
|
14
16
|
const pkgLib = require("../graph/package.cjs");
|
|
15
17
|
const desktopCore = require("../core/desktop-core.cjs");
|
|
16
18
|
|
|
19
|
+
|
|
20
|
+
|
|
17
21
|
function graphRows(ctx, db) {
|
|
18
22
|
if (!ctx.tableExists(db, "automations")) return [];
|
|
19
23
|
const hasGraph = ctx.columnExists(db, "automations", "graph_json");
|
|
@@ -441,7 +445,7 @@ function nodeLine(ctx, node, en) {
|
|
|
441
445
|
// 묻지 않는다. "확인 후 실행" 표시는 거짓이므로 없앴다. 옛 그래프의 approval
|
|
442
446
|
// 선언이 남아 있어도 마찬가지다. 사실 그대로의 고지는 "바깥을 바꿈" 하나다.
|
|
443
447
|
const marks = [
|
|
444
|
-
|
|
448
|
+
reachesOutside(node) ? (en ? "changes things outside" : "바깥을 바꿈") : null,
|
|
445
449
|
node.config?.consumes ? `${en ? "uses" : "사용"} {{${node.config.consumes}}}` : null,
|
|
446
450
|
node.config?.produces ? `${en ? "makes" : "생성"} {{${node.config.produces}}}` : null,
|
|
447
451
|
].filter(Boolean);
|
|
@@ -940,7 +944,7 @@ async function newGraph(ctx, request, flags) {
|
|
|
940
944
|
ctx.out(ctx.ui.dim(` ${bp.goal}`));
|
|
941
945
|
ctx.out("");
|
|
942
946
|
renderGraphTree(ctx, built.graph, en);
|
|
943
|
-
const mutations = built.graph.nodes.filter((n) => n
|
|
947
|
+
const mutations = built.graph.nodes.filter((n) => reachesOutside(n));
|
|
944
948
|
if (mutations.length) {
|
|
945
949
|
ctx.out("");
|
|
946
950
|
// ★사실 그대로의 고지 — 이 단계들은 실행 중에 멈춰 묻지 않는다(승인 게이트 폐지,
|
|
@@ -18,6 +18,10 @@ const COMMANDS = {
|
|
|
18
18
|
mcp: () => require("./mcp.cjs"),
|
|
19
19
|
help: () => require("./help.cjs"),
|
|
20
20
|
run: () => require("./run.cjs"),
|
|
21
|
+
// One 축 — 데스크탑/모바일과 **같은 공유 DB 의 One 대화**를 이어간다.
|
|
22
|
+
// (2026-08-20까지 DESKTOP_ONLY_SURFACES 에 있었다. 대화는 이미 공유 DB 에 있었고
|
|
23
|
+
// 터미널만 그것을 못 봤을 뿐이다 — 표면 부재를 제품 경계로 착각한 선언이었다.)
|
|
24
|
+
one: () => require("./one.cjs"),
|
|
21
25
|
login: () => require("./login.cjs"),
|
|
22
26
|
logout: () => require("./logout.cjs"),
|
|
23
27
|
whoami: () => require("./whoami.cjs"),
|
|
@@ -97,7 +101,6 @@ const DESKTOP_ONLY_SURFACES = {
|
|
|
97
101
|
apps: "Apps surface is Desktop-only.",
|
|
98
102
|
quests: "Quests are Desktop-only.",
|
|
99
103
|
bookmarks: "Hub bookmarks: run `AGENTLAS_TUI=1 agentlas` then /marketplace.",
|
|
100
|
-
one: "Agentlas One is a separate Desktop/Mobile product surface.",
|
|
101
104
|
};
|
|
102
105
|
|
|
103
106
|
/*
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* one — Agentlas One 축을 터미널에 연다.
|
|
4
|
+
*
|
|
5
|
+
* 배경(감사 2026-08-20): 터미널에는 One 축이 **아예 없었다**. commands/index 는
|
|
6
|
+
* `one` 을 DESKTOP_ONLY_SURFACES 에 올려 "One 은 Desktop/Mobile 표면"이라고 선언했다.
|
|
7
|
+
* 그런데 One 은 제품의 개인 에이전트 축이고 대화는 **공유 DB 에 이미 있다** — 터미널만
|
|
8
|
+
* 그 대화를 못 봤다. 그래서 이 명령은 새 개념을 만들지 않는다:
|
|
9
|
+
*
|
|
10
|
+
* · One = 빌트인 에이전트 `builtin-agentlas-one` (데스크탑 architecture/manifest.ts)
|
|
11
|
+
* · One 소유 대화 = chats.origin_surface = 'one'
|
|
12
|
+
* · 실행 = 터미널의 기존 세션 경로(sessions/orchestrator + sessions/session)
|
|
13
|
+
*
|
|
14
|
+
* 러너를 손으로 재구현하지 않는다. 이 파일이 하는 일은 (1) One 신원을 정확히 고르고
|
|
15
|
+
* (2) 이어 갈 One 대화를 고르거나 만들고 (3) 기존 세션을 그 chatId 로 띄우는 것뿐이다.
|
|
16
|
+
*
|
|
17
|
+
* 사용법
|
|
18
|
+
* agentlas one 최근 One 대화를 이어서 대화형
|
|
19
|
+
* agentlas one "<프롬프트>" 한 턴 실행
|
|
20
|
+
* agentlas one --list One 대화 목록
|
|
21
|
+
* agentlas one --new "<프롬프트>" 새 One 대화로 시작
|
|
22
|
+
* agentlas one --chat <id> "<p>" 특정 One 대화에 이어 붙임
|
|
23
|
+
* 공통: -p/--print · --runtime · --model · --effort · --permission
|
|
24
|
+
*/
|
|
25
|
+
const readline = require("node:readline");
|
|
26
|
+
const { rowToAgent } = require("../agents/registry.cjs");
|
|
27
|
+
const { resolveRuntimeForAgent } = require("../runtimes/overrides.cjs");
|
|
28
|
+
const { Orchestrator } = require("../sessions/orchestrator.cjs");
|
|
29
|
+
const { Renderer } = require("../ui/renderer.cjs");
|
|
30
|
+
const permissions = require("../agentlas-permissions.cjs");
|
|
31
|
+
const { EFFORTS } = require("../agentlas-workload-routing.cjs");
|
|
32
|
+
const { projectCwd } = require("../project/paths.cjs");
|
|
33
|
+
const { columnExists, runWriteTransaction } = require("../core/db.cjs");
|
|
34
|
+
const store = require("../sessions/store.cjs");
|
|
35
|
+
|
|
36
|
+
/** 데스크탑 정본 신원(electron/architecture/manifest.ts + builtinAgentId). */
|
|
37
|
+
const ONE_AGENT_ID = "builtin-agentlas-one";
|
|
38
|
+
const ONE_AGENT_SLUG = "agentlas-one";
|
|
39
|
+
const ONE_ORIGIN_SURFACE = "one";
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* One 신원을 공유 DB 에서 읽는다.
|
|
43
|
+
*
|
|
44
|
+
* findAgent() 를 쓰지 않는 이유: One 은 visibility='background' 라 listRoutableAgents 가
|
|
45
|
+
* 걸러 낸다(설계). One 은 라우팅 후보가 아니라 **신원 행**이므로 직접 읽는다.
|
|
46
|
+
*/
|
|
47
|
+
function resolveOneAgent(db) {
|
|
48
|
+
let row = null;
|
|
49
|
+
try {
|
|
50
|
+
row = db.prepare("SELECT * FROM installed_agents WHERE id=?").get(ONE_AGENT_ID)
|
|
51
|
+
|| db.prepare("SELECT * FROM installed_agents WHERE slug=?").get(ONE_AGENT_SLUG);
|
|
52
|
+
} catch {
|
|
53
|
+
row = null;
|
|
54
|
+
}
|
|
55
|
+
return rowToAgent(row);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* One 페르소나의 정본은 데스크탑 계약이다. 벤더된 컴파일 매니페스트에서 읽을 수 있으면
|
|
60
|
+
* 그것을 쓰고(항상 최신 계약), 없으면 그 사실을 사유로 돌려준다 — 조용히 다른 문장으로
|
|
61
|
+
* 대체하지 않는다.
|
|
62
|
+
*/
|
|
63
|
+
function desktopOnePersona() {
|
|
64
|
+
try {
|
|
65
|
+
const { findCoreRoot } = require("../core/desktop-core.cjs");
|
|
66
|
+
const root = findCoreRoot();
|
|
67
|
+
if (!root) return { prompt: null, source: null, reason: "no Desktop core is available on this machine" };
|
|
68
|
+
const path = require("node:path");
|
|
69
|
+
const manifest = require(path.join(root, "electron", "architecture", "manifest.js"));
|
|
70
|
+
const def = (manifest.BUILTIN_AGENTS || []).find((agent) => agent.slug === ONE_AGENT_SLUG);
|
|
71
|
+
if (!def || !def.systemPrompt) {
|
|
72
|
+
return { prompt: null, source: null, reason: "the Desktop core on this machine predates the Agentlas One built-in" };
|
|
73
|
+
}
|
|
74
|
+
return { prompt: def.systemPrompt, source: "desktop-core", reason: null };
|
|
75
|
+
} catch (error) {
|
|
76
|
+
return { prompt: null, source: null, reason: `Desktop core manifest unreadable: ${(error && error.message) || error}` };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function chatsHaveOriginSurface(db) {
|
|
81
|
+
return columnExists(db, "chats", "origin_surface");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** One 소유 대화 목록(최근 순). origin_surface 열이 없는 구형 DB 는 에이전트 소유로 폴백. */
|
|
85
|
+
function listOneChats(db, limit = 20) {
|
|
86
|
+
const bounded = Math.max(1, Math.min(Number(limit) || 20, 200));
|
|
87
|
+
try {
|
|
88
|
+
if (chatsHaveOriginSurface(db)) {
|
|
89
|
+
return db.prepare(
|
|
90
|
+
"SELECT id, title, updated_at, origin_surface FROM chats " +
|
|
91
|
+
"WHERE origin_surface = ? ORDER BY updated_at DESC, rowid DESC LIMIT ?",
|
|
92
|
+
).all(ONE_ORIGIN_SURFACE, bounded);
|
|
93
|
+
}
|
|
94
|
+
return db.prepare(
|
|
95
|
+
"SELECT id, title, updated_at FROM chats WHERE agent_id IN (?, ?) AND (kind IS NULL OR kind <> 'division') " +
|
|
96
|
+
"ORDER BY updated_at DESC, rowid DESC LIMIT ?",
|
|
97
|
+
).all(ONE_AGENT_ID, ONE_AGENT_SLUG, bounded);
|
|
98
|
+
} catch {
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* One 대화를 만든다. origin_surface 열이 있으면 반드시 'one' 으로 찍는다 —
|
|
105
|
+
* 이 한 칸이 데스크탑/모바일이 이 대화를 One 대화로 보는 유일한 근거다.
|
|
106
|
+
* 열이 없는 구형 DB 에서는 만들되, 호출부가 그 사실을 사용자에게 알린다.
|
|
107
|
+
*/
|
|
108
|
+
function createOneChat(db, { agentId, title, workingFolder }) {
|
|
109
|
+
if (!chatsHaveOriginSurface(db)) {
|
|
110
|
+
return { chatId: store.createChat(db, { agentId, title, kind: "user", workingFolder }), originSurfaceStamped: false };
|
|
111
|
+
}
|
|
112
|
+
const id = store.newId();
|
|
113
|
+
const now = new Date().toISOString();
|
|
114
|
+
runWriteTransaction(db, () => {
|
|
115
|
+
db.prepare(
|
|
116
|
+
"INSERT INTO chats (id, agent_id, title, created_at, updated_at, kind, parent_chat_id, working_folder, origin_surface) " +
|
|
117
|
+
"VALUES (?,?,?,?,?,?,?,?,?)",
|
|
118
|
+
).run(id, agentId, title || "One", now, now, "user", null, workingFolder, ONE_ORIGIN_SURFACE);
|
|
119
|
+
});
|
|
120
|
+
return { chatId: id, originSurfaceStamped: true };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function parseArgs(args) {
|
|
124
|
+
const out = { print: false, list: false, fresh: false, chatId: null, runtime: null, model: null, effort: null, permission: null, rest: [] };
|
|
125
|
+
for (let i = 0; i < args.length; i++) {
|
|
126
|
+
const a = args[i];
|
|
127
|
+
if (a === "-p" || a === "--print") out.print = true;
|
|
128
|
+
else if (a === "--list" || a === "list") out.list = true;
|
|
129
|
+
else if (a === "--new" || a === "new") out.fresh = true;
|
|
130
|
+
else if (a === "--chat") out.chatId = args[++i];
|
|
131
|
+
else if (a === "--runtime") out.runtime = args[++i];
|
|
132
|
+
else if (a === "--model") out.model = args[++i];
|
|
133
|
+
else if (a === "--effort") out.effort = args[++i];
|
|
134
|
+
else if (a === "--permission") out.permission = args[++i];
|
|
135
|
+
else out.rest.push(a);
|
|
136
|
+
}
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function renderList(ctx, rows) {
|
|
141
|
+
if (!rows.length) {
|
|
142
|
+
ctx.out(ctx.lang === "ko"
|
|
143
|
+
? "One 대화가 아직 없습니다. `agentlas one \"<하고 싶은 일>\"` 로 시작하세요."
|
|
144
|
+
: "No One conversations yet. Start one with: agentlas one \"<what you want>\"");
|
|
145
|
+
return 0;
|
|
146
|
+
}
|
|
147
|
+
for (const row of rows) {
|
|
148
|
+
const when = String(row.updated_at || "").slice(0, 16).replace("T", " ");
|
|
149
|
+
ctx.out(`${row.id} ${when} ${String(row.title || "One").slice(0, 60)}`);
|
|
150
|
+
}
|
|
151
|
+
return 0;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function runOne(ctx, args) {
|
|
155
|
+
const parsed = parseArgs(args);
|
|
156
|
+
if (parsed.permission && !permissions.LEVELS.includes(String(parsed.permission))) {
|
|
157
|
+
ctx.err(`unknown --permission ${parsed.permission} (use: ${permissions.LEVELS.join(" | ")})`);
|
|
158
|
+
return 1;
|
|
159
|
+
}
|
|
160
|
+
if (parsed.effort && !EFFORTS.includes(String(parsed.effort))) {
|
|
161
|
+
ctx.err(`unknown --effort ${parsed.effort} (use: ${EFFORTS.join(" | ")})`);
|
|
162
|
+
return 1;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const db = ctx.db();
|
|
166
|
+
|
|
167
|
+
// 목록은 신원 행이 없어도 답할 수 있어야 한다 — 대화는 chats 에 있고, One 행은
|
|
168
|
+
// 실행에만 필요하다. 조회를 실행 전제조건 뒤에 두면 "볼 수도 없는" 화면이 된다.
|
|
169
|
+
if (parsed.list) return renderList(ctx, listOneChats(db, 20));
|
|
170
|
+
|
|
171
|
+
const agent = resolveOneAgent(db);
|
|
172
|
+
if (!agent) {
|
|
173
|
+
ctx.err(
|
|
174
|
+
"Agentlas One is not present in the shared database yet.\n" +
|
|
175
|
+
"One is a built-in identity row (builtin-agentlas-one) seeded by the shared architecture.\n" +
|
|
176
|
+
"Run `agentlas doctor`, or launch the Agentlas Desktop app once, then retry.",
|
|
177
|
+
);
|
|
178
|
+
return 1;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// 페르소나 정본은 데스크탑 계약. 읽을 수 있으면 그것을 쓰고, 못 읽으면 DB 행을 쓰되
|
|
182
|
+
// **무엇을 못 읽었는지 말한다**(조용한 대체 금지).
|
|
183
|
+
const persona = desktopOnePersona();
|
|
184
|
+
if (persona.prompt) agent.systemPrompt = persona.prompt;
|
|
185
|
+
else if (persona.reason) {
|
|
186
|
+
ctx.err(ctx.uiInstance.c.dim(
|
|
187
|
+
`One persona came from the shared database row, not the Desktop contract — ${persona.reason}`,
|
|
188
|
+
));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
let runtime;
|
|
192
|
+
try {
|
|
193
|
+
runtime = resolveRuntimeForAgent({
|
|
194
|
+
db,
|
|
195
|
+
prefs: ctx.prefs,
|
|
196
|
+
explicit: parsed.runtime,
|
|
197
|
+
model: parsed.model,
|
|
198
|
+
effort: parsed.effort,
|
|
199
|
+
role: "orchestrator",
|
|
200
|
+
agentId: agent.id,
|
|
201
|
+
});
|
|
202
|
+
} catch (e) {
|
|
203
|
+
ctx.err(String((e && e.message) || e));
|
|
204
|
+
return 1;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const permission = permissions.normalize(parsed.permission || (ctx.prefs && ctx.prefs.permission) || "write");
|
|
208
|
+
const cwd = projectCwd();
|
|
209
|
+
const prompt = parsed.rest.join(" ").trim();
|
|
210
|
+
|
|
211
|
+
// 이어 갈 대화 고르기 — 새 개념을 만들지 않고 기존 One 대화를 쓴다.
|
|
212
|
+
let chatId = null;
|
|
213
|
+
let created = false;
|
|
214
|
+
if (parsed.chatId) {
|
|
215
|
+
const row = listOneChats(db, 200).find((item) => item.id === parsed.chatId);
|
|
216
|
+
if (!row) {
|
|
217
|
+
ctx.err(`No One conversation with id ${parsed.chatId} (see: agentlas one --list)`);
|
|
218
|
+
return 1;
|
|
219
|
+
}
|
|
220
|
+
chatId = row.id;
|
|
221
|
+
} else if (!parsed.fresh) {
|
|
222
|
+
const recent = listOneChats(db, 1);
|
|
223
|
+
if (recent.length) chatId = recent[0].id;
|
|
224
|
+
}
|
|
225
|
+
if (!chatId) {
|
|
226
|
+
const result = createOneChat(db, {
|
|
227
|
+
agentId: agent.id,
|
|
228
|
+
title: prompt ? prompt.slice(0, 60) : "One",
|
|
229
|
+
workingFolder: cwd,
|
|
230
|
+
});
|
|
231
|
+
chatId = result.chatId;
|
|
232
|
+
created = true;
|
|
233
|
+
if (!result.originSurfaceStamped) {
|
|
234
|
+
ctx.err(ctx.uiInstance.c.dim(
|
|
235
|
+
"This shared database has no chats.origin_surface column, so the conversation could not be stamped " +
|
|
236
|
+
"as a One conversation. Desktop and Mobile will show it as a normal chat until the store is migrated.",
|
|
237
|
+
));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const orch = new Orchestrator({ db, lang: ctx.lang });
|
|
242
|
+
const session = orch.spawn({ agent, runtime, permission, cwd, chatId, title: prompt ? prompt.slice(0, 60) : "One" });
|
|
243
|
+
|
|
244
|
+
const interactive = !prompt;
|
|
245
|
+
let renderer = null;
|
|
246
|
+
if (!parsed.print) {
|
|
247
|
+
renderer = new Renderer(ctx.uiInstance);
|
|
248
|
+
renderer.attach(session, { replay: false });
|
|
249
|
+
ctx.err(ctx.uiInstance.c.dim(
|
|
250
|
+
`one · ${runtime.kind}${runtime.model ? ` · ${runtime.model}` : ""} · ${permission} · ` +
|
|
251
|
+
`${created ? "new conversation" : "continuing"} ${chatId}`,
|
|
252
|
+
));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (!interactive) {
|
|
256
|
+
const res = await session.send(prompt);
|
|
257
|
+
if (renderer) renderer.detach();
|
|
258
|
+
if (parsed.print) {
|
|
259
|
+
const finalText = (res && (res.finalText || res.text)) || "";
|
|
260
|
+
if (finalText) process.stdout.write(finalText.trimEnd() + "\n");
|
|
261
|
+
if (session.status === "failed" && session.lastError) ctx.err(session.lastError);
|
|
262
|
+
}
|
|
263
|
+
return session.status === "failed" ? 1 : 0;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// 대화형 — 답할 사람이 있는 자리에서만 연다. 파이프/자동화에서는 정직하게 멈춘다.
|
|
267
|
+
if (!process.stdin.isTTY) {
|
|
268
|
+
if (renderer) renderer.detach();
|
|
269
|
+
ctx.err("Usage: agentlas one \"<prompt>\" (interactive One needs a TTY)");
|
|
270
|
+
return 1;
|
|
271
|
+
}
|
|
272
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stderr, terminal: true });
|
|
273
|
+
let failed = false;
|
|
274
|
+
try {
|
|
275
|
+
for (;;) {
|
|
276
|
+
const line = await new Promise((resolve) => rl.question("one › ", resolve));
|
|
277
|
+
const text = String(line || "").trim();
|
|
278
|
+
if (!text) continue;
|
|
279
|
+
if (/^(?:\/quit|\/exit|quit|exit)$/i.test(text)) break;
|
|
280
|
+
await session.send(text);
|
|
281
|
+
if (session.status === "failed") {
|
|
282
|
+
failed = true;
|
|
283
|
+
if (session.lastError) ctx.err(session.lastError);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
} finally {
|
|
287
|
+
rl.close();
|
|
288
|
+
if (renderer) renderer.detach();
|
|
289
|
+
}
|
|
290
|
+
return failed ? 1 : 0;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function run(ctx, args) {
|
|
294
|
+
return runOne(ctx, args);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
module.exports = {
|
|
298
|
+
run,
|
|
299
|
+
parseArgs,
|
|
300
|
+
resolveOneAgent,
|
|
301
|
+
listOneChats,
|
|
302
|
+
createOneChat,
|
|
303
|
+
desktopOnePersona,
|
|
304
|
+
ONE_AGENT_ID,
|
|
305
|
+
ONE_AGENT_SLUG,
|
|
306
|
+
ONE_ORIGIN_SURFACE,
|
|
307
|
+
};
|
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
*/
|
|
6
6
|
const { runOntologyCli } = require("../project/ontology.cjs");
|
|
7
7
|
|
|
8
|
-
function run(ctx, args) {
|
|
8
|
+
async function run(ctx, args) {
|
|
9
9
|
try {
|
|
10
|
-
const lines = runOntologyCli(args, {
|
|
10
|
+
const lines = await runOntologyCli(args, {
|
|
11
11
|
cwd: process.cwd(),
|
|
12
12
|
projectPath: process.cwd(),
|
|
13
13
|
lang: ctx.lang,
|