@gestaltrun/dsh-workspace-bundle 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.i18n.yaml +6 -6
  2. package/README.md +28 -8
  3. package/README.zh.md +28 -8
  4. package/lib/index.js +1737 -237
  5. package/lib/types/agent-config-contract/agent-configs.d.ts +621 -0
  6. package/lib/types/agent-config-contract/agent-configs.js +68 -0
  7. package/lib/types/agent-config-contract/cs-hosting.d.ts +1333 -0
  8. package/lib/types/agent-config-contract/cs-hosting.js +163 -0
  9. package/lib/types/agent-config-contract/errors.d.ts +224 -0
  10. package/lib/types/agent-config-contract/errors.js +202 -0
  11. package/lib/types/agent-config-contract/ids.d.ts +65 -0
  12. package/lib/types/agent-config-contract/ids.js +35 -0
  13. package/lib/types/agent-config-contract/route.d.ts +56 -0
  14. package/lib/types/agent-config-contract/route.js +1 -0
  15. package/lib/types/agent-config-contract/test-account-claim.d.ts +24 -0
  16. package/lib/types/agent-config-contract/test-account-claim.js +5 -0
  17. package/lib/types/agent-config-format.d.ts +5 -4
  18. package/lib/types/agent-config-format.js +7 -4
  19. package/lib/types/agent-config-observation.d.ts +64 -0
  20. package/lib/types/agent-config-observation.js +448 -0
  21. package/lib/types/agent-config-source.d.ts +31 -5
  22. package/lib/types/agent-config-source.js +175 -32
  23. package/lib/types/agent-config-types.d.ts +33 -0
  24. package/lib/types/bundled-agent-config.d.ts +29 -0
  25. package/lib/types/bundled-agent-config.js +47 -0
  26. package/lib/types/definition.d.ts +18 -4
  27. package/lib/types/definition.js +10 -2
  28. package/lib/types/index.d.ts +9 -1
  29. package/lib/types/index.js +5 -1
  30. package/lib/types/local-host-identity.d.ts +65 -0
  31. package/lib/types/local-host-identity.js +87 -0
  32. package/lib/types/local-host-replacement.d.ts +49 -0
  33. package/lib/types/local-host-replacement.js +112 -0
  34. package/lib/types/provider.d.ts +7 -1
  35. package/lib/types/provider.js +117 -19
  36. package/lib/types/types.d.ts +4 -1
  37. package/locale/en.json +1 -1
  38. package/locale/zh.json +1 -1
  39. package/package.json +17 -13
@@ -3,10 +3,13 @@ import { chmod, mkdir, mkdtemp, readFile, readdir, rename, rm, writeFile } from
3
3
  import { dirname, join } from 'node:path';
4
4
  import { brandString } from '@deepseek-ai/dsh-brand';
5
5
  import { z } from 'zod';
6
+ import semver from 'semver';
7
+ import { WorkspaceBundleError } from "./errors.js";
6
8
  import { AgentConfigDescriptorSchema, AgentConfigError, HOST_AGENT_CONFIG_API_VERSION, decodeAgentConfig } from "./agent-config-format.js";
7
9
  import { loadVerifiedManifest, overallDigest } from "./digest.js";
8
10
  import { createNodeFs } from "./filesystem.js";
9
11
  import { bundledDirectory } from "./paths.js";
12
+ import { journalAgentConfigSha256 } from "./bundled-agent-config.js";
10
13
  const AssignmentSchema = z.discriminatedUnion('source', [
11
14
  z.object({ source: z.literal('builtin'), workspaceKind: z.string() }),
12
15
  z.object({ source: z.literal('registry'), workspaceKind: z.string(), agentConfig: AgentConfigDescriptorSchema }),
@@ -32,6 +35,7 @@ export class WorkspaceAgentConfigSourceOwner {
32
35
  spec;
33
36
  report;
34
37
  download;
38
+ recoverIdle;
35
39
  lifetime = new AbortController();
36
40
  inFlight;
37
41
  requestAbort;
@@ -41,22 +45,32 @@ export class WorkspaceAgentConfigSourceOwner {
41
45
  selected;
42
46
  candidate;
43
47
  failure = null;
48
+ rejection;
49
+ admissionFailure;
44
50
  root;
45
- /** @param registration - 内置模板及鉴权查询回调。 @param spec - 部署预算。 @param report - 固定失败诊断的报告者。 @param download - 文件 HTTP 传输。 */
46
- constructor(registration, spec, report, download = fetch) {
51
+ observation;
52
+ startup = Promise.resolve();
53
+ blockedVersion;
54
+ /** @param registration - 内置模板及鉴权查询回调。 @param spec - 部署预算。 @param report - 固定失败诊断的报告者。 @param download - 文件 HTTP 传输。 @param recoverIdle - Provider 串行检查活动租约后恢复已阻断的配置;省略时保留候选等待获取工作区。 */
55
+ constructor(registration, spec, report, download = fetch, recoverIdle = async () => { }) {
47
56
  this.registration = registration;
48
57
  this.spec = spec;
49
58
  this.report = report;
50
59
  this.download = download;
51
- this.selected = { template: registration.template, sha256: null };
60
+ this.recoverIdle = recoverIdle;
61
+ this.selected = { template: registration.template, sha256: registration.template.agentConfigSha256 ?? null };
52
62
  this.root = bundledDirectory(join(spec.templateCacheRoot, 'agent-config-versions'), registration.source.identity);
53
63
  }
54
- /** @returns 已应用版本、候选版本和最近一次失败。 */
64
+ /** @returns 已应用/候选版本、执行阻断及当前作用域的最近失败和拒绝。 */
55
65
  state() {
56
66
  if (this.candidate !== undefined && this.candidate.revision !== this.registration.source.revision())
57
67
  this.candidate = undefined;
58
- return { current: publicVersion(this.selected.template, this.selected.sha256),
59
- pending: this.candidate === undefined ? null : publicVersion(this.candidate.template, this.candidate.sha256), failure: this.failure };
68
+ if (this.lifetime.signal.aborted || this.rejection?.revision !== this.registration.source.revision())
69
+ this.rejection = undefined;
70
+ return { lastRejection: this.rejection?.value ?? null, blocked: this.admissionFailure !== undefined,
71
+ blockingFailure: this.admissionFailure === undefined ? null : { code: this.admissionFailure.code }, current: this.blockedVersion ?? publicVersion(this.selected.template, this.selected.sha256),
72
+ pending: this.candidate === undefined ? null : publicVersion(this.candidate.template, this.candidate.sha256),
73
+ failure: this.admissionFailure === undefined ? this.failure : { code: this.admissionFailure.code, message: this.admissionFailure.message } };
60
74
  }
61
75
  /**
62
76
  * 从 journal 已提交的摘要定位只读缓存,不从 server 的无分配回答推导降级。
@@ -66,14 +80,54 @@ export class WorkspaceAgentConfigSourceOwner {
66
80
  * @returns 对应完整模板;缺失已安装缓存时拒绝继续。
67
81
  */
68
82
  async recover(digest, version, sha256) {
83
+ const revision = this.registration.source.revision();
84
+ try {
85
+ const template = await this.recoverExecution(digest, version, sha256);
86
+ if (this.rejection?.revision === revision && this.rejection.value.version === template.manifest.pluginVersion)
87
+ this.rejection = undefined;
88
+ return template;
89
+ }
90
+ catch (error) {
91
+ if (digest !== undefined && version !== undefined)
92
+ this.blockedVersion = {
93
+ ...publicVersion(this.registration.template, sha256 ?? null), version, templateDigest: digest,
94
+ };
95
+ const failure = new AgentConfigError(this.configError(error).code, 'blocked');
96
+ const previous = this.admissionFailure;
97
+ this.admissionFailure = failure;
98
+ this.rememberRejection(failure, version, revision, sha256 ?? undefined);
99
+ if (previous?.code !== failure.code)
100
+ this.reportFailure(failure);
101
+ throw failure;
102
+ }
103
+ }
104
+ async recoverExecution(digest, version, sha256) {
105
+ const restored = await this.restoredTemplate(digest, version, sha256);
106
+ this.assertCompatible(restored.range, restored.runtimeApiRange);
107
+ await this.registration.source.validate?.(restored.template, this.lifetime.signal);
108
+ this.selected = { template: restored.template, sha256: restored.template.agentConfigSha256 ?? null };
109
+ this.blockedVersion = undefined;
110
+ this.admissionFailure = undefined;
111
+ if (this.candidate?.sha256 === this.selected.sha256)
112
+ this.candidate = undefined;
113
+ return restored.template;
114
+ }
115
+ /**
116
+ * Load a complete old/new tree only for physical swap recovery; this grants no execution permission.
117
+ * @param digest - Journal template digest.
118
+ * @param version - Journal release version, when recorded.
119
+ * @param sha256 - Journal package digest, absent for builtin.
120
+ * @returns Physically verified template; consumers must subsequently call recover for execution validation.
121
+ */
122
+ async recoverPhysical(digest, version, sha256) {
123
+ return (await this.restoredTemplate(digest, version, sha256)).template;
124
+ }
125
+ async restoredTemplate(digest, version, sha256) {
69
126
  if (digest === undefined || sha256 === undefined || sha256 === null) {
70
- this.selected = { template: this.registration.template, sha256: null };
71
- return this.selected.template;
127
+ const template = this.registration.template;
128
+ return { template, range: template.agentConfigReceipt?.hostAgentConfigApiRange,
129
+ ...(template.agentConfigReceipt?.runtimeApiRange === undefined ? {} : { runtimeApiRange: template.agentConfigReceipt.runtimeApiRange }) };
72
130
  }
73
- if (digest === this.selected.template.manifest.digest
74
- && (version === undefined || version === this.selected.template.manifest.pluginVersion)
75
- && sha256 === this.selected.sha256)
76
- return this.selected.template;
77
131
  if (!/^[0-9a-f]{64}$/u.test(sha256))
78
132
  throw new AgentConfigError('AGENT_CONFIG_INVALID');
79
133
  const fs = createNodeFs();
@@ -86,16 +140,44 @@ export class WorkspaceAgentConfigSourceOwner {
86
140
  if (manifest.digest !== digest || version !== undefined && receipt.version !== version
87
141
  || manifest.pluginVersion !== receipt.version)
88
142
  throw new AgentConfigError('AGENT_CONFIG_INVALID');
89
- this.selected = { template: { ...this.registration.template, sourceRoot: join(directory, receipt.version), manifest,
90
- agentConfigSha256: receipt.sha256 }, sha256: receipt.sha256 };
91
- if (this.candidate?.sha256 === receipt.sha256)
92
- this.candidate = undefined;
93
- return this.selected.template;
143
+ const { bundleId, workspaceKey, title } = this.registration.template;
144
+ return { template: { bundleId, workspaceKey, title, sourceRoot: join(directory, receipt.version), manifest,
145
+ agentConfigSha256: receipt.sha256 }, range: receipt.hostAgentConfigApiRange,
146
+ ...(receipt.runtimeApiRange === undefined ? {} : { runtimeApiRange: receipt.runtimeApiRange }) };
147
+ }
148
+ assertCompatible(range, runtimeApiRange) {
149
+ if (range !== undefined && !semver.satisfies(HOST_AGENT_CONFIG_API_VERSION, range, { includePrerelease: true }))
150
+ throw new AgentConfigError('AGENT_CONFIG_INCOMPATIBLE');
151
+ const runtime = this.registration.source.runtimeApiVersion;
152
+ if (runtimeApiRange !== undefined && (runtime === undefined || !semver.satisfies(runtime, runtimeApiRange, { includePrerelease: true })))
153
+ throw new AgentConfigError('AGENT_CONFIG_INCOMPATIBLE');
154
+ }
155
+ configError(error) {
156
+ return error instanceof AgentConfigError ? error : new AgentConfigError(error instanceof WorkspaceBundleError
157
+ && error.code === 'WORKSPACE_BUNDLE_DIGEST_MISMATCH' ? 'AGENT_CONFIG_DIGEST_MISMATCH' : 'AGENT_CONFIG_INVALID');
158
+ }
159
+ reportFailure(error) {
160
+ try {
161
+ this.report(error);
162
+ }
163
+ catch (reportError) {
164
+ // stderr remains available if the Cordis reporter has already been released.
165
+ process.stderr.write(`${error.code}: ${error.message}\n`);
166
+ }
94
167
  }
95
- /** 注册者调用一次以启动查询,并按配置间隔继续。 */
96
- start() {
168
+ /** @param restore - Restore this identity's committed journal before starting reports or queries; omission is for an owner without a journal. */
169
+ start(restore = async () => { }) {
170
+ this.startup = restore().then(() => {
171
+ if (!this.lifetime.signal.aborted)
172
+ this.observation = this.registration.source.observation?.(() => this.state());
173
+ });
97
174
  const tick = async () => {
98
- await this.refresh();
175
+ try {
176
+ await this.refresh();
177
+ }
178
+ catch (error) {
179
+ this.reportFailure(this.configError(error));
180
+ }
99
181
  if (!this.lifetime.signal.aborted)
100
182
  this.timer = setTimeout(() => { void tick(); }, this.spec.agentConfigPollIntervalMs);
101
183
  };
@@ -103,6 +185,9 @@ export class WorkspaceAgentConfigSourceOwner {
103
185
  }
104
186
  /** @returns 本次查询和完整候选下载结束;失败通过 state 与 report 保留可观察诊断。 */
105
187
  refresh() {
188
+ return this.startup.then(() => this.refreshReady());
189
+ }
190
+ refreshReady() {
106
191
  if (this.lifetime.signal.aborted)
107
192
  return Promise.resolve();
108
193
  const revision = this.registration.source.revision();
@@ -118,13 +203,22 @@ export class WorkspaceAgentConfigSourceOwner {
118
203
  const abort = new AbortController();
119
204
  this.requestAbort = abort;
120
205
  this.requestRevision = revision;
121
- const run = this.fetchCandidate(revision, abort.signal).catch((error) => {
206
+ const attempt = {};
207
+ const run = this.fetchCandidate(revision, abort.signal, attempt).then(async () => {
208
+ if (this.lifetime.signal.aborted || abort.signal.aborted || revision !== this.registration.source.revision())
209
+ return;
210
+ if (this.admissionFailure !== undefined && this.candidate !== undefined)
211
+ await this.recoverIdle();
212
+ if (!this.lifetime.signal.aborted)
213
+ await this.observation?.refresh();
214
+ }).catch((error) => {
122
215
  if (this.lifetime.signal.aborted || abort.signal.aborted || revision !== this.registration.source.revision())
123
216
  return;
124
217
  this.candidate = undefined;
125
- const failure = error instanceof AgentConfigError ? error : new AgentConfigError('AGENT_CONFIG_INVALID');
218
+ const failure = this.configError(error);
126
219
  this.failure = { code: failure.code, message: failure.message };
127
- this.report(failure);
220
+ this.rememberRejection(failure, attempt.version, revision, attempt.sha256);
221
+ this.reportFailure(failure);
128
222
  }).finally(() => { this.inFlight = undefined; this.requestAbort = undefined; this.requestRevision = undefined; });
129
223
  this.inFlight = run;
130
224
  return run;
@@ -133,8 +227,31 @@ export class WorkspaceAgentConfigSourceOwner {
133
227
  template() {
134
228
  if (this.candidate !== undefined && this.candidate.revision !== this.registration.source.revision())
135
229
  this.candidate = undefined;
230
+ if (this.candidate === undefined && this.admissionFailure !== undefined)
231
+ throw this.admissionFailure;
136
232
  return this.candidate?.template ?? this.selected.template;
137
233
  }
234
+ /** @throws AgentConfigError while committed content is blocked, including when another lease remains active. */
235
+ assertExecutionAllowed() {
236
+ if (this.admissionFailure !== undefined)
237
+ throw this.admissionFailure;
238
+ }
239
+ /** @param template - Pending template. @returns After compatibility and consumer validation, without selecting it. */
240
+ async prepare(template) {
241
+ try {
242
+ await this.prepareValidated(template);
243
+ }
244
+ catch (error) {
245
+ throw this.configError(error);
246
+ }
247
+ }
248
+ async prepareValidated(template) {
249
+ this.assertApplicable(template);
250
+ const restored = await this.restoredTemplate(template.manifest.digest, template.manifest.pluginVersion, journalAgentConfigSha256(template, this.registration.template));
251
+ this.assertCompatible(restored.range, restored.runtimeApiRange);
252
+ await this.registration.source.validate?.(restored.template, this.lifetime.signal);
253
+ this.assertApplicable(template);
254
+ }
138
255
  /** @param template - 即将整体替换的候选。 @throws 来源代次已经改变或已撤销时拒绝应用。 */
139
256
  assertApplicable(template) {
140
257
  const revision = this.preparedRevisions.get(template);
@@ -143,7 +260,7 @@ export class WorkspaceAgentConfigSourceOwner {
143
260
  }
144
261
  /** @param template - 已通过 Workspace 整体替换提交的模板。 */
145
262
  async committed(template) {
146
- await this.recover(template.manifest.digest, template.manifest.pluginVersion, template.agentConfigSha256 ?? null);
263
+ await this.recover(template.manifest.digest, template.manifest.pluginVersion, journalAgentConfigSha256(template, this.registration.template));
147
264
  }
148
265
  /**
149
266
  * 清除未提交候选,报告整体替换失败;已提交状态由 journal 恢复结果决定。
@@ -153,18 +270,20 @@ export class WorkspaceAgentConfigSourceOwner {
153
270
  failedApply(template, cause) {
154
271
  if (this.candidate?.template === template)
155
272
  this.candidate = undefined;
156
- const error = cause instanceof AgentConfigError && cause.code === 'AGENT_CONFIG_SCOPE_CHANGED'
157
- ? cause : new AgentConfigError('AGENT_CONFIG_APPLY_FAILED');
273
+ const error = cause instanceof AgentConfigError ? cause : new AgentConfigError('AGENT_CONFIG_APPLY_FAILED');
158
274
  this.failure = { code: error.code, message: error.message };
159
- this.report(error);
275
+ const revision = this.preparedRevisions.get(template) ?? this.registration.source.revision();
276
+ this.rememberRejection(error, template.manifest.pluginVersion, revision, template.agentConfigSha256);
277
+ this.reportFailure(error);
160
278
  }
161
279
  /** 撤销查询、清除后续定时器,并等待下载和目录写入完成。 */
162
280
  async dispose() {
163
281
  this.lifetime.abort();
282
+ this.rejection = undefined;
164
283
  this.candidate = undefined;
165
284
  if (this.timer !== undefined)
166
285
  clearTimeout(this.timer);
167
- await this.inFlight;
286
+ await Promise.all([this.startup, this.inFlight, this.observation?.dispose()]);
168
287
  this.candidate = undefined;
169
288
  }
170
289
  versionDirectory(version, sha256) {
@@ -173,14 +292,25 @@ export class WorkspaceAgentConfigSourceOwner {
173
292
  throw new AgentConfigError('AGENT_CONFIG_INVALID');
174
293
  return join(this.root, sha256);
175
294
  }
176
- async fetchCandidate(revision, requestSignal) {
295
+ rememberRejection(error, version, revision, sha256) {
296
+ if (this.lifetime.signal.aborted || revision !== this.registration.source.revision())
297
+ return;
298
+ if (error.code === 'AGENT_CONFIG_SCOPE_CHANGED') {
299
+ this.rejection = undefined;
300
+ return;
301
+ }
302
+ if (error.code === 'AGENT_CONFIG_QUERY_FAILED')
303
+ return;
304
+ this.rejection = { revision, value: { code: error.code, ...(version === undefined ? {} : { version }), ...(sha256 === undefined ? {} : { sha256 }) } };
305
+ }
306
+ async fetchCandidate(revision, requestSignal, attempt) {
177
307
  const signal = AbortSignal.any([this.lifetime.signal, requestSignal, AbortSignal.timeout(this.spec.agentConfigRequestTimeoutMs)]);
178
308
  let value;
179
309
  try {
180
310
  value = await this.registration.source.query(signal);
181
311
  }
182
312
  catch (error) {
183
- throw new AgentConfigError('AGENT_CONFIG_QUERY_FAILED');
313
+ throw error instanceof AgentConfigError ? error : new AgentConfigError('AGENT_CONFIG_QUERY_FAILED');
184
314
  }
185
315
  const assignment = AssignmentSchema.safeParse(value);
186
316
  if (!assignment.success || assignment.data.workspaceKind !== this.registration.template.bundleId)
@@ -188,14 +318,22 @@ export class WorkspaceAgentConfigSourceOwner {
188
318
  if (assignment.data.source === 'builtin') {
189
319
  this.candidate = undefined;
190
320
  this.failure = null;
321
+ this.rejection = undefined;
191
322
  return;
192
323
  }
193
324
  const descriptor = assignment.data.agentConfig;
325
+ attempt.version = descriptor.version;
326
+ attempt.sha256 = descriptor.sha256;
327
+ if (this.rejection?.value.version !== descriptor.version || this.rejection?.value.sha256 !== descriptor.sha256)
328
+ this.rejection = undefined;
194
329
  if (descriptor.workspaceKind !== assignment.data.workspaceKind)
195
330
  throw new AgentConfigError('AGENT_CONFIG_INVALID');
196
- if (descriptor.sha256 === this.selected.sha256) {
331
+ this.assertCompatible(descriptor.hostAgentConfigApiRange, descriptor.runtimeApiRange);
332
+ // Blocked journal content remains current even when the registered template has a newer digest.
333
+ if (descriptor.sha256 === (this.blockedVersion?.sha256 ?? this.selected.sha256)) {
197
334
  this.candidate = undefined;
198
335
  this.failure = null;
336
+ this.rejection = undefined;
199
337
  return;
200
338
  }
201
339
  if (descriptor.sizeBytes > this.spec.agentConfigMaxDownloadBytes)
@@ -260,9 +398,14 @@ export class WorkspaceAgentConfigSourceOwner {
260
398
  throw new AgentConfigError('AGENT_CONFIG_DIGEST_MISMATCH');
261
399
  if (signal.aborted || revision !== this.registration.source.revision())
262
400
  return;
263
- const template = { ...this.registration.template, sourceRoot: tree, manifest, agentConfigSha256: descriptor.sha256 };
401
+ const { bundleId, workspaceKey, title } = this.registration.template;
402
+ const template = { bundleId, workspaceKey, title, sourceRoot: tree, manifest, agentConfigSha256: descriptor.sha256 };
403
+ await this.registration.source.validate?.(template, signal);
404
+ if (signal.aborted || revision !== this.registration.source.revision())
405
+ return;
264
406
  this.preparedRevisions.set(template, revision);
265
407
  this.candidate = { template, sha256: descriptor.sha256, revision };
266
408
  this.failure = null;
409
+ this.rejection = undefined;
267
410
  }
268
411
  }
@@ -1,13 +1,26 @@
1
1
  /** Workspace Agent 配置版本来源和 turn 记录的 Host 接口。 */
2
2
  import type { WorkspaceBundleIdentity, WorkspaceTemplate } from './types.ts';
3
3
  import type { AgentConfigErrorCode } from './agent-config-format.ts';
4
+ import type { WorkspaceAgentConfigObservation } from './agent-config-observation.ts';
5
+ import type { WorkspaceId } from '@deepseek-ai/dsh-workspace/types';
4
6
  /** 调用方负责通过自身鉴权能力查询 server,不向此服务交出凭据。 */
5
7
  export interface WorkspaceAgentConfigSource {
6
8
  readonly identity: WorkspaceBundleIdentity;
9
+ /** Fixed consumer runtime API, when this module accepts descriptors with runtime requirements. */
10
+ readonly runtimeApiVersion?: string;
11
+ /** Optional authenticated module reporter; its lifetime is owned by this source registration. */
12
+ readonly observation?: (read: () => WorkspaceAgentConfigState) => WorkspaceAgentConfigObservation;
7
13
  /** @returns 随登录、租户或其他分配身份改变的进程内代次。 */
8
14
  revision(): number;
9
15
  /** @param signal - 源卸载或请求超时取消。 @returns #86 的 builtin / registry 分配。 */
10
16
  query(signal: AbortSignal): Promise<unknown>;
17
+ /**
18
+ * Consumer validation before candidate publication, adoption and restored execution; omission keeps generic archive checks.
19
+ * @param template - Verified template within this source's controlled tree.
20
+ * @param signal - Source disposal or request deadline.
21
+ * @throws AgentConfigError for invalid consumer metadata or bytes; no content may execute after failure.
22
+ */
23
+ validate?(template: WorkspaceTemplate, signal: AbortSignal): void | Promise<void>;
11
24
  }
12
25
  /** 下一个 turn 可使用的完整 Agent 配置版本,路径指向已经校验的不可变文件。 */
13
26
  export interface WorkspaceAgentConfigVersion {
@@ -18,14 +31,34 @@ export interface WorkspaceAgentConfigVersion {
18
31
  readonly templateDigest: string;
19
32
  readonly sourceRoot: string;
20
33
  }
34
+ /** Verified content held by one activity; each retained handle owns a separate release. */
35
+ export interface WorkspaceAgentConfigHandle extends WorkspaceAgentConfigVersion {
36
+ readonly workspaceId: WorkspaceId;
37
+ /** @returns another synchronous lease of these exact bytes, including after the parent activity ends. @throws WorkspaceBundleError when this handle was released or the Provider is unloading. */
38
+ retain(): WorkspaceAgentConfigHandle;
39
+ /** Release this activity once; retained consumers keep the content leased. */
40
+ release(): void;
41
+ }
21
42
  /** 内容更新的当前可观察状态;错误只包含固定诊断。 */
22
43
  export interface WorkspaceAgentConfigState {
44
+ /** True when committed content has failed execution validation; current is diagnostic only until compatible replacement. */
45
+ readonly blocked: boolean;
46
+ /** Execution refusal for current content; candidate and query failures cannot clear it. */
47
+ readonly blockingFailure: {
48
+ readonly code: AgentConfigErrorCode;
49
+ } | null;
23
50
  readonly current: WorkspaceAgentConfigVersion;
24
51
  readonly pending: WorkspaceAgentConfigVersion | null;
25
52
  readonly failure: {
26
53
  readonly code: AgentConfigErrorCode;
27
54
  readonly message: string;
28
55
  } | null;
56
+ /** Latest same-scope content refusal; query/scope failures are excluded. Null means no applicable refusal. */
57
+ readonly lastRejection: {
58
+ readonly code: AgentConfigErrorCode;
59
+ readonly version?: string;
60
+ readonly sha256?: string;
61
+ } | null;
29
62
  }
30
63
  /** 注册时固定的内置版本;源查询失败不能使它不可用。 */
31
64
  export interface WorkspaceAgentConfigRegistration {
@@ -0,0 +1,29 @@
1
+ import { z } from 'zod';
2
+ import type { WorkspaceTemplate } from './types.ts';
3
+ /** Public metadata of the exact published gzip installed as a bundled template. */
4
+ export declare const BundledAgentConfigReceiptSchema: z.ZodObject<{
5
+ workspaceKind: z.ZodString;
6
+ version: z.ZodString;
7
+ sha256: z.ZodString;
8
+ sizeBytes: z.ZodNumber;
9
+ hostAgentConfigApiRange: z.ZodString;
10
+ runtimeApiRange: z.ZodOptional<z.ZodString>;
11
+ formatVersion: z.ZodLiteral<1>;
12
+ templateDigest: z.ZodString;
13
+ }, z.core.$strict>;
14
+ /** Parsed identity and compatibility of a published bundled template. */
15
+ export type BundledAgentConfigReceipt = z.infer<typeof BundledAgentConfigReceiptSchema>;
16
+ /**
17
+ * Attach the adjacent publication receipt to a verified packaged template.
18
+ * @param template - Template whose file catalog has already been verified.
19
+ * @returns The original template when no receipt exists, otherwise its published identity and receipt.
20
+ * @throws AgentConfigError when an existing receipt is unreadable, invalid or identifies different content.
21
+ */
22
+ export declare function loadBundledAgentConfigSync(template: WorkspaceTemplate): WorkspaceTemplate;
23
+ /**
24
+ * Identify downloaded content in journals while registered content follows the installation.
25
+ * @param template - Selected template, including a possible downloaded copy.
26
+ * @param registered - This identity's exact registered object, or undefined during recovery without a registration.
27
+ * @returns Null for the registered object or a template without a package digest; otherwise the package digest.
28
+ */
29
+ export declare function journalAgentConfigSha256(template: WorkspaceTemplate, registered: WorkspaceTemplate | undefined): string | null;
@@ -0,0 +1,47 @@
1
+ /** Published package receipts beside shipped templates, separate from model-visible files. */
2
+ import { readFileSync } from 'node:fs';
3
+ import { basename, dirname, join } from 'node:path';
4
+ import { z } from 'zod';
5
+ import { AgentConfigDescriptorSchema, AgentConfigError, AgentConfigSha256 } from "./agent-config-format.js";
6
+ /** Public metadata of the exact published gzip installed as a bundled template. */
7
+ export const BundledAgentConfigReceiptSchema = AgentConfigDescriptorSchema.omit({ downloadUrl: true, releaseNotes: true }).extend({
8
+ formatVersion: z.literal(1),
9
+ templateDigest: AgentConfigSha256,
10
+ });
11
+ /**
12
+ * Attach the adjacent publication receipt to a verified packaged template.
13
+ * @param template - Template whose file catalog has already been verified.
14
+ * @returns The original template when no receipt exists, otherwise its published identity and receipt.
15
+ * @throws AgentConfigError when an existing receipt is unreadable, invalid or identifies different content.
16
+ */
17
+ export function loadBundledAgentConfigSync(template) {
18
+ let bytes;
19
+ try {
20
+ bytes = readFileSync(join(dirname(template.sourceRoot), `${basename(template.sourceRoot)}.agent-config.json`));
21
+ }
22
+ catch (error) {
23
+ if (error.code === 'ENOENT')
24
+ return template;
25
+ throw new AgentConfigError('AGENT_CONFIG_INVALID');
26
+ }
27
+ let receipt;
28
+ try {
29
+ receipt = BundledAgentConfigReceiptSchema.parse(JSON.parse(bytes.toString('utf8')));
30
+ }
31
+ catch (error) {
32
+ throw new AgentConfigError('AGENT_CONFIG_INVALID');
33
+ }
34
+ if (receipt.workspaceKind !== template.bundleId || receipt.version !== template.manifest.pluginVersion
35
+ || receipt.templateDigest !== template.manifest.digest)
36
+ throw new AgentConfigError('AGENT_CONFIG_INVALID');
37
+ return { ...template, agentConfigSha256: receipt.sha256, agentConfigReceipt: receipt };
38
+ }
39
+ /**
40
+ * Identify downloaded content in journals while registered content follows the installation.
41
+ * @param template - Selected template, including a possible downloaded copy.
42
+ * @param registered - This identity's exact registered object, or undefined during recovery without a registration.
43
+ * @returns Null for the registered object or a template without a package digest; otherwise the package digest.
44
+ */
45
+ export function journalAgentConfigSha256(template, registered) {
46
+ return template === registered ? null : template.agentConfigSha256 ?? null;
47
+ }
@@ -1,8 +1,10 @@
1
1
  /** `WorkspaceBundles` Service Definition: Host-only template registration and stable Workspace materialize. */
2
+ import { type LocalHostReplacement } from './local-host-replacement.ts';
3
+ import { type LocalHostIdentity } from './local-host-identity.ts';
2
4
  import { Context, Service } from '@deepseek-ai/cordis';
3
5
  import type { Workspace, WorkspaceId } from '@deepseek-ai/dsh-workspace/types';
4
6
  import type { WorkspaceBundleIdentity, WorkspaceBundleRecord, WorkspaceTemplate } from './types.ts';
5
- import type { WorkspaceAgentConfigSource, WorkspaceAgentConfigState } from './agent-config-types.ts';
7
+ import type { WorkspaceAgentConfigHandle, WorkspaceAgentConfigSource, WorkspaceAgentConfigState } from './agent-config-types.ts';
6
8
  declare module '@deepseek-ai/cordis' {
7
9
  interface Context {
8
10
  /** Bundled Workspace templates and their stable materialize. */
@@ -22,8 +24,12 @@ declare module '@deepseek-ai/cordis' {
22
24
  * replace its tree as a whole. Registration is an effect; the returned disposer forbids later acquire.
23
25
  */
24
26
  export declare abstract class WorkspaceBundles extends Service {
25
- /** @param ctx - context owning this Provider. */
26
- constructor(ctx: Context);
27
+ /** One member-level fence shared by every local module. */
28
+ readonly localHostReplacement: LocalHostReplacement;
29
+ /** One persisted local Host identity per server and member, published by the module that owns the installation identity. */
30
+ readonly localHostIdentity: LocalHostIdentity;
31
+ /** @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. */
32
+ constructor(ctx: Context, replacementPath?: string | null, identityPath?: string | null);
27
33
  /**
28
34
  * Make a template available for {@link ensure} and {@link acquire}.
29
35
  * @param template - verified catalog and source root.
@@ -36,7 +42,7 @@ export declare abstract class WorkspaceBundles extends Service {
36
42
  * @returns 撤销轮询并等待下载停止的 disposer。
37
43
  */
38
44
  abstract registerAgentConfigSource(source: WorkspaceAgentConfigSource): () => Promise<void>;
39
- /** @param identity - 内置 Workspace 身份。 @returns 已安装版本、候选和固定失败诊断;未注册来源时为 undefined。 */
45
+ /** @param identity - 内置 Workspace 身份。 @returns 已安装版本、候选、执行阻断、固定诊断与同作用域最近拒绝;blocked 时 current 只供诊断,未注册来源时为 undefined。 */
40
46
  abstract agentConfig(identity: WorkspaceBundleIdentity): WorkspaceAgentConfigState | undefined;
41
47
  /** @param identity - 内置 Workspace 身份。 @returns 该来源本次查询与下载完成,失败保留在 agentConfig 中。 */
42
48
  abstract refreshAgentConfig(identity: WorkspaceBundleIdentity): Promise<void>;
@@ -45,6 +51,7 @@ export declare abstract class WorkspaceBundles extends Service {
45
51
  * Unchanged trees reuse the existing Workspace id and path.
46
52
  * @param identity - branded pair.
47
53
  * @returns the Workspace after the tree is wholly old or wholly new.
54
+ * @throws AgentConfigError when committed remote content cannot execute; only a compatible validated candidate may repair it.
48
55
  */
49
56
  abstract ensure(identity: WorkspaceBundleIdentity): Promise<Workspace>;
50
57
  /**
@@ -53,6 +60,13 @@ export declare abstract class WorkspaceBundles extends Service {
53
60
  * @returns disposer of the lease.
54
61
  */
55
62
  abstract acquire(workspaceId: WorkspaceId): Promise<() => void>;
63
+ /**
64
+ * Adopt and verify content, then lease its exact identity before another adoption can run.
65
+ * @param identity - registered bundled Workspace.
66
+ * @returns a handle whose immutable source root and version match the committed Workspace; release after all activity and persistence finish.
67
+ * @throws WorkspaceBundleError or AgentConfigError when registration, content, scope or compatibility validation fails; no lease is returned.
68
+ */
69
+ abstract acquireAgentConfig(identity: WorkspaceBundleIdentity): Promise<WorkspaceAgentConfigHandle>;
56
70
  /**
57
71
  * Read the durable product record when one exists.
58
72
  * @param identity - branded pair.
@@ -1,12 +1,20 @@
1
1
  /** `WorkspaceBundles` Service Definition: Host-only template registration and stable Workspace materialize. */
2
+ import { LocalHostReplacementState } from "./local-host-replacement.js";
3
+ import { LocalHostIdentityState } from "./local-host-identity.js";
2
4
  import { Service } from '@deepseek-ai/cordis';
3
5
  /**
4
6
  * Host-only capability: register a packaged template, materialize a stable hidden Workspace, and
5
7
  * replace its tree as a whole. Registration is an effect; the returned disposer forbids later acquire.
6
8
  */
7
9
  export class WorkspaceBundles extends Service {
8
- /** @param ctx - context owning this Provider. */
9
- constructor(ctx) {
10
+ /** One member-level fence shared by every local module. */
11
+ localHostReplacement;
12
+ /** One persisted local Host identity per server and member, published by the module that owns the installation identity. */
13
+ localHostIdentity;
14
+ /** @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. */
15
+ constructor(ctx, replacementPath = null, identityPath = null) {
10
16
  super(ctx, 'workspaceBundles');
17
+ this.localHostReplacement = new LocalHostReplacementState(replacementPath);
18
+ this.localHostIdentity = new LocalHostIdentityState(identityPath);
11
19
  }
12
20
  }
@@ -15,6 +15,14 @@ export type * from './types.ts';
15
15
  export type * from './agent-config-types.ts';
16
16
  export type { WorkspaceAgentConfigVersionEvent } from './session-events.ts';
17
17
  export { recordWorkspaceAgentConfigVersion } from './session-events.ts';
18
- export { HOST_AGENT_CONFIG_API_VERSION, AgentConfigError, agentConfigSha256, decodeAgentConfig, AgentConfigDescriptorSchema } from './agent-config-format.ts';
18
+ export { HOST_AGENT_CONFIG_API_VERSION, AgentConfigError, agentConfigSha256, decodeAgentConfig, AgentConfigDescriptorSchema, isAgentConfigVersion } from './agent-config-format.ts';
19
19
  export type { AgentConfigDescriptor, AgentConfigErrorCode } from './agent-config-format.ts';
20
+ export { BundledAgentConfigReceiptSchema, loadBundledAgentConfigSync } from './bundled-agent-config.ts';
21
+ export type { BundledAgentConfigReceipt } from './bundled-agent-config.ts';
20
22
  export default GestaltWorkspaceBundles;
23
+ export { getModuleAgentConfigAssignment } from './agent-config-contract/agent-configs.ts';
24
+ export { agentConfigHostObservation, sandboxAgentConfigHostObservation } from './agent-config-observation.ts';
25
+ export type { WorkspaceAgentConfigObservation, WorkspaceAgentConfigObservationOptions, WorkspaceAgentConfigHostId, WorkspaceAgentConfigSandbox } from './agent-config-observation.ts';
26
+ export type { LocalHostReplacement, LocalHostMember } from './local-host-replacement.ts';
27
+ export { LocalHostIdentityLoadError, LocalHostIdentityWriteError, sharedLocalHostId } from './local-host-identity.ts';
28
+ export type { LocalHostIdentity, LocalHostIdentityScope, LocalHostIdentitySources, LocalHostIdentityValue } from './local-host-identity.ts';
@@ -11,5 +11,9 @@ export { encodeBundleSegment, bundledDirectory, assertTemplateRelativePath, iden
11
11
  export { createNodeFs, crashableFs } from "./filesystem.js";
12
12
  export { GestaltWorkspaceBundles } from "./provider.js";
13
13
  export { recordWorkspaceAgentConfigVersion } from "./session-events.js";
14
- export { HOST_AGENT_CONFIG_API_VERSION, AgentConfigError, agentConfigSha256, decodeAgentConfig, AgentConfigDescriptorSchema } from "./agent-config-format.js";
14
+ export { HOST_AGENT_CONFIG_API_VERSION, AgentConfigError, agentConfigSha256, decodeAgentConfig, AgentConfigDescriptorSchema, isAgentConfigVersion } from "./agent-config-format.js";
15
+ export { BundledAgentConfigReceiptSchema, loadBundledAgentConfigSync } from "./bundled-agent-config.js";
15
16
  export default GestaltWorkspaceBundles;
17
+ export { getModuleAgentConfigAssignment } from "./agent-config-contract/agent-configs.js";
18
+ export { agentConfigHostObservation, sandboxAgentConfigHostObservation } from "./agent-config-observation.js";
19
+ export { LocalHostIdentityLoadError, LocalHostIdentityWriteError, sharedLocalHostId } from "./local-host-identity.js";