@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.
Files changed (197) hide show
  1. package/README.md +99 -12
  2. package/dist/src/bin/crabline.js +3 -1
  3. package/dist/src/bin/crabline.js.map +1 -1
  4. package/dist/src/cli/program.d.ts +5 -1
  5. package/dist/src/cli/program.js +264 -50
  6. package/dist/src/cli/program.js.map +1 -1
  7. package/dist/src/config/load.js +39 -3
  8. package/dist/src/config/load.js.map +1 -1
  9. package/dist/src/config/schema.d.ts +20 -0
  10. package/dist/src/config/schema.js +163 -35
  11. package/dist/src/config/schema.js.map +1 -1
  12. package/dist/src/core/errors.d.ts +1 -0
  13. package/dist/src/core/errors.js +6 -3
  14. package/dist/src/core/errors.js.map +1 -1
  15. package/dist/src/core/http-path.d.ts +1 -0
  16. package/dist/src/core/http-path.js +14 -0
  17. package/dist/src/core/http-path.js.map +1 -0
  18. package/dist/src/core/matcher.d.ts +3 -1
  19. package/dist/src/core/matcher.js +58 -2
  20. package/dist/src/core/matcher.js.map +1 -1
  21. package/dist/src/core/nonces.d.ts +2 -0
  22. package/dist/src/core/nonces.js +11 -1
  23. package/dist/src/core/nonces.js.map +1 -1
  24. package/dist/src/core/reporters.d.ts +5 -0
  25. package/dist/src/core/reporters.js +26 -2
  26. package/dist/src/core/reporters.js.map +1 -1
  27. package/dist/src/core/run.d.ts +3 -1
  28. package/dist/src/core/run.js +178 -66
  29. package/dist/src/core/run.js.map +1 -1
  30. package/dist/src/matrix-ids.d.ts +5 -0
  31. package/dist/src/matrix-ids.js +83 -0
  32. package/dist/src/matrix-ids.js.map +1 -0
  33. package/dist/src/openclaw/artifact-generation.d.ts +7 -2
  34. package/dist/src/openclaw/artifact-generation.js +399 -27
  35. package/dist/src/openclaw/artifact-generation.js.map +1 -1
  36. package/dist/src/openclaw/bridges/matrix.js +21 -4
  37. package/dist/src/openclaw/bridges/matrix.js.map +1 -1
  38. package/dist/src/openclaw/bridges/mattermost.js +13 -8
  39. package/dist/src/openclaw/bridges/mattermost.js.map +1 -1
  40. package/dist/src/openclaw/bridges/probe-response.d.ts +1 -0
  41. package/dist/src/openclaw/bridges/probe-response.js +5 -0
  42. package/dist/src/openclaw/bridges/probe-response.js.map +1 -0
  43. package/dist/src/openclaw/bridges/signal.js +15 -6
  44. package/dist/src/openclaw/bridges/signal.js.map +1 -1
  45. package/dist/src/openclaw/bridges/slack.js +116 -23
  46. package/dist/src/openclaw/bridges/slack.js.map +1 -1
  47. package/dist/src/openclaw/bridges/telegram.js +45 -19
  48. package/dist/src/openclaw/bridges/telegram.js.map +1 -1
  49. package/dist/src/openclaw/bridges/whatsapp.js +32 -16
  50. package/dist/src/openclaw/bridges/whatsapp.js.map +1 -1
  51. package/dist/src/openclaw/bridges/zalo.js +8 -3
  52. package/dist/src/openclaw/bridges/zalo.js.map +1 -1
  53. package/dist/src/openclaw/private-file.d.ts +38 -3
  54. package/dist/src/openclaw/private-file.js +2008 -87
  55. package/dist/src/openclaw/private-file.js.map +1 -1
  56. package/dist/src/openclaw/shared.d.ts +1 -0
  57. package/dist/src/openclaw/shared.js +48 -13
  58. package/dist/src/openclaw/shared.js.map +1 -1
  59. package/dist/src/openclaw/smoke-lock.d.ts +9 -0
  60. package/dist/src/openclaw/smoke-lock.js +626 -172
  61. package/dist/src/openclaw/smoke-lock.js.map +1 -1
  62. package/dist/src/openclaw.js +225 -42
  63. package/dist/src/openclaw.js.map +1 -1
  64. package/dist/src/platform/process-owned-lock.d.ts +11 -0
  65. package/dist/src/platform/process-owned-lock.js +1698 -0
  66. package/dist/src/platform/process-owned-lock.js.map +1 -0
  67. package/dist/src/platform/recorder-directory.d.ts +1 -0
  68. package/dist/src/platform/recorder-directory.js +14 -0
  69. package/dist/src/platform/recorder-directory.js.map +1 -0
  70. package/dist/src/platform/windows-acl.d.ts +18 -0
  71. package/dist/src/platform/windows-acl.js +1367 -0
  72. package/dist/src/platform/windows-acl.js.map +1 -0
  73. package/dist/src/platform/windows-lock-root.d.ts +13 -0
  74. package/dist/src/platform/windows-lock-root.js +98 -0
  75. package/dist/src/platform/windows-lock-root.js.map +1 -0
  76. package/dist/src/providers/builtin/discord.d.ts +4 -0
  77. package/dist/src/providers/builtin/discord.js +52 -9
  78. package/dist/src/providers/builtin/discord.js.map +1 -1
  79. package/dist/src/providers/builtin/external-webhook-auth.d.ts +1 -0
  80. package/dist/src/providers/builtin/external-webhook-auth.js +38 -1
  81. package/dist/src/providers/builtin/external-webhook-auth.js.map +1 -1
  82. package/dist/src/providers/builtin/feishu.d.ts +1 -0
  83. package/dist/src/providers/builtin/feishu.js +96 -26
  84. package/dist/src/providers/builtin/feishu.js.map +1 -1
  85. package/dist/src/providers/builtin/googlechat.d.ts +1 -0
  86. package/dist/src/providers/builtin/googlechat.js +95 -26
  87. package/dist/src/providers/builtin/googlechat.js.map +1 -1
  88. package/dist/src/providers/builtin/imessage.js +60 -17
  89. package/dist/src/providers/builtin/imessage.js.map +1 -1
  90. package/dist/src/providers/builtin/loopback.js +88 -35
  91. package/dist/src/providers/builtin/loopback.js.map +1 -1
  92. package/dist/src/providers/builtin/matrix.d.ts +2 -2
  93. package/dist/src/providers/builtin/matrix.js +78 -21
  94. package/dist/src/providers/builtin/matrix.js.map +1 -1
  95. package/dist/src/providers/builtin/mattermost.d.ts +11 -7
  96. package/dist/src/providers/builtin/mattermost.js +138 -24
  97. package/dist/src/providers/builtin/mattermost.js.map +1 -1
  98. package/dist/src/providers/builtin/msteams.d.ts +1 -0
  99. package/dist/src/providers/builtin/msteams.js +118 -38
  100. package/dist/src/providers/builtin/msteams.js.map +1 -1
  101. package/dist/src/providers/builtin/native-local-mock.js +4 -4
  102. package/dist/src/providers/builtin/native-local-mock.js.map +1 -1
  103. package/dist/src/providers/builtin/script.js +1057 -146
  104. package/dist/src/providers/builtin/script.js.map +1 -1
  105. package/dist/src/providers/builtin/slack.d.ts +8 -1
  106. package/dist/src/providers/builtin/slack.js +327 -24
  107. package/dist/src/providers/builtin/slack.js.map +1 -1
  108. package/dist/src/providers/builtin/telegram.d.ts +1 -1
  109. package/dist/src/providers/builtin/telegram.js +55 -14
  110. package/dist/src/providers/builtin/telegram.js.map +1 -1
  111. package/dist/src/providers/builtin/whatsapp.d.ts +4 -3
  112. package/dist/src/providers/builtin/whatsapp.js +157 -29
  113. package/dist/src/providers/builtin/whatsapp.js.map +1 -1
  114. package/dist/src/providers/builtin/zalo.d.ts +5 -2
  115. package/dist/src/providers/builtin/zalo.js +31 -7
  116. package/dist/src/providers/builtin/zalo.js.map +1 -1
  117. package/dist/src/providers/catalog.d.ts +1 -1
  118. package/dist/src/providers/catalog.js +1 -1
  119. package/dist/src/providers/catalog.js.map +1 -1
  120. package/dist/src/providers/local-mock.d.ts +4 -0
  121. package/dist/src/providers/local-mock.js +162 -35
  122. package/dist/src/providers/local-mock.js.map +1 -1
  123. package/dist/src/providers/native-ids.d.ts +2 -0
  124. package/dist/src/providers/native-ids.js +3 -0
  125. package/dist/src/providers/native-ids.js.map +1 -1
  126. package/dist/src/providers/recorder.d.ts +14 -3
  127. package/dist/src/providers/recorder.js +521 -41
  128. package/dist/src/providers/recorder.js.map +1 -1
  129. package/dist/src/providers/registry.d.ts +1 -0
  130. package/dist/src/providers/registry.js +65 -20
  131. package/dist/src/providers/registry.js.map +1 -1
  132. package/dist/src/providers/signed-jwt.d.ts +6 -1
  133. package/dist/src/providers/signed-jwt.js +241 -50
  134. package/dist/src/providers/signed-jwt.js.map +1 -1
  135. package/dist/src/providers/slack-ids.d.ts +1 -0
  136. package/dist/src/providers/slack-ids.js +5 -0
  137. package/dist/src/providers/slack-ids.js.map +1 -1
  138. package/dist/src/providers/target-normalizers.d.ts +9 -1
  139. package/dist/src/providers/target-normalizers.js +177 -58
  140. package/dist/src/providers/target-normalizers.js.map +1 -1
  141. package/dist/src/providers/webhook-server.d.ts +3 -0
  142. package/dist/src/providers/webhook-server.js +268 -47
  143. package/dist/src/providers/webhook-server.js.map +1 -1
  144. package/dist/src/servers/http.d.ts +14 -2
  145. package/dist/src/servers/http.js +229 -58
  146. package/dist/src/servers/http.js.map +1 -1
  147. package/dist/src/servers/matrix.d.ts +3 -0
  148. package/dist/src/servers/matrix.js +268 -110
  149. package/dist/src/servers/matrix.js.map +1 -1
  150. package/dist/src/servers/mattermost.d.ts +5 -0
  151. package/dist/src/servers/mattermost.js +389 -91
  152. package/dist/src/servers/mattermost.js.map +1 -1
  153. package/dist/src/servers/recorder.d.ts +11 -0
  154. package/dist/src/servers/recorder.js +1072 -69
  155. package/dist/src/servers/recorder.js.map +1 -1
  156. package/dist/src/servers/signal.d.ts +1 -0
  157. package/dist/src/servers/signal.js +279 -44
  158. package/dist/src/servers/signal.js.map +1 -1
  159. package/dist/src/servers/slack.d.ts +15 -0
  160. package/dist/src/servers/slack.js +259 -51
  161. package/dist/src/servers/slack.js.map +1 -1
  162. package/dist/src/servers/telegram-identity.d.ts +6 -0
  163. package/dist/src/servers/telegram-identity.js +27 -0
  164. package/dist/src/servers/telegram-identity.js.map +1 -0
  165. package/dist/src/servers/telegram.d.ts +17 -6
  166. package/dist/src/servers/telegram.js +1169 -107
  167. package/dist/src/servers/telegram.js.map +1 -1
  168. package/dist/src/servers/webhook-target.d.ts +12 -4
  169. package/dist/src/servers/webhook-target.js +226 -28
  170. package/dist/src/servers/webhook-target.js.map +1 -1
  171. package/dist/src/servers/whatsapp-baileys-websocket.d.ts +60 -1
  172. package/dist/src/servers/whatsapp-baileys-websocket.js +782 -112
  173. package/dist/src/servers/whatsapp-baileys-websocket.js.map +1 -1
  174. package/dist/src/servers/whatsapp-jid.d.ts +1 -0
  175. package/dist/src/servers/whatsapp-jid.js +9 -5
  176. package/dist/src/servers/whatsapp-jid.js.map +1 -1
  177. package/dist/src/servers/whatsapp-wire/binary-node.d.ts +1 -0
  178. package/dist/src/servers/whatsapp-wire/binary-node.js +37 -13
  179. package/dist/src/servers/whatsapp-wire/binary-node.js.map +1 -1
  180. package/dist/src/servers/whatsapp-wire/crypto.d.ts +3 -0
  181. package/dist/src/servers/whatsapp-wire/crypto.js +23 -8
  182. package/dist/src/servers/whatsapp-wire/crypto.js.map +1 -1
  183. package/dist/src/servers/whatsapp-wire/handshake.d.ts +9 -3
  184. package/dist/src/servers/whatsapp-wire/handshake.js +173 -12
  185. package/dist/src/servers/whatsapp-wire/handshake.js.map +1 -1
  186. package/dist/src/servers/whatsapp-wire/signal.js +3 -3
  187. package/dist/src/servers/whatsapp-wire/signal.js.map +1 -1
  188. package/dist/src/servers/whatsapp.d.ts +7 -0
  189. package/dist/src/servers/whatsapp.js +149 -24
  190. package/dist/src/servers/whatsapp.js.map +1 -1
  191. package/dist/src/servers/zalo.d.ts +1 -0
  192. package/dist/src/servers/zalo.js +193 -82
  193. package/dist/src/servers/zalo.js.map +1 -1
  194. package/docs/channel-setup.md +185 -43
  195. package/fixtures/examples/crabline.example.yaml +3 -2
  196. package/package.json +18 -10
  197. 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":"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","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
- export type OpenClawCrablineArtifactPointer = {
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 withRecorderSnapshotPath(providerReadiness, recorderPath) {
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 value;
230
+ let parsed;
50
231
  try {
51
- value = JSON.parse(contents);
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 (value.version !== 1) {
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
- return {
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 assertCurrentGenerationExists(outputDir, pointer) {
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
- const stats = await fs.lstat(path.join(outputDir, artifactPath));
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
- const currentPointer = await readOpenClawCrablineArtifactPointer(outputDir);
525
+ let currentPointer = await readOpenClawCrablineArtifactPointer(outputDir);
157
526
  await output.assertIdentityAt();
158
527
  await store.assertIdentityAt();
159
528
  if (currentPointer) {
160
- await assertCurrentGenerationExists(outputDir, currentPointer);
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),