@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
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { isIP } from "node:net";
|
|
2
|
+
const MAX_MATRIX_IDENTIFIER_BYTES = 255;
|
|
3
|
+
function isMatrixIpv4Address(value) {
|
|
4
|
+
const octets = value.split(".");
|
|
5
|
+
return (octets.length === 4 && octets.every((octet) => /^\d{1,3}$/u.test(octet) && Number(octet) <= 255));
|
|
6
|
+
}
|
|
7
|
+
export function isMatrixServerName(value) {
|
|
8
|
+
const ipv6 = /^\[([^\]]+)\](?::(\d{1,5}))?$/u.exec(value);
|
|
9
|
+
if (ipv6) {
|
|
10
|
+
return isIP(ipv6[1]) === 6;
|
|
11
|
+
}
|
|
12
|
+
const hostAndPort = /^([^:]+?)(?::(\d{1,5}))?$/u.exec(value);
|
|
13
|
+
if (!hostAndPort) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
const hostname = hostAndPort[1];
|
|
17
|
+
if (isMatrixIpv4Address(hostname)) {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
if (/^\d+\.\d+\.\d+\.\d+$/u.test(hostname)) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
return hostname.length <= 255 && /^[A-Za-z0-9.-]+$/u.test(hostname);
|
|
24
|
+
}
|
|
25
|
+
function hasLoneSurrogate(value) {
|
|
26
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
27
|
+
const code = value.charCodeAt(index);
|
|
28
|
+
if (code >= 0xd800 && code <= 0xdbff) {
|
|
29
|
+
const next = value.charCodeAt(index + 1);
|
|
30
|
+
if (next < 0xdc00 || next > 0xdfff) {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
index += 1;
|
|
34
|
+
}
|
|
35
|
+
else if (code >= 0xdc00 && code <= 0xdfff) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
function isMatrixScopedIdentifier(value, sigil, allowEmptyLocalpart = false) {
|
|
42
|
+
const separator = value.indexOf(":");
|
|
43
|
+
const localpart = value.slice(1, separator);
|
|
44
|
+
return (value.startsWith(sigil) &&
|
|
45
|
+
Buffer.byteLength(value, "utf8") <= MAX_MATRIX_IDENTIFIER_BYTES &&
|
|
46
|
+
separator >= (allowEmptyLocalpart ? 1 : 2) &&
|
|
47
|
+
!localpart.includes("\0") &&
|
|
48
|
+
!hasLoneSurrogate(localpart) &&
|
|
49
|
+
isMatrixServerName(value.slice(separator + 1)));
|
|
50
|
+
}
|
|
51
|
+
function isMatrixHashIdentifier(value, sigil, allowLegacyBase64) {
|
|
52
|
+
if (!value.startsWith(sigil) || Buffer.byteLength(value, "utf8") > MAX_MATRIX_IDENTIFIER_BYTES) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
const opaqueId = value.slice(1);
|
|
56
|
+
const encoding = allowLegacyBase64 && /^[A-Za-z0-9+/]{43}$/u.test(opaqueId)
|
|
57
|
+
? "base64"
|
|
58
|
+
: /^[A-Za-z0-9_-]{43}$/u.test(opaqueId)
|
|
59
|
+
? "base64url"
|
|
60
|
+
: undefined;
|
|
61
|
+
if (!encoding) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
const decoded = Buffer.from(opaqueId, encoding);
|
|
65
|
+
return decoded.length === 32 && decoded.toString(encoding).replace(/=+$/u, "") === opaqueId;
|
|
66
|
+
}
|
|
67
|
+
export function isMatrixRoomId(value) {
|
|
68
|
+
return isMatrixScopedIdentifier(value, "!") || isMatrixHashIdentifier(value, "!", false);
|
|
69
|
+
}
|
|
70
|
+
export function isMatrixEventId(value) {
|
|
71
|
+
return isMatrixScopedIdentifier(value, "$") || isMatrixHashIdentifier(value, "$", true);
|
|
72
|
+
}
|
|
73
|
+
export function isMatrixUserId(value) {
|
|
74
|
+
if (!isMatrixScopedIdentifier(value, "@")) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
const localpart = value.slice(1, value.indexOf(":"));
|
|
78
|
+
return /^[a-z0-9._=/+-]+$/u.test(localpart);
|
|
79
|
+
}
|
|
80
|
+
export function isHistoricalMatrixUserId(value) {
|
|
81
|
+
return isMatrixScopedIdentifier(value, "@", true);
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=matrix-ids.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"matrix-ids.js","sourceRoot":"","sources":["../../src/matrix-ids.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAEhC,MAAM,2BAA2B,GAAG,GAAG,CAAC;AAExC,SAAS,mBAAmB,CAAC,KAAa;IACxC,MAAM,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAChC,OAAO,CACL,MAAM,CAAC,MAAM,KAAK,CAAC,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,MAAM,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC,CACjG,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,KAAa;IAC9C,MAAM,IAAI,GAAG,gCAAgC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC1D,IAAI,IAAI,EAAE,CAAC;QACT,OAAO,IAAI,CAAC,IAAI,CAAC,CAAC,CAAE,CAAC,KAAK,CAAC,CAAC;IAC9B,CAAC;IACD,MAAM,WAAW,GAAG,4BAA4B,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC7D,IAAI,CAAC,WAAW,EAAE,CAAC;QACjB,OAAO,KAAK,CAAC;IACf,CAAC;IACD,MAAM,QAAQ,GAAG,WAAW,CAAC,CAAC,CAAE,CAAC;IACjC,IAAI,mBAAmB,CAAC,QAAQ,CAAC,EAAE,CAAC;QAClC,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,uBAAuB,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC3C,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,QAAQ,CAAC,MAAM,IAAI,GAAG,IAAI,mBAAmB,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;AACtE,CAAC;AAED,SAAS,gBAAgB,CAAC,KAAa;IACrC,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC,MAAM,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC;QACrD,MAAM,IAAI,GAAG,KAAK,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;QACrC,IAAI,IAAI,IAAI,MAAM,IAAI,IAAI,IAAI,MAAM,EAAE,CAAC;YACrC,MAAM,IAAI,GAAG,KAAK,CAAC,UAAU,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;YACzC,IAAI,IAAI,GAAG,MAAM,IAAI,IAAI,GAAG,MAAM,EAAE,CAAC;gBACnC,OAAO,IAAI,CAAC;YACd,CAAC;YACD,KAAK,IAAI,CAAC,CAAC;QACb,CAAC;aAAM,IAAI,IAAI,IAAI,MAAM,IAAI,IAAI,IAAI,MAAM,EAAE,CAAC;YAC5C,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,wBAAwB,CAC/B,KAAa,EACb,KAAsB,EACtB,mBAAmB,GAAG,KAAK;IAE3B,MAAM,SAAS,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACrC,MAAM,SAAS,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC;IAC5C,OAAO,CACL,KAAK,CAAC,UAAU,CAAC,KAAK,CAAC;QACvB,MAAM,CAAC,UAAU,CAAC,KAAK,EAAE,MAAM,CAAC,IAAI,2BAA2B;QAC/D,SAAS,IAAI,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC1C,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC;QACzB,CAAC,gBAAgB,CAAC,SAAS,CAAC;QAC5B,kBAAkB,CAAC,KAAK,CAAC,KAAK,CAAC,SAAS,GAAG,CAAC,CAAC,CAAC,CAC/C,CAAC;AACJ,CAAC;AAED,SAAS,sBAAsB,CAC7B,KAAa,EACb,KAAgB,EAChB,iBAA0B;IAE1B,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,KAAK,CAAC,IAAI,MAAM,CAAC,UAAU,CAAC,KAAK,EAAE,MAAM,CAAC,GAAG,2BAA2B,EAAE,CAAC;QAC/F,OAAO,KAAK,CAAC;IACf,CAAC;IACD,MAAM,QAAQ,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAChC,MAAM,QAAQ,GACZ,iBAAiB,IAAI,sBAAsB,CAAC,IAAI,CAAC,QAAQ,CAAC;QACxD,CAAC,CAAC,QAAQ;QACV,CAAC,CAAC,sBAAsB,CAAC,IAAI,CAAC,QAAQ,CAAC;YACrC,CAAC,CAAC,WAAW;YACb,CAAC,CAAC,SAAS,CAAC;IAClB,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,OAAO,KAAK,CAAC;IACf,CAAC;IACD,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IAChD,OAAO,OAAO,CAAC,MAAM,KAAK,EAAE,IAAI,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,KAAK,QAAQ,CAAC;AAC9F,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,KAAa;IAC1C,OAAO,wBAAwB,CAAC,KAAK,EAAE,GAAG,CAAC,IAAI,sBAAsB,CAAC,KAAK,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;AAC3F,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,KAAa;IAC3C,OAAO,wBAAwB,CAAC,KAAK,EAAE,GAAG,CAAC,IAAI,sBAAsB,CAAC,KAAK,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC;AAC1F,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,KAAa;IAC1C,IAAI,CAAC,wBAAwB,CAAC,KAAK,EAAE,GAAG,CAAC,EAAE,CAAC;QAC1C,OAAO,KAAK,CAAC;IACf,CAAC;IACD,MAAM,SAAS,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;IACrD,OAAO,oBAAoB,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;AAC9C,CAAC;AAED,MAAM,UAAU,wBAAwB,CAAC,KAAa;IACpD,OAAO,wBAAwB,CAAC,KAAK,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC;AACpD,CAAC","sourcesContent":["import { isIP } from \"node:net\";\n\nconst MAX_MATRIX_IDENTIFIER_BYTES = 255;\n\nfunction isMatrixIpv4Address(value: string): boolean {\n const octets = value.split(\".\");\n return (\n octets.length === 4 && octets.every((octet) => /^\\d{1,3}$/u.test(octet) && Number(octet) <= 255)\n );\n}\n\nexport function isMatrixServerName(value: string): boolean {\n const ipv6 = /^\\[([^\\]]+)\\](?::(\\d{1,5}))?$/u.exec(value);\n if (ipv6) {\n return isIP(ipv6[1]!) === 6;\n }\n const hostAndPort = /^([^:]+?)(?::(\\d{1,5}))?$/u.exec(value);\n if (!hostAndPort) {\n return false;\n }\n const hostname = hostAndPort[1]!;\n if (isMatrixIpv4Address(hostname)) {\n return true;\n }\n if (/^\\d+\\.\\d+\\.\\d+\\.\\d+$/u.test(hostname)) {\n return false;\n }\n return hostname.length <= 255 && /^[A-Za-z0-9.-]+$/u.test(hostname);\n}\n\nfunction hasLoneSurrogate(value: string): boolean {\n for (let index = 0; index < value.length; index += 1) {\n const code = value.charCodeAt(index);\n if (code >= 0xd800 && code <= 0xdbff) {\n const next = value.charCodeAt(index + 1);\n if (next < 0xdc00 || next > 0xdfff) {\n return true;\n }\n index += 1;\n } else if (code >= 0xdc00 && code <= 0xdfff) {\n return true;\n }\n }\n return false;\n}\n\nfunction isMatrixScopedIdentifier(\n value: string,\n sigil: \"!\" | \"$\" | \"@\",\n allowEmptyLocalpart = false,\n): boolean {\n const separator = value.indexOf(\":\");\n const localpart = value.slice(1, separator);\n return (\n value.startsWith(sigil) &&\n Buffer.byteLength(value, \"utf8\") <= MAX_MATRIX_IDENTIFIER_BYTES &&\n separator >= (allowEmptyLocalpart ? 1 : 2) &&\n !localpart.includes(\"\\0\") &&\n !hasLoneSurrogate(localpart) &&\n isMatrixServerName(value.slice(separator + 1))\n );\n}\n\nfunction isMatrixHashIdentifier(\n value: string,\n sigil: \"!\" | \"$\",\n allowLegacyBase64: boolean,\n): boolean {\n if (!value.startsWith(sigil) || Buffer.byteLength(value, \"utf8\") > MAX_MATRIX_IDENTIFIER_BYTES) {\n return false;\n }\n const opaqueId = value.slice(1);\n const encoding =\n allowLegacyBase64 && /^[A-Za-z0-9+/]{43}$/u.test(opaqueId)\n ? \"base64\"\n : /^[A-Za-z0-9_-]{43}$/u.test(opaqueId)\n ? \"base64url\"\n : undefined;\n if (!encoding) {\n return false;\n }\n const decoded = Buffer.from(opaqueId, encoding);\n return decoded.length === 32 && decoded.toString(encoding).replace(/=+$/u, \"\") === opaqueId;\n}\n\nexport function isMatrixRoomId(value: string): boolean {\n return isMatrixScopedIdentifier(value, \"!\") || isMatrixHashIdentifier(value, \"!\", false);\n}\n\nexport function isMatrixEventId(value: string): boolean {\n return isMatrixScopedIdentifier(value, \"$\") || isMatrixHashIdentifier(value, \"$\", true);\n}\n\nexport function isMatrixUserId(value: string): boolean {\n if (!isMatrixScopedIdentifier(value, \"@\")) {\n return false;\n }\n const localpart = value.slice(1, value.indexOf(\":\"));\n return /^[a-z0-9._=/+-]+$/u.test(localpart);\n}\n\nexport function isHistoricalMatrixUserId(value: string): boolean {\n return isMatrixScopedIdentifier(value, \"@\", true);\n}\n"]}
|
|
@@ -2,15 +2,20 @@ import type { CrablineServerManifest } from "../servers/index.js";
|
|
|
2
2
|
import { publishPrivateFileAtomically, syncParentDirectory } from "./private-file.js";
|
|
3
3
|
import { type OpenClawCrablineChannelDriverSelection } from "./shared.js";
|
|
4
4
|
import type { OpenClawCrablineSmokeRunLock } from "./smoke-lock.js";
|
|
5
|
-
|
|
5
|
+
type OpenClawCrablineArtifactPointerBase = {
|
|
6
6
|
capabilityMatrixPath: string;
|
|
7
7
|
generation: string;
|
|
8
8
|
manifestPath: string;
|
|
9
9
|
previousGeneration?: string;
|
|
10
10
|
providerReadinessArtifactPath: string;
|
|
11
11
|
smokeArtifactPath: string;
|
|
12
|
-
version: 1;
|
|
13
12
|
};
|
|
13
|
+
export type OpenClawCrablineArtifactPointer = (OpenClawCrablineArtifactPointerBase & {
|
|
14
|
+
version: 1;
|
|
15
|
+
}) | (OpenClawCrablineArtifactPointerBase & {
|
|
16
|
+
recorderSnapshotPath: string | null;
|
|
17
|
+
version: 2;
|
|
18
|
+
});
|
|
14
19
|
export type PublishedOpenClawCrablineArtifactGeneration = OpenClawCrablineArtifactPointer & {
|
|
15
20
|
pointerPath: string;
|
|
16
21
|
providerReadiness: Record<string, unknown>;
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { isDeepStrictEqual } from "node:util";
|
|
4
5
|
import { captureDirectoryIdentity, publishPrivateFileAtomically, removeSecuredPrivateDirectory, securePrivateDirectory, syncParentDirectory, } from "./private-file.js";
|
|
5
6
|
import { OPENCLAW_CRABLINE_ARTIFACT_POINTER_PATH, OPENCLAW_CRABLINE_ARTIFACT_STORE_DIRECTORY, OPENCLAW_CRABLINE_CHANNEL_CAPABILITY_MATRIX_PATH, OPENCLAW_CRABLINE_CHANNEL_SMOKE_PATH, OPENCLAW_CRABLINE_PROVIDER_READINESS_PATH, OPENCLAW_CRABLINE_MANIFEST_PATH, } from "./shared.js";
|
|
6
7
|
const GENERATION_NAME_PATTERN = /^generation-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu;
|
|
7
8
|
const STAGING_NAME_PATTERN = /^\.staging-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu;
|
|
8
9
|
const REMOVAL_TOMBSTONE_PATTERN = /^\.(.+)\.\d+\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.remove$/iu;
|
|
10
|
+
const CURRENT_GENERATION_READ_ATTEMPTS = 8;
|
|
9
11
|
function resolveProviderReadinessArtifactPath(selection) {
|
|
10
12
|
const readinessPath = selection.providerReadinessArtifactPath ?? selection.smokeArtifactPath;
|
|
11
13
|
if (selection.capabilityMatrixPath !== OPENCLAW_CRABLINE_CHANNEL_CAPABILITY_MATRIX_PATH ||
|
|
@@ -17,6 +19,179 @@ function resolveProviderReadinessArtifactPath(selection) {
|
|
|
17
19
|
function isMissingPathError(error) {
|
|
18
20
|
return error.code === "ENOENT";
|
|
19
21
|
}
|
|
22
|
+
function isRecord(value) {
|
|
23
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
24
|
+
}
|
|
25
|
+
function hasNonEmptyStringFields(value, fields) {
|
|
26
|
+
return fields.every((field) => typeof value[field] === "string" && value[field].length > 0);
|
|
27
|
+
}
|
|
28
|
+
function isGeneratedManifest(value) {
|
|
29
|
+
if (value.version !== 1 ||
|
|
30
|
+
!hasNonEmptyStringFields(value, ["adminToken", "baseUrl", "provider"]) ||
|
|
31
|
+
!isRecord(value.endpoints) ||
|
|
32
|
+
!isRecord(value.env)) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
const endpoints = value.endpoints;
|
|
36
|
+
const env = value.env;
|
|
37
|
+
const baseUrl = value.baseUrl;
|
|
38
|
+
switch (value.provider) {
|
|
39
|
+
case "mattermost":
|
|
40
|
+
return (hasNonEmptyStringFields(value, ["botToken", "botUserId"]) &&
|
|
41
|
+
hasNonEmptyStringFields(endpoints, ["adminInboundUrl", "apiRoot", "websocketUrl"]) &&
|
|
42
|
+
hasNonEmptyStringFields(env, ["MATTERMOST_BOT_TOKEN", "MATTERMOST_URL"]) &&
|
|
43
|
+
endpoints.adminInboundUrl === `${baseUrl}/crabline/mattermost/inbound` &&
|
|
44
|
+
endpoints.apiRoot === `${baseUrl}/api/v4` &&
|
|
45
|
+
endpoints.websocketUrl === `${baseUrl.replace(/^http/u, "ws")}/api/v4/websocket` &&
|
|
46
|
+
env.MATTERMOST_BOT_TOKEN === value.botToken &&
|
|
47
|
+
env.MATTERMOST_URL === baseUrl);
|
|
48
|
+
case "matrix":
|
|
49
|
+
return (hasNonEmptyStringFields(value, ["accessToken", "botUserId", "deviceId"]) &&
|
|
50
|
+
hasNonEmptyStringFields(endpoints, ["adminInboundUrl", "clientApiRoot", "syncUrl"]) &&
|
|
51
|
+
hasNonEmptyStringFields(env, [
|
|
52
|
+
"MATRIX_ACCESS_TOKEN",
|
|
53
|
+
"MATRIX_BASE_URL",
|
|
54
|
+
"MATRIX_USER_ID",
|
|
55
|
+
]) &&
|
|
56
|
+
endpoints.adminInboundUrl === `${baseUrl}/crabline/matrix/inbound` &&
|
|
57
|
+
endpoints.clientApiRoot === `${baseUrl}/_matrix/client/v3` &&
|
|
58
|
+
endpoints.syncUrl === `${endpoints.clientApiRoot}/sync` &&
|
|
59
|
+
env.MATRIX_ACCESS_TOKEN === value.accessToken &&
|
|
60
|
+
env.MATRIX_BASE_URL === baseUrl &&
|
|
61
|
+
env.MATRIX_USER_ID === value.botUserId);
|
|
62
|
+
case "signal":
|
|
63
|
+
return (hasNonEmptyStringFields(value, ["account"]) &&
|
|
64
|
+
hasNonEmptyStringFields(endpoints, ["adminInboundUrl", "apiRoot", "eventsUrl", "rpcUrl"]) &&
|
|
65
|
+
endpoints.adminInboundUrl === `${baseUrl}/crabline/signal/inbound` &&
|
|
66
|
+
endpoints.apiRoot === baseUrl &&
|
|
67
|
+
endpoints.eventsUrl === `${baseUrl}/api/v1/events` &&
|
|
68
|
+
endpoints.rpcUrl === `${baseUrl}/api/v1/rpc` &&
|
|
69
|
+
Object.keys(env).length === 0);
|
|
70
|
+
case "slack":
|
|
71
|
+
return (hasNonEmptyStringFields(value, ["botToken", "signingSecret"]) &&
|
|
72
|
+
hasNonEmptyStringFields(endpoints, ["adminInboundUrl", "apiRoot", "eventsUrl"]) &&
|
|
73
|
+
hasNonEmptyStringFields(env, [
|
|
74
|
+
"SLACK_API_URL",
|
|
75
|
+
"SLACK_BOT_TOKEN",
|
|
76
|
+
"SLACK_SIGNING_SECRET",
|
|
77
|
+
]) &&
|
|
78
|
+
endpoints.adminInboundUrl === `${baseUrl}/crabline/slack/inbound` &&
|
|
79
|
+
endpoints.apiRoot === `${baseUrl}/api/` &&
|
|
80
|
+
endpoints.eventsUrl === `${baseUrl}/slack/events` &&
|
|
81
|
+
env.SLACK_API_URL === endpoints.apiRoot &&
|
|
82
|
+
env.SLACK_BOT_TOKEN === value.botToken &&
|
|
83
|
+
env.SLACK_SIGNING_SECRET === value.signingSecret);
|
|
84
|
+
case "telegram":
|
|
85
|
+
return (hasNonEmptyStringFields(value, ["botToken"]) &&
|
|
86
|
+
hasNonEmptyStringFields(endpoints, ["adminInboundUrl", "apiRoot"]) &&
|
|
87
|
+
hasNonEmptyStringFields(env, ["TELEGRAM_BOT_TOKEN"]) &&
|
|
88
|
+
endpoints.adminInboundUrl === `${baseUrl}/crabline/telegram/inbound` &&
|
|
89
|
+
endpoints.apiRoot === baseUrl &&
|
|
90
|
+
env.TELEGRAM_BOT_TOKEN === value.botToken);
|
|
91
|
+
case "whatsapp":
|
|
92
|
+
return (hasNonEmptyStringFields(value, [
|
|
93
|
+
"accessToken",
|
|
94
|
+
"graphVersion",
|
|
95
|
+
"phoneNumberId",
|
|
96
|
+
"selfJid",
|
|
97
|
+
]) &&
|
|
98
|
+
hasNonEmptyStringFields(endpoints, [
|
|
99
|
+
"adminInboundUrl",
|
|
100
|
+
"apiRoot",
|
|
101
|
+
"baileysWebSocketUrl",
|
|
102
|
+
"messagesUrl",
|
|
103
|
+
"phoneNumberUrl",
|
|
104
|
+
"statusUrl",
|
|
105
|
+
]) &&
|
|
106
|
+
hasNonEmptyStringFields(env, [
|
|
107
|
+
"CLOUD_API_ACCESS_TOKEN",
|
|
108
|
+
"CLOUD_API_VERSION",
|
|
109
|
+
"WA_BASE_URL",
|
|
110
|
+
"WA_PHONE_NUMBER_ID",
|
|
111
|
+
]) &&
|
|
112
|
+
endpoints.adminInboundUrl === `${baseUrl}/_crabline/admin/whatsapp/inbound` &&
|
|
113
|
+
endpoints.apiRoot === `${baseUrl}/${value.graphVersion}` &&
|
|
114
|
+
endpoints.phoneNumberUrl ===
|
|
115
|
+
`${endpoints.apiRoot}/${value.phoneNumberId}` &&
|
|
116
|
+
endpoints.messagesUrl === `${endpoints.phoneNumberUrl}/messages` &&
|
|
117
|
+
endpoints.statusUrl === endpoints.messagesUrl &&
|
|
118
|
+
endpoints.baileysWebSocketUrl ===
|
|
119
|
+
`${baseUrl.replace(/^http/u, "ws")}/ws/chat?access_token=${encodeURIComponent(value.accessToken)}` &&
|
|
120
|
+
env.CLOUD_API_ACCESS_TOKEN === value.accessToken &&
|
|
121
|
+
env.CLOUD_API_VERSION === value.graphVersion &&
|
|
122
|
+
env.WA_BASE_URL === baseUrl &&
|
|
123
|
+
env.WA_PHONE_NUMBER_ID === value.phoneNumberId);
|
|
124
|
+
case "zalo":
|
|
125
|
+
return (hasNonEmptyStringFields(value, ["botId", "botToken"]) &&
|
|
126
|
+
hasNonEmptyStringFields(endpoints, ["adminInboundUrl", "apiRoot"]) &&
|
|
127
|
+
hasNonEmptyStringFields(env, ["ZALO_API_URL", "ZALO_BOT_TOKEN"]) &&
|
|
128
|
+
endpoints.adminInboundUrl === `${baseUrl}/crabline/zalo/inbound` &&
|
|
129
|
+
endpoints.apiRoot === baseUrl &&
|
|
130
|
+
env.ZALO_API_URL === baseUrl &&
|
|
131
|
+
env.ZALO_BOT_TOKEN === value.botToken);
|
|
132
|
+
default:
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function isSuccessfulProbe(value, manifest) {
|
|
137
|
+
if (!isRecord(value)) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
switch (manifest.provider) {
|
|
141
|
+
case "mattermost":
|
|
142
|
+
return (value.id === manifest.botUserId &&
|
|
143
|
+
typeof value.username === "string" &&
|
|
144
|
+
value.username.trim().length > 0 &&
|
|
145
|
+
typeof value.update_at === "number" &&
|
|
146
|
+
Number.isSafeInteger(value.update_at) &&
|
|
147
|
+
value.update_at >= 0);
|
|
148
|
+
case "matrix":
|
|
149
|
+
return value.user_id === manifest.botUserId;
|
|
150
|
+
case "signal":
|
|
151
|
+
return (value.ok === true &&
|
|
152
|
+
typeof value.status === "number" &&
|
|
153
|
+
Number.isInteger(value.status) &&
|
|
154
|
+
value.status >= 200 &&
|
|
155
|
+
value.status < 300);
|
|
156
|
+
case "slack":
|
|
157
|
+
return value.ok === true;
|
|
158
|
+
case "telegram": {
|
|
159
|
+
const result = value.result;
|
|
160
|
+
const tokenBotId = typeof manifest.botToken === "string"
|
|
161
|
+
? /^([1-9]\d*):/u.exec(manifest.botToken)?.[1]
|
|
162
|
+
: undefined;
|
|
163
|
+
const expectedBotId = tokenBotId === undefined ? undefined : Number(tokenBotId);
|
|
164
|
+
return (value.ok === true &&
|
|
165
|
+
isRecord(result) &&
|
|
166
|
+
typeof result.id === "number" &&
|
|
167
|
+
Number.isSafeInteger(result.id) &&
|
|
168
|
+
result.id > 0 &&
|
|
169
|
+
Number.isSafeInteger(expectedBotId) &&
|
|
170
|
+
result.id === expectedBotId &&
|
|
171
|
+
result.is_bot === true &&
|
|
172
|
+
typeof result.first_name === "string" &&
|
|
173
|
+
result.first_name.length > 0);
|
|
174
|
+
}
|
|
175
|
+
case "whatsapp":
|
|
176
|
+
return value.id === manifest.phoneNumberId;
|
|
177
|
+
case "zalo":
|
|
178
|
+
return value.ok === true && isRecord(value.result) && value.result.id === manifest.botId;
|
|
179
|
+
default:
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function isReadinessSection(value, manifest, manifestPath, requireCurrentFields) {
|
|
184
|
+
if (!isRecord(value) || value.manifestPath !== manifestPath || !isRecord(value.result)) {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
const result = value.result;
|
|
188
|
+
return (result.ok === true &&
|
|
189
|
+
result.provider === manifest.provider &&
|
|
190
|
+
isRecord(result.endpoints) &&
|
|
191
|
+
isDeepStrictEqual(result.endpoints, manifest.endpoints) &&
|
|
192
|
+
isSuccessfulProbe(result.probe, manifest) &&
|
|
193
|
+
(!requireCurrentFields || (result.proof === "provider-api-probe" && result.ready === true)));
|
|
194
|
+
}
|
|
20
195
|
function assertGenerationName(value, field) {
|
|
21
196
|
if (typeof value !== "string" || !GENERATION_NAME_PATTERN.test(value)) {
|
|
22
197
|
throw new Error(`OpenClaw Crabline artifact pointer ${field} is malformed.`);
|
|
@@ -32,28 +207,38 @@ function artifactRemovalTombstoneBaseName(name) {
|
|
|
32
207
|
? originalName
|
|
33
208
|
: null;
|
|
34
209
|
}
|
|
35
|
-
function
|
|
210
|
+
function withPublishedRecorderPath(providerReadiness, recorderPath) {
|
|
36
211
|
const result = providerReadiness.result;
|
|
37
212
|
if (!result || typeof result !== "object" || Array.isArray(result)) {
|
|
38
213
|
throw new Error("OpenClaw Crabline provider readiness result is malformed.");
|
|
39
214
|
}
|
|
215
|
+
const publishedResult = {
|
|
216
|
+
...result,
|
|
217
|
+
};
|
|
218
|
+
if (recorderPath === undefined) {
|
|
219
|
+
delete publishedResult.recorderPath;
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
publishedResult.recorderPath = recorderPath;
|
|
223
|
+
}
|
|
40
224
|
return {
|
|
41
225
|
...providerReadiness,
|
|
42
|
-
result:
|
|
43
|
-
...result,
|
|
44
|
-
recorderPath,
|
|
45
|
-
},
|
|
226
|
+
result: publishedResult,
|
|
46
227
|
};
|
|
47
228
|
}
|
|
48
229
|
function parseArtifactPointer(contents) {
|
|
49
|
-
let
|
|
230
|
+
let parsed;
|
|
50
231
|
try {
|
|
51
|
-
|
|
232
|
+
parsed = JSON.parse(contents);
|
|
52
233
|
}
|
|
53
234
|
catch (error) {
|
|
54
235
|
throw new Error("OpenClaw Crabline artifact pointer is malformed.", { cause: error });
|
|
55
236
|
}
|
|
56
|
-
if (
|
|
237
|
+
if (!isRecord(parsed)) {
|
|
238
|
+
throw new Error("OpenClaw Crabline artifact pointer is malformed.");
|
|
239
|
+
}
|
|
240
|
+
const value = parsed;
|
|
241
|
+
if (value.version !== 1 && value.version !== 2) {
|
|
57
242
|
throw new Error("OpenClaw Crabline artifact pointer is malformed.");
|
|
58
243
|
}
|
|
59
244
|
assertGenerationName(value.generation, "generation");
|
|
@@ -72,6 +257,8 @@ function parseArtifactPointer(contents) {
|
|
|
72
257
|
if (typeof value.capabilityMatrixPath !== "string" ||
|
|
73
258
|
value.capabilityMatrixPath !== expected.capabilityMatrixPath ||
|
|
74
259
|
value.manifestPath !== expected.manifestPath ||
|
|
260
|
+
(value.version === 2 &&
|
|
261
|
+
value.providerReadinessArtifactPath !== expected.providerReadinessArtifactPath) ||
|
|
75
262
|
(value.providerReadinessArtifactPath !== undefined &&
|
|
76
263
|
value.providerReadinessArtifactPath !== expected.providerReadinessArtifactPath) ||
|
|
77
264
|
(value.smokeArtifactPath !== undefined &&
|
|
@@ -79,15 +266,36 @@ function parseArtifactPointer(contents) {
|
|
|
79
266
|
(value.providerReadinessArtifactPath === undefined && value.smokeArtifactPath === undefined)) {
|
|
80
267
|
throw new Error("OpenClaw Crabline artifact pointer is malformed.");
|
|
81
268
|
}
|
|
82
|
-
|
|
269
|
+
if (value.version === 1 && value.recorderSnapshotPath !== undefined) {
|
|
270
|
+
throw new Error("OpenClaw Crabline artifact pointer is malformed.");
|
|
271
|
+
}
|
|
272
|
+
if (value.version === 2) {
|
|
273
|
+
if (value.recorderSnapshotPath !== null && typeof value.recorderSnapshotPath !== "string") {
|
|
274
|
+
throw new Error("OpenClaw Crabline artifact pointer is malformed.");
|
|
275
|
+
}
|
|
276
|
+
if (typeof value.recorderSnapshotPath === "string") {
|
|
277
|
+
const recorderFileName = path.basename(value.recorderSnapshotPath);
|
|
278
|
+
if (!recorderFileName.endsWith(".jsonl") ||
|
|
279
|
+
value.recorderSnapshotPath !== generationArtifactPath(value.generation, recorderFileName)) {
|
|
280
|
+
throw new Error("OpenClaw Crabline artifact pointer is malformed.");
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
const pointer = {
|
|
83
285
|
capabilityMatrixPath: value.capabilityMatrixPath,
|
|
84
286
|
generation: value.generation,
|
|
85
287
|
...(value.previousGeneration ? { previousGeneration: value.previousGeneration } : {}),
|
|
86
288
|
manifestPath: value.manifestPath,
|
|
87
289
|
providerReadinessArtifactPath: value.providerReadinessArtifactPath ?? value.smokeArtifactPath,
|
|
88
290
|
smokeArtifactPath: value.smokeArtifactPath ?? value.providerReadinessArtifactPath,
|
|
89
|
-
version: 1,
|
|
90
291
|
};
|
|
292
|
+
return value.version === 1
|
|
293
|
+
? { ...pointer, version: 1 }
|
|
294
|
+
: {
|
|
295
|
+
...pointer,
|
|
296
|
+
recorderSnapshotPath: value.recorderSnapshotPath,
|
|
297
|
+
version: 2,
|
|
298
|
+
};
|
|
91
299
|
}
|
|
92
300
|
export async function readOpenClawCrablineArtifactPointer(outputDir) {
|
|
93
301
|
try {
|
|
@@ -100,17 +308,178 @@ export async function readOpenClawCrablineArtifactPointer(outputDir) {
|
|
|
100
308
|
throw error;
|
|
101
309
|
}
|
|
102
310
|
}
|
|
103
|
-
async function
|
|
311
|
+
async function assertArtifactGenerationExists(outputDir, pointer) {
|
|
312
|
+
const generationDirectory = path.resolve(outputDir, OPENCLAW_CRABLINE_ARTIFACT_STORE_DIRECTORY, pointer.generation);
|
|
313
|
+
let currentGeneration;
|
|
314
|
+
try {
|
|
315
|
+
currentGeneration = await captureDirectoryIdentity(generationDirectory);
|
|
316
|
+
}
|
|
317
|
+
catch (error) {
|
|
318
|
+
throw new Error("OpenClaw Crabline current artifact generation is incomplete.", {
|
|
319
|
+
cause: error,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
const assertGenerationIdentity = async () => {
|
|
323
|
+
try {
|
|
324
|
+
await currentGeneration.assertIdentityAt();
|
|
325
|
+
}
|
|
326
|
+
catch (error) {
|
|
327
|
+
throw new Error("OpenClaw Crabline current artifact generation is incomplete.", {
|
|
328
|
+
cause: error,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
};
|
|
104
332
|
for (const artifactPath of [
|
|
105
333
|
pointer.manifestPath,
|
|
106
334
|
pointer.capabilityMatrixPath,
|
|
107
335
|
pointer.providerReadinessArtifactPath,
|
|
108
336
|
]) {
|
|
109
|
-
|
|
337
|
+
await assertGenerationIdentity();
|
|
338
|
+
let stats;
|
|
339
|
+
try {
|
|
340
|
+
stats = await fs.lstat(path.join(outputDir, artifactPath));
|
|
341
|
+
}
|
|
342
|
+
catch (error) {
|
|
343
|
+
throw new Error("OpenClaw Crabline current artifact generation is incomplete.", {
|
|
344
|
+
cause: error,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
110
347
|
if (!stats.isFile()) {
|
|
111
348
|
throw new Error("OpenClaw Crabline current artifact generation is incomplete.");
|
|
112
349
|
}
|
|
350
|
+
await assertGenerationIdentity();
|
|
113
351
|
}
|
|
352
|
+
const readArtifactObject = async (artifactPath) => {
|
|
353
|
+
try {
|
|
354
|
+
await assertGenerationIdentity();
|
|
355
|
+
const value = JSON.parse(await fs.readFile(path.join(outputDir, artifactPath), "utf8"));
|
|
356
|
+
await assertGenerationIdentity();
|
|
357
|
+
if (!isRecord(value)) {
|
|
358
|
+
throw new Error("artifact is not an object");
|
|
359
|
+
}
|
|
360
|
+
return value;
|
|
361
|
+
}
|
|
362
|
+
catch (error) {
|
|
363
|
+
throw new Error("OpenClaw Crabline current artifact generation is incomplete.", {
|
|
364
|
+
cause: error,
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
const readNestedRecorderPath = (value) => {
|
|
369
|
+
const result = value.result;
|
|
370
|
+
const recorderPath = result.recorderPath;
|
|
371
|
+
if (recorderPath !== undefined && typeof recorderPath !== "string") {
|
|
372
|
+
throw new Error("OpenClaw Crabline current artifact generation is incomplete.");
|
|
373
|
+
}
|
|
374
|
+
return recorderPath;
|
|
375
|
+
};
|
|
376
|
+
const manifest = await readArtifactObject(pointer.manifestPath);
|
|
377
|
+
const capabilityMatrix = await readArtifactObject(pointer.capabilityMatrixPath);
|
|
378
|
+
const readiness = await readArtifactObject(pointer.providerReadinessArtifactPath);
|
|
379
|
+
const providerReadiness = readiness.providerReadiness;
|
|
380
|
+
const smoke = readiness.smoke;
|
|
381
|
+
if (!isGeneratedManifest(manifest) ||
|
|
382
|
+
capabilityMatrix.version !== 1 ||
|
|
383
|
+
capabilityMatrix.source !== "openclaw/crabline" ||
|
|
384
|
+
capabilityMatrix.manifestPath !== pointer.manifestPath ||
|
|
385
|
+
capabilityMatrix.channelDriver !== "crabline" ||
|
|
386
|
+
typeof capabilityMatrix.selectedChannel !== "string" ||
|
|
387
|
+
capabilityMatrix.selectedChannel.length === 0 ||
|
|
388
|
+
capabilityMatrix.report === null ||
|
|
389
|
+
typeof capabilityMatrix.report !== "object" ||
|
|
390
|
+
Array.isArray(capabilityMatrix.report) ||
|
|
391
|
+
readiness.version !== 1 ||
|
|
392
|
+
readiness.source !== "openclaw/crabline" ||
|
|
393
|
+
readiness.manifestPath !== pointer.manifestPath ||
|
|
394
|
+
readiness.channelDriver !== "crabline" ||
|
|
395
|
+
readiness.selectedChannel !== capabilityMatrix.selectedChannel ||
|
|
396
|
+
manifest.provider !== readiness.selectedChannel ||
|
|
397
|
+
!isReadinessSection(smoke, manifest, pointer.manifestPath, pointer.version === 2 || providerReadiness !== undefined) ||
|
|
398
|
+
(pointer.version === 2 &&
|
|
399
|
+
(!isReadinessSection(providerReadiness, manifest, pointer.manifestPath, true) ||
|
|
400
|
+
!isDeepStrictEqual(providerReadiness, smoke))) ||
|
|
401
|
+
(pointer.version === 1 &&
|
|
402
|
+
providerReadiness !== undefined &&
|
|
403
|
+
(!isReadinessSection(providerReadiness, manifest, pointer.manifestPath, true) ||
|
|
404
|
+
!isDeepStrictEqual(providerReadiness, smoke)))) {
|
|
405
|
+
throw new Error("OpenClaw Crabline current artifact generation is incomplete.");
|
|
406
|
+
}
|
|
407
|
+
const manifestRecorderPath = typeof manifest.recorderPath === "string" ? manifest.recorderPath : undefined;
|
|
408
|
+
const providerReadinessRecorderPath = isRecord(providerReadiness)
|
|
409
|
+
? readNestedRecorderPath(providerReadiness)
|
|
410
|
+
: undefined;
|
|
411
|
+
const smokeRecorderPath = readNestedRecorderPath(smoke);
|
|
412
|
+
const readinessRecorderPaths = [
|
|
413
|
+
...(isRecord(providerReadiness) ? [providerReadinessRecorderPath] : []),
|
|
414
|
+
smokeRecorderPath,
|
|
415
|
+
];
|
|
416
|
+
const recorderPaths = [manifestRecorderPath, ...readinessRecorderPaths];
|
|
417
|
+
if (manifest.recorderPath !== undefined && typeof manifest.recorderPath !== "string") {
|
|
418
|
+
throw new Error("OpenClaw Crabline current artifact generation is incomplete.");
|
|
419
|
+
}
|
|
420
|
+
// The artifact store is owner-only; v1 compatibility covers historical layouts, not hostile same-owner rewrites.
|
|
421
|
+
if (pointer.version === 1 &&
|
|
422
|
+
readinessRecorderPaths.every((recorderPath) => recorderPath === undefined) &&
|
|
423
|
+
(manifestRecorderPath === undefined ||
|
|
424
|
+
path.dirname(path.resolve(outputDir, manifestRecorderPath)) !== generationDirectory)) {
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
if (pointer.version === 2 && pointer.recorderSnapshotPath === null) {
|
|
428
|
+
if (recorderPaths.some((recorderPath) => recorderPath !== undefined)) {
|
|
429
|
+
throw new Error("OpenClaw Crabline current artifact generation is incomplete.");
|
|
430
|
+
}
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
const resolvedRecorderPaths = recorderPaths.map((recorderPath) => recorderPath === undefined ? undefined : path.resolve(outputDir, recorderPath));
|
|
434
|
+
if (resolvedRecorderPaths.some((recorderPath) => recorderPath === undefined || path.dirname(recorderPath) !== generationDirectory) ||
|
|
435
|
+
new Set(resolvedRecorderPaths).size !== 1) {
|
|
436
|
+
throw new Error("OpenClaw Crabline current artifact generation is incomplete.");
|
|
437
|
+
}
|
|
438
|
+
const recorderPath = resolvedRecorderPaths[0];
|
|
439
|
+
if (pointer.version === 2 &&
|
|
440
|
+
pointer.recorderSnapshotPath !== null &&
|
|
441
|
+
recorderPath !== path.resolve(outputDir, pointer.recorderSnapshotPath)) {
|
|
442
|
+
throw new Error("OpenClaw Crabline current artifact generation is incomplete.");
|
|
443
|
+
}
|
|
444
|
+
try {
|
|
445
|
+
await assertGenerationIdentity();
|
|
446
|
+
const recorderStats = await fs.lstat(recorderPath, { bigint: true });
|
|
447
|
+
if (recorderStats.isFile() && recorderStats.nlink === 1n) {
|
|
448
|
+
await assertGenerationIdentity();
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
catch (error) {
|
|
453
|
+
throw new Error("OpenClaw Crabline current artifact generation is incomplete.", {
|
|
454
|
+
cause: error,
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
throw new Error("OpenClaw Crabline current artifact generation is incomplete.");
|
|
458
|
+
}
|
|
459
|
+
async function readValidCurrentArtifactGeneration(outputDir, initialPointer) {
|
|
460
|
+
let pointer = initialPointer;
|
|
461
|
+
let lastValidationError;
|
|
462
|
+
for (let attempt = 0; attempt < CURRENT_GENERATION_READ_ATTEMPTS; attempt += 1) {
|
|
463
|
+
try {
|
|
464
|
+
await assertArtifactGenerationExists(outputDir, pointer);
|
|
465
|
+
lastValidationError = undefined;
|
|
466
|
+
}
|
|
467
|
+
catch (error) {
|
|
468
|
+
lastValidationError = error;
|
|
469
|
+
}
|
|
470
|
+
const currentPointer = await readOpenClawCrablineArtifactPointer(outputDir);
|
|
471
|
+
if (currentPointer === null || isDeepStrictEqual(currentPointer, pointer)) {
|
|
472
|
+
if (lastValidationError !== undefined) {
|
|
473
|
+
throw lastValidationError;
|
|
474
|
+
}
|
|
475
|
+
if (currentPointer === null) {
|
|
476
|
+
throw new Error("OpenClaw Crabline current artifact generation is incomplete.");
|
|
477
|
+
}
|
|
478
|
+
return pointer;
|
|
479
|
+
}
|
|
480
|
+
pointer = currentPointer;
|
|
481
|
+
}
|
|
482
|
+
throw new Error("OpenClaw Crabline current artifact generation changed too frequently to validate.", lastValidationError === undefined ? undefined : { cause: lastValidationError });
|
|
114
483
|
}
|
|
115
484
|
async function pruneArtifactStore(params) {
|
|
116
485
|
const retainedGenerations = new Set(params.pointer
|
|
@@ -153,11 +522,11 @@ export async function publishOpenClawCrablineArtifactGeneration(params, dependen
|
|
|
153
522
|
await output.assertIdentityAt();
|
|
154
523
|
await store.assertIdentityAt();
|
|
155
524
|
await params.lock.assertOwned();
|
|
156
|
-
|
|
525
|
+
let currentPointer = await readOpenClawCrablineArtifactPointer(outputDir);
|
|
157
526
|
await output.assertIdentityAt();
|
|
158
527
|
await store.assertIdentityAt();
|
|
159
528
|
if (currentPointer) {
|
|
160
|
-
await
|
|
529
|
+
currentPointer = await readValidCurrentArtifactGeneration(outputDir, currentPointer);
|
|
161
530
|
await output.assertIdentityAt();
|
|
162
531
|
await store.assertIdentityAt();
|
|
163
532
|
}
|
|
@@ -193,15 +562,6 @@ export async function publishOpenClawCrablineArtifactGeneration(params, dependen
|
|
|
193
562
|
let primaryError;
|
|
194
563
|
let published;
|
|
195
564
|
try {
|
|
196
|
-
const pointer = {
|
|
197
|
-
capabilityMatrixPath: generationArtifactPath(generation, params.selection.capabilityMatrixPath),
|
|
198
|
-
generation,
|
|
199
|
-
manifestPath: generationArtifactPath(generation, OPENCLAW_CRABLINE_MANIFEST_PATH),
|
|
200
|
-
...(currentPointer ? { previousGeneration: currentPointer.generation } : {}),
|
|
201
|
-
providerReadinessArtifactPath: generationArtifactPath(generation, providerReadinessArtifactPath),
|
|
202
|
-
smokeArtifactPath: generationArtifactPath(generation, providerReadinessArtifactPath),
|
|
203
|
-
version: 1,
|
|
204
|
-
};
|
|
205
565
|
if (params.recorderSnapshot &&
|
|
206
566
|
(path.basename(params.recorderSnapshot.fileName) !== params.recorderSnapshot.fileName ||
|
|
207
567
|
!params.recorderSnapshot.fileName.endsWith(".jsonl"))) {
|
|
@@ -210,12 +570,20 @@ export async function publishOpenClawCrablineArtifactGeneration(params, dependen
|
|
|
210
570
|
const recorderSnapshotPath = params.recorderSnapshot
|
|
211
571
|
? generationArtifactPath(generation, params.recorderSnapshot.fileName)
|
|
212
572
|
: undefined;
|
|
573
|
+
const pointer = {
|
|
574
|
+
capabilityMatrixPath: generationArtifactPath(generation, params.selection.capabilityMatrixPath),
|
|
575
|
+
generation,
|
|
576
|
+
manifestPath: generationArtifactPath(generation, OPENCLAW_CRABLINE_MANIFEST_PATH),
|
|
577
|
+
...(currentPointer ? { previousGeneration: currentPointer.generation } : {}),
|
|
578
|
+
providerReadinessArtifactPath: generationArtifactPath(generation, providerReadinessArtifactPath),
|
|
579
|
+
recorderSnapshotPath: recorderSnapshotPath ?? null,
|
|
580
|
+
smokeArtifactPath: generationArtifactPath(generation, providerReadinessArtifactPath),
|
|
581
|
+
version: 2,
|
|
582
|
+
};
|
|
213
583
|
const publishedManifest = recorderSnapshotPath
|
|
214
584
|
? { ...params.manifest, recorderPath: recorderSnapshotPath }
|
|
215
|
-
: params.manifest;
|
|
216
|
-
const providerReadinessBase = recorderSnapshotPath
|
|
217
|
-
? withRecorderSnapshotPath(params.providerReadiness, recorderSnapshotPath)
|
|
218
|
-
: params.providerReadiness;
|
|
585
|
+
: Object.fromEntries(Object.entries(params.manifest).filter(([key]) => key !== "recorderPath"));
|
|
586
|
+
const providerReadinessBase = withPublishedRecorderPath(params.providerReadiness, recorderSnapshotPath);
|
|
219
587
|
const providerReadiness = {
|
|
220
588
|
...providerReadinessBase,
|
|
221
589
|
manifestPath: pointer.manifestPath,
|
|
@@ -280,6 +648,10 @@ export async function publishOpenClawCrablineArtifactGeneration(params, dependen
|
|
|
280
648
|
await output.assertIdentityAt();
|
|
281
649
|
await store.assertIdentityAt();
|
|
282
650
|
await staging.assertIdentityAt(generationPath);
|
|
651
|
+
await assertArtifactGenerationExists(outputDir, pointer);
|
|
652
|
+
await output.assertIdentityAt();
|
|
653
|
+
await store.assertIdentityAt();
|
|
654
|
+
await staging.assertIdentityAt(generationPath);
|
|
283
655
|
await params.lock.commitFileAtomically({
|
|
284
656
|
contents: `${JSON.stringify(pointer, null, 2)}\n`,
|
|
285
657
|
destinationPath: path.join(outputDir, OPENCLAW_CRABLINE_ARTIFACT_POINTER_PATH),
|