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
|
@@ -30,6 +30,132 @@ const {
|
|
|
30
30
|
} = require("../hub/install.cjs");
|
|
31
31
|
|
|
32
32
|
const CLOUD_ASSET_STATE_FILE = "cloud-asset-state.v1.json";
|
|
33
|
+
const CLOUD_ASSET_STATE_MAX_BYTES = 1024 * 1024;
|
|
34
|
+
const CLOUD_ASSET_LOCK_STALE_MS = 30_000;
|
|
35
|
+
const CLOUD_ASSET_LOCK_WAIT_MS = 10_000;
|
|
36
|
+
|
|
37
|
+
function waitSync(milliseconds) {
|
|
38
|
+
const signal = new Int32Array(new SharedArrayBuffer(4));
|
|
39
|
+
Atomics.wait(signal, 0, 0, milliseconds);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function processIsAlive(pid) {
|
|
43
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) return false;
|
|
44
|
+
try {
|
|
45
|
+
process.kill(pid, 0);
|
|
46
|
+
return true;
|
|
47
|
+
} catch (error) {
|
|
48
|
+
if (error && error.code === "ESRCH") return false;
|
|
49
|
+
// EPERM means the process exists but belongs to another user. Unknown
|
|
50
|
+
// failures also stay fail-safe: never steal a lock from a possibly live owner.
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function readLockOwner(lockPath) {
|
|
56
|
+
const ownerPath = path.join(lockPath, "owner.json");
|
|
57
|
+
let fd;
|
|
58
|
+
try {
|
|
59
|
+
fd = fs.openSync(ownerPath, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0));
|
|
60
|
+
const before = fs.fstatSync(fd);
|
|
61
|
+
if (!before.isFile() || before.nlink !== 1 || before.size <= 0 || before.size > 512) {
|
|
62
|
+
throw new Error("lock owner is not a bounded private file");
|
|
63
|
+
}
|
|
64
|
+
const raw = fs.readFileSync(fd, "utf8");
|
|
65
|
+
const after = fs.fstatSync(fd);
|
|
66
|
+
if (
|
|
67
|
+
Buffer.byteLength(raw, "utf8") !== before.size || after.dev !== before.dev || after.ino !== before.ino ||
|
|
68
|
+
after.size !== before.size || after.mtimeMs !== before.mtimeMs || after.ctimeMs !== before.ctimeMs
|
|
69
|
+
) {
|
|
70
|
+
throw new Error("lock owner changed while it was read");
|
|
71
|
+
}
|
|
72
|
+
const parsed = JSON.parse(raw);
|
|
73
|
+
if (!Number.isSafeInteger(parsed.pid) || parsed.pid <= 0 || typeof parsed.nonce !== "string" || !/^[a-f0-9]{32}$/.test(parsed.nonce)) {
|
|
74
|
+
throw new Error("lock owner identity is invalid");
|
|
75
|
+
}
|
|
76
|
+
return parsed;
|
|
77
|
+
} catch (error) {
|
|
78
|
+
if (error && error.code === "ENOENT") return null;
|
|
79
|
+
throw error;
|
|
80
|
+
} finally {
|
|
81
|
+
if (fd !== undefined) try { fs.closeSync(fd); } catch { /* best effort */ }
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Directory rename is the lock hand-off boundary. A stale lock is moved to a
|
|
87
|
+
* unique quarantine path before cleanup, so a new owner's lock can never be
|
|
88
|
+
* removed by the old cleanup path.
|
|
89
|
+
*/
|
|
90
|
+
function withCloudAssetLock(targetPath, label, action) {
|
|
91
|
+
const lockPath = `${targetPath}.lock`;
|
|
92
|
+
const lockParent = path.dirname(lockPath);
|
|
93
|
+
fs.mkdirSync(lockParent, { recursive: true, mode: 0o700 });
|
|
94
|
+
try { fs.chmodSync(lockParent, 0o700); } catch { /* best effort */ }
|
|
95
|
+
const deadline = Date.now() + CLOUD_ASSET_LOCK_WAIT_MS;
|
|
96
|
+
let acquired = false;
|
|
97
|
+
while (!acquired) {
|
|
98
|
+
try {
|
|
99
|
+
fs.mkdirSync(lockPath, { mode: 0o700 });
|
|
100
|
+
} catch (error) {
|
|
101
|
+
if (!error || (error.code !== "EEXIST" && error.code !== "ENOENT")) throw error;
|
|
102
|
+
if (error.code === "ENOENT") continue;
|
|
103
|
+
let stat;
|
|
104
|
+
try { stat = fs.lstatSync(lockPath); } catch (statError) {
|
|
105
|
+
if (statError && statError.code === "ENOENT") continue;
|
|
106
|
+
throw statError;
|
|
107
|
+
}
|
|
108
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error(`${label} lock is unsafe`);
|
|
109
|
+
let owner = null;
|
|
110
|
+
try { owner = readLockOwner(lockPath); } catch { owner = null; }
|
|
111
|
+
if (Date.now() - stat.mtimeMs > CLOUD_ASSET_LOCK_STALE_MS && (!owner || !processIsAlive(owner.pid))) {
|
|
112
|
+
const quarantine = `${lockPath}.stale-${process.pid}-${crypto.randomBytes(6).toString("hex")}`;
|
|
113
|
+
try {
|
|
114
|
+
fs.renameSync(lockPath, quarantine);
|
|
115
|
+
fs.rmSync(quarantine, { recursive: true, force: true });
|
|
116
|
+
continue;
|
|
117
|
+
} catch (reclaimError) {
|
|
118
|
+
if (reclaimError && reclaimError.code === "ENOENT") continue;
|
|
119
|
+
throw reclaimError;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (Date.now() >= deadline) throw new Error(`${label} is busy; retry after the active operation finishes`);
|
|
123
|
+
waitSync(25);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
const owner = {
|
|
128
|
+
pid: process.pid,
|
|
129
|
+
nonce: crypto.randomBytes(16).toString("hex"),
|
|
130
|
+
createdAt: new Date().toISOString(),
|
|
131
|
+
};
|
|
132
|
+
fs.writeFileSync(path.join(lockPath, "owner.json"), JSON.stringify(owner) + "\n", {
|
|
133
|
+
encoding: "utf8",
|
|
134
|
+
mode: 0o600,
|
|
135
|
+
flag: "wx",
|
|
136
|
+
});
|
|
137
|
+
acquired = true;
|
|
138
|
+
} catch (error) {
|
|
139
|
+
const abandoned = `${lockPath}.abandoned-${process.pid}-${crypto.randomBytes(6).toString("hex")}`;
|
|
140
|
+
try {
|
|
141
|
+
fs.renameSync(lockPath, abandoned);
|
|
142
|
+
fs.rmSync(abandoned, { recursive: true, force: true });
|
|
143
|
+
} catch { /* original owner-write error remains authoritative */ }
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
return action();
|
|
149
|
+
} finally {
|
|
150
|
+
const cleanup = `${lockPath}.done-${process.pid}-${crypto.randomBytes(6).toString("hex")}`;
|
|
151
|
+
try {
|
|
152
|
+
fs.renameSync(lockPath, cleanup);
|
|
153
|
+
fs.rmSync(cleanup, { recursive: true, force: true });
|
|
154
|
+
} catch (error) {
|
|
155
|
+
if (!error || error.code !== "ENOENT") throw error;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
33
159
|
|
|
34
160
|
function normalizeCloudScopeFlag(value) {
|
|
35
161
|
if (value === "owner-private" || value === "private" || value === "private-link") return "owner-private";
|
|
@@ -45,45 +171,68 @@ function cloudAssetStatePath() {
|
|
|
45
171
|
return path.join(userDataDir(), CLOUD_ASSET_STATE_FILE);
|
|
46
172
|
}
|
|
47
173
|
|
|
174
|
+
function normalizeCloudAssetState(parsed) {
|
|
175
|
+
if (!parsed || parsed.schemaVersion !== 1 || !parsed.assets || typeof parsed.assets !== "object" || Array.isArray(parsed.assets)) {
|
|
176
|
+
throw new Error("state schema is invalid");
|
|
177
|
+
}
|
|
178
|
+
const assets = {};
|
|
179
|
+
for (const [key, raw] of Object.entries(parsed.assets)) {
|
|
180
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error(`state entry ${key} is invalid`);
|
|
181
|
+
const descriptor = normalizeCloudAssetDescriptor(raw.descriptor, `state entry ${key}`);
|
|
182
|
+
if (key !== cloudDescriptorKey(descriptor)) throw new Error(`state entry ${key} key is invalid`);
|
|
183
|
+
if (!Array.isArray(raw.sourceRoots)) throw new Error(`state entry ${key} sourceRoots is invalid`);
|
|
184
|
+
for (const item of raw.sourceRoots) {
|
|
185
|
+
if (typeof item !== "string" || !path.isAbsolute(item)) throw new Error(`state entry ${key} sourceRoots is invalid`);
|
|
186
|
+
}
|
|
187
|
+
const sourceRoots = [...new Set(raw.sourceRoots.map((item) => path.resolve(item)))].slice(0, 32);
|
|
188
|
+
assets[key] = { descriptor, sourceRoots };
|
|
189
|
+
}
|
|
190
|
+
if (!Array.isArray(parsed.deletedBases)) throw new Error("state deletedBases is invalid");
|
|
191
|
+
const deletedBases = parsed.deletedBases.map((item, index) => {
|
|
192
|
+
if (
|
|
193
|
+
!item || typeof item !== "object" || Array.isArray(item) ||
|
|
194
|
+
typeof item.rootPath !== "string" || !path.isAbsolute(item.rootPath) ||
|
|
195
|
+
typeof item.slug !== "string" || cloudSlug(item.slug) !== item.slug ||
|
|
196
|
+
!CLOUD_ASSET_SCOPES.has(item.scope) || !/^[A-Za-z0-9_-]{8,128}$/.test(String(item.cloudId || "")) ||
|
|
197
|
+
typeof item.revision !== "string" || !item.revision || item.revision.length > 512 || /["\\\u0000-\u001f\u007f]/.test(item.revision)
|
|
198
|
+
) {
|
|
199
|
+
throw new Error(`state deletedBases[${index}] is invalid`);
|
|
200
|
+
}
|
|
201
|
+
return {
|
|
202
|
+
rootPath: path.resolve(item.rootPath),
|
|
203
|
+
slug: item.slug,
|
|
204
|
+
scope: item.scope,
|
|
205
|
+
cloudId: item.cloudId,
|
|
206
|
+
revision: item.revision,
|
|
207
|
+
};
|
|
208
|
+
}).slice(-256);
|
|
209
|
+
return { schemaVersion: 1, assets, deletedBases };
|
|
210
|
+
}
|
|
211
|
+
|
|
48
212
|
function readCloudAssetState() {
|
|
49
213
|
const statePath = cloudAssetStatePath();
|
|
50
|
-
if (!fs.existsSync(statePath)) return { schemaVersion: 1, assets: {}, deletedBases: [] };
|
|
51
214
|
let fd;
|
|
52
215
|
try {
|
|
53
216
|
// O_NOFOLLOW: 상태 파일이 심링크로 바꿔치기되면 읽지 않는다 (로컬 상태 위조 방어).
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
throw new Error("state schema is invalid");
|
|
217
|
+
try {
|
|
218
|
+
fd = fs.openSync(statePath, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0));
|
|
219
|
+
} catch (error) {
|
|
220
|
+
if (error && error.code === "ENOENT") return { schemaVersion: 1, assets: {}, deletedBases: [] };
|
|
221
|
+
throw error;
|
|
60
222
|
}
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const descriptor = normalizeCloudAssetDescriptor(raw.descriptor, `state entry ${key}`);
|
|
65
|
-
if (key !== cloudDescriptorKey(descriptor)) throw new Error(`state entry ${key} key is invalid`);
|
|
66
|
-
const sourceRoots = Array.isArray(raw.sourceRoots)
|
|
67
|
-
? [...new Set(raw.sourceRoots.filter((item) => typeof item === "string" && path.isAbsolute(item)).map((item) => path.resolve(item)))].slice(0, 32)
|
|
68
|
-
: [];
|
|
69
|
-
assets[key] = { descriptor, sourceRoots };
|
|
223
|
+
const before = fs.fstatSync(fd);
|
|
224
|
+
if (!before.isFile() || before.nlink !== 1 || before.size <= 0 || before.size > CLOUD_ASSET_STATE_MAX_BYTES) {
|
|
225
|
+
throw new Error("state file is not a bounded private file");
|
|
70
226
|
}
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
slug: item.slug,
|
|
81
|
-
scope: item.scope,
|
|
82
|
-
cloudId: item.cloudId,
|
|
83
|
-
revision: item.revision,
|
|
84
|
-
})).slice(-256)
|
|
85
|
-
: [];
|
|
86
|
-
return { schemaVersion: 1, assets, deletedBases };
|
|
227
|
+
const raw = fs.readFileSync(fd, "utf8");
|
|
228
|
+
const after = fs.fstatSync(fd);
|
|
229
|
+
if (
|
|
230
|
+
Buffer.byteLength(raw, "utf8") !== before.size || after.dev !== before.dev || after.ino !== before.ino ||
|
|
231
|
+
after.size !== before.size || after.mtimeMs !== before.mtimeMs || after.ctimeMs !== before.ctimeMs
|
|
232
|
+
) {
|
|
233
|
+
throw new Error("state file changed while it was read");
|
|
234
|
+
}
|
|
235
|
+
return normalizeCloudAssetState(JSON.parse(raw));
|
|
87
236
|
} catch (error) {
|
|
88
237
|
// 깨진 상태 파일을 빈 상태로 위장하면 stale 베이스로 원격 리비전을 덮어쓸 수 있다.
|
|
89
238
|
throw new Error(`Agent Cloud local revision state is unreadable: ${error.message || error}`);
|
|
@@ -92,40 +241,62 @@ function readCloudAssetState() {
|
|
|
92
241
|
}
|
|
93
242
|
}
|
|
94
243
|
|
|
95
|
-
function
|
|
244
|
+
function writeCloudAssetStateUnlocked(state) {
|
|
96
245
|
const statePath = cloudAssetStatePath();
|
|
97
|
-
|
|
246
|
+
const normalized = normalizeCloudAssetState(state);
|
|
247
|
+
fs.mkdirSync(path.dirname(statePath), { recursive: true, mode: 0o700 });
|
|
248
|
+
try { fs.chmodSync(path.dirname(statePath), 0o700); } catch { /* best effort */ }
|
|
98
249
|
const temp = `${statePath}.tmp-${process.pid}-${crypto.randomBytes(4).toString("hex")}`;
|
|
99
|
-
|
|
250
|
+
let fd;
|
|
100
251
|
try {
|
|
101
|
-
|
|
102
|
-
|
|
252
|
+
try {
|
|
253
|
+
fd = fs.openSync(temp, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY, 0o600);
|
|
254
|
+
fs.writeFileSync(fd, JSON.stringify(normalized, null, 2) + "\n", "utf8");
|
|
255
|
+
fs.fsyncSync(fd);
|
|
256
|
+
} finally {
|
|
257
|
+
if (fd !== undefined) try { fs.closeSync(fd); } catch { /* best effort */ }
|
|
258
|
+
}
|
|
259
|
+
fs.renameSync(temp, statePath);
|
|
260
|
+
cloudApplyPortableFileMode(statePath, 0o600);
|
|
261
|
+
cloudFsyncDirectory(path.dirname(statePath));
|
|
103
262
|
} finally {
|
|
104
|
-
fs.
|
|
263
|
+
try { fs.rmSync(temp, { force: true }); } catch { /* best effort */ }
|
|
105
264
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
265
|
+
return normalized;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function writeCloudAssetState(state) {
|
|
269
|
+
return withCloudAssetLock(cloudAssetStatePath(), "Agent Cloud local revision state", () => writeCloudAssetStateUnlocked(state));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function updateCloudAssetState(mutator) {
|
|
273
|
+
if (typeof mutator !== "function") throw new TypeError("Cloud asset state mutator must be a function");
|
|
274
|
+
return withCloudAssetLock(cloudAssetStatePath(), "Agent Cloud local revision state", () => {
|
|
275
|
+
const state = readCloudAssetState();
|
|
276
|
+
const result = mutator(state);
|
|
277
|
+
writeCloudAssetStateUnlocked(state);
|
|
278
|
+
return result;
|
|
279
|
+
});
|
|
109
280
|
}
|
|
110
281
|
|
|
111
282
|
/** 서버가 준 리비전 영수증을 관측 상태로 승격. sourceRoot가 있으면 톰스톤도 해제한다. */
|
|
112
283
|
function rememberCloudAssetDescriptor(value, options = {}) {
|
|
113
284
|
const descriptor = normalizeCloudAssetDescriptor(value);
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
285
|
+
return updateCloudAssetState((state) => {
|
|
286
|
+
const key = cloudDescriptorKey(descriptor);
|
|
287
|
+
const previous = state.assets[key];
|
|
288
|
+
const sameRevision = previous && previous.descriptor.cloudId === descriptor.cloudId && previous.descriptor.revision === descriptor.revision;
|
|
289
|
+
const roots = sameRevision ? [...previous.sourceRoots] : [];
|
|
290
|
+
if (options.sourceRoot) {
|
|
291
|
+
const sourceRoot = path.resolve(options.sourceRoot);
|
|
292
|
+
roots.push(sourceRoot);
|
|
293
|
+
state.deletedBases = state.deletedBases.filter(
|
|
294
|
+
(item) => !(item.rootPath === sourceRoot && item.slug === descriptor.slug && item.scope === descriptor.scope),
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
state.assets[key] = { descriptor, sourceRoots: [...new Set(roots)].slice(0, 32) };
|
|
298
|
+
return descriptor;
|
|
299
|
+
});
|
|
129
300
|
}
|
|
130
301
|
|
|
131
302
|
function findCloudAssetDescriptor(slug, scope) {
|
|
@@ -193,18 +364,18 @@ function cloudUnboundDescriptorForSource(rootPath, slug, scope) {
|
|
|
193
364
|
|
|
194
365
|
/** 이 소스 루트를 이미 관측된 클라우드 자산에 명시적으로 연결한다. */
|
|
195
366
|
function bindCloudAssetSourceRoot(rootPath, slug, scope) {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
367
|
+
return updateCloudAssetState((state) => {
|
|
368
|
+
const key = `${scope}:${slug}`;
|
|
369
|
+
const entry = state.assets[key];
|
|
370
|
+
if (!entry) throw new Error(`No observed Cloud revision for ${key} to bind.`);
|
|
371
|
+
const normalizedRoot = path.resolve(rootPath);
|
|
372
|
+
const roots = [...new Set([...entry.sourceRoots, normalizedRoot])].slice(0, 32);
|
|
373
|
+
state.assets[key] = { descriptor: entry.descriptor, sourceRoots: roots };
|
|
374
|
+
state.deletedBases = state.deletedBases.filter(
|
|
375
|
+
(item) => !(item.rootPath === normalizedRoot && item.slug === slug && item.scope === scope),
|
|
376
|
+
);
|
|
377
|
+
return entry.descriptor;
|
|
378
|
+
});
|
|
208
379
|
}
|
|
209
380
|
|
|
210
381
|
function cloudBaseDescriptorForSource(marker, rootPath, slug, scope) {
|
|
@@ -221,64 +392,87 @@ function cloudBaseDescriptorForSource(marker, rootPath, slug, scope) {
|
|
|
221
392
|
const stateDescriptor = entry && entry.sourceRoots.includes(normalizedRoot) ? entry.descriptor : null;
|
|
222
393
|
if (!markerDescriptor) return stateDescriptor;
|
|
223
394
|
if (!stateDescriptor) return markerDescriptor;
|
|
224
|
-
return stateDescriptor.cloudId === markerDescriptor.cloudId && stateDescriptor.updatedAt >= markerDescriptor.updatedAt
|
|
395
|
+
return stateDescriptor.cloudId === markerDescriptor.cloudId && Date.parse(stateDescriptor.updatedAt) >= Date.parse(markerDescriptor.updatedAt)
|
|
225
396
|
? stateDescriptor
|
|
226
397
|
: markerDescriptor;
|
|
227
398
|
}
|
|
228
399
|
|
|
229
400
|
function writeCloudSourceMarker(rootPath, scan, descriptor, options = {}) {
|
|
230
401
|
const markerPath = path.join(rootPath, CLOUD_RESTORE_MARKER_PATH);
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
const
|
|
239
|
-
if (
|
|
240
|
-
|
|
402
|
+
const lockTarget = path.join(
|
|
403
|
+
userDataDir(),
|
|
404
|
+
"cloud-source-marker-locks",
|
|
405
|
+
crypto.createHash("sha256").update(path.resolve(rootPath)).digest("hex"),
|
|
406
|
+
);
|
|
407
|
+
return withCloudAssetLock(lockTarget, "Agent Cloud source marker", () => {
|
|
408
|
+
const previousMarker = readCloudSourceMarker(rootPath);
|
|
409
|
+
const descriptors = cloudMarkerDescriptors(previousMarker);
|
|
410
|
+
if (descriptor) descriptors[descriptor.scope] = descriptor;
|
|
411
|
+
if (options.removeDescriptor) {
|
|
412
|
+
const current = descriptors[options.removeDescriptor.scope];
|
|
413
|
+
if (current && current.cloudId === options.removeDescriptor.cloudId && current.revision === options.removeDescriptor.revision) {
|
|
414
|
+
delete descriptors[options.removeDescriptor.scope];
|
|
415
|
+
}
|
|
241
416
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
417
|
+
const latest = descriptor || Object.values(descriptors)[0] || null;
|
|
418
|
+
const marker = {
|
|
419
|
+
schemaVersion: 1,
|
|
420
|
+
source: "agentlas-cloud",
|
|
421
|
+
slug: latest?.slug || options.removeDescriptor?.slug || cloudSlug(path.basename(rootPath)),
|
|
422
|
+
packageHash: descriptor?.packageHash || options.packageHash || previousMarker?.packageHash || "",
|
|
423
|
+
packageHashVersion: descriptor?.packageHashVersion || options.packageHashVersion || previousMarker?.packageHashVersion || CLOUD_PACKAGE_HASH_V1,
|
|
424
|
+
fileCount: Number.isSafeInteger(options.fileCount) ? options.fileCount : (previousMarker?.fileCount || 0),
|
|
425
|
+
totalBytes: Number.isSafeInteger(options.totalBytes) ? options.totalBytes : (previousMarker?.totalBytes || 0),
|
|
426
|
+
executablePaths: Array.isArray(options.executablePaths) ? options.executablePaths : previousMarker?.executablePaths,
|
|
427
|
+
cloudAssets: descriptors,
|
|
428
|
+
...(latest ? latest : {}),
|
|
429
|
+
restoredAt: previousMarker?.restoredAt,
|
|
430
|
+
savedAt: new Date().toISOString(),
|
|
431
|
+
};
|
|
432
|
+
for (const key of Object.keys(marker)) if (marker[key] === undefined) delete marker[key];
|
|
433
|
+
const temp = path.join(rootPath, `.${CLOUD_RESTORE_MARKER_PATH}.tmp-${process.pid}-${crypto.randomBytes(4).toString("hex")}`);
|
|
434
|
+
let fd;
|
|
435
|
+
try {
|
|
436
|
+
try {
|
|
437
|
+
fd = fs.openSync(temp, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY, 0o600);
|
|
438
|
+
fs.writeFileSync(fd, JSON.stringify(marker, null, 2) + "\n", "utf8");
|
|
439
|
+
fs.fsyncSync(fd);
|
|
440
|
+
} finally {
|
|
441
|
+
if (fd !== undefined) try { fs.closeSync(fd); } catch { /* best effort */ }
|
|
442
|
+
}
|
|
443
|
+
fs.renameSync(temp, markerPath);
|
|
444
|
+
cloudApplyPortableFileMode(markerPath, 0o600);
|
|
445
|
+
cloudFsyncDirectory(rootPath);
|
|
446
|
+
} finally {
|
|
447
|
+
try { fs.rmSync(temp, { force: true }); } catch { /* best effort */ }
|
|
448
|
+
}
|
|
449
|
+
return marker;
|
|
450
|
+
});
|
|
271
451
|
}
|
|
272
452
|
|
|
273
453
|
function readCloudSourceMarker(rootPath) {
|
|
274
454
|
const markerPath = path.join(rootPath, CLOUD_RESTORE_MARKER_PATH);
|
|
275
|
-
if (!fs.existsSync(markerPath)) return null;
|
|
276
455
|
let fd;
|
|
277
456
|
try {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
457
|
+
try {
|
|
458
|
+
fd = fs.openSync(markerPath, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0));
|
|
459
|
+
} catch (error) {
|
|
460
|
+
if (error && error.code === "ENOENT") return null;
|
|
461
|
+
throw error;
|
|
462
|
+
}
|
|
463
|
+
const before = fs.fstatSync(fd);
|
|
464
|
+
if (!before.isFile() || before.nlink !== 1 || before.size <= 0 || before.size > CLOUD_ASSET_STATE_MAX_BYTES) {
|
|
465
|
+
throw new Error("marker is not a bounded private file");
|
|
466
|
+
}
|
|
467
|
+
const raw = fs.readFileSync(fd, "utf8");
|
|
468
|
+
const after = fs.fstatSync(fd);
|
|
469
|
+
if (
|
|
470
|
+
Buffer.byteLength(raw, "utf8") !== before.size || after.dev !== before.dev || after.ino !== before.ino ||
|
|
471
|
+
after.size !== before.size || after.mtimeMs !== before.mtimeMs || after.ctimeMs !== before.ctimeMs
|
|
472
|
+
) {
|
|
473
|
+
throw new Error("marker changed while it was read");
|
|
474
|
+
}
|
|
475
|
+
return JSON.parse(raw);
|
|
282
476
|
} finally {
|
|
283
477
|
if (fd !== undefined) fs.closeSync(fd);
|
|
284
478
|
}
|
|
@@ -291,6 +485,7 @@ module.exports = {
|
|
|
291
485
|
cloudAssetStatePath,
|
|
292
486
|
readCloudAssetState,
|
|
293
487
|
writeCloudAssetState,
|
|
488
|
+
updateCloudAssetState,
|
|
294
489
|
rememberCloudAssetDescriptor,
|
|
295
490
|
findCloudAssetDescriptor,
|
|
296
491
|
cloudMarkerDescriptors,
|
package/engine/commands/acp.cjs
CHANGED
|
@@ -28,14 +28,25 @@ function descriptor() {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
async function run(ctx, args) {
|
|
31
|
-
|
|
31
|
+
const descriptorMode = ctx.output?.format === "json" || (args.length === 1 && ["--info", "--json"].includes(args[0]));
|
|
32
|
+
if (descriptorMode) {
|
|
33
|
+
if (args.some((arg) => !["--info", "--json"].includes(arg)) || new Set(args).size !== args.length) {
|
|
34
|
+
const error = new Error("Usage: agentlas acp [--info]");
|
|
35
|
+
error.code = "INVALID_ARGUMENT";
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
32
38
|
ctx.out(JSON.stringify(descriptor(), null, 2));
|
|
33
39
|
return 0;
|
|
34
40
|
}
|
|
35
|
-
if (args.
|
|
41
|
+
if (args.length === 1 && ["--help", "-h", "help"].includes(args[0])) {
|
|
36
42
|
ctx.out("Usage: agentlas acp [--info]\n Speak the Agent Client Protocol (v1) on stdio so an editor can run Agentlas as its agent.");
|
|
37
43
|
return 0;
|
|
38
44
|
}
|
|
45
|
+
if (args.length) {
|
|
46
|
+
const error = new Error("Usage: agentlas acp [--info]");
|
|
47
|
+
error.code = "INVALID_ARGUMENT";
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
39
50
|
// stdout is the protocol wire from here on: route everything human to stderr.
|
|
40
51
|
const server = new AcpAgentServer({ ctx, input: process.stdin, output: process.stdout });
|
|
41
52
|
await server.start();
|