agentlas 1.0.28 → 1.0.35
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 +102 -0
- package/README.md +5 -2
- package/bin/agentlas.cjs +15 -0
- package/engine/agentlas-cloud-runtime.cjs +54 -4
- package/engine/agentlas-i18n.cjs +4 -4
- package/engine/agentlas-input.cjs +10 -3
- package/engine/agentlas-judgment.cjs +0 -0
- package/engine/agentlas-native-host.cjs +69 -4
- package/engine/agentlas-onboard.cjs +20 -0
- package/engine/agentlas-ui.cjs +2 -0
- package/engine/agentlas-workforce.cjs +69 -13
- package/engine/agentlas.cjs +71 -3
- package/engine/agents/builder.cjs +84 -0
- package/engine/agents/router.cjs +8 -2
- package/engine/automation/launchd.cjs +131 -0
- package/engine/bootstrap-schema.sql +6 -44
- package/engine/browser/cdp.cjs +188 -0
- package/engine/browser/vault.cjs +124 -0
- package/engine/cli-output.cjs +262 -0
- package/engine/cloud-assets/package.cjs +5 -1
- package/engine/commands/automation.cjs +37 -1
- package/engine/commands/billing.cjs +3 -0
- package/engine/commands/browser.cjs +166 -8
- package/engine/commands/build.cjs +101 -20
- package/engine/commands/connect.cjs +162 -9
- package/engine/commands/creds.cjs +65 -3
- package/engine/commands/doctor.cjs +66 -4
- package/engine/commands/document.cjs +79 -0
- package/engine/commands/graph.cjs +1190 -0
- package/engine/commands/help.cjs +87 -9
- package/engine/commands/hep-cloud.cjs +9 -23
- package/engine/commands/hep-hub.cjs +9 -22
- package/engine/commands/hep-local.cjs +9 -24
- package/engine/commands/hep-network.cjs +9 -35
- package/engine/commands/index.cjs +50 -25
- package/engine/commands/list.cjs +10 -1
- package/engine/commands/mcp.cjs +6 -2
- package/engine/commands/native.cjs +18 -2
- package/engine/commands/plugin.cjs +22 -0
- package/engine/commands/project.cjs +79 -16
- package/engine/commands/roles.cjs +210 -0
- package/engine/commands/telegram.cjs +23 -16
- package/engine/commands/workforce.cjs +63 -12
- package/engine/core/desktop-core-fetch.cjs +98 -0
- package/engine/core/desktop-core.cjs +170 -0
- package/engine/graph/ask-model.cjs +159 -0
- package/engine/graph/interview.cjs +960 -0
- package/engine/graph/layout.cjs +139 -0
- package/engine/graph/package.cjs +223 -0
- package/engine/graph/vocabulary.generated.cjs +30 -0
- package/engine/hephaestus/local-core.cjs +159 -0
- package/engine/hephaestus/runtime.cjs +14 -10
- package/engine/project/controller.cjs +8 -8
- package/engine/project/team.cjs +99 -0
- package/engine/runtime-refusal.cjs +71 -0
- package/engine/runtimes/auth-evidence.cjs +78 -0
- package/engine/runtimes/detect.cjs +3 -0
- package/engine/runtimes/resolve.cjs +1 -1
- package/engine/sessions/prompt.cjs +16 -0
- package/engine/sessions/session.cjs +24 -2
- package/engine/telegram/connect.cjs +202 -0
- package/engine/tools/access-notice.cjs +86 -0
- package/engine/ui/palette.cjs +7 -3
- package/engine/ui/repl.cjs +120 -3
- package/engine/vendor/desktop-core.manifest.json +7 -0
- package/engine/workforce/capture.cjs +51 -1
- package/engine/workforce/deps.cjs +13 -0
- package/engine/workforce/local-core-transport.cjs +298 -0
- package/package.json +4 -2
- package/engine/commands/legacy-network.cjs +0 -29
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// 결정적 좌→우 그래프 배치. **데스크탑 `shared/graph-layout.ts`의 미러다.**
|
|
2
|
+
//
|
|
3
|
+
// 왜 사본이 있나: 터미널은 독립 패키지라 데스크탑 소스를 임포트할 수 없다. 이 저장소는
|
|
4
|
+
// 같은 이유로 `interview.cjs`(청사진 컴파일러)도 이미 미러로 두고, **패리티 게이트**가
|
|
5
|
+
// 두 벌이 같은 판단을 하는지 대조한다(`test/graph-interview-parity.cjs`). 배치도 같은 규율:
|
|
6
|
+
// 여기서 바꾸면 데스크탑도 바꾸고, 게이트가 그 둘을 붙잡는다.
|
|
7
|
+
//
|
|
8
|
+
// ★규칙이 갈라지면 같은 그래프가 표면마다 다르게 그려지고, 사용자는 자기 자동화를
|
|
9
|
+
// 어느 쪽이 맞는지 알 수 없게 된다.
|
|
10
|
+
"use strict";
|
|
11
|
+
|
|
12
|
+
const COL_W = 280;
|
|
13
|
+
const ROW_H = 120;
|
|
14
|
+
const NODE_ORIGIN_X = 0;
|
|
15
|
+
const NODE_ORIGIN_Y = 120;
|
|
16
|
+
|
|
17
|
+
/** 노드 카드의 실측 크기 — 이보다 가까우면 화면에서 겹쳐 글자를 못 읽는다. */
|
|
18
|
+
const NODE_W = 230;
|
|
19
|
+
const NODE_H = 90;
|
|
20
|
+
|
|
21
|
+
/** 위상 순서로 각 노드에 컬럼 depth를 매긴다(진입 엣지 없는 노드 = depth 0). */
|
|
22
|
+
function computeDepths(graph) {
|
|
23
|
+
const indeg = new Map();
|
|
24
|
+
const adj = new Map();
|
|
25
|
+
for (const n of graph.nodes) {
|
|
26
|
+
indeg.set(n.id, 0);
|
|
27
|
+
adj.set(n.id, []);
|
|
28
|
+
}
|
|
29
|
+
for (const e of graph.edges || []) {
|
|
30
|
+
if (!indeg.has(e.target) || !adj.has(e.source)) continue; // dangling edge 방어
|
|
31
|
+
indeg.set(e.target, (indeg.get(e.target) || 0) + 1);
|
|
32
|
+
adj.get(e.source).push(e.target);
|
|
33
|
+
}
|
|
34
|
+
const depth = new Map();
|
|
35
|
+
const frontier = graph.nodes.filter((n) => (indeg.get(n.id) || 0) === 0).map((n) => n.id);
|
|
36
|
+
for (const id of frontier) depth.set(id, 0);
|
|
37
|
+
const remaining = new Map(indeg);
|
|
38
|
+
const queue = [...frontier];
|
|
39
|
+
while (queue.length) {
|
|
40
|
+
const id = queue.shift();
|
|
41
|
+
const d = depth.get(id) || 0;
|
|
42
|
+
for (const next of adj.get(id) || []) {
|
|
43
|
+
depth.set(next, Math.max(depth.get(next) || 0, d + 1));
|
|
44
|
+
const left = (remaining.get(next) == null ? 1 : remaining.get(next)) - 1;
|
|
45
|
+
remaining.set(next, left);
|
|
46
|
+
if (left <= 0) queue.push(next);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// 사이클(반복 그래프) 등으로 depth 미할당된 노드는 순서 인덱스로 폴백.
|
|
50
|
+
graph.nodes.forEach((n, i) => {
|
|
51
|
+
if (!depth.has(n.id)) depth.set(n.id, i);
|
|
52
|
+
});
|
|
53
|
+
return depth;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** 한 줄기(strip)에 넣을 **행** 수 — 데스크탑 rowsPerStrip과 같은 식. */
|
|
57
|
+
function rowsPerStrip(totalRows) {
|
|
58
|
+
if (totalRows <= 5) return totalRows;
|
|
59
|
+
return Math.max(5, Math.ceil(Math.sqrt(totalRows * 2)));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** @deprecated 가로 배치 시절 이름. */
|
|
63
|
+
const columnsPerBand = rowsPerStrip;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 그래프를 결정적 **세로 사행(蛇行)** 배치로 재배치한 새 노드 배열(데스크탑과 같은 규칙).
|
|
67
|
+
* 위→아래로 흐르다 줄기가 차면 오른쪽으로 접고, 다음 줄기는 아래→위로 올라간다.
|
|
68
|
+
* (오너 결정 2026-08-06: 사람이 순서를 읽는 방향이 위→아래다.)
|
|
69
|
+
*/
|
|
70
|
+
function layoutGraph(graph) {
|
|
71
|
+
const depth = computeDepths(graph);
|
|
72
|
+
const byRow = new Map();
|
|
73
|
+
for (const n of graph.nodes) {
|
|
74
|
+
const d = depth.get(n.id) || 0;
|
|
75
|
+
if (!byRow.has(d)) byRow.set(d, []);
|
|
76
|
+
byRow.get(d).push(n);
|
|
77
|
+
}
|
|
78
|
+
const rows = [...byRow.keys()].sort((a, b) => a - b);
|
|
79
|
+
const rowOrder = new Map(rows.map((r, i) => [r, i]));
|
|
80
|
+
const totalRows = rows.length;
|
|
81
|
+
const perStrip = rowsPerStrip(totalRows);
|
|
82
|
+
|
|
83
|
+
const stripCount = Math.ceil(totalRows / perStrip);
|
|
84
|
+
const stripWidth = [];
|
|
85
|
+
for (let b = 0; b < stripCount; b += 1) {
|
|
86
|
+
let maxCols = 1;
|
|
87
|
+
for (const [r, i] of rowOrder) {
|
|
88
|
+
if (Math.floor(i / perStrip) === b) maxCols = Math.max(maxCols, byRow.get(r).length);
|
|
89
|
+
}
|
|
90
|
+
stripWidth.push(maxCols * COL_W + COL_W);
|
|
91
|
+
}
|
|
92
|
+
const stripLeft = [];
|
|
93
|
+
let acc = 0;
|
|
94
|
+
for (let b = 0; b < stripCount; b += 1) { stripLeft.push(acc); acc += stripWidth[b]; }
|
|
95
|
+
|
|
96
|
+
const out = [];
|
|
97
|
+
for (const [row, nodes] of byRow) {
|
|
98
|
+
const i = rowOrder.get(row) || 0;
|
|
99
|
+
const strip = Math.floor(i / perStrip);
|
|
100
|
+
const r = i % perStrip;
|
|
101
|
+
// ★모든 줄기는 위→아래(사행 뒤집기 폐기 — 데스크탑 graph-layout.ts와 동일, 2026-08-06).
|
|
102
|
+
const count = nodes.length;
|
|
103
|
+
nodes.forEach((n, col) => {
|
|
104
|
+
const offset = (col - (count - 1) / 2) * COL_W;
|
|
105
|
+
out.push({
|
|
106
|
+
...n,
|
|
107
|
+
position: {
|
|
108
|
+
x: NODE_ORIGIN_X + stripLeft[strip] + (stripWidth[strip] - COL_W) / 2 + offset,
|
|
109
|
+
y: NODE_ORIGIN_Y + r * ROW_H,
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
const orderIndex = new Map(graph.nodes.map((n, i) => [n.id, i]));
|
|
115
|
+
out.sort((a, b) => (orderIndex.get(a.id) || 0) - (orderIndex.get(b.id) || 0));
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 재배치가 필요한가 — **실제로 겹치는가**로 판단한다.
|
|
121
|
+
* ★예전에는 좌표가 완전히 같을 때만 재배치해서, 검증을 +70·갈림길을 +140만 띄운
|
|
122
|
+
* 그래프(노드 폭 230)가 "다른 좌표"라 통과했고 카드가 서로 가렸다(실측 2026-08-05).
|
|
123
|
+
*/
|
|
124
|
+
function needsLayout(graph) {
|
|
125
|
+
if (!graph || !Array.isArray(graph.nodes) || graph.nodes.length <= 1) return false;
|
|
126
|
+
const placed = graph.nodes.map((n) => ({
|
|
127
|
+
x: Math.round((n.position && n.position.x) || 0),
|
|
128
|
+
y: Math.round((n.position && n.position.y) || 0),
|
|
129
|
+
}));
|
|
130
|
+
for (let i = 0; i < placed.length; i += 1) {
|
|
131
|
+
for (let j = i + 1; j < placed.length; j += 1) {
|
|
132
|
+
if (Math.abs(placed[i].x - placed[j].x) < NODE_W
|
|
133
|
+
&& Math.abs(placed[i].y - placed[j].y) < NODE_H) return true;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
module.exports = { layoutGraph, needsLayout, rowsPerStrip, columnsPerBand, COL_W, ROW_H, NODE_W, NODE_H };
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* .agentgraph 패키징 — 그래프를 남에게 줄 수 있는 형태로 만든다.
|
|
4
|
+
*
|
|
5
|
+
* 핵심 계약 두 줄:
|
|
6
|
+
* 1) 지울 수 없는 비밀이 하나라도 남으면 **내보내지 않는다**. 몰래 지우고 통과시키면
|
|
7
|
+
* 사용자는 자기 키가 빠진 줄 알고 공유하게 된다.
|
|
8
|
+
* 2) 모델 고정은 유통될 수 없다. 받는 사람의 기본 모델로 돌아야 하므로 등급 힌트로 바꾼다.
|
|
9
|
+
*
|
|
10
|
+
* 여기서 만드는 건 파일 하나(JSON)다. 허브 업로드는 별도 표면이며, 이 모듈은
|
|
11
|
+
* "무엇을 지웠고 무엇을 채워야 하는지"까지 파일 안에 적어 둔다.
|
|
12
|
+
*/
|
|
13
|
+
const crypto = require("node:crypto");
|
|
14
|
+
|
|
15
|
+
const SCHEMA_VERSION = "agentgraph/1.0";
|
|
16
|
+
|
|
17
|
+
/** 값이 자격증명처럼 보이는가 — 형태 판정만 한다(의미 판정 아님). */
|
|
18
|
+
const SECRET_VALUE_PATTERNS = [
|
|
19
|
+
/\bsk-[A-Za-z0-9_-]{16,}/,
|
|
20
|
+
/\bAKIA[0-9A-Z]{16}\b/,
|
|
21
|
+
/\bghp_[A-Za-z0-9]{20,}/,
|
|
22
|
+
/\bxox[baprs]-[A-Za-z0-9-]{10,}/,
|
|
23
|
+
/-----BEGIN [A-Z ]*PRIVATE KEY-----/,
|
|
24
|
+
/\bBearer\s+[A-Za-z0-9._-]{20,}/i,
|
|
25
|
+
/\beyJ[A-Za-z0-9._-]{30,}/,
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
/** 키 이름이 비밀을 담기로 되어 있는가. 값이 비어 있어도 템플릿 대상이다. */
|
|
29
|
+
const SECRET_KEY_RE = /(token|secret|password|passwd|apikey|api_key|credential|private_key|access_key)/i;
|
|
30
|
+
|
|
31
|
+
/** 로컬 사용자 경로 — 남의 기계에서 의미가 없고, 계정명이 그대로 드러난다. */
|
|
32
|
+
const PERSONAL_PATH_RE = /(\/Users\/[^/\s"']+|\/home\/[^/\s"']+|C:\\Users\\[^\\\s"']+)/g;
|
|
33
|
+
|
|
34
|
+
function vaultKeyFor(nodeId, key) {
|
|
35
|
+
return `${String(key).replace(/[^A-Za-z0-9]+/g, "_").toUpperCase()}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function looksSecretValue(value) {
|
|
39
|
+
return typeof value === "string" && SECRET_VALUE_PATTERNS.some((re) => re.test(value));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 노드 설정을 훑어 비밀·모델핀·개인 경로를 처리한다.
|
|
44
|
+
* 반환: { config, findings, vaultTemplate, blockers }
|
|
45
|
+
*/
|
|
46
|
+
function scrubNodeConfig(nodeId, config) {
|
|
47
|
+
const out = {};
|
|
48
|
+
const findings = [];
|
|
49
|
+
const vaultTemplate = [];
|
|
50
|
+
const blockers = [];
|
|
51
|
+
for (const [key, value] of Object.entries(config || {})) {
|
|
52
|
+
// 1) 비밀로 선언된 칸 — 값 유무와 무관하게 금고 변수로 바꾼다.
|
|
53
|
+
if (SECRET_KEY_RE.test(key)) {
|
|
54
|
+
const vaultKey = vaultKeyFor(nodeId, key);
|
|
55
|
+
out[key] = `$\{vault.${vaultKey}}`;
|
|
56
|
+
vaultTemplate.push({ key: vaultKey, kind: "secret", requiredBy: [nodeId], sourceField: key });
|
|
57
|
+
findings.push({ rule: "secret-field", nodeId, field: key, action: `templated:${vaultKey}` });
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
// 2) 비밀처럼 생긴 값이 엉뚱한 칸에 있으면 — 자동 치환하지 않고 막는다.
|
|
61
|
+
// 이름 없는 칸의 비밀은 무엇을 채워야 하는지 우리가 알 수 없다.
|
|
62
|
+
if (looksSecretValue(value)) {
|
|
63
|
+
blockers.push({
|
|
64
|
+
nodeId,
|
|
65
|
+
field: key,
|
|
66
|
+
reason: `"${key}" 값이 자격증명처럼 보입니다. 어떤 키인지 알 수 없어 자동으로 빈칸 처리할 수 없습니다.`,
|
|
67
|
+
nextAction: `이 값을 금고 변수로 바꾼 뒤(예: $\{vault.MY_TOKEN}) 다시 내보내세요.`,
|
|
68
|
+
});
|
|
69
|
+
out[key] = value;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
// 3) 모델 고정 — 받는 사람 기계엔 그 모델이 없다. 등급 힌트로 바꾼다.
|
|
73
|
+
if (key === "model" && typeof value === "string" && value) {
|
|
74
|
+
out.tierHint = "standard";
|
|
75
|
+
findings.push({ rule: "model-pin", nodeId, field: key, action: "replaced:runner-primary" });
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
// 4) 개인 경로 — 계정명이 그대로 드러난다.
|
|
79
|
+
if (typeof value === "string" && PERSONAL_PATH_RE.test(value)) {
|
|
80
|
+
out[key] = value.replace(PERSONAL_PATH_RE, "<사용자 폴더>");
|
|
81
|
+
findings.push({ rule: "personal-path", nodeId, field: key, action: "removed" });
|
|
82
|
+
PERSONAL_PATH_RE.lastIndex = 0;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
out[key] = value;
|
|
86
|
+
}
|
|
87
|
+
return { config: out, findings, vaultTemplate, blockers };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function digestOf(value) {
|
|
91
|
+
return `sha256:${crypto.createHash("sha256").update(JSON.stringify(value)).digest("hex")}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 그래프 하나를 .agentgraph 패키지로 만든다.
|
|
96
|
+
* 막을 사유가 있으면 { blocked: true, blockers } 를 돌려주고 패키지를 만들지 않는다.
|
|
97
|
+
*/
|
|
98
|
+
function buildPackage(input) {
|
|
99
|
+
const { automation, graph } = input;
|
|
100
|
+
const findings = [];
|
|
101
|
+
const vaultTemplate = [];
|
|
102
|
+
const blockers = [];
|
|
103
|
+
const nodes = [];
|
|
104
|
+
const dependencies = { agents: [], mcp: [], subGraphs: [] };
|
|
105
|
+
|
|
106
|
+
for (const node of graph.nodes || []) {
|
|
107
|
+
const scrubbed = scrubNodeConfig(node.id, node.config);
|
|
108
|
+
findings.push(...scrubbed.findings);
|
|
109
|
+
vaultTemplate.push(...scrubbed.vaultTemplate);
|
|
110
|
+
blockers.push(...scrubbed.blockers);
|
|
111
|
+
nodes.push({ ...node, config: scrubbed.config });
|
|
112
|
+
|
|
113
|
+
// 에이전트 참조는 핀으로 남긴다 — 받는 사람이 무엇을 빌려야 하는지 알아야 한다.
|
|
114
|
+
// 노드가 ref를 선언하지 않으면 자동화의 대상 에이전트를 상속한다(제품의 실제 동작).
|
|
115
|
+
// 그 경우를 빼면 패키지가 "채울 것 없음"이라고 거짓말한다.
|
|
116
|
+
const isAgentish = node.type === "agent" || node.type === "action" || node.type === "output";
|
|
117
|
+
const ref = typeof node.config?.ref === "string" && node.config.ref ? node.config.ref : null;
|
|
118
|
+
const inheritedSlug = automation.target_id || null;
|
|
119
|
+
const slug = ref || (isAgentish && node.type === "agent" ? inheritedSlug : null);
|
|
120
|
+
if (isAgentish && slug) {
|
|
121
|
+
const source = (ref ? node.config?.targetType : automation.target_type) === "hub" ? "hub" : "local";
|
|
122
|
+
if (!dependencies.agents.some((dep) => dep.slug === slug)) {
|
|
123
|
+
dependencies.agents.push({
|
|
124
|
+
nodeId: node.id,
|
|
125
|
+
slug,
|
|
126
|
+
source,
|
|
127
|
+
...(ref ? {} : { inheritedFromAutomation: true }),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const server = node.config?.mcpServer;
|
|
132
|
+
if (typeof server === "string" && server && !dependencies.mcp.some((m) => m.serverSlug === server)) {
|
|
133
|
+
dependencies.mcp.push({ serverSlug: server, requiredBy: [node.id] });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (blockers.length > 0) {
|
|
138
|
+
return { blocked: true, blockers, findings };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const mutationNodes = nodes
|
|
142
|
+
.filter((n) => n.config?.effect === "mutation")
|
|
143
|
+
.map((n) => ({ nodeId: n.id, label: n.label || n.id }));
|
|
144
|
+
|
|
145
|
+
const scrubbedGraph = { version: graph.version ?? 1, nodes, edges: graph.edges || [] };
|
|
146
|
+
if (graph.budget) scrubbedGraph.budget = graph.budget;
|
|
147
|
+
|
|
148
|
+
const manifest = {
|
|
149
|
+
schemaVersion: SCHEMA_VERSION,
|
|
150
|
+
slug: String(automation.name || "graph").trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, ""),
|
|
151
|
+
name: automation.name,
|
|
152
|
+
version: input.version || "1.0.0",
|
|
153
|
+
exportedAt: new Date().toISOString(),
|
|
154
|
+
trigger: {
|
|
155
|
+
kind: automation.trigger_type && automation.trigger_type !== "schedule" ? "input" : "cron",
|
|
156
|
+
schedule: automation.schedule ?? null,
|
|
157
|
+
},
|
|
158
|
+
dependencies,
|
|
159
|
+
vaultTemplate,
|
|
160
|
+
modelPolicy: { binding: "runner-primary" },
|
|
161
|
+
// 받는 사람이 설치 전에 알아야 하는 것 — 무엇이 바깥으로 나가는가.
|
|
162
|
+
permissionsSummary: {
|
|
163
|
+
mutationNodes,
|
|
164
|
+
leasedAgents: dependencies.agents.filter((d) => d.source === "hub").map((d) => d.slug),
|
|
165
|
+
},
|
|
166
|
+
scrubReport: { rulesVersion: "scrub/1.0", scrubbedAt: new Date().toISOString(), findings },
|
|
167
|
+
};
|
|
168
|
+
manifest.integrity = { graphDigest: digestOf(scrubbedGraph), manifestDigest: null };
|
|
169
|
+
manifest.integrity.manifestDigest = digestOf({ ...manifest, integrity: { graphDigest: manifest.integrity.graphDigest, manifestDigest: null } });
|
|
170
|
+
|
|
171
|
+
return { blocked: false, blockers: [], findings, package: { manifest, graph: scrubbedGraph } };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* 패키지를 받았을 때 실행 전에 채워야 하는 것들.
|
|
176
|
+
* "설치했으니 이제 돌아간다"가 아니라 "무엇이 비어 있는가"를 먼저 말한다.
|
|
177
|
+
*/
|
|
178
|
+
function bindingChecklist(pkg) {
|
|
179
|
+
const manifest = pkg?.manifest || {};
|
|
180
|
+
const items = [];
|
|
181
|
+
for (const entry of manifest.vaultTemplate || []) {
|
|
182
|
+
items.push({
|
|
183
|
+
kind: "vault-key",
|
|
184
|
+
key: entry.key,
|
|
185
|
+
requiredBy: entry.requiredBy || [],
|
|
186
|
+
done: false,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
for (const dep of manifest.dependencies?.agents || []) {
|
|
190
|
+
items.push({ kind: "agent", slug: dep.slug, source: dep.source, nodeId: dep.nodeId, done: false });
|
|
191
|
+
}
|
|
192
|
+
for (const dep of manifest.dependencies?.mcp || []) {
|
|
193
|
+
items.push({ kind: "mcp-server", serverSlug: dep.serverSlug, done: false });
|
|
194
|
+
}
|
|
195
|
+
return items;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function verifyPackage(pkg) {
|
|
199
|
+
const problems = [];
|
|
200
|
+
if (!pkg || typeof pkg !== "object") return ["패키지 형식이 아닙니다."];
|
|
201
|
+
const { manifest, graph } = pkg;
|
|
202
|
+
if (!manifest || manifest.schemaVersion !== SCHEMA_VERSION) {
|
|
203
|
+
problems.push(`이 버전이 읽을 수 없는 패키지 형식입니다(${manifest?.schemaVersion ?? "형식 없음"}).`);
|
|
204
|
+
}
|
|
205
|
+
if (!graph || !Array.isArray(graph.nodes) || graph.nodes.length === 0) {
|
|
206
|
+
problems.push("그래프에 단계가 없습니다.");
|
|
207
|
+
}
|
|
208
|
+
if (manifest?.integrity?.graphDigest && graph) {
|
|
209
|
+
if (digestOf(graph) !== manifest.integrity.graphDigest) {
|
|
210
|
+
problems.push("그래프 내용이 매니페스트 지문과 다릅니다(전송 중 변형되었을 수 있습니다).");
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return problems;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
module.exports = {
|
|
217
|
+
SCHEMA_VERSION,
|
|
218
|
+
buildPackage,
|
|
219
|
+
bindingChecklist,
|
|
220
|
+
verifyPackage,
|
|
221
|
+
scrubNodeConfig,
|
|
222
|
+
digestOf,
|
|
223
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// ⚠️ 생성된 파일입니다. 손으로 고치지 마세요.
|
|
2
|
+
// 정본: agentlas_desktop/shared/graph-registry/*.json
|
|
3
|
+
// 생성: (agentlas_desktop) node scripts/gen-graph-registry.cjs
|
|
4
|
+
//
|
|
5
|
+
// 터미널은 데스크탑과 같은 DB를 읽지만 스키마 판이 뒤따라온다. 모르는 값을 만나는 것은
|
|
6
|
+
// 고장이 아니라 정상이며, 그때 **그 항목만** 강등하고 나머지는 정상 처리한다.
|
|
7
|
+
"use strict";
|
|
8
|
+
|
|
9
|
+
const GRAPH_WIRE = "graph/1";
|
|
10
|
+
const GRAPH_ERROR_CODES = ["APPROVAL_REJECTED","APPROVAL_REQUIRED","APPROVAL_TIMED_OUT","ARCHITECT_NO_CHANGE","ARCHITECT_NO_REQUEST","ARCHITECT_OUTPUT_MALFORMED","ARCHITECT_OUTPUT_TOO_LARGE","ARCHITECT_OUTPUT_UNREADABLE","ARCHITECT_UNAVAILABLE","AUTOMATION_NOT_CONNECTED","BUDGET_EXHAUSTED","CODE_DEPENDENCY_MISSING","CODE_NODE_EMPTY","CODE_PRODUCED_NOTHING","CODE_STEP_FAILED","CREATE_INPUT_INVALID","EDGE_CONDITION_UNRESOLVED","EVAL_INCOMPLETE","EVAL_STUCK","EVAL_UNAVAILABLE","INTERVIEW_MODEL_UNAVAILABLE","INTERVIEW_OUTPUT_UNREADABLE","INTERVIEW_REPEATED_QUESTIONS","INTERVIEW_SELF_CORRECTION_EXHAUSTED","INTERVIEW_STATE_INVALID","LOOP_BOUND_INVALID","LOOP_BOUND_UNDECLARED","LOOP_LIMIT_REACHED","LOOP_WITHOUT_EXIT","MUTATION_UNVERIFIED","NODE_FAILED","NODE_INPUT_MISSING","NODE_NEVER_REACHED","NODE_NO_RESULT","NODE_TIMEOUT","NODE_TYPE_UNSUPPORTED","NO_MATCHING_EDGE","OUTPUT_NODE_EMPTY","PATCH_CODE_EMPTY","PATCH_EDGE_CONFLICT","PATCH_EDGE_DANGLING","PATCH_EDGE_HANDLE_MISSING","PATCH_EDGE_MISSING","PATCH_EMPTY","PATCH_LOOP_BOUND_MISSING","PATCH_NODE_CONFLICT","PATCH_NODE_MISSING","PATCH_NO_GRAPH","PATCH_OP_UNKNOWN","REDUCER_MERGE_CONFLICT","REDUCER_WRITE_CONFLICT","RESUME_CONFLICT","RUN_REQUEST_DISABLED","RUN_REQUEST_INPUT_REQUIRED","RUN_REQUEST_NOT_FOUND","RUN_REQUEST_QUEUE_UNAVAILABLE","RUN_REQUEST_REF_AMBIGUOUS","RUN_REQUEST_REF_MISSING","SUBGRAPH_DEPTH_EXCEEDED","SUBGRAPH_FAILED","SUBGRAPH_NOT_FOUND","SUBGRAPH_NO_RESULT","SUBGRAPH_SELF_CALL","SWAP_CAPABILITY_MISMATCH","SWAP_HUB_RELEASE_UNPINNED","SWAP_NODE_NOT_FOUND","SWAP_NOT_AGENT_NODE","SWAP_NO_MATCH","SWAP_UNKNOWN_PROVIDER","TOOL_BROKER_CALL_UNREADABLE","TOOL_BROKER_MUTATION_IN_SIMULATION","TOOL_BROKER_PLAN_UNREADABLE","TOOL_BROKER_TOOL_NOT_DECLARED","TOOL_NODE_UNATTACHED","TOOL_NODE_UNCONFIGURED","TRANSFORM_MODE_UNKNOWN","TRANSFORM_NODE_UNCONFIGURED"];
|
|
11
|
+
const GRAPH_JOURNAL_KINDS = ["blob_externalized","node_failed","node_intent","node_reserved","node_retry","node_routed","node_settled","resumed","run_completed","run_created","run_failed","run_validated","suspended"];
|
|
12
|
+
const GRAPH_NODE_KINDS = ["action","agent","code","condition","eval","output","subgraph","tool","transform","trigger"];
|
|
13
|
+
const GRAPH_BLOCK_UI = {"trigger":{"section":"none","placeable":false,"placeReason":"그래프마다 하나뿐이고 처음 만들 때 함께 지어진다"},"agent":{"section":"inventory","placeable":true},"eval":{"section":"flow","placeable":true},"condition":{"section":"flow","placeable":true},"transform":{"section":"flow","placeable":true},"code":{"section":"flow","placeable":true},"tool":{"section":"inventory","placeable":true},"action":{"section":"actions","placeable":true},"output":{"section":"flow","placeable":true},"loop":{"section":"none","placeable":false,"placeReason":"노드가 아니라 되돌아가는 연결의 성질이다 — 엣지를 이어서 만든다"},"subgraph":{"section":"flow","placeable":true}};
|
|
14
|
+
|
|
15
|
+
/** 모르는 값은 원문을 보존한 채 항목 단위로 강등한다. 집합 폐기 금지. */
|
|
16
|
+
function readEnum(value, allowed) {
|
|
17
|
+
const text = typeof value === "string" ? value : String(value == null ? "" : value);
|
|
18
|
+
return allowed.includes(text) ? { known: text } : { unknown: text };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function degradedLabel(value, lang) {
|
|
22
|
+
if (value && typeof value === "object" && "known" in value) return value.known;
|
|
23
|
+
const raw = value && value.unknown ? value.unknown : "";
|
|
24
|
+
return lang === "en" ? `unknown (raw: ${raw})` : `알 수 없음 (원문: ${raw})`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = {
|
|
28
|
+
GRAPH_WIRE, GRAPH_ERROR_CODES, GRAPH_JOURNAL_KINDS, GRAPH_NODE_KINDS, GRAPH_BLOCK_UI,
|
|
29
|
+
readEnum, degradedLabel,
|
|
30
|
+
};
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* hephaestus/local-core — 로컬 Agentlas Core(`hephaestus mcp serve`)의 stdio MCP 클라이언트.
|
|
4
|
+
*
|
|
5
|
+
* 배경(2026-08-05, 감사 결함 E): 터미널의 편성 루프(agentlas-workforce.cjs)는 원격
|
|
6
|
+
* agentlas.cloud MCP만 쳤다. 그 서버는 공개 Hub 메뉴만 주므로 로컬·오너 Cloud를
|
|
7
|
+
* 포함한 연합(sourceScope network/local/cloud)은 물리적으로 불가능했고, hep-*
|
|
8
|
+
* 명령들은 외부 CLI 스텁(exit 3 host_llm_required)에 배선돼 있었다. 연합을
|
|
9
|
+
* 소유한 것은 로컬 Core다 — 실측(2026-08-05): `hephaestus mcp serve`가
|
|
10
|
+
* workforce.search_candidates/validate_selection/prepare_execution을 전부 노출하고,
|
|
11
|
+
* sourceScope:"local" 검색이 CandidateSet v1(reference-first)을 반환했다.
|
|
12
|
+
*
|
|
13
|
+
* 계약:
|
|
14
|
+
* - 프로세스 수명 = 클라이언트 수명. 명령이 끝나면 반드시 close(). 데몬화 금지.
|
|
15
|
+
* - 폴백 금지: Core 바이너리가 없으면 code="local_core_unavailable"로 정직하게
|
|
16
|
+
* 던진다 — 원격 Hub로 조용히 내려가면 스코프가 거짓이 된다.
|
|
17
|
+
* - 응답은 MCP content[0].text의 JSON. Core가 {status:"rejected", error:…}를 주면
|
|
18
|
+
* 그 코드를 그대로 던진다(거절 원문 보존 — 재작성 금지).
|
|
19
|
+
*/
|
|
20
|
+
const fs = require("node:fs");
|
|
21
|
+
const os = require("node:os");
|
|
22
|
+
const path = require("node:path");
|
|
23
|
+
const { spawn } = require("node:child_process");
|
|
24
|
+
|
|
25
|
+
function localCoreBin() {
|
|
26
|
+
const candidates = [
|
|
27
|
+
process.env.HEPHAESTUS_BIN,
|
|
28
|
+
path.join(os.homedir(), ".agentlas", "runtime", "current", "bin", "hephaestus"),
|
|
29
|
+
];
|
|
30
|
+
if (process.platform === "win32") return null;
|
|
31
|
+
for (const candidate of candidates) {
|
|
32
|
+
try {
|
|
33
|
+
if (candidate && fs.existsSync(candidate)) return candidate;
|
|
34
|
+
} catch { /* keep looking */ }
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function coreError(code, message) {
|
|
40
|
+
const error = new Error(message);
|
|
41
|
+
error.code = code;
|
|
42
|
+
return error;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* createLocalCoreClient({cwd, timeoutMs}) → { call(name,args), close() }
|
|
47
|
+
* call은 initialize를 게으르게 1회 수행한 뒤 tools/call을 보낸다.
|
|
48
|
+
*/
|
|
49
|
+
function createLocalCoreClient({ cwd, timeoutMs = 60_000 } = {}) {
|
|
50
|
+
const bin = localCoreBin();
|
|
51
|
+
if (!bin) {
|
|
52
|
+
throw coreError(
|
|
53
|
+
"local_core_unavailable",
|
|
54
|
+
"Agentlas-OS local core (hephaestus) is not installed — federated staffing needs it. Install Agentlas-OS or set HEPHAESTUS_BIN.",
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
const child = spawn(bin, ["mcp", "serve"], {
|
|
58
|
+
cwd: cwd || process.cwd(),
|
|
59
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
60
|
+
});
|
|
61
|
+
// 프로세스 수명 = 클라이언트 수명 계약의 백스톱. close() 를 못 부르고 부모가
|
|
62
|
+
// 죽는 경로(오류 → process.exit)에서 Core 가 고아로 남는다 — 실측: 프로브
|
|
63
|
+
// 세션에서 `hephaestus mcp serve` 17개 누적. exit 훅으로 반드시 걷는다.
|
|
64
|
+
const reap = () => { try { child.kill(); } catch { /* already gone */ } };
|
|
65
|
+
process.once("exit", reap);
|
|
66
|
+
let buffer = "";
|
|
67
|
+
let nextId = 0;
|
|
68
|
+
let initialized = null;
|
|
69
|
+
const pending = new Map();
|
|
70
|
+
let exited = false;
|
|
71
|
+
|
|
72
|
+
child.on("exit", () => {
|
|
73
|
+
exited = true;
|
|
74
|
+
for (const [, entry] of pending) {
|
|
75
|
+
entry.reject(coreError("local_core_exited", "the local core process exited before responding"));
|
|
76
|
+
}
|
|
77
|
+
pending.clear();
|
|
78
|
+
});
|
|
79
|
+
child.stdout.on("data", (chunk) => {
|
|
80
|
+
buffer += chunk;
|
|
81
|
+
let index;
|
|
82
|
+
while ((index = buffer.indexOf("\n")) >= 0) {
|
|
83
|
+
const line = buffer.slice(0, index);
|
|
84
|
+
buffer = buffer.slice(index + 1);
|
|
85
|
+
if (!line.trim()) continue;
|
|
86
|
+
let message;
|
|
87
|
+
try { message = JSON.parse(line); } catch { continue; }
|
|
88
|
+
const entry = message && message.id != null ? pending.get(message.id) : null;
|
|
89
|
+
if (entry) {
|
|
90
|
+
pending.delete(message.id);
|
|
91
|
+
entry.resolve(message);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const rpc = (method, params) => new Promise((resolve, reject) => {
|
|
97
|
+
if (exited) { reject(coreError("local_core_exited", "the local core process already exited")); return; }
|
|
98
|
+
const id = ++nextId;
|
|
99
|
+
const timer = setTimeout(() => {
|
|
100
|
+
pending.delete(id);
|
|
101
|
+
reject(coreError("local_core_timeout", `${method} did not respond within ${timeoutMs}ms`));
|
|
102
|
+
}, timeoutMs);
|
|
103
|
+
pending.set(id, {
|
|
104
|
+
resolve: (message) => { clearTimeout(timer); resolve(message); },
|
|
105
|
+
reject: (error) => { clearTimeout(timer); reject(error); },
|
|
106
|
+
});
|
|
107
|
+
child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, method, params })}\n`);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
async function ensureInitialized() {
|
|
111
|
+
if (!initialized) {
|
|
112
|
+
initialized = rpc("initialize", {
|
|
113
|
+
protocolVersion: "2024-11-05",
|
|
114
|
+
capabilities: {},
|
|
115
|
+
clientInfo: { name: "agentlas-terminal", version: "2" },
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return initialized;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function call(name, args) {
|
|
122
|
+
await ensureInitialized();
|
|
123
|
+
const response = await rpc("tools/call", { name, arguments: args });
|
|
124
|
+
if (response.error) {
|
|
125
|
+
throw coreError("local_core_rpc_error", `${name}: ${response.error.message || JSON.stringify(response.error)}`);
|
|
126
|
+
}
|
|
127
|
+
const text = response?.result?.content?.[0]?.text;
|
|
128
|
+
if (typeof text !== "string") {
|
|
129
|
+
throw coreError("local_core_invalid_response", `${name} returned no text content`);
|
|
130
|
+
}
|
|
131
|
+
let parsed;
|
|
132
|
+
try { parsed = JSON.parse(text); } catch {
|
|
133
|
+
throw coreError("local_core_invalid_response", `${name} returned non-JSON content`);
|
|
134
|
+
}
|
|
135
|
+
// Core의 거절/오류는 원문 코드로 전파한다 — 사람 문장으로 바꾸면 기계 표식이
|
|
136
|
+
// 죽는다. (실측: 경계 거절은 status:"rejected", 계약 오류는 status:"error".)
|
|
137
|
+
if (parsed && (parsed.status === "rejected" || parsed.status === "error")) {
|
|
138
|
+
const code = typeof parsed.error === "string" ? parsed.error : `local_core_${parsed.status}`;
|
|
139
|
+
// 경계 거절의 issues(무엇이 어느 path에서 걸렸나)는 유일한 진단 근거다 —
|
|
140
|
+
// 코드만 전파하면 다음 사람이 다시 프로브부터 시작한다(2026-08-05 실측 2회).
|
|
141
|
+
const issues = Array.isArray(parsed?.boundary?.issues)
|
|
142
|
+
? parsed.boundary.issues.map((issue) => `${issue.code}@${issue.path}`).join(", ")
|
|
143
|
+
: "";
|
|
144
|
+
const error = coreError(code, `${name} ${parsed.status}: ${code}${issues ? ` [${issues}]` : ""}`);
|
|
145
|
+
error.detail = parsed;
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
return parsed;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function close() {
|
|
152
|
+
process.removeListener("exit", reap);
|
|
153
|
+
try { child.kill(); } catch { /* already gone */ }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return { call, close };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
module.exports = { createLocalCoreClient, localCoreBin };
|
|
@@ -36,20 +36,16 @@ const { CONTEXT_MAP_MIN_CORE_VERSION } = coreHarness;
|
|
|
36
36
|
// ── 명령 usage 문자열 (v1 TOP_LEVEL_COMMAND_USAGE에서 hephaestus 클러스터만 발췌) ──
|
|
37
37
|
const USAGE = Object.freeze({
|
|
38
38
|
build: 'usage: agentlas build "<request>"',
|
|
39
|
-
browser: "usage: agentlas browser <url-or-query|
|
|
39
|
+
browser: "usage: agentlas browser <url-or-query> | status | sites | add <site> | login <site> | mark <site> <valid|expired|none> | go <url> | rm <site>",
|
|
40
40
|
call: 'usage: agentlas call "<agent-slugs>" "<context>"',
|
|
41
41
|
connect: "usage: agentlas connect [status|telegram|help]",
|
|
42
42
|
hep: "usage: agentlas hep <subcommand> [args]",
|
|
43
|
-
// 소스
|
|
44
|
-
//
|
|
45
|
-
// usage
|
|
46
|
-
|
|
47
|
-
"hep-local": 'usage: agentlas hep-local "<request>" # registered Local agents only',
|
|
48
|
-
"hep-cloud": 'usage: agentlas hep-cloud "<request>" # owner Agent Cloud agents only',
|
|
49
|
-
"hep-hub": 'usage: agentlas hep-hub "<request>" # public Agentlas Hub agents only',
|
|
43
|
+
// 소스 스코프 스태핑(hep-network/hep-local/hep-cloud/hep-hub)과 legacy-network 의
|
|
44
|
+
// usage 는 2026-08-05 에 삭제했다. 네이티브가 편성을 수행하지 않고 exit 3 +
|
|
45
|
+
// host_llm_required 만 반환하므로, 여기에 usage 를 두면 "쓸 수 있는 명령"으로
|
|
46
|
+
// 읽힌다. 차단과 안내는 commands/index.cjs 의 HOST_LLM_ONLY_SURFACES 가 한다.
|
|
50
47
|
hephaestus: "usage: agentlas hephaestus <subcommand> [args]",
|
|
51
48
|
journal: "usage: agentlas journal <status|verify|repair|gate> --run-id <id> | --journal <path>",
|
|
52
|
-
"legacy-network": 'usage: agentlas legacy-network "<request>"',
|
|
53
49
|
netadmin: "usage: agentlas netadmin <init|status|reindex|bench|add-source> [args]",
|
|
54
50
|
research: "usage: agentlas research <status|gather|search|read|plan> [args]",
|
|
55
51
|
route: 'usage: agentlas route "<request>" [--json]',
|
|
@@ -403,7 +399,15 @@ function create(ctx, deps = {}) {
|
|
|
403
399
|
if (raw) return runHephaestusInteractive(cleanArgs, { ...opts, human: false });
|
|
404
400
|
if (!hephaestusBin()) return runHephaestusInteractive(cleanArgs, { ...opts, human: false });
|
|
405
401
|
const ui = opts.ui || newUi();
|
|
406
|
-
|
|
402
|
+
// route는 Hub 왕복이라 실측 ~13s 걸린다 — research처럼 진행 표시를 준다.
|
|
403
|
+
// (clig.dev: 몇 초 넘는 작업엔 진행 인디케이터. 예전엔 그 시간 내내 침묵했다.)
|
|
404
|
+
if (typeof ui.startSpinner === "function") ui.startSpinner(ui.lang === "ko" ? "라우팅 미리보기 계산 중…" : "Previewing routing…");
|
|
405
|
+
let result;
|
|
406
|
+
try {
|
|
407
|
+
result = await captureHephaestus(cleanArgs, opts);
|
|
408
|
+
} finally {
|
|
409
|
+
if (typeof ui.stopSpinner === "function") ui.stopSpinner();
|
|
410
|
+
}
|
|
407
411
|
let json = null;
|
|
408
412
|
try {
|
|
409
413
|
const start = result.stdout.indexOf("{");
|
|
@@ -31,10 +31,10 @@ function parseAgentPool(raw) {
|
|
|
31
31
|
try {
|
|
32
32
|
parsed = JSON.parse(raw || "[]");
|
|
33
33
|
} catch {
|
|
34
|
-
throw new Error("This project's agent team cannot be read. Open the project in Agentlas Desktop and save its team again.");
|
|
34
|
+
throw Object.assign(new Error("This project's agent team cannot be read. Open the project in Agentlas Desktop and save its team again."), { code: "project_team_unreadable", honestStop: true });
|
|
35
35
|
}
|
|
36
36
|
if (!Array.isArray(parsed)) {
|
|
37
|
-
throw new Error("This project's agent team cannot be read. Open the project in Agentlas Desktop and save its team again.");
|
|
37
|
+
throw Object.assign(new Error("This project's agent team cannot be read. Open the project in Agentlas Desktop and save its team again."), { code: "project_team_unreadable", honestStop: true });
|
|
38
38
|
}
|
|
39
39
|
return parsed.filter((member) => member && typeof member === "object"
|
|
40
40
|
&& typeof member.agentId === "string" && member.agentId.trim()
|
|
@@ -45,7 +45,7 @@ function parseAgentPool(raw) {
|
|
|
45
45
|
function resolveProjectForCwd(db, cwd) {
|
|
46
46
|
const columns = projectColumns(db);
|
|
47
47
|
if (!columns.has("folder_path") || !columns.has("agent_pool_json")) {
|
|
48
|
-
throw new Error("This Agentlas data store does not support project teams yet. Open the latest Agentlas Desktop once, then retry.");
|
|
48
|
+
throw Object.assign(new Error("This Agentlas data store does not support project teams yet. Open the latest Agentlas Desktop once, then retry."), { code: "project_teams_unsupported", honestStop: true });
|
|
49
49
|
}
|
|
50
50
|
const rows = db.prepare(
|
|
51
51
|
`SELECT id, name, system_prompt, agent_pool_json, source_type, source_ref, folder_path
|
|
@@ -57,12 +57,12 @@ function resolveProjectForCwd(db, cwd) {
|
|
|
57
57
|
.filter(({ root }) => pathContains(root, target))
|
|
58
58
|
.sort((a, b) => b.root.length - a.root.length);
|
|
59
59
|
if (!matches.length) {
|
|
60
|
-
throw new Error("This folder is not connected to an Agentlas project. Connect it in Desktop Work, or pass an exact agent for an advanced direct invocation.");
|
|
60
|
+
throw Object.assign(new Error("This folder is not connected to an Agentlas project. Connect it in Desktop Work, or pass an exact agent for an advanced direct invocation."), { code: "project_not_connected", honestStop: true });
|
|
61
61
|
}
|
|
62
62
|
const bestLength = matches[0].root.length;
|
|
63
63
|
const best = matches.filter((entry) => entry.root.length === bestLength);
|
|
64
64
|
if (best.length !== 1) {
|
|
65
|
-
throw new Error("More than one Agentlas project is connected to this folder. Keep one source connection, then retry.");
|
|
65
|
+
throw Object.assign(new Error("More than one Agentlas project is connected to this folder. Keep one source connection, then retry."), { code: "project_ambiguous", honestStop: true });
|
|
66
66
|
}
|
|
67
67
|
return { ...best[0].row, rootPath: best[0].root, agentPool: parseAgentPool(best[0].row.agent_pool_json) };
|
|
68
68
|
}
|
|
@@ -70,15 +70,15 @@ function resolveProjectForCwd(db, cwd) {
|
|
|
70
70
|
function resolveProjectController(db, cwd) {
|
|
71
71
|
const project = resolveProjectForCwd(db, cwd);
|
|
72
72
|
if (!project.agentPool.length) {
|
|
73
|
-
throw new Error("This project has no agent team. Drag agents into the project in Desktop Work, then retry.");
|
|
73
|
+
throw Object.assign(new Error("This project has no agent team. Drag agents into the project in Desktop Work, then retry."), { code: "project_team_empty", honestStop: true });
|
|
74
74
|
}
|
|
75
75
|
const controllerRef = project.agentPool[0];
|
|
76
76
|
if (controllerRef.source !== "local") {
|
|
77
|
-
throw new Error(`The project controller “${controllerRef.nameSnapshot}” is not installed locally for Terminal execution. Install that exact release locally or reorder the project team.`);
|
|
77
|
+
throw Object.assign(new Error(`The project controller “${controllerRef.nameSnapshot}” is not installed locally for Terminal execution. Install that exact release locally or reorder the project team.`), { code: "controller_not_installed", honestStop: true });
|
|
78
78
|
}
|
|
79
79
|
const controller = listRoutableAgents(db).find((agent) => agent.id === controllerRef.agentId) || null;
|
|
80
80
|
if (!controller) {
|
|
81
|
-
throw new Error(`The project controller “${controllerRef.nameSnapshot}” is unavailable. Restore that agent or explicitly choose a new first agent in Desktop Work.`);
|
|
81
|
+
throw Object.assign(new Error(`The project controller “${controllerRef.nameSnapshot}” is unavailable. Restore that agent or explicitly choose a new first agent in Desktop Work.`), { code: "controller_unavailable", honestStop: true });
|
|
82
82
|
}
|
|
83
83
|
return { project, controller };
|
|
84
84
|
}
|