odai-dsh-agent 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +138 -138
  3. package/bin/odai-dsh-agent.mjs +108 -108
  4. package/package.json +69 -69
  5. package/preset/odai/agent.cordis.yml +261 -260
  6. package/preset/odai/preset.yml +3 -3
  7. package/preset/odai/runtime/compaction-config.mjs +388 -388
  8. package/preset/odai/runtime/governance.mjs +128 -128
  9. package/preset/odai/runtime/index.mjs +2461 -2263
  10. package/preset/odai/runtime/output-config.mjs +360 -304
  11. package/preset/odai/runtime/research-packet.mjs +150 -150
  12. package/preset/odai/runtime/responsibility-scope.mjs +35 -2
  13. package/preset/odai/runtime/role-overlays.mjs +27 -27
  14. package/preset/odai/runtime/route-card.mjs +168 -152
  15. package/preset/odai/runtime/router.mjs +683 -651
  16. package/preset/odai/runtime/routing-config.mjs +480 -436
  17. package/preset/odai/runtime/routing-context.mjs +321 -321
  18. package/preset/odai/runtime/semantic-memory-store.mjs +452 -452
  19. package/preset/odai/runtime/semantic-memory.mjs +842 -842
  20. package/preset/odai/runtime/session-compat.mjs +389 -389
  21. package/preset/odai/runtime/skill-bundle.mjs +318 -318
  22. package/preset/odai/runtime/skill-evolution.mjs +1413 -1413
  23. package/preset/odai/skills/odai/agents/openai.yaml +4 -4
  24. package/preset/odai/skills/odai/assets/claude-agents/agent.md +11 -11
  25. package/preset/odai/skills/odai/assets/codex-agents/config.toml +11 -11
  26. package/preset/odai/skills/odai/assets/codex-agents/role.toml +5 -5
  27. package/preset/odai/skills/odai/assets/copilot-agents/agent.md +13 -13
  28. package/preset/odai/skills/odai/assets/hooks-policy.example.json +24 -24
  29. package/preset/odai/skills/odai/assets/routing-roles/controller.md +15 -15
  30. package/preset/odai/skills/odai/assets/routing-roles/executor.md +11 -11
  31. package/preset/odai/skills/odai/assets/routing-roles/frontend.md +7 -7
  32. package/preset/odai/skills/odai/assets/routing-roles/planner.md +29 -29
  33. package/preset/odai/skills/odai/assets/routing-roles/researcher.md +6 -6
  34. package/preset/odai/skills/odai/assets/routing-roles/reviewer.md +11 -11
  35. package/preset/odai/skills/odai/assets/task-state.md +14 -14
  36. package/preset/odai/skills/odai/manifest.json +37 -37
  37. package/preset/odai/skills/odai/references/craft.md +54 -54
  38. package/preset/odai/skills/odai/references/dao.md +30 -30
  39. package/preset/odai/skills/odai/references/leverage.md +53 -53
  40. package/preset/odai/skills/odai/references/planning.md +82 -82
  41. package/preset/odai/skills/odai/references/support.md +36 -36
  42. package/preset/odai/skills/odai/references/verification.md +23 -23
  43. package/preset/odai/skills/odai/scripts/build-hooks.mjs +273 -273
  44. package/preset/odai/skills/odai/scripts/build-routing.mjs +274 -274
  45. package/preset/odai/skills/odai/scripts/install-routing.mjs +397 -397
  46. package/preset/odai/skills/odai/scripts/odai-hook.mjs +457 -457
  47. package/preset/odai/skills/odai/scripts/run-role.mjs +336 -336
  48. package/preset/odai/skills/odai/scripts/run-routing.mjs +1154 -1154
  49. package/preset/odai/skills/odai/scripts/verify-routing.mjs +103 -103
  50. package/scripts/verify-agent-load.mjs +257 -257
  51. package/src/installer.mjs +398 -382
package/src/installer.mjs CHANGED
@@ -1,382 +1,398 @@
1
- import { createHash, randomUUID } from "node:crypto";
2
- import {
3
- chmod,
4
- cp,
5
- lstat,
6
- mkdir,
7
- readFile,
8
- readdir,
9
- rename,
10
- rm,
11
- writeFile,
12
- } from "node:fs/promises";
13
- import { homedir } from "node:os";
14
- import { dirname, relative, resolve, sep } from "node:path";
15
- import { fileURLToPath, pathToFileURL } from "node:url";
16
-
17
- import { parse as parseYaml } from "yaml";
18
-
19
- export const DEFAULT_PRESET_ID = "odai";
20
- export const MANIFEST_FILE = ".odai-agent.json";
21
-
22
- const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
23
- const defaultSourceRoot = resolve(packageRoot, "preset/odai");
24
- const packageMetadata = JSON.parse(await readFile(resolve(packageRoot, "package.json"), "utf8"));
25
- const DSH_VERSION_PATTERN = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u;
26
- export const SUPPORTED_DSH_VERSIONS = Object.freeze(
27
- packageMetadata.peerDependencies["@deepseek-ai/dsh"]
28
- .split(/\s*\|\|\s*/u)
29
- .filter(Boolean),
30
- );
31
- if (SUPPORTED_DSH_VERSIONS.length === 0 || SUPPORTED_DSH_VERSIONS.some((version) => !DSH_VERSION_PATTERN.test(version))) {
32
- throw new Error("odai-dsh-agent peer dependency must list exact supported DSH versions");
33
- }
34
- export const SUPPORTED_DSH_VERSION = SUPPORTED_DSH_VERSIONS.at(-1);
35
- const requiredFiles = Object.freeze([
36
- "agent.cordis.yml",
37
- "preset.yml",
38
- "runtime/index.mjs",
39
- "runtime/session-compat.mjs",
40
- "runtime/session-evidence.mjs",
41
- "runtime/skill-bundle.mjs",
42
- "runtime/skill-evolution.mjs",
43
- "runtime/skill-selection-state.mjs",
44
- "runtime/skill-selector.mjs",
45
- "runtime/skill-source-config.mjs",
46
- "skills/odai/SKILL.md",
47
- "skills/odai/manifest.json",
48
- ]);
49
-
50
- function replaceCompositionContract(composition, before, after, label) {
51
- const first = composition.indexOf(before);
52
- if (first < 0 || composition.indexOf(before, first + before.length) >= 0) {
53
- throw new Error(`Odai Agent composition does not contain exactly one rc.7 ${label} contract`);
54
- }
55
- return `${composition.slice(0, first)}${after}${composition.slice(first + before.length)}`;
56
- }
57
-
58
- export function renderAgentCompositionForDsh(composition, dshVersion = SUPPORTED_DSH_VERSION) {
59
- if (typeof composition !== "string" || composition.trim() === "") {
60
- throw new TypeError("agent composition must be a non-empty string");
61
- }
62
- if (!SUPPORTED_DSH_VERSIONS.includes(dshVersion)) {
63
- throw new Error(`unsupported DSH version ${dshVersion || "<empty>"}; expected one of ${SUPPORTED_DSH_VERSIONS.join(", ")}`);
64
- }
65
- let rendered = composition.replace(/\r\n/gu, "\n");
66
- if (dshVersion === "0.1.0-rc.7") return rendered;
67
- rendered = replaceCompositionContract(
68
- rendered,
69
- " # Production dsh does not install these optional providers. An opting-in\n # Profile mounts each provider once on the host plane; copy this preset,\n # then remove `disabled` from the matching tool row.",
70
- " # Product providers are host-plane singletons. Copy this preset, then\n # remove `disabled` from either ordinary tool row to expose that product\n # only to agents composed from the copy.",
71
- "optional-provider comment",
72
- );
73
- rendered = replaceCompositionContract(
74
- rendered,
75
- " provider: codex\n toolName: subagent_codex\n backgroundMode: one-shot",
76
- " provider: codex\n toolName: subagent_codex\n enableRunInBackground: false",
77
- "Codex provider",
78
- );
79
- return replaceCompositionContract(
80
- rendered,
81
- " provider: claude-code\n toolName: subagent_claude_code\n backgroundMode: one-shot",
82
- " provider: claude-code\n toolName: subagent_claude_code\n enableRunInBackground: false",
83
- "Claude Code provider",
84
- );
85
- }
86
-
87
- export function resolveDshHome(configured, env = process.env) {
88
- const value = configured ?? env.DSH_HOME ?? resolve(homedir(), ".dsh");
89
- if (typeof value !== "string" || value.trim() === "") {
90
- throw new TypeError("DSH home must be a non-empty path");
91
- }
92
- const trimmed = value.trim();
93
- if (trimmed === "~") return homedir();
94
- if (trimmed.startsWith(`~${sep}`)) return resolve(homedir(), trimmed.slice(2));
95
- return resolve(trimmed);
96
- }
97
-
98
- export async function inspectAgentInstallation(options = {}) {
99
- const presetId = assertPresetId(options.presetId ?? DEFAULT_PRESET_ID);
100
- const dshHome = resolveDshHome(options.dshHome);
101
- const target = resolve(dshHome, ".agent-presets", presetId);
102
- const state = await inspectTarget(target, presetId);
103
- return Object.freeze({ dshHome, presetId, target, ...state });
104
- }
105
-
106
- export async function installAgentPreset(options = {}) {
107
- const presetId = assertPresetId(options.presetId ?? DEFAULT_PRESET_ID);
108
- const dshHome = resolveDshHome(options.dshHome);
109
- const sourceRoot = resolve(options.sourceRoot ?? defaultSourceRoot);
110
- const dshVersion = options.dshVersion ?? SUPPORTED_DSH_VERSION;
111
- if (!SUPPORTED_DSH_VERSIONS.includes(dshVersion)) {
112
- throw new Error(`unsupported DSH version ${dshVersion || "<empty>"}; expected one of ${SUPPORTED_DSH_VERSIONS.join(", ")}`);
113
- }
114
- const targetRoot = resolve(dshHome, ".agent-presets");
115
- const target = resolve(targetRoot, presetId);
116
- const current = await inspectTarget(target, presetId);
117
- const previousCompositionSize = current.status === "installed"
118
- ? Buffer.byteLength(await readFile(resolve(target, "agent.cordis.yml")))
119
- : undefined;
120
-
121
- if (current.status === "drifted") {
122
- throw new Error(`refusing to replace modified preset at ${target}: ${current.issues.join("; ")}`);
123
- }
124
-
125
- await assertSource(sourceRoot);
126
- const sessionCompatibility = await repairSessionCompatibility(
127
- dshHome,
128
- [sourceRoot],
129
- options.confirmDshStopped === true,
130
- options.processScanner,
131
- );
132
- await mkdir(targetRoot, { recursive: true, mode: 0o700 });
133
- const staging = resolve(targetRoot, `.${presetId}.tmp-${process.pid}-${randomUUID()}`);
134
- const backup = resolve(targetRoot, `.${presetId}.backup-${process.pid}-${randomUUID()}`);
135
- let backupCreated = false;
136
-
137
- try {
138
- await cp(sourceRoot, staging, { recursive: true, errorOnExist: true });
139
- await tightenTree(staging);
140
- const compositionPath = resolve(staging, "agent.cordis.yml");
141
- const composition = renderAgentCompositionForDsh(await readFile(compositionPath, "utf8"), dshVersion);
142
- let stampedComposition = `${composition.trimEnd()}\n# odai-dsh-agent generation ${packageMetadata.version}:${randomUUID()}\n`;
143
- // Supported DSH releases key standing generations by mtime and size, so size must change even on same-tick updates.
144
- if (previousCompositionSize === Buffer.byteLength(stampedComposition)) {
145
- stampedComposition += "# odai-dsh-agent generation size bump\n";
146
- }
147
- await writeFile(compositionPath, stampedComposition, "utf8");
148
- const files = await hashTree(staging);
149
- const manifest = {
150
- schemaVersion: 1,
151
- package: packageMetadata.name,
152
- version: packageMetadata.version,
153
- dshVersion,
154
- presetId,
155
- files,
156
- };
157
- const manifestPath = resolve(staging, MANIFEST_FILE);
158
- await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
159
- await chmod(manifestPath, 0o600);
160
-
161
- if (current.status === "installed") {
162
- await rename(target, backup);
163
- backupCreated = true;
164
- }
165
- await rename(staging, target);
166
- if (backupCreated) {
167
- await rm(backup, { recursive: true, force: true });
168
- backupCreated = false;
169
- }
170
-
171
- return Object.freeze({
172
- operation: current.status === "installed" ? "updated" : "installed",
173
- dshHome,
174
- presetId,
175
- target,
176
- version: packageMetadata.version,
177
- dshVersion,
178
- trust: "user",
179
- security: "DSH user presets have the same privileges as shell access; install only reviewed preset code.",
180
- sessionCompatibility,
181
- });
182
- } catch (error) {
183
- await rm(staging, { recursive: true, force: true });
184
- if (backupCreated) {
185
- const targetState = await pathState(target);
186
- if (targetState === "missing") await rename(backup, target);
187
- }
188
- throw error;
189
- } finally {
190
- await rm(backup, { recursive: true, force: true });
191
- }
192
- }
193
-
194
- export async function uninstallAgentPreset(options = {}) {
195
- const inspected = await inspectAgentInstallation(options);
196
- if (inspected.status === "absent") {
197
- return Object.freeze({ operation: "absent", target: inspected.target, presetId: inspected.presetId });
198
- }
199
- if (inspected.status === "drifted") {
200
- throw new Error(`refusing to remove modified preset at ${inspected.target}: ${inspected.issues.join("; ")}`);
201
- }
202
- await assertPresetIsNotDefault(inspected.dshHome, inspected.presetId);
203
- const sessionCompatibility = await repairSessionCompatibility(
204
- inspected.dshHome,
205
- [defaultSourceRoot, inspected.target],
206
- options.confirmDshStopped === true,
207
- options.processScanner,
208
- );
209
- if (sessionCompatibility.failures.length > 0) {
210
- throw new Error(`refusing to uninstall before legacy Odai session evidence is made ignorable: ${sessionCompatibility.failures.map((failure) => `${failure.path}: ${failure.error}`).join("; ")}`);
211
- }
212
- await rm(inspected.target, { recursive: true, force: true });
213
- return Object.freeze({
214
- operation: "uninstalled",
215
- target: inspected.target,
216
- presetId: inspected.presetId,
217
- sessionCompatibility,
218
- });
219
- }
220
-
221
- async function assertPresetIsNotDefault(dshHome, presetId) {
222
- const settingsPath = resolve(dshHome, "settings.yaml");
223
- let text;
224
- try {
225
- text = await readFile(settingsPath, "utf8");
226
- } catch (error) {
227
- if (error?.code === "ENOENT") return;
228
- throw error;
229
- }
230
- let settings;
231
- try {
232
- settings = parseYaml(text) ?? {};
233
- } catch (error) {
234
- throw new Error(`cannot verify the default agent preset in ${settingsPath}: ${error.message}`);
235
- }
236
- if (settings?.["agent-presets"]?.default === presetId) {
237
- throw new Error(`refusing to uninstall the default preset ${presetId}; select another agent-presets.default first`);
238
- }
239
- }
240
-
241
- async function inspectTarget(target, presetId) {
242
- const state = await pathState(target);
243
- if (state === "missing") return { status: "absent", issues: [] };
244
- if (state !== "directory") return { status: "drifted", issues: ["target is not a regular directory"] };
245
-
246
- let manifest;
247
- try {
248
- manifest = JSON.parse(await readFile(resolve(target, MANIFEST_FILE), "utf8"));
249
- } catch (error) {
250
- return { status: "drifted", issues: [`managed manifest is unavailable: ${error.message}`] };
251
- }
252
-
253
- const issues = validateManifest(manifest, presetId);
254
- let actualFiles = {};
255
- try {
256
- actualFiles = await hashTree(target, { exclude: new Set([MANIFEST_FILE]) });
257
- } catch (error) {
258
- issues.push(error.message);
259
- }
260
-
261
- if (issues.length === 0) {
262
- const expectedFiles = manifest.files;
263
- for (const [path, hash] of Object.entries(expectedFiles)) {
264
- if (!(path in actualFiles)) issues.push(`missing managed file ${path}`);
265
- else if (actualFiles[path] !== hash) issues.push(`modified managed file ${path}`);
266
- }
267
- for (const path of Object.keys(actualFiles)) {
268
- if (!(path in expectedFiles)) issues.push(`unmanaged file ${path}`);
269
- }
270
- }
271
-
272
- return {
273
- status: issues.length === 0 ? "installed" : "drifted",
274
- issues,
275
- version: typeof manifest?.version === "string" ? manifest.version : undefined,
276
- dshVersion: typeof manifest?.dshVersion === "string" ? manifest.dshVersion : undefined,
277
- };
278
- }
279
-
280
- function validateManifest(manifest, presetId) {
281
- const issues = [];
282
- if (manifest?.schemaVersion !== 1) issues.push("unsupported managed manifest schema");
283
- if (manifest?.package !== packageMetadata.name) issues.push("managed manifest package mismatch");
284
- if (manifest?.presetId !== presetId) issues.push("managed manifest preset id mismatch");
285
- if (!manifest?.files || typeof manifest.files !== "object" || Array.isArray(manifest.files)) {
286
- issues.push("managed manifest file map is invalid");
287
- }
288
- return issues;
289
- }
290
-
291
- async function repairSessionCompatibility(dshHome, sourceRoots, confirmDshStopped, processScanner) {
292
- for (const sourceRoot of sourceRoots) {
293
- const modulePath = resolve(sourceRoot, "runtime/session-compat.mjs");
294
- if (await pathState(modulePath) !== "file") continue;
295
- const compatibility = await import(pathToFileURL(modulePath).href);
296
- if (typeof compatibility.inspectLegacySessionLogs !== "function"
297
- || typeof compatibility.repairLegacySessionLogs !== "function") {
298
- throw new Error(`agent package session compatibility module is invalid: ${modulePath}`);
299
- }
300
- const inspected = compatibility.inspectLegacySessionLogs({ dshHome });
301
- if (inspected.failures.length > 0) {
302
- throw new Error(`cannot inspect legacy Odai session evidence safely: ${inspected.failures.map((failure) => `${failure.path}: ${failure.error}`).join("; ")}`);
303
- }
304
- if (inspected.matchedEvents === 0) return inspected;
305
- if (!confirmDshStopped) {
306
- throw new Error(`found ${inspected.matchedEvents} legacy Odai session event(s); stop every DSH process and rerun with --yes before changing the Agent preset`);
307
- }
308
- const repaired = compatibility.repairLegacySessionLogs({
309
- dshHome,
310
- confirmDshStopped: true,
311
- processScanner,
312
- });
313
- if (repaired.failures.length > 0) {
314
- throw new Error(`cannot repair legacy Odai session evidence safely: ${repaired.failures.map((failure) => `${failure.path}: ${failure.error}`).join("; ")}`);
315
- }
316
- return repaired;
317
- }
318
- throw new Error("agent package is incomplete: missing runtime/session-compat.mjs");
319
- }
320
-
321
- async function assertSource(sourceRoot) {
322
- for (const path of requiredFiles) {
323
- const state = await pathState(resolve(sourceRoot, path));
324
- if (state !== "file") throw new Error(`agent package is incomplete: missing ${path}`);
325
- }
326
- }
327
-
328
- async function tightenTree(root) {
329
- await chmod(root, 0o700);
330
- const entries = await readdir(root, { withFileTypes: true });
331
- for (const entry of entries) {
332
- const path = resolve(root, entry.name);
333
- if (entry.isSymbolicLink()) throw new Error(`symbolic link is not allowed in managed preset: ${entry.name}`);
334
- if (entry.isDirectory()) await tightenTree(path);
335
- else if (entry.isFile()) await chmod(path, 0o600);
336
- else throw new Error(`unsupported filesystem entry in managed preset: ${entry.name}`);
337
- }
338
- }
339
-
340
- async function hashTree(root, options = {}) {
341
- const found = {};
342
- await walk(root, "");
343
- return Object.fromEntries(Object.entries(found).sort(([left], [right]) => left.localeCompare(right)));
344
-
345
- async function walk(base, prefix) {
346
- const entries = await readdir(base, { withFileTypes: true });
347
- entries.sort((left, right) => left.name.localeCompare(right.name));
348
- for (const entry of entries) {
349
- const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
350
- if (options.exclude?.has(relativePath)) continue;
351
- const absolutePath = resolve(base, entry.name);
352
- if (entry.isSymbolicLink()) throw new Error(`symbolic link is not allowed in managed preset: ${relativePath}`);
353
- if (entry.isDirectory()) await walk(absolutePath, relativePath);
354
- else if (entry.isFile()) found[relativePath] = await hashFile(absolutePath);
355
- else throw new Error(`unsupported filesystem entry in managed preset: ${relativePath}`);
356
- }
357
- }
358
- }
359
-
360
- async function hashFile(path) {
361
- return createHash("sha256").update(await readFile(path)).digest("hex");
362
- }
363
-
364
- async function pathState(path) {
365
- try {
366
- const stats = await lstat(path);
367
- if (stats.isSymbolicLink()) return "symlink";
368
- if (stats.isDirectory()) return "directory";
369
- if (stats.isFile()) return "file";
370
- return "other";
371
- } catch (error) {
372
- if (error?.code === "ENOENT") return "missing";
373
- throw error;
374
- }
375
- }
376
-
377
- function assertPresetId(value) {
378
- if (typeof value !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/u.test(value)) {
379
- throw new TypeError("preset id must contain only letters, digits, dots, underscores, or hyphens");
380
- }
381
- return value;
382
- }
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import {
3
+ chmod,
4
+ cp,
5
+ lstat,
6
+ mkdir,
7
+ readFile,
8
+ readdir,
9
+ rename,
10
+ rm,
11
+ writeFile,
12
+ } from "node:fs/promises";
13
+ import { homedir } from "node:os";
14
+ import { dirname, relative, resolve, sep } from "node:path";
15
+ import { fileURLToPath, pathToFileURL } from "node:url";
16
+
17
+ import { parse as parseYaml } from "yaml";
18
+
19
+ export const DEFAULT_PRESET_ID = "odai";
20
+ export const MANIFEST_FILE = ".odai-agent.json";
21
+
22
+ const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
23
+ const defaultSourceRoot = resolve(packageRoot, "preset/odai");
24
+ const packageMetadata = JSON.parse(await readFile(resolve(packageRoot, "package.json"), "utf8"));
25
+ const DSH_VERSION_PATTERN = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u;
26
+ export const SUPPORTED_DSH_VERSIONS = Object.freeze(
27
+ packageMetadata.peerDependencies["@deepseek-ai/dsh"]
28
+ .split(/\s*\|\|\s*/u)
29
+ .filter(Boolean),
30
+ );
31
+ if (SUPPORTED_DSH_VERSIONS.length === 0 || SUPPORTED_DSH_VERSIONS.some((version) => !DSH_VERSION_PATTERN.test(version))) {
32
+ throw new Error("odai-dsh-agent peer dependency must list exact supported DSH versions");
33
+ }
34
+ const SOURCE_DSH_VERSION = "0.1.0-rc.8";
35
+ const OPTIONAL_PROVIDER_COMMENTS = Object.freeze({
36
+ "0.1.0-rc.6": " # Product providers are host-plane singletons. Copy this preset, then\n # remove `disabled` from either ordinary tool row to expose that product\n # only to agents composed from the copy.",
37
+ "0.1.0-rc.7": " # Production dsh does not install these optional providers. An opting-in\n # Profile mounts each provider once on the host plane; copy this preset,\n # then remove `disabled` from the matching tool row.",
38
+ "0.1.0-rc.8": " # Production dsh does not install these optional providers. Install the\n # matching Bundle in this Profile and restart the Host, then copy this\n # preset and remove `disabled` from the matching tool row. Host availability\n # alone grants no tool.",
39
+ });
40
+ const compositionVersions = Object.keys(OPTIONAL_PROVIDER_COMMENTS);
41
+ if (JSON.stringify(SUPPORTED_DSH_VERSIONS) !== JSON.stringify(compositionVersions)) {
42
+ throw new Error(`Odai Agent peer versions ${SUPPORTED_DSH_VERSIONS.join(", ")} must exactly match composition contracts ${compositionVersions.join(", ")}`);
43
+ }
44
+ if (compositionVersions.at(-1) !== SOURCE_DSH_VERSION) {
45
+ throw new Error(`Odai Agent source composition targets ${SOURCE_DSH_VERSION}, not contract baseline ${compositionVersions.at(-1)}`);
46
+ }
47
+ export const SUPPORTED_DSH_VERSION = SOURCE_DSH_VERSION;
48
+ const requiredFiles = Object.freeze([
49
+ "agent.cordis.yml",
50
+ "preset.yml",
51
+ "runtime/index.mjs",
52
+ "runtime/session-compat.mjs",
53
+ "runtime/session-evidence.mjs",
54
+ "runtime/skill-bundle.mjs",
55
+ "runtime/skill-evolution.mjs",
56
+ "runtime/skill-selection-state.mjs",
57
+ "runtime/skill-selector.mjs",
58
+ "runtime/skill-source-config.mjs",
59
+ "skills/odai/SKILL.md",
60
+ "skills/odai/manifest.json",
61
+ ]);
62
+
63
+ function replaceCompositionContract(composition, before, after, label) {
64
+ const first = composition.indexOf(before);
65
+ if (first < 0 || composition.indexOf(before, first + before.length) >= 0) {
66
+ throw new Error(`Odai Agent source composition does not contain exactly one ${label} contract`);
67
+ }
68
+ return `${composition.slice(0, first)}${after}${composition.slice(first + before.length)}`;
69
+ }
70
+
71
+ export function renderAgentCompositionForDsh(composition, dshVersion = SUPPORTED_DSH_VERSION) {
72
+ if (typeof composition !== "string" || composition.trim() === "") {
73
+ throw new TypeError("agent composition must be a non-empty string");
74
+ }
75
+ if (!SUPPORTED_DSH_VERSIONS.includes(dshVersion)) {
76
+ throw new Error(`unsupported DSH version ${dshVersion || "<empty>"}; expected one of ${SUPPORTED_DSH_VERSIONS.join(", ")}`);
77
+ }
78
+ let rendered = composition.replace(/\r\n/gu, "\n");
79
+ if (dshVersion === SOURCE_DSH_VERSION) return rendered;
80
+ const targetComment = OPTIONAL_PROVIDER_COMMENTS[dshVersion];
81
+ if (!targetComment) throw new Error(`missing Agent composition contract for DSH ${dshVersion}`);
82
+ rendered = replaceCompositionContract(
83
+ rendered,
84
+ OPTIONAL_PROVIDER_COMMENTS[SOURCE_DSH_VERSION],
85
+ targetComment,
86
+ "optional-provider comment",
87
+ );
88
+ if (dshVersion === "0.1.0-rc.7") return rendered;
89
+ rendered = replaceCompositionContract(
90
+ rendered,
91
+ " provider: codex\n toolName: subagent_codex\n backgroundMode: one-shot",
92
+ " provider: codex\n toolName: subagent_codex\n enableRunInBackground: false",
93
+ "Codex provider",
94
+ );
95
+ return replaceCompositionContract(
96
+ rendered,
97
+ " provider: claude-code\n toolName: subagent_claude_code\n backgroundMode: one-shot",
98
+ " provider: claude-code\n toolName: subagent_claude_code\n enableRunInBackground: false",
99
+ "Claude Code provider",
100
+ );
101
+ }
102
+
103
+ export function resolveDshHome(configured, env = process.env) {
104
+ const value = configured ?? env.DSH_HOME ?? resolve(homedir(), ".dsh");
105
+ if (typeof value !== "string" || value.trim() === "") {
106
+ throw new TypeError("DSH home must be a non-empty path");
107
+ }
108
+ const trimmed = value.trim();
109
+ if (trimmed === "~") return homedir();
110
+ if (trimmed.startsWith(`~${sep}`)) return resolve(homedir(), trimmed.slice(2));
111
+ return resolve(trimmed);
112
+ }
113
+
114
+ export async function inspectAgentInstallation(options = {}) {
115
+ const presetId = assertPresetId(options.presetId ?? DEFAULT_PRESET_ID);
116
+ const dshHome = resolveDshHome(options.dshHome);
117
+ const target = resolve(dshHome, ".agent-presets", presetId);
118
+ const state = await inspectTarget(target, presetId);
119
+ return Object.freeze({ dshHome, presetId, target, ...state });
120
+ }
121
+
122
+ export async function installAgentPreset(options = {}) {
123
+ const presetId = assertPresetId(options.presetId ?? DEFAULT_PRESET_ID);
124
+ const dshHome = resolveDshHome(options.dshHome);
125
+ const sourceRoot = resolve(options.sourceRoot ?? defaultSourceRoot);
126
+ const dshVersion = options.dshVersion ?? SUPPORTED_DSH_VERSION;
127
+ if (!SUPPORTED_DSH_VERSIONS.includes(dshVersion)) {
128
+ throw new Error(`unsupported DSH version ${dshVersion || "<empty>"}; expected one of ${SUPPORTED_DSH_VERSIONS.join(", ")}`);
129
+ }
130
+ const targetRoot = resolve(dshHome, ".agent-presets");
131
+ const target = resolve(targetRoot, presetId);
132
+ const current = await inspectTarget(target, presetId);
133
+ const previousCompositionSize = current.status === "installed"
134
+ ? Buffer.byteLength(await readFile(resolve(target, "agent.cordis.yml")))
135
+ : undefined;
136
+
137
+ if (current.status === "drifted") {
138
+ throw new Error(`refusing to replace modified preset at ${target}: ${current.issues.join("; ")}`);
139
+ }
140
+
141
+ await assertSource(sourceRoot);
142
+ const sessionCompatibility = await repairSessionCompatibility(
143
+ dshHome,
144
+ [sourceRoot],
145
+ options.confirmDshStopped === true,
146
+ options.processScanner,
147
+ );
148
+ await mkdir(targetRoot, { recursive: true, mode: 0o700 });
149
+ const staging = resolve(targetRoot, `.${presetId}.tmp-${process.pid}-${randomUUID()}`);
150
+ const backup = resolve(targetRoot, `.${presetId}.backup-${process.pid}-${randomUUID()}`);
151
+ let backupCreated = false;
152
+
153
+ try {
154
+ await cp(sourceRoot, staging, { recursive: true, errorOnExist: true });
155
+ await tightenTree(staging);
156
+ const compositionPath = resolve(staging, "agent.cordis.yml");
157
+ const composition = renderAgentCompositionForDsh(await readFile(compositionPath, "utf8"), dshVersion);
158
+ let stampedComposition = `${composition.trimEnd()}\n# odai-dsh-agent generation ${packageMetadata.version}:${randomUUID()}\n`;
159
+ // Supported DSH releases key standing generations by mtime and size, so size must change even on same-tick updates.
160
+ if (previousCompositionSize === Buffer.byteLength(stampedComposition)) {
161
+ stampedComposition += "# odai-dsh-agent generation size bump\n";
162
+ }
163
+ await writeFile(compositionPath, stampedComposition, "utf8");
164
+ const files = await hashTree(staging);
165
+ const manifest = {
166
+ schemaVersion: 1,
167
+ package: packageMetadata.name,
168
+ version: packageMetadata.version,
169
+ dshVersion,
170
+ presetId,
171
+ files,
172
+ };
173
+ const manifestPath = resolve(staging, MANIFEST_FILE);
174
+ await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
175
+ await chmod(manifestPath, 0o600);
176
+
177
+ if (current.status === "installed") {
178
+ await rename(target, backup);
179
+ backupCreated = true;
180
+ }
181
+ await rename(staging, target);
182
+ if (backupCreated) {
183
+ await rm(backup, { recursive: true, force: true });
184
+ backupCreated = false;
185
+ }
186
+
187
+ return Object.freeze({
188
+ operation: current.status === "installed" ? "updated" : "installed",
189
+ dshHome,
190
+ presetId,
191
+ target,
192
+ version: packageMetadata.version,
193
+ dshVersion,
194
+ trust: "user",
195
+ security: "DSH user presets have the same privileges as shell access; install only reviewed preset code.",
196
+ sessionCompatibility,
197
+ });
198
+ } catch (error) {
199
+ await rm(staging, { recursive: true, force: true });
200
+ if (backupCreated) {
201
+ const targetState = await pathState(target);
202
+ if (targetState === "missing") await rename(backup, target);
203
+ }
204
+ throw error;
205
+ } finally {
206
+ await rm(backup, { recursive: true, force: true });
207
+ }
208
+ }
209
+
210
+ export async function uninstallAgentPreset(options = {}) {
211
+ const inspected = await inspectAgentInstallation(options);
212
+ if (inspected.status === "absent") {
213
+ return Object.freeze({ operation: "absent", target: inspected.target, presetId: inspected.presetId });
214
+ }
215
+ if (inspected.status === "drifted") {
216
+ throw new Error(`refusing to remove modified preset at ${inspected.target}: ${inspected.issues.join("; ")}`);
217
+ }
218
+ await assertPresetIsNotDefault(inspected.dshHome, inspected.presetId);
219
+ const sessionCompatibility = await repairSessionCompatibility(
220
+ inspected.dshHome,
221
+ [defaultSourceRoot, inspected.target],
222
+ options.confirmDshStopped === true,
223
+ options.processScanner,
224
+ );
225
+ if (sessionCompatibility.failures.length > 0) {
226
+ throw new Error(`refusing to uninstall before legacy Odai session evidence is made ignorable: ${sessionCompatibility.failures.map((failure) => `${failure.path}: ${failure.error}`).join("; ")}`);
227
+ }
228
+ await rm(inspected.target, { recursive: true, force: true });
229
+ return Object.freeze({
230
+ operation: "uninstalled",
231
+ target: inspected.target,
232
+ presetId: inspected.presetId,
233
+ sessionCompatibility,
234
+ });
235
+ }
236
+
237
+ async function assertPresetIsNotDefault(dshHome, presetId) {
238
+ const settingsPath = resolve(dshHome, "settings.yaml");
239
+ let text;
240
+ try {
241
+ text = await readFile(settingsPath, "utf8");
242
+ } catch (error) {
243
+ if (error?.code === "ENOENT") return;
244
+ throw error;
245
+ }
246
+ let settings;
247
+ try {
248
+ settings = parseYaml(text) ?? {};
249
+ } catch (error) {
250
+ throw new Error(`cannot verify the default agent preset in ${settingsPath}: ${error.message}`);
251
+ }
252
+ if (settings?.["agent-presets"]?.default === presetId) {
253
+ throw new Error(`refusing to uninstall the default preset ${presetId}; select another agent-presets.default first`);
254
+ }
255
+ }
256
+
257
+ async function inspectTarget(target, presetId) {
258
+ const state = await pathState(target);
259
+ if (state === "missing") return { status: "absent", issues: [] };
260
+ if (state !== "directory") return { status: "drifted", issues: ["target is not a regular directory"] };
261
+
262
+ let manifest;
263
+ try {
264
+ manifest = JSON.parse(await readFile(resolve(target, MANIFEST_FILE), "utf8"));
265
+ } catch (error) {
266
+ return { status: "drifted", issues: [`managed manifest is unavailable: ${error.message}`] };
267
+ }
268
+
269
+ const issues = validateManifest(manifest, presetId);
270
+ let actualFiles = {};
271
+ try {
272
+ actualFiles = await hashTree(target, { exclude: new Set([MANIFEST_FILE]) });
273
+ } catch (error) {
274
+ issues.push(error.message);
275
+ }
276
+
277
+ if (issues.length === 0) {
278
+ const expectedFiles = manifest.files;
279
+ for (const [path, hash] of Object.entries(expectedFiles)) {
280
+ if (!(path in actualFiles)) issues.push(`missing managed file ${path}`);
281
+ else if (actualFiles[path] !== hash) issues.push(`modified managed file ${path}`);
282
+ }
283
+ for (const path of Object.keys(actualFiles)) {
284
+ if (!(path in expectedFiles)) issues.push(`unmanaged file ${path}`);
285
+ }
286
+ }
287
+
288
+ return {
289
+ status: issues.length === 0 ? "installed" : "drifted",
290
+ issues,
291
+ version: typeof manifest?.version === "string" ? manifest.version : undefined,
292
+ dshVersion: typeof manifest?.dshVersion === "string" ? manifest.dshVersion : undefined,
293
+ };
294
+ }
295
+
296
+ function validateManifest(manifest, presetId) {
297
+ const issues = [];
298
+ if (manifest?.schemaVersion !== 1) issues.push("unsupported managed manifest schema");
299
+ if (manifest?.package !== packageMetadata.name) issues.push("managed manifest package mismatch");
300
+ if (manifest?.presetId !== presetId) issues.push("managed manifest preset id mismatch");
301
+ if (!manifest?.files || typeof manifest.files !== "object" || Array.isArray(manifest.files)) {
302
+ issues.push("managed manifest file map is invalid");
303
+ }
304
+ return issues;
305
+ }
306
+
307
+ async function repairSessionCompatibility(dshHome, sourceRoots, confirmDshStopped, processScanner) {
308
+ for (const sourceRoot of sourceRoots) {
309
+ const modulePath = resolve(sourceRoot, "runtime/session-compat.mjs");
310
+ if (await pathState(modulePath) !== "file") continue;
311
+ const compatibility = await import(pathToFileURL(modulePath).href);
312
+ if (typeof compatibility.inspectLegacySessionLogs !== "function"
313
+ || typeof compatibility.repairLegacySessionLogs !== "function") {
314
+ throw new Error(`agent package session compatibility module is invalid: ${modulePath}`);
315
+ }
316
+ const inspected = compatibility.inspectLegacySessionLogs({ dshHome });
317
+ if (inspected.failures.length > 0) {
318
+ throw new Error(`cannot inspect legacy Odai session evidence safely: ${inspected.failures.map((failure) => `${failure.path}: ${failure.error}`).join("; ")}`);
319
+ }
320
+ if (inspected.matchedEvents === 0) return inspected;
321
+ if (!confirmDshStopped) {
322
+ throw new Error(`found ${inspected.matchedEvents} legacy Odai session event(s); stop every DSH process and rerun with --yes before changing the Agent preset`);
323
+ }
324
+ const repaired = compatibility.repairLegacySessionLogs({
325
+ dshHome,
326
+ confirmDshStopped: true,
327
+ processScanner,
328
+ });
329
+ if (repaired.failures.length > 0) {
330
+ throw new Error(`cannot repair legacy Odai session evidence safely: ${repaired.failures.map((failure) => `${failure.path}: ${failure.error}`).join("; ")}`);
331
+ }
332
+ return repaired;
333
+ }
334
+ throw new Error("agent package is incomplete: missing runtime/session-compat.mjs");
335
+ }
336
+
337
+ async function assertSource(sourceRoot) {
338
+ for (const path of requiredFiles) {
339
+ const state = await pathState(resolve(sourceRoot, path));
340
+ if (state !== "file") throw new Error(`agent package is incomplete: missing ${path}`);
341
+ }
342
+ }
343
+
344
+ async function tightenTree(root) {
345
+ await chmod(root, 0o700);
346
+ const entries = await readdir(root, { withFileTypes: true });
347
+ for (const entry of entries) {
348
+ const path = resolve(root, entry.name);
349
+ if (entry.isSymbolicLink()) throw new Error(`symbolic link is not allowed in managed preset: ${entry.name}`);
350
+ if (entry.isDirectory()) await tightenTree(path);
351
+ else if (entry.isFile()) await chmod(path, 0o600);
352
+ else throw new Error(`unsupported filesystem entry in managed preset: ${entry.name}`);
353
+ }
354
+ }
355
+
356
+ async function hashTree(root, options = {}) {
357
+ const found = {};
358
+ await walk(root, "");
359
+ return Object.fromEntries(Object.entries(found).sort(([left], [right]) => left.localeCompare(right)));
360
+
361
+ async function walk(base, prefix) {
362
+ const entries = await readdir(base, { withFileTypes: true });
363
+ entries.sort((left, right) => left.name.localeCompare(right.name));
364
+ for (const entry of entries) {
365
+ const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
366
+ if (options.exclude?.has(relativePath)) continue;
367
+ const absolutePath = resolve(base, entry.name);
368
+ if (entry.isSymbolicLink()) throw new Error(`symbolic link is not allowed in managed preset: ${relativePath}`);
369
+ if (entry.isDirectory()) await walk(absolutePath, relativePath);
370
+ else if (entry.isFile()) found[relativePath] = await hashFile(absolutePath);
371
+ else throw new Error(`unsupported filesystem entry in managed preset: ${relativePath}`);
372
+ }
373
+ }
374
+ }
375
+
376
+ async function hashFile(path) {
377
+ return createHash("sha256").update(await readFile(path)).digest("hex");
378
+ }
379
+
380
+ async function pathState(path) {
381
+ try {
382
+ const stats = await lstat(path);
383
+ if (stats.isSymbolicLink()) return "symlink";
384
+ if (stats.isDirectory()) return "directory";
385
+ if (stats.isFile()) return "file";
386
+ return "other";
387
+ } catch (error) {
388
+ if (error?.code === "ENOENT") return "missing";
389
+ throw error;
390
+ }
391
+ }
392
+
393
+ function assertPresetId(value) {
394
+ if (typeof value !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/u.test(value)) {
395
+ throw new TypeError("preset id must contain only letters, digits, dots, underscores, or hyphens");
396
+ }
397
+ return value;
398
+ }