@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,82 @@
|
|
|
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';
|
|
4
|
+
import { Context, Service } from '@deepseek-ai/cordis';
|
|
5
|
+
import type { Workspace, WorkspaceId } from '@deepseek-ai/dsh-workspace/types';
|
|
6
|
+
import type { WorkspaceBundleIdentity, WorkspaceBundleRecord, WorkspaceTemplate } from './types.ts';
|
|
7
|
+
import type { WorkspaceAgentConfigHandle, WorkspaceAgentConfigSource, WorkspaceAgentConfigState } from './agent-config-types.ts';
|
|
8
|
+
declare module '@deepseek-ai/cordis' {
|
|
9
|
+
interface Context {
|
|
10
|
+
/** Bundled Workspace templates and their stable materialize. */
|
|
11
|
+
workspaceBundles: WorkspaceBundles;
|
|
12
|
+
}
|
|
13
|
+
interface Events {
|
|
14
|
+
/**
|
|
15
|
+
* Drain consumers after the durable upgrade barrier closes admission and before tree replacement.
|
|
16
|
+
* @param workspaceId - existing workspace whose consumers must release their leases.
|
|
17
|
+
* @mode serial
|
|
18
|
+
*/
|
|
19
|
+
'workspace-bundle/quiesce'(workspaceId: WorkspaceId): Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Host-only capability: register a packaged template, materialize a stable hidden Workspace, and
|
|
24
|
+
* replace its tree as a whole. Registration is an effect; the returned disposer forbids later acquire.
|
|
25
|
+
*/
|
|
26
|
+
export declare abstract class WorkspaceBundles extends Service {
|
|
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);
|
|
33
|
+
/**
|
|
34
|
+
* Make a template available for {@link ensure} and {@link acquire}.
|
|
35
|
+
* @param template - verified catalog and source root.
|
|
36
|
+
* @returns disposer that forbids new acquire and ensure for this identity.
|
|
37
|
+
*/
|
|
38
|
+
abstract register(template: WorkspaceTemplate): () => void;
|
|
39
|
+
/**
|
|
40
|
+
* 为已注册模板启动通用分配查询;回调保留鉴权所有权。
|
|
41
|
+
* @param source - 与模板相同的身份和 server 查询。
|
|
42
|
+
* @returns 撤销轮询并等待下载停止的 disposer。
|
|
43
|
+
*/
|
|
44
|
+
abstract registerAgentConfigSource(source: WorkspaceAgentConfigSource): () => Promise<void>;
|
|
45
|
+
/** @param identity - 内置 Workspace 身份。 @returns 已安装版本、候选、执行阻断、固定诊断与同作用域最近拒绝;blocked 时 current 只供诊断,未注册来源时为 undefined。 */
|
|
46
|
+
abstract agentConfig(identity: WorkspaceBundleIdentity): WorkspaceAgentConfigState | undefined;
|
|
47
|
+
/** @param identity - 内置 Workspace 身份。 @returns 该来源本次查询与下载完成,失败保留在 agentConfig 中。 */
|
|
48
|
+
abstract refreshAgentConfig(identity: WorkspaceBundleIdentity): Promise<void>;
|
|
49
|
+
/**
|
|
50
|
+
* Materialize or upgrade the stable Workspace for a registered template.
|
|
51
|
+
* Unchanged trees reuse the existing Workspace id and path.
|
|
52
|
+
* @param identity - branded pair.
|
|
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.
|
|
55
|
+
*/
|
|
56
|
+
abstract ensure(identity: WorkspaceBundleIdentity): Promise<Workspace>;
|
|
57
|
+
/**
|
|
58
|
+
* Hold a lease that delays upgrade quiesce until it is released.
|
|
59
|
+
* @param workspaceId - materialized Workspace.
|
|
60
|
+
* @returns disposer of the lease.
|
|
61
|
+
*/
|
|
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>;
|
|
70
|
+
/**
|
|
71
|
+
* Read the durable product record when one exists.
|
|
72
|
+
* @param identity - branded pair.
|
|
73
|
+
*/
|
|
74
|
+
abstract record(identity: WorkspaceBundleIdentity): WorkspaceBundleRecord | undefined;
|
|
75
|
+
/**
|
|
76
|
+
* Absolute materialize directory for this identity.
|
|
77
|
+
* Derived from `materializeRoot` and the identity segments; does not require
|
|
78
|
+
* {@link ensure} and does not read the durable record.
|
|
79
|
+
* @param identity - branded pair.
|
|
80
|
+
*/
|
|
81
|
+
abstract pathOf(identity: WorkspaceBundleIdentity): string;
|
|
82
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
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";
|
|
4
|
+
import { Service } from '@deepseek-ai/cordis';
|
|
5
|
+
/**
|
|
6
|
+
* Host-only capability: register a packaged template, materialize a stable hidden Workspace, and
|
|
7
|
+
* replace its tree as a whole. Registration is an effect; the returned disposer forbids later acquire.
|
|
8
|
+
*/
|
|
9
|
+
export class WorkspaceBundles extends Service {
|
|
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) {
|
|
16
|
+
super(ctx, 'workspaceBundles');
|
|
17
|
+
this.localHostReplacement = new LocalHostReplacementState(replacementPath);
|
|
18
|
+
this.localHostIdentity = new LocalHostIdentityState(identityPath);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { WorkspaceBundleFs, WorkspaceTemplateDigest, WorkspaceTemplateFile, WorkspaceTemplateFileDigest, WorkspaceTemplateManifest } from './types.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Digest of the ordered path+file-digest catalog.
|
|
4
|
+
* @param files - sorted catalog.
|
|
5
|
+
*/
|
|
6
|
+
export declare function overallDigest(files: readonly WorkspaceTemplateFile[]): WorkspaceTemplateDigest;
|
|
7
|
+
/**
|
|
8
|
+
* Parse and validate a template manifest document.
|
|
9
|
+
* @param value - JSON value.
|
|
10
|
+
*/
|
|
11
|
+
export declare function parseTemplateManifest(value: unknown): WorkspaceTemplateManifest;
|
|
12
|
+
/**
|
|
13
|
+
* Read every regular file under `root` as a POSIX-relative catalog.
|
|
14
|
+
* @param fs - filesystem.
|
|
15
|
+
* @param root - directory to walk.
|
|
16
|
+
*/
|
|
17
|
+
export declare function listRegularFiles(fs: WorkspaceBundleFs, root: string): Promise<readonly string[]>;
|
|
18
|
+
/**
|
|
19
|
+
* Verify every listed file and the overall digest against a directory.
|
|
20
|
+
* @param fs - filesystem.
|
|
21
|
+
* @param root - directory holding the files.
|
|
22
|
+
* @param manifest - expected catalog.
|
|
23
|
+
*/
|
|
24
|
+
export declare function verifyTemplateTree(fs: WorkspaceBundleFs, root: string, manifest: WorkspaceTemplateManifest): Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* Load `manifest.json` from a template directory and verify the tree.
|
|
27
|
+
* @param fs - filesystem.
|
|
28
|
+
* @param root - template directory.
|
|
29
|
+
*/
|
|
30
|
+
export declare function loadVerifiedManifest(fs: WorkspaceBundleFs, root: string): Promise<WorkspaceTemplateManifest>;
|
|
31
|
+
/** @param bytes - file contents. */
|
|
32
|
+
export declare function digestOf(bytes: Uint8Array): WorkspaceTemplateFileDigest;
|
|
33
|
+
/**
|
|
34
|
+
* Load and verify a packaged template at plugin construction. A missing or
|
|
35
|
+
* invalid tree fails load instead of registering a half-checked catalog.
|
|
36
|
+
* @param root - template directory.
|
|
37
|
+
*/
|
|
38
|
+
export declare function loadVerifiedManifestSync(root: string): WorkspaceTemplateManifest;
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/** Template manifest parsing and per-file / overall digest verification. */
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { existsSync, lstatSync, readdirSync, readFileSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { brandString } from '@deepseek-ai/dsh-brand';
|
|
6
|
+
import { WorkspaceBundleError } from "./errors.js";
|
|
7
|
+
import { assertTemplateRelativePath } from "./paths.js";
|
|
8
|
+
const SHA256 = /^[0-9a-f]{64}$/u;
|
|
9
|
+
function fileDigest(bytes) {
|
|
10
|
+
return brandString(createHash('sha256').update(bytes).digest('hex'));
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Digest of the ordered path+file-digest catalog.
|
|
14
|
+
* @param files - sorted catalog.
|
|
15
|
+
*/
|
|
16
|
+
export function overallDigest(files) {
|
|
17
|
+
const hash = createHash('sha256');
|
|
18
|
+
for (const file of files)
|
|
19
|
+
hash.update(`${file.path}\0${file.digest}\n`);
|
|
20
|
+
return brandString(hash.update('').digest('hex'));
|
|
21
|
+
}
|
|
22
|
+
function sourceOf(value) {
|
|
23
|
+
if (value === undefined)
|
|
24
|
+
return undefined;
|
|
25
|
+
if (typeof value !== 'object' || value === null) {
|
|
26
|
+
throw new WorkspaceBundleError('WORKSPACE_BUNDLE_TEMPLATE_INVALID', 'manifest source must be an object');
|
|
27
|
+
}
|
|
28
|
+
const record = value;
|
|
29
|
+
if (typeof record.repository !== 'string' || typeof record.commit !== 'string') {
|
|
30
|
+
throw new WorkspaceBundleError('WORKSPACE_BUNDLE_TEMPLATE_INVALID', 'manifest source must name repository and commit');
|
|
31
|
+
}
|
|
32
|
+
return { repository: record.repository, commit: record.commit };
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Parse and validate a template manifest document.
|
|
36
|
+
* @param value - JSON value.
|
|
37
|
+
*/
|
|
38
|
+
export function parseTemplateManifest(value) {
|
|
39
|
+
if (typeof value !== 'object' || value === null) {
|
|
40
|
+
throw new WorkspaceBundleError('WORKSPACE_BUNDLE_TEMPLATE_INVALID', 'manifest must be an object');
|
|
41
|
+
}
|
|
42
|
+
const record = value;
|
|
43
|
+
if (record.formatVersion !== 1) {
|
|
44
|
+
throw new WorkspaceBundleError('WORKSPACE_BUNDLE_TEMPLATE_INVALID', 'manifest formatVersion must be 1');
|
|
45
|
+
}
|
|
46
|
+
if (typeof record.pluginVersion !== 'string' || record.pluginVersion === '') {
|
|
47
|
+
throw new WorkspaceBundleError('WORKSPACE_BUNDLE_TEMPLATE_INVALID', 'manifest pluginVersion is required');
|
|
48
|
+
}
|
|
49
|
+
if (!Array.isArray(record.files)) {
|
|
50
|
+
throw new WorkspaceBundleError('WORKSPACE_BUNDLE_TEMPLATE_INVALID', 'manifest files must be an array');
|
|
51
|
+
}
|
|
52
|
+
const files = record.files.map((entry, index) => {
|
|
53
|
+
if (typeof entry !== 'object' || entry === null) {
|
|
54
|
+
throw new WorkspaceBundleError('WORKSPACE_BUNDLE_TEMPLATE_INVALID', `manifest files[${index}] must be an object`);
|
|
55
|
+
}
|
|
56
|
+
const file = entry;
|
|
57
|
+
if (typeof file.path !== 'string' || typeof file.bytes !== 'number' || typeof file.digest !== 'string' || !SHA256.test(file.digest)) {
|
|
58
|
+
throw new WorkspaceBundleError('WORKSPACE_BUNDLE_TEMPLATE_INVALID', `manifest files[${index}] is incomplete`);
|
|
59
|
+
}
|
|
60
|
+
assertTemplateRelativePath(file.path);
|
|
61
|
+
return {
|
|
62
|
+
path: file.path,
|
|
63
|
+
bytes: file.bytes,
|
|
64
|
+
digest: brandString(file.digest),
|
|
65
|
+
};
|
|
66
|
+
});
|
|
67
|
+
const sorted = [...files].sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0);
|
|
68
|
+
if (sorted.some((file, index) => file.path !== files[index]?.path)) {
|
|
69
|
+
throw new WorkspaceBundleError('WORKSPACE_BUNDLE_TEMPLATE_INVALID', 'manifest files must be sorted by path');
|
|
70
|
+
}
|
|
71
|
+
if (sorted.some((file, index) => index > 0 && file.path === sorted[index - 1]?.path)) {
|
|
72
|
+
throw new WorkspaceBundleError('WORKSPACE_BUNDLE_TEMPLATE_INVALID', 'manifest files must not repeat a path');
|
|
73
|
+
}
|
|
74
|
+
if (typeof record.digest !== 'string' || !SHA256.test(record.digest)) {
|
|
75
|
+
throw new WorkspaceBundleError('WORKSPACE_BUNDLE_TEMPLATE_INVALID', 'manifest digest must be a SHA-256 hex string');
|
|
76
|
+
}
|
|
77
|
+
const digest = brandString(record.digest);
|
|
78
|
+
if (digest !== overallDigest(sorted)) {
|
|
79
|
+
throw new WorkspaceBundleError('WORKSPACE_BUNDLE_DIGEST_MISMATCH', 'manifest overall digest does not match its files');
|
|
80
|
+
}
|
|
81
|
+
const source = sourceOf(record.source);
|
|
82
|
+
return source === undefined
|
|
83
|
+
? { formatVersion: 1, pluginVersion: record.pluginVersion, files: sorted, digest }
|
|
84
|
+
: { formatVersion: 1, pluginVersion: record.pluginVersion, source, files: sorted, digest };
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Read every regular file under `root` as a POSIX-relative catalog.
|
|
88
|
+
* @param fs - filesystem.
|
|
89
|
+
* @param root - directory to walk.
|
|
90
|
+
*/
|
|
91
|
+
export async function listRegularFiles(fs, root) {
|
|
92
|
+
const acc = [];
|
|
93
|
+
const walk = async (directory, prefix) => {
|
|
94
|
+
let names;
|
|
95
|
+
try {
|
|
96
|
+
names = await fs.readdir(directory);
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
const code = error.code;
|
|
100
|
+
if (code === 'ENOENT')
|
|
101
|
+
throw new WorkspaceBundleError('WORKSPACE_BUNDLE_MISSING_ASSET', `template root ${JSON.stringify(root)} is missing`);
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
for (const name of [...names].sort()) {
|
|
105
|
+
const path = join(directory, name);
|
|
106
|
+
const relative = prefix === '' ? name : `${prefix}/${name}`;
|
|
107
|
+
const stat = await fs.lstat(path);
|
|
108
|
+
if (stat.isSymbolicLink()) {
|
|
109
|
+
throw new WorkspaceBundleError('WORKSPACE_BUNDLE_SYMLINK_REJECTED', `template path ${JSON.stringify(relative)} is a symbolic link`);
|
|
110
|
+
}
|
|
111
|
+
if (stat.isDirectory())
|
|
112
|
+
await walk(path, relative);
|
|
113
|
+
else if (stat.isFile()) {
|
|
114
|
+
assertTemplateRelativePath(relative);
|
|
115
|
+
acc.push(relative);
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
throw new WorkspaceBundleError('WORKSPACE_BUNDLE_TEMPLATE_INVALID', `template path ${JSON.stringify(relative)} is not a regular file`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
await walk(root, '');
|
|
123
|
+
return acc;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Verify every listed file and the overall digest against a directory.
|
|
127
|
+
* @param fs - filesystem.
|
|
128
|
+
* @param root - directory holding the files.
|
|
129
|
+
* @param manifest - expected catalog.
|
|
130
|
+
*/
|
|
131
|
+
export async function verifyTemplateTree(fs, root, manifest) {
|
|
132
|
+
const present = new Set(await listRegularFiles(fs, root));
|
|
133
|
+
present.delete('manifest.json');
|
|
134
|
+
for (const file of manifest.files) {
|
|
135
|
+
if (!present.has(file.path)) {
|
|
136
|
+
throw new WorkspaceBundleError('WORKSPACE_BUNDLE_MISSING_ASSET', `template is missing ${JSON.stringify(file.path)}`);
|
|
137
|
+
}
|
|
138
|
+
const bytes = await fs.readFile(join(root, ...file.path.split('/')));
|
|
139
|
+
if (bytes.length !== file.bytes || fileDigest(bytes) !== file.digest) {
|
|
140
|
+
throw new WorkspaceBundleError('WORKSPACE_BUNDLE_DIGEST_MISMATCH', `template file ${JSON.stringify(file.path)} digest mismatch`);
|
|
141
|
+
}
|
|
142
|
+
present.delete(file.path);
|
|
143
|
+
}
|
|
144
|
+
if (present.size > 0) {
|
|
145
|
+
throw new WorkspaceBundleError('WORKSPACE_BUNDLE_TEMPLATE_INVALID', `template has unexpected files ${[...present].join(', ')}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Load `manifest.json` from a template directory and verify the tree.
|
|
150
|
+
* @param fs - filesystem.
|
|
151
|
+
* @param root - template directory.
|
|
152
|
+
*/
|
|
153
|
+
export async function loadVerifiedManifest(fs, root) {
|
|
154
|
+
let raw;
|
|
155
|
+
try {
|
|
156
|
+
raw = await fs.readFile(join(root, 'manifest.json'));
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
const code = error.code;
|
|
160
|
+
if (code === 'ENOENT')
|
|
161
|
+
throw new WorkspaceBundleError('WORKSPACE_BUNDLE_MISSING_ASSET', 'template is missing manifest.json');
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
let value;
|
|
165
|
+
try {
|
|
166
|
+
value = JSON.parse(raw.toString('utf8'));
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
throw new WorkspaceBundleError('WORKSPACE_BUNDLE_TEMPLATE_INVALID', 'manifest.json is not JSON');
|
|
170
|
+
}
|
|
171
|
+
const manifest = parseTemplateManifest(value);
|
|
172
|
+
await verifyTemplateTree(fs, root, manifest);
|
|
173
|
+
return manifest;
|
|
174
|
+
}
|
|
175
|
+
/** @param bytes - file contents. */
|
|
176
|
+
export function digestOf(bytes) {
|
|
177
|
+
return fileDigest(bytes);
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Load and verify a packaged template at plugin construction. A missing or
|
|
181
|
+
* invalid tree fails load instead of registering a half-checked catalog.
|
|
182
|
+
* @param root - template directory.
|
|
183
|
+
*/
|
|
184
|
+
export function loadVerifiedManifestSync(root) {
|
|
185
|
+
const manifestPath = join(root, 'manifest.json');
|
|
186
|
+
if (!existsSync(manifestPath)) {
|
|
187
|
+
throw new WorkspaceBundleError('WORKSPACE_BUNDLE_MISSING_ASSET', 'template is missing manifest.json');
|
|
188
|
+
}
|
|
189
|
+
let value;
|
|
190
|
+
try {
|
|
191
|
+
value = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
throw new WorkspaceBundleError('WORKSPACE_BUNDLE_TEMPLATE_INVALID', 'manifest.json is not JSON');
|
|
195
|
+
}
|
|
196
|
+
const manifest = parseTemplateManifest(value);
|
|
197
|
+
const present = new Set();
|
|
198
|
+
const walk = (directory, prefix) => {
|
|
199
|
+
let names;
|
|
200
|
+
try {
|
|
201
|
+
names = readdirSync(directory);
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
const code = error.code;
|
|
205
|
+
if (code === 'ENOENT')
|
|
206
|
+
throw new WorkspaceBundleError('WORKSPACE_BUNDLE_MISSING_ASSET', `template root ${JSON.stringify(root)} is missing`);
|
|
207
|
+
throw error;
|
|
208
|
+
}
|
|
209
|
+
for (const name of [...names].sort()) {
|
|
210
|
+
const path = join(directory, name);
|
|
211
|
+
const relative = prefix === '' ? name : `${prefix}/${name}`;
|
|
212
|
+
const stat = lstatSync(path);
|
|
213
|
+
if (stat.isSymbolicLink()) {
|
|
214
|
+
throw new WorkspaceBundleError('WORKSPACE_BUNDLE_SYMLINK_REJECTED', `template path ${JSON.stringify(relative)} is a symbolic link`);
|
|
215
|
+
}
|
|
216
|
+
if (stat.isDirectory())
|
|
217
|
+
walk(path, relative);
|
|
218
|
+
else if (stat.isFile()) {
|
|
219
|
+
assertTemplateRelativePath(relative);
|
|
220
|
+
present.add(relative);
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
throw new WorkspaceBundleError('WORKSPACE_BUNDLE_TEMPLATE_INVALID', `template path ${JSON.stringify(relative)} is not a regular file`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
walk(root, '');
|
|
228
|
+
present.delete('manifest.json');
|
|
229
|
+
for (const file of manifest.files) {
|
|
230
|
+
if (!present.has(file.path)) {
|
|
231
|
+
throw new WorkspaceBundleError('WORKSPACE_BUNDLE_MISSING_ASSET', `template is missing ${JSON.stringify(file.path)}`);
|
|
232
|
+
}
|
|
233
|
+
const bytes = readFileSync(join(root, ...file.path.split('/')));
|
|
234
|
+
if (bytes.length !== file.bytes || fileDigest(bytes) !== file.digest) {
|
|
235
|
+
throw new WorkspaceBundleError('WORKSPACE_BUNDLE_DIGEST_MISMATCH', `template file ${JSON.stringify(file.path)} digest mismatch`);
|
|
236
|
+
}
|
|
237
|
+
present.delete(file.path);
|
|
238
|
+
}
|
|
239
|
+
if (present.size > 0) {
|
|
240
|
+
throw new WorkspaceBundleError('WORKSPACE_BUNDLE_TEMPLATE_INVALID', `template has unexpected files ${[...present].join(', ')}`);
|
|
241
|
+
}
|
|
242
|
+
return manifest;
|
|
243
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Named failures of WorkspaceBundles configuration and materialize. */
|
|
2
|
+
import type { WorkspaceBundleErrorCode, WorkspaceBundlesConfigErrorCode } from './types.ts';
|
|
3
|
+
/** Misconfiguration of the workspace-bundle row; plugin load fails with it. */
|
|
4
|
+
export declare class WorkspaceBundlesConfigError extends Error {
|
|
5
|
+
readonly code: WorkspaceBundlesConfigErrorCode;
|
|
6
|
+
readonly name = "WorkspaceBundlesConfigError";
|
|
7
|
+
/**
|
|
8
|
+
* @param code - stable failure code.
|
|
9
|
+
* @param message - diagnostic naming the offending field.
|
|
10
|
+
*/
|
|
11
|
+
constructor(code: WorkspaceBundlesConfigErrorCode, message: string);
|
|
12
|
+
}
|
|
13
|
+
/** Template, path, digest, or upgrade failure; the Host reports it and does not fall back. */
|
|
14
|
+
export declare class WorkspaceBundleError extends Error {
|
|
15
|
+
readonly code: WorkspaceBundleErrorCode;
|
|
16
|
+
readonly name = "WorkspaceBundleError";
|
|
17
|
+
/**
|
|
18
|
+
* @param code - stable failure code.
|
|
19
|
+
* @param message - diagnostic.
|
|
20
|
+
*/
|
|
21
|
+
constructor(code: WorkspaceBundleErrorCode, message: string);
|
|
22
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Misconfiguration of the workspace-bundle row; plugin load fails with it. */
|
|
2
|
+
export class WorkspaceBundlesConfigError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
name = 'WorkspaceBundlesConfigError';
|
|
5
|
+
/**
|
|
6
|
+
* @param code - stable failure code.
|
|
7
|
+
* @param message - diagnostic naming the offending field.
|
|
8
|
+
*/
|
|
9
|
+
constructor(code, message) {
|
|
10
|
+
super(`workspace-bundle: ${message} (${code})`);
|
|
11
|
+
this.code = code;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/** Template, path, digest, or upgrade failure; the Host reports it and does not fall back. */
|
|
15
|
+
export class WorkspaceBundleError extends Error {
|
|
16
|
+
code;
|
|
17
|
+
name = 'WorkspaceBundleError';
|
|
18
|
+
/**
|
|
19
|
+
* @param code - stable failure code.
|
|
20
|
+
* @param message - diagnostic.
|
|
21
|
+
*/
|
|
22
|
+
constructor(code, message) {
|
|
23
|
+
super(`workspace-bundle: ${message} (${code})`);
|
|
24
|
+
this.code = code;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { WorkspaceBundleCrashPoint, WorkspaceBundleFs } from './types.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Node.js implementation of {@link WorkspaceBundleFs}.
|
|
4
|
+
* @param options.onPoint - optional test hook; throwing aborts the current step.
|
|
5
|
+
*/
|
|
6
|
+
export declare function createNodeFs(options?: {
|
|
7
|
+
onPoint?: (point: WorkspaceBundleCrashPoint) => void;
|
|
8
|
+
}): WorkspaceBundleFs;
|
|
9
|
+
/**
|
|
10
|
+
* Wrap an fs with a crash injector used only by tests.
|
|
11
|
+
* @param inner - real or fake filesystem.
|
|
12
|
+
* @param crashAt - the single point that throws {@link WorkspaceBundleError} `WORKSPACE_BUNDLE_CRASH_INJECTED`.
|
|
13
|
+
*/
|
|
14
|
+
export declare function crashableFs(inner: WorkspaceBundleFs, crashAt: WorkspaceBundleCrashPoint | undefined): WorkspaceBundleFs & {
|
|
15
|
+
crash(point: WorkspaceBundleCrashPoint): void;
|
|
16
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/** Node filesystem adapter and test crash injection. */
|
|
2
|
+
import { copyFile, lstat, mkdir, readdir, readFile, realpath, rename, rm, writeFile, access } from 'node:fs/promises';
|
|
3
|
+
import { constants } from 'node:fs';
|
|
4
|
+
import { dirname } from 'node:path';
|
|
5
|
+
import { WorkspaceBundleError } from "./errors.js";
|
|
6
|
+
/**
|
|
7
|
+
* Node.js implementation of {@link WorkspaceBundleFs}.
|
|
8
|
+
* @param options.onPoint - optional test hook; throwing aborts the current step.
|
|
9
|
+
*/
|
|
10
|
+
export function createNodeFs(options = {}) {
|
|
11
|
+
return {
|
|
12
|
+
async mkdir(path) {
|
|
13
|
+
await mkdir(path, { recursive: true });
|
|
14
|
+
},
|
|
15
|
+
async writeFile(path, data) {
|
|
16
|
+
await mkdir(dirname(path), { recursive: true });
|
|
17
|
+
await writeFile(path, data);
|
|
18
|
+
},
|
|
19
|
+
readFile(path) {
|
|
20
|
+
return readFile(path);
|
|
21
|
+
},
|
|
22
|
+
rename(from, to) {
|
|
23
|
+
return rename(from, to);
|
|
24
|
+
},
|
|
25
|
+
rm(path, options) {
|
|
26
|
+
return rm(path, { recursive: options?.recursive === true, force: true });
|
|
27
|
+
},
|
|
28
|
+
lstat(path) {
|
|
29
|
+
return lstat(path);
|
|
30
|
+
},
|
|
31
|
+
async readdir(path) {
|
|
32
|
+
return readdir(path);
|
|
33
|
+
},
|
|
34
|
+
realpath(path) {
|
|
35
|
+
return realpath(path);
|
|
36
|
+
},
|
|
37
|
+
copyFile(from, to) {
|
|
38
|
+
return copyFile(from, to);
|
|
39
|
+
},
|
|
40
|
+
async exists(path) {
|
|
41
|
+
try {
|
|
42
|
+
await access(path, constants.F_OK);
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Wrap an fs with a crash injector used only by tests.
|
|
53
|
+
* @param inner - real or fake filesystem.
|
|
54
|
+
* @param crashAt - the single point that throws {@link WorkspaceBundleError} `WORKSPACE_BUNDLE_CRASH_INJECTED`.
|
|
55
|
+
*/
|
|
56
|
+
export function crashableFs(inner, crashAt) {
|
|
57
|
+
return {
|
|
58
|
+
...inner,
|
|
59
|
+
crash(point) {
|
|
60
|
+
if (crashAt === point)
|
|
61
|
+
throw new WorkspaceBundleError('WORKSPACE_BUNDLE_CRASH_INJECTED', point);
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `WorkspaceBundles` Service Definition (`ctx.workspaceBundles`) and its Provider.
|
|
3
|
+
* The Provider resolves its Config at load, so missing materialize roots fail plugin load.
|
|
4
|
+
*/
|
|
5
|
+
import { GestaltWorkspaceBundles } from './provider.ts';
|
|
6
|
+
export { Config, resolveWorkspaceBundlesSpec } from './config.ts';
|
|
7
|
+
export type { Config as WorkspaceBundlesConfig, ResolvedConfig } from './config.ts';
|
|
8
|
+
export { WorkspaceBundles } from './definition.ts';
|
|
9
|
+
export { WorkspaceBundleError, WorkspaceBundlesConfigError } from './errors.ts';
|
|
10
|
+
export { parseTemplateManifest, loadVerifiedManifest, loadVerifiedManifestSync, verifyTemplateTree, overallDigest, digestOf } from './digest.ts';
|
|
11
|
+
export { encodeBundleSegment, bundledDirectory, assertTemplateRelativePath, identityOf } from './paths.ts';
|
|
12
|
+
export { createNodeFs, crashableFs } from './filesystem.ts';
|
|
13
|
+
export { GestaltWorkspaceBundles } from './provider.ts';
|
|
14
|
+
export type * from './types.ts';
|
|
15
|
+
export type * from './agent-config-types.ts';
|
|
16
|
+
export type { WorkspaceAgentConfigVersionEvent } from './session-events.ts';
|
|
17
|
+
export { recordWorkspaceAgentConfigVersion } from './session-events.ts';
|
|
18
|
+
export { HOST_AGENT_CONFIG_API_VERSION, AgentConfigError, agentConfigSha256, decodeAgentConfig, AgentConfigDescriptorSchema, isAgentConfigVersion } from './agent-config-format.ts';
|
|
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';
|
|
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';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `WorkspaceBundles` Service Definition (`ctx.workspaceBundles`) and its Provider.
|
|
3
|
+
* The Provider resolves its Config at load, so missing materialize roots fail plugin load.
|
|
4
|
+
*/
|
|
5
|
+
import { GestaltWorkspaceBundles } from "./provider.js";
|
|
6
|
+
export { Config, resolveWorkspaceBundlesSpec } from "./config.js";
|
|
7
|
+
export { WorkspaceBundles } from "./definition.js";
|
|
8
|
+
export { WorkspaceBundleError, WorkspaceBundlesConfigError } from "./errors.js";
|
|
9
|
+
export { parseTemplateManifest, loadVerifiedManifest, loadVerifiedManifestSync, verifyTemplateTree, overallDigest, digestOf } from "./digest.js";
|
|
10
|
+
export { encodeBundleSegment, bundledDirectory, assertTemplateRelativePath, identityOf } from "./paths.js";
|
|
11
|
+
export { createNodeFs, crashableFs } from "./filesystem.js";
|
|
12
|
+
export { GestaltWorkspaceBundles } from "./provider.js";
|
|
13
|
+
export { recordWorkspaceAgentConfigVersion } from "./session-events.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";
|
|
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";
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { WorkspaceBundleFs, WorkspaceBundleJournal, WorkspaceBundleRecord, WorkspaceTemplateDigest } from './types.ts';
|
|
2
|
+
import type { WorkspaceBundleIdentity } from './types.ts';
|
|
3
|
+
/** @returns a journal with no in-flight replacement. */
|
|
4
|
+
export declare function idleJournal(): WorkspaceBundleJournal;
|
|
5
|
+
/**
|
|
6
|
+
* @param parent - journal root from Spec.
|
|
7
|
+
* @param identity - bundled Workspace.
|
|
8
|
+
*/
|
|
9
|
+
export declare function journalPath(parent: string, identity: WorkspaceBundleIdentity): string;
|
|
10
|
+
/**
|
|
11
|
+
* @param parent - journal root from Spec.
|
|
12
|
+
* @param identity - bundled Workspace.
|
|
13
|
+
*/
|
|
14
|
+
export declare function recordPath(parent: string, identity: WorkspaceBundleIdentity): string;
|
|
15
|
+
/**
|
|
16
|
+
* Persist the product record.
|
|
17
|
+
* @param fs - filesystem.
|
|
18
|
+
* @param path - record.json path.
|
|
19
|
+
* @param record - value.
|
|
20
|
+
*/
|
|
21
|
+
export declare function writeRecord(fs: WorkspaceBundleFs, path: string, record: WorkspaceBundleRecord): Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* Read a product record when present.
|
|
24
|
+
* @param fs - filesystem.
|
|
25
|
+
* @param path - record.json path.
|
|
26
|
+
*/
|
|
27
|
+
export declare function readRecord(fs: WorkspaceBundleFs, path: string): Promise<WorkspaceBundleRecord | undefined>;
|
|
28
|
+
/**
|
|
29
|
+
* Reject stored paths whose canonical locations differ from the identity's controlled directories.
|
|
30
|
+
* @param fs - filesystem used by recovery, including realpath resolution.
|
|
31
|
+
* @param record - stored product record.
|
|
32
|
+
* @param root - materialize directory derived from the requested identity and current Spec.
|
|
33
|
+
* @returns fulfills when every stored path has the same canonical location as its controlled directory.
|
|
34
|
+
* @throws {WorkspaceBundleError} Before recovery mutates files when a stored path differs.
|
|
35
|
+
*/
|
|
36
|
+
export declare function assertRecordPaths(fs: WorkspaceBundleFs, record: WorkspaceBundleRecord, root: string): Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* Persist the journal as part of the product record.
|
|
39
|
+
* @param fs - filesystem.
|
|
40
|
+
* @param path - record.json path.
|
|
41
|
+
* @param record - current record.
|
|
42
|
+
* @param journal - next journal.
|
|
43
|
+
*/
|
|
44
|
+
export declare function writeJournal(fs: WorkspaceBundleFs, path: string, record: WorkspaceBundleRecord, journal: WorkspaceBundleJournal): Promise<WorkspaceBundleRecord>;
|
|
45
|
+
/**
|
|
46
|
+
* Mark a replacement as committed: idle journal, new digest and plugin version.
|
|
47
|
+
* @param record - current record.
|
|
48
|
+
* @param digest - verified tree digest.
|
|
49
|
+
* @param pluginVersion - packaged plugin version.
|
|
50
|
+
*/
|
|
51
|
+
export declare function committedRecord(record: WorkspaceBundleRecord, digest: WorkspaceTemplateDigest, pluginVersion: string): WorkspaceBundleRecord;
|