@openclaw/crabline 0.1.10 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +12 -2
  34. package/dist/src/openclaw/artifact-generation.js +436 -15
  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 +40 -3
  54. package/dist/src/openclaw/private-file.js +2070 -95
  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 +696 -187
  61. package/dist/src/openclaw/smoke-lock.js.map +1 -1
  62. package/dist/src/openclaw.js +352 -29
  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 +532 -42
  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 +1241 -22
  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 +1193 -118
  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
@@ -1,9 +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;
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;
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;
7
11
  function resolveProviderReadinessArtifactPath(selection) {
8
12
  const readinessPath = selection.providerReadinessArtifactPath ?? selection.smokeArtifactPath;
9
13
  if (selection.capabilityMatrixPath !== OPENCLAW_CRABLINE_CHANNEL_CAPABILITY_MATRIX_PATH ||
@@ -15,6 +19,179 @@ function resolveProviderReadinessArtifactPath(selection) {
15
19
  function isMissingPathError(error) {
16
20
  return error.code === "ENOENT";
17
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
+ }
18
195
  function assertGenerationName(value, field) {
19
196
  if (typeof value !== "string" || !GENERATION_NAME_PATTERN.test(value)) {
20
197
  throw new Error(`OpenClaw Crabline artifact pointer ${field} is malformed.`);
@@ -23,15 +200,45 @@ function assertGenerationName(value, field) {
23
200
  function generationArtifactPath(generation, fileName) {
24
201
  return path.join(OPENCLAW_CRABLINE_ARTIFACT_STORE_DIRECTORY, generation, fileName);
25
202
  }
203
+ function artifactRemovalTombstoneBaseName(name) {
204
+ const originalName = REMOVAL_TOMBSTONE_PATTERN.exec(name)?.[1];
205
+ return originalName !== undefined &&
206
+ (GENERATION_NAME_PATTERN.test(originalName) || STAGING_NAME_PATTERN.test(originalName))
207
+ ? originalName
208
+ : null;
209
+ }
210
+ function withPublishedRecorderPath(providerReadiness, recorderPath) {
211
+ const result = providerReadiness.result;
212
+ if (!result || typeof result !== "object" || Array.isArray(result)) {
213
+ throw new Error("OpenClaw Crabline provider readiness result is malformed.");
214
+ }
215
+ const publishedResult = {
216
+ ...result,
217
+ };
218
+ if (recorderPath === undefined) {
219
+ delete publishedResult.recorderPath;
220
+ }
221
+ else {
222
+ publishedResult.recorderPath = recorderPath;
223
+ }
224
+ return {
225
+ ...providerReadiness,
226
+ result: publishedResult,
227
+ };
228
+ }
26
229
  function parseArtifactPointer(contents) {
27
- let value;
230
+ let parsed;
28
231
  try {
29
- value = JSON.parse(contents);
232
+ parsed = JSON.parse(contents);
30
233
  }
31
234
  catch (error) {
32
235
  throw new Error("OpenClaw Crabline artifact pointer is malformed.", { cause: error });
33
236
  }
34
- 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) {
35
242
  throw new Error("OpenClaw Crabline artifact pointer is malformed.");
36
243
  }
37
244
  assertGenerationName(value.generation, "generation");
@@ -50,6 +257,8 @@ function parseArtifactPointer(contents) {
50
257
  if (typeof value.capabilityMatrixPath !== "string" ||
51
258
  value.capabilityMatrixPath !== expected.capabilityMatrixPath ||
52
259
  value.manifestPath !== expected.manifestPath ||
260
+ (value.version === 2 &&
261
+ value.providerReadinessArtifactPath !== expected.providerReadinessArtifactPath) ||
53
262
  (value.providerReadinessArtifactPath !== undefined &&
54
263
  value.providerReadinessArtifactPath !== expected.providerReadinessArtifactPath) ||
55
264
  (value.smokeArtifactPath !== undefined &&
@@ -57,15 +266,36 @@ function parseArtifactPointer(contents) {
57
266
  (value.providerReadinessArtifactPath === undefined && value.smokeArtifactPath === undefined)) {
58
267
  throw new Error("OpenClaw Crabline artifact pointer is malformed.");
59
268
  }
60
- 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 = {
61
285
  capabilityMatrixPath: value.capabilityMatrixPath,
62
286
  generation: value.generation,
63
287
  ...(value.previousGeneration ? { previousGeneration: value.previousGeneration } : {}),
64
288
  manifestPath: value.manifestPath,
65
289
  providerReadinessArtifactPath: value.providerReadinessArtifactPath ?? value.smokeArtifactPath,
66
290
  smokeArtifactPath: value.smokeArtifactPath ?? value.providerReadinessArtifactPath,
67
- version: 1,
68
291
  };
292
+ return value.version === 1
293
+ ? { ...pointer, version: 1 }
294
+ : {
295
+ ...pointer,
296
+ recorderSnapshotPath: value.recorderSnapshotPath,
297
+ version: 2,
298
+ };
69
299
  }
70
300
  export async function readOpenClawCrablineArtifactPointer(outputDir) {
71
301
  try {
@@ -78,34 +308,199 @@ export async function readOpenClawCrablineArtifactPointer(outputDir) {
78
308
  throw error;
79
309
  }
80
310
  }
81
- 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
+ };
82
332
  for (const artifactPath of [
83
333
  pointer.manifestPath,
84
334
  pointer.capabilityMatrixPath,
85
335
  pointer.providerReadinessArtifactPath,
86
336
  ]) {
87
- 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
+ }
88
347
  if (!stats.isFile()) {
89
348
  throw new Error("OpenClaw Crabline current artifact generation is incomplete.");
90
349
  }
350
+ await assertGenerationIdentity();
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.");
91
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 });
92
483
  }
93
484
  async function pruneArtifactStore(params) {
94
485
  const retainedGenerations = new Set(params.pointer
95
486
  ? [params.pointer.generation, params.pointer.previousGeneration].filter((generation) => generation !== undefined)
96
487
  : []);
97
488
  for (const entry of await fs.readdir(params.store.directoryPath, { withFileTypes: true })) {
98
- const isAbandonedStaging = entry.isDirectory() && entry.name.startsWith(".staging-");
489
+ const isAbandonedStaging = entry.isDirectory() && STAGING_NAME_PATTERN.test(entry.name);
99
490
  const isObsoleteGeneration = entry.isDirectory() &&
100
491
  GENERATION_NAME_PATTERN.test(entry.name) &&
101
492
  !retainedGenerations.has(entry.name);
102
- if (!isAbandonedStaging && !isObsoleteGeneration) {
493
+ const removalTombstoneBaseName = entry.isDirectory()
494
+ ? artifactRemovalTombstoneBaseName(entry.name)
495
+ : null;
496
+ const isRemovalTombstone = removalTombstoneBaseName !== null;
497
+ if (!isAbandonedStaging && !isObsoleteGeneration && !isRemovalTombstone) {
103
498
  continue;
104
499
  }
105
500
  await params.lock.assertOwned();
106
501
  await params.store.assertIdentityAt();
107
502
  const obsolete = await securePrivateDirectory(path.join(params.store.directoryPath, entry.name), params.directoryOptions);
108
- await removeSecuredPrivateDirectory(obsolete);
503
+ await removeSecuredPrivateDirectory(obsolete, undefined, removalTombstoneBaseName ?? entry.name);
109
504
  await params.store.assertIdentityAt();
110
505
  }
111
506
  }
@@ -121,16 +516,17 @@ export async function publishOpenClawCrablineArtifactGeneration(params, dependen
121
516
  ...(dependencies.secureWindowsDirectory
122
517
  ? { secureWindowsDirectory: dependencies.secureWindowsDirectory }
123
518
  : {}),
519
+ ...(dependencies.syncParent ? { syncParent: dependencies.syncParent } : {}),
124
520
  };
125
521
  const store = await securePrivateDirectory(storePath, directoryOptions);
126
522
  await output.assertIdentityAt();
127
523
  await store.assertIdentityAt();
128
524
  await params.lock.assertOwned();
129
- const currentPointer = await readOpenClawCrablineArtifactPointer(outputDir);
525
+ let currentPointer = await readOpenClawCrablineArtifactPointer(outputDir);
130
526
  await output.assertIdentityAt();
131
527
  await store.assertIdentityAt();
132
528
  if (currentPointer) {
133
- await assertCurrentGenerationExists(outputDir, currentPointer);
529
+ currentPointer = await readValidCurrentArtifactGeneration(outputDir, currentPointer);
134
530
  await output.assertIdentityAt();
135
531
  await store.assertIdentityAt();
136
532
  }
@@ -166,22 +562,35 @@ export async function publishOpenClawCrablineArtifactGeneration(params, dependen
166
562
  let primaryError;
167
563
  let published;
168
564
  try {
565
+ if (params.recorderSnapshot &&
566
+ (path.basename(params.recorderSnapshot.fileName) !== params.recorderSnapshot.fileName ||
567
+ !params.recorderSnapshot.fileName.endsWith(".jsonl"))) {
568
+ throw new Error("OpenClaw Crabline recorder snapshot filename is malformed.");
569
+ }
570
+ const recorderSnapshotPath = params.recorderSnapshot
571
+ ? generationArtifactPath(generation, params.recorderSnapshot.fileName)
572
+ : undefined;
169
573
  const pointer = {
170
574
  capabilityMatrixPath: generationArtifactPath(generation, params.selection.capabilityMatrixPath),
171
575
  generation,
172
576
  manifestPath: generationArtifactPath(generation, OPENCLAW_CRABLINE_MANIFEST_PATH),
173
577
  ...(currentPointer ? { previousGeneration: currentPointer.generation } : {}),
174
578
  providerReadinessArtifactPath: generationArtifactPath(generation, providerReadinessArtifactPath),
579
+ recorderSnapshotPath: recorderSnapshotPath ?? null,
175
580
  smokeArtifactPath: generationArtifactPath(generation, providerReadinessArtifactPath),
176
- version: 1,
581
+ version: 2,
177
582
  };
583
+ const publishedManifest = recorderSnapshotPath
584
+ ? { ...params.manifest, recorderPath: recorderSnapshotPath }
585
+ : Object.fromEntries(Object.entries(params.manifest).filter(([key]) => key !== "recorderPath"));
586
+ const providerReadinessBase = withPublishedRecorderPath(params.providerReadiness, recorderSnapshotPath);
178
587
  const providerReadiness = {
179
- ...params.providerReadiness,
588
+ ...providerReadinessBase,
180
589
  manifestPath: pointer.manifestPath,
181
590
  };
182
591
  const artifactContents = [
183
592
  {
184
- contents: `${JSON.stringify(params.manifest, null, 2)}\n`,
593
+ contents: `${JSON.stringify(publishedManifest, null, 2)}\n`,
185
594
  fileName: OPENCLAW_CRABLINE_MANIFEST_PATH,
186
595
  },
187
596
  {
@@ -207,6 +616,14 @@ export async function publishOpenClawCrablineArtifactGeneration(params, dependen
207
616
  }, null, 2)}\n`,
208
617
  fileName: providerReadinessArtifactPath,
209
618
  },
619
+ ...(params.recorderSnapshot
620
+ ? [
621
+ {
622
+ contents: params.recorderSnapshot.contents,
623
+ fileName: params.recorderSnapshot.fileName,
624
+ },
625
+ ]
626
+ : []),
210
627
  ];
211
628
  await params.lock.assertOwned();
212
629
  for (const artifact of artifactContents) {
@@ -231,6 +648,10 @@ export async function publishOpenClawCrablineArtifactGeneration(params, dependen
231
648
  await output.assertIdentityAt();
232
649
  await store.assertIdentityAt();
233
650
  await staging.assertIdentityAt(generationPath);
651
+ await assertArtifactGenerationExists(outputDir, pointer);
652
+ await output.assertIdentityAt();
653
+ await store.assertIdentityAt();
654
+ await staging.assertIdentityAt(generationPath);
234
655
  await params.lock.commitFileAtomically({
235
656
  contents: `${JSON.stringify(pointer, null, 2)}\n`,
236
657
  destinationPath: path.join(outputDir, OPENCLAW_CRABLINE_ARTIFACT_POINTER_PATH),