@gestaltrun/dsh-workspace-bundle 0.0.0-stage → 0.1.0
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 +72 -2
- package/README.zh.md +73 -0
- package/lib/index.js +1368 -0
- package/lib/types/agent-config-format.d.ts +85 -0
- package/lib/types/agent-config-format.js +116 -0
- package/lib/types/agent-config-source.d.ts +52 -0
- package/lib/types/agent-config-source.js +268 -0
- package/lib/types/agent-config-types.d.ts +34 -0
- package/lib/types/agent-config-types.js +1 -0
- package/lib/types/config.d.ts +34 -0
- package/lib/types/config.js +53 -0
- package/lib/types/definition.d.ts +68 -0
- package/lib/types/definition.js +12 -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 +20 -0
- package/lib/types/index.js +15 -0
- package/lib/types/journal.d.ts +51 -0
- package/lib/types/journal.js +109 -0
- package/lib/types/paths.d.ts +26 -0
- package/lib/types/paths.js +48 -0
- package/lib/types/provider.d.ts +77 -0
- package/lib/types/provider.js +462 -0
- package/lib/types/session-events.d.ts +23 -0
- package/lib/types/session-events.js +14 -0
- package/lib/types/types.d.ts +123 -0
- package/lib/types/types.js +1 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +65 -3
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/** 本 Host 实现的 Agent 配置接口版本;白名单与写操作授权由 Host 保持。 */
|
|
3
|
+
export declare const HOST_AGENT_CONFIG_API_VERSION = "1.0.0";
|
|
4
|
+
/** Agent 配置 SHA-256 的十六进制编码。 */
|
|
5
|
+
export declare const AgentConfigSha256: z.ZodString;
|
|
6
|
+
/** 内置 Workspace 种类;不含路径分隔符。 */
|
|
7
|
+
export declare const WorkspaceKind: z.ZodString;
|
|
8
|
+
/** @param value - 完整 Agent 配置版本。 @returns 是否为保留构建元数据的规范 SemVer;优先级由 semver 按规范比较。 */
|
|
9
|
+
export declare function isAgentConfigVersion(value: string): boolean;
|
|
10
|
+
/** server 分配中可公开的包信息,未携带商家凭据。 */
|
|
11
|
+
export declare const AgentConfigDescriptorSchema: z.ZodObject<{
|
|
12
|
+
workspaceKind: z.ZodString;
|
|
13
|
+
version: z.ZodString;
|
|
14
|
+
sha256: z.ZodString;
|
|
15
|
+
sizeBytes: z.ZodNumber;
|
|
16
|
+
downloadUrl: z.ZodURL;
|
|
17
|
+
hostAgentConfigApiRange: z.ZodString;
|
|
18
|
+
releaseNotes: z.ZodString;
|
|
19
|
+
}, z.core.$strict>;
|
|
20
|
+
/** 经解析的 server 分配。 */
|
|
21
|
+
export type AgentConfigDescriptor = z.infer<typeof AgentConfigDescriptorSchema>;
|
|
22
|
+
/** 包内逐文件摘要与字节数,文件路径相对版本目录。 */
|
|
23
|
+
export declare const AgentConfigManifestSchema: z.ZodObject<{
|
|
24
|
+
formatVersion: z.ZodLiteral<1>;
|
|
25
|
+
workspaceKind: z.ZodString;
|
|
26
|
+
version: z.ZodString;
|
|
27
|
+
hostAgentConfigApiRange: z.ZodString;
|
|
28
|
+
files: z.ZodArray<z.ZodObject<{
|
|
29
|
+
path: z.ZodString;
|
|
30
|
+
sizeBytes: z.ZodNumber;
|
|
31
|
+
sha256: z.ZodString;
|
|
32
|
+
}, z.core.$strict>>;
|
|
33
|
+
}, z.core.$strict>;
|
|
34
|
+
/** 确定性 gzip JSON Agent 配置:清单和按同序排列的 base64 文件。 */
|
|
35
|
+
export declare const AgentConfigDocumentSchema: z.ZodObject<{
|
|
36
|
+
manifest: z.ZodObject<{
|
|
37
|
+
formatVersion: z.ZodLiteral<1>;
|
|
38
|
+
workspaceKind: z.ZodString;
|
|
39
|
+
version: z.ZodString;
|
|
40
|
+
hostAgentConfigApiRange: z.ZodString;
|
|
41
|
+
files: z.ZodArray<z.ZodObject<{
|
|
42
|
+
path: z.ZodString;
|
|
43
|
+
sizeBytes: z.ZodNumber;
|
|
44
|
+
sha256: z.ZodString;
|
|
45
|
+
}, z.core.$strict>>;
|
|
46
|
+
}, z.core.$strict>;
|
|
47
|
+
files: z.ZodArray<z.ZodObject<{
|
|
48
|
+
path: z.ZodString;
|
|
49
|
+
base64: z.ZodString;
|
|
50
|
+
}, z.core.$strict>>;
|
|
51
|
+
}, z.core.$strict>;
|
|
52
|
+
/** 包内文件清单。 */
|
|
53
|
+
export type AgentConfigManifest = z.infer<typeof AgentConfigManifestSchema>;
|
|
54
|
+
/** 已解码并校验的文件。 */
|
|
55
|
+
export interface AgentConfigFile {
|
|
56
|
+
readonly path: string;
|
|
57
|
+
readonly bytes: Buffer;
|
|
58
|
+
}
|
|
59
|
+
/** 内容更新失败类型;报告不包含下载 URL、响应正文或凭据。 */
|
|
60
|
+
export type AgentConfigErrorCode = 'AGENT_CONFIG_QUERY_FAILED' | 'AGENT_CONFIG_INVALID' | 'AGENT_CONFIG_INCOMPATIBLE' | 'AGENT_CONFIG_DIGEST_MISMATCH' | 'AGENT_CONFIG_DOWNLOAD_FAILED' | 'AGENT_CONFIG_APPLY_FAILED' | 'AGENT_CONFIG_SCOPE_CHANGED';
|
|
61
|
+
/** 有界内容更新的结构化失败。 */
|
|
62
|
+
export declare class AgentConfigError extends Error {
|
|
63
|
+
readonly code: AgentConfigErrorCode;
|
|
64
|
+
/** @param code - 失败类别。 */
|
|
65
|
+
constructor(code: AgentConfigErrorCode);
|
|
66
|
+
}
|
|
67
|
+
/** @param bytes - 完整文件字节。 @returns SHA-256 小写十六进制编码。 */
|
|
68
|
+
export declare function agentConfigSha256(bytes: Uint8Array): string;
|
|
69
|
+
/**
|
|
70
|
+
* 拒绝无法在各 Host 文件系统安全表达的相对路径。
|
|
71
|
+
* @param path - 包内 POSIX 路径。
|
|
72
|
+
* @returns 原始路径。
|
|
73
|
+
*/
|
|
74
|
+
export declare function contentPath(path: string): string;
|
|
75
|
+
/**
|
|
76
|
+
* 同时认证压缩包和逐文件字节,任何失败均发生在版本目录写入前。
|
|
77
|
+
* @param bytes - 下载的完整 gzip 字节。
|
|
78
|
+
* @param descriptor - server 经鉴权查询给出的登记信息。
|
|
79
|
+
* @param maxExpandedBytes - 解压 JSON 和文件总字节上限。
|
|
80
|
+
* @returns 清单和已校验文件。
|
|
81
|
+
*/
|
|
82
|
+
export declare function decodeAgentConfig(bytes: Buffer, descriptor: AgentConfigDescriptor, maxExpandedBytes: number): {
|
|
83
|
+
readonly manifest: AgentConfigManifest;
|
|
84
|
+
readonly files: readonly AgentConfigFile[];
|
|
85
|
+
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/** 内置 Workspace Agent 配置的文件校验;目录和压缩字节的大小均由部署配置限制。 */
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { gunzipSync } from 'node:zlib';
|
|
4
|
+
import semver from 'semver';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { assertTemplateRelativePath } from "./paths.js";
|
|
7
|
+
/** 本 Host 实现的 Agent 配置接口版本;白名单与写操作授权由 Host 保持。 */
|
|
8
|
+
export const HOST_AGENT_CONFIG_API_VERSION = '1.0.0';
|
|
9
|
+
/** Agent 配置 SHA-256 的十六进制编码。 */
|
|
10
|
+
export const AgentConfigSha256 = z.string().regex(/^[a-f0-9]{64}$/u);
|
|
11
|
+
/** 内置 Workspace 种类;不含路径分隔符。 */
|
|
12
|
+
export const WorkspaceKind = z.string().regex(/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u).max(80);
|
|
13
|
+
/** @param value - 完整 Agent 配置版本。 @returns 是否为保留构建元数据的规范 SemVer;优先级由 semver 按规范比较。 */
|
|
14
|
+
export function isAgentConfigVersion(value) {
|
|
15
|
+
const parsed = semver.parse(value);
|
|
16
|
+
return parsed !== null && `${parsed.version}${parsed.build.length === 0 ? '' : `+${parsed.build.join('.')}`}` === value;
|
|
17
|
+
}
|
|
18
|
+
/** server 分配中可公开的包信息,未携带商家凭据。 */
|
|
19
|
+
export const AgentConfigDescriptorSchema = z.strictObject({
|
|
20
|
+
workspaceKind: WorkspaceKind,
|
|
21
|
+
version: z.string().refine(isAgentConfigVersion),
|
|
22
|
+
sha256: AgentConfigSha256,
|
|
23
|
+
sizeBytes: z.number().int().positive().max(Number.MAX_SAFE_INTEGER),
|
|
24
|
+
downloadUrl: z.url(),
|
|
25
|
+
hostAgentConfigApiRange: z.string().refine(value => semver.validRange(value) !== null),
|
|
26
|
+
releaseNotes: z.string().min(1).max(20000),
|
|
27
|
+
});
|
|
28
|
+
/** 包内逐文件摘要与字节数,文件路径相对版本目录。 */
|
|
29
|
+
export const AgentConfigManifestSchema = z.strictObject({
|
|
30
|
+
formatVersion: z.literal(1), workspaceKind: WorkspaceKind,
|
|
31
|
+
version: z.string().refine(isAgentConfigVersion),
|
|
32
|
+
hostAgentConfigApiRange: z.string().refine(value => semver.validRange(value) !== null),
|
|
33
|
+
files: z.array(z.strictObject({ path: z.string(), sizeBytes: z.number().int().nonnegative(), sha256: AgentConfigSha256 })).min(1).max(10000),
|
|
34
|
+
});
|
|
35
|
+
/** 确定性 gzip JSON Agent 配置:清单和按同序排列的 base64 文件。 */
|
|
36
|
+
export const AgentConfigDocumentSchema = z.strictObject({
|
|
37
|
+
manifest: AgentConfigManifestSchema,
|
|
38
|
+
files: z.array(z.strictObject({ path: z.string(), base64: z.string() })).min(1).max(10000),
|
|
39
|
+
});
|
|
40
|
+
/** 有界内容更新的结构化失败。 */
|
|
41
|
+
export class AgentConfigError extends Error {
|
|
42
|
+
code;
|
|
43
|
+
/** @param code - 失败类别。 */
|
|
44
|
+
constructor(code) {
|
|
45
|
+
const messages = {
|
|
46
|
+
AGENT_CONFIG_QUERY_FAILED: 'Agent 配置版本查询失败,保留当前版本;请检查服务器连接。',
|
|
47
|
+
AGENT_CONFIG_INVALID: 'Agent 配置文件或清单无效,保留当前版本;请重新打包发布。',
|
|
48
|
+
AGENT_CONFIG_INCOMPATIBLE: 'Agent 配置不兼容当前 Host,保留当前版本;请分配兼容版本或升级客户端。',
|
|
49
|
+
AGENT_CONFIG_DIGEST_MISMATCH: 'Agent 配置摘要不匹配,保留当前版本;请核对发布登记。',
|
|
50
|
+
AGENT_CONFIG_DOWNLOAD_FAILED: 'Agent 配置下载失败,保留当前版本;请检查文件服务连接。',
|
|
51
|
+
AGENT_CONFIG_APPLY_FAILED: 'Agent 配置应用失败;请检查工作区恢复状态后再继续。',
|
|
52
|
+
AGENT_CONFIG_SCOPE_CHANGED: 'Agent 配置版本来源已变更,已丢弃旧候选;保留当前版本并重新查询。',
|
|
53
|
+
};
|
|
54
|
+
super(messages[code]);
|
|
55
|
+
this.code = code;
|
|
56
|
+
this.name = 'AgentConfigError';
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** @param bytes - 完整文件字节。 @returns SHA-256 小写十六进制编码。 */
|
|
60
|
+
export function agentConfigSha256(bytes) { return createHash('sha256').update(bytes).digest('hex'); }
|
|
61
|
+
/**
|
|
62
|
+
* 拒绝无法在各 Host 文件系统安全表达的相对路径。
|
|
63
|
+
* @param path - 包内 POSIX 路径。
|
|
64
|
+
* @returns 原始路径。
|
|
65
|
+
*/
|
|
66
|
+
export function contentPath(path) {
|
|
67
|
+
assertTemplateRelativePath(path);
|
|
68
|
+
if (path === 'manifest.json' || path.split('/').some(part => /[<>:"|?*\x00-\x1f]/u.test(part)
|
|
69
|
+
|| /[. ]$/u.test(part) || /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/iu.test(part)))
|
|
70
|
+
throw new AgentConfigError('AGENT_CONFIG_INVALID');
|
|
71
|
+
return path;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* 同时认证压缩包和逐文件字节,任何失败均发生在版本目录写入前。
|
|
75
|
+
* @param bytes - 下载的完整 gzip 字节。
|
|
76
|
+
* @param descriptor - server 经鉴权查询给出的登记信息。
|
|
77
|
+
* @param maxExpandedBytes - 解压 JSON 和文件总字节上限。
|
|
78
|
+
* @returns 清单和已校验文件。
|
|
79
|
+
*/
|
|
80
|
+
export function decodeAgentConfig(bytes, descriptor, maxExpandedBytes) {
|
|
81
|
+
if (bytes.length !== descriptor.sizeBytes || agentConfigSha256(bytes) !== descriptor.sha256)
|
|
82
|
+
throw new AgentConfigError('AGENT_CONFIG_DIGEST_MISMATCH');
|
|
83
|
+
if (!semver.satisfies(HOST_AGENT_CONFIG_API_VERSION, descriptor.hostAgentConfigApiRange, { includePrerelease: true }))
|
|
84
|
+
throw new AgentConfigError('AGENT_CONFIG_INCOMPATIBLE');
|
|
85
|
+
let document;
|
|
86
|
+
try {
|
|
87
|
+
document = AgentConfigDocumentSchema.parse(JSON.parse(gunzipSync(bytes, { maxOutputLength: maxExpandedBytes }).toString('utf8')));
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
throw new AgentConfigError('AGENT_CONFIG_INVALID');
|
|
91
|
+
}
|
|
92
|
+
const { manifest } = document;
|
|
93
|
+
if (manifest.workspaceKind !== descriptor.workspaceKind || manifest.version !== descriptor.version
|
|
94
|
+
|| manifest.hostAgentConfigApiRange !== descriptor.hostAgentConfigApiRange || document.files.length !== manifest.files.length)
|
|
95
|
+
throw new AgentConfigError('AGENT_CONFIG_INVALID');
|
|
96
|
+
const seen = new Set();
|
|
97
|
+
let total = 0;
|
|
98
|
+
let previous = '';
|
|
99
|
+
const files = document.files.map((file, index) => {
|
|
100
|
+
const entry = manifest.files[index];
|
|
101
|
+
const path = contentPath(file.path);
|
|
102
|
+
const normalized = path.toLowerCase();
|
|
103
|
+
if (entry.path !== path || path <= previous || seen.has(normalized)
|
|
104
|
+
|| [...seen].some(parent => normalized.startsWith(`${parent}/`) || parent.startsWith(`${normalized}/`)))
|
|
105
|
+
throw new AgentConfigError('AGENT_CONFIG_INVALID');
|
|
106
|
+
seen.add(normalized);
|
|
107
|
+
previous = path;
|
|
108
|
+
const data = Buffer.from(file.base64, 'base64');
|
|
109
|
+
total += data.length;
|
|
110
|
+
if (total > maxExpandedBytes || data.toString('base64') !== file.base64 || data.length !== entry.sizeBytes
|
|
111
|
+
|| agentConfigSha256(data) !== entry.sha256)
|
|
112
|
+
throw new AgentConfigError('AGENT_CONFIG_DIGEST_MISMATCH');
|
|
113
|
+
return { path, bytes: data };
|
|
114
|
+
});
|
|
115
|
+
return { manifest, files };
|
|
116
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { AgentConfigError } from './agent-config-format.ts';
|
|
2
|
+
import type { WorkspaceAgentConfigRegistration, WorkspaceAgentConfigState } from './agent-config-types.ts';
|
|
3
|
+
import type { WorkspaceBundlesSpec, WorkspaceTemplate, WorkspaceTemplateDigest } from './types.ts';
|
|
4
|
+
/** 一个注册来源的缓存、轮询和候选版本;卸载必须等待所有网络与文件工作结束。 */
|
|
5
|
+
export declare class WorkspaceAgentConfigSourceOwner {
|
|
6
|
+
readonly registration: WorkspaceAgentConfigRegistration;
|
|
7
|
+
private readonly spec;
|
|
8
|
+
private readonly report;
|
|
9
|
+
private readonly download;
|
|
10
|
+
private readonly lifetime;
|
|
11
|
+
private inFlight;
|
|
12
|
+
private requestAbort;
|
|
13
|
+
private requestRevision;
|
|
14
|
+
private readonly preparedRevisions;
|
|
15
|
+
private timer;
|
|
16
|
+
private selected;
|
|
17
|
+
private candidate;
|
|
18
|
+
private failure;
|
|
19
|
+
private readonly root;
|
|
20
|
+
/** @param registration - 内置模板及鉴权查询回调。 @param spec - 部署预算。 @param report - 固定失败诊断的报告者。 @param download - 文件 HTTP 传输。 */
|
|
21
|
+
constructor(registration: WorkspaceAgentConfigRegistration, spec: WorkspaceBundlesSpec, report: (error: AgentConfigError) => void, download?: typeof fetch);
|
|
22
|
+
/** @returns 已应用版本、候选版本和最近一次失败。 */
|
|
23
|
+
state(): WorkspaceAgentConfigState;
|
|
24
|
+
/**
|
|
25
|
+
* 从 journal 已提交的摘要定位只读缓存,不从 server 的无分配回答推导降级。
|
|
26
|
+
* @param digest - 已提交 Workspace 模板摘要;undefined 表示首次使用。
|
|
27
|
+
* @param version - 已提交完整版本,包括构建元数据。
|
|
28
|
+
* @param sha256 - 已提交包摘要;undefined 支持旧内置记录。
|
|
29
|
+
* @returns 对应完整模板;缺失已安装缓存时拒绝继续。
|
|
30
|
+
*/
|
|
31
|
+
recover(digest: WorkspaceTemplateDigest | undefined, version?: string, sha256?: string | null): Promise<WorkspaceTemplate>;
|
|
32
|
+
/** 注册者调用一次以启动查询,并按配置间隔继续。 */
|
|
33
|
+
start(): void;
|
|
34
|
+
/** @returns 本次查询和完整候选下载结束;失败通过 state 与 report 保留可观察诊断。 */
|
|
35
|
+
refresh(): Promise<void>;
|
|
36
|
+
/** @returns 下一个 turn 可应用的候选;没有候选时使用已安装版本。 */
|
|
37
|
+
template(): WorkspaceTemplate;
|
|
38
|
+
/** @param template - 即将整体替换的候选。 @throws 来源代次已经改变或已撤销时拒绝应用。 */
|
|
39
|
+
assertApplicable(template: WorkspaceTemplate): void;
|
|
40
|
+
/** @param template - 已通过 Workspace 整体替换提交的模板。 */
|
|
41
|
+
committed(template: WorkspaceTemplate): Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* 清除未提交候选,报告整体替换失败;已提交状态由 journal 恢复结果决定。
|
|
44
|
+
* @param template - 未能完成提交的模板。
|
|
45
|
+
* @param cause - 保留来源失效分类;其他原因报告固定应用失败诊断。
|
|
46
|
+
*/
|
|
47
|
+
failedApply(template: WorkspaceTemplate, cause: unknown): void;
|
|
48
|
+
/** 撤销查询、清除后续定时器,并等待下载和目录写入完成。 */
|
|
49
|
+
dispose(): Promise<void>;
|
|
50
|
+
private versionDirectory;
|
|
51
|
+
private fetchCandidate;
|
|
52
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
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 { AgentConfigDescriptorSchema, AgentConfigError, HOST_AGENT_CONFIG_API_VERSION, decodeAgentConfig } from "./agent-config-format.js";
|
|
7
|
+
import { loadVerifiedManifest, overallDigest } from "./digest.js";
|
|
8
|
+
import { createNodeFs } from "./filesystem.js";
|
|
9
|
+
import { bundledDirectory } from "./paths.js";
|
|
10
|
+
const AssignmentSchema = z.discriminatedUnion('source', [
|
|
11
|
+
z.object({ source: z.literal('builtin'), workspaceKind: z.string() }),
|
|
12
|
+
z.object({ source: z.literal('registry'), workspaceKind: z.string(), agentConfig: AgentConfigDescriptorSchema }),
|
|
13
|
+
]);
|
|
14
|
+
const ReceiptSchema = AgentConfigDescriptorSchema.omit({ downloadUrl: true, releaseNotes: true });
|
|
15
|
+
function publicVersion(template, sha256) {
|
|
16
|
+
return { workspaceKind: template.bundleId, version: template.manifest.pluginVersion, sha256,
|
|
17
|
+
hostAgentConfigApiVersion: HOST_AGENT_CONFIG_API_VERSION, templateDigest: template.manifest.digest, sourceRoot: template.sourceRoot };
|
|
18
|
+
}
|
|
19
|
+
async function freezeTree(root) {
|
|
20
|
+
for (const entry of await readdir(root, { withFileTypes: true })) {
|
|
21
|
+
const path = join(root, entry.name);
|
|
22
|
+
if (entry.isDirectory())
|
|
23
|
+
await freezeTree(path);
|
|
24
|
+
else
|
|
25
|
+
await chmod(path, 0o444);
|
|
26
|
+
}
|
|
27
|
+
await chmod(root, 0o555);
|
|
28
|
+
}
|
|
29
|
+
/** 一个注册来源的缓存、轮询和候选版本;卸载必须等待所有网络与文件工作结束。 */
|
|
30
|
+
export class WorkspaceAgentConfigSourceOwner {
|
|
31
|
+
registration;
|
|
32
|
+
spec;
|
|
33
|
+
report;
|
|
34
|
+
download;
|
|
35
|
+
lifetime = new AbortController();
|
|
36
|
+
inFlight;
|
|
37
|
+
requestAbort;
|
|
38
|
+
requestRevision;
|
|
39
|
+
preparedRevisions = new WeakMap();
|
|
40
|
+
timer;
|
|
41
|
+
selected;
|
|
42
|
+
candidate;
|
|
43
|
+
failure = null;
|
|
44
|
+
root;
|
|
45
|
+
/** @param registration - 内置模板及鉴权查询回调。 @param spec - 部署预算。 @param report - 固定失败诊断的报告者。 @param download - 文件 HTTP 传输。 */
|
|
46
|
+
constructor(registration, spec, report, download = fetch) {
|
|
47
|
+
this.registration = registration;
|
|
48
|
+
this.spec = spec;
|
|
49
|
+
this.report = report;
|
|
50
|
+
this.download = download;
|
|
51
|
+
this.selected = { template: registration.template, sha256: null };
|
|
52
|
+
this.root = bundledDirectory(join(spec.templateCacheRoot, 'agent-config-versions'), registration.source.identity);
|
|
53
|
+
}
|
|
54
|
+
/** @returns 已应用版本、候选版本和最近一次失败。 */
|
|
55
|
+
state() {
|
|
56
|
+
if (this.candidate !== undefined && this.candidate.revision !== this.registration.source.revision())
|
|
57
|
+
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 };
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* 从 journal 已提交的摘要定位只读缓存,不从 server 的无分配回答推导降级。
|
|
63
|
+
* @param digest - 已提交 Workspace 模板摘要;undefined 表示首次使用。
|
|
64
|
+
* @param version - 已提交完整版本,包括构建元数据。
|
|
65
|
+
* @param sha256 - 已提交包摘要;undefined 支持旧内置记录。
|
|
66
|
+
* @returns 对应完整模板;缺失已安装缓存时拒绝继续。
|
|
67
|
+
*/
|
|
68
|
+
async recover(digest, version, sha256) {
|
|
69
|
+
if (digest === undefined || sha256 === undefined || sha256 === null) {
|
|
70
|
+
this.selected = { template: this.registration.template, sha256: null };
|
|
71
|
+
return this.selected.template;
|
|
72
|
+
}
|
|
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
|
+
if (!/^[0-9a-f]{64}$/u.test(sha256))
|
|
78
|
+
throw new AgentConfigError('AGENT_CONFIG_INVALID');
|
|
79
|
+
const fs = createNodeFs();
|
|
80
|
+
const receipt = ReceiptSchema.parse(JSON.parse(await readFile(join(this.root, sha256, 'receipt.json'), 'utf8')));
|
|
81
|
+
const directory = this.versionDirectory(receipt.version, receipt.sha256);
|
|
82
|
+
if (receipt.sha256 !== sha256 || receipt.workspaceKind !== this.registration.template.bundleId
|
|
83
|
+
|| (await fs.lstat(directory)).isSymbolicLink() || (await fs.lstat(join(directory, receipt.version))).isSymbolicLink())
|
|
84
|
+
throw new AgentConfigError('AGENT_CONFIG_INVALID');
|
|
85
|
+
const manifest = await loadVerifiedManifest(fs, join(directory, receipt.version));
|
|
86
|
+
if (manifest.digest !== digest || version !== undefined && receipt.version !== version
|
|
87
|
+
|| manifest.pluginVersion !== receipt.version)
|
|
88
|
+
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;
|
|
94
|
+
}
|
|
95
|
+
/** 注册者调用一次以启动查询,并按配置间隔继续。 */
|
|
96
|
+
start() {
|
|
97
|
+
const tick = async () => {
|
|
98
|
+
await this.refresh();
|
|
99
|
+
if (!this.lifetime.signal.aborted)
|
|
100
|
+
this.timer = setTimeout(() => { void tick(); }, this.spec.agentConfigPollIntervalMs);
|
|
101
|
+
};
|
|
102
|
+
void tick();
|
|
103
|
+
}
|
|
104
|
+
/** @returns 本次查询和完整候选下载结束;失败通过 state 与 report 保留可观察诊断。 */
|
|
105
|
+
refresh() {
|
|
106
|
+
if (this.lifetime.signal.aborted)
|
|
107
|
+
return Promise.resolve();
|
|
108
|
+
const revision = this.registration.source.revision();
|
|
109
|
+
if (this.candidate !== undefined && this.candidate.revision !== revision)
|
|
110
|
+
this.candidate = undefined;
|
|
111
|
+
if (this.inFlight !== undefined) {
|
|
112
|
+
if (revision === this.requestRevision && !this.requestAbort?.signal.aborted)
|
|
113
|
+
return this.inFlight;
|
|
114
|
+
this.requestAbort?.abort();
|
|
115
|
+
this.failure = null;
|
|
116
|
+
return this.inFlight.then(() => this.refresh());
|
|
117
|
+
}
|
|
118
|
+
const abort = new AbortController();
|
|
119
|
+
this.requestAbort = abort;
|
|
120
|
+
this.requestRevision = revision;
|
|
121
|
+
const run = this.fetchCandidate(revision, abort.signal).catch((error) => {
|
|
122
|
+
if (this.lifetime.signal.aborted || abort.signal.aborted || revision !== this.registration.source.revision())
|
|
123
|
+
return;
|
|
124
|
+
this.candidate = undefined;
|
|
125
|
+
const failure = error instanceof AgentConfigError ? error : new AgentConfigError('AGENT_CONFIG_INVALID');
|
|
126
|
+
this.failure = { code: failure.code, message: failure.message };
|
|
127
|
+
this.report(failure);
|
|
128
|
+
}).finally(() => { this.inFlight = undefined; this.requestAbort = undefined; this.requestRevision = undefined; });
|
|
129
|
+
this.inFlight = run;
|
|
130
|
+
return run;
|
|
131
|
+
}
|
|
132
|
+
/** @returns 下一个 turn 可应用的候选;没有候选时使用已安装版本。 */
|
|
133
|
+
template() {
|
|
134
|
+
if (this.candidate !== undefined && this.candidate.revision !== this.registration.source.revision())
|
|
135
|
+
this.candidate = undefined;
|
|
136
|
+
return this.candidate?.template ?? this.selected.template;
|
|
137
|
+
}
|
|
138
|
+
/** @param template - 即将整体替换的候选。 @throws 来源代次已经改变或已撤销时拒绝应用。 */
|
|
139
|
+
assertApplicable(template) {
|
|
140
|
+
const revision = this.preparedRevisions.get(template);
|
|
141
|
+
if (this.lifetime.signal.aborted || revision !== undefined && revision !== this.registration.source.revision())
|
|
142
|
+
throw new AgentConfigError('AGENT_CONFIG_SCOPE_CHANGED');
|
|
143
|
+
}
|
|
144
|
+
/** @param template - 已通过 Workspace 整体替换提交的模板。 */
|
|
145
|
+
async committed(template) {
|
|
146
|
+
await this.recover(template.manifest.digest, template.manifest.pluginVersion, template.agentConfigSha256 ?? null);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* 清除未提交候选,报告整体替换失败;已提交状态由 journal 恢复结果决定。
|
|
150
|
+
* @param template - 未能完成提交的模板。
|
|
151
|
+
* @param cause - 保留来源失效分类;其他原因报告固定应用失败诊断。
|
|
152
|
+
*/
|
|
153
|
+
failedApply(template, cause) {
|
|
154
|
+
if (this.candidate?.template === template)
|
|
155
|
+
this.candidate = undefined;
|
|
156
|
+
const error = cause instanceof AgentConfigError && cause.code === 'AGENT_CONFIG_SCOPE_CHANGED'
|
|
157
|
+
? cause : new AgentConfigError('AGENT_CONFIG_APPLY_FAILED');
|
|
158
|
+
this.failure = { code: error.code, message: error.message };
|
|
159
|
+
this.report(error);
|
|
160
|
+
}
|
|
161
|
+
/** 撤销查询、清除后续定时器,并等待下载和目录写入完成。 */
|
|
162
|
+
async dispose() {
|
|
163
|
+
this.lifetime.abort();
|
|
164
|
+
this.candidate = undefined;
|
|
165
|
+
if (this.timer !== undefined)
|
|
166
|
+
clearTimeout(this.timer);
|
|
167
|
+
await this.inFlight;
|
|
168
|
+
this.candidate = undefined;
|
|
169
|
+
}
|
|
170
|
+
versionDirectory(version, sha256) {
|
|
171
|
+
if (!AgentConfigDescriptorSchema.shape.version.safeParse(version).success
|
|
172
|
+
|| !/^[0-9a-f]{64}$/u.test(sha256))
|
|
173
|
+
throw new AgentConfigError('AGENT_CONFIG_INVALID');
|
|
174
|
+
return join(this.root, sha256);
|
|
175
|
+
}
|
|
176
|
+
async fetchCandidate(revision, requestSignal) {
|
|
177
|
+
const signal = AbortSignal.any([this.lifetime.signal, requestSignal, AbortSignal.timeout(this.spec.agentConfigRequestTimeoutMs)]);
|
|
178
|
+
let value;
|
|
179
|
+
try {
|
|
180
|
+
value = await this.registration.source.query(signal);
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
throw new AgentConfigError('AGENT_CONFIG_QUERY_FAILED');
|
|
184
|
+
}
|
|
185
|
+
const assignment = AssignmentSchema.safeParse(value);
|
|
186
|
+
if (!assignment.success || assignment.data.workspaceKind !== this.registration.template.bundleId)
|
|
187
|
+
throw new AgentConfigError('AGENT_CONFIG_INVALID');
|
|
188
|
+
if (assignment.data.source === 'builtin') {
|
|
189
|
+
this.candidate = undefined;
|
|
190
|
+
this.failure = null;
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const descriptor = assignment.data.agentConfig;
|
|
194
|
+
if (descriptor.workspaceKind !== assignment.data.workspaceKind)
|
|
195
|
+
throw new AgentConfigError('AGENT_CONFIG_INVALID');
|
|
196
|
+
if (descriptor.sha256 === this.selected.sha256) {
|
|
197
|
+
this.candidate = undefined;
|
|
198
|
+
this.failure = null;
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (descriptor.sizeBytes > this.spec.agentConfigMaxDownloadBytes)
|
|
202
|
+
throw new AgentConfigError('AGENT_CONFIG_INVALID');
|
|
203
|
+
const url = new URL(descriptor.downloadUrl);
|
|
204
|
+
if (url.username !== '' || url.password !== '' || url.protocol !== 'https:'
|
|
205
|
+
&& !(this.spec.allowLoopbackAgentConfigHttp && url.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname)))
|
|
206
|
+
throw new AgentConfigError('AGENT_CONFIG_INVALID');
|
|
207
|
+
let bytes;
|
|
208
|
+
try {
|
|
209
|
+
const response = await this.download(url, { signal, redirect: 'error' });
|
|
210
|
+
if (!response.ok || response.body === null || response.headers.get('content-length') !== null
|
|
211
|
+
&& Number(response.headers.get('content-length')) !== descriptor.sizeBytes)
|
|
212
|
+
throw new AgentConfigError('AGENT_CONFIG_DOWNLOAD_FAILED');
|
|
213
|
+
const chunks = [];
|
|
214
|
+
let count = 0;
|
|
215
|
+
for await (const chunk of response.body) {
|
|
216
|
+
count += chunk.byteLength;
|
|
217
|
+
if (count > descriptor.sizeBytes) {
|
|
218
|
+
await response.body.cancel().catch(() => undefined);
|
|
219
|
+
throw new AgentConfigError('AGENT_CONFIG_DOWNLOAD_FAILED');
|
|
220
|
+
}
|
|
221
|
+
chunks.push(chunk);
|
|
222
|
+
}
|
|
223
|
+
bytes = Buffer.concat(chunks);
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
throw new AgentConfigError('AGENT_CONFIG_DOWNLOAD_FAILED');
|
|
227
|
+
}
|
|
228
|
+
const content = decodeAgentConfig(bytes, descriptor, this.spec.agentConfigMaxExpandedBytes);
|
|
229
|
+
if (signal.aborted || revision !== this.registration.source.revision())
|
|
230
|
+
return;
|
|
231
|
+
await mkdir(this.root, { recursive: true });
|
|
232
|
+
const directory = this.versionDirectory(descriptor.version, descriptor.sha256);
|
|
233
|
+
const tree = join(directory, descriptor.version);
|
|
234
|
+
const fs = createNodeFs();
|
|
235
|
+
if (!await fs.exists(directory)) {
|
|
236
|
+
const staging = await mkdtemp(join(this.root, '.staging-'));
|
|
237
|
+
try {
|
|
238
|
+
const stagingTree = join(staging, descriptor.version);
|
|
239
|
+
await mkdir(stagingTree);
|
|
240
|
+
for (const file of content.files) {
|
|
241
|
+
const path = join(stagingTree, ...file.path.split('/'));
|
|
242
|
+
await mkdir(dirname(path), { recursive: true });
|
|
243
|
+
await writeFile(path, file.bytes, { flag: 'wx' });
|
|
244
|
+
}
|
|
245
|
+
const files = content.manifest.files.map(file => ({ path: file.path, bytes: file.sizeBytes, digest: brandString(file.sha256) }));
|
|
246
|
+
await writeFile(join(stagingTree, 'manifest.json'), JSON.stringify({ formatVersion: 1, pluginVersion: descriptor.version, files, digest: overallDigest(files) }) + '\n');
|
|
247
|
+
const { downloadUrl: _downloadUrl, releaseNotes: _releaseNotes, ...receipt } = descriptor;
|
|
248
|
+
await writeFile(join(staging, 'receipt.json'), JSON.stringify(ReceiptSchema.parse(receipt)) + '\n');
|
|
249
|
+
await rename(staging, directory);
|
|
250
|
+
await freezeTree(tree);
|
|
251
|
+
await chmod(join(directory, 'receipt.json'), 0o444);
|
|
252
|
+
}
|
|
253
|
+
finally {
|
|
254
|
+
await rm(staging, { recursive: true, force: true });
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
const manifest = await loadVerifiedManifest(fs, tree);
|
|
258
|
+
const files = content.manifest.files.map(file => ({ path: file.path, bytes: file.sizeBytes, digest: brandString(file.sha256) }));
|
|
259
|
+
if (manifest.pluginVersion !== descriptor.version || manifest.digest !== overallDigest(files))
|
|
260
|
+
throw new AgentConfigError('AGENT_CONFIG_DIGEST_MISMATCH');
|
|
261
|
+
if (signal.aborted || revision !== this.registration.source.revision())
|
|
262
|
+
return;
|
|
263
|
+
const template = { ...this.registration.template, sourceRoot: tree, manifest, agentConfigSha256: descriptor.sha256 };
|
|
264
|
+
this.preparedRevisions.set(template, revision);
|
|
265
|
+
this.candidate = { template, sha256: descriptor.sha256, revision };
|
|
266
|
+
this.failure = null;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Workspace Agent 配置版本来源和 turn 记录的 Host 接口。 */
|
|
2
|
+
import type { WorkspaceBundleIdentity, WorkspaceTemplate } from './types.ts';
|
|
3
|
+
import type { AgentConfigErrorCode } from './agent-config-format.ts';
|
|
4
|
+
/** 调用方负责通过自身鉴权能力查询 server,不向此服务交出凭据。 */
|
|
5
|
+
export interface WorkspaceAgentConfigSource {
|
|
6
|
+
readonly identity: WorkspaceBundleIdentity;
|
|
7
|
+
/** @returns 随登录、租户或其他分配身份改变的进程内代次。 */
|
|
8
|
+
revision(): number;
|
|
9
|
+
/** @param signal - 源卸载或请求超时取消。 @returns #86 的 builtin / registry 分配。 */
|
|
10
|
+
query(signal: AbortSignal): Promise<unknown>;
|
|
11
|
+
}
|
|
12
|
+
/** 下一个 turn 可使用的完整 Agent 配置版本,路径指向已经校验的不可变文件。 */
|
|
13
|
+
export interface WorkspaceAgentConfigVersion {
|
|
14
|
+
readonly workspaceKind: string;
|
|
15
|
+
readonly version: string;
|
|
16
|
+
readonly sha256: string | null;
|
|
17
|
+
readonly hostAgentConfigApiVersion: string;
|
|
18
|
+
readonly templateDigest: string;
|
|
19
|
+
readonly sourceRoot: string;
|
|
20
|
+
}
|
|
21
|
+
/** 内容更新的当前可观察状态;错误只包含固定诊断。 */
|
|
22
|
+
export interface WorkspaceAgentConfigState {
|
|
23
|
+
readonly current: WorkspaceAgentConfigVersion;
|
|
24
|
+
readonly pending: WorkspaceAgentConfigVersion | null;
|
|
25
|
+
readonly failure: {
|
|
26
|
+
readonly code: AgentConfigErrorCode;
|
|
27
|
+
readonly message: string;
|
|
28
|
+
} | null;
|
|
29
|
+
}
|
|
30
|
+
/** 注册时固定的内置版本;源查询失败不能使它不可用。 */
|
|
31
|
+
export interface WorkspaceAgentConfigRegistration {
|
|
32
|
+
readonly template: WorkspaceTemplate;
|
|
33
|
+
readonly source: WorkspaceAgentConfigSource;
|
|
34
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -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
|
+
}
|