@gestaltrun/dsh-workspace-bundle 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.i18n.yaml +37 -0
- package/README.md +92 -2
- package/README.zh.md +93 -0
- package/lib/index.js +2868 -0
- 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 +86 -0
- package/lib/types/agent-config-format.js +119 -0
- 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 +78 -0
- package/lib/types/agent-config-source.js +411 -0
- package/lib/types/agent-config-types.d.ts +67 -0
- package/lib/types/agent-config-types.js +1 -0
- package/lib/types/bundled-agent-config.d.ts +29 -0
- package/lib/types/bundled-agent-config.js +47 -0
- package/lib/types/config.d.ts +34 -0
- package/lib/types/config.js +53 -0
- package/lib/types/definition.d.ts +82 -0
- package/lib/types/definition.js +20 -0
- package/lib/types/digest.d.ts +38 -0
- package/lib/types/digest.js +243 -0
- package/lib/types/errors.d.ts +22 -0
- package/lib/types/errors.js +26 -0
- package/lib/types/filesystem.d.ts +16 -0
- package/lib/types/filesystem.js +64 -0
- package/lib/types/index.d.ts +28 -0
- package/lib/types/index.js +19 -0
- package/lib/types/journal.d.ts +51 -0
- package/lib/types/journal.js +109 -0
- 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/paths.d.ts +26 -0
- package/lib/types/paths.js +48 -0
- package/lib/types/provider.d.ts +83 -0
- package/lib/types/provider.js +560 -0
- package/lib/types/session-events.d.ts +23 -0
- package/lib/types/session-events.js +14 -0
- package/lib/types/types.d.ts +126 -0
- package/lib/types/types.js +1 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +69 -3
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
/** 内容来源的有界拉取和只读版本缓存;此模块不替换运行中的 Workspace。 */
|
|
2
|
+
import { chmod, mkdir, mkdtemp, readFile, readdir, rename, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { brandString } from '@deepseek-ai/dsh-brand';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import semver from 'semver';
|
|
7
|
+
import { WorkspaceBundleError } from "./errors.js";
|
|
8
|
+
import { AgentConfigDescriptorSchema, AgentConfigError, HOST_AGENT_CONFIG_API_VERSION, decodeAgentConfig } from "./agent-config-format.js";
|
|
9
|
+
import { loadVerifiedManifest, overallDigest } from "./digest.js";
|
|
10
|
+
import { createNodeFs } from "./filesystem.js";
|
|
11
|
+
import { bundledDirectory } from "./paths.js";
|
|
12
|
+
import { journalAgentConfigSha256 } from "./bundled-agent-config.js";
|
|
13
|
+
const AssignmentSchema = z.discriminatedUnion('source', [
|
|
14
|
+
z.object({ source: z.literal('builtin'), workspaceKind: z.string() }),
|
|
15
|
+
z.object({ source: z.literal('registry'), workspaceKind: z.string(), agentConfig: AgentConfigDescriptorSchema }),
|
|
16
|
+
]);
|
|
17
|
+
const ReceiptSchema = AgentConfigDescriptorSchema.omit({ downloadUrl: true, releaseNotes: true });
|
|
18
|
+
function publicVersion(template, sha256) {
|
|
19
|
+
return { workspaceKind: template.bundleId, version: template.manifest.pluginVersion, sha256,
|
|
20
|
+
hostAgentConfigApiVersion: HOST_AGENT_CONFIG_API_VERSION, templateDigest: template.manifest.digest, sourceRoot: template.sourceRoot };
|
|
21
|
+
}
|
|
22
|
+
async function freezeTree(root) {
|
|
23
|
+
for (const entry of await readdir(root, { withFileTypes: true })) {
|
|
24
|
+
const path = join(root, entry.name);
|
|
25
|
+
if (entry.isDirectory())
|
|
26
|
+
await freezeTree(path);
|
|
27
|
+
else
|
|
28
|
+
await chmod(path, 0o444);
|
|
29
|
+
}
|
|
30
|
+
await chmod(root, 0o555);
|
|
31
|
+
}
|
|
32
|
+
/** 一个注册来源的缓存、轮询和候选版本;卸载必须等待所有网络与文件工作结束。 */
|
|
33
|
+
export class WorkspaceAgentConfigSourceOwner {
|
|
34
|
+
registration;
|
|
35
|
+
spec;
|
|
36
|
+
report;
|
|
37
|
+
download;
|
|
38
|
+
recoverIdle;
|
|
39
|
+
lifetime = new AbortController();
|
|
40
|
+
inFlight;
|
|
41
|
+
requestAbort;
|
|
42
|
+
requestRevision;
|
|
43
|
+
preparedRevisions = new WeakMap();
|
|
44
|
+
timer;
|
|
45
|
+
selected;
|
|
46
|
+
candidate;
|
|
47
|
+
failure = null;
|
|
48
|
+
rejection;
|
|
49
|
+
admissionFailure;
|
|
50
|
+
root;
|
|
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 () => { }) {
|
|
56
|
+
this.registration = registration;
|
|
57
|
+
this.spec = spec;
|
|
58
|
+
this.report = report;
|
|
59
|
+
this.download = download;
|
|
60
|
+
this.recoverIdle = recoverIdle;
|
|
61
|
+
this.selected = { template: registration.template, sha256: registration.template.agentConfigSha256 ?? null };
|
|
62
|
+
this.root = bundledDirectory(join(spec.templateCacheRoot, 'agent-config-versions'), registration.source.identity);
|
|
63
|
+
}
|
|
64
|
+
/** @returns 已应用/候选版本、执行阻断及当前作用域的最近失败和拒绝。 */
|
|
65
|
+
state() {
|
|
66
|
+
if (this.candidate !== undefined && this.candidate.revision !== this.registration.source.revision())
|
|
67
|
+
this.candidate = undefined;
|
|
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 } };
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* 从 journal 已提交的摘要定位只读缓存,不从 server 的无分配回答推导降级。
|
|
77
|
+
* @param digest - 已提交 Workspace 模板摘要;undefined 表示首次使用。
|
|
78
|
+
* @param version - 已提交完整版本,包括构建元数据。
|
|
79
|
+
* @param sha256 - 已提交包摘要;undefined 支持旧内置记录。
|
|
80
|
+
* @returns 对应完整模板;缺失已安装缓存时拒绝继续。
|
|
81
|
+
*/
|
|
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) {
|
|
126
|
+
if (digest === undefined || sha256 === undefined || sha256 === null) {
|
|
127
|
+
const template = this.registration.template;
|
|
128
|
+
return { template, range: template.agentConfigReceipt?.hostAgentConfigApiRange,
|
|
129
|
+
...(template.agentConfigReceipt?.runtimeApiRange === undefined ? {} : { runtimeApiRange: template.agentConfigReceipt.runtimeApiRange }) };
|
|
130
|
+
}
|
|
131
|
+
if (!/^[0-9a-f]{64}$/u.test(sha256))
|
|
132
|
+
throw new AgentConfigError('AGENT_CONFIG_INVALID');
|
|
133
|
+
const fs = createNodeFs();
|
|
134
|
+
const receipt = ReceiptSchema.parse(JSON.parse(await readFile(join(this.root, sha256, 'receipt.json'), 'utf8')));
|
|
135
|
+
const directory = this.versionDirectory(receipt.version, receipt.sha256);
|
|
136
|
+
if (receipt.sha256 !== sha256 || receipt.workspaceKind !== this.registration.template.bundleId
|
|
137
|
+
|| (await fs.lstat(directory)).isSymbolicLink() || (await fs.lstat(join(directory, receipt.version))).isSymbolicLink())
|
|
138
|
+
throw new AgentConfigError('AGENT_CONFIG_INVALID');
|
|
139
|
+
const manifest = await loadVerifiedManifest(fs, join(directory, receipt.version));
|
|
140
|
+
if (manifest.digest !== digest || version !== undefined && receipt.version !== version
|
|
141
|
+
|| manifest.pluginVersion !== receipt.version)
|
|
142
|
+
throw new AgentConfigError('AGENT_CONFIG_INVALID');
|
|
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
|
+
}
|
|
167
|
+
}
|
|
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
|
+
});
|
|
174
|
+
const tick = async () => {
|
|
175
|
+
try {
|
|
176
|
+
await this.refresh();
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
this.reportFailure(this.configError(error));
|
|
180
|
+
}
|
|
181
|
+
if (!this.lifetime.signal.aborted)
|
|
182
|
+
this.timer = setTimeout(() => { void tick(); }, this.spec.agentConfigPollIntervalMs);
|
|
183
|
+
};
|
|
184
|
+
void tick();
|
|
185
|
+
}
|
|
186
|
+
/** @returns 本次查询和完整候选下载结束;失败通过 state 与 report 保留可观察诊断。 */
|
|
187
|
+
refresh() {
|
|
188
|
+
return this.startup.then(() => this.refreshReady());
|
|
189
|
+
}
|
|
190
|
+
refreshReady() {
|
|
191
|
+
if (this.lifetime.signal.aborted)
|
|
192
|
+
return Promise.resolve();
|
|
193
|
+
const revision = this.registration.source.revision();
|
|
194
|
+
if (this.candidate !== undefined && this.candidate.revision !== revision)
|
|
195
|
+
this.candidate = undefined;
|
|
196
|
+
if (this.inFlight !== undefined) {
|
|
197
|
+
if (revision === this.requestRevision && !this.requestAbort?.signal.aborted)
|
|
198
|
+
return this.inFlight;
|
|
199
|
+
this.requestAbort?.abort();
|
|
200
|
+
this.failure = null;
|
|
201
|
+
return this.inFlight.then(() => this.refresh());
|
|
202
|
+
}
|
|
203
|
+
const abort = new AbortController();
|
|
204
|
+
this.requestAbort = abort;
|
|
205
|
+
this.requestRevision = revision;
|
|
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) => {
|
|
215
|
+
if (this.lifetime.signal.aborted || abort.signal.aborted || revision !== this.registration.source.revision())
|
|
216
|
+
return;
|
|
217
|
+
this.candidate = undefined;
|
|
218
|
+
const failure = this.configError(error);
|
|
219
|
+
this.failure = { code: failure.code, message: failure.message };
|
|
220
|
+
this.rememberRejection(failure, attempt.version, revision, attempt.sha256);
|
|
221
|
+
this.reportFailure(failure);
|
|
222
|
+
}).finally(() => { this.inFlight = undefined; this.requestAbort = undefined; this.requestRevision = undefined; });
|
|
223
|
+
this.inFlight = run;
|
|
224
|
+
return run;
|
|
225
|
+
}
|
|
226
|
+
/** @returns 下一个 turn 可应用的候选;没有候选时使用已安装版本。 */
|
|
227
|
+
template() {
|
|
228
|
+
if (this.candidate !== undefined && this.candidate.revision !== this.registration.source.revision())
|
|
229
|
+
this.candidate = undefined;
|
|
230
|
+
if (this.candidate === undefined && this.admissionFailure !== undefined)
|
|
231
|
+
throw this.admissionFailure;
|
|
232
|
+
return this.candidate?.template ?? this.selected.template;
|
|
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
|
+
}
|
|
255
|
+
/** @param template - 即将整体替换的候选。 @throws 来源代次已经改变或已撤销时拒绝应用。 */
|
|
256
|
+
assertApplicable(template) {
|
|
257
|
+
const revision = this.preparedRevisions.get(template);
|
|
258
|
+
if (this.lifetime.signal.aborted || revision !== undefined && revision !== this.registration.source.revision())
|
|
259
|
+
throw new AgentConfigError('AGENT_CONFIG_SCOPE_CHANGED');
|
|
260
|
+
}
|
|
261
|
+
/** @param template - 已通过 Workspace 整体替换提交的模板。 */
|
|
262
|
+
async committed(template) {
|
|
263
|
+
await this.recover(template.manifest.digest, template.manifest.pluginVersion, journalAgentConfigSha256(template, this.registration.template));
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* 清除未提交候选,报告整体替换失败;已提交状态由 journal 恢复结果决定。
|
|
267
|
+
* @param template - 未能完成提交的模板。
|
|
268
|
+
* @param cause - 保留来源失效分类;其他原因报告固定应用失败诊断。
|
|
269
|
+
*/
|
|
270
|
+
failedApply(template, cause) {
|
|
271
|
+
if (this.candidate?.template === template)
|
|
272
|
+
this.candidate = undefined;
|
|
273
|
+
const error = cause instanceof AgentConfigError ? cause : new AgentConfigError('AGENT_CONFIG_APPLY_FAILED');
|
|
274
|
+
this.failure = { code: error.code, message: error.message };
|
|
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);
|
|
278
|
+
}
|
|
279
|
+
/** 撤销查询、清除后续定时器,并等待下载和目录写入完成。 */
|
|
280
|
+
async dispose() {
|
|
281
|
+
this.lifetime.abort();
|
|
282
|
+
this.rejection = undefined;
|
|
283
|
+
this.candidate = undefined;
|
|
284
|
+
if (this.timer !== undefined)
|
|
285
|
+
clearTimeout(this.timer);
|
|
286
|
+
await Promise.all([this.startup, this.inFlight, this.observation?.dispose()]);
|
|
287
|
+
this.candidate = undefined;
|
|
288
|
+
}
|
|
289
|
+
versionDirectory(version, sha256) {
|
|
290
|
+
if (!AgentConfigDescriptorSchema.shape.version.safeParse(version).success
|
|
291
|
+
|| !/^[0-9a-f]{64}$/u.test(sha256))
|
|
292
|
+
throw new AgentConfigError('AGENT_CONFIG_INVALID');
|
|
293
|
+
return join(this.root, sha256);
|
|
294
|
+
}
|
|
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) {
|
|
307
|
+
const signal = AbortSignal.any([this.lifetime.signal, requestSignal, AbortSignal.timeout(this.spec.agentConfigRequestTimeoutMs)]);
|
|
308
|
+
let value;
|
|
309
|
+
try {
|
|
310
|
+
value = await this.registration.source.query(signal);
|
|
311
|
+
}
|
|
312
|
+
catch (error) {
|
|
313
|
+
throw error instanceof AgentConfigError ? error : new AgentConfigError('AGENT_CONFIG_QUERY_FAILED');
|
|
314
|
+
}
|
|
315
|
+
const assignment = AssignmentSchema.safeParse(value);
|
|
316
|
+
if (!assignment.success || assignment.data.workspaceKind !== this.registration.template.bundleId)
|
|
317
|
+
throw new AgentConfigError('AGENT_CONFIG_INVALID');
|
|
318
|
+
if (assignment.data.source === 'builtin') {
|
|
319
|
+
this.candidate = undefined;
|
|
320
|
+
this.failure = null;
|
|
321
|
+
this.rejection = undefined;
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
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;
|
|
329
|
+
if (descriptor.workspaceKind !== assignment.data.workspaceKind)
|
|
330
|
+
throw new AgentConfigError('AGENT_CONFIG_INVALID');
|
|
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)) {
|
|
334
|
+
this.candidate = undefined;
|
|
335
|
+
this.failure = null;
|
|
336
|
+
this.rejection = undefined;
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
if (descriptor.sizeBytes > this.spec.agentConfigMaxDownloadBytes)
|
|
340
|
+
throw new AgentConfigError('AGENT_CONFIG_INVALID');
|
|
341
|
+
const url = new URL(descriptor.downloadUrl);
|
|
342
|
+
if (url.username !== '' || url.password !== '' || url.protocol !== 'https:'
|
|
343
|
+
&& !(this.spec.allowLoopbackAgentConfigHttp && url.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname)))
|
|
344
|
+
throw new AgentConfigError('AGENT_CONFIG_INVALID');
|
|
345
|
+
let bytes;
|
|
346
|
+
try {
|
|
347
|
+
const response = await this.download(url, { signal, redirect: 'error' });
|
|
348
|
+
if (!response.ok || response.body === null || response.headers.get('content-length') !== null
|
|
349
|
+
&& Number(response.headers.get('content-length')) !== descriptor.sizeBytes)
|
|
350
|
+
throw new AgentConfigError('AGENT_CONFIG_DOWNLOAD_FAILED');
|
|
351
|
+
const chunks = [];
|
|
352
|
+
let count = 0;
|
|
353
|
+
for await (const chunk of response.body) {
|
|
354
|
+
count += chunk.byteLength;
|
|
355
|
+
if (count > descriptor.sizeBytes) {
|
|
356
|
+
await response.body.cancel().catch(() => undefined);
|
|
357
|
+
throw new AgentConfigError('AGENT_CONFIG_DOWNLOAD_FAILED');
|
|
358
|
+
}
|
|
359
|
+
chunks.push(chunk);
|
|
360
|
+
}
|
|
361
|
+
bytes = Buffer.concat(chunks);
|
|
362
|
+
}
|
|
363
|
+
catch (error) {
|
|
364
|
+
throw new AgentConfigError('AGENT_CONFIG_DOWNLOAD_FAILED');
|
|
365
|
+
}
|
|
366
|
+
const content = decodeAgentConfig(bytes, descriptor, this.spec.agentConfigMaxExpandedBytes);
|
|
367
|
+
if (signal.aborted || revision !== this.registration.source.revision())
|
|
368
|
+
return;
|
|
369
|
+
await mkdir(this.root, { recursive: true });
|
|
370
|
+
const directory = this.versionDirectory(descriptor.version, descriptor.sha256);
|
|
371
|
+
const tree = join(directory, descriptor.version);
|
|
372
|
+
const fs = createNodeFs();
|
|
373
|
+
if (!await fs.exists(directory)) {
|
|
374
|
+
const staging = await mkdtemp(join(this.root, '.staging-'));
|
|
375
|
+
try {
|
|
376
|
+
const stagingTree = join(staging, descriptor.version);
|
|
377
|
+
await mkdir(stagingTree);
|
|
378
|
+
for (const file of content.files) {
|
|
379
|
+
const path = join(stagingTree, ...file.path.split('/'));
|
|
380
|
+
await mkdir(dirname(path), { recursive: true });
|
|
381
|
+
await writeFile(path, file.bytes, { flag: 'wx' });
|
|
382
|
+
}
|
|
383
|
+
const files = content.manifest.files.map(file => ({ path: file.path, bytes: file.sizeBytes, digest: brandString(file.sha256) }));
|
|
384
|
+
await writeFile(join(stagingTree, 'manifest.json'), JSON.stringify({ formatVersion: 1, pluginVersion: descriptor.version, files, digest: overallDigest(files) }) + '\n');
|
|
385
|
+
const { downloadUrl: _downloadUrl, releaseNotes: _releaseNotes, ...receipt } = descriptor;
|
|
386
|
+
await writeFile(join(staging, 'receipt.json'), JSON.stringify(ReceiptSchema.parse(receipt)) + '\n');
|
|
387
|
+
await rename(staging, directory);
|
|
388
|
+
await freezeTree(tree);
|
|
389
|
+
await chmod(join(directory, 'receipt.json'), 0o444);
|
|
390
|
+
}
|
|
391
|
+
finally {
|
|
392
|
+
await rm(staging, { recursive: true, force: true });
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
const manifest = await loadVerifiedManifest(fs, tree);
|
|
396
|
+
const files = content.manifest.files.map(file => ({ path: file.path, bytes: file.sizeBytes, digest: brandString(file.sha256) }));
|
|
397
|
+
if (manifest.pluginVersion !== descriptor.version || manifest.digest !== overallDigest(files))
|
|
398
|
+
throw new AgentConfigError('AGENT_CONFIG_DIGEST_MISMATCH');
|
|
399
|
+
if (signal.aborted || revision !== this.registration.source.revision())
|
|
400
|
+
return;
|
|
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;
|
|
406
|
+
this.preparedRevisions.set(template, revision);
|
|
407
|
+
this.candidate = { template, sha256: descriptor.sha256, revision };
|
|
408
|
+
this.failure = null;
|
|
409
|
+
this.rejection = undefined;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/** Workspace Agent 配置版本来源和 turn 记录的 Host 接口。 */
|
|
2
|
+
import type { WorkspaceBundleIdentity, WorkspaceTemplate } from './types.ts';
|
|
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';
|
|
6
|
+
/** 调用方负责通过自身鉴权能力查询 server,不向此服务交出凭据。 */
|
|
7
|
+
export interface WorkspaceAgentConfigSource {
|
|
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;
|
|
13
|
+
/** @returns 随登录、租户或其他分配身份改变的进程内代次。 */
|
|
14
|
+
revision(): number;
|
|
15
|
+
/** @param signal - 源卸载或请求超时取消。 @returns #86 的 builtin / registry 分配。 */
|
|
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>;
|
|
24
|
+
}
|
|
25
|
+
/** 下一个 turn 可使用的完整 Agent 配置版本,路径指向已经校验的不可变文件。 */
|
|
26
|
+
export interface WorkspaceAgentConfigVersion {
|
|
27
|
+
readonly workspaceKind: string;
|
|
28
|
+
readonly version: string;
|
|
29
|
+
readonly sha256: string | null;
|
|
30
|
+
readonly hostAgentConfigApiVersion: string;
|
|
31
|
+
readonly templateDigest: string;
|
|
32
|
+
readonly sourceRoot: string;
|
|
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
|
+
}
|
|
42
|
+
/** 内容更新的当前可观察状态;错误只包含固定诊断。 */
|
|
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;
|
|
50
|
+
readonly current: WorkspaceAgentConfigVersion;
|
|
51
|
+
readonly pending: WorkspaceAgentConfigVersion | null;
|
|
52
|
+
readonly failure: {
|
|
53
|
+
readonly code: AgentConfigErrorCode;
|
|
54
|
+
readonly message: string;
|
|
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;
|
|
62
|
+
}
|
|
63
|
+
/** 注册时固定的内置版本;源查询失败不能使它不可用。 */
|
|
64
|
+
export interface WorkspaceAgentConfigRegistration {
|
|
65
|
+
readonly template: WorkspaceTemplate;
|
|
66
|
+
readonly source: WorkspaceAgentConfigSource;
|
|
67
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import Schema from '@deepseek-ai/schemastery';
|
|
2
|
+
import type { WorkspaceBundlesSpec } from './types.ts';
|
|
3
|
+
/** Raw cordis.yml settings of the workspace-bundle row. */
|
|
4
|
+
export interface Config {
|
|
5
|
+
/** Parent of `{bundleId}/{workspaceKey}` Workspace roots; required absolute path. */
|
|
6
|
+
readonly materializeRoot?: string;
|
|
7
|
+
/** Parent of immutable template copies; required absolute path. */
|
|
8
|
+
readonly templateCacheRoot?: string;
|
|
9
|
+
/** Parent of per-Workspace journals; required absolute path. */
|
|
10
|
+
readonly journalRoot?: string;
|
|
11
|
+
/** Upgrade lease drain deadline, 1000–600000 ms. */
|
|
12
|
+
readonly quiesceTimeoutMs?: number;
|
|
13
|
+
/** Backup recycle delay after a successful commit, 0–600000 ms. */
|
|
14
|
+
readonly recycleTimeoutMs?: number;
|
|
15
|
+
readonly agentConfigPollIntervalMs?: number;
|
|
16
|
+
readonly agentConfigRequestTimeoutMs?: number;
|
|
17
|
+
readonly agentConfigMaxDownloadBytes?: number;
|
|
18
|
+
readonly agentConfigMaxExpandedBytes?: number;
|
|
19
|
+
readonly allowLoopbackAgentConfigHttp?: boolean;
|
|
20
|
+
}
|
|
21
|
+
/** Settings after {@link Config} applies its defaults; path fields may still be absent. */
|
|
22
|
+
export type ResolvedConfig = Required<Omit<Config, 'materializeRoot' | 'templateCacheRoot' | 'journalRoot'>> & Pick<Config, 'materializeRoot' | 'templateCacheRoot' | 'journalRoot'>;
|
|
23
|
+
/**
|
|
24
|
+
* Field types and bounds. Path fields are optional here so a missing value reaches
|
|
25
|
+
* {@link resolveWorkspaceBundlesSpec} and fails with its named diagnostic.
|
|
26
|
+
*/
|
|
27
|
+
export declare const Config: Schema<Config, ResolvedConfig>;
|
|
28
|
+
/**
|
|
29
|
+
* Resolve raw settings into the Spec the Provider runs with.
|
|
30
|
+
* @param config - raw row settings.
|
|
31
|
+
* @returns every field set, with roots normalized.
|
|
32
|
+
* @throws {WorkspaceBundlesConfigError} When a required path is missing or not absolute.
|
|
33
|
+
*/
|
|
34
|
+
export declare function resolveWorkspaceBundlesSpec(config?: Config): WorkspaceBundlesSpec;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/** workspace-bundle Config: Schemastery field validation and explicit resolution into a Spec. */
|
|
2
|
+
import { isAbsolute, normalize, sep } from 'node:path';
|
|
3
|
+
import Schema from '@deepseek-ai/schemastery';
|
|
4
|
+
import { WorkspaceBundlesConfigError } from "./errors.js";
|
|
5
|
+
/**
|
|
6
|
+
* Field types and bounds. Path fields are optional here so a missing value reaches
|
|
7
|
+
* {@link resolveWorkspaceBundlesSpec} and fails with its named diagnostic.
|
|
8
|
+
*/
|
|
9
|
+
export const Config = Schema.object({
|
|
10
|
+
materializeRoot: Schema.string().description('Absolute parent of bundled Workspace roots'),
|
|
11
|
+
templateCacheRoot: Schema.string().description('Absolute parent of immutable template copies'),
|
|
12
|
+
journalRoot: Schema.string().description('Absolute parent of upgrade journals'),
|
|
13
|
+
quiesceTimeoutMs: Schema.number().min(1_000).max(600_000).step(1).default(30_000),
|
|
14
|
+
recycleTimeoutMs: Schema.number().min(0).max(600_000).step(1).default(60_000),
|
|
15
|
+
agentConfigPollIntervalMs: Schema.number().min(1_000).max(86_400_000).step(1).default(300_000),
|
|
16
|
+
agentConfigRequestTimeoutMs: Schema.number().min(100).max(600_000).step(1).default(30_000),
|
|
17
|
+
agentConfigMaxDownloadBytes: Schema.number().min(1_024).max(268_435_456).step(1).default(67_108_864),
|
|
18
|
+
agentConfigMaxExpandedBytes: Schema.number().min(1_024).max(536_870_912).step(1).default(134_217_728),
|
|
19
|
+
allowLoopbackAgentConfigHttp: Schema.boolean().default(false),
|
|
20
|
+
});
|
|
21
|
+
function absoluteDirectory(value, field) {
|
|
22
|
+
if (value === undefined || value.trim() === '') {
|
|
23
|
+
const code = field === 'materializeRoot' ? 'WORKSPACE_BUNDLES_MATERIALIZE_ROOT_MISSING'
|
|
24
|
+
: field === 'templateCacheRoot' ? 'WORKSPACE_BUNDLES_TEMPLATE_CACHE_ROOT_MISSING'
|
|
25
|
+
: 'WORKSPACE_BUNDLES_JOURNAL_ROOT_MISSING';
|
|
26
|
+
throw new WorkspaceBundlesConfigError(code, `${field} is required`);
|
|
27
|
+
}
|
|
28
|
+
if (!isAbsolute(value) || value.includes('\0') || normalize(value).includes(`..${sep}`) || normalize(value).endsWith(`${sep}..`)) {
|
|
29
|
+
throw new WorkspaceBundlesConfigError('WORKSPACE_BUNDLES_PATH_INVALID', `${field} ${JSON.stringify(value)} must be an absolute path without parent segments`);
|
|
30
|
+
}
|
|
31
|
+
return normalize(value);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Resolve raw settings into the Spec the Provider runs with.
|
|
35
|
+
* @param config - raw row settings.
|
|
36
|
+
* @returns every field set, with roots normalized.
|
|
37
|
+
* @throws {WorkspaceBundlesConfigError} When a required path is missing or not absolute.
|
|
38
|
+
*/
|
|
39
|
+
export function resolveWorkspaceBundlesSpec(config = {}) {
|
|
40
|
+
const resolved = Config(config);
|
|
41
|
+
return {
|
|
42
|
+
materializeRoot: absoluteDirectory(resolved.materializeRoot, 'materializeRoot'),
|
|
43
|
+
templateCacheRoot: absoluteDirectory(resolved.templateCacheRoot, 'templateCacheRoot'),
|
|
44
|
+
journalRoot: absoluteDirectory(resolved.journalRoot, 'journalRoot'),
|
|
45
|
+
quiesceTimeoutMs: resolved.quiesceTimeoutMs,
|
|
46
|
+
recycleTimeoutMs: resolved.recycleTimeoutMs,
|
|
47
|
+
agentConfigPollIntervalMs: resolved.agentConfigPollIntervalMs,
|
|
48
|
+
agentConfigRequestTimeoutMs: resolved.agentConfigRequestTimeoutMs,
|
|
49
|
+
agentConfigMaxDownloadBytes: resolved.agentConfigMaxDownloadBytes,
|
|
50
|
+
agentConfigMaxExpandedBytes: resolved.agentConfigMaxExpandedBytes,
|
|
51
|
+
allowLoopbackAgentConfigHttp: resolved.allowLoopbackAgentConfigHttp,
|
|
52
|
+
};
|
|
53
|
+
}
|