agentlas 1.0.64 → 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 +87 -0
- package/README.md +8 -6
- 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/bootstrap-schema.sql +4 -4
- package/engine/browser/cdp.cjs +315 -32
- 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 +67 -34
- 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 +40 -12
- 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 +187 -24
- 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
|
@@ -52,6 +52,7 @@ const cas = require("./cas.cjs");
|
|
|
52
52
|
// 터미널 업로드가 그 파일들을 아예 열지 않았다.
|
|
53
53
|
const CLOUD_TEXT_EXTS = new Set(SECRET_SCAN_TEXT_EXTENSIONS);
|
|
54
54
|
const CLOUD_AGENT_FILES = new Set(UPLOAD_AGENT_DEFINITION_FILES);
|
|
55
|
+
const CLOUD_AGENT_FILE_KEYS = new Set([...CLOUD_AGENT_FILES].map((name) => name.toLowerCase()));
|
|
55
56
|
const CLOUD_SKIP_DIRS = new Set(UPLOAD_SKIP_DIRECTORIES);
|
|
56
57
|
const CLOUD_BLOCKED_FILE_RE = [/^\.env(?:\..*)?$/i, /^id_rsa(?:\.pub)?$/i, /^credentials(?:\..*)?$/i, /^secrets?(?:\..*)?$/i, /^cloud-asset-state\.v1\.json$/i, /(?:^|[._-])service-account(?:[._-]|$)/i, /\.(?:key|pem|p12|pfx|mobileprovision)$/i];
|
|
57
58
|
const CLOUD_ROUTING_CARD_PATH = ".agentlas/routing-card.json";
|
|
@@ -385,7 +386,11 @@ const CLOUD_BULK_DATA_RE = /\.(log|jsonl|csv|tsv|lock)$/;
|
|
|
385
386
|
function cloudTrimRank(rel, bytes) {
|
|
386
387
|
const lower = String(rel || "").toLowerCase();
|
|
387
388
|
const base = lower.split("/").pop() || lower;
|
|
388
|
-
|
|
389
|
+
// The generated catalog intentionally carries case-distinct root names
|
|
390
|
+
// (`AGENTS.md`, `AGENT.md`, `agent.md`). Ranking lower-cases paths, so the
|
|
391
|
+
// lookup must lower-case the catalog as well. Otherwise a package at the
|
|
392
|
+
// byte limit can evict AGENTS.md while retaining an unrelated cache file.
|
|
393
|
+
if (CLOUD_AGENT_FILE_KEYS.has(base)) return 0;
|
|
389
394
|
if (lower.startsWith(".agentlas/")) return 0;
|
|
390
395
|
if (base === "agentlas.json" || base === "manifest.json" || base === "package.json") return 0;
|
|
391
396
|
if (CLOUD_CAPABILITY_DIR_RE.test(lower)) return 0;
|
|
@@ -427,13 +432,16 @@ function scanCloudFolder(rootPath) {
|
|
|
427
432
|
let freed = 0;
|
|
428
433
|
while (totalBytes - freed + needBytes > CLOUD_MAX_TOTAL_BYTES) {
|
|
429
434
|
let victimIndex = -1;
|
|
430
|
-
let victimRank =
|
|
435
|
+
let victimRank = Number.POSITIVE_INFINITY;
|
|
431
436
|
let victimBytes = -1;
|
|
432
437
|
for (let index = 0; index < included.length; index += 1) {
|
|
433
438
|
const candidate = included[index];
|
|
434
439
|
const rank = cloudTrimRank(candidate.path, candidate.bytes);
|
|
435
440
|
if (rank === 0) continue;
|
|
436
|
-
|
|
441
|
+
// Rank 1 is disposable build/cache output; larger numbers are closer
|
|
442
|
+
// to ordinary authored content. Desktop's canonical trimmer removes
|
|
443
|
+
// the smallest positive rank first, then the largest file in that tier.
|
|
444
|
+
if (rank < victimRank || (rank === victimRank && candidate.bytes > victimBytes)) {
|
|
437
445
|
victimIndex = index;
|
|
438
446
|
victimRank = rank;
|
|
439
447
|
victimBytes = candidate.bytes;
|
|
@@ -709,6 +717,60 @@ function scanCloudFolder(rootPath) {
|
|
|
709
717
|
return { files, included, findings, omissions, totalBytes, localPackageMarker };
|
|
710
718
|
}
|
|
711
719
|
|
|
720
|
+
/**
|
|
721
|
+
* Generated metadata is added after the stable source scan. Re-apply the
|
|
722
|
+
* public package limits at that exact boundary so injecting agentlas.json (or
|
|
723
|
+
* replacing a redacted public card) cannot turn a valid 400-file scan into a
|
|
724
|
+
* 401-file bundle that the server will reject.
|
|
725
|
+
*/
|
|
726
|
+
function cloudFitGeneratedMetadataToLimits(scan) {
|
|
727
|
+
const removeVictim = () => {
|
|
728
|
+
let victimIndex = -1;
|
|
729
|
+
let victimRank = Number.POSITIVE_INFINITY;
|
|
730
|
+
let victimBytes = -1;
|
|
731
|
+
for (let index = 0; index < scan.included.length; index += 1) {
|
|
732
|
+
const candidate = scan.included[index];
|
|
733
|
+
const rank = cloudTrimRank(candidate.path, candidate.bytes);
|
|
734
|
+
if (rank === 0) continue;
|
|
735
|
+
if (rank < victimRank || (rank === victimRank && candidate.bytes > victimBytes)) {
|
|
736
|
+
victimIndex = index;
|
|
737
|
+
victimRank = rank;
|
|
738
|
+
victimBytes = candidate.bytes;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
if (victimIndex < 0) return false;
|
|
742
|
+
const victim = scan.included.splice(victimIndex, 1)[0];
|
|
743
|
+
scan.omissions.push({
|
|
744
|
+
path: victim.path,
|
|
745
|
+
reason: "trimmed-for-package-limits",
|
|
746
|
+
sourceBytes: victim.bytes,
|
|
747
|
+
sourceSha256: victim.sha256,
|
|
748
|
+
sourceHashKind: "content",
|
|
749
|
+
});
|
|
750
|
+
const record = scan.files.find((item) => item.path === victim.path && item.included);
|
|
751
|
+
if (record) {
|
|
752
|
+
record.included = false;
|
|
753
|
+
record.reason = "trimmed-for-package-limits";
|
|
754
|
+
}
|
|
755
|
+
return true;
|
|
756
|
+
};
|
|
757
|
+
|
|
758
|
+
scan.totalBytes = scan.included.reduce((sum, file) => sum + file.bytes, 0);
|
|
759
|
+
while (scan.included.length > CLOUD_MAX_FILES || scan.totalBytes > CLOUD_MAX_TOTAL_BYTES) {
|
|
760
|
+
if (!removeVictim()) {
|
|
761
|
+
const error = new Error(
|
|
762
|
+
"Agent Cloud package cannot fit required agent metadata within the package limits. " +
|
|
763
|
+
"Reduce the number or size of capability files and retry.",
|
|
764
|
+
);
|
|
765
|
+
error.code = "AGENTLAS_CLOUD_PACKAGE_LIMITS";
|
|
766
|
+
throw error;
|
|
767
|
+
}
|
|
768
|
+
scan.totalBytes = scan.included.reduce((sum, file) => sum + file.bytes, 0);
|
|
769
|
+
}
|
|
770
|
+
scan.included.sort(cloudCodePointPathOrder);
|
|
771
|
+
scan.omissions.sort(cloudCodePointPathOrder);
|
|
772
|
+
}
|
|
773
|
+
|
|
712
774
|
function cloudOmissionReceipt(omissions) {
|
|
713
775
|
if (!Array.isArray(omissions) || omissions.length === 0) return undefined;
|
|
714
776
|
const entries = omissions.map((entry) => ({
|
|
@@ -1074,6 +1136,7 @@ async function packageCloudAgent(db, root, opts = {}) {
|
|
|
1074
1136
|
}
|
|
1075
1137
|
// 신원은 해시 계산 전에 채운다 — 클라우드 해시는 agentlas.json 을 포함한다.
|
|
1076
1138
|
cloudEnsureAgentIdentity(scan, snapshot, rootPath.split("/").pop());
|
|
1139
|
+
cloudFitGeneratedMetadataToLimits(scan);
|
|
1077
1140
|
snapshot = cloudPackageSnapshot(scan.included);
|
|
1078
1141
|
const routingCard = isPublicHubPublish ? readCloudRoutingCard(snapshot) : {};
|
|
1079
1142
|
if (routingCard.finding) scan.findings.push(routingCard.finding);
|
|
@@ -30,15 +30,18 @@ async function listOwnedCloudAgents(limit = 100) {
|
|
|
30
30
|
};
|
|
31
31
|
if (!Array.isArray(result.results)) throw new Error("Agent Cloud list returned an invalid results contract.");
|
|
32
32
|
if (result.results.length) {
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
const descriptor
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
33
|
+
const descriptors = result.results.map((raw) => normalizeCloudAssetDescriptor(raw, "Agent Cloud list result"));
|
|
34
|
+
state.updateCloudAssetState((stateValue) => {
|
|
35
|
+
for (const descriptor of descriptors) {
|
|
36
|
+
const key = state.cloudDescriptorKey(descriptor);
|
|
37
|
+
const previous = stateValue.assets[key];
|
|
38
|
+
// A list response may race a save that completed after the server made
|
|
39
|
+
// the list snapshot. Never regress a newer locally observed receipt.
|
|
40
|
+
if (previous && Date.parse(previous.descriptor.updatedAt) > Date.parse(descriptor.updatedAt)) continue;
|
|
41
|
+
const preserveRoots = previous && previous.descriptor.cloudId === descriptor.cloudId && previous.descriptor.revision === descriptor.revision;
|
|
42
|
+
stateValue.assets[key] = { descriptor, sourceRoots: preserveRoots ? previous.sourceRoots : [] };
|
|
43
|
+
}
|
|
44
|
+
});
|
|
42
45
|
}
|
|
43
46
|
return result;
|
|
44
47
|
}
|
|
@@ -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,
|