@gestaltrun/dsh-workspace-bundle 0.0.0-stage → 0.1.1

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 (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.i18n.yaml +37 -0
  3. package/README.md +92 -2
  4. package/README.zh.md +93 -0
  5. package/lib/index.js +2868 -0
  6. package/lib/types/agent-config-contract/agent-configs.d.ts +621 -0
  7. package/lib/types/agent-config-contract/agent-configs.js +68 -0
  8. package/lib/types/agent-config-contract/cs-hosting.d.ts +1333 -0
  9. package/lib/types/agent-config-contract/cs-hosting.js +163 -0
  10. package/lib/types/agent-config-contract/errors.d.ts +224 -0
  11. package/lib/types/agent-config-contract/errors.js +202 -0
  12. package/lib/types/agent-config-contract/ids.d.ts +65 -0
  13. package/lib/types/agent-config-contract/ids.js +35 -0
  14. package/lib/types/agent-config-contract/route.d.ts +56 -0
  15. package/lib/types/agent-config-contract/route.js +1 -0
  16. package/lib/types/agent-config-contract/test-account-claim.d.ts +24 -0
  17. package/lib/types/agent-config-contract/test-account-claim.js +5 -0
  18. package/lib/types/agent-config-format.d.ts +86 -0
  19. package/lib/types/agent-config-format.js +119 -0
  20. package/lib/types/agent-config-observation.d.ts +64 -0
  21. package/lib/types/agent-config-observation.js +448 -0
  22. package/lib/types/agent-config-source.d.ts +78 -0
  23. package/lib/types/agent-config-source.js +411 -0
  24. package/lib/types/agent-config-types.d.ts +67 -0
  25. package/lib/types/agent-config-types.js +1 -0
  26. package/lib/types/bundled-agent-config.d.ts +29 -0
  27. package/lib/types/bundled-agent-config.js +47 -0
  28. package/lib/types/config.d.ts +34 -0
  29. package/lib/types/config.js +53 -0
  30. package/lib/types/definition.d.ts +82 -0
  31. package/lib/types/definition.js +20 -0
  32. package/lib/types/digest.d.ts +38 -0
  33. package/lib/types/digest.js +243 -0
  34. package/lib/types/errors.d.ts +22 -0
  35. package/lib/types/errors.js +26 -0
  36. package/lib/types/filesystem.d.ts +16 -0
  37. package/lib/types/filesystem.js +64 -0
  38. package/lib/types/index.d.ts +28 -0
  39. package/lib/types/index.js +19 -0
  40. package/lib/types/journal.d.ts +51 -0
  41. package/lib/types/journal.js +109 -0
  42. package/lib/types/local-host-identity.d.ts +65 -0
  43. package/lib/types/local-host-identity.js +87 -0
  44. package/lib/types/local-host-replacement.d.ts +49 -0
  45. package/lib/types/local-host-replacement.js +112 -0
  46. package/lib/types/paths.d.ts +26 -0
  47. package/lib/types/paths.js +48 -0
  48. package/lib/types/provider.d.ts +83 -0
  49. package/lib/types/provider.js +560 -0
  50. package/lib/types/session-events.d.ts +23 -0
  51. package/lib/types/session-events.js +14 -0
  52. package/lib/types/types.d.ts +126 -0
  53. package/lib/types/types.js +1 -0
  54. package/locale/en.json +6 -0
  55. package/locale/zh.json +6 -0
  56. package/package.json +69 -3
package/lib/index.js ADDED
@@ -0,0 +1,2868 @@
1
+ import { basename, dirname, isAbsolute, join, normalize, posix, sep } from "node:path";
2
+ import { constants, existsSync, lstatSync, readFileSync, readdirSync } from "node:fs";
3
+ import { z } from "zod";
4
+ import { createHash, randomUUID } from "node:crypto";
5
+ import { gunzipSync } from "node:zlib";
6
+ import semver from "semver";
7
+ import { brandString } from "@deepseek-ai/dsh-brand";
8
+ import Schema from "@deepseek-ai/schemastery";
9
+ import { withFileLock, writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
10
+ import { Service } from "@deepseek-ai/cordis";
11
+ import { access, chmod, copyFile, lstat, mkdir, mkdtemp, readFile, readdir, realpath, rename, rm, writeFile } from "node:fs/promises";
12
+ //#region lib/types/errors.js
13
+ /** Misconfiguration of the workspace-bundle row; plugin load fails with it. */
14
+ var WorkspaceBundlesConfigError = class extends Error {
15
+ code;
16
+ name = "WorkspaceBundlesConfigError";
17
+ /**
18
+ * @param code - stable failure code.
19
+ * @param message - diagnostic naming the offending field.
20
+ */
21
+ constructor(code, message) {
22
+ super(`workspace-bundle: ${message} (${code})`);
23
+ this.code = code;
24
+ }
25
+ };
26
+ /** Template, path, digest, or upgrade failure; the Host reports it and does not fall back. */
27
+ var WorkspaceBundleError = class extends Error {
28
+ code;
29
+ name = "WorkspaceBundleError";
30
+ /**
31
+ * @param code - stable failure code.
32
+ * @param message - diagnostic.
33
+ */
34
+ constructor(code, message) {
35
+ super(`workspace-bundle: ${message} (${code})`);
36
+ this.code = code;
37
+ }
38
+ };
39
+ //#endregion
40
+ //#region lib/types/paths.js
41
+ /** Controlled encoding of branded Workspace bundle identities into Home path segments. */
42
+ const SEGMENT = /^[a-z0-9][a-z0-9._-]{0,63}$/u;
43
+ /**
44
+ * Encode one branded identity as a single path segment.
45
+ * @param value - bundle id or workspace key.
46
+ * @param field - diagnostic name.
47
+ * @returns the same spelling when it is a controlled segment.
48
+ * @throws {WorkspaceBundleError} When the value could traverse or collide with `.` / `..`.
49
+ */
50
+ function encodeBundleSegment(value, field) {
51
+ if (!SEGMENT.test(value) || value === "." || value === ".." || value.includes(sep) || value.includes(posix.sep) || value.includes("\0")) throw new WorkspaceBundleError("WORKSPACE_BUNDLE_IDENTITY_INVALID", `${field} ${JSON.stringify(value)} is not a controlled path segment`);
52
+ return value;
53
+ }
54
+ /**
55
+ * Join a parent with `{bundleId}/{workspaceKey}`.
56
+ * @param parent - absolute parent already validated by Config.
57
+ * @param identity - branded pair.
58
+ * @returns the joined path using the host separator.
59
+ */
60
+ function bundledDirectory(parent, identity) {
61
+ return join(parent, encodeBundleSegment(identity.bundleId, "bundleId"), encodeBundleSegment(identity.workspaceKey, "workspaceKey"));
62
+ }
63
+ /**
64
+ * Reject a template-relative path that leaves the root, is absolute, or uses a reserved segment.
65
+ * @param relativePath - POSIX relative path from the manifest.
66
+ * @returns the same spelling.
67
+ * @throws {WorkspaceBundleError} On traversal, absolute paths, empty segments, or backslashes.
68
+ */
69
+ function assertTemplateRelativePath(relativePath) {
70
+ if (relativePath === "" || relativePath.startsWith("/") || relativePath.includes("\\") || relativePath.includes("\0")) throw new WorkspaceBundleError("WORKSPACE_BUNDLE_PATH_TRAVERSAL", `template path ${JSON.stringify(relativePath)} is not a relative POSIX path`);
71
+ if (relativePath.split("/").some((part) => part === "" || part === "." || part === "..")) throw new WorkspaceBundleError("WORKSPACE_BUNDLE_PATH_TRAVERSAL", `template path ${JSON.stringify(relativePath)} contains a reserved segment`);
72
+ return relativePath;
73
+ }
74
+ /** @param bundleId - branded bundle identity. @param workspaceKey - branded workspace key. */
75
+ function identityOf(bundleId, workspaceKey) {
76
+ encodeBundleSegment(bundleId, "bundleId");
77
+ encodeBundleSegment(workspaceKey, "workspaceKey");
78
+ return {
79
+ bundleId,
80
+ workspaceKey
81
+ };
82
+ }
83
+ //#endregion
84
+ //#region lib/types/agent-config-format.js
85
+ /** 内置 Workspace Agent 配置的文件校验;目录和压缩字节的大小均由部署配置限制。 */
86
+ /** 本 Host 实现的 Agent 配置接口版本;白名单与写操作授权由 Host 保持。 */
87
+ const HOST_AGENT_CONFIG_API_VERSION = "2.0.0";
88
+ /** Agent 配置 SHA-256 的十六进制编码。 */
89
+ const AgentConfigSha256 = z.string().regex(/^[a-f0-9]{64}$/u);
90
+ /** 内置 Workspace 种类;不含路径分隔符。 */
91
+ const WorkspaceKind$1 = z.string().regex(/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u).max(80);
92
+ /** @param value - 完整 Agent 配置版本。 @returns 是否为保留构建元数据的规范 SemVer;优先级由 semver 按规范比较。 */
93
+ function isAgentConfigVersion(value) {
94
+ const parsed = semver.parse(value);
95
+ return parsed !== null && `${parsed.version}${parsed.build.length === 0 ? "" : `+${parsed.build.join(".")}`}` === value;
96
+ }
97
+ /** server 分配中可公开的包信息,未携带商家凭据。 */
98
+ const AgentConfigDescriptorSchema = z.strictObject({
99
+ workspaceKind: WorkspaceKind$1,
100
+ version: z.string().refine(isAgentConfigVersion),
101
+ sha256: AgentConfigSha256,
102
+ sizeBytes: z.number().int().positive().max(Number.MAX_SAFE_INTEGER),
103
+ downloadUrl: z.url(),
104
+ hostAgentConfigApiRange: z.string().refine((value) => semver.validRange(value) !== null),
105
+ runtimeApiRange: z.string().refine((value) => semver.validRange(value) !== null).optional(),
106
+ releaseNotes: z.string().min(1).max(2e4)
107
+ });
108
+ /** 包内逐文件摘要与字节数,文件路径相对版本目录。 */
109
+ const AgentConfigManifestSchema = z.strictObject({
110
+ formatVersion: z.literal(1),
111
+ workspaceKind: WorkspaceKind$1,
112
+ version: z.string().refine(isAgentConfigVersion),
113
+ hostAgentConfigApiRange: z.string().refine((value) => semver.validRange(value) !== null),
114
+ files: z.array(z.strictObject({
115
+ path: z.string(),
116
+ sizeBytes: z.number().int().nonnegative(),
117
+ sha256: AgentConfigSha256
118
+ })).min(1).max(1e4)
119
+ });
120
+ /** 确定性 gzip JSON Agent 配置:清单和按同序排列的 base64 文件。 */
121
+ const AgentConfigDocumentSchema = z.strictObject({
122
+ manifest: AgentConfigManifestSchema,
123
+ files: z.array(z.strictObject({
124
+ path: z.string(),
125
+ base64: z.string()
126
+ })).min(1).max(1e4)
127
+ });
128
+ /** 有界内容更新的结构化失败。 */
129
+ var AgentConfigError = class extends Error {
130
+ code;
131
+ /** @param code - 失败类别。 @param disposition - candidate 保留当前版本;blocked 表示已提交内容不可执行。 */
132
+ constructor(code, disposition = "candidate") {
133
+ super(disposition === "blocked" ? "已提交的 Agent 配置未通过执行校验,自动客服已阻断;保留工作区与缓存,请重新分配兼容且完整的配置版本后再继续。" : {
134
+ AGENT_CONFIG_QUERY_FAILED: "Agent 配置版本查询失败,保留当前版本;请检查服务器连接。",
135
+ AGENT_CONFIG_INVALID: "Agent 配置文件或清单无效,保留当前版本;请重新打包发布。",
136
+ AGENT_CONFIG_INCOMPATIBLE: "Agent 配置不兼容当前 Host,保留当前版本;请分配兼容版本或升级客户端。",
137
+ AGENT_CONFIG_DIGEST_MISMATCH: "Agent 配置摘要不匹配,保留当前版本;请核对发布登记。",
138
+ AGENT_CONFIG_DOWNLOAD_FAILED: "Agent 配置下载失败,保留当前版本;请检查文件服务连接。",
139
+ AGENT_CONFIG_APPLY_FAILED: "Agent 配置应用失败;请检查工作区恢复状态后再继续。",
140
+ AGENT_CONFIG_SCOPE_CHANGED: "Agent 配置版本来源已变更,已丢弃旧候选;保留当前版本并重新查询。"
141
+ }[code]);
142
+ this.code = code;
143
+ this.name = "AgentConfigError";
144
+ }
145
+ };
146
+ /** @param bytes - 完整文件字节。 @returns SHA-256 小写十六进制编码。 */
147
+ function agentConfigSha256(bytes) {
148
+ return createHash("sha256").update(bytes).digest("hex");
149
+ }
150
+ /**
151
+ * 拒绝无法在各 Host 文件系统安全表达的相对路径。
152
+ * @param path - 包内 POSIX 路径。
153
+ * @returns 原始路径。
154
+ */
155
+ function contentPath(path) {
156
+ assertTemplateRelativePath(path);
157
+ if (path === "manifest.json" || path.split("/").some((part) => /[<>:"|?*\x00-\x1f]/u.test(part) || /[. ]$/u.test(part) || /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/iu.test(part))) throw new AgentConfigError("AGENT_CONFIG_INVALID");
158
+ return path;
159
+ }
160
+ /**
161
+ * 同时认证压缩包和逐文件字节,任何失败均发生在版本目录写入前。
162
+ * @param bytes - 下载的完整 gzip 字节。
163
+ * @param descriptor - server 经鉴权查询给出的登记信息。
164
+ * @param maxExpandedBytes - 解压 JSON 和文件总字节上限。
165
+ * @returns 清单和已校验文件。
166
+ */
167
+ function decodeAgentConfig(bytes, descriptor, maxExpandedBytes) {
168
+ if (bytes.length !== descriptor.sizeBytes || agentConfigSha256(bytes) !== descriptor.sha256) throw new AgentConfigError("AGENT_CONFIG_DIGEST_MISMATCH");
169
+ if (!semver.satisfies("2.0.0", descriptor.hostAgentConfigApiRange, { includePrerelease: true })) throw new AgentConfigError("AGENT_CONFIG_INCOMPATIBLE");
170
+ let document;
171
+ try {
172
+ document = AgentConfigDocumentSchema.parse(JSON.parse(gunzipSync(bytes, { maxOutputLength: maxExpandedBytes }).toString("utf8")));
173
+ } catch (error) {
174
+ throw new AgentConfigError("AGENT_CONFIG_INVALID");
175
+ }
176
+ const { manifest } = document;
177
+ if (manifest.workspaceKind !== descriptor.workspaceKind || manifest.version !== descriptor.version || manifest.hostAgentConfigApiRange !== descriptor.hostAgentConfigApiRange || document.files.length !== manifest.files.length) throw new AgentConfigError("AGENT_CONFIG_INVALID");
178
+ const seen = /* @__PURE__ */ new Set();
179
+ let total = 0;
180
+ let previous = "";
181
+ return {
182
+ manifest,
183
+ files: document.files.map((file, index) => {
184
+ const entry = manifest.files[index];
185
+ const path = contentPath(file.path);
186
+ const normalized = path.toLowerCase();
187
+ if (entry.path !== path || path <= previous || seen.has(normalized) || [...seen].some((parent) => normalized.startsWith(`${parent}/`) || parent.startsWith(`${normalized}/`))) throw new AgentConfigError("AGENT_CONFIG_INVALID");
188
+ seen.add(normalized);
189
+ previous = path;
190
+ const data = Buffer.from(file.base64, "base64");
191
+ total += data.length;
192
+ if (total > maxExpandedBytes || data.toString("base64") !== file.base64 || data.length !== entry.sizeBytes || agentConfigSha256(data) !== entry.sha256) throw new AgentConfigError("AGENT_CONFIG_DIGEST_MISMATCH");
193
+ return {
194
+ path,
195
+ bytes: data
196
+ };
197
+ })
198
+ };
199
+ }
200
+ //#endregion
201
+ //#region lib/types/bundled-agent-config.js
202
+ /** Published package receipts beside shipped templates, separate from model-visible files. */
203
+ /** Public metadata of the exact published gzip installed as a bundled template. */
204
+ const BundledAgentConfigReceiptSchema = AgentConfigDescriptorSchema.omit({
205
+ downloadUrl: true,
206
+ releaseNotes: true
207
+ }).extend({
208
+ formatVersion: z.literal(1),
209
+ templateDigest: AgentConfigSha256
210
+ });
211
+ /**
212
+ * Attach the adjacent publication receipt to a verified packaged template.
213
+ * @param template - Template whose file catalog has already been verified.
214
+ * @returns The original template when no receipt exists, otherwise its published identity and receipt.
215
+ * @throws AgentConfigError when an existing receipt is unreadable, invalid or identifies different content.
216
+ */
217
+ function loadBundledAgentConfigSync(template) {
218
+ let bytes;
219
+ try {
220
+ bytes = readFileSync(join(dirname(template.sourceRoot), `${basename(template.sourceRoot)}.agent-config.json`));
221
+ } catch (error) {
222
+ if (error.code === "ENOENT") return template;
223
+ throw new AgentConfigError("AGENT_CONFIG_INVALID");
224
+ }
225
+ let receipt;
226
+ try {
227
+ receipt = BundledAgentConfigReceiptSchema.parse(JSON.parse(bytes.toString("utf8")));
228
+ } catch (error) {
229
+ throw new AgentConfigError("AGENT_CONFIG_INVALID");
230
+ }
231
+ if (receipt.workspaceKind !== template.bundleId || receipt.version !== template.manifest.pluginVersion || receipt.templateDigest !== template.manifest.digest) throw new AgentConfigError("AGENT_CONFIG_INVALID");
232
+ return {
233
+ ...template,
234
+ agentConfigSha256: receipt.sha256,
235
+ agentConfigReceipt: receipt
236
+ };
237
+ }
238
+ /**
239
+ * Identify downloaded content in journals while registered content follows the installation.
240
+ * @param template - Selected template, including a possible downloaded copy.
241
+ * @param registered - This identity's exact registered object, or undefined during recovery without a registration.
242
+ * @returns Null for the registered object or a template without a package digest; otherwise the package digest.
243
+ */
244
+ function journalAgentConfigSha256(template, registered) {
245
+ return template === registered ? null : template.agentConfigSha256 ?? null;
246
+ }
247
+ //#endregion
248
+ //#region lib/types/config.js
249
+ /** workspace-bundle Config: Schemastery field validation and explicit resolution into a Spec. */
250
+ /**
251
+ * Field types and bounds. Path fields are optional here so a missing value reaches
252
+ * {@link resolveWorkspaceBundlesSpec} and fails with its named diagnostic.
253
+ */
254
+ const Config = Schema.object({
255
+ materializeRoot: Schema.string().description("Absolute parent of bundled Workspace roots"),
256
+ templateCacheRoot: Schema.string().description("Absolute parent of immutable template copies"),
257
+ journalRoot: Schema.string().description("Absolute parent of upgrade journals"),
258
+ quiesceTimeoutMs: Schema.number().min(1e3).max(6e5).step(1).default(3e4),
259
+ recycleTimeoutMs: Schema.number().min(0).max(6e5).step(1).default(6e4),
260
+ agentConfigPollIntervalMs: Schema.number().min(1e3).max(864e5).step(1).default(3e5),
261
+ agentConfigRequestTimeoutMs: Schema.number().min(100).max(6e5).step(1).default(3e4),
262
+ agentConfigMaxDownloadBytes: Schema.number().min(1024).max(268435456).step(1).default(67108864),
263
+ agentConfigMaxExpandedBytes: Schema.number().min(1024).max(536870912).step(1).default(134217728),
264
+ allowLoopbackAgentConfigHttp: Schema.boolean().default(false)
265
+ });
266
+ function absoluteDirectory(value, field) {
267
+ if (value === void 0 || value.trim() === "") throw new WorkspaceBundlesConfigError(field === "materializeRoot" ? "WORKSPACE_BUNDLES_MATERIALIZE_ROOT_MISSING" : field === "templateCacheRoot" ? "WORKSPACE_BUNDLES_TEMPLATE_CACHE_ROOT_MISSING" : "WORKSPACE_BUNDLES_JOURNAL_ROOT_MISSING", `${field} is required`);
268
+ if (!isAbsolute(value) || value.includes("\0") || normalize(value).includes(`..${sep}`) || normalize(value).endsWith(`${sep}..`)) throw new WorkspaceBundlesConfigError("WORKSPACE_BUNDLES_PATH_INVALID", `${field} ${JSON.stringify(value)} must be an absolute path without parent segments`);
269
+ return normalize(value);
270
+ }
271
+ /**
272
+ * Resolve raw settings into the Spec the Provider runs with.
273
+ * @param config - raw row settings.
274
+ * @returns every field set, with roots normalized.
275
+ * @throws {WorkspaceBundlesConfigError} When a required path is missing or not absolute.
276
+ */
277
+ function resolveWorkspaceBundlesSpec(config = {}) {
278
+ const resolved = Config(config);
279
+ return {
280
+ materializeRoot: absoluteDirectory(resolved.materializeRoot, "materializeRoot"),
281
+ templateCacheRoot: absoluteDirectory(resolved.templateCacheRoot, "templateCacheRoot"),
282
+ journalRoot: absoluteDirectory(resolved.journalRoot, "journalRoot"),
283
+ quiesceTimeoutMs: resolved.quiesceTimeoutMs,
284
+ recycleTimeoutMs: resolved.recycleTimeoutMs,
285
+ agentConfigPollIntervalMs: resolved.agentConfigPollIntervalMs,
286
+ agentConfigRequestTimeoutMs: resolved.agentConfigRequestTimeoutMs,
287
+ agentConfigMaxDownloadBytes: resolved.agentConfigMaxDownloadBytes,
288
+ agentConfigMaxExpandedBytes: resolved.agentConfigMaxExpandedBytes,
289
+ allowLoopbackAgentConfigHttp: resolved.allowLoopbackAgentConfigHttp
290
+ };
291
+ }
292
+ //#endregion
293
+ //#region lib/types/local-host-replacement.js
294
+ /** One durable replacement fence per authenticated shop member, independent of installation and module. */
295
+ const State$2 = z.strictObject({
296
+ version: z.literal(1),
297
+ replaced: z.array(z.string().regex(/^[a-f0-9]{64}$/u)),
298
+ migrated: z.array(z.string().regex(/^[a-f0-9]{64}\/(cs-hosting|resource-intake)$/u))
299
+ });
300
+ function keyOf$2(member) {
301
+ return createHash("sha256").update(JSON.stringify([
302
+ member.serverOrigin,
303
+ member.userId,
304
+ member.currentTenantId
305
+ ])).digest("hex");
306
+ }
307
+ /** The memory fence remains active; callers retain fallback markers and report this failure. */
308
+ var LocalHostReplacementWriteError = class extends Error {
309
+ /** @param cause - underlying private filesystem failure. */
310
+ constructor(cause) {
311
+ super("LOCAL_HOST_REPLACEMENT_WRITE_FAILED", { cause });
312
+ }
313
+ };
314
+ /** Shared by module reporters, lease supervision and checkpoints within one Host. */
315
+ var LocalHostReplacementState = class {
316
+ path;
317
+ state = {
318
+ version: 1,
319
+ replaced: [],
320
+ migrated: []
321
+ };
322
+ listeners = /* @__PURE__ */ new Set();
323
+ persisted = this.state;
324
+ writes = Promise.resolve();
325
+ rejections = /* @__PURE__ */ new Map();
326
+ /** @param path - owner-only state file; null creates an explicit in-memory owner. */
327
+ constructor(path) {
328
+ this.path = path;
329
+ if (path === null) return;
330
+ try {
331
+ this.state = State$2.parse(JSON.parse(readFileSync(path, "utf8")));
332
+ } catch (error) {
333
+ if (error.code !== "ENOENT") throw error;
334
+ }
335
+ this.persisted = this.state;
336
+ }
337
+ /** @param member - current authenticated member. @returns whether explicit activation is required. */
338
+ isReplaced(member) {
339
+ return this.state.replaced.includes(keyOf$2(member));
340
+ }
341
+ /** @param listener - reacts to a changed fence. @returns its disposer. */
342
+ subscribe(listener) {
343
+ this.listeners.add(listener);
344
+ return () => {
345
+ this.listeners.delete(listener);
346
+ };
347
+ }
348
+ /** @param member - rejected member. Stops consumers before persistence starts. @returns durable completion; failure permits a later retry. */
349
+ markReplaced(member) {
350
+ const key = keyOf$2(member);
351
+ this.rejections.set(key, (this.rejections.get(key) ?? 0) + 1);
352
+ if (!this.state.replaced.includes(key)) {
353
+ this.state = {
354
+ ...this.state,
355
+ replaced: [...this.state.replaced, key]
356
+ };
357
+ for (const listener of this.listeners) listener();
358
+ }
359
+ if (this.persisted === this.state) return Promise.resolve();
360
+ return this.enqueue(async () => {
361
+ const state = this.state;
362
+ await this.save(state);
363
+ this.persisted = state;
364
+ });
365
+ }
366
+ /** @param member - member whose two module registrations have both been acknowledged. @returns durable completion, unless a newer rejection requires another activation. */
367
+ activate(member) {
368
+ const key = keyOf$2(member), rejection = this.rejections.get(key);
369
+ if (!this.state.replaced.includes(key)) return Promise.resolve();
370
+ this.persisted = void 0;
371
+ return this.enqueue(async () => {
372
+ const previous = this.state;
373
+ if (!previous.replaced.includes(key)) return;
374
+ if (this.rejections.get(key) !== rejection) throw new Error("LOCAL_HOST_REPLACED");
375
+ const next = {
376
+ ...previous,
377
+ replaced: previous.replaced.filter((value) => value !== key)
378
+ };
379
+ await this.save(next);
380
+ if (this.state !== previous || this.rejections.get(key) !== rejection) {
381
+ await this.save(this.state);
382
+ throw new Error("LOCAL_HOST_REPLACED");
383
+ }
384
+ this.persisted = this.state = next;
385
+ for (const listener of this.listeners) listener();
386
+ });
387
+ }
388
+ /** @param member - current member. @param module - module whose legacy files were inspected. @returns whether migration is complete. */
389
+ migrated(member, module) {
390
+ return this.state.migrated.includes(`${keyOf$2(member)}/${module}`);
391
+ }
392
+ /** @param member - current member. @param module - legacy source. @param replaced - whether any prior installation was stopped. @returns durable completion before the caller removes fallback flags. */
393
+ async importLegacy(member, module, replaced) {
394
+ if (replaced) await this.markReplaced(member);
395
+ if (this.migrated(member, module)) return;
396
+ this.state = {
397
+ ...this.state,
398
+ migrated: [...this.state.migrated, `${keyOf$2(member)}/${module}`]
399
+ };
400
+ await this.enqueue(async () => {
401
+ const state = this.state;
402
+ await this.save(state);
403
+ this.persisted = state;
404
+ });
405
+ }
406
+ enqueue(write) {
407
+ const work = this.writes.then(write);
408
+ this.writes = work.catch((error) => {});
409
+ return work;
410
+ }
411
+ async save(state) {
412
+ if (this.path === null) return;
413
+ try {
414
+ await writeFileAtomic(this.path, JSON.stringify(state) + "\n", {
415
+ mode: 384,
416
+ dirMode: 448
417
+ });
418
+ } catch (error) {
419
+ throw new LocalHostReplacementWriteError(error);
420
+ }
421
+ }
422
+ };
423
+ //#endregion
424
+ //#region lib/types/local-host-identity.js
425
+ /** One durable local Host identity per server and member, reused by every local module observation. */
426
+ const State$1 = z.strictObject({
427
+ version: z.literal(1),
428
+ scopes: z.record(z.string().regex(/^[a-f0-9]{64}$/u), z.strictObject({
429
+ hostId: z.uuid(),
430
+ confirmed: z.boolean()
431
+ }))
432
+ });
433
+ function keyOf$1(scope) {
434
+ return createHash("sha256").update(JSON.stringify([scope.serverOrigin, scope.userId])).digest("hex");
435
+ }
436
+ /** Load stops instead of letting modules report under their own identities, which the server may already have replaced. */
437
+ var LocalHostIdentityLoadError = class extends Error {
438
+ /** @param path - state file that could not be read or parsed. @param cause - underlying read or validation failure. */
439
+ constructor(path, cause) {
440
+ super(`The shared local Host identity file ${path} cannot be read or is invalid. Restore it from a backup or fix its permissions; deleting it makes local modules report under their own identities and may require reactivating local hosting.`, { cause });
441
+ }
442
+ };
443
+ /** The previous identity remains visible; the owner reports this failure and keeps its own reporting. */
444
+ var LocalHostIdentityWriteError = class extends Error {
445
+ /** @param cause - underlying private filesystem failure. */
446
+ constructor(cause) {
447
+ super("LOCAL_HOST_IDENTITY_WRITE_FAILED", { cause });
448
+ }
449
+ };
450
+ /** Loads synchronously so the first module report already reads the persisted identity. */
451
+ var LocalHostIdentityState = class {
452
+ path;
453
+ state = {
454
+ version: 1,
455
+ scopes: {}
456
+ };
457
+ writes = Promise.resolve();
458
+ /** @param path - owner-only state file; null creates an explicit in-memory owner. An unreadable or invalid file fails load instead of exposing no identity. */
459
+ constructor(path) {
460
+ this.path = path;
461
+ if (path === null) return;
462
+ try {
463
+ this.state = State$1.parse(JSON.parse(readFileSync(path, "utf8")));
464
+ } catch (error) {
465
+ if (error.code === "ENOENT") return;
466
+ throw new LocalHostIdentityLoadError(path, error);
467
+ }
468
+ }
469
+ /** @inheritdoc */
470
+ get(scope) {
471
+ const entry = this.state.scopes[keyOf$1(scope)];
472
+ return entry === void 0 ? void 0 : brandString(entry.hostId);
473
+ }
474
+ /** @inheritdoc */
475
+ publish(scope, read) {
476
+ const work = this.writes.then(async () => {
477
+ const key = keyOf$1(scope), previous = this.state.scopes[key], next = read();
478
+ if (next === void 0) return false;
479
+ if (previous !== void 0 && (previous.hostId === next.hostId ? previous.confirmed || !next.confirmed : previous.confirmed)) return false;
480
+ const state = {
481
+ ...this.state,
482
+ scopes: {
483
+ ...this.state.scopes,
484
+ [key]: {
485
+ hostId: next.hostId,
486
+ confirmed: next.confirmed
487
+ }
488
+ }
489
+ };
490
+ await this.save(state);
491
+ this.state = state;
492
+ return previous?.hostId !== next.hostId;
493
+ });
494
+ this.writes = work.then(() => {}, (error) => {});
495
+ return work;
496
+ }
497
+ async save(state) {
498
+ if (this.path === null) return;
499
+ try {
500
+ await writeFileAtomic(this.path, JSON.stringify(state) + "\n", {
501
+ mode: 384,
502
+ dirMode: 448
503
+ });
504
+ } catch (error) {
505
+ throw new LocalHostIdentityWriteError(error);
506
+ }
507
+ }
508
+ };
509
+ /**
510
+ * Identity getter for a local module reporter: the shared identity of the signed-in member, otherwise the module's own identity.
511
+ * Pass it as `hostId` and keep the fallback fixed; once an identity is shared, the reporter never returns to the fallback.
512
+ * @param ctx - context with the merchant session and WorkspaceBundles.
513
+ * @param fallback - module identity used before any local module published a shared one.
514
+ * @returns getter read before each registration.
515
+ */
516
+ function sharedLocalHostId(ctx, fallback) {
517
+ return () => {
518
+ const scope = ctx.merchantAccount.captureIdentity()?.identity;
519
+ return (scope === void 0 ? void 0 : ctx.workspaceBundles.localHostIdentity.get(scope)) ?? fallback;
520
+ };
521
+ }
522
+ //#endregion
523
+ //#region lib/types/definition.js
524
+ /** `WorkspaceBundles` Service Definition: Host-only template registration and stable Workspace materialize. */
525
+ /**
526
+ * Host-only capability: register a packaged template, materialize a stable hidden Workspace, and
527
+ * replace its tree as a whole. Registration is an effect; the returned disposer forbids later acquire.
528
+ */
529
+ var WorkspaceBundles = class extends Service {
530
+ /** One member-level fence shared by every local module. */
531
+ localHostReplacement;
532
+ /** One persisted local Host identity per server and member, published by the module that owns the installation identity. */
533
+ localHostIdentity;
534
+ /** @param ctx - context owning this Provider. @param replacementPath - durable fence file, or null for an in-memory provider. @param identityPath - durable shared identity file, or null for an in-memory provider. */
535
+ constructor(ctx, replacementPath = null, identityPath = null) {
536
+ super(ctx, "workspaceBundles");
537
+ this.localHostReplacement = new LocalHostReplacementState(replacementPath);
538
+ this.localHostIdentity = new LocalHostIdentityState(identityPath);
539
+ }
540
+ };
541
+ //#endregion
542
+ //#region lib/types/digest.js
543
+ /** Template manifest parsing and per-file / overall digest verification. */
544
+ const SHA256 = /^[0-9a-f]{64}$/u;
545
+ function fileDigest(bytes) {
546
+ return brandString(createHash("sha256").update(bytes).digest("hex"));
547
+ }
548
+ /**
549
+ * Digest of the ordered path+file-digest catalog.
550
+ * @param files - sorted catalog.
551
+ */
552
+ function overallDigest(files) {
553
+ const hash = createHash("sha256");
554
+ for (const file of files) hash.update(`${file.path}\0${file.digest}\n`);
555
+ return brandString(hash.update("").digest("hex"));
556
+ }
557
+ function sourceOf(value) {
558
+ if (value === void 0) return void 0;
559
+ if (typeof value !== "object" || value === null) throw new WorkspaceBundleError("WORKSPACE_BUNDLE_TEMPLATE_INVALID", "manifest source must be an object");
560
+ const record = value;
561
+ if (typeof record.repository !== "string" || typeof record.commit !== "string") throw new WorkspaceBundleError("WORKSPACE_BUNDLE_TEMPLATE_INVALID", "manifest source must name repository and commit");
562
+ return {
563
+ repository: record.repository,
564
+ commit: record.commit
565
+ };
566
+ }
567
+ /**
568
+ * Parse and validate a template manifest document.
569
+ * @param value - JSON value.
570
+ */
571
+ function parseTemplateManifest(value) {
572
+ if (typeof value !== "object" || value === null) throw new WorkspaceBundleError("WORKSPACE_BUNDLE_TEMPLATE_INVALID", "manifest must be an object");
573
+ const record = value;
574
+ if (record.formatVersion !== 1) throw new WorkspaceBundleError("WORKSPACE_BUNDLE_TEMPLATE_INVALID", "manifest formatVersion must be 1");
575
+ if (typeof record.pluginVersion !== "string" || record.pluginVersion === "") throw new WorkspaceBundleError("WORKSPACE_BUNDLE_TEMPLATE_INVALID", "manifest pluginVersion is required");
576
+ if (!Array.isArray(record.files)) throw new WorkspaceBundleError("WORKSPACE_BUNDLE_TEMPLATE_INVALID", "manifest files must be an array");
577
+ const files = record.files.map((entry, index) => {
578
+ if (typeof entry !== "object" || entry === null) throw new WorkspaceBundleError("WORKSPACE_BUNDLE_TEMPLATE_INVALID", `manifest files[${index}] must be an object`);
579
+ const file = entry;
580
+ if (typeof file.path !== "string" || typeof file.bytes !== "number" || typeof file.digest !== "string" || !SHA256.test(file.digest)) throw new WorkspaceBundleError("WORKSPACE_BUNDLE_TEMPLATE_INVALID", `manifest files[${index}] is incomplete`);
581
+ assertTemplateRelativePath(file.path);
582
+ return {
583
+ path: file.path,
584
+ bytes: file.bytes,
585
+ digest: brandString(file.digest)
586
+ };
587
+ });
588
+ const sorted = [...files].sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0);
589
+ if (sorted.some((file, index) => file.path !== files[index]?.path)) throw new WorkspaceBundleError("WORKSPACE_BUNDLE_TEMPLATE_INVALID", "manifest files must be sorted by path");
590
+ if (sorted.some((file, index) => index > 0 && file.path === sorted[index - 1]?.path)) throw new WorkspaceBundleError("WORKSPACE_BUNDLE_TEMPLATE_INVALID", "manifest files must not repeat a path");
591
+ if (typeof record.digest !== "string" || !SHA256.test(record.digest)) throw new WorkspaceBundleError("WORKSPACE_BUNDLE_TEMPLATE_INVALID", "manifest digest must be a SHA-256 hex string");
592
+ const digest = brandString(record.digest);
593
+ if (digest !== overallDigest(sorted)) throw new WorkspaceBundleError("WORKSPACE_BUNDLE_DIGEST_MISMATCH", "manifest overall digest does not match its files");
594
+ const source = sourceOf(record.source);
595
+ return source === void 0 ? {
596
+ formatVersion: 1,
597
+ pluginVersion: record.pluginVersion,
598
+ files: sorted,
599
+ digest
600
+ } : {
601
+ formatVersion: 1,
602
+ pluginVersion: record.pluginVersion,
603
+ source,
604
+ files: sorted,
605
+ digest
606
+ };
607
+ }
608
+ /**
609
+ * Read every regular file under `root` as a POSIX-relative catalog.
610
+ * @param fs - filesystem.
611
+ * @param root - directory to walk.
612
+ */
613
+ async function listRegularFiles(fs, root) {
614
+ const acc = [];
615
+ const walk = async (directory, prefix) => {
616
+ let names;
617
+ try {
618
+ names = await fs.readdir(directory);
619
+ } catch (error) {
620
+ if (error.code === "ENOENT") throw new WorkspaceBundleError("WORKSPACE_BUNDLE_MISSING_ASSET", `template root ${JSON.stringify(root)} is missing`);
621
+ throw error;
622
+ }
623
+ for (const name of [...names].sort()) {
624
+ const path = join(directory, name);
625
+ const relative = prefix === "" ? name : `${prefix}/${name}`;
626
+ const stat = await fs.lstat(path);
627
+ if (stat.isSymbolicLink()) throw new WorkspaceBundleError("WORKSPACE_BUNDLE_SYMLINK_REJECTED", `template path ${JSON.stringify(relative)} is a symbolic link`);
628
+ if (stat.isDirectory()) await walk(path, relative);
629
+ else if (stat.isFile()) {
630
+ assertTemplateRelativePath(relative);
631
+ acc.push(relative);
632
+ } else throw new WorkspaceBundleError("WORKSPACE_BUNDLE_TEMPLATE_INVALID", `template path ${JSON.stringify(relative)} is not a regular file`);
633
+ }
634
+ };
635
+ await walk(root, "");
636
+ return acc;
637
+ }
638
+ /**
639
+ * Verify every listed file and the overall digest against a directory.
640
+ * @param fs - filesystem.
641
+ * @param root - directory holding the files.
642
+ * @param manifest - expected catalog.
643
+ */
644
+ async function verifyTemplateTree(fs, root, manifest) {
645
+ const present = new Set(await listRegularFiles(fs, root));
646
+ present.delete("manifest.json");
647
+ for (const file of manifest.files) {
648
+ if (!present.has(file.path)) throw new WorkspaceBundleError("WORKSPACE_BUNDLE_MISSING_ASSET", `template is missing ${JSON.stringify(file.path)}`);
649
+ const bytes = await fs.readFile(join(root, ...file.path.split("/")));
650
+ if (bytes.length !== file.bytes || fileDigest(bytes) !== file.digest) throw new WorkspaceBundleError("WORKSPACE_BUNDLE_DIGEST_MISMATCH", `template file ${JSON.stringify(file.path)} digest mismatch`);
651
+ present.delete(file.path);
652
+ }
653
+ if (present.size > 0) throw new WorkspaceBundleError("WORKSPACE_BUNDLE_TEMPLATE_INVALID", `template has unexpected files ${[...present].join(", ")}`);
654
+ }
655
+ /**
656
+ * Load `manifest.json` from a template directory and verify the tree.
657
+ * @param fs - filesystem.
658
+ * @param root - template directory.
659
+ */
660
+ async function loadVerifiedManifest(fs, root) {
661
+ let raw;
662
+ try {
663
+ raw = await fs.readFile(join(root, "manifest.json"));
664
+ } catch (error) {
665
+ if (error.code === "ENOENT") throw new WorkspaceBundleError("WORKSPACE_BUNDLE_MISSING_ASSET", "template is missing manifest.json");
666
+ throw error;
667
+ }
668
+ let value;
669
+ try {
670
+ value = JSON.parse(raw.toString("utf8"));
671
+ } catch {
672
+ throw new WorkspaceBundleError("WORKSPACE_BUNDLE_TEMPLATE_INVALID", "manifest.json is not JSON");
673
+ }
674
+ const manifest = parseTemplateManifest(value);
675
+ await verifyTemplateTree(fs, root, manifest);
676
+ return manifest;
677
+ }
678
+ /** @param bytes - file contents. */
679
+ function digestOf(bytes) {
680
+ return fileDigest(bytes);
681
+ }
682
+ /**
683
+ * Load and verify a packaged template at plugin construction. A missing or
684
+ * invalid tree fails load instead of registering a half-checked catalog.
685
+ * @param root - template directory.
686
+ */
687
+ function loadVerifiedManifestSync(root) {
688
+ const manifestPath = join(root, "manifest.json");
689
+ if (!existsSync(manifestPath)) throw new WorkspaceBundleError("WORKSPACE_BUNDLE_MISSING_ASSET", "template is missing manifest.json");
690
+ let value;
691
+ try {
692
+ value = JSON.parse(readFileSync(manifestPath, "utf8"));
693
+ } catch {
694
+ throw new WorkspaceBundleError("WORKSPACE_BUNDLE_TEMPLATE_INVALID", "manifest.json is not JSON");
695
+ }
696
+ const manifest = parseTemplateManifest(value);
697
+ const present = /* @__PURE__ */ new Set();
698
+ const walk = (directory, prefix) => {
699
+ let names;
700
+ try {
701
+ names = readdirSync(directory);
702
+ } catch (error) {
703
+ if (error.code === "ENOENT") throw new WorkspaceBundleError("WORKSPACE_BUNDLE_MISSING_ASSET", `template root ${JSON.stringify(root)} is missing`);
704
+ throw error;
705
+ }
706
+ for (const name of [...names].sort()) {
707
+ const path = join(directory, name);
708
+ const relative = prefix === "" ? name : `${prefix}/${name}`;
709
+ const stat = lstatSync(path);
710
+ if (stat.isSymbolicLink()) throw new WorkspaceBundleError("WORKSPACE_BUNDLE_SYMLINK_REJECTED", `template path ${JSON.stringify(relative)} is a symbolic link`);
711
+ if (stat.isDirectory()) walk(path, relative);
712
+ else if (stat.isFile()) {
713
+ assertTemplateRelativePath(relative);
714
+ present.add(relative);
715
+ } else throw new WorkspaceBundleError("WORKSPACE_BUNDLE_TEMPLATE_INVALID", `template path ${JSON.stringify(relative)} is not a regular file`);
716
+ }
717
+ };
718
+ walk(root, "");
719
+ present.delete("manifest.json");
720
+ for (const file of manifest.files) {
721
+ if (!present.has(file.path)) throw new WorkspaceBundleError("WORKSPACE_BUNDLE_MISSING_ASSET", `template is missing ${JSON.stringify(file.path)}`);
722
+ const bytes = readFileSync(join(root, ...file.path.split("/")));
723
+ if (bytes.length !== file.bytes || fileDigest(bytes) !== file.digest) throw new WorkspaceBundleError("WORKSPACE_BUNDLE_DIGEST_MISMATCH", `template file ${JSON.stringify(file.path)} digest mismatch`);
724
+ present.delete(file.path);
725
+ }
726
+ if (present.size > 0) throw new WorkspaceBundleError("WORKSPACE_BUNDLE_TEMPLATE_INVALID", `template has unexpected files ${[...present].join(", ")}`);
727
+ return manifest;
728
+ }
729
+ //#endregion
730
+ //#region lib/types/filesystem.js
731
+ /** Node filesystem adapter and test crash injection. */
732
+ /**
733
+ * Node.js implementation of {@link WorkspaceBundleFs}.
734
+ * @param options.onPoint - optional test hook; throwing aborts the current step.
735
+ */
736
+ function createNodeFs(options = {}) {
737
+ return {
738
+ async mkdir(path) {
739
+ await mkdir(path, { recursive: true });
740
+ },
741
+ async writeFile(path, data) {
742
+ await mkdir(dirname(path), { recursive: true });
743
+ await writeFile(path, data);
744
+ },
745
+ readFile(path) {
746
+ return readFile(path);
747
+ },
748
+ rename(from, to) {
749
+ return rename(from, to);
750
+ },
751
+ rm(path, options) {
752
+ return rm(path, {
753
+ recursive: options?.recursive === true,
754
+ force: true
755
+ });
756
+ },
757
+ lstat(path) {
758
+ return lstat(path);
759
+ },
760
+ async readdir(path) {
761
+ return readdir(path);
762
+ },
763
+ realpath(path) {
764
+ return realpath(path);
765
+ },
766
+ copyFile(from, to) {
767
+ return copyFile(from, to);
768
+ },
769
+ async exists(path) {
770
+ try {
771
+ await access(path, constants.F_OK);
772
+ return true;
773
+ } catch {
774
+ return false;
775
+ }
776
+ }
777
+ };
778
+ }
779
+ /**
780
+ * Wrap an fs with a crash injector used only by tests.
781
+ * @param inner - real or fake filesystem.
782
+ * @param crashAt - the single point that throws {@link WorkspaceBundleError} `WORKSPACE_BUNDLE_CRASH_INJECTED`.
783
+ */
784
+ function crashableFs(inner, crashAt) {
785
+ return {
786
+ ...inner,
787
+ crash(point) {
788
+ if (crashAt === point) throw new WorkspaceBundleError("WORKSPACE_BUNDLE_CRASH_INJECTED", point);
789
+ }
790
+ };
791
+ }
792
+ //#endregion
793
+ //#region lib/types/journal.js
794
+ /** Durable upgrade journal: staging → updating → swap → verify → commit, with crash recovery. */
795
+ const IDLE = {
796
+ phase: "idle",
797
+ fromDigest: null,
798
+ toDigest: null,
799
+ pluginVersion: null,
800
+ stagingPath: null,
801
+ backupPath: null,
802
+ swapComplete: false
803
+ };
804
+ /** @returns a journal with no in-flight replacement. */
805
+ function idleJournal() {
806
+ return IDLE;
807
+ }
808
+ /**
809
+ * @param parent - journal root from Spec.
810
+ * @param identity - bundled Workspace.
811
+ */
812
+ function recordPath(parent, identity) {
813
+ return join(bundledDirectory(parent, identity), "record.json");
814
+ }
815
+ async function writeJson(fs, path, value) {
816
+ const tmp = `${path}.tmp`;
817
+ await fs.writeFile(tmp, `${JSON.stringify(value)}\n`);
818
+ await fs.rename(tmp, path);
819
+ }
820
+ /**
821
+ * Persist the product record.
822
+ * @param fs - filesystem.
823
+ * @param path - record.json path.
824
+ * @param record - value.
825
+ */
826
+ async function writeRecord(fs, path, record) {
827
+ await writeJson(fs, path, record);
828
+ }
829
+ /**
830
+ * Read a product record when present.
831
+ * @param fs - filesystem.
832
+ * @param path - record.json path.
833
+ */
834
+ async function readRecord(fs, path) {
835
+ if (!await fs.exists(path)) return void 0;
836
+ const value = JSON.parse((await fs.readFile(path)).toString("utf8"));
837
+ if (value.version !== 1) throw new Error(`workspace-bundle: unsupported record version ${String(value.version)}`);
838
+ return value;
839
+ }
840
+ async function canonicalPath(fs, path) {
841
+ try {
842
+ return await fs.realpath(path);
843
+ } catch (error) {
844
+ if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT" || dirname(path) === path) throw error;
845
+ return join(await canonicalPath(fs, dirname(path)), basename(path));
846
+ }
847
+ }
848
+ /**
849
+ * Reject stored paths whose canonical locations differ from the identity's controlled directories.
850
+ * @param fs - filesystem used by recovery, including realpath resolution.
851
+ * @param record - stored product record.
852
+ * @param root - materialize directory derived from the requested identity and current Spec.
853
+ * @returns fulfills when every stored path has the same canonical location as its controlled directory.
854
+ * @throws {WorkspaceBundleError} Before recovery mutates files when a stored path differs.
855
+ */
856
+ async function assertRecordPaths(fs, record, root) {
857
+ for (const [stored, controlled] of [
858
+ [record.path, root],
859
+ [record.journal.stagingPath, `${root}.staging`],
860
+ [record.journal.backupPath, `${root}.backup`]
861
+ ]) if (stored !== null && await canonicalPath(fs, stored) !== await canonicalPath(fs, controlled)) throw new WorkspaceBundleError("WORKSPACE_BUNDLE_PATH_TRAVERSAL", `stored Workspace paths must match the canonical controlled directory ${controlled}`);
862
+ }
863
+ /**
864
+ * Persist the journal as part of the product record.
865
+ * @param fs - filesystem.
866
+ * @param path - record.json path.
867
+ * @param record - current record.
868
+ * @param journal - next journal.
869
+ */
870
+ async function writeJournal(fs, path, record, journal) {
871
+ const next = {
872
+ ...record,
873
+ journal
874
+ };
875
+ await writeRecord(fs, path, next);
876
+ return next;
877
+ }
878
+ /**
879
+ * Mark a replacement as committed: idle journal, new digest and plugin version.
880
+ * @param record - current record.
881
+ * @param digest - verified tree digest.
882
+ * @param pluginVersion - packaged plugin version.
883
+ */
884
+ function committedRecord(record, digest, pluginVersion) {
885
+ return {
886
+ ...record,
887
+ pluginVersion,
888
+ templateDigest: digest,
889
+ journal: idleJournal(),
890
+ lastFailure: null
891
+ };
892
+ }
893
+ //#endregion
894
+ //#region lib/types/agent-config-source.js
895
+ /** 内容来源的有界拉取和只读版本缓存;此模块不替换运行中的 Workspace。 */
896
+ const AssignmentSchema = z.discriminatedUnion("source", [z.object({
897
+ source: z.literal("builtin"),
898
+ workspaceKind: z.string()
899
+ }), z.object({
900
+ source: z.literal("registry"),
901
+ workspaceKind: z.string(),
902
+ agentConfig: AgentConfigDescriptorSchema
903
+ })]);
904
+ const ReceiptSchema = AgentConfigDescriptorSchema.omit({
905
+ downloadUrl: true,
906
+ releaseNotes: true
907
+ });
908
+ function publicVersion(template, sha256) {
909
+ return {
910
+ workspaceKind: template.bundleId,
911
+ version: template.manifest.pluginVersion,
912
+ sha256,
913
+ hostAgentConfigApiVersion: HOST_AGENT_CONFIG_API_VERSION,
914
+ templateDigest: template.manifest.digest,
915
+ sourceRoot: template.sourceRoot
916
+ };
917
+ }
918
+ async function freezeTree(root) {
919
+ for (const entry of await readdir(root, { withFileTypes: true })) {
920
+ const path = join(root, entry.name);
921
+ if (entry.isDirectory()) await freezeTree(path);
922
+ else await chmod(path, 292);
923
+ }
924
+ await chmod(root, 365);
925
+ }
926
+ /** 一个注册来源的缓存、轮询和候选版本;卸载必须等待所有网络与文件工作结束。 */
927
+ var WorkspaceAgentConfigSourceOwner = class {
928
+ registration;
929
+ spec;
930
+ report;
931
+ download;
932
+ recoverIdle;
933
+ lifetime = new AbortController();
934
+ inFlight;
935
+ requestAbort;
936
+ requestRevision;
937
+ preparedRevisions = /* @__PURE__ */ new WeakMap();
938
+ timer;
939
+ selected;
940
+ candidate;
941
+ failure = null;
942
+ rejection;
943
+ admissionFailure;
944
+ root;
945
+ observation;
946
+ startup = Promise.resolve();
947
+ blockedVersion;
948
+ /** @param registration - 内置模板及鉴权查询回调。 @param spec - 部署预算。 @param report - 固定失败诊断的报告者。 @param download - 文件 HTTP 传输。 @param recoverIdle - Provider 串行检查活动租约后恢复已阻断的配置;省略时保留候选等待获取工作区。 */
949
+ constructor(registration, spec, report, download = fetch, recoverIdle = async () => {}) {
950
+ this.registration = registration;
951
+ this.spec = spec;
952
+ this.report = report;
953
+ this.download = download;
954
+ this.recoverIdle = recoverIdle;
955
+ this.selected = {
956
+ template: registration.template,
957
+ sha256: registration.template.agentConfigSha256 ?? null
958
+ };
959
+ this.root = bundledDirectory(join(spec.templateCacheRoot, "agent-config-versions"), registration.source.identity);
960
+ }
961
+ /** @returns 已应用/候选版本、执行阻断及当前作用域的最近失败和拒绝。 */
962
+ state() {
963
+ if (this.candidate !== void 0 && this.candidate.revision !== this.registration.source.revision()) this.candidate = void 0;
964
+ if (this.lifetime.signal.aborted || this.rejection?.revision !== this.registration.source.revision()) this.rejection = void 0;
965
+ return {
966
+ lastRejection: this.rejection?.value ?? null,
967
+ blocked: this.admissionFailure !== void 0,
968
+ blockingFailure: this.admissionFailure === void 0 ? null : { code: this.admissionFailure.code },
969
+ current: this.blockedVersion ?? publicVersion(this.selected.template, this.selected.sha256),
970
+ pending: this.candidate === void 0 ? null : publicVersion(this.candidate.template, this.candidate.sha256),
971
+ failure: this.admissionFailure === void 0 ? this.failure : {
972
+ code: this.admissionFailure.code,
973
+ message: this.admissionFailure.message
974
+ }
975
+ };
976
+ }
977
+ /**
978
+ * 从 journal 已提交的摘要定位只读缓存,不从 server 的无分配回答推导降级。
979
+ * @param digest - 已提交 Workspace 模板摘要;undefined 表示首次使用。
980
+ * @param version - 已提交完整版本,包括构建元数据。
981
+ * @param sha256 - 已提交包摘要;undefined 支持旧内置记录。
982
+ * @returns 对应完整模板;缺失已安装缓存时拒绝继续。
983
+ */
984
+ async recover(digest, version, sha256) {
985
+ const revision = this.registration.source.revision();
986
+ try {
987
+ const template = await this.recoverExecution(digest, version, sha256);
988
+ if (this.rejection?.revision === revision && this.rejection.value.version === template.manifest.pluginVersion) this.rejection = void 0;
989
+ return template;
990
+ } catch (error) {
991
+ if (digest !== void 0 && version !== void 0) this.blockedVersion = {
992
+ ...publicVersion(this.registration.template, sha256 ?? null),
993
+ version,
994
+ templateDigest: digest
995
+ };
996
+ const failure = new AgentConfigError(this.configError(error).code, "blocked");
997
+ const previous = this.admissionFailure;
998
+ this.admissionFailure = failure;
999
+ this.rememberRejection(failure, version, revision, sha256 ?? void 0);
1000
+ if (previous?.code !== failure.code) this.reportFailure(failure);
1001
+ throw failure;
1002
+ }
1003
+ }
1004
+ async recoverExecution(digest, version, sha256) {
1005
+ const restored = await this.restoredTemplate(digest, version, sha256);
1006
+ this.assertCompatible(restored.range, restored.runtimeApiRange);
1007
+ await this.registration.source.validate?.(restored.template, this.lifetime.signal);
1008
+ this.selected = {
1009
+ template: restored.template,
1010
+ sha256: restored.template.agentConfigSha256 ?? null
1011
+ };
1012
+ this.blockedVersion = void 0;
1013
+ this.admissionFailure = void 0;
1014
+ if (this.candidate?.sha256 === this.selected.sha256) this.candidate = void 0;
1015
+ return restored.template;
1016
+ }
1017
+ /**
1018
+ * Load a complete old/new tree only for physical swap recovery; this grants no execution permission.
1019
+ * @param digest - Journal template digest.
1020
+ * @param version - Journal release version, when recorded.
1021
+ * @param sha256 - Journal package digest, absent for builtin.
1022
+ * @returns Physically verified template; consumers must subsequently call recover for execution validation.
1023
+ */
1024
+ async recoverPhysical(digest, version, sha256) {
1025
+ return (await this.restoredTemplate(digest, version, sha256)).template;
1026
+ }
1027
+ async restoredTemplate(digest, version, sha256) {
1028
+ if (digest === void 0 || sha256 === void 0 || sha256 === null) {
1029
+ const template = this.registration.template;
1030
+ return {
1031
+ template,
1032
+ range: template.agentConfigReceipt?.hostAgentConfigApiRange,
1033
+ ...template.agentConfigReceipt?.runtimeApiRange === void 0 ? {} : { runtimeApiRange: template.agentConfigReceipt.runtimeApiRange }
1034
+ };
1035
+ }
1036
+ if (!/^[0-9a-f]{64}$/u.test(sha256)) throw new AgentConfigError("AGENT_CONFIG_INVALID");
1037
+ const fs = createNodeFs();
1038
+ const receipt = ReceiptSchema.parse(JSON.parse(await readFile(join(this.root, sha256, "receipt.json"), "utf8")));
1039
+ const directory = this.versionDirectory(receipt.version, receipt.sha256);
1040
+ if (receipt.sha256 !== sha256 || receipt.workspaceKind !== this.registration.template.bundleId || (await fs.lstat(directory)).isSymbolicLink() || (await fs.lstat(join(directory, receipt.version))).isSymbolicLink()) throw new AgentConfigError("AGENT_CONFIG_INVALID");
1041
+ const manifest = await loadVerifiedManifest(fs, join(directory, receipt.version));
1042
+ if (manifest.digest !== digest || version !== void 0 && receipt.version !== version || manifest.pluginVersion !== receipt.version) throw new AgentConfigError("AGENT_CONFIG_INVALID");
1043
+ const { bundleId, workspaceKey, title } = this.registration.template;
1044
+ return {
1045
+ template: {
1046
+ bundleId,
1047
+ workspaceKey,
1048
+ title,
1049
+ sourceRoot: join(directory, receipt.version),
1050
+ manifest,
1051
+ agentConfigSha256: receipt.sha256
1052
+ },
1053
+ range: receipt.hostAgentConfigApiRange,
1054
+ ...receipt.runtimeApiRange === void 0 ? {} : { runtimeApiRange: receipt.runtimeApiRange }
1055
+ };
1056
+ }
1057
+ assertCompatible(range, runtimeApiRange) {
1058
+ if (range !== void 0 && !semver.satisfies("2.0.0", range, { includePrerelease: true })) throw new AgentConfigError("AGENT_CONFIG_INCOMPATIBLE");
1059
+ const runtime = this.registration.source.runtimeApiVersion;
1060
+ if (runtimeApiRange !== void 0 && (runtime === void 0 || !semver.satisfies(runtime, runtimeApiRange, { includePrerelease: true }))) throw new AgentConfigError("AGENT_CONFIG_INCOMPATIBLE");
1061
+ }
1062
+ configError(error) {
1063
+ return error instanceof AgentConfigError ? error : new AgentConfigError(error instanceof WorkspaceBundleError && error.code === "WORKSPACE_BUNDLE_DIGEST_MISMATCH" ? "AGENT_CONFIG_DIGEST_MISMATCH" : "AGENT_CONFIG_INVALID");
1064
+ }
1065
+ reportFailure(error) {
1066
+ try {
1067
+ this.report(error);
1068
+ } catch (reportError) {
1069
+ process.stderr.write(`${error.code}: ${error.message}\n`);
1070
+ }
1071
+ }
1072
+ /** @param restore - Restore this identity's committed journal before starting reports or queries; omission is for an owner without a journal. */
1073
+ start(restore = async () => {}) {
1074
+ this.startup = restore().then(() => {
1075
+ if (!this.lifetime.signal.aborted) this.observation = this.registration.source.observation?.(() => this.state());
1076
+ });
1077
+ const tick = async () => {
1078
+ try {
1079
+ await this.refresh();
1080
+ } catch (error) {
1081
+ this.reportFailure(this.configError(error));
1082
+ }
1083
+ if (!this.lifetime.signal.aborted) this.timer = setTimeout(() => {
1084
+ tick();
1085
+ }, this.spec.agentConfigPollIntervalMs);
1086
+ };
1087
+ tick();
1088
+ }
1089
+ /** @returns 本次查询和完整候选下载结束;失败通过 state 与 report 保留可观察诊断。 */
1090
+ refresh() {
1091
+ return this.startup.then(() => this.refreshReady());
1092
+ }
1093
+ refreshReady() {
1094
+ if (this.lifetime.signal.aborted) return Promise.resolve();
1095
+ const revision = this.registration.source.revision();
1096
+ if (this.candidate !== void 0 && this.candidate.revision !== revision) this.candidate = void 0;
1097
+ if (this.inFlight !== void 0) {
1098
+ if (revision === this.requestRevision && !this.requestAbort?.signal.aborted) return this.inFlight;
1099
+ this.requestAbort?.abort();
1100
+ this.failure = null;
1101
+ return this.inFlight.then(() => this.refresh());
1102
+ }
1103
+ const abort = new AbortController();
1104
+ this.requestAbort = abort;
1105
+ this.requestRevision = revision;
1106
+ const attempt = {};
1107
+ const run = this.fetchCandidate(revision, abort.signal, attempt).then(async () => {
1108
+ if (this.lifetime.signal.aborted || abort.signal.aborted || revision !== this.registration.source.revision()) return;
1109
+ if (this.admissionFailure !== void 0 && this.candidate !== void 0) await this.recoverIdle();
1110
+ if (!this.lifetime.signal.aborted) await this.observation?.refresh();
1111
+ }).catch((error) => {
1112
+ if (this.lifetime.signal.aborted || abort.signal.aborted || revision !== this.registration.source.revision()) return;
1113
+ this.candidate = void 0;
1114
+ const failure = this.configError(error);
1115
+ this.failure = {
1116
+ code: failure.code,
1117
+ message: failure.message
1118
+ };
1119
+ this.rememberRejection(failure, attempt.version, revision, attempt.sha256);
1120
+ this.reportFailure(failure);
1121
+ }).finally(() => {
1122
+ this.inFlight = void 0;
1123
+ this.requestAbort = void 0;
1124
+ this.requestRevision = void 0;
1125
+ });
1126
+ this.inFlight = run;
1127
+ return run;
1128
+ }
1129
+ /** @returns 下一个 turn 可应用的候选;没有候选时使用已安装版本。 */
1130
+ template() {
1131
+ if (this.candidate !== void 0 && this.candidate.revision !== this.registration.source.revision()) this.candidate = void 0;
1132
+ if (this.candidate === void 0 && this.admissionFailure !== void 0) throw this.admissionFailure;
1133
+ return this.candidate?.template ?? this.selected.template;
1134
+ }
1135
+ /** @throws AgentConfigError while committed content is blocked, including when another lease remains active. */
1136
+ assertExecutionAllowed() {
1137
+ if (this.admissionFailure !== void 0) throw this.admissionFailure;
1138
+ }
1139
+ /** @param template - Pending template. @returns After compatibility and consumer validation, without selecting it. */
1140
+ async prepare(template) {
1141
+ try {
1142
+ await this.prepareValidated(template);
1143
+ } catch (error) {
1144
+ throw this.configError(error);
1145
+ }
1146
+ }
1147
+ async prepareValidated(template) {
1148
+ this.assertApplicable(template);
1149
+ const restored = await this.restoredTemplate(template.manifest.digest, template.manifest.pluginVersion, journalAgentConfigSha256(template, this.registration.template));
1150
+ this.assertCompatible(restored.range, restored.runtimeApiRange);
1151
+ await this.registration.source.validate?.(restored.template, this.lifetime.signal);
1152
+ this.assertApplicable(template);
1153
+ }
1154
+ /** @param template - 即将整体替换的候选。 @throws 来源代次已经改变或已撤销时拒绝应用。 */
1155
+ assertApplicable(template) {
1156
+ const revision = this.preparedRevisions.get(template);
1157
+ if (this.lifetime.signal.aborted || revision !== void 0 && revision !== this.registration.source.revision()) throw new AgentConfigError("AGENT_CONFIG_SCOPE_CHANGED");
1158
+ }
1159
+ /** @param template - 已通过 Workspace 整体替换提交的模板。 */
1160
+ async committed(template) {
1161
+ await this.recover(template.manifest.digest, template.manifest.pluginVersion, journalAgentConfigSha256(template, this.registration.template));
1162
+ }
1163
+ /**
1164
+ * 清除未提交候选,报告整体替换失败;已提交状态由 journal 恢复结果决定。
1165
+ * @param template - 未能完成提交的模板。
1166
+ * @param cause - 保留来源失效分类;其他原因报告固定应用失败诊断。
1167
+ */
1168
+ failedApply(template, cause) {
1169
+ if (this.candidate?.template === template) this.candidate = void 0;
1170
+ const error = cause instanceof AgentConfigError ? cause : new AgentConfigError("AGENT_CONFIG_APPLY_FAILED");
1171
+ this.failure = {
1172
+ code: error.code,
1173
+ message: error.message
1174
+ };
1175
+ const revision = this.preparedRevisions.get(template) ?? this.registration.source.revision();
1176
+ this.rememberRejection(error, template.manifest.pluginVersion, revision, template.agentConfigSha256);
1177
+ this.reportFailure(error);
1178
+ }
1179
+ /** 撤销查询、清除后续定时器,并等待下载和目录写入完成。 */
1180
+ async dispose() {
1181
+ this.lifetime.abort();
1182
+ this.rejection = void 0;
1183
+ this.candidate = void 0;
1184
+ if (this.timer !== void 0) clearTimeout(this.timer);
1185
+ await Promise.all([
1186
+ this.startup,
1187
+ this.inFlight,
1188
+ this.observation?.dispose()
1189
+ ]);
1190
+ this.candidate = void 0;
1191
+ }
1192
+ versionDirectory(version, sha256) {
1193
+ if (!AgentConfigDescriptorSchema.shape.version.safeParse(version).success || !/^[0-9a-f]{64}$/u.test(sha256)) throw new AgentConfigError("AGENT_CONFIG_INVALID");
1194
+ return join(this.root, sha256);
1195
+ }
1196
+ rememberRejection(error, version, revision, sha256) {
1197
+ if (this.lifetime.signal.aborted || revision !== this.registration.source.revision()) return;
1198
+ if (error.code === "AGENT_CONFIG_SCOPE_CHANGED") {
1199
+ this.rejection = void 0;
1200
+ return;
1201
+ }
1202
+ if (error.code === "AGENT_CONFIG_QUERY_FAILED") return;
1203
+ this.rejection = {
1204
+ revision,
1205
+ value: {
1206
+ code: error.code,
1207
+ ...version === void 0 ? {} : { version },
1208
+ ...sha256 === void 0 ? {} : { sha256 }
1209
+ }
1210
+ };
1211
+ }
1212
+ async fetchCandidate(revision, requestSignal, attempt) {
1213
+ const signal = AbortSignal.any([
1214
+ this.lifetime.signal,
1215
+ requestSignal,
1216
+ AbortSignal.timeout(this.spec.agentConfigRequestTimeoutMs)
1217
+ ]);
1218
+ let value;
1219
+ try {
1220
+ value = await this.registration.source.query(signal);
1221
+ } catch (error) {
1222
+ throw error instanceof AgentConfigError ? error : new AgentConfigError("AGENT_CONFIG_QUERY_FAILED");
1223
+ }
1224
+ const assignment = AssignmentSchema.safeParse(value);
1225
+ if (!assignment.success || assignment.data.workspaceKind !== this.registration.template.bundleId) throw new AgentConfigError("AGENT_CONFIG_INVALID");
1226
+ if (assignment.data.source === "builtin") {
1227
+ this.candidate = void 0;
1228
+ this.failure = null;
1229
+ this.rejection = void 0;
1230
+ return;
1231
+ }
1232
+ const descriptor = assignment.data.agentConfig;
1233
+ attempt.version = descriptor.version;
1234
+ attempt.sha256 = descriptor.sha256;
1235
+ if (this.rejection?.value.version !== descriptor.version || this.rejection?.value.sha256 !== descriptor.sha256) this.rejection = void 0;
1236
+ if (descriptor.workspaceKind !== assignment.data.workspaceKind) throw new AgentConfigError("AGENT_CONFIG_INVALID");
1237
+ this.assertCompatible(descriptor.hostAgentConfigApiRange, descriptor.runtimeApiRange);
1238
+ if (descriptor.sha256 === (this.blockedVersion?.sha256 ?? this.selected.sha256)) {
1239
+ this.candidate = void 0;
1240
+ this.failure = null;
1241
+ this.rejection = void 0;
1242
+ return;
1243
+ }
1244
+ if (descriptor.sizeBytes > this.spec.agentConfigMaxDownloadBytes) throw new AgentConfigError("AGENT_CONFIG_INVALID");
1245
+ const url = new URL(descriptor.downloadUrl);
1246
+ if (url.username !== "" || url.password !== "" || url.protocol !== "https:" && !(this.spec.allowLoopbackAgentConfigHttp && url.protocol === "http:" && [
1247
+ "localhost",
1248
+ "127.0.0.1",
1249
+ "[::1]"
1250
+ ].includes(url.hostname))) throw new AgentConfigError("AGENT_CONFIG_INVALID");
1251
+ let bytes;
1252
+ try {
1253
+ const response = await this.download(url, {
1254
+ signal,
1255
+ redirect: "error"
1256
+ });
1257
+ if (!response.ok || response.body === null || response.headers.get("content-length") !== null && Number(response.headers.get("content-length")) !== descriptor.sizeBytes) throw new AgentConfigError("AGENT_CONFIG_DOWNLOAD_FAILED");
1258
+ const chunks = [];
1259
+ let count = 0;
1260
+ for await (const chunk of response.body) {
1261
+ count += chunk.byteLength;
1262
+ if (count > descriptor.sizeBytes) {
1263
+ await response.body.cancel().catch(() => void 0);
1264
+ throw new AgentConfigError("AGENT_CONFIG_DOWNLOAD_FAILED");
1265
+ }
1266
+ chunks.push(chunk);
1267
+ }
1268
+ bytes = Buffer.concat(chunks);
1269
+ } catch (error) {
1270
+ throw new AgentConfigError("AGENT_CONFIG_DOWNLOAD_FAILED");
1271
+ }
1272
+ const content = decodeAgentConfig(bytes, descriptor, this.spec.agentConfigMaxExpandedBytes);
1273
+ if (signal.aborted || revision !== this.registration.source.revision()) return;
1274
+ await mkdir(this.root, { recursive: true });
1275
+ const directory = this.versionDirectory(descriptor.version, descriptor.sha256);
1276
+ const tree = join(directory, descriptor.version);
1277
+ const fs = createNodeFs();
1278
+ if (!await fs.exists(directory)) {
1279
+ const staging = await mkdtemp(join(this.root, ".staging-"));
1280
+ try {
1281
+ const stagingTree = join(staging, descriptor.version);
1282
+ await mkdir(stagingTree);
1283
+ for (const file of content.files) {
1284
+ const path = join(stagingTree, ...file.path.split("/"));
1285
+ await mkdir(dirname(path), { recursive: true });
1286
+ await writeFile(path, file.bytes, { flag: "wx" });
1287
+ }
1288
+ const files = content.manifest.files.map((file) => ({
1289
+ path: file.path,
1290
+ bytes: file.sizeBytes,
1291
+ digest: brandString(file.sha256)
1292
+ }));
1293
+ await writeFile(join(stagingTree, "manifest.json"), JSON.stringify({
1294
+ formatVersion: 1,
1295
+ pluginVersion: descriptor.version,
1296
+ files,
1297
+ digest: overallDigest(files)
1298
+ }) + "\n");
1299
+ const { downloadUrl: _downloadUrl, releaseNotes: _releaseNotes, ...receipt } = descriptor;
1300
+ await writeFile(join(staging, "receipt.json"), JSON.stringify(ReceiptSchema.parse(receipt)) + "\n");
1301
+ await rename(staging, directory);
1302
+ await freezeTree(tree);
1303
+ await chmod(join(directory, "receipt.json"), 292);
1304
+ } finally {
1305
+ await rm(staging, {
1306
+ recursive: true,
1307
+ force: true
1308
+ });
1309
+ }
1310
+ }
1311
+ const manifest = await loadVerifiedManifest(fs, tree);
1312
+ const files = content.manifest.files.map((file) => ({
1313
+ path: file.path,
1314
+ bytes: file.sizeBytes,
1315
+ digest: brandString(file.sha256)
1316
+ }));
1317
+ if (manifest.pluginVersion !== descriptor.version || manifest.digest !== overallDigest(files)) throw new AgentConfigError("AGENT_CONFIG_DIGEST_MISMATCH");
1318
+ if (signal.aborted || revision !== this.registration.source.revision()) return;
1319
+ const { bundleId, workspaceKey, title } = this.registration.template;
1320
+ const template = {
1321
+ bundleId,
1322
+ workspaceKey,
1323
+ title,
1324
+ sourceRoot: tree,
1325
+ manifest,
1326
+ agentConfigSha256: descriptor.sha256
1327
+ };
1328
+ await this.registration.source.validate?.(template, signal);
1329
+ if (signal.aborted || revision !== this.registration.source.revision()) return;
1330
+ this.preparedRevisions.set(template, revision);
1331
+ this.candidate = {
1332
+ template,
1333
+ sha256: descriptor.sha256,
1334
+ revision
1335
+ };
1336
+ this.failure = null;
1337
+ this.rejection = void 0;
1338
+ }
1339
+ };
1340
+ //#endregion
1341
+ //#region lib/types/provider.js
1342
+ /**
1343
+ * Host Provider of `WorkspaceBundles`: register a packaged template, materialize a stable
1344
+ * hidden Workspace, and replace its tree as a whole with crash recovery.
1345
+ */
1346
+ function keyOf(identity) {
1347
+ return `${identity.bundleId}/${identity.workspaceKey}`;
1348
+ }
1349
+ function originOf(identity) {
1350
+ return {
1351
+ kind: "bundle",
1352
+ bundleId: identity.bundleId,
1353
+ workspaceKey: identity.workspaceKey
1354
+ };
1355
+ }
1356
+ async function copyTree(fs, from, to, files) {
1357
+ await fs.rm(to, { recursive: true });
1358
+ await fs.mkdir(to);
1359
+ for (const file of files) {
1360
+ const relative = file.path.split("/");
1361
+ const destination = join(to, ...relative);
1362
+ await fs.mkdir(join(destination, ".."));
1363
+ await fs.copyFile(join(from, ...relative), destination);
1364
+ }
1365
+ }
1366
+ /** Provider for packaged Workspace templates. */
1367
+ var GestaltWorkspaceBundles = class extends WorkspaceBundles {
1368
+ static inject = ["workspaceRegistry"];
1369
+ static Config = Config;
1370
+ /** Deployment settings resolved at load. */
1371
+ spec;
1372
+ fs;
1373
+ templates = /* @__PURE__ */ new Map();
1374
+ agentConfigSources = /* @__PURE__ */ new Map();
1375
+ records = /* @__PURE__ */ new Map();
1376
+ leases = /* @__PURE__ */ new Map();
1377
+ waiters = /* @__PURE__ */ new Map();
1378
+ chain = Promise.resolve();
1379
+ recycle = /* @__PURE__ */ new Map();
1380
+ recycleWork = /* @__PURE__ */ new Map();
1381
+ contentDrains = /* @__PURE__ */ new Set();
1382
+ closed = false;
1383
+ download;
1384
+ /**
1385
+ * @param ctx - Host context with the Workspace registry.
1386
+ * @param config - raw row settings.
1387
+ * @param options.fs - filesystem; tests inject crashes through `crashAt`.
1388
+ * @param options.crashAt - single crash point; production omits it.
1389
+ * @param options.fetch - 文件 HTTP 传输;默认使用 Host fetch。
1390
+ */
1391
+ constructor(ctx, config = {}, options = {}) {
1392
+ const spec = resolveWorkspaceBundlesSpec(config);
1393
+ super(ctx, join(spec.journalRoot, "local-host-replacement.json"), join(spec.journalRoot, "local-host-identity.json"));
1394
+ this.spec = spec;
1395
+ this.fs = crashableFs(options.fs ?? createNodeFs(), options.crashAt);
1396
+ this.download = options.fetch ?? fetch;
1397
+ ctx.effect(() => async () => {
1398
+ this.closed = true;
1399
+ for (const timer of this.recycle.values()) clearTimeout(timer);
1400
+ this.recycle.clear();
1401
+ await Promise.all([...this.agentConfigSources.values()].map((source) => source.dispose()));
1402
+ await this.chain;
1403
+ await Promise.all(this.contentDrains);
1404
+ await Promise.all(this.recycleWork.values());
1405
+ this.agentConfigSources.clear();
1406
+ }, "workspace-bundle: recycle timers");
1407
+ }
1408
+ /** @inheritdoc */
1409
+ register(template) {
1410
+ if (this.closed) throw new WorkspaceBundleError("WORKSPACE_BUNDLE_NOT_REGISTERED", "Workspace 内容服务已停止");
1411
+ identityOf(template.bundleId, template.workspaceKey);
1412
+ const key = keyOf(template);
1413
+ this.templates.set(key, template);
1414
+ const record = this.records.get(key);
1415
+ if (record !== void 0) this.records.set(key, {
1416
+ ...record,
1417
+ registered: true
1418
+ });
1419
+ return () => {
1420
+ if (this.templates.get(key) !== template) return;
1421
+ this.templates.delete(key);
1422
+ const current = this.records.get(key);
1423
+ if (current !== void 0) this.records.set(key, {
1424
+ ...current,
1425
+ registered: false
1426
+ });
1427
+ };
1428
+ }
1429
+ /** @inheritdoc */
1430
+ ensure(identity) {
1431
+ if (this.closed) return Promise.reject(new WorkspaceBundleError("WORKSPACE_BUNDLE_NOT_REGISTERED", "Workspace 内容服务已停止"));
1432
+ identityOf(identity.bundleId, identity.workspaceKey);
1433
+ return this.enqueue(() => this.ensureOne(identity));
1434
+ }
1435
+ /** @inheritdoc */
1436
+ registerAgentConfigSource(source) {
1437
+ if (this.closed) throw new WorkspaceBundleError("WORKSPACE_BUNDLE_NOT_REGISTERED", "Workspace 内容服务已停止");
1438
+ const key = keyOf(source.identity);
1439
+ const template = this.requireTemplate(source.identity);
1440
+ if (this.agentConfigSources.has(key)) throw new WorkspaceBundleError("WORKSPACE_BUNDLE_TEMPLATE_INVALID", "Agent 配置版本来源已注册");
1441
+ const owner = new WorkspaceAgentConfigSourceOwner({
1442
+ template,
1443
+ source
1444
+ }, this.spec, (error) => {
1445
+ this.ctx.logger.warn("%s:%s", error.code, error.message);
1446
+ }, this.download, () => this.enqueue(async () => {
1447
+ if (this.closed || this.agentConfigSources.get(key) !== owner) return;
1448
+ const state = owner.state();
1449
+ const record = this.records.get(key);
1450
+ if (!state.blocked || state.pending === null || record?.workspaceId != null && (this.leases.get(record.workspaceId)?.size ?? 0) > 0) return;
1451
+ await this.ensureOne(source.identity);
1452
+ }));
1453
+ this.agentConfigSources.set(key, owner);
1454
+ owner.start(() => this.enqueue(async () => {
1455
+ await this.recoverOne(source.identity);
1456
+ const record = this.records.get(key);
1457
+ try {
1458
+ await owner.recover(record?.templateDigest, record?.pluginVersion, record?.agentConfigSha256);
1459
+ } catch (error) {
1460
+ if (!(error instanceof AgentConfigError)) throw error;
1461
+ }
1462
+ }));
1463
+ return async () => {
1464
+ await owner.dispose();
1465
+ if (this.agentConfigSources.get(key) === owner) this.agentConfigSources.delete(key);
1466
+ };
1467
+ }
1468
+ /** @inheritdoc */
1469
+ agentConfig(identity) {
1470
+ return this.agentConfigSources.get(keyOf(identity))?.state();
1471
+ }
1472
+ /** @inheritdoc */
1473
+ async refreshAgentConfig(identity) {
1474
+ const source = this.agentConfigSources.get(keyOf(identity));
1475
+ if (source === void 0) throw new WorkspaceBundleError("WORKSPACE_BUNDLE_NOT_REGISTERED", "Agent 配置版本来源未注册");
1476
+ await source.refresh();
1477
+ }
1478
+ /** @inheritdoc */
1479
+ async acquire(workspaceId) {
1480
+ if (this.closed) throw new WorkspaceBundleError("WORKSPACE_BUNDLE_NOT_REGISTERED", "Workspace 内容服务已停止");
1481
+ if ([...this.records.values()].some((record) => record.workspaceId === workspaceId && record.journal.phase === "updating")) throw new WorkspaceBundleError("WORKSPACE_BUNDLE_NOT_REGISTERED", `workspace ${workspaceId} is updating`);
1482
+ return this.enqueue(() => this.acquireOne(workspaceId));
1483
+ }
1484
+ /** @inheritdoc */
1485
+ acquireAgentConfig(identity) {
1486
+ if (this.closed) return Promise.reject(new WorkspaceBundleError("WORKSPACE_BUNDLE_NOT_REGISTERED", "Workspace content service is stopped"));
1487
+ identityOf(identity.bundleId, identity.workspaceKey);
1488
+ return this.enqueue(async () => {
1489
+ if (this.closed) throw new WorkspaceBundleError("WORKSPACE_BUNDLE_NOT_REGISTERED", "Workspace content service is stopped");
1490
+ const registered = this.requireTemplate(identity);
1491
+ const source = this.agentConfigSources.get(keyOf(identity));
1492
+ const revision = source?.registration.source.revision();
1493
+ const workspace = await this.ensureOne(identity);
1494
+ const record = this.records.get(keyOf(identity));
1495
+ const template = source === void 0 ? this.requireTemplate(identity) : await source.recover(record.templateDigest, record.pluginVersion, record.agentConfigSha256);
1496
+ await verifyTemplateTree(this.fs, template.sourceRoot, template.manifest);
1497
+ if (this.closed) throw new WorkspaceBundleError("WORKSPACE_BUNDLE_NOT_REGISTERED", "Workspace content service is stopped");
1498
+ if (this.requireTemplate(identity) !== registered || this.agentConfigSources.get(keyOf(identity)) !== source || source?.registration.source.revision() !== revision) throw new AgentConfigError("AGENT_CONFIG_SCOPE_CHANGED");
1499
+ if (template.manifest.digest !== record.templateDigest || template.manifest.pluginVersion !== record.pluginVersion) throw new AgentConfigError("AGENT_CONFIG_INVALID");
1500
+ const version = {
1501
+ workspaceKind: identity.bundleId,
1502
+ workspaceId: workspace.id,
1503
+ version: record.pluginVersion,
1504
+ sha256: template.agentConfigSha256 ?? null,
1505
+ hostAgentConfigApiVersion: HOST_AGENT_CONFIG_API_VERSION,
1506
+ templateDigest: record.templateDigest,
1507
+ sourceRoot: template.sourceRoot
1508
+ };
1509
+ return this.contentHandle(version, this.holdLease(workspace.id));
1510
+ });
1511
+ }
1512
+ contentHandle(version, release) {
1513
+ let references = 0;
1514
+ const drained = Promise.withResolvers();
1515
+ this.contentDrains.add(drained.promise);
1516
+ const retain = () => {
1517
+ references++;
1518
+ let released = false;
1519
+ return Object.freeze({
1520
+ ...version,
1521
+ retain: () => {
1522
+ if (released || this.closed) throw new WorkspaceBundleError("WORKSPACE_BUNDLE_NOT_REGISTERED", "The content activity is closed");
1523
+ return retain();
1524
+ },
1525
+ release: () => {
1526
+ if (released) return;
1527
+ released = true;
1528
+ if (--references === 0) {
1529
+ release();
1530
+ this.contentDrains.delete(drained.promise);
1531
+ drained.resolve();
1532
+ }
1533
+ }
1534
+ });
1535
+ };
1536
+ return retain();
1537
+ }
1538
+ async acquireOne(workspaceId) {
1539
+ const record = [...this.records.values()].find((item) => item.workspaceId === workspaceId);
1540
+ if (record === void 0 || !record.registered) throw new WorkspaceBundleError("WORKSPACE_BUNDLE_NOT_REGISTERED", `workspace ${workspaceId} is not an acquired bundle Workspace`);
1541
+ if (record.journal.phase === "updating") throw new WorkspaceBundleError("WORKSPACE_BUNDLE_NOT_REGISTERED", `workspace ${workspaceId} is updating`);
1542
+ if ((this.leases.get(workspaceId)?.size ?? 0) === 0 && this.agentConfigSources.has(keyOf(record))) await this.ensureOne(record);
1543
+ this.agentConfigSources.get(keyOf(record))?.assertExecutionAllowed();
1544
+ return this.holdLease(workspaceId);
1545
+ }
1546
+ holdLease(workspaceId) {
1547
+ const token = Symbol("workspace-bundle-lease");
1548
+ let leases = this.leases.get(workspaceId);
1549
+ if (leases === void 0) {
1550
+ leases = /* @__PURE__ */ new Set();
1551
+ this.leases.set(workspaceId, leases);
1552
+ }
1553
+ leases.add(token);
1554
+ return () => {
1555
+ leases.delete(token);
1556
+ if (leases.size === 0) this.leases.delete(workspaceId);
1557
+ const waiters = this.waiters.get(workspaceId);
1558
+ if (waiters !== void 0) for (const waiter of waiters) waiter();
1559
+ };
1560
+ }
1561
+ /** @inheritdoc */
1562
+ record(identity) {
1563
+ return this.records.get(keyOf(identity));
1564
+ }
1565
+ /** @inheritdoc */
1566
+ pathOf(identity) {
1567
+ identityOf(identity.bundleId, identity.workspaceKey);
1568
+ return bundledDirectory(this.spec.materializeRoot, identity);
1569
+ }
1570
+ /**
1571
+ * Replay an interrupted journal before serving the Workspace.
1572
+ * @param identity - branded pair.
1573
+ * @throws {WorkspaceBundleError} Before any rename or deletion if stored paths differ from this identity's controlled paths.
1574
+ * @throws {AgentConfigError} After physical recovery when committed content fails compatibility or consumer validation.
1575
+ */
1576
+ async recover(identity) {
1577
+ return this.enqueue(async () => {
1578
+ const workspace = await this.recoverOne(identity);
1579
+ const record = this.records.get(keyOf(identity));
1580
+ await this.agentConfigSources.get(keyOf(identity))?.recover(record?.templateDigest, record?.pluginVersion, record?.agentConfigSha256);
1581
+ return workspace;
1582
+ });
1583
+ }
1584
+ enqueue(work) {
1585
+ const run = this.chain.then(work, work);
1586
+ this.chain = run.then(() => void 0, () => void 0);
1587
+ return run;
1588
+ }
1589
+ async ensureOne(identity) {
1590
+ let template = this.requireTemplate(identity);
1591
+ const source = this.agentConfigSources.get(keyOf(identity));
1592
+ const recovered = await this.recoverOne(identity);
1593
+ const record = this.records.get(keyOf(identity));
1594
+ if (source !== void 0) try {
1595
+ template = await source.recover(record?.templateDigest, record?.pluginVersion, record?.agentConfigSha256);
1596
+ } catch (error) {
1597
+ if (!(error instanceof AgentConfigError) || record?.workspaceId !== null && record?.workspaceId !== void 0 && (this.leases.get(record.workspaceId)?.size ?? 0) > 0) throw error;
1598
+ template = source.template();
1599
+ }
1600
+ if (source !== void 0 && (record?.workspaceId === null || record?.workspaceId === void 0 || (this.leases.get(record.workspaceId)?.size ?? 0) === 0)) template = source.template();
1601
+ if (recovered !== void 0 && record !== void 0 && record.templateDigest === template.manifest.digest && record.pluginVersion === template.manifest.pluginVersion && (record.agentConfigSha256 ?? null) === journalAgentConfigSha256(template, this.requireTemplate(identity)) && record.journal.phase === "idle") return recovered;
1602
+ try {
1603
+ await source?.prepare(template);
1604
+ const workspace = await this.replace(template, record, source === void 0 ? void 0 : () => source.assertApplicable(template));
1605
+ await source?.committed(template);
1606
+ return workspace;
1607
+ } catch (error) {
1608
+ if (source === void 0) throw error;
1609
+ if (error instanceof AgentConfigError && error.code === "AGENT_CONFIG_SCOPE_CHANGED") await this.rollbackAgentConfig(identity, record);
1610
+ else await this.recoverOne(identity);
1611
+ const restored = this.records.get(keyOf(identity));
1612
+ await source.recover(restored?.templateDigest, restored?.pluginVersion, restored?.agentConfigSha256);
1613
+ source.failedApply(template, error);
1614
+ if (error instanceof AgentConfigError && error.code === "AGENT_CONFIG_SCOPE_CHANGED" && !this.closed) return (restored === void 0 || !await this.fs.exists(this.pathOf(identity)) ? void 0 : await this.workspaceOf(restored)) ?? this.replace(this.requireTemplate(identity), restored);
1615
+ this.ctx.logger.warn("Agent 配置应用失败,已尝试按 journal 恢复;当前版本未确认。");
1616
+ throw error;
1617
+ }
1618
+ }
1619
+ async rollbackAgentConfig(identity, previous) {
1620
+ const current = this.records.get(keyOf(identity));
1621
+ if (current === void 0) {
1622
+ await this.fs.rm(this.stagingOf(identity), { recursive: true });
1623
+ return;
1624
+ }
1625
+ if (current === previous && current.journal.phase === "idle") {
1626
+ await this.fs.rm(this.stagingOf(identity), { recursive: true });
1627
+ return;
1628
+ }
1629
+ const builtin = this.requireTemplate(identity);
1630
+ const rollback = {
1631
+ ...previous ?? {
1632
+ ...current,
1633
+ pluginVersion: builtin.manifest.pluginVersion,
1634
+ templateDigest: brandString("0".repeat(64)),
1635
+ agentConfigSha256: null
1636
+ },
1637
+ journal: {
1638
+ ...current.journal,
1639
+ phase: "updating",
1640
+ fromDigest: previous?.templateDigest ?? null,
1641
+ toDigest: current.templateDigest,
1642
+ backupPath: await this.fs.exists(this.backupOf(identity)) ? this.backupOf(identity) : null,
1643
+ stagingPath: this.stagingOf(identity),
1644
+ swapComplete: false
1645
+ }
1646
+ };
1647
+ this.records.set(keyOf(identity), rollback);
1648
+ await writeRecord(this.fs, this.recordFile(identity), rollback);
1649
+ await this.restoreOld(identity, rollback);
1650
+ }
1651
+ requireTemplate(identity) {
1652
+ const template = this.templates.get(keyOf(identity));
1653
+ if (template === void 0) throw new WorkspaceBundleError("WORKSPACE_BUNDLE_NOT_REGISTERED", `${keyOf(identity)} is not registered`);
1654
+ return template;
1655
+ }
1656
+ stagingOf(identity) {
1657
+ return `${this.pathOf(identity)}.staging`;
1658
+ }
1659
+ backupOf(identity) {
1660
+ return `${this.pathOf(identity)}.backup`;
1661
+ }
1662
+ recordFile(identity) {
1663
+ return recordPath(this.spec.journalRoot, identity);
1664
+ }
1665
+ async loadStored(identity) {
1666
+ const stored = await readRecord(this.fs, this.recordFile(identity));
1667
+ if (stored !== void 0) this.records.set(keyOf(identity), stored);
1668
+ return stored;
1669
+ }
1670
+ async recoverOne(identity) {
1671
+ const stored = this.records.get(keyOf(identity)) ?? await this.loadStored(identity);
1672
+ if (stored === void 0) return void 0;
1673
+ await assertRecordPaths(this.fs, stored, this.pathOf(identity));
1674
+ if (stored.journal.phase === "idle") return this.workspaceOf(stored);
1675
+ return stored.journal.swapComplete ? this.finishNew(identity, stored) : this.restoreOld(identity, stored);
1676
+ }
1677
+ async workspaceOf(record) {
1678
+ if (record.workspaceId === null) return void 0;
1679
+ return this.ctx.workspaceRegistry.get(record.workspaceId) ?? await this.ctx.workspaceRegistry.create(record.path, void 0, originOf({
1680
+ bundleId: record.bundleId,
1681
+ workspaceKey: record.workspaceKey
1682
+ }));
1683
+ }
1684
+ async restoreOld(identity, record) {
1685
+ const root = this.pathOf(identity);
1686
+ const backup = record.journal.backupPath ?? this.backupOf(identity);
1687
+ const staging = record.journal.stagingPath ?? this.stagingOf(identity);
1688
+ if (await this.fs.exists(backup)) {
1689
+ if (await this.fs.exists(root)) await this.fs.rm(root, { recursive: true });
1690
+ await this.fs.rename(backup, root);
1691
+ } else if (record.journal.fromDigest === null && await this.fs.exists(root)) await this.fs.rm(root, { recursive: true });
1692
+ if (await this.fs.exists(staging)) await this.fs.rm(staging, { recursive: true });
1693
+ const next = {
1694
+ ...record,
1695
+ journal: idleJournal(),
1696
+ lastFailure: record.lastFailure
1697
+ };
1698
+ await writeRecord(this.fs, this.recordFile(identity), next);
1699
+ this.records.set(keyOf(identity), next);
1700
+ return this.workspaceOf(next);
1701
+ }
1702
+ async finishNew(identity, record) {
1703
+ let template = this.templates.get(keyOf(identity));
1704
+ const root = this.pathOf(identity);
1705
+ const toDigest = record.journal.toDigest;
1706
+ try {
1707
+ const source = this.agentConfigSources.get(keyOf(identity));
1708
+ if (source !== void 0 && toDigest !== null) template = await source.recoverPhysical(toDigest, record.journal.pluginVersion ?? void 0, record.journal.agentConfigSha256);
1709
+ if (template !== void 0 && toDigest === template.manifest.digest) {
1710
+ await verifyTemplateTree(this.fs, root, template.manifest);
1711
+ return await this.commit(identity, record, template);
1712
+ }
1713
+ if (toDigest !== null && template === void 0) {
1714
+ const cached = bundledDirectory(this.spec.templateCacheRoot, identity);
1715
+ const manifest = await loadVerifiedManifest(this.fs, cached);
1716
+ if (manifest.digest === toDigest) {
1717
+ await verifyTemplateTree(this.fs, root, manifest);
1718
+ const synthetic = {
1719
+ bundleId: identity.bundleId,
1720
+ workspaceKey: identity.workspaceKey,
1721
+ title: "Workspace",
1722
+ sourceRoot: cached,
1723
+ manifest
1724
+ };
1725
+ return await this.commit(identity, record, synthetic);
1726
+ }
1727
+ }
1728
+ } catch (error) {}
1729
+ return this.restoreOld(identity, record);
1730
+ }
1731
+ async replace(template, existing, assertApplicable) {
1732
+ const identity = identityOf(template.bundleId, template.workspaceKey);
1733
+ assertApplicable?.();
1734
+ await loadVerifiedManifest(this.fs, template.sourceRoot);
1735
+ const cache = bundledDirectory(this.spec.templateCacheRoot, identity);
1736
+ await copyTree(this.fs, template.sourceRoot, cache, [...template.manifest.files, { path: "manifest.json" }]);
1737
+ const staging = this.stagingOf(identity);
1738
+ await copyTree(this.fs, template.sourceRoot, staging, template.manifest.files);
1739
+ assertApplicable?.();
1740
+ this.fs.crash("after-staging");
1741
+ const root = this.pathOf(identity);
1742
+ const backup = this.backupOf(identity);
1743
+ await this.prepareBackup(backup);
1744
+ if (await this.fs.exists(backup)) await this.fs.rm(backup, { recursive: true });
1745
+ const record = existing ?? {
1746
+ version: 1,
1747
+ bundleId: identity.bundleId,
1748
+ workspaceKey: identity.workspaceKey,
1749
+ workspaceId: null,
1750
+ path: root,
1751
+ pluginVersion: template.manifest.pluginVersion,
1752
+ templateDigest: brandString("0".repeat(64)),
1753
+ registered: true,
1754
+ journal: idleJournal(),
1755
+ lastFailure: null
1756
+ };
1757
+ const journal = {
1758
+ phase: "updating",
1759
+ fromDigest: existing?.templateDigest ?? null,
1760
+ toDigest: template.manifest.digest,
1761
+ pluginVersion: template.manifest.pluginVersion,
1762
+ stagingPath: staging,
1763
+ backupPath: await this.fs.exists(root) ? backup : null,
1764
+ swapComplete: false,
1765
+ agentConfigSha256: journalAgentConfigSha256(template, this.requireTemplate(identity))
1766
+ };
1767
+ const updating = await writeJournal(this.fs, this.recordFile(identity), {
1768
+ ...record,
1769
+ registered: true,
1770
+ path: root
1771
+ }, journal);
1772
+ this.records.set(keyOf(identity), updating);
1773
+ this.fs.crash("after-journal-updating");
1774
+ if (updating.workspaceId !== null) await this.ctx.serial("workspace-bundle/quiesce", updating.workspaceId);
1775
+ await this.quiesce(updating.workspaceId);
1776
+ assertApplicable?.();
1777
+ this.fs.crash("after-quiesce");
1778
+ await this.fs.mkdir(join(root, ".."));
1779
+ if (await this.fs.exists(root)) {
1780
+ if (await this.fs.exists(backup)) await this.fs.rm(backup, { recursive: true });
1781
+ await this.fs.rename(root, backup);
1782
+ }
1783
+ this.fs.crash("after-rename-backup");
1784
+ assertApplicable?.();
1785
+ await this.fs.rename(staging, root);
1786
+ const swapped = await writeJournal(this.fs, this.recordFile(identity), updating, {
1787
+ ...journal,
1788
+ swapComplete: true,
1789
+ stagingPath: null
1790
+ });
1791
+ this.records.set(keyOf(identity), swapped);
1792
+ this.fs.crash("after-rename-staging");
1793
+ await verifyTemplateTree(this.fs, root, template.manifest);
1794
+ this.fs.crash("after-verify");
1795
+ assertApplicable?.();
1796
+ const workspace = await this.commit(identity, swapped, template);
1797
+ assertApplicable?.();
1798
+ this.fs.crash("after-journal-commit");
1799
+ this.scheduleRecycle(backup);
1800
+ return workspace;
1801
+ }
1802
+ async commit(identity, record, template) {
1803
+ const workspace = await this.ctx.workspaceRegistry.create(record.path, template.title, originOf(identity));
1804
+ const next = committedRecord({
1805
+ ...record,
1806
+ workspaceId: workspace.id,
1807
+ path: workspace.path,
1808
+ registered: true,
1809
+ agentConfigSha256: journalAgentConfigSha256(template, this.templates.get(keyOf(identity)))
1810
+ }, template.manifest.digest, template.manifest.pluginVersion);
1811
+ await writeRecord(this.fs, this.recordFile(identity), next);
1812
+ this.records.set(keyOf(identity), next);
1813
+ return workspace;
1814
+ }
1815
+ async quiesce(workspaceId) {
1816
+ if (workspaceId === null) return;
1817
+ const deadline = Date.now() + this.spec.quiesceTimeoutMs;
1818
+ while ((this.leases.get(workspaceId)?.size ?? 0) > 0) {
1819
+ if (Date.now() >= deadline) throw new WorkspaceBundleError("WORKSPACE_BUNDLE_QUIESCE_TIMEOUT", `workspace ${workspaceId} still has active leases`);
1820
+ await new Promise((resolve) => {
1821
+ let waiters = this.waiters.get(workspaceId);
1822
+ if (waiters === void 0) {
1823
+ waiters = /* @__PURE__ */ new Set();
1824
+ this.waiters.set(workspaceId, waiters);
1825
+ }
1826
+ const timer = setTimeout(resolve, 25);
1827
+ const waiter = () => {
1828
+ clearTimeout(timer);
1829
+ waiters.delete(waiter);
1830
+ resolve();
1831
+ };
1832
+ waiters.add(waiter);
1833
+ });
1834
+ }
1835
+ }
1836
+ scheduleRecycle(backup) {
1837
+ if (this.closed) return;
1838
+ const timer = setTimeout(() => {
1839
+ this.recycle.delete(backup);
1840
+ const work = this.fs.rm(backup, { recursive: true }).catch((error) => {
1841
+ this.ctx.logger.warn("旧 Workspace 备份清理失败,请检查磁盘;当前版本保持可用。");
1842
+ }).finally(() => {
1843
+ this.recycleWork.delete(backup);
1844
+ });
1845
+ this.recycleWork.set(backup, work);
1846
+ }, this.spec.recycleTimeoutMs);
1847
+ this.recycle.set(backup, timer);
1848
+ }
1849
+ async prepareBackup(backup) {
1850
+ const timer = this.recycle.get(backup);
1851
+ if (timer !== void 0) clearTimeout(timer);
1852
+ this.recycle.delete(backup);
1853
+ await this.recycleWork.get(backup);
1854
+ }
1855
+ };
1856
+ //#endregion
1857
+ //#region lib/types/session-events.js
1858
+ /**
1859
+ * 在每个 Session 首次使用一个不同 Agent 配置版本的 turn 写入身份,重复 step 不重复记录。
1860
+ * @param session - 即将请求模型的 Session。
1861
+ * @param current - 当前租约固定的完整版本。
1862
+ */
1863
+ function recordWorkspaceAgentConfigVersion(session, current) {
1864
+ const previous = session.snapshotEvents().findLast((event) => event.type === "workspace-bundle/agent-config-version");
1865
+ if (previous?.type === "workspace-bundle/agent-config-version" && previous.data.version === current.version && previous.data.agentConfigSha256 === current.sha256 && previous.data.templateDigest === current.templateDigest) return;
1866
+ session.append("workspace-bundle/agent-config-version", {
1867
+ workspaceKind: current.workspaceKind,
1868
+ version: current.version,
1869
+ previousVersion: previous?.type === "workspace-bundle/agent-config-version" ? previous.data.version : null,
1870
+ templateDigest: current.templateDigest,
1871
+ agentConfigSha256: current.sha256,
1872
+ hostAgentConfigApiVersion: current.hostAgentConfigApiVersion
1873
+ });
1874
+ }
1875
+ //#endregion
1876
+ //#region lib/types/agent-config-contract/ids.js
1877
+ /**
1878
+ * Branded identifiers. Each is a UUID string; the brand keeps ids of different entities apart at compile time.
1879
+ * @module
1880
+ */
1881
+ /** Identifier of a user. */
1882
+ const UserId = z.uuid().brand();
1883
+ /** Identifier of a tenant. */
1884
+ const TenantId = z.uuid().brand();
1885
+ z.uuid().brand();
1886
+ z.uuid().brand();
1887
+ z.uuid().brand();
1888
+ z.uuid().brand();
1889
+ z.uuid().brand();
1890
+ z.uuid().brand();
1891
+ z.uuid().brand();
1892
+ z.uuid().brand();
1893
+ z.uuid().brand();
1894
+ z.uuid().brand();
1895
+ z.uuid().brand();
1896
+ z.uuid().brand();
1897
+ z.uuid().brand();
1898
+ //#endregion
1899
+ //#region lib/types/agent-config-contract/cs-hosting.js
1900
+ /** 客服托管权、租约与运行状态检查点契约。子账号归属使用本服务 UserId。 @module */
1901
+ /** 旺旺账号的稳定、不透明标识;不作昵称或大小写归一化。 */
1902
+ const WangwangAccountId = z.string().min(1).max(200).brand();
1903
+ /** 本机安装标识,由 Host 产生并持久保存。 */
1904
+ const InstallationId = z.uuid().brand();
1905
+ /** 云端沙箱标识,不限定执行器的标识格式。 */
1906
+ const SandboxId = z.string().min(1).max(200).brand();
1907
+ /** 持有者种类参与匹配,相同文本的安装与沙箱不是同一持有者。 */
1908
+ const HostingHolder = z.discriminatedUnion("kind", [z.object({
1909
+ kind: z.literal("installation"),
1910
+ id: InstallationId
1911
+ }), z.object({
1912
+ kind: z.literal("sandbox"),
1913
+ id: SandboxId
1914
+ })]).meta({ id: "HostingHolder" });
1915
+ /** 单调递增的 CAS 代次,JSON 安全整数。 */
1916
+ const HostingEpoch = z.number().int().min(0).max(Number.MAX_SAFE_INTEGER);
1917
+ z.object({
1918
+ tenantId: TenantId,
1919
+ userId: UserId,
1920
+ accountId: WangwangAccountId
1921
+ });
1922
+ z.strictObject({});
1923
+ /** 未开通不保存记录,调用方按本地既有行为继续运行。 */
1924
+ const HostingNotEnabled = z.object({
1925
+ cloudEnabled: z.literal(false),
1926
+ serverTime: z.iso.datetime()
1927
+ }).meta({ id: "HostingNotEnabled" });
1928
+ /** 有效性以 serverTime 为准;到期记录保留持有者与 epoch 供 CAS 接管。 */
1929
+ const HostingEnabled = z.object({
1930
+ cloudEnabled: z.literal(true),
1931
+ holder: HostingHolder.nullable(),
1932
+ epoch: HostingEpoch,
1933
+ state: z.enum([
1934
+ "unheld",
1935
+ "active",
1936
+ "expired"
1937
+ ]),
1938
+ leaseExpiresAt: z.iso.datetime().nullable(),
1939
+ remainingSeconds: z.number().int().nonnegative(),
1940
+ serverTime: z.iso.datetime(),
1941
+ updatedAt: z.iso.datetime()
1942
+ }).meta({ id: "HostingEnabled" });
1943
+ const HostingLeaseResponse = z.discriminatedUnion("cloudEnabled", [HostingNotEnabled, HostingEnabled]).meta({ id: "HostingLeaseResponse" });
1944
+ z.strictObject({
1945
+ holder: HostingHolder,
1946
+ expectedEpoch: HostingEpoch
1947
+ }).meta({ id: "AcquireHostingLeaseRequest" });
1948
+ z.strictObject({ holder: z.strictObject({
1949
+ kind: z.literal("sandbox"),
1950
+ id: SandboxId
1951
+ }) }).meta({ id: "FirstEnableHostingLeaseRequest" });
1952
+ z.strictObject({
1953
+ holder: HostingHolder,
1954
+ epoch: HostingEpoch
1955
+ }).meta({ id: "HostingLeaseOwnerRequest" }).extend({ target: HostingHolder }).meta({ id: "HandoffHostingLeaseRequest" });
1956
+ const problems = {
1957
+ 503: ["SERVICE_UNAVAILABLE"],
1958
+ 401: ["SESSION_REQUIRED", "SESSION_EXPIRED"],
1959
+ 403: [
1960
+ "NO_CURRENT_TENANT",
1961
+ "NOT_A_MEMBER",
1962
+ "HOSTING_UNAVAILABLE",
1963
+ "HOSTING_USER_MISMATCH"
1964
+ ],
1965
+ 409: [
1966
+ "TENANT_NOT_CURRENT",
1967
+ "HOSTING_EPOCH_MISMATCH",
1968
+ "HOSTING_HELD",
1969
+ "HOSTING_HOLDER_MISMATCH",
1970
+ "HOSTING_LEASE_EXPIRED"
1971
+ ]
1972
+ };
1973
+ const responses = { 200: {
1974
+ description: "托管权状态;未开通的操作同样返回 cloudEnabled=false,不创建记录",
1975
+ schema: HostingLeaseResponse,
1976
+ headers: { "Cache-Control": {
1977
+ description: "租约状态不得缓存",
1978
+ schema: z.literal("no-store")
1979
+ } }
1980
+ } };
1981
+ responses[200].headers;
1982
+ z.literal("no-store");
1983
+ responses[200].headers;
1984
+ ({ ...problems }), [...problems[409]];
1985
+ ({ ...problems }), [...problems[403]];
1986
+ responses[200].headers;
1987
+ ({ ...problems }), [...problems[403]], [...problems[409]];
1988
+ ({ ...problems }), [...problems[409]];
1989
+ const whitespace = /\s/u;
1990
+ /** 不透明令牌:非空,且不含空白或 ASCII 控制字符。 */
1991
+ function opaqueText(value) {
1992
+ if (whitespace.test(value)) return false;
1993
+ for (const char of value) {
1994
+ const code = char.codePointAt(0) ?? 0;
1995
+ if (code < 32 || code === 127) return false;
1996
+ }
1997
+ return true;
1998
+ }
1999
+ const opaque = (max) => z.string().min(1).max(max).refine(opaqueText);
2000
+ const count = z.number().int().min(0).max(Number.MAX_SAFE_INTEGER);
2001
+ /** 一条消息流的不透明游标。同一次上报里 streamId 不可重复,不含消息正文。 */
2002
+ const HostingStreamCursor = z.strictObject({
2003
+ streamId: opaque(200),
2004
+ cursor: opaque(1024)
2005
+ }).meta({ id: "HostingStreamCursor" });
2006
+ /** 各消息流游标,最多 64 条。 */
2007
+ const HostingStreamCursors = z.array(HostingStreamCursor).max(64).superRefine((items, ctx) => {
2008
+ const seen = /* @__PURE__ */ new Set();
2009
+ items.forEach((item, index) => {
2010
+ if (!seen.has(item.streamId)) {
2011
+ seen.add(item.streamId);
2012
+ return;
2013
+ }
2014
+ ctx.addIssue({
2015
+ code: "custom",
2016
+ path: [index, "streamId"],
2017
+ message: "Duplicate stream"
2018
+ });
2019
+ });
2020
+ });
2021
+ /** 已提交事件的 sha256,不是正文摘要。 */
2022
+ const HostingCheckpointSession = z.strictObject({
2023
+ id: opaque(200),
2024
+ eventCount: count,
2025
+ digest: z.string().regex(/^[a-f0-9]{64}$/)
2026
+ }).meta({ id: "HostingCheckpointSession" });
2027
+ /** running 正在自动接待,draining 正在排空,stopped 已停止自动接待,unavailable 无法确认仍在接待。 */
2028
+ const HostingCheckpointStatus = z.enum([
2029
+ "running",
2030
+ "draining",
2031
+ "stopped",
2032
+ "unavailable"
2033
+ ]).meta({ id: "HostingCheckpointStatus" });
2034
+ const hostVersion = z.string().regex(/^[0-9A-Za-z][0-9A-Za-z.+_-]{0,99}$/);
2035
+ /** 规范 SemVer 2.0,最多 200 字符,保留预发布和构建元数据,不接受前缀或空白。 */
2036
+ const AgentConfigSemver = z.string().max(200).regex(/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?(?![\s\S])/);
2037
+ /** 与 Host Agent 配置错误分类对齐;查询失败与作用域变化不计采用拒绝。 */
2038
+ const AgentConfigErrorCode = z.enum([
2039
+ "AGENT_CONFIG_INCOMPATIBLE",
2040
+ "AGENT_CONFIG_DIGEST_MISMATCH",
2041
+ "AGENT_CONFIG_INVALID",
2042
+ "AGENT_CONFIG_DOWNLOAD_FAILED",
2043
+ "AGENT_CONFIG_APPLY_FAILED",
2044
+ "AGENT_CONFIG_QUERY_FAILED",
2045
+ "AGENT_CONFIG_SCOPE_CHANGED"
2046
+ ]).meta({ id: "AgentConfigErrorCode" });
2047
+ /** 仅包含固定码与已知时的被拒版本,不包含消息、路径、URL 或凭据。 */
2048
+ const AgentConfigRejection = z.strictObject({
2049
+ code: AgentConfigErrorCode,
2050
+ version: AgentConfigSemver.optional()
2051
+ }).meta({ id: "AgentConfigRejection" });
2052
+ z.strictObject({
2053
+ tenantId: TenantId,
2054
+ userId: UserId,
2055
+ accountId: WangwangAccountId,
2056
+ epoch: HostingEpoch,
2057
+ revision: z.number().int().min(1).max(Number.MAX_SAFE_INTEGER),
2058
+ streamCursors: HostingStreamCursors,
2059
+ session: HostingCheckpointSession.nullable(),
2060
+ pendingCount: count,
2061
+ unknownSendCount: count,
2062
+ status: HostingCheckpointStatus,
2063
+ hostVersion,
2064
+ reportedAt: z.iso.datetime()
2065
+ }).meta({ id: "HostingCheckpoint" });
2066
+ z.strictObject({
2067
+ /** 本地 Host 限制开启时必须提供;与模块名册的 hostId 相同。 */
2068
+ holder: HostingHolder.optional(),
2069
+ epoch: HostingEpoch,
2070
+ expectedRevision: count,
2071
+ streamCursors: HostingStreamCursors,
2072
+ session: HostingCheckpointSession.nullable(),
2073
+ pendingCount: count,
2074
+ unknownSendCount: count,
2075
+ status: HostingCheckpointStatus,
2076
+ hostVersion,
2077
+ /** Host 已经成功应用的 Agent 配置版本;未上报表示未知,不接受以分配版本代替。 */
2078
+ agentConfigVersion: z.string().min(1).max(200).optional(),
2079
+ /** Host 的 Agent 配置 API,独立于应用 hostVersion;缺失表示未知。 */
2080
+ hostAgentConfigApiVersion: AgentConfigSemver.optional(),
2081
+ /** 已提交内容是否禁止执行;缺失不代表 false。 */
2082
+ agentConfigBlocked: z.boolean().optional(),
2083
+ /** null 明确表示没有拒绝,缺失表示未上报;三个新字段齐全才构成完整观测。 */
2084
+ agentConfigRejection: AgentConfigRejection.nullable().optional()
2085
+ }).meta({ id: "ReportHostingCheckpointRequest" });
2086
+ z.literal("no-store");
2087
+ const checkpointProblems = {
2088
+ 503: ["SERVICE_UNAVAILABLE"],
2089
+ 401: ["SESSION_REQUIRED", "SESSION_EXPIRED"],
2090
+ 403: [
2091
+ "NO_CURRENT_TENANT",
2092
+ "NOT_A_MEMBER",
2093
+ "HOSTING_UNAVAILABLE",
2094
+ "HOSTING_USER_MISMATCH"
2095
+ ],
2096
+ 409: ["TENANT_NOT_CURRENT"]
2097
+ };
2098
+ ({ ...checkpointProblems });
2099
+ ({ ...checkpointProblems });
2100
+ //#endregion
2101
+ //#region lib/types/agent-config-contract/agent-configs.js
2102
+ /** 内置 workspace Agent 配置:查询本人当前店铺租户的分配,兼容性由 Host 检查。 @module */
2103
+ /** 可扩展的内置 workspace 种类;首个种类为 cs-hosting。 */
2104
+ const WorkspaceKind = z.string().regex(/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/).max(80).meta({ id: "WorkspaceKind" });
2105
+ /** Agent 配置登记的完整元数据;sha256 来自经 PR 审阅的规则文件。 */
2106
+ const AgentConfig = z.object({
2107
+ workspaceKind: WorkspaceKind,
2108
+ version: z.string().min(1),
2109
+ sha256: z.string().regex(/^[a-f0-9]{64}$/),
2110
+ sizeBytes: z.number().int().positive().max(Number.MAX_SAFE_INTEGER),
2111
+ downloadUrl: z.url({ protocol: /^https$/ }),
2112
+ hostContentApiRange: z.string().min(1),
2113
+ releaseNotes: z.string().min(1).max(2e4)
2114
+ }).meta({ id: "AgentConfig" });
2115
+ /** builtin 表示没有分配:已有下载版本时保留当前版本,从未下载时使用安装包内置版本。 */
2116
+ const AgentConfigAssignment = z.discriminatedUnion("source", [z.object({
2117
+ source: z.literal("builtin"),
2118
+ tenantId: TenantId,
2119
+ userId: UserId,
2120
+ workspaceKind: WorkspaceKind
2121
+ }), z.object({
2122
+ source: z.literal("registry"),
2123
+ tenantId: TenantId,
2124
+ userId: UserId,
2125
+ workspaceKind: WorkspaceKind,
2126
+ agentConfig: AgentConfig
2127
+ })]).meta({ id: "AgentConfigAssignment" });
2128
+ /** 路径中的租户必须是会话当前租户;用户由会话确定,不接受调用方指定。 */
2129
+ const AgentConfigParams = z.object({
2130
+ tenantId: TenantId,
2131
+ workspaceKind: WorkspaceKind
2132
+ }).meta({ id: "AgentConfigParams" });
2133
+ /** Host 在启动和轮询时查询;不按版本大小排序,回退可返回更低版本;服务不可达时保留当前版本。 */
2134
+ const getAgentConfigAssignment = {
2135
+ method: "get",
2136
+ path: "/v1/tenants/{tenantId}/agent-configs/{workspaceKind}",
2137
+ summary: "查询本人内置 workspace Agent 配置分配",
2138
+ auth: "tenant",
2139
+ request: {
2140
+ params: AgentConfigParams,
2141
+ query: z.strictObject({})
2142
+ },
2143
+ responses: { 200: {
2144
+ description: "分配版本或使用内置版本;Host 检查兼容范围并核对下载字节的 sha256",
2145
+ schema: AgentConfigAssignment,
2146
+ headers: { "Cache-Control": {
2147
+ description: "每次读取当前分配,及时观察回退",
2148
+ schema: z.literal("no-store")
2149
+ } }
2150
+ } },
2151
+ problems: {
2152
+ 401: ["SESSION_REQUIRED", "SESSION_EXPIRED"],
2153
+ 403: [
2154
+ "NO_CURRENT_TENANT",
2155
+ "NOT_A_MEMBER",
2156
+ "AGENT_CONFIGS_UNAVAILABLE"
2157
+ ],
2158
+ 409: ["TENANT_NOT_CURRENT"]
2159
+ }
2160
+ };
2161
+ /** 双模块观测与控制仅支持已实现的模块;不设默认种类。 */
2162
+ const AgentConfigModule = z.enum(["cs-hosting", "resource-intake"]).meta({ id: "AgentConfigModule" });
2163
+ const AgentConfigDigest = z.string().regex(/^[a-f0-9]{64}$/);
2164
+ const sequence$1 = z.number().int().min(1).max(Number.MAX_SAFE_INTEGER);
2165
+ const generation = z.number().int().min(0).max(Number.MAX_SAFE_INTEGER);
2166
+ /** 完整身份包括 build metadata;builtin 没有远端归档摘要。 */
2167
+ const AgentConfigCommitted = z.discriminatedUnion("source", [z.strictObject({
2168
+ source: z.literal("builtin"),
2169
+ version: AgentConfigSemver,
2170
+ sha256: z.null()
2171
+ }), z.strictObject({
2172
+ source: z.literal("registry"),
2173
+ version: AgentConfigSemver,
2174
+ sha256: AgentConfigDigest
2175
+ })]).meta({ id: "AgentConfigCommitted" });
2176
+ const AgentConfigTarget = z.strictObject({
2177
+ version: AgentConfigSemver,
2178
+ sha256: AgentConfigDigest
2179
+ }).meta({ id: "AgentConfigTarget" });
2180
+ /** 已提交版本恢复/采用的独立阻断原因;不随候选目标拒绝变化,不含诊断正文。 */
2181
+ const AgentConfigBlockingFailure = z.strictObject({
2182
+ code: AgentConfigErrorCode,
2183
+ committed: AgentConfigCommitted
2184
+ }).meta({ id: "AgentConfigBlockingFailure" });
2185
+ /** 固定码与精确目标归因;查询失败不能作为采用拒绝。 */
2186
+ const ModuleAgentConfigSnapshot = z.strictObject({
2187
+ hostAgentConfigApiVersion: AgentConfigSemver,
2188
+ runtimeApiVersion: AgentConfigSemver.nullable(),
2189
+ current: AgentConfigCommitted,
2190
+ pending: AgentConfigTarget.nullable(),
2191
+ blocked: z.boolean(),
2192
+ lastRejection: z.strictObject({
2193
+ code: AgentConfigErrorCode,
2194
+ target: AgentConfigTarget.nullable()
2195
+ }).nullable(),
2196
+ blockingFailure: AgentConfigBlockingFailure.nullable().optional()
2197
+ }).meta({ id: "ModuleAgentConfigSnapshot" });
2198
+ /** 服务端从当前有效 sandbox 会话和活动 placement key 解析的绑定;不是调用方可指定的身份。 */
2199
+ const AgentConfigSandboxBinding = z.strictObject({
2200
+ sandboxId: z.uuid(),
2201
+ instanceId: z.string().min(1).max(200),
2202
+ sandboxGeneration: generation
2203
+ }).meta({ id: "AgentConfigSandboxBinding" });
2204
+ /** 云端读回只要求现有 sandbox/instance 配置;代次由服务端提供。 */
2205
+ const ReadAgentConfigSandboxHostQuery = AgentConfigSandboxBinding.pick({
2206
+ sandboxId: true,
2207
+ instanceId: true
2208
+ }).meta({ id: "ReadAgentConfigSandboxHostQuery" });
2209
+ /** 外层绑定为本次认证的当前实例;内层为已存注册,可来自旧实例。null 表示尚无注册。 */
2210
+ const AgentConfigSandboxHostReadback = z.strictObject({
2211
+ tenantId: TenantId,
2212
+ userId: UserId,
2213
+ workspaceKind: AgentConfigModule,
2214
+ sandboxBinding: AgentConfigSandboxBinding,
2215
+ registration: z.strictObject({
2216
+ hostId: z.uuid(),
2217
+ generation: sequence$1,
2218
+ bootSequence: sequence$1,
2219
+ sandboxBinding: AgentConfigSandboxBinding
2220
+ }).nullable()
2221
+ }).meta({ id: "AgentConfigSandboxHostReadback" });
2222
+ /** bootSequence 随 device 持久递增;sandbox 丢失状态时先在本地锁内读回,再携带绑定与明确代次 CAS,不盲目重试。 */
2223
+ const RegisterAgentConfigHostRequest = z.strictObject({
2224
+ hostId: z.uuid(),
2225
+ bootId: z.uuid(),
2226
+ requestId: z.uuid(),
2227
+ bootSequence: sequence$1,
2228
+ expectedGeneration: generation,
2229
+ explicitActivation: z.boolean().optional(),
2230
+ sandboxBinding: AgentConfigSandboxBinding.optional()
2231
+ }).meta({ id: "RegisterAgentConfigHostRequest" });
2232
+ const AgentConfigHostRegistration = z.strictObject({
2233
+ workspaceKind: AgentConfigModule,
2234
+ hostId: z.uuid(),
2235
+ generation: sequence$1,
2236
+ holder: z.strictObject({
2237
+ kind: z.enum(["installation", "sandbox"]),
2238
+ id: z.uuid()
2239
+ })
2240
+ }).meta({ id: "AgentConfigHostRegistration" });
2241
+ const ReportAgentConfigHostRequest = ModuleAgentConfigSnapshot.extend({
2242
+ generation: sequence$1,
2243
+ sequence: sequence$1
2244
+ }).meta({ id: "ReportAgentConfigHostRequest" });
2245
+ const AgentConfigHostReceipt = z.strictObject({
2246
+ generation: sequence$1,
2247
+ sequence: sequence$1,
2248
+ reportedAt: z.iso.datetime()
2249
+ }).meta({ id: "AgentConfigHostReceipt" });
2250
+ const hostProblems = {
2251
+ 401: ["SESSION_REQUIRED", "SESSION_EXPIRED"],
2252
+ 403: [
2253
+ "NO_CURRENT_TENANT",
2254
+ "NOT_A_MEMBER",
2255
+ "AGENT_CONFIGS_UNAVAILABLE",
2256
+ "SANDBOX_SESSION_UNBOUND"
2257
+ ],
2258
+ 404: ["NOT_FOUND"],
2259
+ 409: [
2260
+ "TENANT_NOT_CURRENT",
2261
+ "VERSION_CONFLICT",
2262
+ "OPERATION_CONFLICT",
2263
+ "LOCAL_HOST_REPLACED"
2264
+ ],
2265
+ 503: ["SERVICE_UNAVAILABLE"]
2266
+ };
2267
+ const hostHeaders = { "Cache-Control": {
2268
+ description: "观测与回执不得缓存",
2269
+ schema: z.literal("no-store")
2270
+ } };
2271
+ const getAgentConfigSandboxHost = {
2272
+ method: "get",
2273
+ path: "/v1/tenants/{tenantId}/agent-configs/{workspaceKind}/hosts/current",
2274
+ summary: "只读本人当前沙箱模块 Host 注册,不允许本机安装调用",
2275
+ auth: "tenant",
2276
+ request: {
2277
+ params: AgentConfigParams,
2278
+ query: ReadAgentConfigSandboxHostQuery
2279
+ },
2280
+ responses: { 200: {
2281
+ description: "sandboxId/instanceId 须匹配当前已认证 sandbox;返回服务端代次及已存注册,读回不授予业务权限,重新注册须携带完整 sandboxBinding 和明确 expectedGeneration",
2282
+ schema: AgentConfigSandboxHostReadback,
2283
+ headers: hostHeaders
2284
+ } },
2285
+ problems: hostProblems
2286
+ };
2287
+ const registerAgentConfigHost = {
2288
+ method: "post",
2289
+ path: "/v1/tenants/{tenantId}/agent-configs/{workspaceKind}/hosts/register",
2290
+ summary: "注册本人模块 Host 运行,不取得业务写权限",
2291
+ auth: "tenant",
2292
+ request: {
2293
+ params: AgentConfigParams,
2294
+ body: RegisterAgentConfigHostRequest
2295
+ },
2296
+ responses: { 200: {
2297
+ description: "服务端绑定 holder;提供 sandboxBinding 时须在事务内匹配当前 sandbox/instance/代次,installation 不接受该字段;新运行以 bootSequence 和 expectedGeneration 比较更新,旧运行不可复活",
2298
+ schema: AgentConfigHostRegistration,
2299
+ headers: hostHeaders
2300
+ } },
2301
+ problems: hostProblems
2302
+ };
2303
+ const reportAgentConfigHost = {
2304
+ method: "put",
2305
+ path: "/v1/tenants/{tenantId}/agent-configs/{workspaceKind}/hosts/{hostId}/observation",
2306
+ summary: "上报本人模块 Host 的完整采用快照",
2307
+ auth: "tenant",
2308
+ request: {
2309
+ params: AgentConfigParams.extend({ hostId: z.uuid() }),
2310
+ body: ReportAgentConfigHostRequest
2311
+ },
2312
+ responses: { 200: {
2313
+ description: "同序同内容返回原接收时间,同序异内容或旧代次冲突;时间由服务端确定",
2314
+ schema: AgentConfigHostReceipt,
2315
+ headers: hostHeaders
2316
+ } },
2317
+ problems: hostProblems
2318
+ };
2319
+ /** 仅新模块契约暴露可选runtime要求;旧assignment显式投影原AgentConfig。 */
2320
+ const ModuleAgentConfigMetadata = AgentConfig.extend({ runtimeApiRange: z.string().min(1).optional() }).meta({ id: "ModuleAgentConfigMetadata" });
2321
+ const ModuleAgentConfigAssignment = z.discriminatedUnion("source", [z.object({
2322
+ source: z.literal("builtin"),
2323
+ tenantId: TenantId,
2324
+ userId: UserId,
2325
+ workspaceKind: AgentConfigModule
2326
+ }), z.object({
2327
+ source: z.literal("registry"),
2328
+ tenantId: TenantId,
2329
+ userId: UserId,
2330
+ workspaceKind: AgentConfigModule,
2331
+ agentConfig: ModuleAgentConfigMetadata
2332
+ })]).meta({ id: "ModuleAgentConfigAssignment" });
2333
+ const getModuleAgentConfigAssignment = {
2334
+ ...getAgentConfigAssignment,
2335
+ path: "/v1/tenants/{tenantId}/agent-configs/{workspaceKind}/module",
2336
+ summary: "查询显式模块分配与runtime要求",
2337
+ responses: { 200: {
2338
+ ...getAgentConfigAssignment.responses[200],
2339
+ schema: ModuleAgentConfigAssignment
2340
+ } },
2341
+ problems: hostProblems
2342
+ };
2343
+ //#endregion
2344
+ //#region lib/types/agent-config-observation.js
2345
+ const sequence = z.number().int().min(0).max(Number.MAX_SAFE_INTEGER);
2346
+ const ScopeState = z.strictObject({
2347
+ generation: sequence,
2348
+ replaced: z.boolean().optional(),
2349
+ running: z.strictObject({
2350
+ bootId: z.uuid(),
2351
+ bootSequence: sequence,
2352
+ sequence
2353
+ }).nullable(),
2354
+ pendingRegistration: RegisterAgentConfigHostRequest.nullable(),
2355
+ pendingReport: ReportAgentConfigHostRequest.nullable()
2356
+ });
2357
+ const State = z.strictObject({
2358
+ version: z.literal(1),
2359
+ hostId: z.uuid(),
2360
+ bootSequence: sequence,
2361
+ scopes: z.record(z.string().regex(/^[a-f0-9]{64}$/u), ScopeState)
2362
+ });
2363
+ function sameSandbox(left, right) {
2364
+ return left?.sandboxId === right.sandboxId && left.instanceId === right.instanceId && left.sandboxGeneration === right.sandboxGeneration;
2365
+ }
2366
+ var ObservationError = class extends Error {
2367
+ code;
2368
+ constructor(code) {
2369
+ super(code);
2370
+ this.code = code;
2371
+ }
2372
+ };
2373
+ /**
2374
+ * Build the observation owner when its source is registered; the server owns holder attestation.
2375
+ * @param ctx - authenticated merchant transport and scope-change events.
2376
+ * @param options - module identity, private persistent directory and resolved scheduling limits.
2377
+ * @returns factory bound to the source's actual committed state.
2378
+ */
2379
+ function agentConfigHostObservation(ctx, options) {
2380
+ return (read) => new ModuleHostObservation(ctx, options, read);
2381
+ }
2382
+ /**
2383
+ * Cloud-only factory; importing this export rejects older packages that cannot enforce sandbox identity.
2384
+ * @param ctx - Authenticated merchant transport and scope-change events.
2385
+ * @param options - Required sandbox deployment identity and private reporter settings.
2386
+ * @returns factory whose registrations require authenticated sandbox readback.
2387
+ */
2388
+ function sandboxAgentConfigHostObservation(ctx, options) {
2389
+ return (read) => new ModuleHostObservation(ctx, options, read);
2390
+ }
2391
+ var ModuleHostObservation = class {
2392
+ ctx;
2393
+ options;
2394
+ read;
2395
+ lifetime = new AbortController();
2396
+ active;
2397
+ run;
2398
+ stopped;
2399
+ chain = Promise.resolve();
2400
+ timer;
2401
+ stopScope;
2402
+ stopActivation;
2403
+ stopActivated;
2404
+ preparedRevision;
2405
+ preparedHostId;
2406
+ activationRequest = false;
2407
+ stopReplacement;
2408
+ migrated = /* @__PURE__ */ new Set();
2409
+ member() {
2410
+ return this.ctx.merchantAccount.captureIdentity()?.identity;
2411
+ }
2412
+ get replaced() {
2413
+ if (this.options.sandbox !== void 0) return false;
2414
+ const member = this.member();
2415
+ return member !== void 0 && this.options.replacementState.isReplaced(member);
2416
+ }
2417
+ stopIdentity;
2418
+ hostId() {
2419
+ return typeof this.options.hostId === "function" ? this.options.hostId() : this.options.hostId;
2420
+ }
2421
+ constructor(ctx, options, read) {
2422
+ this.ctx = ctx;
2423
+ this.options = options;
2424
+ this.read = read;
2425
+ this.stopReplacement = options.sandbox === void 0 ? options.replacementState.subscribe(() => {
2426
+ if (!this.replaced) return;
2427
+ if (!this.activationRequest) this.active?.abort();
2428
+ this.options.onReplaced?.(this.hostId());
2429
+ this.refresh();
2430
+ }) : () => {};
2431
+ this.stopScope = ctx.on("merchant-account/scope-changed", () => {
2432
+ this.active?.abort();
2433
+ this.refresh();
2434
+ });
2435
+ this.stopIdentity = options.sandbox === void 0 ? ctx.on("workspace-bundle/local-host-identity-changed", () => {
2436
+ this.active?.abort();
2437
+ this.stopped = void 0;
2438
+ return this.refresh();
2439
+ }) : () => {};
2440
+ this.stopActivation = options.sandbox === void 0 ? ctx.on("workspace-bundle/activate-local-host", async (revision, registered) => {
2441
+ await this.activate(revision);
2442
+ registered.add(this.options.workspaceKind);
2443
+ }) : () => {};
2444
+ this.stopActivated = options.sandbox === void 0 ? ctx.on("workspace-bundle/local-host-activated", (revision) => {
2445
+ if (this.preparedRevision !== revision || this.preparedHostId !== this.hostId()) return Promise.reject(new ObservationError("AGENT_CONFIG_HOST_UNAVAILABLE"));
2446
+ const work = this.chain.then(() => this.report(revision, false, true));
2447
+ this.chain = work.catch((error) => {});
2448
+ return work;
2449
+ }) : () => {};
2450
+ this.cycle();
2451
+ }
2452
+ async cycle() {
2453
+ await this.refresh();
2454
+ if (this.lifetime.signal.aborted) return;
2455
+ this.timer = setTimeout(() => {
2456
+ this.cycle();
2457
+ }, this.options.intervalMs);
2458
+ this.timer.unref();
2459
+ }
2460
+ refresh() {
2461
+ if (this.lifetime.signal.aborted) return this.chain;
2462
+ const revision = this.ctx.merchantAccount.getScopeRevision();
2463
+ this.chain = this.chain.then(() => this.report(revision)).catch((error) => {
2464
+ if (this.lifetime.signal.aborted || revision !== this.ctx.merchantAccount.getScopeRevision()) return;
2465
+ if (this.run !== void 0 && this.run.hostId !== this.hostId()) return;
2466
+ const code = error instanceof LocalHostReplacementWriteError ? "LOCAL_HOST_REPLACEMENT_WRITE_FAILED" : error instanceof ObservationError ? error.code : "AGENT_CONFIG_HOST_STATE_INVALID";
2467
+ if (code === "LOCAL_HOST_REPLACEMENT_WRITE_FAILED") {
2468
+ this.ctx.logger.warn("workspace-bundle: %s replacement state could not be saved; reporting remains stopped; check disk space and directory permissions before retrying", this.options.workspaceKind);
2469
+ return;
2470
+ }
2471
+ if (code !== "LOCAL_HOST_REPLACED" && code !== "AGENT_CONFIG_HOST_UNAVAILABLE" && this.run !== void 0) this.stopped = {
2472
+ key: this.run.key,
2473
+ revision
2474
+ };
2475
+ this.ctx.logger.warn("workspace-bundle: %s module observation failed (%s); %s", this.options.workspaceKind, code, code === "AGENT_CONFIG_HOST_UNAVAILABLE" ? "the exact pending request is retained for retry" : "reporting is stopped; restore matching Host state before restarting");
2476
+ });
2477
+ return this.chain;
2478
+ }
2479
+ activate(revision) {
2480
+ if (this.lifetime.signal.aborted || revision !== this.ctx.merchantAccount.getScopeRevision()) return Promise.reject(new ObservationError("AGENT_CONFIG_HOST_UNAVAILABLE"));
2481
+ const work = this.chain.then(() => this.report(revision, true));
2482
+ this.chain = work.catch((error) => {});
2483
+ return work;
2484
+ }
2485
+ scope(revision) {
2486
+ const state = this.ctx.merchantAccount.getState();
2487
+ const identity = state.status === "signed-in" ? state.identity : null;
2488
+ const tenantId = identity?.currentTenantId;
2489
+ if (identity === null || tenantId == null || !identity.memberships.some((member) => member.tenantId === tenantId && member.kind === "shop")) return void 0;
2490
+ if (revision !== this.ctx.merchantAccount.getScopeRevision()) return void 0;
2491
+ const userId = identity.user.id;
2492
+ const key = createHash("sha256").update(JSON.stringify(this.options.sandbox === void 0 ? [
2493
+ tenantId,
2494
+ userId,
2495
+ this.options.workspaceKind
2496
+ ] : [
2497
+ this.member()?.serverOrigin,
2498
+ tenantId,
2499
+ userId,
2500
+ this.options.workspaceKind,
2501
+ this.options.sandbox.sandboxId,
2502
+ this.options.sandbox.instanceId
2503
+ ])).digest("hex");
2504
+ if (this.stopped?.key === key && this.stopped.revision === revision) return void 0;
2505
+ if (this.run?.key !== key || this.run.revision !== revision || this.run.hostId !== this.hostId()) this.run = {
2506
+ key,
2507
+ tenantId,
2508
+ userId,
2509
+ revision,
2510
+ hostId: this.hostId(),
2511
+ bootId: brandString(randomUUID()),
2512
+ started: false
2513
+ };
2514
+ return this.run;
2515
+ }
2516
+ current(run, signal) {
2517
+ signal.throwIfAborted();
2518
+ const state = this.ctx.merchantAccount.getState();
2519
+ if (run.hostId !== this.hostId() || run.revision !== this.ctx.merchantAccount.getScopeRevision() || state.status !== "signed-in" || state.identity?.currentTenantId !== run.tenantId || state.identity.user.id !== run.userId) throw new ObservationError("AGENT_CONFIG_HOST_UNAVAILABLE");
2520
+ }
2521
+ async load(hostId) {
2522
+ let text;
2523
+ try {
2524
+ text = await readFile(join(this.options.stateRoot, `agent-config-${this.options.workspaceKind}-${hostId}.json`), "utf8");
2525
+ } catch (error) {
2526
+ if (error.code !== "ENOENT") throw new ObservationError("AGENT_CONFIG_HOST_STATE_INVALID");
2527
+ try {
2528
+ text = await readFile(join(this.options.stateRoot, `agent-config-${this.options.workspaceKind}-host.json`), "utf8");
2529
+ } catch (legacyError) {
2530
+ if (legacyError.code !== "ENOENT") throw new ObservationError("AGENT_CONFIG_HOST_STATE_INVALID");
2531
+ return {
2532
+ version: 1,
2533
+ hostId,
2534
+ bootSequence: 0,
2535
+ scopes: {}
2536
+ };
2537
+ }
2538
+ if (State.parse(JSON.parse(text)).hostId !== hostId) return {
2539
+ version: 1,
2540
+ hostId,
2541
+ bootSequence: 0,
2542
+ scopes: {}
2543
+ };
2544
+ }
2545
+ const parsed = State.safeParse(JSON.parse(text));
2546
+ if (!parsed.success || parsed.data.hostId !== hostId) throw new ObservationError("AGENT_CONFIG_HOST_STATE_INVALID");
2547
+ return parsed.data;
2548
+ }
2549
+ async migrate(run, member, explicitActivation) {
2550
+ if (!explicitActivation && this.migrated.has(run.key)) return;
2551
+ await mkdir(this.options.stateRoot, {
2552
+ recursive: true,
2553
+ mode: 448
2554
+ });
2555
+ const paths = (await readdir(this.options.stateRoot)).filter((path) => path.startsWith(`agent-config-${this.options.workspaceKind}-`) && path.endsWith(".json"));
2556
+ const legacy = await Promise.all(paths.map(async (path) => ({
2557
+ path,
2558
+ state: State.parse(JSON.parse(await readFile(join(this.options.stateRoot, path), "utf8")))
2559
+ })));
2560
+ await this.options.replacementState.importLegacy(member, this.options.workspaceKind, legacy.some(({ state }) => state.scopes[run.key]?.replaced === true));
2561
+ for (const { path, state } of legacy) {
2562
+ const record = state.scopes[run.key];
2563
+ if (record?.replaced === void 0) continue;
2564
+ delete record.replaced;
2565
+ await writeFileAtomic(join(this.options.stateRoot, path), JSON.stringify(state) + "\n", {
2566
+ mode: 384,
2567
+ dirMode: 448
2568
+ });
2569
+ }
2570
+ this.migrated.add(run.key);
2571
+ }
2572
+ async save(state) {
2573
+ await writeFileAtomic(join(this.options.stateRoot, `agent-config-${this.options.workspaceKind}-${state.hostId}.json`), JSON.stringify(state) + "\n", {
2574
+ mode: 384,
2575
+ dirMode: 448
2576
+ });
2577
+ }
2578
+ async rememberReplacement(state, record, member) {
2579
+ const saveModule = record.replaced !== true;
2580
+ record.replaced = true;
2581
+ const writes = await Promise.allSettled([saveModule ? this.save(state) : Promise.resolve(), this.options.replacementState.markReplaced(member)]);
2582
+ for (const write of writes) if (write.status === "rejected") throw new LocalHostReplacementWriteError(write.reason);
2583
+ }
2584
+ failure(failure) {
2585
+ if (failure.kind === "server" && failure.status === 409 && failure.code === "LOCAL_HOST_REPLACED") return new ObservationError(this.options.sandbox === void 0 ? "LOCAL_HOST_REPLACED" : "AGENT_CONFIG_HOST_PROTOCOL");
2586
+ if (failure.kind === "server" && failure.status === 409 && failure.code === "VERSION_CONFLICT") return new ObservationError("AGENT_CONFIG_HOST_GENERATION_CONFLICT");
2587
+ if (failure.kind === "contract") return new ObservationError("AGENT_CONFIG_HOST_PROTOCOL");
2588
+ return new ObservationError(failure.kind === "server" && (failure.status === 409 || failure.status === 404) ? "AGENT_CONFIG_HOST_CONFLICT" : "AGENT_CONFIG_HOST_UNAVAILABLE");
2589
+ }
2590
+ async attestSandbox(state, record, run, signal) {
2591
+ const expected = this.options.sandbox;
2592
+ if (expected === void 0) return;
2593
+ this.current(run, signal);
2594
+ const result = await this.ctx.merchantAccount.request(getAgentConfigSandboxHost, {
2595
+ params: {
2596
+ tenantId: run.tenantId,
2597
+ workspaceKind: this.options.workspaceKind
2598
+ },
2599
+ query: {
2600
+ sandboxId: expected.sandboxId,
2601
+ instanceId: expected.instanceId
2602
+ },
2603
+ tenantContextReload: "caller"
2604
+ }, signal).catch(() => {
2605
+ throw new ObservationError("AGENT_CONFIG_HOST_UNAVAILABLE");
2606
+ });
2607
+ this.current(run, signal);
2608
+ if (!result.ok) throw this.failure(result.failure);
2609
+ const value = result.value;
2610
+ if (String(value.tenantId) !== run.tenantId || String(value.userId) !== run.userId || value.workspaceKind !== this.options.workspaceKind || value.sandboxBinding.sandboxId !== expected.sandboxId || value.sandboxBinding.instanceId !== expected.instanceId) throw new ObservationError("AGENT_CONFIG_HOST_PROTOCOL");
2611
+ if (run.started && !sameSandbox(run.sandboxBinding, value.sandboxBinding)) throw new ObservationError("AGENT_CONFIG_HOST_CONFLICT");
2612
+ run.sandboxBinding = value.sandboxBinding;
2613
+ if (record.pendingRegistration !== null && !sameSandbox(record.pendingRegistration.sandboxBinding, value.sandboxBinding)) {
2614
+ record.pendingRegistration = null;
2615
+ record.pendingReport = null;
2616
+ record.running = null;
2617
+ }
2618
+ if (run.started) return;
2619
+ const registration = value.registration;
2620
+ if (record.pendingReport !== null && (registration?.hostId !== run.hostId || registration.generation !== record.pendingReport.generation || !sameSandbox(registration.sandboxBinding, value.sandboxBinding))) record.pendingReport = null;
2621
+ if (record.pendingRegistration === null) record.generation = registration?.generation ?? 0;
2622
+ state.bootSequence = Math.max(state.bootSequence, registration?.bootSequence ?? 0);
2623
+ await this.save(state);
2624
+ }
2625
+ async registration(state, record, run, signal) {
2626
+ const body = record.pendingRegistration;
2627
+ if (body === null) return;
2628
+ if (this.options.sandbox !== void 0 && (run.sandboxBinding === void 0 || !sameSandbox(body.sandboxBinding, run.sandboxBinding))) throw new ObservationError("AGENT_CONFIG_HOST_PROTOCOL");
2629
+ this.current(run, signal);
2630
+ const response = await this.ctx.merchantAccount.request(registerAgentConfigHost, {
2631
+ params: {
2632
+ tenantId: run.tenantId,
2633
+ workspaceKind: this.options.workspaceKind
2634
+ },
2635
+ body,
2636
+ tenantContextReload: "caller"
2637
+ }, signal).catch(() => {
2638
+ throw new ObservationError("AGENT_CONFIG_HOST_UNAVAILABLE");
2639
+ });
2640
+ this.current(run, signal);
2641
+ if (!response.ok) throw this.failure(response.failure);
2642
+ const receipt = response.value;
2643
+ if (receipt.workspaceKind !== this.options.workspaceKind || receipt.hostId !== run.hostId || receipt.generation !== body.expectedGeneration + 1 || receipt.holder.kind === "installation" && receipt.holder.id !== run.hostId || this.options.sandbox !== void 0 && (receipt.holder.kind !== "sandbox" || receipt.holder.id !== this.options.sandbox.sandboxId)) throw new ObservationError("AGENT_CONFIG_HOST_PROTOCOL");
2644
+ run.holder = receipt.holder.kind;
2645
+ record.generation = receipt.generation;
2646
+ record.running = {
2647
+ bootId: body.bootId,
2648
+ bootSequence: body.bootSequence,
2649
+ sequence: 0
2650
+ };
2651
+ record.pendingRegistration = null;
2652
+ await this.save(state);
2653
+ }
2654
+ async observation(state, record, run, signal) {
2655
+ const body = record.pendingReport;
2656
+ if (body === null) return;
2657
+ this.current(run, signal);
2658
+ const response = await this.ctx.merchantAccount.request(reportAgentConfigHost, {
2659
+ params: {
2660
+ tenantId: run.tenantId,
2661
+ workspaceKind: this.options.workspaceKind,
2662
+ hostId: run.hostId
2663
+ },
2664
+ body,
2665
+ tenantContextReload: "caller"
2666
+ }, signal).catch(() => {
2667
+ throw new ObservationError("AGENT_CONFIG_HOST_UNAVAILABLE");
2668
+ });
2669
+ this.current(run, signal);
2670
+ if (!response.ok) throw this.failure(response.failure);
2671
+ const receipt = response.value;
2672
+ if (receipt.generation !== body.generation || receipt.sequence !== body.sequence || record.running === null) throw new ObservationError("AGENT_CONFIG_HOST_PROTOCOL");
2673
+ record.running.sequence = receipt.sequence;
2674
+ record.pendingReport = null;
2675
+ await this.save(state);
2676
+ }
2677
+ /**
2678
+ * Pending reports from an earlier run can be discarded on a generation conflict.
2679
+ * Reporters without an explicit sandbox identity can also rebind a server-attested sandbox run.
2680
+ * An explicitly bound live sandbox run stops on conflict.
2681
+ * @returns whether the report was discarded and the run needs a new registration.
2682
+ */
2683
+ async replay(state, record, run, signal) {
2684
+ try {
2685
+ await this.observation(state, record, run, signal);
2686
+ } catch (error) {
2687
+ if (!(error instanceof ObservationError) || error.code !== "AGENT_CONFIG_HOST_GENERATION_CONFLICT" || run.started && (this.options.sandbox !== void 0 || run.holder !== "sandbox")) throw error;
2688
+ record.pendingReport = null;
2689
+ if (run.started) {
2690
+ run.bootId = brandString(randomUUID());
2691
+ run.started = false;
2692
+ }
2693
+ await this.save(state);
2694
+ return true;
2695
+ }
2696
+ return false;
2697
+ }
2698
+ snapshot() {
2699
+ const state = this.read();
2700
+ const rejected = state.lastRejection;
2701
+ const current = state.current.sha256 === null ? {
2702
+ source: "builtin",
2703
+ version: state.current.version,
2704
+ sha256: null
2705
+ } : {
2706
+ source: "registry",
2707
+ version: state.current.version,
2708
+ sha256: state.current.sha256
2709
+ };
2710
+ return ModuleAgentConfigSnapshot.parse({
2711
+ hostAgentConfigApiVersion: HOST_AGENT_CONFIG_API_VERSION,
2712
+ runtimeApiVersion: this.options.runtimeApiVersion,
2713
+ current,
2714
+ ...this.options.sandbox === void 0 ? {} : { blockingFailure: state.blockingFailure === null ? null : {
2715
+ code: state.blockingFailure.code,
2716
+ committed: current
2717
+ } },
2718
+ pending: state.pending === null ? null : {
2719
+ version: state.pending.version,
2720
+ sha256: state.pending.sha256
2721
+ },
2722
+ blocked: state.blocked,
2723
+ lastRejection: rejected === null ? null : {
2724
+ code: rejected.code,
2725
+ target: rejected.version === void 0 || rejected.sha256 === void 0 ? null : {
2726
+ version: rejected.version,
2727
+ sha256: rejected.sha256
2728
+ }
2729
+ }
2730
+ });
2731
+ }
2732
+ async report(revision, explicitActivation = false, activated = false) {
2733
+ if (this.lifetime.signal.aborted) return;
2734
+ if (explicitActivation) this.stopped = void 0;
2735
+ const run = this.scope(revision);
2736
+ if (run === void 0) {
2737
+ if (explicitActivation) throw new ObservationError("AGENT_CONFIG_HOST_UNAVAILABLE");
2738
+ return;
2739
+ }
2740
+ const member = this.member();
2741
+ if (member === void 0) return;
2742
+ if (this.options.sandbox === void 0) await this.migrate(run, member, explicitActivation);
2743
+ if (explicitActivation) await this.options.replacementState.markReplaced(member);
2744
+ else if (activated && this.preparedHostId !== run.hostId) throw new ObservationError("AGENT_CONFIG_HOST_UNAVAILABLE");
2745
+ this.activationRequest = explicitActivation || activated;
2746
+ const active = this.active = new AbortController();
2747
+ const signal = AbortSignal.any([
2748
+ active.signal,
2749
+ this.lifetime.signal,
2750
+ AbortSignal.timeout(this.options.requestTimeoutMs)
2751
+ ]);
2752
+ try {
2753
+ await mkdir(this.options.stateRoot, {
2754
+ recursive: true,
2755
+ mode: 448
2756
+ });
2757
+ await withFileLock(join(this.options.stateRoot, `agent-config-${this.options.workspaceKind}-${run.hostId}.json`), async () => {
2758
+ this.current(run, signal);
2759
+ const state = await this.load(run.hostId);
2760
+ this.current(run, signal);
2761
+ const record = state.scopes[run.key] ??= {
2762
+ generation: 0,
2763
+ running: null,
2764
+ pendingRegistration: null,
2765
+ pendingReport: null
2766
+ };
2767
+ await this.attestSandbox(state, record, run, signal);
2768
+ const uncertainGeneration = record.pendingRegistration?.expectedGeneration;
2769
+ if (explicitActivation) {
2770
+ this.preparedRevision = void 0;
2771
+ this.preparedHostId = void 0;
2772
+ record.pendingRegistration = null;
2773
+ record.pendingReport = null;
2774
+ run.bootId = brandString(randomUUID());
2775
+ run.started = false;
2776
+ } else if (!activated && (this.replaced || record.pendingRegistration?.explicitActivation === true)) {
2777
+ await this.rememberReplacement(state, record, member);
2778
+ this.options.onReplaced?.(run.hostId);
2779
+ return;
2780
+ }
2781
+ try {
2782
+ if (run.started && record.running?.bootId !== run.bootId) throw new ObservationError("AGENT_CONFIG_HOST_CONFLICT");
2783
+ await this.registration(state, record, run, signal);
2784
+ await this.replay(state, record, run, signal);
2785
+ if (record.running?.bootId === run.bootId) run.started = true;
2786
+ for (let rebinds = 0;; rebinds++) {
2787
+ if (!run.started) {
2788
+ if (state.bootSequence >= Number.MAX_SAFE_INTEGER) throw new ObservationError("AGENT_CONFIG_HOST_STATE_INVALID");
2789
+ state.bootSequence++;
2790
+ record.pendingRegistration = {
2791
+ hostId: run.hostId,
2792
+ bootId: run.bootId,
2793
+ requestId: randomUUID(),
2794
+ bootSequence: state.bootSequence,
2795
+ expectedGeneration: explicitActivation ? uncertainGeneration ?? record.generation : record.generation,
2796
+ ...run.sandboxBinding === void 0 ? {} : { sandboxBinding: run.sandboxBinding },
2797
+ ...explicitActivation ? { explicitActivation: true } : {}
2798
+ };
2799
+ await this.save(state);
2800
+ try {
2801
+ await this.registration(state, record, run, signal);
2802
+ } catch (error) {
2803
+ if (!explicitActivation || uncertainGeneration === void 0 || !(error instanceof ObservationError) || error.code !== "AGENT_CONFIG_HOST_GENERATION_CONFLICT") throw error;
2804
+ const pending = record.pendingRegistration;
2805
+ record.pendingRegistration = {
2806
+ ...pending,
2807
+ requestId: randomUUID(),
2808
+ expectedGeneration: pending.expectedGeneration + 1
2809
+ };
2810
+ await this.save(state);
2811
+ await this.registration(state, record, run, signal);
2812
+ }
2813
+ run.started = true;
2814
+ }
2815
+ if (explicitActivation) {
2816
+ this.preparedRevision = revision;
2817
+ this.preparedHostId = run.hostId;
2818
+ return;
2819
+ }
2820
+ if (record.running === null || record.running.sequence >= Number.MAX_SAFE_INTEGER) throw new ObservationError("AGENT_CONFIG_HOST_STATE_INVALID");
2821
+ record.pendingReport = {
2822
+ ...this.snapshot(),
2823
+ generation: record.generation,
2824
+ sequence: record.running.sequence + 1
2825
+ };
2826
+ await this.save(state);
2827
+ if (!await this.replay(state, record, run, signal)) break;
2828
+ if (rebinds > 0) throw new ObservationError("AGENT_CONFIG_HOST_UNAVAILABLE");
2829
+ }
2830
+ delete record.replaced;
2831
+ if (activated) await this.save(state);
2832
+ } catch (error) {
2833
+ if (this.options.sandbox === void 0 && (explicitActivation || error instanceof ObservationError && error.code === "LOCAL_HOST_REPLACED")) {
2834
+ await this.rememberReplacement(state, record, member);
2835
+ this.options.onReplaced?.(run.hostId);
2836
+ }
2837
+ throw error;
2838
+ }
2839
+ }, {
2840
+ signal,
2841
+ waitMs: this.options.requestTimeoutMs
2842
+ });
2843
+ } finally {
2844
+ this.activationRequest = false;
2845
+ if (this.active === active) this.active = void 0;
2846
+ }
2847
+ }
2848
+ async dispose() {
2849
+ this.lifetime.abort();
2850
+ this.active?.abort();
2851
+ if (this.timer !== void 0) clearTimeout(this.timer);
2852
+ this.stopScope();
2853
+ this.stopActivation();
2854
+ this.stopActivated();
2855
+ this.stopIdentity();
2856
+ this.stopReplacement();
2857
+ await this.chain;
2858
+ }
2859
+ };
2860
+ //#endregion
2861
+ //#region lib/types/index.js
2862
+ /**
2863
+ * `WorkspaceBundles` Service Definition (`ctx.workspaceBundles`) and its Provider.
2864
+ * The Provider resolves its Config at load, so missing materialize roots fail plugin load.
2865
+ */
2866
+ var types_default = GestaltWorkspaceBundles;
2867
+ //#endregion
2868
+ export { AgentConfigDescriptorSchema, AgentConfigError, BundledAgentConfigReceiptSchema, Config, GestaltWorkspaceBundles, HOST_AGENT_CONFIG_API_VERSION, LocalHostIdentityLoadError, LocalHostIdentityWriteError, WorkspaceBundleError, WorkspaceBundles, WorkspaceBundlesConfigError, agentConfigHostObservation, agentConfigSha256, assertTemplateRelativePath, bundledDirectory, crashableFs, createNodeFs, decodeAgentConfig, types_default as default, digestOf, encodeBundleSegment, getModuleAgentConfigAssignment, identityOf, isAgentConfigVersion, loadBundledAgentConfigSync, loadVerifiedManifest, loadVerifiedManifestSync, overallDigest, parseTemplateManifest, recordWorkspaceAgentConfigVersion, resolveWorkspaceBundlesSpec, sandboxAgentConfigHostObservation, sharedLocalHostId, verifyTemplateTree };