@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.
- package/README.i18n.yaml +6 -6
- package/README.md +28 -8
- package/README.zh.md +28 -8
- package/lib/index.js +1737 -237
- package/lib/types/agent-config-contract/agent-configs.d.ts +621 -0
- package/lib/types/agent-config-contract/agent-configs.js +68 -0
- package/lib/types/agent-config-contract/cs-hosting.d.ts +1333 -0
- package/lib/types/agent-config-contract/cs-hosting.js +163 -0
- package/lib/types/agent-config-contract/errors.d.ts +224 -0
- package/lib/types/agent-config-contract/errors.js +202 -0
- package/lib/types/agent-config-contract/ids.d.ts +65 -0
- package/lib/types/agent-config-contract/ids.js +35 -0
- package/lib/types/agent-config-contract/route.d.ts +56 -0
- package/lib/types/agent-config-contract/route.js +1 -0
- package/lib/types/agent-config-contract/test-account-claim.d.ts +24 -0
- package/lib/types/agent-config-contract/test-account-claim.js +5 -0
- package/lib/types/agent-config-format.d.ts +5 -4
- package/lib/types/agent-config-format.js +7 -4
- package/lib/types/agent-config-observation.d.ts +64 -0
- package/lib/types/agent-config-observation.js +448 -0
- package/lib/types/agent-config-source.d.ts +31 -5
- package/lib/types/agent-config-source.js +175 -32
- package/lib/types/agent-config-types.d.ts +33 -0
- package/lib/types/bundled-agent-config.d.ts +29 -0
- package/lib/types/bundled-agent-config.js +47 -0
- package/lib/types/definition.d.ts +18 -4
- package/lib/types/definition.js +10 -2
- package/lib/types/index.d.ts +9 -1
- package/lib/types/index.js +5 -1
- package/lib/types/local-host-identity.d.ts +65 -0
- package/lib/types/local-host-identity.js +87 -0
- package/lib/types/local-host-replacement.d.ts +49 -0
- package/lib/types/local-host-replacement.js +112 -0
- package/lib/types/provider.d.ts +7 -1
- package/lib/types/provider.js +117 -19
- package/lib/types/types.d.ts +4 -1
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- 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
|
-
/**
|
|
92
|
-
|
|
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
|
-
*
|
|
560
|
-
* @param
|
|
561
|
-
* @
|
|
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
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
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
|
|
571
|
-
* @param
|
|
572
|
-
* @param
|
|
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
|
|
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
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
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
|
-
|
|
658
|
-
|
|
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
|
-
|
|
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
|
-
|
|
688
|
-
|
|
689
|
-
|
|
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
|
-
|
|
1043
|
+
const { bundleId, workspaceKey, title } = this.registration.template;
|
|
1044
|
+
return {
|
|
702
1045
|
template: {
|
|
703
|
-
|
|
1046
|
+
bundleId,
|
|
1047
|
+
workspaceKey,
|
|
1048
|
+
title,
|
|
704
1049
|
sourceRoot: join(directory, receipt.version),
|
|
705
1050
|
manifest,
|
|
706
1051
|
agentConfigSha256: receipt.sha256
|
|
707
1052
|
},
|
|
708
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
1114
|
+
const failure = this.configError(error);
|
|
741
1115
|
this.failure = {
|
|
742
1116
|
code: failure.code,
|
|
743
1117
|
message: failure.message
|
|
744
1118
|
};
|
|
745
|
-
this.
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
962
|
-
|
|
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(() =>
|
|
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)
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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 };
|