@openclaw/crabline 0.1.11 → 0.1.12
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/README.md +99 -12
- package/dist/src/bin/crabline.js +3 -1
- package/dist/src/bin/crabline.js.map +1 -1
- package/dist/src/cli/program.d.ts +5 -1
- package/dist/src/cli/program.js +264 -50
- package/dist/src/cli/program.js.map +1 -1
- package/dist/src/config/load.js +39 -3
- package/dist/src/config/load.js.map +1 -1
- package/dist/src/config/schema.d.ts +20 -0
- package/dist/src/config/schema.js +163 -35
- package/dist/src/config/schema.js.map +1 -1
- package/dist/src/core/errors.d.ts +1 -0
- package/dist/src/core/errors.js +6 -3
- package/dist/src/core/errors.js.map +1 -1
- package/dist/src/core/http-path.d.ts +1 -0
- package/dist/src/core/http-path.js +14 -0
- package/dist/src/core/http-path.js.map +1 -0
- package/dist/src/core/matcher.d.ts +3 -1
- package/dist/src/core/matcher.js +58 -2
- package/dist/src/core/matcher.js.map +1 -1
- package/dist/src/core/nonces.d.ts +2 -0
- package/dist/src/core/nonces.js +11 -1
- package/dist/src/core/nonces.js.map +1 -1
- package/dist/src/core/reporters.d.ts +5 -0
- package/dist/src/core/reporters.js +26 -2
- package/dist/src/core/reporters.js.map +1 -1
- package/dist/src/core/run.d.ts +3 -1
- package/dist/src/core/run.js +178 -66
- package/dist/src/core/run.js.map +1 -1
- package/dist/src/matrix-ids.d.ts +5 -0
- package/dist/src/matrix-ids.js +83 -0
- package/dist/src/matrix-ids.js.map +1 -0
- package/dist/src/openclaw/artifact-generation.d.ts +7 -2
- package/dist/src/openclaw/artifact-generation.js +399 -27
- package/dist/src/openclaw/artifact-generation.js.map +1 -1
- package/dist/src/openclaw/bridges/matrix.js +21 -4
- package/dist/src/openclaw/bridges/matrix.js.map +1 -1
- package/dist/src/openclaw/bridges/mattermost.js +13 -8
- package/dist/src/openclaw/bridges/mattermost.js.map +1 -1
- package/dist/src/openclaw/bridges/probe-response.d.ts +1 -0
- package/dist/src/openclaw/bridges/probe-response.js +5 -0
- package/dist/src/openclaw/bridges/probe-response.js.map +1 -0
- package/dist/src/openclaw/bridges/signal.js +15 -6
- package/dist/src/openclaw/bridges/signal.js.map +1 -1
- package/dist/src/openclaw/bridges/slack.js +116 -23
- package/dist/src/openclaw/bridges/slack.js.map +1 -1
- package/dist/src/openclaw/bridges/telegram.js +45 -19
- package/dist/src/openclaw/bridges/telegram.js.map +1 -1
- package/dist/src/openclaw/bridges/whatsapp.js +32 -16
- package/dist/src/openclaw/bridges/whatsapp.js.map +1 -1
- package/dist/src/openclaw/bridges/zalo.js +8 -3
- package/dist/src/openclaw/bridges/zalo.js.map +1 -1
- package/dist/src/openclaw/private-file.d.ts +38 -3
- package/dist/src/openclaw/private-file.js +2008 -87
- package/dist/src/openclaw/private-file.js.map +1 -1
- package/dist/src/openclaw/shared.d.ts +1 -0
- package/dist/src/openclaw/shared.js +48 -13
- package/dist/src/openclaw/shared.js.map +1 -1
- package/dist/src/openclaw/smoke-lock.d.ts +9 -0
- package/dist/src/openclaw/smoke-lock.js +626 -172
- package/dist/src/openclaw/smoke-lock.js.map +1 -1
- package/dist/src/openclaw.js +225 -42
- package/dist/src/openclaw.js.map +1 -1
- package/dist/src/platform/process-owned-lock.d.ts +11 -0
- package/dist/src/platform/process-owned-lock.js +1698 -0
- package/dist/src/platform/process-owned-lock.js.map +1 -0
- package/dist/src/platform/recorder-directory.d.ts +1 -0
- package/dist/src/platform/recorder-directory.js +14 -0
- package/dist/src/platform/recorder-directory.js.map +1 -0
- package/dist/src/platform/windows-acl.d.ts +18 -0
- package/dist/src/platform/windows-acl.js +1367 -0
- package/dist/src/platform/windows-acl.js.map +1 -0
- package/dist/src/platform/windows-lock-root.d.ts +13 -0
- package/dist/src/platform/windows-lock-root.js +98 -0
- package/dist/src/platform/windows-lock-root.js.map +1 -0
- package/dist/src/providers/builtin/discord.d.ts +4 -0
- package/dist/src/providers/builtin/discord.js +52 -9
- package/dist/src/providers/builtin/discord.js.map +1 -1
- package/dist/src/providers/builtin/external-webhook-auth.d.ts +1 -0
- package/dist/src/providers/builtin/external-webhook-auth.js +38 -1
- package/dist/src/providers/builtin/external-webhook-auth.js.map +1 -1
- package/dist/src/providers/builtin/feishu.d.ts +1 -0
- package/dist/src/providers/builtin/feishu.js +96 -26
- package/dist/src/providers/builtin/feishu.js.map +1 -1
- package/dist/src/providers/builtin/googlechat.d.ts +1 -0
- package/dist/src/providers/builtin/googlechat.js +95 -26
- package/dist/src/providers/builtin/googlechat.js.map +1 -1
- package/dist/src/providers/builtin/imessage.js +60 -17
- package/dist/src/providers/builtin/imessage.js.map +1 -1
- package/dist/src/providers/builtin/loopback.js +88 -35
- package/dist/src/providers/builtin/loopback.js.map +1 -1
- package/dist/src/providers/builtin/matrix.d.ts +2 -2
- package/dist/src/providers/builtin/matrix.js +78 -21
- package/dist/src/providers/builtin/matrix.js.map +1 -1
- package/dist/src/providers/builtin/mattermost.d.ts +11 -7
- package/dist/src/providers/builtin/mattermost.js +138 -24
- package/dist/src/providers/builtin/mattermost.js.map +1 -1
- package/dist/src/providers/builtin/msteams.d.ts +1 -0
- package/dist/src/providers/builtin/msteams.js +118 -38
- package/dist/src/providers/builtin/msteams.js.map +1 -1
- package/dist/src/providers/builtin/native-local-mock.js +4 -4
- package/dist/src/providers/builtin/native-local-mock.js.map +1 -1
- package/dist/src/providers/builtin/script.js +1057 -146
- package/dist/src/providers/builtin/script.js.map +1 -1
- package/dist/src/providers/builtin/slack.d.ts +8 -1
- package/dist/src/providers/builtin/slack.js +327 -24
- package/dist/src/providers/builtin/slack.js.map +1 -1
- package/dist/src/providers/builtin/telegram.d.ts +1 -1
- package/dist/src/providers/builtin/telegram.js +55 -14
- package/dist/src/providers/builtin/telegram.js.map +1 -1
- package/dist/src/providers/builtin/whatsapp.d.ts +4 -3
- package/dist/src/providers/builtin/whatsapp.js +157 -29
- package/dist/src/providers/builtin/whatsapp.js.map +1 -1
- package/dist/src/providers/builtin/zalo.d.ts +5 -2
- package/dist/src/providers/builtin/zalo.js +31 -7
- package/dist/src/providers/builtin/zalo.js.map +1 -1
- package/dist/src/providers/catalog.d.ts +1 -1
- package/dist/src/providers/catalog.js +1 -1
- package/dist/src/providers/catalog.js.map +1 -1
- package/dist/src/providers/local-mock.d.ts +4 -0
- package/dist/src/providers/local-mock.js +162 -35
- package/dist/src/providers/local-mock.js.map +1 -1
- package/dist/src/providers/native-ids.d.ts +2 -0
- package/dist/src/providers/native-ids.js +3 -0
- package/dist/src/providers/native-ids.js.map +1 -1
- package/dist/src/providers/recorder.d.ts +14 -3
- package/dist/src/providers/recorder.js +521 -41
- package/dist/src/providers/recorder.js.map +1 -1
- package/dist/src/providers/registry.d.ts +1 -0
- package/dist/src/providers/registry.js +65 -20
- package/dist/src/providers/registry.js.map +1 -1
- package/dist/src/providers/signed-jwt.d.ts +6 -1
- package/dist/src/providers/signed-jwt.js +241 -50
- package/dist/src/providers/signed-jwt.js.map +1 -1
- package/dist/src/providers/slack-ids.d.ts +1 -0
- package/dist/src/providers/slack-ids.js +5 -0
- package/dist/src/providers/slack-ids.js.map +1 -1
- package/dist/src/providers/target-normalizers.d.ts +9 -1
- package/dist/src/providers/target-normalizers.js +177 -58
- package/dist/src/providers/target-normalizers.js.map +1 -1
- package/dist/src/providers/webhook-server.d.ts +3 -0
- package/dist/src/providers/webhook-server.js +268 -47
- package/dist/src/providers/webhook-server.js.map +1 -1
- package/dist/src/servers/http.d.ts +14 -2
- package/dist/src/servers/http.js +229 -58
- package/dist/src/servers/http.js.map +1 -1
- package/dist/src/servers/matrix.d.ts +3 -0
- package/dist/src/servers/matrix.js +268 -110
- package/dist/src/servers/matrix.js.map +1 -1
- package/dist/src/servers/mattermost.d.ts +5 -0
- package/dist/src/servers/mattermost.js +389 -91
- package/dist/src/servers/mattermost.js.map +1 -1
- package/dist/src/servers/recorder.d.ts +11 -0
- package/dist/src/servers/recorder.js +1072 -69
- package/dist/src/servers/recorder.js.map +1 -1
- package/dist/src/servers/signal.d.ts +1 -0
- package/dist/src/servers/signal.js +279 -44
- package/dist/src/servers/signal.js.map +1 -1
- package/dist/src/servers/slack.d.ts +15 -0
- package/dist/src/servers/slack.js +259 -51
- package/dist/src/servers/slack.js.map +1 -1
- package/dist/src/servers/telegram-identity.d.ts +6 -0
- package/dist/src/servers/telegram-identity.js +27 -0
- package/dist/src/servers/telegram-identity.js.map +1 -0
- package/dist/src/servers/telegram.d.ts +17 -6
- package/dist/src/servers/telegram.js +1169 -107
- package/dist/src/servers/telegram.js.map +1 -1
- package/dist/src/servers/webhook-target.d.ts +12 -4
- package/dist/src/servers/webhook-target.js +226 -28
- package/dist/src/servers/webhook-target.js.map +1 -1
- package/dist/src/servers/whatsapp-baileys-websocket.d.ts +60 -1
- package/dist/src/servers/whatsapp-baileys-websocket.js +782 -112
- package/dist/src/servers/whatsapp-baileys-websocket.js.map +1 -1
- package/dist/src/servers/whatsapp-jid.d.ts +1 -0
- package/dist/src/servers/whatsapp-jid.js +9 -5
- package/dist/src/servers/whatsapp-jid.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/binary-node.d.ts +1 -0
- package/dist/src/servers/whatsapp-wire/binary-node.js +37 -13
- package/dist/src/servers/whatsapp-wire/binary-node.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/crypto.d.ts +3 -0
- package/dist/src/servers/whatsapp-wire/crypto.js +23 -8
- package/dist/src/servers/whatsapp-wire/crypto.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/handshake.d.ts +9 -3
- package/dist/src/servers/whatsapp-wire/handshake.js +173 -12
- package/dist/src/servers/whatsapp-wire/handshake.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/signal.js +3 -3
- package/dist/src/servers/whatsapp-wire/signal.js.map +1 -1
- package/dist/src/servers/whatsapp.d.ts +7 -0
- package/dist/src/servers/whatsapp.js +149 -24
- package/dist/src/servers/whatsapp.js.map +1 -1
- package/dist/src/servers/zalo.d.ts +1 -0
- package/dist/src/servers/zalo.js +193 -82
- package/dist/src/servers/zalo.js.map +1 -1
- package/docs/channel-setup.md +185 -43
- package/fixtures/examples/crabline.example.yaml +3 -2
- package/package.json +18 -10
- package/fixtures/examples/openclaw-bridge.yaml +0 -125
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
|
-
import { readFileSync } from "node:fs";
|
|
3
|
+
import fsSync, { readFileSync } from "node:fs";
|
|
4
4
|
import fs, {} from "node:fs/promises";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { performance } from "node:perf_hooks";
|
|
@@ -8,10 +8,12 @@ import { isCrablineServerChannel } from "../servers/index.js";
|
|
|
8
8
|
import { resolveWindowsPowerShellPath, securePrivateDirectory } from "./private-file.js";
|
|
9
9
|
import { OPENCLAW_CRABLINE_MANIFEST_PATH } from "./shared.js";
|
|
10
10
|
const LOCK_OWNER_FILE = "owner.json";
|
|
11
|
-
const LOCK_COMMIT_STAGE_FILE_PREFIX = "commit-stage";
|
|
12
11
|
const LOCK_LEASE_FILE_PREFIX = "lease.";
|
|
12
|
+
const LOCK_COMMIT_STAGE_FILE_PREFIX = "commit-stage.";
|
|
13
|
+
const LOCK_STAGED_FILE_PREFIX = ".commit-file.";
|
|
13
14
|
const LEGACY_RECOVERY_SUFFIX = ".recovering";
|
|
14
15
|
const COMMIT_CLAIM_SUFFIX = ".commit";
|
|
16
|
+
const OWNED_CLAIM_SUFFIX = ".owned";
|
|
15
17
|
const RELEASE_CLAIM_SUFFIX = ".release";
|
|
16
18
|
const LOCK_LEASE_MS = 10 * 60 * 1000;
|
|
17
19
|
const RELEASE_ATTEMPTS = 3;
|
|
@@ -26,58 +28,6 @@ export function processStartedAtMsFromTimeOrigin(timeOrigin) {
|
|
|
26
28
|
}
|
|
27
29
|
return Math.trunc(timeOrigin);
|
|
28
30
|
}
|
|
29
|
-
function parseCommitStagePath(contents, token) {
|
|
30
|
-
let value;
|
|
31
|
-
try {
|
|
32
|
-
value = JSON.parse(contents);
|
|
33
|
-
}
|
|
34
|
-
catch (error) {
|
|
35
|
-
throw new Error("OpenClaw Crabline smoke lock commit stage metadata is malformed.", {
|
|
36
|
-
cause: error,
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
const expectedPrefix = `.commit-file.${token}.`;
|
|
40
|
-
if (value.token !== token ||
|
|
41
|
-
typeof value.path !== "string" ||
|
|
42
|
-
!path.isAbsolute(value.path) ||
|
|
43
|
-
!path.basename(value.path).startsWith(expectedPrefix) ||
|
|
44
|
-
!path.basename(value.path).endsWith(".tmp")) {
|
|
45
|
-
throw new Error("OpenClaw Crabline smoke lock commit stage metadata is malformed.");
|
|
46
|
-
}
|
|
47
|
-
return value.path;
|
|
48
|
-
}
|
|
49
|
-
async function readCommitStagePath(lockDirectory, token) {
|
|
50
|
-
try {
|
|
51
|
-
return parseCommitStagePath(await fs.readFile(path.join(lockDirectory, commitStageFileName(token)), "utf8"), token);
|
|
52
|
-
}
|
|
53
|
-
catch (error) {
|
|
54
|
-
if (isMissingPathError(error)) {
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
throw error;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
function commitStageFileName(token) {
|
|
61
|
-
return `${LOCK_COMMIT_STAGE_FILE_PREFIX}.${token}.json`;
|
|
62
|
-
}
|
|
63
|
-
async function writeCommitStagePath(lockDirectory, stagedFilePath, token) {
|
|
64
|
-
const temporaryPath = path.join(lockDirectory, `.${commitStageFileName(token)}.${randomUUID()}.tmp`);
|
|
65
|
-
try {
|
|
66
|
-
await fs.writeFile(temporaryPath, `${JSON.stringify({ path: stagedFilePath, token })}\n`, {
|
|
67
|
-
encoding: "utf8",
|
|
68
|
-
flag: "wx",
|
|
69
|
-
mode: 0o600,
|
|
70
|
-
});
|
|
71
|
-
await fs.chmod(temporaryPath, 0o600);
|
|
72
|
-
await fs.rename(temporaryPath, path.join(lockDirectory, commitStageFileName(token)));
|
|
73
|
-
}
|
|
74
|
-
finally {
|
|
75
|
-
await fs.rm(temporaryPath, { force: true }).catch(() => undefined);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
const removeLockDirectory = async (lockDirectory) => {
|
|
79
|
-
await fs.rm(lockDirectory, { force: true, recursive: true });
|
|
80
|
-
};
|
|
81
31
|
const sleep = async (delayMs) => {
|
|
82
32
|
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
83
33
|
};
|
|
@@ -153,9 +103,98 @@ async function readDirectoryIdentity(directoryPath) {
|
|
|
153
103
|
throw error;
|
|
154
104
|
}
|
|
155
105
|
}
|
|
106
|
+
async function readFileIdentity(filePath) {
|
|
107
|
+
try {
|
|
108
|
+
const stats = await fs.lstat(filePath, { bigint: true });
|
|
109
|
+
if (!stats.isFile() || stats.nlink !== 1n || stats.ino <= 0n) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
device: stats.dev,
|
|
114
|
+
inode: stats.ino,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
if (isMissingPathError(error)) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
156
124
|
function hasSameDirectoryIdentity(left, right) {
|
|
157
125
|
return (left !== null && right !== null && left.device === right.device && left.inode === right.inode);
|
|
158
126
|
}
|
|
127
|
+
function isOwnedLockArtifactName(name, token) {
|
|
128
|
+
const leaseName = leaseFileName(token);
|
|
129
|
+
const commitStageName = `${LOCK_COMMIT_STAGE_FILE_PREFIX}${token}.json`;
|
|
130
|
+
return (name === LOCK_OWNER_FILE ||
|
|
131
|
+
name === leaseName ||
|
|
132
|
+
(name.startsWith(`.${leaseName}.`) && name.endsWith(".tmp")) ||
|
|
133
|
+
name === commitStageName ||
|
|
134
|
+
(name.startsWith(`.${LOCK_COMMIT_STAGE_FILE_PREFIX}${token}.`) && name.endsWith(".tmp")) ||
|
|
135
|
+
(name.startsWith(`${LOCK_STAGED_FILE_PREFIX}${token}.`) && name.endsWith(".tmp")));
|
|
136
|
+
}
|
|
137
|
+
async function removeVerifiedLockDirectory(params) {
|
|
138
|
+
if (!hasSameDirectoryIdentity(params.expectedIdentity, await readDirectoryIdentity(params.directoryPath))) {
|
|
139
|
+
throw new Error("OpenClaw Crabline smoke lock cleanup target changed.");
|
|
140
|
+
}
|
|
141
|
+
await params.beforeRemove?.();
|
|
142
|
+
const quarantinePath = `${params.directoryPath}.cleanup.${randomUUID()}`;
|
|
143
|
+
fsSync.renameSync(params.directoryPath, quarantinePath);
|
|
144
|
+
let quarantinedIdentity = null;
|
|
145
|
+
const stats = fsSync.lstatSync(quarantinePath, { bigint: true });
|
|
146
|
+
if (stats.isDirectory() && !stats.isSymbolicLink() && stats.ino > 0n) {
|
|
147
|
+
quarantinedIdentity = { device: stats.dev, inode: stats.ino };
|
|
148
|
+
}
|
|
149
|
+
if (!hasSameDirectoryIdentity(params.expectedIdentity, quarantinedIdentity)) {
|
|
150
|
+
throw new Error("OpenClaw Crabline smoke lock cleanup target changed.");
|
|
151
|
+
}
|
|
152
|
+
const entries = fsSync.readdirSync(quarantinePath);
|
|
153
|
+
const artifacts = [];
|
|
154
|
+
for (const entry of entries) {
|
|
155
|
+
if (!isOwnedLockArtifactName(entry, params.token)) {
|
|
156
|
+
throw new Error("OpenClaw Crabline smoke lock cleanup found an unexpected artifact.");
|
|
157
|
+
}
|
|
158
|
+
const artifactPath = path.join(quarantinePath, entry);
|
|
159
|
+
const artifactStats = fsSync.lstatSync(artifactPath, { bigint: true });
|
|
160
|
+
if (!artifactStats.isFile() ||
|
|
161
|
+
artifactStats.isSymbolicLink() ||
|
|
162
|
+
artifactStats.nlink !== 1n ||
|
|
163
|
+
artifactStats.ino <= 0n) {
|
|
164
|
+
throw new Error("OpenClaw Crabline smoke lock cleanup found an unsafe artifact.");
|
|
165
|
+
}
|
|
166
|
+
artifacts.push({
|
|
167
|
+
identity: { device: artifactStats.dev, inode: artifactStats.ino },
|
|
168
|
+
path: artifactPath,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
artifacts.sort((left, right) => Number(path.basename(left.path) === LOCK_OWNER_FILE) -
|
|
172
|
+
Number(path.basename(right.path) === LOCK_OWNER_FILE));
|
|
173
|
+
for (const artifact of artifacts) {
|
|
174
|
+
const directoryStats = fsSync.lstatSync(quarantinePath, { bigint: true });
|
|
175
|
+
const artifactStats = fsSync.lstatSync(artifact.path, { bigint: true });
|
|
176
|
+
if (!directoryStats.isDirectory() ||
|
|
177
|
+
directoryStats.isSymbolicLink() ||
|
|
178
|
+
directoryStats.dev !== params.expectedIdentity.device ||
|
|
179
|
+
directoryStats.ino !== params.expectedIdentity.inode ||
|
|
180
|
+
!artifactStats.isFile() ||
|
|
181
|
+
artifactStats.isSymbolicLink() ||
|
|
182
|
+
artifactStats.nlink !== 1n ||
|
|
183
|
+
artifactStats.dev !== artifact.identity.device ||
|
|
184
|
+
artifactStats.ino !== artifact.identity.inode) {
|
|
185
|
+
throw new Error("OpenClaw Crabline smoke lock cleanup artifact changed.");
|
|
186
|
+
}
|
|
187
|
+
fsSync.unlinkSync(artifact.path);
|
|
188
|
+
}
|
|
189
|
+
const finalStats = fsSync.lstatSync(quarantinePath, { bigint: true });
|
|
190
|
+
if (!finalStats.isDirectory() ||
|
|
191
|
+
finalStats.isSymbolicLink() ||
|
|
192
|
+
finalStats.dev !== params.expectedIdentity.device ||
|
|
193
|
+
finalStats.ino !== params.expectedIdentity.inode) {
|
|
194
|
+
throw new Error("OpenClaw Crabline smoke lock cleanup target changed.");
|
|
195
|
+
}
|
|
196
|
+
fsSync.rmdirSync(quarantinePath);
|
|
197
|
+
}
|
|
159
198
|
function isPositiveSafeInteger(value) {
|
|
160
199
|
return Number.isSafeInteger(value) && Number(value) > 0;
|
|
161
200
|
}
|
|
@@ -164,15 +203,19 @@ function isValidProcessId(value) {
|
|
|
164
203
|
}
|
|
165
204
|
const LOCK_TOKEN_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u;
|
|
166
205
|
function parseLockOwner(contents) {
|
|
167
|
-
let
|
|
206
|
+
let parsed;
|
|
168
207
|
try {
|
|
169
|
-
|
|
208
|
+
parsed = JSON.parse(contents);
|
|
170
209
|
}
|
|
171
210
|
catch (error) {
|
|
172
211
|
throw new Error("OpenClaw Crabline smoke lock owner metadata is malformed.", {
|
|
173
212
|
cause: error,
|
|
174
213
|
});
|
|
175
214
|
}
|
|
215
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
216
|
+
throw new Error("OpenClaw Crabline smoke lock owner metadata is malformed.");
|
|
217
|
+
}
|
|
218
|
+
const owner = parsed;
|
|
176
219
|
if (typeof owner.channel !== "string" ||
|
|
177
220
|
!isCrablineServerChannel(owner.channel) ||
|
|
178
221
|
!isValidProcessId(owner.pid) ||
|
|
@@ -194,7 +237,11 @@ function parseLockOwner(contents) {
|
|
|
194
237
|
(owner.processIdentity !== undefined &&
|
|
195
238
|
(typeof owner.processIdentity !== "string" ||
|
|
196
239
|
owner.processIdentity.length === 0 ||
|
|
197
|
-
owner.processIdentity.length > 256))
|
|
240
|
+
owner.processIdentity.length > 256)) ||
|
|
241
|
+
(owner.processIdentityV2 !== undefined &&
|
|
242
|
+
(typeof owner.processIdentityV2 !== "string" ||
|
|
243
|
+
owner.processIdentityV2.length === 0 ||
|
|
244
|
+
owner.processIdentityV2.length > 256))) {
|
|
198
245
|
throw new Error("OpenClaw Crabline smoke lock owner metadata is malformed.");
|
|
199
246
|
}
|
|
200
247
|
if (owner.leaseVersion === undefined) {
|
|
@@ -203,6 +250,7 @@ function parseLockOwner(contents) {
|
|
|
203
250
|
createdAtMs: owner.createdAtMs,
|
|
204
251
|
pid: owner.pid,
|
|
205
252
|
...(owner.processIdentity ? { processIdentity: owner.processIdentity } : {}),
|
|
253
|
+
...(owner.processIdentityV2 ? { processIdentityV2: owner.processIdentityV2 } : {}),
|
|
206
254
|
processStartedAtMs: owner.processStartedAtMs,
|
|
207
255
|
token: owner.token,
|
|
208
256
|
};
|
|
@@ -216,6 +264,7 @@ function parseLockOwner(contents) {
|
|
|
216
264
|
leaseVersion: owner.leaseVersion,
|
|
217
265
|
pid: owner.pid,
|
|
218
266
|
...(owner.processIdentity ? { processIdentity: owner.processIdentity } : {}),
|
|
267
|
+
...(owner.processIdentityV2 ? { processIdentityV2: owner.processIdentityV2 } : {}),
|
|
219
268
|
processStartedAtMs: owner.processStartedAtMs,
|
|
220
269
|
token: owner.token,
|
|
221
270
|
};
|
|
@@ -303,6 +352,17 @@ export function processIdentityFromLinuxStat(value, bootId) {
|
|
|
303
352
|
}
|
|
304
353
|
export function processIdentityFromDarwin(processStartedAt, bootTime) {
|
|
305
354
|
const bootMatch = /\bsec = (\d+), usec = (\d+)\b/u.exec(bootTime);
|
|
355
|
+
const launchMatch = /^Launch Time:\s*(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})\.(\d{3,6}) ([+-])(\d{2})(\d{2})$/mu.exec(processStartedAt);
|
|
356
|
+
if (bootMatch && launchMatch) {
|
|
357
|
+
const [, year, month, day, hour, minute, second, fraction, sign, offsetHour, offsetMinute] = launchMatch;
|
|
358
|
+
const offsetMs = (Number(offsetHour) * 60 + Number(offsetMinute)) * 60_000 * (sign === "+" ? 1 : -1);
|
|
359
|
+
const fractionMicros = Number(fraction.padEnd(6, "0"));
|
|
360
|
+
const utcMilliseconds = Date.UTC(Number(year), Number(month) - 1, Number(day), Number(hour), Number(minute), Number(second), Math.floor(fractionMicros / 1_000)) - offsetMs;
|
|
361
|
+
if (Number.isSafeInteger(utcMilliseconds) && utcMilliseconds > 0) {
|
|
362
|
+
const startedAtMicros = BigInt(utcMilliseconds) * 1000n + BigInt(fractionMicros % 1_000);
|
|
363
|
+
return `darwin:${bootMatch[1]}.${bootMatch[2]}:us:${startedAtMicros}`;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
306
366
|
const normalizedStartedAt = processStartedAt.trim().replace(/\s+/gu, " ");
|
|
307
367
|
if (!bootMatch || normalizedStartedAt.length === 0 || normalizedStartedAt.length > 64) {
|
|
308
368
|
return null;
|
|
@@ -325,11 +385,15 @@ const getProcessIdentity = (pid) => {
|
|
|
325
385
|
const options = {
|
|
326
386
|
encoding: "utf8",
|
|
327
387
|
env: darwinProcessIdentityEnvironment(process.env),
|
|
388
|
+
maxBuffer: 512 * 1024,
|
|
328
389
|
timeout: 1_000,
|
|
329
390
|
};
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
391
|
+
const bootTime = spawnSync("/usr/sbin/sysctl", ["-n", "kern.boottime"], options);
|
|
392
|
+
if (bootTime.status !== 0) {
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
const startedAt = spawnSync("/bin/ps", ["-o", "lstart=", "-p", String(pid)], options);
|
|
396
|
+
return startedAt.status === 0
|
|
333
397
|
? processIdentityFromDarwin(startedAt.stdout, bootTime.stdout)
|
|
334
398
|
: null;
|
|
335
399
|
}
|
|
@@ -352,32 +416,85 @@ const getProcessIdentity = (pid) => {
|
|
|
352
416
|
const ticks = result.status === 0 ? result.stdout.trim() : "";
|
|
353
417
|
return /^\d+$/u.test(ticks) ? `windows:${ticks}` : null;
|
|
354
418
|
};
|
|
419
|
+
const getProcessIdentityV2 = (pid) => {
|
|
420
|
+
if (process.platform !== "darwin") {
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
const options = {
|
|
424
|
+
encoding: "utf8",
|
|
425
|
+
env: darwinProcessIdentityEnvironment(process.env),
|
|
426
|
+
maxBuffer: 512 * 1024,
|
|
427
|
+
timeout: 3_000,
|
|
428
|
+
};
|
|
429
|
+
const bootTime = spawnSync("/usr/sbin/sysctl", ["-n", "kern.boottime"], options);
|
|
430
|
+
const processDetails = spawnSync("/usr/bin/vmmap", ["-summary", String(pid)], options);
|
|
431
|
+
if (bootTime.status !== 0 || processDetails.status !== 0) {
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
const identity = processIdentityFromDarwin(processDetails.stdout, bootTime.stdout);
|
|
435
|
+
return identity?.includes(":us:") ? identity : null;
|
|
436
|
+
};
|
|
437
|
+
let cachedCurrentProcessIdentity = null;
|
|
438
|
+
let cachedCurrentProcessIdentityV2 = null;
|
|
439
|
+
function getCachedCurrentProcessIdentity() {
|
|
440
|
+
return (cachedCurrentProcessIdentity ??= getProcessIdentity(process.pid));
|
|
441
|
+
}
|
|
442
|
+
function getCachedCurrentProcessIdentityV2() {
|
|
443
|
+
return (cachedCurrentProcessIdentityV2 ??= getProcessIdentityV2(process.pid));
|
|
444
|
+
}
|
|
355
445
|
function hasExactProcessIdentity(owner) {
|
|
356
446
|
return hasProcessIdentity(owner) && typeof owner.processIdentity === "string";
|
|
357
447
|
}
|
|
358
|
-
function
|
|
448
|
+
function hasCoarseDarwinProcessIdentity(owner) {
|
|
449
|
+
if (!hasExactProcessIdentity(owner)) {
|
|
450
|
+
return false;
|
|
451
|
+
}
|
|
452
|
+
return /^darwin:\d+\.\d+:(?!us:).+$/u.test(owner.processIdentity);
|
|
453
|
+
}
|
|
454
|
+
function compareProcessIdentity(owner, runtime) {
|
|
359
455
|
if (hasProcessIdentity(owner) &&
|
|
360
456
|
owner.pid === runtime.currentPid &&
|
|
361
457
|
owner.processStartedAtMs !== runtime.currentProcessStartedAtMs) {
|
|
362
|
-
return
|
|
458
|
+
return "mismatch";
|
|
363
459
|
}
|
|
364
460
|
if (!hasExactProcessIdentity(owner)) {
|
|
365
|
-
return
|
|
461
|
+
return "unknown";
|
|
462
|
+
}
|
|
463
|
+
if (hasProcessIdentity(owner) && typeof owner.processIdentityV2 === "string") {
|
|
464
|
+
const actualProcessIdentityV2 = owner.pid === runtime.currentPid
|
|
465
|
+
? runtime.currentProcessIdentityV2
|
|
466
|
+
: runtime.getProcessIdentityV2(owner.pid);
|
|
467
|
+
if (actualProcessIdentityV2 !== null) {
|
|
468
|
+
return owner.processIdentityV2 === actualProcessIdentityV2 ? "v2" : "mismatch";
|
|
469
|
+
}
|
|
366
470
|
}
|
|
367
471
|
const actualProcessIdentity = owner.pid === runtime.currentPid
|
|
368
472
|
? runtime.currentProcessIdentity
|
|
369
473
|
: runtime.getProcessIdentity(owner.pid);
|
|
370
|
-
|
|
474
|
+
if (actualProcessIdentity === null) {
|
|
475
|
+
return "unknown";
|
|
476
|
+
}
|
|
477
|
+
return owner.processIdentity === actualProcessIdentity ? "legacy" : "mismatch";
|
|
478
|
+
}
|
|
479
|
+
function hasProcessIdentityMismatch(owner, runtime) {
|
|
480
|
+
return compareProcessIdentity(owner, runtime) === "mismatch";
|
|
371
481
|
}
|
|
372
482
|
function isLockOwnerActive(record, runtime) {
|
|
373
483
|
const { owner } = record;
|
|
374
484
|
if (!runtime.isProcessAlive(owner.pid)) {
|
|
375
485
|
return false;
|
|
376
486
|
}
|
|
377
|
-
|
|
487
|
+
const identityMatch = compareProcessIdentity(owner, runtime);
|
|
488
|
+
if (identityMatch === "mismatch") {
|
|
378
489
|
return false;
|
|
379
490
|
}
|
|
380
491
|
if (!isRenewableOwner(owner)) {
|
|
492
|
+
if (owner.pid !== runtime.currentPid &&
|
|
493
|
+
hasCoarseDarwinProcessIdentity(owner) &&
|
|
494
|
+
identityMatch !== "v2") {
|
|
495
|
+
const ageMs = runtime.now() - Math.max(owner.createdAtMs, record.renewedAtMs);
|
|
496
|
+
return ageMs >= -runtime.leaseMs && ageMs <= runtime.leaseMs;
|
|
497
|
+
}
|
|
381
498
|
return true;
|
|
382
499
|
}
|
|
383
500
|
const ageMs = runtime.now() - Math.max(owner.createdAtMs, record.renewedAtMs);
|
|
@@ -389,11 +506,14 @@ function needsLiveOwnerConfirmation(record, runtime) {
|
|
|
389
506
|
!hasProcessIdentityMismatch(record.owner, runtime) &&
|
|
390
507
|
!isLockOwnerActive(record, runtime));
|
|
391
508
|
}
|
|
509
|
+
function heartbeatIntervalMs(leaseMs) {
|
|
510
|
+
return Math.max(1, Math.floor(leaseMs / 3));
|
|
511
|
+
}
|
|
392
512
|
async function confirmObservedOwner(lockDirectory, observed, runtime) {
|
|
393
513
|
if (!needsLiveOwnerConfirmation(observed, runtime)) {
|
|
394
514
|
return "unchanged";
|
|
395
515
|
}
|
|
396
|
-
await runtime.sleep(runtime.leaseMs);
|
|
516
|
+
await runtime.sleep(heartbeatIntervalMs(runtime.leaseMs));
|
|
397
517
|
const revalidated = await readLockRecord(lockDirectory);
|
|
398
518
|
if (revalidated.kind !== "record") {
|
|
399
519
|
return "unchanged";
|
|
@@ -409,7 +529,33 @@ async function confirmObservedOwner(lockDirectory, observed, runtime) {
|
|
|
409
529
|
function activeRunError(params) {
|
|
410
530
|
return new Error(`OpenClaw Crabline smoke is already running for channel "${params.channel ?? "unknown"}" in "${params.outputDir}"; cannot start channel "${params.requestedChannel}".`, params.cause === undefined ? undefined : { cause: params.cause });
|
|
411
531
|
}
|
|
532
|
+
function isPathConfinedTo(rootPath, candidatePath) {
|
|
533
|
+
const relative = path.relative(rootPath, candidatePath);
|
|
534
|
+
return (relative === "" ||
|
|
535
|
+
(!path.isAbsolute(relative) && relative !== ".." && !relative.startsWith(`..${path.sep}`)));
|
|
536
|
+
}
|
|
537
|
+
async function resolveConfinedPath(rootPath, candidatePath, description, candidateKind) {
|
|
538
|
+
const resolvedPath = path.resolve(candidatePath);
|
|
539
|
+
if (resolvedPath !== candidatePath) {
|
|
540
|
+
throw new Error(`OpenClaw Crabline smoke lock ${description} escapes its output directory.`);
|
|
541
|
+
}
|
|
542
|
+
const parentPath = candidateKind === "directory" ? resolvedPath : path.dirname(resolvedPath);
|
|
543
|
+
let realParentPath;
|
|
544
|
+
try {
|
|
545
|
+
realParentPath = await fs.realpath(parentPath);
|
|
546
|
+
}
|
|
547
|
+
catch {
|
|
548
|
+
throw new Error(`OpenClaw Crabline smoke lock ${description} is invalid.`);
|
|
549
|
+
}
|
|
550
|
+
if (!isPathConfinedTo(rootPath, realParentPath)) {
|
|
551
|
+
throw new Error(`OpenClaw Crabline smoke lock ${description} escapes its output directory.`);
|
|
552
|
+
}
|
|
553
|
+
return candidateKind === "directory"
|
|
554
|
+
? realParentPath
|
|
555
|
+
: path.join(realParentPath, path.basename(resolvedPath));
|
|
556
|
+
}
|
|
412
557
|
async function removeOwnedLock(params) {
|
|
558
|
+
await params.beforeClaim?.();
|
|
413
559
|
const observed = await readLockRecord(params.ownedDirectory);
|
|
414
560
|
if (observed.kind === "missing" || observed.record.owner.token !== params.token) {
|
|
415
561
|
return false;
|
|
@@ -418,7 +564,15 @@ async function removeOwnedLock(params) {
|
|
|
418
564
|
if (observedIdentity === null) {
|
|
419
565
|
return false;
|
|
420
566
|
}
|
|
421
|
-
await params.
|
|
567
|
+
const revalidated = await readLockRecord(params.ownedDirectory);
|
|
568
|
+
const revalidatedIdentity = await readDirectoryIdentity(params.ownedDirectory);
|
|
569
|
+
if (revalidated.kind === "missing" ||
|
|
570
|
+
revalidated.record.owner.token !== params.token ||
|
|
571
|
+
!hasSameDirectoryIdentity(observedIdentity, revalidatedIdentity) ||
|
|
572
|
+
(params.runtime !== undefined && isLockOwnerActive(revalidated.record, params.runtime))) {
|
|
573
|
+
return false;
|
|
574
|
+
}
|
|
575
|
+
await params.beforeRename?.();
|
|
422
576
|
const releaseClaim = `${params.lockDirectory}${RELEASE_CLAIM_SUFFIX}.${params.token}.${randomUUID()}`;
|
|
423
577
|
try {
|
|
424
578
|
await fs.rename(params.ownedDirectory, releaseClaim);
|
|
@@ -449,22 +603,62 @@ async function removeOwnedLock(params) {
|
|
|
449
603
|
}
|
|
450
604
|
return false;
|
|
451
605
|
}
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
}
|
|
463
|
-
}
|
|
606
|
+
if (params.removeDirectory) {
|
|
607
|
+
await params.removeDirectory(releaseClaim);
|
|
608
|
+
}
|
|
609
|
+
else {
|
|
610
|
+
await removeVerifiedLockDirectory({
|
|
611
|
+
...(params.beforeRemove ? { beforeRemove: params.beforeRemove } : {}),
|
|
612
|
+
directoryPath: releaseClaim,
|
|
613
|
+
expectedIdentity: observedIdentity,
|
|
614
|
+
token: params.token,
|
|
615
|
+
});
|
|
464
616
|
}
|
|
465
|
-
await (params.removeDirectory ?? removeLockDirectory)(releaseClaim);
|
|
466
617
|
return true;
|
|
467
618
|
}
|
|
619
|
+
async function renewCompatibilityMarker(params) {
|
|
620
|
+
await params.securedDirectory.assertIdentityAt(params.lockDirectory);
|
|
621
|
+
const observed = await readLockRecord(params.lockDirectory);
|
|
622
|
+
if (observed.kind === "missing" || observed.record.owner.token !== params.token) {
|
|
623
|
+
return false;
|
|
624
|
+
}
|
|
625
|
+
const renewedAt = new Date(params.renewedAtMs);
|
|
626
|
+
await params.handle.utimes(renewedAt, renewedAt);
|
|
627
|
+
await params.handle.sync();
|
|
628
|
+
await params.securedDirectory.assertIdentityAt(params.lockDirectory);
|
|
629
|
+
const revalidated = await readLockRecord(params.lockDirectory);
|
|
630
|
+
return revalidated.kind === "record" && revalidated.record.owner.token === params.token;
|
|
631
|
+
}
|
|
632
|
+
async function assertCompatibilityMarkerOwned(params) {
|
|
633
|
+
await params.securedDirectory.assertIdentityAt(params.lockDirectory);
|
|
634
|
+
const observed = await readLockRecord(params.lockDirectory);
|
|
635
|
+
if (observed.kind === "missing" || observed.record.owner.token !== params.token) {
|
|
636
|
+
throw new Error("OpenClaw Crabline smoke lock compatibility marker was lost.");
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
async function retireCompatibilityMarker(handle, owner) {
|
|
640
|
+
const retiredOwner = {
|
|
641
|
+
...owner,
|
|
642
|
+
createdAtMs: 1,
|
|
643
|
+
pid: MAX_PROCESS_ID,
|
|
644
|
+
...(owner.processIdentity ? { processIdentity: "retired" } : {}),
|
|
645
|
+
...(owner.processIdentityV2 ? { processIdentityV2: "retired" } : {}),
|
|
646
|
+
processStartedAtMs: 1,
|
|
647
|
+
};
|
|
648
|
+
const originalBytes = Buffer.byteLength(`${JSON.stringify(owner)}\n`, "utf8");
|
|
649
|
+
const retiredJson = JSON.stringify(retiredOwner);
|
|
650
|
+
const retiredBytes = Buffer.byteLength(retiredJson, "utf8") + 1;
|
|
651
|
+
const targetBytes = Math.max(originalBytes, retiredBytes);
|
|
652
|
+
const paddingBytes = targetBytes - retiredBytes;
|
|
653
|
+
const retiredContents = `${retiredJson}${" ".repeat(paddingBytes)}\n`;
|
|
654
|
+
const written = await handle.write(retiredContents, 0, "utf8");
|
|
655
|
+
if (written.bytesWritten !== targetBytes) {
|
|
656
|
+
throw new Error("OpenClaw Crabline smoke lock compatibility marker retirement was incomplete.");
|
|
657
|
+
}
|
|
658
|
+
const retiredAt = new Date(1);
|
|
659
|
+
await handle.utimes(retiredAt, retiredAt);
|
|
660
|
+
await handle.sync();
|
|
661
|
+
}
|
|
468
662
|
async function renewOwnedLock(lockDirectory, token, renewedAtMs) {
|
|
469
663
|
const temporaryLeasePath = path.join(lockDirectory, `.${leaseFileName(token)}.${randomUUID()}.tmp`);
|
|
470
664
|
try {
|
|
@@ -509,6 +703,7 @@ async function listLockClaims(lockDirectory) {
|
|
|
509
703
|
const prefixes = [
|
|
510
704
|
{ kind: "recovering", prefix: claimPrefix(lockDirectory, LEGACY_RECOVERY_SUFFIX) },
|
|
511
705
|
{ kind: "commit", prefix: claimPrefix(lockDirectory, COMMIT_CLAIM_SUFFIX) },
|
|
706
|
+
{ kind: "owned", prefix: claimPrefix(lockDirectory, OWNED_CLAIM_SUFFIX) },
|
|
512
707
|
{ kind: "release", prefix: claimPrefix(lockDirectory, RELEASE_CLAIM_SUFFIX) },
|
|
513
708
|
];
|
|
514
709
|
const entries = await fs.readdir(directory, { withFileTypes: true });
|
|
@@ -683,7 +878,8 @@ async function resolveLockContention(params) {
|
|
|
683
878
|
});
|
|
684
879
|
}
|
|
685
880
|
async function createLockCandidate(params) {
|
|
686
|
-
const candidateDirectory =
|
|
881
|
+
const candidateDirectory = params.candidateDirectory ??
|
|
882
|
+
`${params.lockDirectory}.${params.owner.pid}.${params.owner.token}.tmp`;
|
|
687
883
|
await fs.mkdir(candidateDirectory, { mode: 0o700 });
|
|
688
884
|
try {
|
|
689
885
|
const securedDirectory = await securePrivateDirectory(candidateDirectory, {
|
|
@@ -704,14 +900,16 @@ async function createLockCandidate(params) {
|
|
|
704
900
|
await fs.utimes(ownerPath, createdAt, createdAt);
|
|
705
901
|
await params.afterOwnerWrite?.(candidateDirectory);
|
|
706
902
|
await securedDirectory.assertIdentityAt();
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
903
|
+
if (params.includeLease !== false) {
|
|
904
|
+
const leasePath = path.join(candidateDirectory, leaseFileName(params.owner.token));
|
|
905
|
+
await fs.writeFile(leasePath, `${JSON.stringify({ renewedAtMs: params.owner.createdAtMs, token: params.owner.token })}\n`, {
|
|
906
|
+
encoding: "utf8",
|
|
907
|
+
flag: "wx",
|
|
908
|
+
mode: 0o600,
|
|
909
|
+
});
|
|
910
|
+
await fs.chmod(leasePath, 0o600);
|
|
911
|
+
await fs.utimes(leasePath, createdAt, createdAt);
|
|
912
|
+
}
|
|
715
913
|
await securedDirectory.assertIdentityAt();
|
|
716
914
|
return { candidateDirectory, securedDirectory };
|
|
717
915
|
}
|
|
@@ -721,18 +919,39 @@ async function createLockCandidate(params) {
|
|
|
721
919
|
}
|
|
722
920
|
}
|
|
723
921
|
export async function acquireOpenClawCrablineSmokeRunLock(params, dependencies = {}) {
|
|
724
|
-
const
|
|
922
|
+
const requestedOutputDir = path.resolve(params.outputDir);
|
|
923
|
+
await fs.mkdir(requestedOutputDir, { recursive: true });
|
|
924
|
+
const outputDir = await fs.realpath(requestedOutputDir);
|
|
925
|
+
const privateDirectoryOptions = {
|
|
926
|
+
...(dependencies.platform ? { platform: dependencies.platform } : {}),
|
|
927
|
+
...(dependencies.secureWindowsDirectory
|
|
928
|
+
? { secureWindowsDirectory: dependencies.secureWindowsDirectory }
|
|
929
|
+
: {}),
|
|
930
|
+
};
|
|
931
|
+
const securedOutputDirectory = await securePrivateDirectory(outputDir, privateDirectoryOptions);
|
|
932
|
+
await securedOutputDirectory.assertIdentityAt(outputDir);
|
|
933
|
+
const outputDirectoryIdentity = await readDirectoryIdentity(outputDir);
|
|
934
|
+
if (outputDirectoryIdentity === null) {
|
|
935
|
+
throw new Error("OpenClaw Crabline smoke lock output directory is invalid.");
|
|
936
|
+
}
|
|
725
937
|
const lockDirectory = path.join(outputDir, `.${OPENCLAW_CRABLINE_MANIFEST_PATH}.lock`);
|
|
726
938
|
const token = randomUUID();
|
|
727
939
|
const currentPid = dependencies.pid ?? process.pid;
|
|
728
|
-
const getRuntimeProcessIdentity = dependencies.getProcessIdentity ??
|
|
940
|
+
const getRuntimeProcessIdentity = dependencies.getProcessIdentity ??
|
|
941
|
+
((pid) => (pid === process.pid ? getCachedCurrentProcessIdentity() : getProcessIdentity(pid)));
|
|
942
|
+
const getRuntimeProcessIdentityV2 = dependencies.getProcessIdentityV2 ??
|
|
943
|
+
((pid) => pid === process.pid ? getCachedCurrentProcessIdentityV2() : getProcessIdentityV2(pid));
|
|
729
944
|
const runtime = {
|
|
730
945
|
currentPid,
|
|
731
946
|
currentProcessIdentity: dependencies.processIdentity === undefined
|
|
732
947
|
? getRuntimeProcessIdentity(currentPid)
|
|
733
948
|
: dependencies.processIdentity,
|
|
949
|
+
currentProcessIdentityV2: dependencies.processIdentityV2 === undefined
|
|
950
|
+
? getRuntimeProcessIdentityV2(currentPid)
|
|
951
|
+
: dependencies.processIdentityV2,
|
|
734
952
|
currentProcessStartedAtMs: dependencies.processStartedAtMs ?? CURRENT_PROCESS_STARTED_AT_MS,
|
|
735
953
|
getProcessIdentity: getRuntimeProcessIdentity,
|
|
954
|
+
getProcessIdentityV2: getRuntimeProcessIdentityV2,
|
|
736
955
|
isProcessAlive: dependencies.isProcessAlive ?? isProcessAlive,
|
|
737
956
|
leaseMs: dependencies.leaseMs ?? LOCK_LEASE_MS,
|
|
738
957
|
now: dependencies.now ?? Date.now,
|
|
@@ -743,6 +962,9 @@ export async function acquireOpenClawCrablineSmokeRunLock(params, dependencies =
|
|
|
743
962
|
(runtime.currentProcessIdentity !== null &&
|
|
744
963
|
(runtime.currentProcessIdentity.length === 0 ||
|
|
745
964
|
runtime.currentProcessIdentity.length > 256)) ||
|
|
965
|
+
(runtime.currentProcessIdentityV2 !== null &&
|
|
966
|
+
(runtime.currentProcessIdentityV2.length === 0 ||
|
|
967
|
+
runtime.currentProcessIdentityV2.length > 256)) ||
|
|
746
968
|
!isPositiveSafeInteger(runtime.currentProcessStartedAtMs) ||
|
|
747
969
|
!isPositiveSafeInteger(runtime.leaseMs) ||
|
|
748
970
|
!isPositiveSafeInteger(createdAtMs)) {
|
|
@@ -754,78 +976,196 @@ export async function acquireOpenClawCrablineSmokeRunLock(params, dependencies =
|
|
|
754
976
|
leaseVersion: 1,
|
|
755
977
|
pid: runtime.currentPid,
|
|
756
978
|
...(runtime.currentProcessIdentity ? { processIdentity: runtime.currentProcessIdentity } : {}),
|
|
979
|
+
...(runtime.currentProcessIdentityV2
|
|
980
|
+
? { processIdentityV2: runtime.currentProcessIdentityV2 }
|
|
981
|
+
: {}),
|
|
757
982
|
processStartedAtMs: runtime.currentProcessStartedAtMs,
|
|
758
983
|
token,
|
|
759
984
|
};
|
|
760
|
-
|
|
985
|
+
const compatibilityOwner = {
|
|
986
|
+
...owner,
|
|
987
|
+
createdAtMs: 1,
|
|
988
|
+
};
|
|
761
989
|
for (;;) {
|
|
762
990
|
const lockClaims = await listLockClaims(lockDirectory);
|
|
763
991
|
if (lockClaims.length > 0 || (await pathExists(`${lockDirectory}${LEGACY_RECOVERY_SUFFIX}`))) {
|
|
764
992
|
await resolveLockClaims({
|
|
765
993
|
beforeRecoveryDeleteClaim: dependencies.beforeRecoveryDeleteClaim ?? (async () => undefined),
|
|
766
994
|
lockDirectory,
|
|
767
|
-
outputDir,
|
|
995
|
+
outputDir: requestedOutputDir,
|
|
768
996
|
requestedChannel: params.channel,
|
|
769
997
|
runtime,
|
|
770
998
|
});
|
|
771
999
|
continue;
|
|
772
1000
|
}
|
|
1001
|
+
const ownerDirectory = `${lockDirectory}${OWNED_CLAIM_SUFFIX}.${token}`;
|
|
773
1002
|
const { candidateDirectory, securedDirectory } = await createLockCandidate({
|
|
774
1003
|
...(dependencies.afterLockOwnerWrite
|
|
775
1004
|
? { afterOwnerWrite: dependencies.afterLockOwnerWrite }
|
|
776
1005
|
: {}),
|
|
1006
|
+
includeLease: false,
|
|
777
1007
|
lockDirectory,
|
|
778
|
-
owner,
|
|
1008
|
+
owner: compatibilityOwner,
|
|
779
1009
|
...(dependencies.platform ? { platform: dependencies.platform } : {}),
|
|
780
1010
|
...(dependencies.secureWindowsDirectory
|
|
781
1011
|
? { secureWindowsDirectory: dependencies.secureWindowsDirectory }
|
|
782
1012
|
: {}),
|
|
783
1013
|
});
|
|
1014
|
+
let ownerCandidate;
|
|
1015
|
+
try {
|
|
1016
|
+
ownerCandidate = await createLockCandidate({
|
|
1017
|
+
candidateDirectory: `${lockDirectory}.${runtime.currentPid}.${token}.owned.tmp`,
|
|
1018
|
+
lockDirectory: ownerDirectory,
|
|
1019
|
+
owner,
|
|
1020
|
+
...(dependencies.platform ? { platform: dependencies.platform } : {}),
|
|
1021
|
+
...(dependencies.secureWindowsDirectory
|
|
1022
|
+
? { secureWindowsDirectory: dependencies.secureWindowsDirectory }
|
|
1023
|
+
: {}),
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
catch (error) {
|
|
1027
|
+
await fs.rm(candidateDirectory, { force: true, recursive: true }).catch(() => undefined);
|
|
1028
|
+
throw error;
|
|
1029
|
+
}
|
|
1030
|
+
let compatibilityMarkerHandle;
|
|
1031
|
+
try {
|
|
1032
|
+
compatibilityMarkerHandle = await fs.open(path.join(candidateDirectory, LOCK_OWNER_FILE), "r+");
|
|
1033
|
+
const compatibilityRecord = await readLockRecordFromHandle(compatibilityMarkerHandle);
|
|
1034
|
+
if (compatibilityRecord.owner.token !== token) {
|
|
1035
|
+
throw new Error("OpenClaw Crabline smoke lock compatibility marker was replaced.");
|
|
1036
|
+
}
|
|
1037
|
+
const activeAt = new Date(createdAtMs);
|
|
1038
|
+
await compatibilityMarkerHandle.utimes(activeAt, activeAt);
|
|
1039
|
+
await compatibilityMarkerHandle.sync();
|
|
1040
|
+
}
|
|
1041
|
+
catch (error) {
|
|
1042
|
+
await compatibilityMarkerHandle?.close().catch(() => undefined);
|
|
1043
|
+
await fs.rm(candidateDirectory, { force: true, recursive: true }).catch(() => undefined);
|
|
1044
|
+
await fs
|
|
1045
|
+
.rm(ownerCandidate.candidateDirectory, { force: true, recursive: true })
|
|
1046
|
+
.catch(() => undefined);
|
|
1047
|
+
throw error;
|
|
1048
|
+
}
|
|
1049
|
+
if (!compatibilityMarkerHandle) {
|
|
1050
|
+
throw new Error("OpenClaw Crabline smoke lock compatibility marker was not opened.");
|
|
1051
|
+
}
|
|
1052
|
+
const markerHandle = compatibilityMarkerHandle;
|
|
784
1053
|
try {
|
|
785
1054
|
await fs.rename(candidateDirectory, lockDirectory);
|
|
786
1055
|
}
|
|
787
1056
|
catch (error) {
|
|
1057
|
+
await markerHandle.close().catch(() => undefined);
|
|
788
1058
|
await fs.rm(candidateDirectory, { force: true, recursive: true }).catch(() => undefined);
|
|
1059
|
+
await fs
|
|
1060
|
+
.rm(ownerCandidate.candidateDirectory, { force: true, recursive: true })
|
|
1061
|
+
.catch(() => undefined);
|
|
789
1062
|
await resolveLockContention({
|
|
790
1063
|
beforeRecoveryDeleteClaim: dependencies.beforeRecoveryDeleteClaim ?? (async () => undefined),
|
|
791
1064
|
cause: error,
|
|
792
1065
|
lockDirectory,
|
|
793
|
-
outputDir,
|
|
1066
|
+
outputDir: requestedOutputDir,
|
|
794
1067
|
requestedChannel: params.channel,
|
|
795
1068
|
runtime,
|
|
796
1069
|
beforeRecoveryClaim: dependencies.beforeRecoveryClaim ?? (async () => undefined),
|
|
797
1070
|
});
|
|
798
1071
|
continue;
|
|
799
1072
|
}
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
1073
|
+
let ownerInstalled = false;
|
|
1074
|
+
try {
|
|
1075
|
+
await dependencies.afterLockCandidateInstall?.(lockDirectory);
|
|
1076
|
+
await securedDirectory.assertIdentityAt(lockDirectory);
|
|
1077
|
+
const installed = await readLockRecord(lockDirectory);
|
|
1078
|
+
if (installed.kind !== "record" || installed.record.owner.token !== token) {
|
|
1079
|
+
await retireCompatibilityMarker(markerHandle, compatibilityOwner);
|
|
1080
|
+
await markerHandle.close();
|
|
1081
|
+
await fs.rm(ownerCandidate.candidateDirectory, { force: true, recursive: true });
|
|
1082
|
+
continue;
|
|
1083
|
+
}
|
|
1084
|
+
await fs.rename(ownerCandidate.candidateDirectory, ownerDirectory);
|
|
1085
|
+
ownerInstalled = true;
|
|
1086
|
+
await dependencies.afterLockOwnerClaimInstall?.(ownerDirectory);
|
|
1087
|
+
await ownerCandidate.securedDirectory.assertIdentityAt(ownerDirectory);
|
|
1088
|
+
}
|
|
1089
|
+
catch (error) {
|
|
1090
|
+
let cleanupError;
|
|
1091
|
+
try {
|
|
1092
|
+
if (ownerInstalled) {
|
|
1093
|
+
await removeOwnedLock({
|
|
1094
|
+
lockDirectory,
|
|
1095
|
+
ownedDirectory: ownerDirectory,
|
|
1096
|
+
token,
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
else {
|
|
1100
|
+
await fs.rm(ownerCandidate.candidateDirectory, { force: true, recursive: true });
|
|
1101
|
+
}
|
|
1102
|
+
await retireCompatibilityMarker(markerHandle, compatibilityOwner);
|
|
1103
|
+
}
|
|
1104
|
+
catch (caughtCleanupError) {
|
|
1105
|
+
cleanupError = caughtCleanupError;
|
|
1106
|
+
}
|
|
1107
|
+
try {
|
|
1108
|
+
await markerHandle.close();
|
|
1109
|
+
}
|
|
1110
|
+
catch (closeError) {
|
|
1111
|
+
cleanupError =
|
|
1112
|
+
cleanupError === undefined
|
|
1113
|
+
? closeError
|
|
1114
|
+
: new AggregateError([cleanupError, closeError], "OpenClaw Crabline smoke lock setup cleanup also failed.");
|
|
1115
|
+
}
|
|
1116
|
+
if (cleanupError !== undefined) {
|
|
1117
|
+
const aggregateError = new AggregateError([error, cleanupError], "OpenClaw Crabline smoke lock setup and cleanup failed.");
|
|
1118
|
+
aggregateError.cause = error;
|
|
1119
|
+
throw aggregateError;
|
|
1120
|
+
}
|
|
1121
|
+
throw error;
|
|
1122
|
+
}
|
|
1123
|
+
const competingClaims = (await listLockClaims(lockDirectory)).filter((claim) => claim.directoryPath !== ownerDirectory);
|
|
1124
|
+
if (competingClaims.length > 0 ||
|
|
803
1125
|
(await pathExists(`${lockDirectory}${LEGACY_RECOVERY_SUFFIX}`))) {
|
|
804
|
-
await removeOwnedLock({
|
|
1126
|
+
const removed = await removeOwnedLock({
|
|
805
1127
|
lockDirectory,
|
|
806
|
-
ownedDirectory:
|
|
1128
|
+
ownedDirectory: ownerDirectory,
|
|
807
1129
|
token,
|
|
808
1130
|
});
|
|
1131
|
+
if (!removed) {
|
|
1132
|
+
await markerHandle.close();
|
|
1133
|
+
throw new Error("OpenClaw Crabline smoke lock ownership was lost.");
|
|
1134
|
+
}
|
|
1135
|
+
try {
|
|
1136
|
+
await retireCompatibilityMarker(markerHandle, compatibilityOwner);
|
|
1137
|
+
}
|
|
1138
|
+
finally {
|
|
1139
|
+
await markerHandle.close();
|
|
1140
|
+
}
|
|
809
1141
|
await resolveLockClaims({
|
|
810
1142
|
beforeRecoveryDeleteClaim: dependencies.beforeRecoveryDeleteClaim ?? (async () => undefined),
|
|
811
1143
|
lockDirectory,
|
|
812
|
-
outputDir,
|
|
1144
|
+
outputDir: requestedOutputDir,
|
|
813
1145
|
requestedChannel: params.channel,
|
|
814
1146
|
runtime,
|
|
815
1147
|
});
|
|
816
1148
|
continue;
|
|
817
1149
|
}
|
|
818
|
-
|
|
819
|
-
if (installed.kind !== "record" || installed.record.owner.token !== token) {
|
|
820
|
-
continue;
|
|
821
|
-
}
|
|
822
|
-
let ownedDirectory = lockDirectory;
|
|
1150
|
+
let ownedDirectory = ownerDirectory;
|
|
823
1151
|
let clockRegressed = false;
|
|
824
1152
|
let lastObservedNowMs = createdAtMs;
|
|
825
1153
|
let lastRenewedAtMs = createdAtMs;
|
|
826
1154
|
let pendingRenewal = Promise.resolve();
|
|
827
1155
|
let commitFenceConsumed = false;
|
|
1156
|
+
let markerClosed = false;
|
|
1157
|
+
let markerRetired = false;
|
|
828
1158
|
let heartbeat;
|
|
1159
|
+
const retireMarker = async () => {
|
|
1160
|
+
if (!markerRetired) {
|
|
1161
|
+
await retireCompatibilityMarker(markerHandle, compatibilityOwner);
|
|
1162
|
+
markerRetired = true;
|
|
1163
|
+
}
|
|
1164
|
+
if (!markerClosed) {
|
|
1165
|
+
await markerHandle.close();
|
|
1166
|
+
markerClosed = true;
|
|
1167
|
+
}
|
|
1168
|
+
};
|
|
829
1169
|
const renew = async () => {
|
|
830
1170
|
const renewal = pendingRenewal
|
|
831
1171
|
.catch(() => undefined)
|
|
@@ -845,122 +1185,235 @@ export async function acquireOpenClawCrablineSmokeRunLock(params, dependencies =
|
|
|
845
1185
|
if (!(await renewOwnedLock(ownedDirectory, token, renewedAtMs))) {
|
|
846
1186
|
throw new Error("OpenClaw Crabline smoke lock ownership was lost.");
|
|
847
1187
|
}
|
|
1188
|
+
await dependencies.beforeCompatibilityMarkerRenew?.();
|
|
1189
|
+
if (!(await renewCompatibilityMarker({
|
|
1190
|
+
handle: markerHandle,
|
|
1191
|
+
lockDirectory,
|
|
1192
|
+
renewedAtMs,
|
|
1193
|
+
securedDirectory,
|
|
1194
|
+
token,
|
|
1195
|
+
}))) {
|
|
1196
|
+
throw new Error("OpenClaw Crabline smoke lock compatibility marker was lost.");
|
|
1197
|
+
}
|
|
848
1198
|
lastRenewedAtMs = renewedAtMs;
|
|
849
1199
|
});
|
|
850
1200
|
pendingRenewal = renewal;
|
|
851
1201
|
await renewal;
|
|
852
1202
|
};
|
|
853
1203
|
try {
|
|
854
|
-
heartbeat = (dependencies.startHeartbeat ?? startHeartbeat)(renew,
|
|
1204
|
+
heartbeat = (dependencies.startHeartbeat ?? startHeartbeat)(renew, heartbeatIntervalMs(runtime.leaseMs));
|
|
855
1205
|
}
|
|
856
1206
|
catch (error) {
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
1207
|
+
try {
|
|
1208
|
+
await removeOwnedLock({
|
|
1209
|
+
lockDirectory,
|
|
1210
|
+
ownedDirectory: ownerDirectory,
|
|
1211
|
+
token,
|
|
1212
|
+
});
|
|
1213
|
+
await retireMarker();
|
|
1214
|
+
}
|
|
1215
|
+
catch (cleanupError) {
|
|
1216
|
+
const aggregateError = new AggregateError([error, cleanupError], "OpenClaw Crabline smoke lock heartbeat startup and cleanup failed.");
|
|
1217
|
+
aggregateError.cause = error;
|
|
1218
|
+
throw aggregateError;
|
|
1219
|
+
}
|
|
862
1220
|
throw error;
|
|
863
1221
|
}
|
|
864
1222
|
let heartbeatStopped = false;
|
|
865
1223
|
let released = false;
|
|
866
1224
|
return {
|
|
867
1225
|
async assertOwned() {
|
|
868
|
-
if (released) {
|
|
1226
|
+
if (released || heartbeatStopped) {
|
|
869
1227
|
throw new Error("OpenClaw Crabline smoke lock has already been released.");
|
|
870
1228
|
}
|
|
871
1229
|
heartbeat.assertHealthy();
|
|
872
1230
|
await renew();
|
|
873
1231
|
heartbeat.assertHealthy();
|
|
1232
|
+
await assertCompatibilityMarkerOwned({
|
|
1233
|
+
lockDirectory,
|
|
1234
|
+
securedDirectory,
|
|
1235
|
+
token,
|
|
1236
|
+
});
|
|
874
1237
|
},
|
|
875
1238
|
async commitFileAtomically({ contents, destinationPath, stageDirectory, stageFile }) {
|
|
876
1239
|
if (released) {
|
|
877
1240
|
throw new Error("OpenClaw Crabline smoke lock has already been released.");
|
|
878
1241
|
}
|
|
879
|
-
if (commitFenceConsumed || heartbeatStopped || ownedDirectory !==
|
|
1242
|
+
if (commitFenceConsumed || heartbeatStopped || ownedDirectory !== ownerDirectory) {
|
|
880
1243
|
throw new Error("OpenClaw Crabline smoke lock has already committed its fence.");
|
|
881
1244
|
}
|
|
882
1245
|
heartbeat.assertHealthy();
|
|
883
1246
|
await renew();
|
|
884
1247
|
heartbeat.assertHealthy();
|
|
885
|
-
const
|
|
886
|
-
const
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
await
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
await dependencies.beforeCommitClaim?.();
|
|
904
|
-
const commitClaim = `${lockDirectory}${COMMIT_CLAIM_SUFFIX}.${token}.${randomUUID()}`;
|
|
1248
|
+
const resolvedDestinationPath = await resolveConfinedPath(outputDir, destinationPath, "commit destination", "file");
|
|
1249
|
+
const destinationDirectory = path.dirname(resolvedDestinationPath);
|
|
1250
|
+
const securedDestinationDirectory = destinationDirectory === outputDir
|
|
1251
|
+
? securedOutputDirectory
|
|
1252
|
+
: await securePrivateDirectory(destinationDirectory, privateDirectoryOptions);
|
|
1253
|
+
await securedDestinationDirectory.assertIdentityAt(destinationDirectory);
|
|
1254
|
+
const destinationDirectoryIdentity = await readDirectoryIdentity(destinationDirectory);
|
|
1255
|
+
if (destinationDirectoryIdentity === null) {
|
|
1256
|
+
throw new Error("OpenClaw Crabline smoke lock commit destination is invalid.");
|
|
1257
|
+
}
|
|
1258
|
+
if (destinationDirectoryIdentity.device !== outputDirectoryIdentity.device) {
|
|
1259
|
+
throw new Error("OpenClaw Crabline smoke lock commit destination is on another filesystem.");
|
|
1260
|
+
}
|
|
1261
|
+
const resolvedStageDirectory = stageDirectory
|
|
1262
|
+
? await resolveConfinedPath(outputDir, stageDirectory, "commit stage directory", "directory")
|
|
1263
|
+
: ownerDirectory;
|
|
1264
|
+
let stageDirectoryIdentity = null;
|
|
1265
|
+
if (stageDirectory) {
|
|
905
1266
|
try {
|
|
906
|
-
await
|
|
1267
|
+
stageDirectoryIdentity = await readDirectoryIdentity(resolvedStageDirectory);
|
|
907
1268
|
}
|
|
908
1269
|
catch (error) {
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
});
|
|
913
|
-
}
|
|
914
|
-
throw error;
|
|
1270
|
+
throw new Error("OpenClaw Crabline smoke lock commit stage directory is invalid.", {
|
|
1271
|
+
cause: error,
|
|
1272
|
+
});
|
|
915
1273
|
}
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
1274
|
+
}
|
|
1275
|
+
if (stageDirectory && stageDirectoryIdentity === null) {
|
|
1276
|
+
throw new Error("OpenClaw Crabline smoke lock commit stage directory is invalid.");
|
|
1277
|
+
}
|
|
1278
|
+
if (stageDirectoryIdentity !== null &&
|
|
1279
|
+
stageDirectoryIdentity.device !== outputDirectoryIdentity.device) {
|
|
1280
|
+
throw new Error("OpenClaw Crabline smoke lock commit stage directory is on another filesystem.");
|
|
1281
|
+
}
|
|
1282
|
+
const stagedFileName = `.commit-file.${token}.${randomUUID()}.tmp`;
|
|
1283
|
+
const stagedFilePath = path.join(ownerDirectory, stagedFileName);
|
|
1284
|
+
await stageFile(stagedFilePath, contents);
|
|
1285
|
+
if (stageDirectory) {
|
|
1286
|
+
if (!hasSameDirectoryIdentity(stageDirectoryIdentity, await readDirectoryIdentity(resolvedStageDirectory))) {
|
|
1287
|
+
throw new Error("OpenClaw Crabline smoke lock commit stage directory identity changed.");
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
const stagedFileIdentity = await readFileIdentity(stagedFilePath);
|
|
1291
|
+
if (stagedFileIdentity === null) {
|
|
1292
|
+
throw new Error("OpenClaw Crabline smoke lock commit stage file is invalid.");
|
|
1293
|
+
}
|
|
1294
|
+
heartbeat.assertHealthy();
|
|
1295
|
+
await renew();
|
|
1296
|
+
heartbeat.assertHealthy();
|
|
1297
|
+
await dependencies.beforeCommitClaim?.();
|
|
1298
|
+
await assertCompatibilityMarkerOwned({
|
|
1299
|
+
lockDirectory,
|
|
1300
|
+
securedDirectory,
|
|
1301
|
+
token,
|
|
1302
|
+
});
|
|
1303
|
+
const commitClaim = `${lockDirectory}${COMMIT_CLAIM_SUFFIX}.${token}.${randomUUID()}`;
|
|
1304
|
+
const observedIdentity = await readDirectoryIdentity(ownerDirectory);
|
|
1305
|
+
const observed = await readLockRecord(ownerDirectory);
|
|
1306
|
+
if (observedIdentity === null ||
|
|
1307
|
+
observed.kind === "missing" ||
|
|
1308
|
+
observed.record.owner.token !== token) {
|
|
1309
|
+
throw new Error("OpenClaw Crabline smoke lock ownership was lost.");
|
|
1310
|
+
}
|
|
1311
|
+
try {
|
|
1312
|
+
await fs.rename(ownerDirectory, commitClaim);
|
|
1313
|
+
}
|
|
1314
|
+
catch (error) {
|
|
1315
|
+
if (isMissingPathError(error)) {
|
|
1316
|
+
throw new Error("OpenClaw Crabline smoke lock ownership was lost.", {
|
|
1317
|
+
cause: error,
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
1320
|
+
throw error;
|
|
1321
|
+
}
|
|
1322
|
+
ownedDirectory = commitClaim;
|
|
1323
|
+
commitFenceConsumed = true;
|
|
1324
|
+
const claimed = await readLockRecord(commitClaim);
|
|
1325
|
+
const claimedIdentity = await readDirectoryIdentity(commitClaim);
|
|
1326
|
+
if (claimed.kind === "missing" ||
|
|
1327
|
+
claimed.record.owner.token !== token ||
|
|
1328
|
+
!hasSameDirectoryIdentity(observedIdentity, claimedIdentity)) {
|
|
1329
|
+
if (!(await pathExists(ownerDirectory))) {
|
|
1330
|
+
try {
|
|
1331
|
+
await fs.rename(commitClaim, ownerDirectory);
|
|
1332
|
+
ownedDirectory = ownerDirectory;
|
|
1333
|
+
}
|
|
1334
|
+
catch (error) {
|
|
1335
|
+
if (!isMissingPathError(error)) {
|
|
1336
|
+
throw error;
|
|
932
1337
|
}
|
|
933
1338
|
}
|
|
934
|
-
throw new Error("OpenClaw Crabline smoke lock commit fence was lost.");
|
|
935
1339
|
}
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
1340
|
+
throw new Error("OpenClaw Crabline smoke lock commit fence was lost.");
|
|
1341
|
+
}
|
|
1342
|
+
await dependencies.beforeCommitFileRename?.();
|
|
1343
|
+
heartbeat.assertHealthy();
|
|
1344
|
+
await renew();
|
|
1345
|
+
heartbeat.assertHealthy();
|
|
1346
|
+
await heartbeat.settle();
|
|
1347
|
+
heartbeat.assertHealthy();
|
|
1348
|
+
await dependencies.beforeCommitRename?.();
|
|
1349
|
+
await assertCompatibilityMarkerOwned({
|
|
1350
|
+
lockDirectory,
|
|
1351
|
+
securedDirectory,
|
|
1352
|
+
token,
|
|
1353
|
+
});
|
|
1354
|
+
const claimedStagedFilePath = path.join(commitClaim, stagedFileName);
|
|
1355
|
+
if (stageDirectory) {
|
|
1356
|
+
const currentDirectoryIdentity = await readDirectoryIdentity(resolvedStageDirectory);
|
|
1357
|
+
const currentFileIdentity = await readFileIdentity(claimedStagedFilePath);
|
|
1358
|
+
if (!hasSameDirectoryIdentity(stageDirectoryIdentity, currentDirectoryIdentity) ||
|
|
1359
|
+
!hasSameDirectoryIdentity(stagedFileIdentity, currentFileIdentity)) {
|
|
1360
|
+
const error = new Error("OpenClaw Crabline smoke lock commit stage identity changed.");
|
|
1361
|
+
Object.assign(error, { code: "ENOENT", path: claimedStagedFilePath });
|
|
1362
|
+
throw error;
|
|
949
1363
|
}
|
|
950
1364
|
}
|
|
1365
|
+
else if (!hasSameDirectoryIdentity(stagedFileIdentity, await readFileIdentity(claimedStagedFilePath))) {
|
|
1366
|
+
throw new Error("OpenClaw Crabline smoke lock commit stage identity changed.");
|
|
1367
|
+
}
|
|
1368
|
+
if (!hasSameDirectoryIdentity(outputDirectoryIdentity, await readDirectoryIdentity(outputDir))) {
|
|
1369
|
+
throw new Error("OpenClaw Crabline smoke lock output directory identity changed.");
|
|
1370
|
+
}
|
|
1371
|
+
await securedOutputDirectory.assertIdentityAt(outputDir);
|
|
1372
|
+
let currentDestinationDirectoryIdentity = null;
|
|
1373
|
+
try {
|
|
1374
|
+
currentDestinationDirectoryIdentity = await readDirectoryIdentity(destinationDirectory);
|
|
1375
|
+
}
|
|
1376
|
+
catch {
|
|
1377
|
+
// A replaced directory or symlink is an identity mismatch.
|
|
1378
|
+
}
|
|
1379
|
+
if (!hasSameDirectoryIdentity(destinationDirectoryIdentity, currentDestinationDirectoryIdentity)) {
|
|
1380
|
+
throw new Error("OpenClaw Crabline smoke lock commit destination directory identity changed.");
|
|
1381
|
+
}
|
|
1382
|
+
await securedDestinationDirectory.assertIdentityAt(destinationDirectory);
|
|
1383
|
+
const revalidatedDestinationPath = await resolveConfinedPath(outputDir, resolvedDestinationPath, "commit destination", "file");
|
|
1384
|
+
if (revalidatedDestinationPath !== resolvedDestinationPath) {
|
|
1385
|
+
throw new Error("OpenClaw Crabline smoke lock commit destination path identity changed.");
|
|
1386
|
+
}
|
|
1387
|
+
// The owner-only output tree and smoke lock serialize supported writers; confinement does
|
|
1388
|
+
// not treat another process running as the same OS user as a lower-privilege adversary.
|
|
1389
|
+
await fs.rename(claimedStagedFilePath, revalidatedDestinationPath);
|
|
951
1390
|
},
|
|
952
1391
|
async release() {
|
|
953
1392
|
if (released) {
|
|
954
1393
|
return;
|
|
955
1394
|
}
|
|
956
1395
|
if (!heartbeatStopped) {
|
|
957
|
-
await heartbeat.stop();
|
|
958
1396
|
heartbeatStopped = true;
|
|
1397
|
+
try {
|
|
1398
|
+
await heartbeat.stop();
|
|
1399
|
+
}
|
|
1400
|
+
finally {
|
|
1401
|
+
await pendingRenewal.catch(() => undefined);
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
else {
|
|
1405
|
+
await pendingRenewal.catch(() => undefined);
|
|
959
1406
|
}
|
|
960
1407
|
await removeOwnedLock({
|
|
961
1408
|
...(dependencies.beforeReleaseClaim
|
|
962
1409
|
? { beforeClaim: dependencies.beforeReleaseClaim }
|
|
963
1410
|
: {}),
|
|
1411
|
+
...(dependencies.beforeReleaseRename
|
|
1412
|
+
? { beforeRename: dependencies.beforeReleaseRename }
|
|
1413
|
+
: {}),
|
|
1414
|
+
...(dependencies.beforeReleaseRemove
|
|
1415
|
+
? { beforeRemove: dependencies.beforeReleaseRemove }
|
|
1416
|
+
: {}),
|
|
964
1417
|
lockDirectory,
|
|
965
1418
|
onOwnedDirectoryChange: (directoryPath) => {
|
|
966
1419
|
ownedDirectory = directoryPath;
|
|
@@ -982,6 +1435,7 @@ export async function acquireOpenClawCrablineSmokeRunLock(params, dependencies =
|
|
|
982
1435
|
token,
|
|
983
1436
|
});
|
|
984
1437
|
}
|
|
1438
|
+
await retireMarker();
|
|
985
1439
|
released = true;
|
|
986
1440
|
},
|
|
987
1441
|
};
|