agentlas 1.0.65 → 1.0.66
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 +78 -0
- package/README.md +6 -4
- package/bin/agentlas.cjs +341 -16
- package/engine/acp/server.cjs +27 -1
- 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 +29 -5
- package/engine/agentlas-workload-routing.cjs +83 -10
- package/engine/agentlas.cjs +10 -3
- 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 +80 -28
- 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 +308 -113
- 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 +42 -12
- 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 +8 -2
- 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 +29 -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 +230 -61
- 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 +15 -5
- package/engine/mcp/plan.cjs +5 -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 +212 -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
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
const crypto = require("node:crypto");
|
|
16
16
|
const os = require("node:os");
|
|
17
|
-
const { columnExists, tableExists } = require("../core/db.cjs");
|
|
17
|
+
const { columnExists, tableExists, runWriteTransaction } = require("../core/db.cjs");
|
|
18
18
|
const { nextAutomationRun } = require("./schedule.cjs");
|
|
19
19
|
|
|
20
20
|
const LEASE_TTL_MS = 15 * 60 * 1000; // 앱 store/automations.ts LEASE_TTL_MS와 동일
|
|
@@ -31,7 +31,49 @@ function listAutomations(db) {
|
|
|
31
31
|
|
|
32
32
|
function getAutomationByPrefix(db, idPrefix) {
|
|
33
33
|
if (!tableExists(db, "automations")) return null;
|
|
34
|
-
|
|
34
|
+
const prefix = String(idPrefix || "").trim();
|
|
35
|
+
if (!prefix) return null;
|
|
36
|
+
const exact = db.prepare("SELECT * FROM automations WHERE id = ?").get(prefix);
|
|
37
|
+
if (exact) return exact;
|
|
38
|
+
// LIKE를 쓰면 사용자가 입력한 %/_가 와일드카드가 되어 전혀 다른 행을 고른다.
|
|
39
|
+
// 두 행 이상이면 임의의 첫 행을 mutation 대상으로 삼지 않고 전체 ID를 요구한다.
|
|
40
|
+
const matches = db.prepare(
|
|
41
|
+
"SELECT * FROM automations WHERE substr(id, 1, length(?)) = ? ORDER BY id LIMIT 2",
|
|
42
|
+
).all(prefix, prefix);
|
|
43
|
+
if (matches.length > 1) throw new Error(`automation id prefix is ambiguous: ${prefix}`);
|
|
44
|
+
return matches[0] || null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function resultChanges(result) {
|
|
48
|
+
return Number(result && (result.changes ?? result.rowsAffected) || 0);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function validLease(lease) {
|
|
52
|
+
return Boolean(
|
|
53
|
+
lease && typeof lease.owner === "string" && lease.owner &&
|
|
54
|
+
typeof lease.claimedAt === "string" && lease.claimedAt,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Build the fencing predicate for a post-claim write. When a legacy caller has
|
|
60
|
+
* no lease token, only an actually-unclaimed row may be updated; an active
|
|
61
|
+
* successor lease is never touched. The daemon always supplies the exact
|
|
62
|
+
* owner+claimed_at pair returned by claimAutomationLease().
|
|
63
|
+
*/
|
|
64
|
+
function leaseFence(db, id, lease = null, row = null) {
|
|
65
|
+
const parts = ["id = ?"];
|
|
66
|
+
const params = [id];
|
|
67
|
+
if (!leaseSupported(db)) return { sql: parts.join(" AND "), params };
|
|
68
|
+
if (validLease(lease)) {
|
|
69
|
+
parts.push("lease_owner = ?", "claimed_at = ?");
|
|
70
|
+
params.push(lease.owner, lease.claimedAt);
|
|
71
|
+
} else if (row && row.lease_owner == null && row.claimed_at == null) {
|
|
72
|
+
parts.push("lease_owner IS NULL", "claimed_at IS NULL");
|
|
73
|
+
} else {
|
|
74
|
+
parts.push("0");
|
|
75
|
+
}
|
|
76
|
+
return { sql: parts.join(" AND "), params };
|
|
35
77
|
}
|
|
36
78
|
|
|
37
79
|
/**
|
|
@@ -75,17 +117,21 @@ function addAutomation(db, spec, next) {
|
|
|
75
117
|
|
|
76
118
|
/** on: next_run_at 재계산 후 활성화. off: 비활성화(스케줄 보존). */
|
|
77
119
|
function setEnabled(db, row, enabled) {
|
|
120
|
+
return setEnabledChecked(db, row, enabled).next;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function setEnabledChecked(db, row, enabled) {
|
|
78
124
|
if (enabled) {
|
|
79
125
|
const next = nextAutomationRun(row) || null;
|
|
80
|
-
db.prepare("UPDATE automations SET enabled=1, next_run_at=? WHERE id=?").run(next ? next.toISOString() : null, row.id);
|
|
81
|
-
return next;
|
|
126
|
+
const result = db.prepare("UPDATE automations SET enabled=1, next_run_at=? WHERE id=?").run(next ? next.toISOString() : null, row.id);
|
|
127
|
+
return { changed: resultChanges(result) > 0, next };
|
|
82
128
|
}
|
|
83
|
-
db.prepare("UPDATE automations SET enabled=0 WHERE id=?").run(row.id);
|
|
84
|
-
return null;
|
|
129
|
+
const result = db.prepare("UPDATE automations SET enabled=0 WHERE id=?").run(row.id);
|
|
130
|
+
return { changed: resultChanges(result) > 0, next: null };
|
|
85
131
|
}
|
|
86
132
|
|
|
87
133
|
function removeAutomation(db, id) {
|
|
88
|
-
db.prepare("DELETE FROM automations WHERE id=?").run(id);
|
|
134
|
+
return resultChanges(db.prepare("DELETE FROM automations WHERE id=?").run(id)) > 0;
|
|
89
135
|
}
|
|
90
136
|
|
|
91
137
|
/**
|
|
@@ -94,15 +140,21 @@ function removeAutomation(db, id) {
|
|
|
94
140
|
* @returns {boolean} true = 이 프로세스가 배타 실행권을 가짐
|
|
95
141
|
*/
|
|
96
142
|
function claimAutomation(db, id, now = new Date(), owner = LEASE_OWNER) {
|
|
143
|
+
return Boolean(claimAutomationLease(db, id, now, owner));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Claim and return the exact owner+claimed_at fence used by this run. */
|
|
147
|
+
function claimAutomationLease(db, id, now = new Date(), owner = LEASE_OWNER) {
|
|
97
148
|
// 리스 열이 없으면 배타성을 증명할 수 없다 — 중복 실행 위험을 감수하지 않고 거부.
|
|
98
|
-
if (!leaseSupported(db)) return
|
|
149
|
+
if (!leaseSupported(db)) return null;
|
|
99
150
|
const cutoff = new Date(now.getTime() - LEASE_TTL_MS).toISOString();
|
|
151
|
+
const claimedAt = now.toISOString();
|
|
100
152
|
const result = db
|
|
101
153
|
.prepare(
|
|
102
154
|
"UPDATE automations SET claimed_at = ?, lease_owner = ? WHERE id = ? AND enabled = 1 AND (claimed_at IS NULL OR claimed_at < ?)",
|
|
103
155
|
)
|
|
104
|
-
.run(
|
|
105
|
-
return (result
|
|
156
|
+
.run(claimedAt, owner, id, cutoff);
|
|
157
|
+
return resultChanges(result) > 0 ? { owner, claimedAt } : null;
|
|
106
158
|
}
|
|
107
159
|
|
|
108
160
|
/**
|
|
@@ -115,6 +167,7 @@ function claimAutomation(db, id, now = new Date(), owner = LEASE_OWNER) {
|
|
|
115
167
|
* 리스를 되빼앗지는 않는다.
|
|
116
168
|
*/
|
|
117
169
|
function renewAutomationLease(db, id, now = new Date(), owner = LEASE_OWNER) {
|
|
170
|
+
if (arguments.length >= 5) return Boolean(renewAutomationLeaseToken(db, id, now, { owner, claimedAt: arguments[4] }));
|
|
118
171
|
if (!leaseSupported(db)) return false;
|
|
119
172
|
// false means definitive ownership loss. SQLite busy/I/O errors must throw
|
|
120
173
|
// so the caller can retry instead of killing a valid run as if a peer had
|
|
@@ -122,15 +175,30 @@ function renewAutomationLease(db, id, now = new Date(), owner = LEASE_OWNER) {
|
|
|
122
175
|
const result = db
|
|
123
176
|
.prepare("UPDATE automations SET claimed_at = ? WHERE id = ? AND lease_owner = ? AND claimed_at IS NOT NULL")
|
|
124
177
|
.run(now.toISOString(), id, owner);
|
|
125
|
-
return (result
|
|
178
|
+
return resultChanges(result) > 0;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Renew an exact fence and return its new claimed_at value. */
|
|
182
|
+
function renewAutomationLeaseToken(db, id, now = new Date(), lease) {
|
|
183
|
+
if (!leaseSupported(db) || !validLease(lease)) return null;
|
|
184
|
+
const claimedAt = now.toISOString();
|
|
185
|
+
const result = db
|
|
186
|
+
.prepare("UPDATE automations SET claimed_at = ? WHERE id = ? AND lease_owner = ? AND claimed_at = ?")
|
|
187
|
+
.run(claimedAt, id, lease.owner, lease.claimedAt);
|
|
188
|
+
return resultChanges(result) > 0 ? { owner: lease.owner, claimedAt } : null;
|
|
126
189
|
}
|
|
127
190
|
|
|
128
|
-
function releaseAutomation(db, id, owner = LEASE_OWNER) {
|
|
191
|
+
function releaseAutomation(db, id, owner = LEASE_OWNER, claimedAt = null) {
|
|
129
192
|
try {
|
|
193
|
+
if (owner && typeof owner === "object") {
|
|
194
|
+
claimedAt = owner.claimedAt || null;
|
|
195
|
+
owner = owner.owner || LEASE_OWNER;
|
|
196
|
+
}
|
|
197
|
+
const token = typeof claimedAt === "string" && claimedAt ? " AND claimed_at = ?" : "";
|
|
130
198
|
const result = db
|
|
131
|
-
.prepare(
|
|
132
|
-
.run(id, owner);
|
|
133
|
-
return (result
|
|
199
|
+
.prepare(`UPDATE automations SET claimed_at = NULL, lease_owner = NULL WHERE id = ? AND lease_owner = ?${token}`)
|
|
200
|
+
.run(...(token ? [id, owner, claimedAt] : [id, owner]));
|
|
201
|
+
return resultChanges(result) > 0;
|
|
134
202
|
} catch { return false; }
|
|
135
203
|
}
|
|
136
204
|
|
|
@@ -138,22 +206,33 @@ function releaseAutomation(db, id, owner = LEASE_OWNER) {
|
|
|
138
206
|
* 실행 기록 — run_history(v1/Desktop 스케줄러 패리티) + automation_runs(있으면) 양쪽.
|
|
139
207
|
* best-effort: 기록 실패가 자동화 실행 자체를 죽이면 안 된다.
|
|
140
208
|
*/
|
|
141
|
-
function recordRun(db, automationId, status, error, scheduledFor) {
|
|
209
|
+
function recordRun(db, automationId, status, error, scheduledFor, lease) {
|
|
210
|
+
if (!leaseSupported(db) || !validLease(lease)) return { ok: false, leaseLost: true, recorded: false };
|
|
142
211
|
const nowIso = new Date().toISOString();
|
|
143
212
|
try {
|
|
144
|
-
|
|
145
|
-
db
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
213
|
+
return runWriteTransaction(db, () => {
|
|
214
|
+
const fence = leaseFence(db, automationId, lease);
|
|
215
|
+
let recorded = false;
|
|
216
|
+
if (tableExists(db, "run_history")) {
|
|
217
|
+
const result = db.prepare(
|
|
218
|
+
`INSERT INTO run_history (id, automation_id, scheduled_for, ran_at, status, skipped_count, error)
|
|
219
|
+
SELECT ?,?,?,?,?,0,? FROM automations WHERE ${fence.sql}`,
|
|
220
|
+
).run(crypto.randomUUID(), automationId, scheduledFor || null, nowIso, status, error || null, ...fence.params);
|
|
221
|
+
recorded = recorded || resultChanges(result) > 0;
|
|
222
|
+
}
|
|
223
|
+
if (tableExists(db, "automation_runs")) {
|
|
224
|
+
const result = db.prepare(
|
|
225
|
+
`INSERT INTO automation_runs (id, automation_id, started_at, status, node_states_json)
|
|
226
|
+
SELECT ?,?,?,?,NULL FROM automations WHERE ${fence.sql}`,
|
|
227
|
+
).run(crypto.randomUUID(), automationId, nowIso, status, ...fence.params);
|
|
228
|
+
recorded = recorded || resultChanges(result) > 0;
|
|
229
|
+
}
|
|
230
|
+
const stillOwned = db.prepare(`SELECT 1 FROM automations WHERE ${fence.sql}`).get(...fence.params);
|
|
231
|
+
return { ok: Boolean(stillOwned), leaseLost: !stillOwned, recorded };
|
|
232
|
+
});
|
|
233
|
+
} catch (error) {
|
|
234
|
+
return { ok: false, leaseLost: false, recorded: false, error: String((error && error.message) || error) };
|
|
235
|
+
}
|
|
157
236
|
}
|
|
158
237
|
|
|
159
238
|
function listRuns(db, limit = 15) {
|
|
@@ -174,8 +253,9 @@ function listRuns(db, limit = 15) {
|
|
|
174
253
|
* ("Disabled: … morning" 을 보고 exit 0 인데도) 다음 예정 시각에 또 발화한다.
|
|
175
254
|
* 그래서 enabled 는 "켜기"로는 절대 쓰지 않고, 종료 조건일 때만 0 으로 내린다.
|
|
176
255
|
*/
|
|
177
|
-
function disableExhausted(db, id) {
|
|
178
|
-
|
|
256
|
+
function disableExhausted(db, id, lease = null, row = null) {
|
|
257
|
+
const fence = leaseFence(db, id, lease, row);
|
|
258
|
+
return resultChanges(db.prepare(`UPDATE automations SET enabled = 0 WHERE ${fence.sql}`).run(...fence.params)) > 0;
|
|
179
259
|
}
|
|
180
260
|
|
|
181
261
|
/**
|
|
@@ -188,40 +268,61 @@ function disableExhausted(db, id) {
|
|
|
188
268
|
* (같은 이유로 성공/실패 두 분기 모두 같은 규칙을 따라야 한다: 한쪽만 고치면
|
|
189
269
|
* 실패로 끝난 실행이 여전히 끈 자동화를 부활시킨다.)
|
|
190
270
|
*/
|
|
191
|
-
function advanceAfterRun(db, row, { ok, advanceSchedule, ranAt = new Date() } = {}) {
|
|
271
|
+
function advanceAfterRun(db, row, { ok, advanceSchedule, ranAt = new Date(), lease = null } = {}) {
|
|
192
272
|
const hasRunCount = columnExists(db, "automations", "run_count");
|
|
193
273
|
const shouldAdvance = !!advanceSchedule && (row.trigger_type || "schedule") === "schedule";
|
|
194
274
|
const advance = shouldAdvance ? nextAutomationRun(row, ranAt) : null;
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
275
|
+
try {
|
|
276
|
+
return runWriteTransaction(db, () => {
|
|
277
|
+
const fence = leaseFence(db, row.id, lease, row);
|
|
278
|
+
let result;
|
|
279
|
+
if (ok) {
|
|
280
|
+
if (shouldAdvance) {
|
|
281
|
+
const sql = hasRunCount
|
|
282
|
+
? "UPDATE automations SET last_run_at = ?, run_count = run_count + 1, next_run_at = ?"
|
|
283
|
+
: "UPDATE automations SET last_run_at = ?, next_run_at = ?";
|
|
284
|
+
result = db.prepare(`${sql} WHERE ${fence.sql}`).run(
|
|
285
|
+
ranAt.toISOString(), advance ? advance.toISOString() : null, ...fence.params,
|
|
286
|
+
);
|
|
287
|
+
} else if (hasRunCount) {
|
|
288
|
+
result = db.prepare(`UPDATE automations SET last_run_at = ?, run_count = run_count + 1 WHERE ${fence.sql}`)
|
|
289
|
+
.run(ranAt.toISOString(), ...fence.params);
|
|
290
|
+
} else {
|
|
291
|
+
result = db.prepare(`UPDATE automations SET last_run_at = ? WHERE ${fence.sql}`).run(ranAt.toISOString(), ...fence.params);
|
|
292
|
+
}
|
|
293
|
+
} else if (shouldAdvance) {
|
|
294
|
+
result = db.prepare(`UPDATE automations SET last_run_at = ?, next_run_at = ? WHERE ${fence.sql}`)
|
|
295
|
+
.run(ranAt.toISOString(), advance ? advance.toISOString() : null, ...fence.params);
|
|
201
296
|
} else {
|
|
202
|
-
db.prepare(
|
|
203
|
-
.run(ranAt.toISOString(), advance ? advance.toISOString() : null, row.id);
|
|
297
|
+
result = db.prepare(`UPDATE automations SET last_run_at = ? WHERE ${fence.sql}`).run(ranAt.toISOString(), ...fence.params);
|
|
204
298
|
}
|
|
205
|
-
if (
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
299
|
+
if (resultChanges(result) === 0) return { advance, maxRunsReached: false, updated: false, leaseLost: true };
|
|
300
|
+
|
|
301
|
+
let maxRunsReached = false;
|
|
302
|
+
if (shouldAdvance && !advance) {
|
|
303
|
+
if (!disableExhausted(db, row.id, lease, row)) {
|
|
304
|
+
return { advance, maxRunsReached: false, updated: false, leaseLost: true };
|
|
305
|
+
}
|
|
306
|
+
maxRunsReached = true;
|
|
307
|
+
}
|
|
308
|
+
// max_runs 도달 시 비활성화 (앱과 동일한 종료 조건).
|
|
309
|
+
if (ok && row.max_runs && (row.run_count || 0) + 1 >= row.max_runs) {
|
|
310
|
+
if (!disableExhausted(db, row.id, lease, row)) {
|
|
311
|
+
return { advance, maxRunsReached: false, updated: false, leaseLost: true };
|
|
312
|
+
}
|
|
313
|
+
maxRunsReached = true;
|
|
314
|
+
}
|
|
315
|
+
return { advance, maxRunsReached, updated: true, leaseLost: false };
|
|
316
|
+
});
|
|
317
|
+
} catch (error) {
|
|
318
|
+
return {
|
|
319
|
+
advance,
|
|
320
|
+
maxRunsReached: false,
|
|
321
|
+
updated: false,
|
|
322
|
+
leaseLost: false,
|
|
323
|
+
error: String((error && error.message) || error),
|
|
324
|
+
};
|
|
223
325
|
}
|
|
224
|
-
return { advance, maxRunsReached: false };
|
|
225
326
|
}
|
|
226
327
|
|
|
227
328
|
/**
|
|
@@ -279,9 +380,12 @@ module.exports = {
|
|
|
279
380
|
getAutomationByPrefix,
|
|
280
381
|
addAutomation,
|
|
281
382
|
setEnabled,
|
|
383
|
+
setEnabledChecked,
|
|
282
384
|
removeAutomation,
|
|
283
385
|
claimAutomation,
|
|
386
|
+
claimAutomationLease,
|
|
284
387
|
renewAutomationLease,
|
|
388
|
+
renewAutomationLeaseToken,
|
|
285
389
|
releaseAutomation,
|
|
286
390
|
recordRun,
|
|
287
391
|
listRuns,
|
package/engine/browser/cdp.cjs
CHANGED
|
@@ -17,24 +17,83 @@
|
|
|
17
17
|
* 안전: Agentlas 전용 프로필 Chrome 에만 붙는다(개인 크롬 아님). 되돌릴 수 없는
|
|
18
18
|
* 행동은 호출자가 판단한다 — 이 모듈은 조종 primitive 만 제공한다.
|
|
19
19
|
*/
|
|
20
|
+
const crypto = require("node:crypto");
|
|
21
|
+
const fs = require("node:fs");
|
|
20
22
|
const http = require("node:http");
|
|
23
|
+
const path = require("node:path");
|
|
21
24
|
|
|
22
25
|
const DEFAULT_PORT = Number(process.env.AGENTLAS_CDP_PORT || 9222);
|
|
23
26
|
const DEFAULT_WS_OPEN_TIMEOUT_MS = 5_000;
|
|
27
|
+
const CDP_HTTP_TIMEOUT_MS = 3_000;
|
|
28
|
+
const CDP_HTTP_MAX_BYTES = 8 * 1024 * 1024;
|
|
29
|
+
const CDP_PDF_MAX_BYTES = 64 * 1024 * 1024;
|
|
24
30
|
const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "::1", "[::1]"]);
|
|
25
31
|
|
|
26
|
-
function httpJson(port,
|
|
32
|
+
function httpJson(port, requestPath, { timeout = CDP_HTTP_TIMEOUT_MS, method = "GET" } = {}) {
|
|
33
|
+
const limit = Number.isFinite(Number(timeout)) && Number(timeout) > 0
|
|
34
|
+
? Number(timeout)
|
|
35
|
+
: CDP_HTTP_TIMEOUT_MS;
|
|
27
36
|
return new Promise((resolve, reject) => {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
37
|
+
let req = null;
|
|
38
|
+
let response = null;
|
|
39
|
+
let timer = null;
|
|
40
|
+
let settled = false;
|
|
41
|
+
const finish = (error, value) => {
|
|
42
|
+
if (settled) return;
|
|
43
|
+
settled = true;
|
|
44
|
+
if (timer) clearTimeout(timer);
|
|
45
|
+
if (error) {
|
|
46
|
+
try { response?.destroy(); } catch { /* already closed */ }
|
|
47
|
+
try { req?.destroy(); } catch { /* already closed */ }
|
|
48
|
+
reject(error);
|
|
49
|
+
} else {
|
|
50
|
+
resolve(value);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
const timeoutError = () => {
|
|
54
|
+
const error = new Error(`CDP ${requestPath} timed out`);
|
|
55
|
+
error.code = "CDP_HTTP_TIMEOUT";
|
|
56
|
+
return error;
|
|
57
|
+
};
|
|
58
|
+
try {
|
|
59
|
+
req = http.request({ host: "127.0.0.1", port, path: requestPath, timeout: limit, method }, (res) => {
|
|
60
|
+
response = res;
|
|
61
|
+
const chunks = [];
|
|
62
|
+
let bodyBytes = 0;
|
|
63
|
+
res.on("data", (chunk) => {
|
|
64
|
+
if (settled) return;
|
|
65
|
+
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
66
|
+
bodyBytes += bytes.length;
|
|
67
|
+
if (bodyBytes > CDP_HTTP_MAX_BYTES) {
|
|
68
|
+
const error = new Error(`CDP ${requestPath} response exceeded ${CDP_HTTP_MAX_BYTES} bytes`);
|
|
69
|
+
error.code = "CDP_HTTP_BODY_LIMIT";
|
|
70
|
+
finish(error);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
chunks.push(bytes);
|
|
74
|
+
});
|
|
75
|
+
res.on("aborted", () => finish(new Error(`CDP ${requestPath} response aborted`)));
|
|
76
|
+
res.on("error", (error) => finish(error));
|
|
77
|
+
res.on("end", () => {
|
|
78
|
+
if (settled) return;
|
|
79
|
+
try {
|
|
80
|
+
finish(null, JSON.parse(Buffer.concat(chunks, bodyBytes).toString("utf8")));
|
|
81
|
+
} catch (error) {
|
|
82
|
+
finish(error);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
33
85
|
});
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
86
|
+
req.on("error", (error) => finish(error));
|
|
87
|
+
// Keep the inactivity timeout as a fast-path, but also enforce the
|
|
88
|
+
// absolute deadline below so a peer cannot keep this request alive by
|
|
89
|
+
// dripping one byte before every inactivity interval.
|
|
90
|
+
req.on("timeout", () => finish(timeoutError()));
|
|
91
|
+
timer = setTimeout(() => finish(timeoutError()), limit);
|
|
92
|
+
timer.unref?.();
|
|
93
|
+
req.end();
|
|
94
|
+
} catch (error) {
|
|
95
|
+
finish(error);
|
|
96
|
+
}
|
|
38
97
|
});
|
|
39
98
|
}
|
|
40
99
|
|
|
@@ -96,21 +155,115 @@ async function pickPageTarget(port = DEFAULT_PORT, options = {}) {
|
|
|
96
155
|
|
|
97
156
|
function closePageTarget(port, targetId) {
|
|
98
157
|
return new Promise((resolve) => {
|
|
158
|
+
let settled = false;
|
|
159
|
+
let timer = null;
|
|
160
|
+
const finish = () => {
|
|
161
|
+
if (settled) return;
|
|
162
|
+
settled = true;
|
|
163
|
+
if (timer) clearTimeout(timer);
|
|
164
|
+
resolve();
|
|
165
|
+
};
|
|
99
166
|
const req = http.request({
|
|
100
167
|
host: "127.0.0.1",
|
|
101
168
|
port,
|
|
102
169
|
path: `/json/close/${encodeURIComponent(String(targetId))}`,
|
|
103
|
-
timeout:
|
|
170
|
+
timeout: CDP_HTTP_TIMEOUT_MS,
|
|
104
171
|
}, (res) => {
|
|
105
172
|
res.resume();
|
|
106
|
-
res.once("end",
|
|
173
|
+
res.once("end", finish);
|
|
174
|
+
res.once("aborted", finish);
|
|
175
|
+
res.once("error", finish);
|
|
107
176
|
});
|
|
108
|
-
req.once("error",
|
|
109
|
-
req.once("timeout", () => { req.destroy();
|
|
177
|
+
req.once("error", finish);
|
|
178
|
+
req.once("timeout", () => { req.destroy(); finish(); });
|
|
179
|
+
timer = setTimeout(() => { req.destroy(); finish(); }, CDP_HTTP_TIMEOUT_MS);
|
|
180
|
+
timer.unref?.();
|
|
110
181
|
req.end();
|
|
111
182
|
});
|
|
112
183
|
}
|
|
113
184
|
|
|
185
|
+
function sameFileIdentity(left, right) {
|
|
186
|
+
return left && right && String(left.dev) === String(right.dev) && String(left.ino) === String(right.ino);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function writeAtomicRegularFile(outPath, data) {
|
|
190
|
+
const target = path.resolve(String(outPath));
|
|
191
|
+
let original = null;
|
|
192
|
+
try {
|
|
193
|
+
original = fs.lstatSync(target);
|
|
194
|
+
if (!original.isFile() || original.nlink !== 1) {
|
|
195
|
+
const error = new Error(`CDP output path must be a regular file with one link: ${target}`);
|
|
196
|
+
error.code = "CDP_UNSAFE_OUTPUT_PATH";
|
|
197
|
+
throw error;
|
|
198
|
+
}
|
|
199
|
+
} catch (error) {
|
|
200
|
+
if (error && error.code !== "ENOENT") throw error;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const dir = path.dirname(target);
|
|
204
|
+
const base = path.basename(target);
|
|
205
|
+
const flags = fs.constants.O_WRONLY
|
|
206
|
+
| fs.constants.O_CREAT
|
|
207
|
+
| fs.constants.O_EXCL
|
|
208
|
+
| (fs.constants.O_NOFOLLOW || 0);
|
|
209
|
+
let temp = null;
|
|
210
|
+
let fd = null;
|
|
211
|
+
try {
|
|
212
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
213
|
+
temp = path.join(dir, `.${base}.${process.pid}.${crypto.randomUUID()}.tmp`);
|
|
214
|
+
try {
|
|
215
|
+
fd = fs.openSync(temp, flags, 0o600);
|
|
216
|
+
break;
|
|
217
|
+
} catch (error) {
|
|
218
|
+
temp = null;
|
|
219
|
+
if (!error || error.code !== "EEXIST" || attempt === 2) throw error;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (fd == null || !temp) throw new Error("could not create a temporary CDP output file");
|
|
223
|
+
let offset = 0;
|
|
224
|
+
while (offset < data.length) {
|
|
225
|
+
const written = fs.writeSync(fd, data, offset, data.length - offset);
|
|
226
|
+
if (!written) throw new Error("could not write the CDP output file");
|
|
227
|
+
offset += written;
|
|
228
|
+
}
|
|
229
|
+
fs.fsyncSync(fd);
|
|
230
|
+
fs.closeSync(fd);
|
|
231
|
+
fd = null;
|
|
232
|
+
|
|
233
|
+
// Refuse a path that changed identity while the temporary file was being
|
|
234
|
+
// written. The final rename itself never follows a symlink or hardlink.
|
|
235
|
+
let current = null;
|
|
236
|
+
try { current = fs.lstatSync(target); } catch (error) {
|
|
237
|
+
if (!error || error.code !== "ENOENT") throw error;
|
|
238
|
+
}
|
|
239
|
+
if (original) {
|
|
240
|
+
if (!current || !current.isFile() || current.nlink !== 1 || !sameFileIdentity(original, current)) {
|
|
241
|
+
const error = new Error(`CDP output path changed while writing: ${target}`);
|
|
242
|
+
error.code = "CDP_OUTPUT_IDENTITY_CHANGED";
|
|
243
|
+
throw error;
|
|
244
|
+
}
|
|
245
|
+
} else if (current) {
|
|
246
|
+
// The caller observed an absent path. A file appearing before commit is
|
|
247
|
+
// a competing writer, even when it is otherwise a safe regular file.
|
|
248
|
+
// Never overwrite a file whose identity this operation did not capture.
|
|
249
|
+
const error = new Error(`CDP output path appeared while writing: ${target}`);
|
|
250
|
+
error.code = current.isFile() && current.nlink === 1
|
|
251
|
+
? "CDP_OUTPUT_IDENTITY_CHANGED"
|
|
252
|
+
: "CDP_UNSAFE_OUTPUT_PATH";
|
|
253
|
+
throw error;
|
|
254
|
+
}
|
|
255
|
+
fs.renameSync(temp, target);
|
|
256
|
+
temp = null;
|
|
257
|
+
} finally {
|
|
258
|
+
if (fd != null) {
|
|
259
|
+
try { fs.closeSync(fd); } catch { /* already closed */ }
|
|
260
|
+
}
|
|
261
|
+
if (temp) {
|
|
262
|
+
try { fs.unlinkSync(temp); } catch { /* best-effort cleanup */ }
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
114
267
|
function waitForWebSocketOpen(ws, timeoutMs) {
|
|
115
268
|
return new Promise((resolve, reject) => {
|
|
116
269
|
let settled = false;
|
|
@@ -280,9 +433,19 @@ async function attachPage({ port = DEFAULT_PORT, wsUrl, selection = "new", targe
|
|
|
280
433
|
async function printPdf(outPath, { landscape = false, printBackground = true, scale = 1 } = {}) {
|
|
281
434
|
const res = await send("Page.printToPDF", { landscape, printBackground, scale, transferMode: "ReturnAsBase64" }, { timeout: 60000 });
|
|
282
435
|
const b64 = res && res.data;
|
|
283
|
-
if (!b64) throw new Error("Page.printToPDF returned no data");
|
|
436
|
+
if (typeof b64 !== "string" || !b64) throw new Error("Page.printToPDF returned no data");
|
|
437
|
+
if (b64.length > Math.ceil(CDP_PDF_MAX_BYTES / 3) * 4) {
|
|
438
|
+
const error = new Error(`Page.printToPDF response exceeded ${CDP_PDF_MAX_BYTES} bytes`);
|
|
439
|
+
error.code = "CDP_PDF_BODY_LIMIT";
|
|
440
|
+
throw error;
|
|
441
|
+
}
|
|
284
442
|
const buf = Buffer.from(b64, "base64");
|
|
285
|
-
|
|
443
|
+
if (buf.length > CDP_PDF_MAX_BYTES) {
|
|
444
|
+
const error = new Error(`Page.printToPDF response exceeded ${CDP_PDF_MAX_BYTES} bytes`);
|
|
445
|
+
error.code = "CDP_PDF_BODY_LIMIT";
|
|
446
|
+
throw error;
|
|
447
|
+
}
|
|
448
|
+
writeAtomicRegularFile(outPath, buf);
|
|
286
449
|
return { path: outPath, bytes: buf.length };
|
|
287
450
|
}
|
|
288
451
|
|
|
@@ -301,4 +464,14 @@ async function attachPage({ port = DEFAULT_PORT, wsUrl, selection = "new", targe
|
|
|
301
464
|
};
|
|
302
465
|
}
|
|
303
466
|
|
|
304
|
-
module.exports = {
|
|
467
|
+
module.exports = {
|
|
468
|
+
cdpReady,
|
|
469
|
+
pickPageTarget,
|
|
470
|
+
attachPage,
|
|
471
|
+
DEFAULT_PORT,
|
|
472
|
+
DEFAULT_WS_OPEN_TIMEOUT_MS,
|
|
473
|
+
CDP_HTTP_TIMEOUT_MS,
|
|
474
|
+
CDP_HTTP_MAX_BYTES,
|
|
475
|
+
CDP_PDF_MAX_BYTES,
|
|
476
|
+
writeAtomicRegularFile,
|
|
477
|
+
};
|
package/engine/browser/vault.cjs
CHANGED
|
@@ -22,6 +22,7 @@ function normalizeSite(input) {
|
|
|
22
22
|
if (!raw) return "";
|
|
23
23
|
let u;
|
|
24
24
|
try { u = new URL(/^[a-z][a-z0-9+.-]*:\/\//i.test(raw) ? raw : `https://${raw}`); } catch { return ""; }
|
|
25
|
+
if (u.protocol !== "http:" && u.protocol !== "https:") return "";
|
|
25
26
|
if (u.username || u.password) return ""; // 자격증명은 사이트 키에 절대 들이지 않는다
|
|
26
27
|
const host = u.host.toLowerCase().replace(/^www\./, "");
|
|
27
28
|
return host && !/\s/.test(host) ? host : "";
|