@openclaw/crabline 0.1.10 → 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 +12 -2
- package/dist/src/openclaw/artifact-generation.js +436 -15
- 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 +40 -3
- package/dist/src/openclaw/private-file.js +2070 -95
- 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 +696 -187
- package/dist/src/openclaw/smoke-lock.js.map +1 -1
- package/dist/src/openclaw.js +352 -29
- 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 +532 -42
- 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 +1241 -22
- 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 +1193 -118
- 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,21 +506,56 @@ function needsLiveOwnerConfirmation(record, runtime) {
|
|
|
389
506
|
!hasProcessIdentityMismatch(record.owner, runtime) &&
|
|
390
507
|
!isLockOwnerActive(record, runtime));
|
|
391
508
|
}
|
|
392
|
-
|
|
509
|
+
function heartbeatIntervalMs(leaseMs) {
|
|
510
|
+
return Math.max(1, Math.floor(leaseMs / 3));
|
|
511
|
+
}
|
|
512
|
+
async function confirmObservedOwner(lockDirectory, observed, runtime) {
|
|
393
513
|
if (!needsLiveOwnerConfirmation(observed, runtime)) {
|
|
394
|
-
return
|
|
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
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
518
|
+
if (revalidated.kind !== "record") {
|
|
519
|
+
return "unchanged";
|
|
520
|
+
}
|
|
521
|
+
if (revalidated.record.owner.token !== observed.owner.token) {
|
|
522
|
+
return "changed";
|
|
523
|
+
}
|
|
524
|
+
return revalidated.record.renewedAtMs !== observed.renewedAtMs ||
|
|
525
|
+
isLockOwnerActive(revalidated.record, runtime)
|
|
526
|
+
? "renewed"
|
|
527
|
+
: "unchanged";
|
|
402
528
|
}
|
|
403
529
|
function activeRunError(params) {
|
|
404
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 });
|
|
405
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
|
+
}
|
|
406
557
|
async function removeOwnedLock(params) {
|
|
558
|
+
await params.beforeClaim?.();
|
|
407
559
|
const observed = await readLockRecord(params.ownedDirectory);
|
|
408
560
|
if (observed.kind === "missing" || observed.record.owner.token !== params.token) {
|
|
409
561
|
return false;
|
|
@@ -412,7 +564,15 @@ async function removeOwnedLock(params) {
|
|
|
412
564
|
if (observedIdentity === null) {
|
|
413
565
|
return false;
|
|
414
566
|
}
|
|
415
|
-
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?.();
|
|
416
576
|
const releaseClaim = `${params.lockDirectory}${RELEASE_CLAIM_SUFFIX}.${params.token}.${randomUUID()}`;
|
|
417
577
|
try {
|
|
418
578
|
await fs.rename(params.ownedDirectory, releaseClaim);
|
|
@@ -443,22 +603,62 @@ async function removeOwnedLock(params) {
|
|
|
443
603
|
}
|
|
444
604
|
return false;
|
|
445
605
|
}
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
}
|
|
457
|
-
}
|
|
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
|
+
});
|
|
458
616
|
}
|
|
459
|
-
await (params.removeDirectory ?? removeLockDirectory)(releaseClaim);
|
|
460
617
|
return true;
|
|
461
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
|
+
}
|
|
462
662
|
async function renewOwnedLock(lockDirectory, token, renewedAtMs) {
|
|
463
663
|
const temporaryLeasePath = path.join(lockDirectory, `.${leaseFileName(token)}.${randomUUID()}.tmp`);
|
|
464
664
|
try {
|
|
@@ -503,6 +703,7 @@ async function listLockClaims(lockDirectory) {
|
|
|
503
703
|
const prefixes = [
|
|
504
704
|
{ kind: "recovering", prefix: claimPrefix(lockDirectory, LEGACY_RECOVERY_SUFFIX) },
|
|
505
705
|
{ kind: "commit", prefix: claimPrefix(lockDirectory, COMMIT_CLAIM_SUFFIX) },
|
|
706
|
+
{ kind: "owned", prefix: claimPrefix(lockDirectory, OWNED_CLAIM_SUFFIX) },
|
|
506
707
|
{ kind: "release", prefix: claimPrefix(lockDirectory, RELEASE_CLAIM_SUFFIX) },
|
|
507
708
|
];
|
|
508
709
|
const entries = await fs.readdir(directory, { withFileTypes: true });
|
|
@@ -581,7 +782,25 @@ async function resolveLockClaim(params) {
|
|
|
581
782
|
requestedChannel: params.requestedChannel,
|
|
582
783
|
});
|
|
583
784
|
}
|
|
584
|
-
|
|
785
|
+
const confirmation = await confirmObservedOwner(params.claim.directoryPath, observed.record, params.runtime);
|
|
786
|
+
if (confirmation === "renewed") {
|
|
787
|
+
if (params.claim.kind === "recovering") {
|
|
788
|
+
try {
|
|
789
|
+
await fs.rename(params.claim.directoryPath, params.lockDirectory);
|
|
790
|
+
}
|
|
791
|
+
catch (error) {
|
|
792
|
+
if (!(await pathExists(params.lockDirectory))) {
|
|
793
|
+
throw error;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
throw activeRunError({
|
|
798
|
+
channel: observed.record.owner.channel,
|
|
799
|
+
outputDir: params.outputDir,
|
|
800
|
+
requestedChannel: params.requestedChannel,
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
if (confirmation === "changed") {
|
|
585
804
|
await resolveLockClaim(params);
|
|
586
805
|
return;
|
|
587
806
|
}
|
|
@@ -624,10 +843,19 @@ async function resolveLockContention(params) {
|
|
|
624
843
|
requestedChannel: params.requestedChannel,
|
|
625
844
|
});
|
|
626
845
|
}
|
|
627
|
-
if (observed.kind === "record"
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
846
|
+
if (observed.kind === "record") {
|
|
847
|
+
const confirmation = await confirmObservedOwner(params.lockDirectory, observed.record, params.runtime);
|
|
848
|
+
if (confirmation === "renewed") {
|
|
849
|
+
throw activeRunError({
|
|
850
|
+
channel: observed.record.owner.channel,
|
|
851
|
+
outputDir: params.outputDir,
|
|
852
|
+
requestedChannel: params.requestedChannel,
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
if (confirmation === "changed") {
|
|
856
|
+
await resolveLockContention(params);
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
631
859
|
}
|
|
632
860
|
await params.beforeRecoveryClaim();
|
|
633
861
|
const claimDirectory = `${params.lockDirectory}${LEGACY_RECOVERY_SUFFIX}.${randomUUID()}`;
|
|
@@ -650,7 +878,8 @@ async function resolveLockContention(params) {
|
|
|
650
878
|
});
|
|
651
879
|
}
|
|
652
880
|
async function createLockCandidate(params) {
|
|
653
|
-
const candidateDirectory =
|
|
881
|
+
const candidateDirectory = params.candidateDirectory ??
|
|
882
|
+
`${params.lockDirectory}.${params.owner.pid}.${params.owner.token}.tmp`;
|
|
654
883
|
await fs.mkdir(candidateDirectory, { mode: 0o700 });
|
|
655
884
|
try {
|
|
656
885
|
const securedDirectory = await securePrivateDirectory(candidateDirectory, {
|
|
@@ -671,14 +900,16 @@ async function createLockCandidate(params) {
|
|
|
671
900
|
await fs.utimes(ownerPath, createdAt, createdAt);
|
|
672
901
|
await params.afterOwnerWrite?.(candidateDirectory);
|
|
673
902
|
await securedDirectory.assertIdentityAt();
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
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
|
+
}
|
|
682
913
|
await securedDirectory.assertIdentityAt();
|
|
683
914
|
return { candidateDirectory, securedDirectory };
|
|
684
915
|
}
|
|
@@ -688,18 +919,39 @@ async function createLockCandidate(params) {
|
|
|
688
919
|
}
|
|
689
920
|
}
|
|
690
921
|
export async function acquireOpenClawCrablineSmokeRunLock(params, dependencies = {}) {
|
|
691
|
-
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
|
+
}
|
|
692
937
|
const lockDirectory = path.join(outputDir, `.${OPENCLAW_CRABLINE_MANIFEST_PATH}.lock`);
|
|
693
938
|
const token = randomUUID();
|
|
694
939
|
const currentPid = dependencies.pid ?? process.pid;
|
|
695
|
-
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));
|
|
696
944
|
const runtime = {
|
|
697
945
|
currentPid,
|
|
698
946
|
currentProcessIdentity: dependencies.processIdentity === undefined
|
|
699
947
|
? getRuntimeProcessIdentity(currentPid)
|
|
700
948
|
: dependencies.processIdentity,
|
|
949
|
+
currentProcessIdentityV2: dependencies.processIdentityV2 === undefined
|
|
950
|
+
? getRuntimeProcessIdentityV2(currentPid)
|
|
951
|
+
: dependencies.processIdentityV2,
|
|
701
952
|
currentProcessStartedAtMs: dependencies.processStartedAtMs ?? CURRENT_PROCESS_STARTED_AT_MS,
|
|
702
953
|
getProcessIdentity: getRuntimeProcessIdentity,
|
|
954
|
+
getProcessIdentityV2: getRuntimeProcessIdentityV2,
|
|
703
955
|
isProcessAlive: dependencies.isProcessAlive ?? isProcessAlive,
|
|
704
956
|
leaseMs: dependencies.leaseMs ?? LOCK_LEASE_MS,
|
|
705
957
|
now: dependencies.now ?? Date.now,
|
|
@@ -710,6 +962,9 @@ export async function acquireOpenClawCrablineSmokeRunLock(params, dependencies =
|
|
|
710
962
|
(runtime.currentProcessIdentity !== null &&
|
|
711
963
|
(runtime.currentProcessIdentity.length === 0 ||
|
|
712
964
|
runtime.currentProcessIdentity.length > 256)) ||
|
|
965
|
+
(runtime.currentProcessIdentityV2 !== null &&
|
|
966
|
+
(runtime.currentProcessIdentityV2.length === 0 ||
|
|
967
|
+
runtime.currentProcessIdentityV2.length > 256)) ||
|
|
713
968
|
!isPositiveSafeInteger(runtime.currentProcessStartedAtMs) ||
|
|
714
969
|
!isPositiveSafeInteger(runtime.leaseMs) ||
|
|
715
970
|
!isPositiveSafeInteger(createdAtMs)) {
|
|
@@ -721,191 +976,444 @@ export async function acquireOpenClawCrablineSmokeRunLock(params, dependencies =
|
|
|
721
976
|
leaseVersion: 1,
|
|
722
977
|
pid: runtime.currentPid,
|
|
723
978
|
...(runtime.currentProcessIdentity ? { processIdentity: runtime.currentProcessIdentity } : {}),
|
|
979
|
+
...(runtime.currentProcessIdentityV2
|
|
980
|
+
? { processIdentityV2: runtime.currentProcessIdentityV2 }
|
|
981
|
+
: {}),
|
|
724
982
|
processStartedAtMs: runtime.currentProcessStartedAtMs,
|
|
725
983
|
token,
|
|
726
984
|
};
|
|
727
|
-
|
|
985
|
+
const compatibilityOwner = {
|
|
986
|
+
...owner,
|
|
987
|
+
createdAtMs: 1,
|
|
988
|
+
};
|
|
728
989
|
for (;;) {
|
|
729
990
|
const lockClaims = await listLockClaims(lockDirectory);
|
|
730
991
|
if (lockClaims.length > 0 || (await pathExists(`${lockDirectory}${LEGACY_RECOVERY_SUFFIX}`))) {
|
|
731
992
|
await resolveLockClaims({
|
|
732
993
|
beforeRecoveryDeleteClaim: dependencies.beforeRecoveryDeleteClaim ?? (async () => undefined),
|
|
733
994
|
lockDirectory,
|
|
734
|
-
outputDir,
|
|
995
|
+
outputDir: requestedOutputDir,
|
|
735
996
|
requestedChannel: params.channel,
|
|
736
997
|
runtime,
|
|
737
998
|
});
|
|
738
999
|
continue;
|
|
739
1000
|
}
|
|
1001
|
+
const ownerDirectory = `${lockDirectory}${OWNED_CLAIM_SUFFIX}.${token}`;
|
|
740
1002
|
const { candidateDirectory, securedDirectory } = await createLockCandidate({
|
|
741
1003
|
...(dependencies.afterLockOwnerWrite
|
|
742
1004
|
? { afterOwnerWrite: dependencies.afterLockOwnerWrite }
|
|
743
1005
|
: {}),
|
|
1006
|
+
includeLease: false,
|
|
744
1007
|
lockDirectory,
|
|
745
|
-
owner,
|
|
1008
|
+
owner: compatibilityOwner,
|
|
746
1009
|
...(dependencies.platform ? { platform: dependencies.platform } : {}),
|
|
747
1010
|
...(dependencies.secureWindowsDirectory
|
|
748
1011
|
? { secureWindowsDirectory: dependencies.secureWindowsDirectory }
|
|
749
1012
|
: {}),
|
|
750
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;
|
|
751
1053
|
try {
|
|
752
1054
|
await fs.rename(candidateDirectory, lockDirectory);
|
|
753
1055
|
}
|
|
754
1056
|
catch (error) {
|
|
1057
|
+
await markerHandle.close().catch(() => undefined);
|
|
755
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);
|
|
756
1062
|
await resolveLockContention({
|
|
757
1063
|
beforeRecoveryDeleteClaim: dependencies.beforeRecoveryDeleteClaim ?? (async () => undefined),
|
|
758
1064
|
cause: error,
|
|
759
1065
|
lockDirectory,
|
|
760
|
-
outputDir,
|
|
1066
|
+
outputDir: requestedOutputDir,
|
|
761
1067
|
requestedChannel: params.channel,
|
|
762
1068
|
runtime,
|
|
763
1069
|
beforeRecoveryClaim: dependencies.beforeRecoveryClaim ?? (async () => undefined),
|
|
764
1070
|
});
|
|
765
1071
|
continue;
|
|
766
1072
|
}
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
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 ||
|
|
770
1125
|
(await pathExists(`${lockDirectory}${LEGACY_RECOVERY_SUFFIX}`))) {
|
|
771
|
-
await removeOwnedLock({
|
|
1126
|
+
const removed = await removeOwnedLock({
|
|
772
1127
|
lockDirectory,
|
|
773
|
-
ownedDirectory:
|
|
1128
|
+
ownedDirectory: ownerDirectory,
|
|
774
1129
|
token,
|
|
775
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
|
+
}
|
|
776
1141
|
await resolveLockClaims({
|
|
777
1142
|
beforeRecoveryDeleteClaim: dependencies.beforeRecoveryDeleteClaim ?? (async () => undefined),
|
|
778
1143
|
lockDirectory,
|
|
779
|
-
outputDir,
|
|
1144
|
+
outputDir: requestedOutputDir,
|
|
780
1145
|
requestedChannel: params.channel,
|
|
781
1146
|
runtime,
|
|
782
1147
|
});
|
|
783
1148
|
continue;
|
|
784
1149
|
}
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
let
|
|
1150
|
+
let ownedDirectory = ownerDirectory;
|
|
1151
|
+
let clockRegressed = false;
|
|
1152
|
+
let lastObservedNowMs = createdAtMs;
|
|
1153
|
+
let lastRenewedAtMs = createdAtMs;
|
|
1154
|
+
let pendingRenewal = Promise.resolve();
|
|
790
1155
|
let commitFenceConsumed = false;
|
|
1156
|
+
let markerClosed = false;
|
|
1157
|
+
let markerRetired = false;
|
|
791
1158
|
let heartbeat;
|
|
792
|
-
const
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
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;
|
|
796
1167
|
}
|
|
797
1168
|
};
|
|
1169
|
+
const renew = async () => {
|
|
1170
|
+
const renewal = pendingRenewal
|
|
1171
|
+
.catch(() => undefined)
|
|
1172
|
+
.then(async () => {
|
|
1173
|
+
const nowMs = runtime.now();
|
|
1174
|
+
if (nowMs < lastObservedNowMs) {
|
|
1175
|
+
clockRegressed = true;
|
|
1176
|
+
}
|
|
1177
|
+
else if (clockRegressed && nowMs >= lastRenewedAtMs) {
|
|
1178
|
+
clockRegressed = false;
|
|
1179
|
+
}
|
|
1180
|
+
lastObservedNowMs = nowMs;
|
|
1181
|
+
const renewedAtMs = clockRegressed ? lastRenewedAtMs + 1 : nowMs;
|
|
1182
|
+
if (!Number.isSafeInteger(renewedAtMs)) {
|
|
1183
|
+
throw new Error("OpenClaw Crabline smoke lock renewal timestamp overflowed.");
|
|
1184
|
+
}
|
|
1185
|
+
if (!(await renewOwnedLock(ownedDirectory, token, renewedAtMs))) {
|
|
1186
|
+
throw new Error("OpenClaw Crabline smoke lock ownership was lost.");
|
|
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
|
+
}
|
|
1198
|
+
lastRenewedAtMs = renewedAtMs;
|
|
1199
|
+
});
|
|
1200
|
+
pendingRenewal = renewal;
|
|
1201
|
+
await renewal;
|
|
1202
|
+
};
|
|
798
1203
|
try {
|
|
799
|
-
heartbeat = (dependencies.startHeartbeat ?? startHeartbeat)(renew,
|
|
1204
|
+
heartbeat = (dependencies.startHeartbeat ?? startHeartbeat)(renew, heartbeatIntervalMs(runtime.leaseMs));
|
|
800
1205
|
}
|
|
801
1206
|
catch (error) {
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
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
|
+
}
|
|
807
1220
|
throw error;
|
|
808
1221
|
}
|
|
809
1222
|
let heartbeatStopped = false;
|
|
810
1223
|
let released = false;
|
|
811
1224
|
return {
|
|
812
1225
|
async assertOwned() {
|
|
813
|
-
if (released) {
|
|
1226
|
+
if (released || heartbeatStopped) {
|
|
814
1227
|
throw new Error("OpenClaw Crabline smoke lock has already been released.");
|
|
815
1228
|
}
|
|
816
1229
|
heartbeat.assertHealthy();
|
|
817
1230
|
await renew();
|
|
818
1231
|
heartbeat.assertHealthy();
|
|
1232
|
+
await assertCompatibilityMarkerOwned({
|
|
1233
|
+
lockDirectory,
|
|
1234
|
+
securedDirectory,
|
|
1235
|
+
token,
|
|
1236
|
+
});
|
|
819
1237
|
},
|
|
820
1238
|
async commitFileAtomically({ contents, destinationPath, stageDirectory, stageFile }) {
|
|
821
1239
|
if (released) {
|
|
822
1240
|
throw new Error("OpenClaw Crabline smoke lock has already been released.");
|
|
823
1241
|
}
|
|
824
|
-
if (commitFenceConsumed || heartbeatStopped || ownedDirectory !==
|
|
1242
|
+
if (commitFenceConsumed || heartbeatStopped || ownedDirectory !== ownerDirectory) {
|
|
825
1243
|
throw new Error("OpenClaw Crabline smoke lock has already committed its fence.");
|
|
826
1244
|
}
|
|
827
1245
|
heartbeat.assertHealthy();
|
|
828
1246
|
await renew();
|
|
829
1247
|
heartbeat.assertHealthy();
|
|
830
|
-
const
|
|
831
|
-
const
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
await
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
await dependencies.beforeCommitClaim?.();
|
|
849
|
-
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) {
|
|
850
1266
|
try {
|
|
851
|
-
await
|
|
1267
|
+
stageDirectoryIdentity = await readDirectoryIdentity(resolvedStageDirectory);
|
|
852
1268
|
}
|
|
853
1269
|
catch (error) {
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
});
|
|
858
|
-
}
|
|
859
|
-
throw error;
|
|
1270
|
+
throw new Error("OpenClaw Crabline smoke lock commit stage directory is invalid.", {
|
|
1271
|
+
cause: error,
|
|
1272
|
+
});
|
|
860
1273
|
}
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
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;
|
|
877
1337
|
}
|
|
878
1338
|
}
|
|
879
|
-
throw new Error("OpenClaw Crabline smoke lock commit fence was lost.");
|
|
880
1339
|
}
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
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;
|
|
894
1363
|
}
|
|
895
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);
|
|
896
1390
|
},
|
|
897
1391
|
async release() {
|
|
898
1392
|
if (released) {
|
|
899
1393
|
return;
|
|
900
1394
|
}
|
|
901
1395
|
if (!heartbeatStopped) {
|
|
902
|
-
await heartbeat.stop();
|
|
903
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);
|
|
904
1406
|
}
|
|
905
1407
|
await removeOwnedLock({
|
|
906
1408
|
...(dependencies.beforeReleaseClaim
|
|
907
1409
|
? { beforeClaim: dependencies.beforeReleaseClaim }
|
|
908
1410
|
: {}),
|
|
1411
|
+
...(dependencies.beforeReleaseRename
|
|
1412
|
+
? { beforeRename: dependencies.beforeReleaseRename }
|
|
1413
|
+
: {}),
|
|
1414
|
+
...(dependencies.beforeReleaseRemove
|
|
1415
|
+
? { beforeRemove: dependencies.beforeReleaseRemove }
|
|
1416
|
+
: {}),
|
|
909
1417
|
lockDirectory,
|
|
910
1418
|
onOwnedDirectoryChange: (directoryPath) => {
|
|
911
1419
|
ownedDirectory = directoryPath;
|
|
@@ -927,6 +1435,7 @@ export async function acquireOpenClawCrablineSmokeRunLock(params, dependencies =
|
|
|
927
1435
|
token,
|
|
928
1436
|
});
|
|
929
1437
|
}
|
|
1438
|
+
await retireMarker();
|
|
930
1439
|
released = true;
|
|
931
1440
|
},
|
|
932
1441
|
};
|