agentlas 1.0.65 → 1.0.67
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 +94 -0
- package/README.md +12 -4
- package/bin/agentlas.cjs +354 -16
- package/engine/acp/server.cjs +34 -3
- package/engine/agentlas-api-agent.cjs +151 -120
- package/engine/agentlas-banner.cjs +6 -2
- package/engine/agentlas-capabilities.cjs +17 -10
- package/engine/agentlas-cloud-runtime.cjs +4 -5
- package/engine/agentlas-config.cjs +19 -54
- package/engine/agentlas-core-harness.cjs +59 -5
- package/engine/agentlas-desktop-loadout.cjs +17 -3
- package/engine/agentlas-evolution.cjs +75 -18
- package/engine/agentlas-experience-exchange.cjs +145 -14
- package/engine/agentlas-experience-intake.cjs +2 -2
- package/engine/agentlas-i18n.cjs +0 -2
- package/engine/agentlas-input.cjs +273 -20
- package/engine/agentlas-judgment.cjs +0 -0
- package/engine/agentlas-mcp-env.cjs +84 -23
- package/engine/agentlas-mcp-wrapper.cjs +36 -4
- package/engine/agentlas-memory-governance.cjs +2 -1
- package/engine/agentlas-memory-import.cjs +49 -12
- package/engine/agentlas-native-host.cjs +279 -44
- package/engine/agentlas-onboard.cjs +23 -1
- package/engine/agentlas-permissions.cjs +5 -1
- package/engine/agentlas-sqlite-policy.cjs +40 -2
- package/engine/agentlas-tasks.cjs +7 -0
- package/engine/agentlas-tools.cjs +37 -10
- package/engine/agentlas-ui.cjs +26 -4
- package/engine/agentlas-workforce.cjs +30 -5
- package/engine/agentlas-workload-routing.cjs +83 -10
- package/engine/agentlas.cjs +21 -4
- package/engine/agents/builder.cjs +35 -10
- package/engine/agents/files.cjs +81 -10
- package/engine/agents/import-local.cjs +10 -2
- package/engine/agents/registry.cjs +10 -3
- package/engine/agents/router.cjs +7 -7
- package/engine/agents/routes.cjs +31 -9
- package/engine/automation/daemon.cjs +154 -40
- package/engine/automation/launchd.cjs +395 -16
- package/engine/automation/schedule.cjs +90 -3
- package/engine/automation/store.cjs +163 -59
- package/engine/browser/cdp.cjs +190 -17
- package/engine/browser/vault.cjs +1 -0
- package/engine/cli-output.cjs +96 -30
- package/engine/cloud/auth.cjs +131 -13
- package/engine/cloud/hub-client.cjs +41 -16
- package/engine/cloud-assets/cas.cjs +18 -9
- package/engine/cloud-assets/commands.cjs +108 -30
- package/engine/cloud-assets/package.cjs +66 -3
- package/engine/cloud-assets/restore.cjs +12 -9
- package/engine/cloud-assets/state.cjs +609 -116
- package/engine/commands/acp.cjs +13 -2
- package/engine/commands/automation.cjs +75 -24
- package/engine/commands/billing.cjs +6 -1
- package/engine/commands/browser.cjs +61 -29
- package/engine/commands/build.cjs +72 -14
- package/engine/commands/call.cjs +1 -1
- package/engine/commands/cd.cjs +1 -1
- package/engine/commands/cloud.cjs +2 -1
- package/engine/commands/connect.cjs +38 -13
- package/engine/commands/creds.cjs +74 -29
- package/engine/commands/doctor.cjs +17 -1
- package/engine/commands/document.cjs +38 -10
- package/engine/commands/env.cjs +6 -1
- package/engine/commands/firm.cjs +42 -12
- package/engine/commands/graph.cjs +36 -32
- package/engine/commands/help.cjs +11 -1
- package/engine/commands/hep.cjs +1 -1
- package/engine/commands/import.cjs +5 -4
- package/engine/commands/index.cjs +1 -1
- package/engine/commands/install.cjs +6 -4
- package/engine/commands/list.cjs +56 -16
- package/engine/commands/login.cjs +5 -0
- package/engine/commands/logout.cjs +6 -1
- package/engine/commands/mcp.cjs +150 -37
- package/engine/commands/memory.cjs +3 -2
- package/engine/commands/multimodal.cjs +12 -0
- package/engine/commands/native.cjs +5 -1
- package/engine/commands/one.cjs +78 -33
- package/engine/commands/plugin.cjs +13 -2
- package/engine/commands/project.cjs +15 -12
- package/engine/commands/research.cjs +18 -7
- package/engine/commands/roles.cjs +104 -28
- package/engine/commands/route.cjs +2 -2
- package/engine/commands/run.cjs +42 -9
- package/engine/commands/search.cjs +23 -2
- package/engine/commands/setup.cjs +6 -1
- package/engine/commands/storm.cjs +44 -6
- package/engine/commands/swarm.cjs +47 -9
- package/engine/commands/telegram.cjs +6 -1
- package/engine/commands/uninstall.cjs +12 -6
- package/engine/commands/update.cjs +5 -0
- package/engine/commands/upload.cjs +2 -1
- package/engine/commands/usage.cjs +6 -1
- package/engine/commands/version.cjs +6 -1
- package/engine/commands/whoami.cjs +6 -1
- package/engine/commands/workforce.cjs +60 -16
- package/engine/core/capability-grants.cjs +7 -2
- package/engine/core/daemon-client.cjs +36 -7
- package/engine/core/db.cjs +10 -2
- package/engine/core/desktop-core-fetch.cjs +477 -77
- package/engine/core/schema-ensure.cjs +23 -11
- package/engine/experience/build.cjs +5 -1
- package/engine/experience/intents.cjs +35 -82
- package/engine/experience/runtime.cjs +7 -1
- package/engine/experience/variant.cjs +58 -4
- package/engine/firms/orchestrate.cjs +9 -6
- package/engine/graph/ask-model.cjs +22 -11
- package/engine/graph/interview.cjs +74 -23
- package/engine/graph/package.cjs +63 -8
- package/engine/hephaestus/local-core.cjs +18 -5
- package/engine/hephaestus/runtime.cjs +48 -43
- package/engine/hub/install.cjs +484 -63
- package/engine/hub/plugins.cjs +391 -29
- package/engine/mcp/consent.cjs +25 -17
- package/engine/mcp/contract.cjs +229 -24
- package/engine/mcp/inventory.cjs +46 -10
- package/engine/mcp/plan.cjs +10 -1
- package/engine/mcp/probe.cjs +24 -9
- package/engine/memory-cli/curate.cjs +74 -16
- package/engine/oberon/common.cjs +37 -12
- package/engine/oberon/manifest.cjs +25 -2
- package/engine/oberon/outputs.cjs +49 -19
- package/engine/oberon/render.cjs +665 -40
- package/engine/project/career-graph.cjs +15 -2
- package/engine/project/controller.cjs +133 -15
- package/engine/project/credentials.cjs +382 -34
- package/engine/project/env-file.cjs +93 -8
- package/engine/project/memory-context.cjs +11 -57
- package/engine/project/ontology.cjs +66 -25
- package/engine/project/paths.cjs +42 -8
- package/engine/project/seed.cjs +88 -51
- package/engine/project/state.cjs +41 -16
- package/engine/project/team.cjs +26 -1
- package/engine/runtime-refusal.cjs +4 -1
- package/engine/runtimes/acp-driver.cjs +56 -9
- package/engine/runtimes/auth-evidence.cjs +21 -2
- package/engine/runtimes/detect.cjs +28 -8
- package/engine/runtimes/kinds.cjs +12 -4
- package/engine/runtimes/overrides.cjs +4 -3
- package/engine/runtimes/resolve.cjs +8 -7
- package/engine/runtimes/roles.cjs +7 -5
- package/engine/sessions/apply-fences.cjs +396 -56
- package/engine/sessions/fences.cjs +111 -27
- package/engine/sessions/memory-turn.cjs +62 -3
- package/engine/sessions/orchestrator.cjs +11 -1
- package/engine/sessions/prompt.cjs +66 -9
- package/engine/sessions/session.cjs +70 -10
- package/engine/sessions/sink.cjs +78 -12
- package/engine/sessions/store.cjs +29 -7
- package/engine/storm/storm.cjs +26 -3
- package/engine/storm/swarm.cjs +31 -3
- package/engine/telegram/connect.cjs +185 -22
- package/engine/ui/commands-catalog.cjs +1 -1
- package/engine/ui/palette.cjs +12 -2
- package/engine/ui/repl.cjs +57 -9
- package/engine/ui/screens.cjs +2 -4
- package/engine/vendor/desktop-core.manifest.json +5 -5
- package/engine/workforce/capture.cjs +9 -27
- package/engine/workforce/deps.cjs +18 -6
- package/engine/workforce/local-core-transport.cjs +68 -5
- package/package.json +1 -1
- package/engine/commands/career-graph.cjs +0 -51
|
@@ -122,9 +122,16 @@ function findAgent(db, token) {
|
|
|
122
122
|
const q = String(token || "").trim().toLowerCase();
|
|
123
123
|
if (!q) return null;
|
|
124
124
|
const agents = listRoutableAgents(db);
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
125
|
+
const exactId = agents.find((agent) => agent.id === token);
|
|
126
|
+
if (exactId) return exactId; // primary key is authoritative
|
|
127
|
+
const exactSlug = agents.filter((agent) => agent.slug.toLowerCase() === q);
|
|
128
|
+
if (exactSlug.length === 1) return exactSlug[0];
|
|
129
|
+
if (exactSlug.length > 1) return null;
|
|
130
|
+
const exactName = agents.filter((agent) =>
|
|
131
|
+
(agent.name || "").toLowerCase() === q || (agent.nameEn || "").toLowerCase() === q,
|
|
132
|
+
);
|
|
133
|
+
if (exactName.length === 1) return exactName[0];
|
|
134
|
+
if (exactName.length > 1) return null;
|
|
128
135
|
const partial = agents.filter((a) =>
|
|
129
136
|
a.slug.toLowerCase().includes(q) || (a.name || "").toLowerCase().includes(q) || (a.nameEn || "").toLowerCase().includes(q));
|
|
130
137
|
if (partial.length === 1) return partial[0];
|
package/engine/agents/router.cjs
CHANGED
|
@@ -72,11 +72,11 @@ function ensureJudgeRunner(db, runtime) {
|
|
|
72
72
|
const fetchImpl = typeof baseFetch === "function" && signal
|
|
73
73
|
? (url, init) => baseFetch(url, { ...(init || {}), signal })
|
|
74
74
|
: baseFetch;
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
75
|
+
// CLI 판정과 같은 정직한 실패 계약을 쓴다. 여기서 API 오류를 빈 문자열로
|
|
76
|
+
// 바꾸면 judgment 서비스가 인증·한도·연결 사유를 보존할 기회를 잃고
|
|
77
|
+
// "유효한 판단 없음"으로 오진한다. judgeLabels가 예외 문구를 bounded reason으로
|
|
78
|
+
// 정규화하므로 원래 오류를 그대로 올린다.
|
|
79
|
+
return capture.runApi(backend, resolved.model || null, system, prompt, { fetch: fetchImpl });
|
|
80
80
|
});
|
|
81
81
|
return judgment;
|
|
82
82
|
}
|
|
@@ -158,9 +158,9 @@ async function resolveAutoRoute(db, task, opts = {}) {
|
|
|
158
158
|
|
|
159
159
|
if (verdict.source !== "llm" || !verdict.labels.length) {
|
|
160
160
|
return finish(unresolvedChoice(lang,
|
|
161
|
-
lang === "ko"
|
|
161
|
+
verdict.reason || (lang === "ko"
|
|
162
162
|
? "연결된 모델이 유효한 판단을 반환하지 않았습니다"
|
|
163
|
-
: "The connected model did not return a valid judgment",
|
|
163
|
+
: "The connected model did not return a valid judgment"),
|
|
164
164
|
"model_unavailable"));
|
|
165
165
|
}
|
|
166
166
|
const picked = verdict.labels[0];
|
package/engine/agents/routes.cjs
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
const fs = require("node:fs");
|
|
7
7
|
const path = require("node:path");
|
|
8
|
+
const { randomUUID } = require("node:crypto");
|
|
8
9
|
const { userDataDir } = require("../core/paths.cjs");
|
|
9
10
|
|
|
10
11
|
function routesPath() {
|
|
@@ -14,26 +15,47 @@ function routesPath() {
|
|
|
14
15
|
function routesMap() {
|
|
15
16
|
try {
|
|
16
17
|
const parsed = JSON.parse(fs.readFileSync(routesPath(), "utf8"));
|
|
17
|
-
|
|
18
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return Object.create(null);
|
|
19
|
+
return Object.assign(Object.create(null), parsed);
|
|
18
20
|
} catch {
|
|
19
|
-
return
|
|
21
|
+
return Object.create(null);
|
|
20
22
|
}
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
/** 원자적 사설 쓰기 (0600) — 데스크탑과 파일을 공유하므로 부분 쓰기가 보이면 안 된다. */
|
|
24
26
|
function saveRoutes(routes) {
|
|
25
27
|
const file = routesPath();
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
28
|
+
const parent = path.dirname(file);
|
|
29
|
+
fs.mkdirSync(parent, { recursive: true });
|
|
30
|
+
const tmp = path.join(parent, `.${path.basename(file)}.${randomUUID()}.tmp`);
|
|
31
|
+
let fd = null;
|
|
29
32
|
try {
|
|
30
|
-
|
|
33
|
+
fd = fs.openSync(
|
|
34
|
+
tmp,
|
|
35
|
+
fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_NOFOLLOW,
|
|
36
|
+
0o600,
|
|
37
|
+
);
|
|
38
|
+
fs.writeFileSync(fd, JSON.stringify(routes, null, 2) + "\n", "utf8");
|
|
31
39
|
fs.fsyncSync(fd);
|
|
32
|
-
} finally {
|
|
33
40
|
fs.closeSync(fd);
|
|
41
|
+
fd = null;
|
|
42
|
+
fs.renameSync(tmp, file);
|
|
43
|
+
fsyncDirectoryBestEffort(parent);
|
|
44
|
+
} finally {
|
|
45
|
+
if (fd !== null) fs.closeSync(fd);
|
|
46
|
+
try { fs.unlinkSync(tmp); } catch { /* rename consumed it, or cleanup is best-effort */ }
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function fsyncDirectoryBestEffort(directory) {
|
|
51
|
+
try {
|
|
52
|
+
const fd = fs.openSync(directory, fs.constants.O_RDONLY);
|
|
53
|
+
try { fs.fsyncSync(fd); }
|
|
54
|
+
finally { fs.closeSync(fd); }
|
|
55
|
+
} catch {
|
|
56
|
+
// Some filesystems do not support directory fsync. The same-directory
|
|
57
|
+
// rename still keeps readers on either the complete old or complete new map.
|
|
34
58
|
}
|
|
35
|
-
fs.renameSync(tmp, file);
|
|
36
|
-
try { fs.chmodSync(file, 0o600); } catch { /* win32 */ }
|
|
37
59
|
}
|
|
38
60
|
|
|
39
61
|
function routeForAgent(agentId) {
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
* 리스를 못 잡으면 실행하지 않는다(skip, 정직 보고).
|
|
12
12
|
*/
|
|
13
13
|
const crypto = require("node:crypto");
|
|
14
|
+
const fs = require("node:fs");
|
|
15
|
+
const path = require("node:path");
|
|
14
16
|
const { findAgent, rowToAgent } = require("../agents/registry.cjs");
|
|
15
17
|
const { resolveRuntime } = require("../runtimes/resolve.cjs");
|
|
16
18
|
const { Orchestrator } = require("../sessions/orchestrator.cjs");
|
|
@@ -24,7 +26,11 @@ const store = require("./store.cjs");
|
|
|
24
26
|
// getAutomationExecutionContractState 동형) ──────────────────────────────
|
|
25
27
|
// 손상된/미래 계약 값은 절대 조용히 넓혀 실행하지 않는다 — raw-row 게이트로
|
|
26
28
|
// 무인 실행 직전에 검사한다(데스크탑 automation-scheduler.ts:538-549).
|
|
27
|
-
const {
|
|
29
|
+
const {
|
|
30
|
+
CONTRACT_RUNTIME_KINDS,
|
|
31
|
+
CONTRACT_RUNTIME_BACKENDS,
|
|
32
|
+
canonicalRuntimeKind,
|
|
33
|
+
} = require("../runtimes/kinds.cjs");
|
|
28
34
|
const RUNTIME_KINDS = new Set(CONTRACT_RUNTIME_KINDS);
|
|
29
35
|
const RUNTIME_BACKENDS = new Set(CONTRACT_RUNTIME_BACKENDS);
|
|
30
36
|
const RUNTIME_SELECTION_KEYS = new Set(["kind", "backend", "source", "model", "longContext", "effort"]);
|
|
@@ -33,22 +39,86 @@ function decodeRuntimeSelection(raw) {
|
|
|
33
39
|
if (raw == null) return { state: "missing" };
|
|
34
40
|
try {
|
|
35
41
|
const value = JSON.parse(raw);
|
|
42
|
+
// Desktop의 저장 디코더와 같은 레거시 보정. 예전 kind=gemini 행은 현재
|
|
43
|
+
// Antigravity 선택으로 간주하되, 당시 검증되지 않은 source/model은 버린다.
|
|
44
|
+
const normalized = value && value.kind === "gemini"
|
|
45
|
+
? { ...value, kind: "antigravity", source: undefined, model: undefined }
|
|
46
|
+
: value;
|
|
36
47
|
if (
|
|
37
|
-
|
|
38
|
-
Object.keys(
|
|
39
|
-
typeof
|
|
40
|
-
(
|
|
41
|
-
(
|
|
42
|
-
(
|
|
43
|
-
(
|
|
44
|
-
(
|
|
48
|
+
normalized && typeof normalized === "object" && !Array.isArray(normalized) &&
|
|
49
|
+
Object.keys(normalized).every((key) => RUNTIME_SELECTION_KEYS.has(key)) &&
|
|
50
|
+
typeof normalized.kind === "string" && RUNTIME_KINDS.has(normalized.kind) &&
|
|
51
|
+
(normalized.backend === undefined || (typeof normalized.backend === "string" && RUNTIME_BACKENDS.has(normalized.backend))) &&
|
|
52
|
+
(normalized.source === undefined || (typeof normalized.source === "string" && normalized.source.length > 0 && normalized.source.length <= 2048)) &&
|
|
53
|
+
(normalized.model === undefined || (typeof normalized.model === "string" && normalized.model.length > 0 && normalized.model.length <= 512)) &&
|
|
54
|
+
(normalized.longContext === undefined || typeof normalized.longContext === "boolean") &&
|
|
55
|
+
(normalized.effort === undefined || (typeof normalized.effort === "string" && normalized.effort.length <= 128))
|
|
45
56
|
) {
|
|
46
|
-
return {
|
|
57
|
+
return {
|
|
58
|
+
state: "valid",
|
|
59
|
+
value: {
|
|
60
|
+
...normalized,
|
|
61
|
+
storedKind: normalized.kind,
|
|
62
|
+
kind: canonicalRuntimeKind(normalized.kind),
|
|
63
|
+
},
|
|
64
|
+
};
|
|
47
65
|
}
|
|
48
66
|
} catch { /* 손상 데이터는 아래에서 invalid — 진짜 없는 레거시 핀과 구분한다 */ }
|
|
49
67
|
return { state: "invalid" };
|
|
50
68
|
}
|
|
51
69
|
|
|
70
|
+
const PIN_RUNTIME_IDENTITIES = Object.freeze({
|
|
71
|
+
"claude-code": { backend: "anthropic", source: "claude" },
|
|
72
|
+
codex: { backend: "openai", source: "codex" },
|
|
73
|
+
agy: { backend: "google", source: "agy" },
|
|
74
|
+
kimi: { backend: "kimi", source: "kimi" },
|
|
75
|
+
grok: { backend: "custom", source: "grok" },
|
|
76
|
+
cursor: { backend: "cursor", source: "cursor-agent" },
|
|
77
|
+
ollama: { backend: "ollama", source: "ollama" },
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
function sameExecutableSource(source, resolvedBin, expectedName) {
|
|
81
|
+
if (!source) return true;
|
|
82
|
+
const requested = String(source);
|
|
83
|
+
if (!resolvedBin) return requested === expectedName;
|
|
84
|
+
const requestedBase = path.basename(requested);
|
|
85
|
+
const resolvedBase = path.basename(String(resolvedBin));
|
|
86
|
+
if (requestedBase !== resolvedBase || requestedBase !== expectedName) return false;
|
|
87
|
+
if (!path.isAbsolute(requested)) return true;
|
|
88
|
+
try {
|
|
89
|
+
return fs.realpathSync(requested) === fs.realpathSync(String(resolvedBin));
|
|
90
|
+
} catch {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function materializePinnedRuntime(pin, resolver = resolveRuntime, context = {}) {
|
|
96
|
+
const label = pin.storedKind || pin.kind;
|
|
97
|
+
const unavailable = () => new Error(
|
|
98
|
+
`Pinned automation runtime is unavailable: ${label}${pin.model ? ` · ${pin.model}` : ""}`,
|
|
99
|
+
);
|
|
100
|
+
let runtime;
|
|
101
|
+
try {
|
|
102
|
+
runtime = resolver({ db: context.db, prefs: context.prefs, explicit: pin.kind });
|
|
103
|
+
} catch {
|
|
104
|
+
throw unavailable();
|
|
105
|
+
}
|
|
106
|
+
const identity = PIN_RUNTIME_IDENTITIES[pin.kind];
|
|
107
|
+
if (pin.backend && (!identity || pin.backend !== identity.backend)) throw unavailable();
|
|
108
|
+
if (pin.source && (!identity || !sameExecutableSource(pin.source, runtime.bin, identity.source))) {
|
|
109
|
+
throw unavailable();
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
...runtime,
|
|
113
|
+
...(identity ? { backend: identity.backend, runtimeSource: pin.source || runtime.bin || identity.source } : {}),
|
|
114
|
+
...(pin.model ? { model: pin.model } : {}),
|
|
115
|
+
...(pin.effort ? { effort: pin.effort } : {}),
|
|
116
|
+
...(pin.longContext !== undefined ? { longContext: pin.longContext } : {}),
|
|
117
|
+
source: "automation-pin",
|
|
118
|
+
pinAuthoritative: true,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
52
122
|
function automationContractState(db, row) {
|
|
53
123
|
const runtimeSelection = columnExists(db, "automations", "runtime_selection_json")
|
|
54
124
|
? decodeRuntimeSelection(row.runtime_selection_json)
|
|
@@ -143,7 +213,23 @@ function runnerSkip(db, row, ko) {
|
|
|
143
213
|
}
|
|
144
214
|
|
|
145
215
|
/**
|
|
146
|
-
*
|
|
216
|
+
* Automation permission is data, not a default capability. New stores use
|
|
217
|
+
* execution_permission; older stores use permission. If neither column is
|
|
218
|
+
* present, or either value is corrupt/empty, agentlas-permissions deliberately
|
|
219
|
+
* returns read so a damaged row cannot become a write-capable background run.
|
|
220
|
+
*/
|
|
221
|
+
function automationPermission(db, row) {
|
|
222
|
+
const raw = columnExists(db, "automations", "execution_permission")
|
|
223
|
+
? row.execution_permission
|
|
224
|
+
: columnExists(db, "automations", "permission")
|
|
225
|
+
? row.permission
|
|
226
|
+
: undefined;
|
|
227
|
+
return permissions.normalize(raw);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* 자동화 1건 실행(헤드리스). 권한은 execution_permission/permission 열에서만
|
|
232
|
+
* 읽으며, 누락·손상 값은 agentlas-permissions를 거쳐 read로 fail-closed 한다.
|
|
147
233
|
* opts:
|
|
148
234
|
* advanceSchedule 데몬 경로 true / run-now false (v1과 동일)
|
|
149
235
|
* scheduledFor 기록용 예정 시각(ISO)
|
|
@@ -175,7 +261,8 @@ async function runAutomationOnce(ctx, db, row, opts = {}) {
|
|
|
175
261
|
return { ok: false, skipped: true, reason: "lease-unsupported" };
|
|
176
262
|
}
|
|
177
263
|
// run-now도 리스를 잡는다 — 앱 스케줄러가 같은 행을 동시에 돌리는 것을 방지.
|
|
178
|
-
|
|
264
|
+
const lease = store.claimAutomationLease(db, row.id);
|
|
265
|
+
if (!lease) {
|
|
179
266
|
ctx.err(ko
|
|
180
267
|
? `다른 실행기가 이 자동화 리스를 보유 중입니다(15분 TTL): ${row.name}`
|
|
181
268
|
: `Another runner holds this automation (lease TTL 15 minutes): ${row.name}`);
|
|
@@ -190,11 +277,13 @@ async function runAutomationOnce(ctx, db, row, opts = {}) {
|
|
|
190
277
|
let leaseRenewWarningEmitted = false;
|
|
191
278
|
const leaseHeartbeat = setInterval(() => {
|
|
192
279
|
try {
|
|
193
|
-
const renewed = store.
|
|
280
|
+
const renewed = store.renewAutomationLeaseToken(db, row.id, new Date(), lease);
|
|
194
281
|
if (!renewed) {
|
|
195
282
|
leaseOwnershipLost = true;
|
|
196
283
|
if (activeSession && typeof activeSession.kill === "function") activeSession.kill();
|
|
197
284
|
} else {
|
|
285
|
+
lease.owner = renewed.owner;
|
|
286
|
+
lease.claimedAt = renewed.claimedAt;
|
|
198
287
|
leaseRenewWarningEmitted = false;
|
|
199
288
|
}
|
|
200
289
|
} catch (error) {
|
|
@@ -211,6 +300,31 @@ async function runAutomationOnce(ctx, db, row, opts = {}) {
|
|
|
211
300
|
}, Math.max(10, Number(opts.leaseHeartbeatMs) || 60_000));
|
|
212
301
|
if (typeof leaseHeartbeat.unref === "function") leaseHeartbeat.unref();
|
|
213
302
|
|
|
303
|
+
const leaseLostResult = () => ({
|
|
304
|
+
ok: false,
|
|
305
|
+
skipped: true,
|
|
306
|
+
reason: "lease-lost",
|
|
307
|
+
error: "Automation execution lease ownership lost",
|
|
308
|
+
});
|
|
309
|
+
const completeRun = (status, error, ok) => {
|
|
310
|
+
const recorded = store.recordRun(db, row.id, status, error, opts.scheduledFor, lease);
|
|
311
|
+
if (recorded && recorded.leaseLost) return { leaseLost: true };
|
|
312
|
+
const advanced = store.advanceAfterRun(db, row, {
|
|
313
|
+
ok,
|
|
314
|
+
advanceSchedule: !!opts.advanceSchedule,
|
|
315
|
+
lease,
|
|
316
|
+
});
|
|
317
|
+
if (advanced && advanced.leaseLost) return { leaseLost: true };
|
|
318
|
+
if (!advanced || !advanced.updated) {
|
|
319
|
+
return {
|
|
320
|
+
bookkeepingError: advanced && advanced.error
|
|
321
|
+
? advanced.error
|
|
322
|
+
: "automation completion state was not updated",
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
return { recorded, advanced };
|
|
326
|
+
};
|
|
327
|
+
|
|
214
328
|
try {
|
|
215
329
|
// ── raw-row 실행 계약 게이트 (데스크탑 automation-scheduler.ts:538-549 동형) ──
|
|
216
330
|
// 손상된 계약 값으로는 무인 실행하지 않는다 — 문구까지 데스크탑과 동일.
|
|
@@ -218,15 +332,17 @@ async function runAutomationOnce(ctx, db, row, opts = {}) {
|
|
|
218
332
|
if (contract.runtimeSelection === "invalid") {
|
|
219
333
|
const msg = "pinned_runtime_contract_invalid: the saved runtime pin is malformed and requires an explicit runtime selection.";
|
|
220
334
|
ctx.err(msg);
|
|
221
|
-
|
|
222
|
-
|
|
335
|
+
const completed = completeRun("needs_input", msg, false);
|
|
336
|
+
if (completed.leaseLost) return leaseLostResult();
|
|
337
|
+
if (completed.bookkeepingError) ctx.err(completed.bookkeepingError);
|
|
223
338
|
return { ok: false, error: msg };
|
|
224
339
|
}
|
|
225
340
|
if (contract.hubMode === "invalid") {
|
|
226
341
|
const msg = "automation_hub_mode_contract_invalid: the saved Hub routing policy is unknown and requires an explicit selection.";
|
|
227
342
|
ctx.err(msg);
|
|
228
|
-
|
|
229
|
-
|
|
343
|
+
const completed = completeRun("needs_input", msg, false);
|
|
344
|
+
if (completed.leaseLost) return leaseLostResult();
|
|
345
|
+
if (completed.bookkeepingError) ctx.err(completed.bookkeepingError);
|
|
230
346
|
return { ok: false, error: msg };
|
|
231
347
|
}
|
|
232
348
|
|
|
@@ -237,17 +353,10 @@ async function runAutomationOnce(ctx, db, row, opts = {}) {
|
|
|
237
353
|
let runtime = opts.runtime || null;
|
|
238
354
|
if (!runtime && !opts.runtimeOverride && contract.runtimePin) {
|
|
239
355
|
const pin = contract.runtimePin;
|
|
240
|
-
|
|
241
|
-
runtime = resolveRuntime({ db, prefs: ctx.prefs, explicit: pin.kind });
|
|
242
|
-
if (pin.model) runtime.model = pin.model;
|
|
243
|
-
} catch {
|
|
244
|
-
throw new Error(`Pinned automation runtime is unavailable: ${pin.kind}${pin.model ? ` · ${pin.model}` : ""}`);
|
|
245
|
-
}
|
|
356
|
+
runtime = materializePinnedRuntime(pin, opts.resolveRuntime || resolveRuntime, { db, prefs: ctx.prefs });
|
|
246
357
|
}
|
|
247
358
|
if (!runtime) runtime = resolveRuntime({ db, prefs: ctx.prefs, explicit: opts.runtimeOverride || null });
|
|
248
|
-
|
|
249
|
-
const rowPermission = columnExists(db, "automations", "permission") ? row.permission : null;
|
|
250
|
-
const permission = permissions.normalize(rowPermission || "write", "write");
|
|
359
|
+
const permission = automationPermission(db, row);
|
|
251
360
|
|
|
252
361
|
ctx.err(ctx.ui.dim(`${agent.slug} · ${runtime.kind} · ${permission} · ${ko ? "자동화" : "automation"} ${String(row.id).slice(0, 8)}`));
|
|
253
362
|
|
|
@@ -281,14 +390,17 @@ async function runAutomationOnce(ctx, db, row, opts = {}) {
|
|
|
281
390
|
const failed = session.status === "failed";
|
|
282
391
|
const errMsg = failed ? String(session.lastError || "runtime turn failed").slice(0, 500) : null;
|
|
283
392
|
|
|
284
|
-
if (failed)
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
393
|
+
if (failed) ctx.err(errMsg);
|
|
394
|
+
const completed = completeRun(failed ? "error" : "ok", failed ? errMsg : null, !failed);
|
|
395
|
+
if (completed.leaseLost) {
|
|
396
|
+
ctx.err("Automation execution lease ownership lost");
|
|
397
|
+
return leaseLostResult();
|
|
289
398
|
}
|
|
290
|
-
|
|
291
|
-
|
|
399
|
+
if (completed.bookkeepingError) {
|
|
400
|
+
ctx.err(completed.bookkeepingError);
|
|
401
|
+
return { ok: false, error: completed.bookkeepingError };
|
|
402
|
+
}
|
|
403
|
+
if (completed.advanced.maxRunsReached) {
|
|
292
404
|
ctx.err(ko ? "max_runs 도달 — 자동화를 비활성화했습니다." : "max_runs reached — automation disabled.");
|
|
293
405
|
}
|
|
294
406
|
return failed ? { ok: false, error: errMsg } : { ok: true, finalText };
|
|
@@ -298,12 +410,13 @@ async function runAutomationOnce(ctx, db, row, opts = {}) {
|
|
|
298
410
|
if (leaseOwnershipLost) {
|
|
299
411
|
return { ok: false, skipped: true, reason: "lease-lost", error: "Automation execution lease ownership lost" };
|
|
300
412
|
}
|
|
301
|
-
|
|
302
|
-
|
|
413
|
+
const completed = completeRun("error", msg, false);
|
|
414
|
+
if (completed.leaseLost) return leaseLostResult();
|
|
415
|
+
if (completed.bookkeepingError) ctx.err(completed.bookkeepingError);
|
|
303
416
|
return { ok: false, error: msg };
|
|
304
417
|
} finally {
|
|
305
418
|
clearInterval(leaseHeartbeat);
|
|
306
|
-
store.releaseAutomation(db, row.id,
|
|
419
|
+
store.releaseAutomation(db, row.id, lease.owner, lease.claimedAt);
|
|
307
420
|
}
|
|
308
421
|
}
|
|
309
422
|
|
|
@@ -360,10 +473,8 @@ async function daemonTick(ctx, db, opts = {}) {
|
|
|
360
473
|
) opts.skipAnnounced.add(row.id);
|
|
361
474
|
continue;
|
|
362
475
|
}
|
|
363
|
-
//
|
|
364
|
-
|
|
365
|
-
db.prepare("UPDATE automations SET enabled = 0 WHERE id = ? AND (schedule IS NULL OR schedule = '')").run(row.id);
|
|
366
|
-
}
|
|
476
|
+
// 1회성 종료는 advanceAfterRun 이 동일 lease fence 안에서 처리한다.
|
|
477
|
+
// 여기서 lease 해제 후 row id만 다시 쓰면 successor 실행을 끌 수 있다.
|
|
367
478
|
}
|
|
368
479
|
return ran;
|
|
369
480
|
}
|
|
@@ -406,5 +517,8 @@ module.exports = {
|
|
|
406
517
|
automationContractState,
|
|
407
518
|
automationSessionChatId,
|
|
408
519
|
runnerSkip,
|
|
520
|
+
automationPermission,
|
|
409
521
|
decodeRuntimeSelection,
|
|
522
|
+
materializePinnedRuntime,
|
|
523
|
+
sameExecutableSource,
|
|
410
524
|
};
|