@gestaltrun/dsh-workspace-bundle 0.1.0 → 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 (39) hide show
  1. package/README.i18n.yaml +6 -6
  2. package/README.md +28 -8
  3. package/README.zh.md +28 -8
  4. package/lib/index.js +1737 -237
  5. package/lib/types/agent-config-contract/agent-configs.d.ts +621 -0
  6. package/lib/types/agent-config-contract/agent-configs.js +68 -0
  7. package/lib/types/agent-config-contract/cs-hosting.d.ts +1333 -0
  8. package/lib/types/agent-config-contract/cs-hosting.js +163 -0
  9. package/lib/types/agent-config-contract/errors.d.ts +224 -0
  10. package/lib/types/agent-config-contract/errors.js +202 -0
  11. package/lib/types/agent-config-contract/ids.d.ts +65 -0
  12. package/lib/types/agent-config-contract/ids.js +35 -0
  13. package/lib/types/agent-config-contract/route.d.ts +56 -0
  14. package/lib/types/agent-config-contract/route.js +1 -0
  15. package/lib/types/agent-config-contract/test-account-claim.d.ts +24 -0
  16. package/lib/types/agent-config-contract/test-account-claim.js +5 -0
  17. package/lib/types/agent-config-format.d.ts +5 -4
  18. package/lib/types/agent-config-format.js +7 -4
  19. package/lib/types/agent-config-observation.d.ts +64 -0
  20. package/lib/types/agent-config-observation.js +448 -0
  21. package/lib/types/agent-config-source.d.ts +31 -5
  22. package/lib/types/agent-config-source.js +175 -32
  23. package/lib/types/agent-config-types.d.ts +33 -0
  24. package/lib/types/bundled-agent-config.d.ts +29 -0
  25. package/lib/types/bundled-agent-config.js +47 -0
  26. package/lib/types/definition.d.ts +18 -4
  27. package/lib/types/definition.js +10 -2
  28. package/lib/types/index.d.ts +9 -1
  29. package/lib/types/index.js +5 -1
  30. package/lib/types/local-host-identity.d.ts +65 -0
  31. package/lib/types/local-host-identity.js +87 -0
  32. package/lib/types/local-host-replacement.d.ts +49 -0
  33. package/lib/types/local-host-replacement.js +112 -0
  34. package/lib/types/provider.d.ts +7 -1
  35. package/lib/types/provider.js +117 -19
  36. package/lib/types/types.d.ts +4 -1
  37. package/locale/en.json +1 -1
  38. package/locale/zh.json +1 -1
  39. package/package.json +17 -13
package/lib/index.js CHANGED
@@ -1,13 +1,14 @@
1
1
  import { basename, dirname, isAbsolute, join, normalize, posix, sep } from "node:path";
2
- import { brandString } from "@deepseek-ai/dsh-brand";
3
- import Schema from "@deepseek-ai/schemastery";
4
- import { Service } from "@deepseek-ai/cordis";
5
- import { createHash } from "node:crypto";
6
2
  import { constants, existsSync, lstatSync, readFileSync, readdirSync } from "node:fs";
7
- import { access, chmod, copyFile, lstat, mkdir, mkdtemp, readFile, readdir, realpath, rename, rm, writeFile } from "node:fs/promises";
8
3
  import { z } from "zod";
4
+ import { createHash, randomUUID } from "node:crypto";
9
5
  import { gunzipSync } from "node:zlib";
10
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";
11
12
  //#region lib/types/errors.js
12
13
  /** Misconfiguration of the workspace-bundle row; plugin load fails with it. */
13
14
  var WorkspaceBundlesConfigError = class extends Error {
@@ -36,6 +37,214 @@ var WorkspaceBundleError = class extends Error {
36
37
  }
37
38
  };
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
39
248
  //#region lib/types/config.js
40
249
  /** workspace-bundle Config: Schemastery field validation and explicit resolution into a Spec. */
41
250
  /**
@@ -81,6 +290,236 @@ function resolveWorkspaceBundlesSpec(config = {}) {
81
290
  };
82
291
  }
83
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
84
523
  //#region lib/types/definition.js
85
524
  /** `WorkspaceBundles` Service Definition: Host-only template registration and stable Workspace materialize. */
86
525
  /**
@@ -88,56 +527,18 @@ function resolveWorkspaceBundlesSpec(config = {}) {
88
527
  * replace its tree as a whole. Registration is an effect; the returned disposer forbids later acquire.
89
528
  */
90
529
  var WorkspaceBundles = class extends Service {
91
- /** @param ctx - context owning this Provider. */
92
- constructor(ctx) {
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) {
93
536
  super(ctx, "workspaceBundles");
537
+ this.localHostReplacement = new LocalHostReplacementState(replacementPath);
538
+ this.localHostIdentity = new LocalHostIdentityState(identityPath);
94
539
  }
95
540
  };
96
541
  //#endregion
97
- //#region lib/types/paths.js
98
- /** Controlled encoding of branded Workspace bundle identities into Home path segments. */
99
- const SEGMENT = /^[a-z0-9][a-z0-9._-]{0,63}$/u;
100
- /**
101
- * Encode one branded identity as a single path segment.
102
- * @param value - bundle id or workspace key.
103
- * @param field - diagnostic name.
104
- * @returns the same spelling when it is a controlled segment.
105
- * @throws {WorkspaceBundleError} When the value could traverse or collide with `.` / `..`.
106
- */
107
- function encodeBundleSegment(value, field) {
108
- 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`);
109
- return value;
110
- }
111
- /**
112
- * Join a parent with `{bundleId}/{workspaceKey}`.
113
- * @param parent - absolute parent already validated by Config.
114
- * @param identity - branded pair.
115
- * @returns the joined path using the host separator.
116
- */
117
- function bundledDirectory(parent, identity) {
118
- return join(parent, encodeBundleSegment(identity.bundleId, "bundleId"), encodeBundleSegment(identity.workspaceKey, "workspaceKey"));
119
- }
120
- /**
121
- * Reject a template-relative path that leaves the root, is absolute, or uses a reserved segment.
122
- * @param relativePath - POSIX relative path from the manifest.
123
- * @returns the same spelling.
124
- * @throws {WorkspaceBundleError} On traversal, absolute paths, empty segments, or backslashes.
125
- */
126
- function assertTemplateRelativePath(relativePath) {
127
- 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`);
128
- if (relativePath.split("/").some((part) => part === "" || part === "." || part === "..")) throw new WorkspaceBundleError("WORKSPACE_BUNDLE_PATH_TRAVERSAL", `template path ${JSON.stringify(relativePath)} contains a reserved segment`);
129
- return relativePath;
130
- }
131
- /** @param bundleId - branded bundle identity. @param workspaceKey - branded workspace key. */
132
- function identityOf(bundleId, workspaceKey) {
133
- encodeBundleSegment(bundleId, "bundleId");
134
- encodeBundleSegment(workspaceKey, "workspaceKey");
135
- return {
136
- bundleId,
137
- workspaceKey
138
- };
139
- }
140
- //#endregion
141
542
  //#region lib/types/digest.js
142
543
  /** Template manifest parsing and per-file / overall digest verification. */
143
544
  const SHA256 = /^[0-9a-f]{64}$/u;
@@ -456,153 +857,37 @@ async function assertRecordPaths(fs, record, root) {
456
857
  for (const [stored, controlled] of [
457
858
  [record.path, root],
458
859
  [record.journal.stagingPath, `${root}.staging`],
459
- [record.journal.backupPath, `${root}.backup`]
460
- ]) 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}`);
461
- }
462
- /**
463
- * Persist the journal as part of the product record.
464
- * @param fs - filesystem.
465
- * @param path - record.json path.
466
- * @param record - current record.
467
- * @param journal - next journal.
468
- */
469
- async function writeJournal(fs, path, record, journal) {
470
- const next = {
471
- ...record,
472
- journal
473
- };
474
- await writeRecord(fs, path, next);
475
- return next;
476
- }
477
- /**
478
- * Mark a replacement as committed: idle journal, new digest and plugin version.
479
- * @param record - current record.
480
- * @param digest - verified tree digest.
481
- * @param pluginVersion - packaged plugin version.
482
- */
483
- function committedRecord(record, digest, pluginVersion) {
484
- return {
485
- ...record,
486
- pluginVersion,
487
- templateDigest: digest,
488
- journal: idleJournal(),
489
- lastFailure: null
490
- };
491
- }
492
- //#endregion
493
- //#region lib/types/agent-config-format.js
494
- /** 内置 Workspace Agent 配置的文件校验;目录和压缩字节的大小均由部署配置限制。 */
495
- /** 本 Host 实现的 Agent 配置接口版本;白名单与写操作授权由 Host 保持。 */
496
- const HOST_AGENT_CONFIG_API_VERSION = "1.0.0";
497
- /** Agent 配置 SHA-256 的十六进制编码。 */
498
- const AgentConfigSha256 = z.string().regex(/^[a-f0-9]{64}$/u);
499
- /** 内置 Workspace 种类;不含路径分隔符。 */
500
- const WorkspaceKind = z.string().regex(/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u).max(80);
501
- /** @param value - 完整 Agent 配置版本。 @returns 是否为保留构建元数据的规范 SemVer;优先级由 semver 按规范比较。 */
502
- function isAgentConfigVersion(value) {
503
- const parsed = semver.parse(value);
504
- return parsed !== null && `${parsed.version}${parsed.build.length === 0 ? "" : `+${parsed.build.join(".")}`}` === value;
505
- }
506
- /** server 分配中可公开的包信息,未携带商家凭据。 */
507
- const AgentConfigDescriptorSchema = z.strictObject({
508
- workspaceKind: WorkspaceKind,
509
- version: z.string().refine(isAgentConfigVersion),
510
- sha256: AgentConfigSha256,
511
- sizeBytes: z.number().int().positive().max(Number.MAX_SAFE_INTEGER),
512
- downloadUrl: z.url(),
513
- hostAgentConfigApiRange: z.string().refine((value) => semver.validRange(value) !== null),
514
- releaseNotes: z.string().min(1).max(2e4)
515
- });
516
- /** 包内逐文件摘要与字节数,文件路径相对版本目录。 */
517
- const AgentConfigManifestSchema = z.strictObject({
518
- formatVersion: z.literal(1),
519
- workspaceKind: WorkspaceKind,
520
- version: z.string().refine(isAgentConfigVersion),
521
- hostAgentConfigApiRange: z.string().refine((value) => semver.validRange(value) !== null),
522
- files: z.array(z.strictObject({
523
- path: z.string(),
524
- sizeBytes: z.number().int().nonnegative(),
525
- sha256: AgentConfigSha256
526
- })).min(1).max(1e4)
527
- });
528
- /** 确定性 gzip JSON Agent 配置:清单和按同序排列的 base64 文件。 */
529
- const AgentConfigDocumentSchema = z.strictObject({
530
- manifest: AgentConfigManifestSchema,
531
- files: z.array(z.strictObject({
532
- path: z.string(),
533
- base64: z.string()
534
- })).min(1).max(1e4)
535
- });
536
- /** 有界内容更新的结构化失败。 */
537
- var AgentConfigError = class extends Error {
538
- code;
539
- /** @param code - 失败类别。 */
540
- constructor(code) {
541
- super({
542
- AGENT_CONFIG_QUERY_FAILED: "Agent 配置版本查询失败,保留当前版本;请检查服务器连接。",
543
- AGENT_CONFIG_INVALID: "Agent 配置文件或清单无效,保留当前版本;请重新打包发布。",
544
- AGENT_CONFIG_INCOMPATIBLE: "Agent 配置不兼容当前 Host,保留当前版本;请分配兼容版本或升级客户端。",
545
- AGENT_CONFIG_DIGEST_MISMATCH: "Agent 配置摘要不匹配,保留当前版本;请核对发布登记。",
546
- AGENT_CONFIG_DOWNLOAD_FAILED: "Agent 配置下载失败,保留当前版本;请检查文件服务连接。",
547
- AGENT_CONFIG_APPLY_FAILED: "Agent 配置应用失败;请检查工作区恢复状态后再继续。",
548
- AGENT_CONFIG_SCOPE_CHANGED: "Agent 配置版本来源已变更,已丢弃旧候选;保留当前版本并重新查询。"
549
- }[code]);
550
- this.code = code;
551
- this.name = "AgentConfigError";
552
- }
553
- };
554
- /** @param bytes - 完整文件字节。 @returns SHA-256 小写十六进制编码。 */
555
- function agentConfigSha256(bytes) {
556
- return createHash("sha256").update(bytes).digest("hex");
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}`);
557
862
  }
558
863
  /**
559
- * 拒绝无法在各 Host 文件系统安全表达的相对路径。
560
- * @param path - 包内 POSIX 路径。
561
- * @returns 原始路径。
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.
562
869
  */
563
- function contentPath(path) {
564
- assertTemplateRelativePath(path);
565
- 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");
566
- return path;
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;
567
877
  }
568
878
  /**
569
- * 同时认证压缩包和逐文件字节,任何失败均发生在版本目录写入前。
570
- * @param bytes - 下载的完整 gzip 字节。
571
- * @param descriptor - server 经鉴权查询给出的登记信息。
572
- * @param maxExpandedBytes - 解压 JSON 和文件总字节上限。
573
- * @returns 清单和已校验文件。
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.
574
883
  */
575
- function decodeAgentConfig(bytes, descriptor, maxExpandedBytes) {
576
- if (bytes.length !== descriptor.sizeBytes || agentConfigSha256(bytes) !== descriptor.sha256) throw new AgentConfigError("AGENT_CONFIG_DIGEST_MISMATCH");
577
- if (!semver.satisfies("1.0.0", descriptor.hostAgentConfigApiRange, { includePrerelease: true })) throw new AgentConfigError("AGENT_CONFIG_INCOMPATIBLE");
578
- let document;
579
- try {
580
- document = AgentConfigDocumentSchema.parse(JSON.parse(gunzipSync(bytes, { maxOutputLength: maxExpandedBytes }).toString("utf8")));
581
- } catch (error) {
582
- throw new AgentConfigError("AGENT_CONFIG_INVALID");
583
- }
584
- const { manifest } = document;
585
- 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");
586
- const seen = /* @__PURE__ */ new Set();
587
- let total = 0;
588
- let previous = "";
884
+ function committedRecord(record, digest, pluginVersion) {
589
885
  return {
590
- manifest,
591
- files: document.files.map((file, index) => {
592
- const entry = manifest.files[index];
593
- const path = contentPath(file.path);
594
- const normalized = path.toLowerCase();
595
- if (entry.path !== path || path <= previous || seen.has(normalized) || [...seen].some((parent) => normalized.startsWith(`${parent}/`) || parent.startsWith(`${normalized}/`))) throw new AgentConfigError("AGENT_CONFIG_INVALID");
596
- seen.add(normalized);
597
- previous = path;
598
- const data = Buffer.from(file.base64, "base64");
599
- total += data.length;
600
- if (total > maxExpandedBytes || data.toString("base64") !== file.base64 || data.length !== entry.sizeBytes || agentConfigSha256(data) !== entry.sha256) throw new AgentConfigError("AGENT_CONFIG_DIGEST_MISMATCH");
601
- return {
602
- path,
603
- bytes: data
604
- };
605
- })
886
+ ...record,
887
+ pluginVersion,
888
+ templateDigest: digest,
889
+ journal: idleJournal(),
890
+ lastFailure: null
606
891
  };
607
892
  }
608
893
  //#endregion
@@ -644,6 +929,7 @@ var WorkspaceAgentConfigSourceOwner = class {
644
929
  spec;
645
930
  report;
646
931
  download;
932
+ recoverIdle;
647
933
  lifetime = new AbortController();
648
934
  inFlight;
649
935
  requestAbort;
@@ -653,26 +939,39 @@ var WorkspaceAgentConfigSourceOwner = class {
653
939
  selected;
654
940
  candidate;
655
941
  failure = null;
942
+ rejection;
943
+ admissionFailure;
656
944
  root;
657
- /** @param registration - 内置模板及鉴权查询回调。 @param spec - 部署预算。 @param report - 固定失败诊断的报告者。 @param download - 文件 HTTP 传输。 */
658
- constructor(registration, spec, report, download = fetch) {
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 () => {}) {
659
950
  this.registration = registration;
660
951
  this.spec = spec;
661
952
  this.report = report;
662
953
  this.download = download;
954
+ this.recoverIdle = recoverIdle;
663
955
  this.selected = {
664
956
  template: registration.template,
665
- sha256: null
957
+ sha256: registration.template.agentConfigSha256 ?? null
666
958
  };
667
959
  this.root = bundledDirectory(join(spec.templateCacheRoot, "agent-config-versions"), registration.source.identity);
668
960
  }
669
- /** @returns 已应用版本、候选版本和最近一次失败。 */
961
+ /** @returns 已应用/候选版本、执行阻断及当前作用域的最近失败和拒绝。 */
670
962
  state() {
671
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;
672
965
  return {
673
- current: publicVersion(this.selected.template, this.selected.sha256),
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),
674
970
  pending: this.candidate === void 0 ? null : publicVersion(this.candidate.template, this.candidate.sha256),
675
- failure: this.failure
971
+ failure: this.admissionFailure === void 0 ? this.failure : {
972
+ code: this.admissionFailure.code,
973
+ message: this.admissionFailure.message
974
+ }
676
975
  };
677
976
  }
678
977
  /**
@@ -683,14 +982,57 @@ var WorkspaceAgentConfigSourceOwner = class {
683
982
  * @returns 对应完整模板;缺失已安装缓存时拒绝继续。
684
983
  */
685
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) {
686
1028
  if (digest === void 0 || sha256 === void 0 || sha256 === null) {
687
- this.selected = {
688
- template: this.registration.template,
689
- 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 }
690
1034
  };
691
- return this.selected.template;
692
1035
  }
693
- if (digest === this.selected.template.manifest.digest && (version === void 0 || version === this.selected.template.manifest.pluginVersion) && sha256 === this.selected.sha256) return this.selected.template;
694
1036
  if (!/^[0-9a-f]{64}$/u.test(sha256)) throw new AgentConfigError("AGENT_CONFIG_INVALID");
695
1037
  const fs = createNodeFs();
696
1038
  const receipt = ReceiptSchema.parse(JSON.parse(await readFile(join(this.root, sha256, "receipt.json"), "utf8")));
@@ -698,22 +1040,46 @@ var WorkspaceAgentConfigSourceOwner = class {
698
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");
699
1041
  const manifest = await loadVerifiedManifest(fs, join(directory, receipt.version));
700
1042
  if (manifest.digest !== digest || version !== void 0 && receipt.version !== version || manifest.pluginVersion !== receipt.version) throw new AgentConfigError("AGENT_CONFIG_INVALID");
701
- this.selected = {
1043
+ const { bundleId, workspaceKey, title } = this.registration.template;
1044
+ return {
702
1045
  template: {
703
- ...this.registration.template,
1046
+ bundleId,
1047
+ workspaceKey,
1048
+ title,
704
1049
  sourceRoot: join(directory, receipt.version),
705
1050
  manifest,
706
1051
  agentConfigSha256: receipt.sha256
707
1052
  },
708
- sha256: receipt.sha256
1053
+ range: receipt.hostAgentConfigApiRange,
1054
+ ...receipt.runtimeApiRange === void 0 ? {} : { runtimeApiRange: receipt.runtimeApiRange }
709
1055
  };
710
- if (this.candidate?.sha256 === receipt.sha256) this.candidate = void 0;
711
- return this.selected.template;
712
1056
  }
713
- /** 注册者调用一次以启动查询,并按配置间隔继续。 */
714
- start() {
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
+ });
715
1077
  const tick = async () => {
716
- await this.refresh();
1078
+ try {
1079
+ await this.refresh();
1080
+ } catch (error) {
1081
+ this.reportFailure(this.configError(error));
1082
+ }
717
1083
  if (!this.lifetime.signal.aborted) this.timer = setTimeout(() => {
718
1084
  tick();
719
1085
  }, this.spec.agentConfigPollIntervalMs);
@@ -722,6 +1088,9 @@ var WorkspaceAgentConfigSourceOwner = class {
722
1088
  }
723
1089
  /** @returns 本次查询和完整候选下载结束;失败通过 state 与 report 保留可观察诊断。 */
724
1090
  refresh() {
1091
+ return this.startup.then(() => this.refreshReady());
1092
+ }
1093
+ refreshReady() {
725
1094
  if (this.lifetime.signal.aborted) return Promise.resolve();
726
1095
  const revision = this.registration.source.revision();
727
1096
  if (this.candidate !== void 0 && this.candidate.revision !== revision) this.candidate = void 0;
@@ -734,15 +1103,21 @@ var WorkspaceAgentConfigSourceOwner = class {
734
1103
  const abort = new AbortController();
735
1104
  this.requestAbort = abort;
736
1105
  this.requestRevision = revision;
737
- const run = this.fetchCandidate(revision, abort.signal).catch((error) => {
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) => {
738
1112
  if (this.lifetime.signal.aborted || abort.signal.aborted || revision !== this.registration.source.revision()) return;
739
1113
  this.candidate = void 0;
740
- const failure = error instanceof AgentConfigError ? error : new AgentConfigError("AGENT_CONFIG_INVALID");
1114
+ const failure = this.configError(error);
741
1115
  this.failure = {
742
1116
  code: failure.code,
743
1117
  message: failure.message
744
1118
  };
745
- this.report(failure);
1119
+ this.rememberRejection(failure, attempt.version, revision, attempt.sha256);
1120
+ this.reportFailure(failure);
746
1121
  }).finally(() => {
747
1122
  this.inFlight = void 0;
748
1123
  this.requestAbort = void 0;
@@ -754,8 +1129,28 @@ var WorkspaceAgentConfigSourceOwner = class {
754
1129
  /** @returns 下一个 turn 可应用的候选;没有候选时使用已安装版本。 */
755
1130
  template() {
756
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;
757
1133
  return this.candidate?.template ?? this.selected.template;
758
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
+ }
759
1154
  /** @param template - 即将整体替换的候选。 @throws 来源代次已经改变或已撤销时拒绝应用。 */
760
1155
  assertApplicable(template) {
761
1156
  const revision = this.preparedRevisions.get(template);
@@ -763,7 +1158,7 @@ var WorkspaceAgentConfigSourceOwner = class {
763
1158
  }
764
1159
  /** @param template - 已通过 Workspace 整体替换提交的模板。 */
765
1160
  async committed(template) {
766
- await this.recover(template.manifest.digest, template.manifest.pluginVersion, template.agentConfigSha256 ?? null);
1161
+ await this.recover(template.manifest.digest, template.manifest.pluginVersion, journalAgentConfigSha256(template, this.registration.template));
767
1162
  }
768
1163
  /**
769
1164
  * 清除未提交候选,报告整体替换失败;已提交状态由 journal 恢复结果决定。
@@ -772,26 +1167,49 @@ var WorkspaceAgentConfigSourceOwner = class {
772
1167
  */
773
1168
  failedApply(template, cause) {
774
1169
  if (this.candidate?.template === template) this.candidate = void 0;
775
- const error = cause instanceof AgentConfigError && cause.code === "AGENT_CONFIG_SCOPE_CHANGED" ? cause : new AgentConfigError("AGENT_CONFIG_APPLY_FAILED");
1170
+ const error = cause instanceof AgentConfigError ? cause : new AgentConfigError("AGENT_CONFIG_APPLY_FAILED");
776
1171
  this.failure = {
777
1172
  code: error.code,
778
1173
  message: error.message
779
1174
  };
780
- this.report(error);
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);
781
1178
  }
782
1179
  /** 撤销查询、清除后续定时器,并等待下载和目录写入完成。 */
783
1180
  async dispose() {
784
1181
  this.lifetime.abort();
1182
+ this.rejection = void 0;
785
1183
  this.candidate = void 0;
786
1184
  if (this.timer !== void 0) clearTimeout(this.timer);
787
- await this.inFlight;
1185
+ await Promise.all([
1186
+ this.startup,
1187
+ this.inFlight,
1188
+ this.observation?.dispose()
1189
+ ]);
788
1190
  this.candidate = void 0;
789
1191
  }
790
1192
  versionDirectory(version, sha256) {
791
1193
  if (!AgentConfigDescriptorSchema.shape.version.safeParse(version).success || !/^[0-9a-f]{64}$/u.test(sha256)) throw new AgentConfigError("AGENT_CONFIG_INVALID");
792
1194
  return join(this.root, sha256);
793
1195
  }
794
- async fetchCandidate(revision, requestSignal) {
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) {
795
1213
  const signal = AbortSignal.any([
796
1214
  this.lifetime.signal,
797
1215
  requestSignal,
@@ -801,20 +1219,26 @@ var WorkspaceAgentConfigSourceOwner = class {
801
1219
  try {
802
1220
  value = await this.registration.source.query(signal);
803
1221
  } catch (error) {
804
- throw new AgentConfigError("AGENT_CONFIG_QUERY_FAILED");
1222
+ throw error instanceof AgentConfigError ? error : new AgentConfigError("AGENT_CONFIG_QUERY_FAILED");
805
1223
  }
806
1224
  const assignment = AssignmentSchema.safeParse(value);
807
1225
  if (!assignment.success || assignment.data.workspaceKind !== this.registration.template.bundleId) throw new AgentConfigError("AGENT_CONFIG_INVALID");
808
1226
  if (assignment.data.source === "builtin") {
809
1227
  this.candidate = void 0;
810
1228
  this.failure = null;
1229
+ this.rejection = void 0;
811
1230
  return;
812
1231
  }
813
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;
814
1236
  if (descriptor.workspaceKind !== assignment.data.workspaceKind) throw new AgentConfigError("AGENT_CONFIG_INVALID");
815
- if (descriptor.sha256 === this.selected.sha256) {
1237
+ this.assertCompatible(descriptor.hostAgentConfigApiRange, descriptor.runtimeApiRange);
1238
+ if (descriptor.sha256 === (this.blockedVersion?.sha256 ?? this.selected.sha256)) {
816
1239
  this.candidate = void 0;
817
1240
  this.failure = null;
1241
+ this.rejection = void 0;
818
1242
  return;
819
1243
  }
820
1244
  if (descriptor.sizeBytes > this.spec.agentConfigMaxDownloadBytes) throw new AgentConfigError("AGENT_CONFIG_INVALID");
@@ -892,12 +1316,17 @@ var WorkspaceAgentConfigSourceOwner = class {
892
1316
  }));
893
1317
  if (manifest.pluginVersion !== descriptor.version || manifest.digest !== overallDigest(files)) throw new AgentConfigError("AGENT_CONFIG_DIGEST_MISMATCH");
894
1318
  if (signal.aborted || revision !== this.registration.source.revision()) return;
1319
+ const { bundleId, workspaceKey, title } = this.registration.template;
895
1320
  const template = {
896
- ...this.registration.template,
1321
+ bundleId,
1322
+ workspaceKey,
1323
+ title,
897
1324
  sourceRoot: tree,
898
1325
  manifest,
899
1326
  agentConfigSha256: descriptor.sha256
900
1327
  };
1328
+ await this.registration.source.validate?.(template, signal);
1329
+ if (signal.aborted || revision !== this.registration.source.revision()) return;
901
1330
  this.preparedRevisions.set(template, revision);
902
1331
  this.candidate = {
903
1332
  template,
@@ -905,6 +1334,7 @@ var WorkspaceAgentConfigSourceOwner = class {
905
1334
  revision
906
1335
  };
907
1336
  this.failure = null;
1337
+ this.rejection = void 0;
908
1338
  }
909
1339
  };
910
1340
  //#endregion
@@ -948,6 +1378,7 @@ var GestaltWorkspaceBundles = class extends WorkspaceBundles {
948
1378
  chain = Promise.resolve();
949
1379
  recycle = /* @__PURE__ */ new Map();
950
1380
  recycleWork = /* @__PURE__ */ new Map();
1381
+ contentDrains = /* @__PURE__ */ new Set();
951
1382
  closed = false;
952
1383
  download;
953
1384
  /**
@@ -958,8 +1389,9 @@ var GestaltWorkspaceBundles = class extends WorkspaceBundles {
958
1389
  * @param options.fetch - 文件 HTTP 传输;默认使用 Host fetch。
959
1390
  */
960
1391
  constructor(ctx, config = {}, options = {}) {
961
- super(ctx);
962
- this.spec = resolveWorkspaceBundlesSpec(config);
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;
963
1395
  this.fs = crashableFs(options.fs ?? createNodeFs(), options.crashAt);
964
1396
  this.download = options.fetch ?? fetch;
965
1397
  ctx.effect(() => async () => {
@@ -968,6 +1400,7 @@ var GestaltWorkspaceBundles = class extends WorkspaceBundles {
968
1400
  this.recycle.clear();
969
1401
  await Promise.all([...this.agentConfigSources.values()].map((source) => source.dispose()));
970
1402
  await this.chain;
1403
+ await Promise.all(this.contentDrains);
971
1404
  await Promise.all(this.recycleWork.values());
972
1405
  this.agentConfigSources.clear();
973
1406
  }, "workspace-bundle: recycle timers");
@@ -1010,9 +1443,23 @@ var GestaltWorkspaceBundles = class extends WorkspaceBundles {
1010
1443
  source
1011
1444
  }, this.spec, (error) => {
1012
1445
  this.ctx.logger.warn("%s:%s", error.code, error.message);
1013
- }, this.download);
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
+ }));
1014
1453
  this.agentConfigSources.set(key, owner);
1015
- owner.start();
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
+ }));
1016
1463
  return async () => {
1017
1464
  await owner.dispose();
1018
1465
  if (this.agentConfigSources.get(key) === owner) this.agentConfigSources.delete(key);
@@ -1034,11 +1481,69 @@ var GestaltWorkspaceBundles = class extends WorkspaceBundles {
1034
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`);
1035
1482
  return this.enqueue(() => this.acquireOne(workspaceId));
1036
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
+ }
1037
1538
  async acquireOne(workspaceId) {
1038
1539
  const record = [...this.records.values()].find((item) => item.workspaceId === workspaceId);
1039
1540
  if (record === void 0 || !record.registered) throw new WorkspaceBundleError("WORKSPACE_BUNDLE_NOT_REGISTERED", `workspace ${workspaceId} is not an acquired bundle Workspace`);
1040
1541
  if (record.journal.phase === "updating") throw new WorkspaceBundleError("WORKSPACE_BUNDLE_NOT_REGISTERED", `workspace ${workspaceId} is updating`);
1041
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) {
1042
1547
  const token = Symbol("workspace-bundle-lease");
1043
1548
  let leases = this.leases.get(workspaceId);
1044
1549
  if (leases === void 0) {
@@ -1066,9 +1571,15 @@ var GestaltWorkspaceBundles = class extends WorkspaceBundles {
1066
1571
  * Replay an interrupted journal before serving the Workspace.
1067
1572
  * @param identity - branded pair.
1068
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.
1069
1575
  */
1070
1576
  async recover(identity) {
1071
- return this.enqueue(() => this.recoverOne(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
+ });
1072
1583
  }
1073
1584
  enqueue(work) {
1074
1585
  const run = this.chain.then(work, work);
@@ -1078,14 +1589,18 @@ var GestaltWorkspaceBundles = class extends WorkspaceBundles {
1078
1589
  async ensureOne(identity) {
1079
1590
  let template = this.requireTemplate(identity);
1080
1591
  const source = this.agentConfigSources.get(keyOf(identity));
1081
- const stored = this.records.get(keyOf(identity)) ?? await this.loadStored(identity);
1082
- if (source !== void 0) template = await source.recover(stored?.templateDigest, stored?.pluginVersion, stored?.agentConfigSha256);
1083
1592
  const recovered = await this.recoverOne(identity);
1084
1593
  const record = this.records.get(keyOf(identity));
1085
- if (source !== void 0) template = await source.recover(record?.templateDigest, record?.pluginVersion, record?.agentConfigSha256);
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
+ }
1086
1600
  if (source !== void 0 && (record?.workspaceId === null || record?.workspaceId === void 0 || (this.leases.get(record.workspaceId)?.size ?? 0) === 0)) template = source.template();
1087
- if (recovered !== void 0 && record !== void 0 && record.templateDigest === template.manifest.digest && record.pluginVersion === template.manifest.pluginVersion && (record.agentConfigSha256 ?? null) === (template.agentConfigSha256 ?? null) && record.journal.phase === "idle") return recovered;
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;
1088
1602
  try {
1603
+ await source?.prepare(template);
1089
1604
  const workspace = await this.replace(template, record, source === void 0 ? void 0 : () => source.assertApplicable(template));
1090
1605
  await source?.committed(template);
1091
1606
  return workspace;
@@ -1190,10 +1705,10 @@ var GestaltWorkspaceBundles = class extends WorkspaceBundles {
1190
1705
  const toDigest = record.journal.toDigest;
1191
1706
  try {
1192
1707
  const source = this.agentConfigSources.get(keyOf(identity));
1193
- if (source !== void 0 && toDigest !== null) template = await source.recover(toDigest, record.journal.pluginVersion ?? void 0, record.journal.agentConfigSha256);
1708
+ if (source !== void 0 && toDigest !== null) template = await source.recoverPhysical(toDigest, record.journal.pluginVersion ?? void 0, record.journal.agentConfigSha256);
1194
1709
  if (template !== void 0 && toDigest === template.manifest.digest) {
1195
1710
  await verifyTemplateTree(this.fs, root, template.manifest);
1196
- return this.commit(identity, record, template);
1711
+ return await this.commit(identity, record, template);
1197
1712
  }
1198
1713
  if (toDigest !== null && template === void 0) {
1199
1714
  const cached = bundledDirectory(this.spec.templateCacheRoot, identity);
@@ -1207,10 +1722,10 @@ var GestaltWorkspaceBundles = class extends WorkspaceBundles {
1207
1722
  sourceRoot: cached,
1208
1723
  manifest
1209
1724
  };
1210
- return this.commit(identity, record, synthetic);
1725
+ return await this.commit(identity, record, synthetic);
1211
1726
  }
1212
1727
  }
1213
- } catch {}
1728
+ } catch (error) {}
1214
1729
  return this.restoreOld(identity, record);
1215
1730
  }
1216
1731
  async replace(template, existing, assertApplicable) {
@@ -1247,7 +1762,7 @@ var GestaltWorkspaceBundles = class extends WorkspaceBundles {
1247
1762
  stagingPath: staging,
1248
1763
  backupPath: await this.fs.exists(root) ? backup : null,
1249
1764
  swapComplete: false,
1250
- ...template.agentConfigSha256 === void 0 ? {} : { agentConfigSha256: template.agentConfigSha256 }
1765
+ agentConfigSha256: journalAgentConfigSha256(template, this.requireTemplate(identity))
1251
1766
  };
1252
1767
  const updating = await writeJournal(this.fs, this.recordFile(identity), {
1253
1768
  ...record,
@@ -1291,7 +1806,7 @@ var GestaltWorkspaceBundles = class extends WorkspaceBundles {
1291
1806
  workspaceId: workspace.id,
1292
1807
  path: workspace.path,
1293
1808
  registered: true,
1294
- ...this.agentConfigSources.has(keyOf(identity)) ? { agentConfigSha256: template.agentConfigSha256 ?? null } : {}
1809
+ agentConfigSha256: journalAgentConfigSha256(template, this.templates.get(keyOf(identity)))
1295
1810
  }, template.manifest.digest, template.manifest.pluginVersion);
1296
1811
  await writeRecord(this.fs, this.recordFile(identity), next);
1297
1812
  this.records.set(keyOf(identity), next);
@@ -1358,6 +1873,991 @@ function recordWorkspaceAgentConfigVersion(session, current) {
1358
1873
  });
1359
1874
  }
1360
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
1361
2861
  //#region lib/types/index.js
1362
2862
  /**
1363
2863
  * `WorkspaceBundles` Service Definition (`ctx.workspaceBundles`) and its Provider.
@@ -1365,4 +2865,4 @@ function recordWorkspaceAgentConfigVersion(session, current) {
1365
2865
  */
1366
2866
  var types_default = GestaltWorkspaceBundles;
1367
2867
  //#endregion
1368
- export { AgentConfigDescriptorSchema, AgentConfigError, Config, GestaltWorkspaceBundles, HOST_AGENT_CONFIG_API_VERSION, WorkspaceBundleError, WorkspaceBundles, WorkspaceBundlesConfigError, agentConfigSha256, assertTemplateRelativePath, bundledDirectory, crashableFs, createNodeFs, decodeAgentConfig, types_default as default, digestOf, encodeBundleSegment, identityOf, loadVerifiedManifest, loadVerifiedManifestSync, overallDigest, parseTemplateManifest, recordWorkspaceAgentConfigVersion, resolveWorkspaceBundlesSpec, verifyTemplateTree };
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 };