odai-dsh-agent 0.2.0 → 0.2.2
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 -21
- package/README.md +138 -134
- package/bin/odai-dsh-agent.mjs +108 -108
- package/package.json +69 -69
- package/preset/odai/agent.cordis.yml +260 -260
- package/preset/odai/preset.yml +3 -3
- package/preset/odai/runtime/compaction-config.mjs +388 -388
- package/preset/odai/runtime/context-activation.mjs +71 -0
- package/preset/odai/runtime/context-capability.mjs +86 -0
- package/preset/odai/runtime/governance.mjs +128 -121
- package/preset/odai/runtime/human-care.mjs +45 -0
- package/preset/odai/runtime/human-safety-continuity.mjs +2 -2
- package/preset/odai/runtime/human-safety.mjs +3 -3
- package/preset/odai/runtime/index.mjs +2461 -1977
- package/preset/odai/runtime/output-config.mjs +360 -304
- package/preset/odai/runtime/research-packet.mjs +150 -150
- package/preset/odai/runtime/responsibility-gap.mjs +8 -10
- package/preset/odai/runtime/responsibility-scope.mjs +199 -0
- package/preset/odai/runtime/role-overlays.mjs +27 -27
- package/preset/odai/runtime/route-card.mjs +168 -152
- package/preset/odai/runtime/router.mjs +683 -651
- package/preset/odai/runtime/routing-config.mjs +480 -436
- package/preset/odai/runtime/routing-context.mjs +321 -321
- package/preset/odai/runtime/semantic-memory-store.mjs +452 -452
- package/preset/odai/runtime/semantic-memory.mjs +842 -842
- package/preset/odai/runtime/session-compat.mjs +389 -389
- package/preset/odai/runtime/skill-bundle.mjs +318 -318
- package/preset/odai/runtime/skill-evolution.mjs +1413 -1413
- package/preset/odai/skills/odai/SKILL.md +69 -68
- package/preset/odai/skills/odai/agents/openai.yaml +4 -4
- package/preset/odai/skills/odai/assets/claude-agents/agent.md +11 -11
- package/preset/odai/skills/odai/assets/codex-agents/config.toml +11 -11
- package/preset/odai/skills/odai/assets/codex-agents/role.toml +5 -5
- package/preset/odai/skills/odai/assets/copilot-agents/agent.md +13 -13
- package/preset/odai/skills/odai/assets/hooks-policy.example.json +24 -24
- package/preset/odai/skills/odai/assets/routing-roles/controller.md +15 -15
- package/preset/odai/skills/odai/assets/routing-roles/executor.md +11 -11
- package/preset/odai/skills/odai/assets/routing-roles/frontend.md +7 -7
- package/preset/odai/skills/odai/assets/routing-roles/planner.md +29 -29
- package/preset/odai/skills/odai/assets/routing-roles/researcher.md +6 -6
- package/preset/odai/skills/odai/assets/routing-roles/reviewer.md +11 -11
- package/preset/odai/skills/odai/assets/task-state.md +14 -14
- package/preset/odai/skills/odai/manifest.json +37 -36
- package/preset/odai/skills/odai/references/care.md +26 -0
- package/preset/odai/skills/odai/references/craft.md +54 -54
- package/preset/odai/skills/odai/references/dao.md +30 -30
- package/preset/odai/skills/odai/references/human-safety.md +33 -54
- package/preset/odai/skills/odai/references/leverage.md +53 -53
- package/preset/odai/skills/odai/references/planning.md +82 -82
- package/preset/odai/skills/odai/references/support.md +36 -36
- package/preset/odai/skills/odai/references/verification.md +23 -23
- package/preset/odai/skills/odai/scripts/build-hooks.mjs +273 -273
- package/preset/odai/skills/odai/scripts/build-routing.mjs +274 -274
- package/preset/odai/skills/odai/scripts/install-routing.mjs +397 -397
- package/preset/odai/skills/odai/scripts/odai-hook.mjs +457 -457
- package/preset/odai/skills/odai/scripts/run-role.mjs +336 -336
- package/preset/odai/skills/odai/scripts/run-routing.mjs +1154 -1154
- package/preset/odai/skills/odai/scripts/verify-routing.mjs +103 -103
- package/scripts/verify-agent-load.mjs +257 -257
- package/src/dsh-version.mjs +6 -2
- package/src/installer.mjs +382 -382
|
@@ -1,257 +1,257 @@
|
|
|
1
|
-
import { cp, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
-
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
3
|
-
import { createRequire } from "node:module";
|
|
4
|
-
import { createServer } from "node:net";
|
|
5
|
-
import { tmpdir } from "node:os";
|
|
6
|
-
import { dirname, resolve } from "node:path";
|
|
7
|
-
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
8
|
-
import { execFileSync } from "node:child_process";
|
|
9
|
-
import { randomUUID } from "node:crypto";
|
|
10
|
-
|
|
11
|
-
import { spawnDsh } from "../src/dsh-version.mjs";
|
|
12
|
-
import {
|
|
13
|
-
installAgentPreset,
|
|
14
|
-
renderAgentCompositionForDsh,
|
|
15
|
-
SUPPORTED_DSH_VERSIONS,
|
|
16
|
-
} from "../src/installer.mjs";
|
|
17
|
-
|
|
18
|
-
const agentRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
19
|
-
const repoRoot = resolve(agentRoot, "../..");
|
|
20
|
-
const dsh = process.env.DSH_BIN ?? "dsh";
|
|
21
|
-
const dshRoot = process.env.DSH_PACKAGE_ROOT
|
|
22
|
-
? resolve(process.env.DSH_PACKAGE_ROOT)
|
|
23
|
-
: findDshPackageRoot(dsh);
|
|
24
|
-
const requireFromDsh = createRequire(resolve(dshRoot, "package.json"));
|
|
25
|
-
const scopeModule = requireFromDsh.resolve("@deepseek-ai/dsh-scope");
|
|
26
|
-
const targetDshVersion = await verifyPinnedComposition();
|
|
27
|
-
const scratch = await mkdtemp(resolve(tmpdir(), "odai-agent-scope-"));
|
|
28
|
-
const home = resolve(scratch, "home");
|
|
29
|
-
const workspace = resolve(scratch, "workspace");
|
|
30
|
-
const sourceRoot = resolve(scratch, "source-preset");
|
|
31
|
-
const markerPath = resolve(scratch, "scope-results.json");
|
|
32
|
-
const probePluginPath = resolve(scratch, "scope-probe-plugin.mjs");
|
|
33
|
-
const patchPath = resolve(scratch, "scope-probe.patch.yml");
|
|
34
|
-
|
|
35
|
-
const yaml = (value) => JSON.stringify(value);
|
|
36
|
-
|
|
37
|
-
function findDshPackageRoot(command) {
|
|
38
|
-
const locator = process.platform === "win32" ? "where" : "which";
|
|
39
|
-
const located = existsSync(command)
|
|
40
|
-
? [resolve(command)]
|
|
41
|
-
: execFileSync(locator, [command], { encoding: "utf8" })
|
|
42
|
-
.trim()
|
|
43
|
-
.split(/\r?\n/u)
|
|
44
|
-
.filter(Boolean);
|
|
45
|
-
const candidates = new Set();
|
|
46
|
-
for (const path of located) {
|
|
47
|
-
const commandDir = dirname(realpathSync(path));
|
|
48
|
-
candidates.add(commandDir);
|
|
49
|
-
candidates.add(resolve(commandDir, "node_modules/@deepseek-ai/dsh"));
|
|
50
|
-
candidates.add(resolve(commandDir, "../@deepseek-ai/dsh"));
|
|
51
|
-
}
|
|
52
|
-
for (const candidate of candidates) {
|
|
53
|
-
let current = candidate;
|
|
54
|
-
for (;;) {
|
|
55
|
-
try {
|
|
56
|
-
const metadata = JSON.parse(readFileSync(resolve(current, "package.json"), "utf8"));
|
|
57
|
-
if (metadata.name === "@deepseek-ai/dsh") return current;
|
|
58
|
-
} catch (error) {
|
|
59
|
-
if (error?.code !== "ENOENT") throw error;
|
|
60
|
-
}
|
|
61
|
-
const parent = dirname(current);
|
|
62
|
-
if (parent === current) break;
|
|
63
|
-
current = parent;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
throw new Error(`cannot locate the @deepseek-ai/dsh package behind ${command}`);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
async function verifyPinnedComposition() {
|
|
70
|
-
const dshMetadata = JSON.parse(await readFile(resolve(dshRoot, "package.json"), "utf8"));
|
|
71
|
-
if (!SUPPORTED_DSH_VERSIONS.includes(dshMetadata.version)) {
|
|
72
|
-
throw new Error(`agent preset expects one of ${SUPPORTED_DSH_VERSIONS.join(", ")}, found ${dshMetadata.version}`);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const standard = (await readFile(resolve(dshRoot, "config/agent-presets/standard/agent.cordis.yml"), "utf8"))
|
|
76
|
-
.replace(/\r\n/gu, "\n");
|
|
77
|
-
const odaiSuffix = [
|
|
78
|
-
"# Odai contributes scoped prompt, guard, routing, user-owned responsibility",
|
|
79
|
-
"# mappings, and evidence listeners. Base controller selection stays host-owned.",
|
|
80
|
-
"- id: odai-governance",
|
|
81
|
-
" name: ./runtime/index.mjs",
|
|
82
|
-
" config:",
|
|
83
|
-
" routing:",
|
|
84
|
-
" mode: auto",
|
|
85
|
-
" provider: spawn",
|
|
86
|
-
].join("\n");
|
|
87
|
-
const expected = `${standard
|
|
88
|
-
.replace(
|
|
89
|
-
"# The preset's own persona, shadowing the deployment default for this agent.\n# `{{model}}` and `{{cwd}}` resolve from the agent's own route and workspace.",
|
|
90
|
-
"# The preset's own model-neutral persona shadows the deployment default. Auto\n# routing can upgrade after prompt assembly; `{{cwd}}` remains workspace-local.",
|
|
91
|
-
)
|
|
92
|
-
.replace(
|
|
93
|
-
"You are a coding agent powered by the {{model}} model. Your working directory is {{cwd}}.",
|
|
94
|
-
"You are Odai, a coding agent. Your working directory is {{cwd}}.",
|
|
95
|
-
)
|
|
96
|
-
.trimEnd()}\n\n${odaiSuffix}`;
|
|
97
|
-
const actual = renderAgentCompositionForDsh(
|
|
98
|
-
await readFile(resolve(agentRoot, "preset/odai/agent.cordis.yml"), "utf8"),
|
|
99
|
-
dshMetadata.version,
|
|
100
|
-
).trimEnd();
|
|
101
|
-
if (actual !== expected) {
|
|
102
|
-
throw new Error(`Odai Agent composition drifted from the DSH ${dshMetadata.version} standard preset`);
|
|
103
|
-
}
|
|
104
|
-
return dshMetadata.version;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
async function freePort() {
|
|
108
|
-
return await new Promise((accept, reject) => {
|
|
109
|
-
const server = createServer();
|
|
110
|
-
server.once("error", reject);
|
|
111
|
-
server.listen(0, "127.0.0.1", () => {
|
|
112
|
-
const address = server.address();
|
|
113
|
-
const port = typeof address === "object" && address ? address.port : undefined;
|
|
114
|
-
server.close((error) => error ? reject(error) : accept(port));
|
|
115
|
-
});
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
async function rpc(baseUrl, method, payload) {
|
|
120
|
-
const rpcId = randomUUID();
|
|
121
|
-
const response = await fetch(`${baseUrl}/api/${method}`, {
|
|
122
|
-
method: "POST",
|
|
123
|
-
headers: { "content-type": "application/json" },
|
|
124
|
-
body: JSON.stringify({ type: "client-request", rpcId, method, payload }),
|
|
125
|
-
});
|
|
126
|
-
const text = await response.text();
|
|
127
|
-
if (!response.ok) throw new Error(`${method} HTTP ${response.status}: ${text}`);
|
|
128
|
-
const body = JSON.parse(text);
|
|
129
|
-
if (body.result?.ok !== true) throw new Error(`${method} failed: ${text}`);
|
|
130
|
-
return body.result.value;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
async function waitForServer(baseUrl, child, output) {
|
|
134
|
-
const deadline = Date.now() + 20_000;
|
|
135
|
-
while (Date.now() < deadline) {
|
|
136
|
-
if (child.exitCode !== null) throw new Error(`dsh web exited early (${child.exitCode})\n${output()}`);
|
|
137
|
-
try {
|
|
138
|
-
await rpc(baseUrl, "agentPreset.list", {});
|
|
139
|
-
return;
|
|
140
|
-
} catch {
|
|
141
|
-
await new Promise((accept) => setTimeout(accept, 75));
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
throw new Error(`timed out waiting for dsh web\n${output()}`);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
async function waitForMarker(child, output) {
|
|
148
|
-
const deadline = Date.now() + 15_000;
|
|
149
|
-
while (Date.now() < deadline) {
|
|
150
|
-
if (existsSync(markerPath)) return JSON.parse(await readFile(markerPath, "utf8"));
|
|
151
|
-
if (child.exitCode !== null) throw new Error(`dsh web exited before probe completed (${child.exitCode})\n${output()}`);
|
|
152
|
-
await new Promise((accept) => setTimeout(accept, 50));
|
|
153
|
-
}
|
|
154
|
-
throw new Error(`timed out waiting for scope marker\n${output()}`);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
async function terminateChild(child) {
|
|
158
|
-
if (process.platform === "win32" && child.pid) {
|
|
159
|
-
try {
|
|
160
|
-
execFileSync("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore" });
|
|
161
|
-
} catch {
|
|
162
|
-
if (child.exitCode === null) child.kill("SIGTERM");
|
|
163
|
-
}
|
|
164
|
-
} else if (child.exitCode === null) {
|
|
165
|
-
child.kill("SIGTERM");
|
|
166
|
-
}
|
|
167
|
-
await new Promise((accept) => {
|
|
168
|
-
if (child.exitCode !== null) return accept();
|
|
169
|
-
const timeout = setTimeout(() => { child.kill("SIGKILL"); accept(); }, 3_000);
|
|
170
|
-
child.once("exit", () => { clearTimeout(timeout); accept(); });
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
await mkdir(workspace, { recursive: true });
|
|
175
|
-
await cp(resolve(agentRoot, "preset/odai"), sourceRoot, { recursive: true });
|
|
176
|
-
const developmentRuntime = resolve(repoRoot, "dsh/runtime/src");
|
|
177
|
-
const developmentSkill = resolve(repoRoot, "skills/odai");
|
|
178
|
-
if (existsSync(developmentRuntime) && existsSync(developmentSkill)) {
|
|
179
|
-
await Promise.all([
|
|
180
|
-
cp(developmentRuntime, resolve(sourceRoot, "runtime"), { recursive: true }),
|
|
181
|
-
cp(developmentSkill, resolve(sourceRoot, "skills/odai"), { recursive: true }),
|
|
182
|
-
]);
|
|
183
|
-
} else if (!existsSync(resolve(sourceRoot, "runtime/index.mjs"))
|
|
184
|
-
|| !existsSync(resolve(sourceRoot, "skills/odai/SKILL.md"))) {
|
|
185
|
-
throw new Error("Odai Agent verification requires either repository sources or packaged runtime and skill files");
|
|
186
|
-
}
|
|
187
|
-
await installAgentPreset({ dshHome: home, sourceRoot, dshVersion: targetDshVersion });
|
|
188
|
-
|
|
189
|
-
const probePlugin = `import { existsSync, writeFileSync } from "node:fs";\nimport { bindScopeParent } from ${JSON.stringify(pathToFileURL(scopeModule).href)};\n\nexport const name = "odai-agent-scope-probe";\nexport const inject = ["systemPrompt", "tools"];\n\nexport function apply(ctx, config) {\n const results = {};\n let writing = Promise.resolve();\n\n ctx.on("agent/created", ({ agent }) => {\n writing = writing.then(async () => {\n const preset = agent.session?.header?.agentPreset;\n if (preset !== "standard" && preset !== "odai") return;\n\n const assembly = await ctx.systemPrompt.assemble({ scope: agent });\n const canonicalSections = assembly.sections.filter((section) => section.name === "odai:canonical-governance");\n const writePath = preset === "odai" ? config.odaiWritePath : config.standardWritePath;\n const childSession = new Proxy(agent.session, {\n get(target, property) {\n if (property === "header") {\n return { ...target.header, origin: "subagent", delegationDepth: 1 };\n }\n return Reflect.get(target, property, target);\n },\n });\n const child = { id: agent.id, session: childSession };\n bindScopeParent(child, agent);\n const toolResult = await ctx.tools.execute({\n callId: \`scope-probe-\${preset}\`,\n name: "write",\n arguments: { file_path: writePath, content: \`\${preset} child write reached body\\n\` },\n agent: child,\n signal: new AbortController().signal,\n });\n\n let routingProtected;\n if (preset === "odai") {\n const routingResult = await ctx.tools.execute({\n callId: "scope-probe-odai-routing-config",\n name: "odai_routing_config",\n arguments: { action: "set", responsibility: "executor", provider: "probe-provider", model: "probe-executor", reasoningEffort: "high" },\n agent,\n signal: new AbortController().signal,\n });\n routingProtected = routingResult.isError === true\n && /NO_ADAPTER/u.test(routingResult.error?.message ?? "")\n && !existsSync(config.routingConfigPath);\n }\n\n results[preset] = {\n canonicalSectionCount: canonicalSections.length,\n toolIsError: toolResult.isError === true,\n toolError: toolResult.isError === true ? toolResult.error?.message : undefined,\n writeReachedBody: existsSync(writePath),\n ...(routingProtected === undefined ? {} : { routingProtected }),\n };\n if (results.standard && results.odai) {\n writeFileSync(config.markerPath, JSON.stringify(results, null, 2) + "\\n", "utf8");\n }\n }).catch((error) => {\n writeFileSync(config.markerPath, JSON.stringify({ probeError: error?.stack ?? String(error) }, null, 2) + "\\n", "utf8");\n });\n }, { global: true });\n}\n`;
|
|
190
|
-
await writeFile(probePluginPath, probePlugin, "utf8");
|
|
191
|
-
await writeFile(patchPath, [
|
|
192
|
-
"- insert:",
|
|
193
|
-
" - id: odai-agent-scope-probe",
|
|
194
|
-
` name: ${yaml(pathToFileURL(probePluginPath).href)}`,
|
|
195
|
-
" config:",
|
|
196
|
-
` markerPath: ${yaml(markerPath)}`,
|
|
197
|
-
` standardWritePath: ${yaml(resolve(workspace, "standard-child-write.txt"))}`,
|
|
198
|
-
` odaiWritePath: ${yaml(resolve(workspace, "odai-child-write.txt"))}`,
|
|
199
|
-
` routingConfigPath: ${yaml(resolve(home, "odai/routing.json"))}`,
|
|
200
|
-
"",
|
|
201
|
-
].join("\n"), "utf8");
|
|
202
|
-
|
|
203
|
-
const port = await freePort();
|
|
204
|
-
const baseUrl = `http://127.0.0.1:${port}`;
|
|
205
|
-
const child = spawnDsh(dsh, [
|
|
206
|
-
"--profile", "web",
|
|
207
|
-
"--patch", patchPath,
|
|
208
|
-
"--host", "127.0.0.1",
|
|
209
|
-
"--port", String(port),
|
|
210
|
-
], {
|
|
211
|
-
cwd: workspace,
|
|
212
|
-
env: {
|
|
213
|
-
...process.env,
|
|
214
|
-
DSH_HOME: home,
|
|
215
|
-
DSH_TELEMETRY_MODE: "DISABLED",
|
|
216
|
-
},
|
|
217
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
218
|
-
});
|
|
219
|
-
let output = "";
|
|
220
|
-
child.stdout.on("data", (chunk) => { output += chunk.toString(); });
|
|
221
|
-
child.stderr.on("data", (chunk) => { output += chunk.toString(); });
|
|
222
|
-
const capturedOutput = () => output;
|
|
223
|
-
|
|
224
|
-
try {
|
|
225
|
-
await waitForServer(baseUrl, child, capturedOutput);
|
|
226
|
-
const roster = await rpc(baseUrl, "agentPreset.list", {});
|
|
227
|
-
const ids = roster.presets.map((preset) => preset.id);
|
|
228
|
-
if (!ids.includes("standard") || !ids.includes("odai")) {
|
|
229
|
-
throw new Error(`expected standard and odai presets, got ${JSON.stringify(ids)}`);
|
|
230
|
-
}
|
|
231
|
-
await rpc(baseUrl, "session.create", { cwd: workspace, agentPreset: "standard" });
|
|
232
|
-
await rpc(baseUrl, "session.create", { cwd: workspace, agentPreset: "odai" });
|
|
233
|
-
const results = await waitForMarker(child, capturedOutput);
|
|
234
|
-
if (results.probeError) throw new Error(results.probeError);
|
|
235
|
-
if (results.standard?.canonicalSectionCount !== 0) {
|
|
236
|
-
throw new Error(`standard saw odai canonical prompt: ${JSON.stringify(results)}`);
|
|
237
|
-
}
|
|
238
|
-
if (results.standard?.toolError?.startsWith("ODAI_SUBAGENT_BOUNDARY:")) {
|
|
239
|
-
throw new Error(`standard child call reached the odai guard: ${JSON.stringify(results)}`);
|
|
240
|
-
}
|
|
241
|
-
if (results.odai?.canonicalSectionCount !== 1) {
|
|
242
|
-
throw new Error(`odai canonical prompt count was not exactly one: ${JSON.stringify(results)}`);
|
|
243
|
-
}
|
|
244
|
-
if (results.odai?.writeReachedBody !== false || results.odai?.toolIsError !== true
|
|
245
|
-
|| !results.odai?.toolError?.startsWith("ODAI_SUBAGENT_BOUNDARY:")) {
|
|
246
|
-
throw new Error(`odai child write was not denied by odai guard: ${JSON.stringify(results)}`);
|
|
247
|
-
}
|
|
248
|
-
if (results.odai?.routingProtected !== true) {
|
|
249
|
-
throw new Error(`odai routing tool did not reject an unavailable route without mutation: ${JSON.stringify(results)}`);
|
|
250
|
-
}
|
|
251
|
-
process.stdout.write(`${JSON.stringify({ scratch, roster: ids, results }, null, 2)}\n`);
|
|
252
|
-
} finally {
|
|
253
|
-
await terminateChild(child);
|
|
254
|
-
if (process.env.KEEP_ODAI_SCOPE_PROBE !== "1") {
|
|
255
|
-
await rm(scratch, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
|
|
256
|
-
}
|
|
257
|
-
}
|
|
1
|
+
import { cp, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { createServer } from "node:net";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { dirname, resolve } from "node:path";
|
|
7
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
8
|
+
import { execFileSync } from "node:child_process";
|
|
9
|
+
import { randomUUID } from "node:crypto";
|
|
10
|
+
|
|
11
|
+
import { spawnDsh } from "../src/dsh-version.mjs";
|
|
12
|
+
import {
|
|
13
|
+
installAgentPreset,
|
|
14
|
+
renderAgentCompositionForDsh,
|
|
15
|
+
SUPPORTED_DSH_VERSIONS,
|
|
16
|
+
} from "../src/installer.mjs";
|
|
17
|
+
|
|
18
|
+
const agentRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
19
|
+
const repoRoot = resolve(agentRoot, "../..");
|
|
20
|
+
const dsh = process.env.DSH_BIN ?? "dsh";
|
|
21
|
+
const dshRoot = process.env.DSH_PACKAGE_ROOT
|
|
22
|
+
? resolve(process.env.DSH_PACKAGE_ROOT)
|
|
23
|
+
: findDshPackageRoot(dsh);
|
|
24
|
+
const requireFromDsh = createRequire(resolve(dshRoot, "package.json"));
|
|
25
|
+
const scopeModule = requireFromDsh.resolve("@deepseek-ai/dsh-scope");
|
|
26
|
+
const targetDshVersion = await verifyPinnedComposition();
|
|
27
|
+
const scratch = await mkdtemp(resolve(tmpdir(), "odai-agent-scope-"));
|
|
28
|
+
const home = resolve(scratch, "home");
|
|
29
|
+
const workspace = resolve(scratch, "workspace");
|
|
30
|
+
const sourceRoot = resolve(scratch, "source-preset");
|
|
31
|
+
const markerPath = resolve(scratch, "scope-results.json");
|
|
32
|
+
const probePluginPath = resolve(scratch, "scope-probe-plugin.mjs");
|
|
33
|
+
const patchPath = resolve(scratch, "scope-probe.patch.yml");
|
|
34
|
+
|
|
35
|
+
const yaml = (value) => JSON.stringify(value);
|
|
36
|
+
|
|
37
|
+
function findDshPackageRoot(command) {
|
|
38
|
+
const locator = process.platform === "win32" ? "where" : "which";
|
|
39
|
+
const located = existsSync(command)
|
|
40
|
+
? [resolve(command)]
|
|
41
|
+
: execFileSync(locator, [command], { encoding: "utf8" })
|
|
42
|
+
.trim()
|
|
43
|
+
.split(/\r?\n/u)
|
|
44
|
+
.filter(Boolean);
|
|
45
|
+
const candidates = new Set();
|
|
46
|
+
for (const path of located) {
|
|
47
|
+
const commandDir = dirname(realpathSync(path));
|
|
48
|
+
candidates.add(commandDir);
|
|
49
|
+
candidates.add(resolve(commandDir, "node_modules/@deepseek-ai/dsh"));
|
|
50
|
+
candidates.add(resolve(commandDir, "../@deepseek-ai/dsh"));
|
|
51
|
+
}
|
|
52
|
+
for (const candidate of candidates) {
|
|
53
|
+
let current = candidate;
|
|
54
|
+
for (;;) {
|
|
55
|
+
try {
|
|
56
|
+
const metadata = JSON.parse(readFileSync(resolve(current, "package.json"), "utf8"));
|
|
57
|
+
if (metadata.name === "@deepseek-ai/dsh") return current;
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (error?.code !== "ENOENT") throw error;
|
|
60
|
+
}
|
|
61
|
+
const parent = dirname(current);
|
|
62
|
+
if (parent === current) break;
|
|
63
|
+
current = parent;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
throw new Error(`cannot locate the @deepseek-ai/dsh package behind ${command}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function verifyPinnedComposition() {
|
|
70
|
+
const dshMetadata = JSON.parse(await readFile(resolve(dshRoot, "package.json"), "utf8"));
|
|
71
|
+
if (!SUPPORTED_DSH_VERSIONS.includes(dshMetadata.version)) {
|
|
72
|
+
throw new Error(`agent preset expects one of ${SUPPORTED_DSH_VERSIONS.join(", ")}, found ${dshMetadata.version}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const standard = (await readFile(resolve(dshRoot, "config/agent-presets/standard/agent.cordis.yml"), "utf8"))
|
|
76
|
+
.replace(/\r\n/gu, "\n");
|
|
77
|
+
const odaiSuffix = [
|
|
78
|
+
"# Odai contributes scoped prompt, guard, routing, user-owned responsibility",
|
|
79
|
+
"# mappings, and evidence listeners. Base controller selection stays host-owned.",
|
|
80
|
+
"- id: odai-governance",
|
|
81
|
+
" name: ./runtime/index.mjs",
|
|
82
|
+
" config:",
|
|
83
|
+
" routing:",
|
|
84
|
+
" mode: auto",
|
|
85
|
+
" provider: spawn",
|
|
86
|
+
].join("\n");
|
|
87
|
+
const expected = `${standard
|
|
88
|
+
.replace(
|
|
89
|
+
"# The preset's own persona, shadowing the deployment default for this agent.\n# `{{model}}` and `{{cwd}}` resolve from the agent's own route and workspace.",
|
|
90
|
+
"# The preset's own model-neutral persona shadows the deployment default. Auto\n# routing can upgrade after prompt assembly; `{{cwd}}` remains workspace-local.",
|
|
91
|
+
)
|
|
92
|
+
.replace(
|
|
93
|
+
"You are a coding agent powered by the {{model}} model. Your working directory is {{cwd}}.",
|
|
94
|
+
"You are Odai, a coding agent. Your working directory is {{cwd}}.",
|
|
95
|
+
)
|
|
96
|
+
.trimEnd()}\n\n${odaiSuffix}`;
|
|
97
|
+
const actual = renderAgentCompositionForDsh(
|
|
98
|
+
await readFile(resolve(agentRoot, "preset/odai/agent.cordis.yml"), "utf8"),
|
|
99
|
+
dshMetadata.version,
|
|
100
|
+
).trimEnd();
|
|
101
|
+
if (actual !== expected) {
|
|
102
|
+
throw new Error(`Odai Agent composition drifted from the DSH ${dshMetadata.version} standard preset`);
|
|
103
|
+
}
|
|
104
|
+
return dshMetadata.version;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function freePort() {
|
|
108
|
+
return await new Promise((accept, reject) => {
|
|
109
|
+
const server = createServer();
|
|
110
|
+
server.once("error", reject);
|
|
111
|
+
server.listen(0, "127.0.0.1", () => {
|
|
112
|
+
const address = server.address();
|
|
113
|
+
const port = typeof address === "object" && address ? address.port : undefined;
|
|
114
|
+
server.close((error) => error ? reject(error) : accept(port));
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function rpc(baseUrl, method, payload) {
|
|
120
|
+
const rpcId = randomUUID();
|
|
121
|
+
const response = await fetch(`${baseUrl}/api/${method}`, {
|
|
122
|
+
method: "POST",
|
|
123
|
+
headers: { "content-type": "application/json" },
|
|
124
|
+
body: JSON.stringify({ type: "client-request", rpcId, method, payload }),
|
|
125
|
+
});
|
|
126
|
+
const text = await response.text();
|
|
127
|
+
if (!response.ok) throw new Error(`${method} HTTP ${response.status}: ${text}`);
|
|
128
|
+
const body = JSON.parse(text);
|
|
129
|
+
if (body.result?.ok !== true) throw new Error(`${method} failed: ${text}`);
|
|
130
|
+
return body.result.value;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function waitForServer(baseUrl, child, output) {
|
|
134
|
+
const deadline = Date.now() + 20_000;
|
|
135
|
+
while (Date.now() < deadline) {
|
|
136
|
+
if (child.exitCode !== null) throw new Error(`dsh web exited early (${child.exitCode})\n${output()}`);
|
|
137
|
+
try {
|
|
138
|
+
await rpc(baseUrl, "agentPreset.list", {});
|
|
139
|
+
return;
|
|
140
|
+
} catch {
|
|
141
|
+
await new Promise((accept) => setTimeout(accept, 75));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
throw new Error(`timed out waiting for dsh web\n${output()}`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function waitForMarker(child, output) {
|
|
148
|
+
const deadline = Date.now() + 15_000;
|
|
149
|
+
while (Date.now() < deadline) {
|
|
150
|
+
if (existsSync(markerPath)) return JSON.parse(await readFile(markerPath, "utf8"));
|
|
151
|
+
if (child.exitCode !== null) throw new Error(`dsh web exited before probe completed (${child.exitCode})\n${output()}`);
|
|
152
|
+
await new Promise((accept) => setTimeout(accept, 50));
|
|
153
|
+
}
|
|
154
|
+
throw new Error(`timed out waiting for scope marker\n${output()}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function terminateChild(child) {
|
|
158
|
+
if (process.platform === "win32" && child.pid) {
|
|
159
|
+
try {
|
|
160
|
+
execFileSync("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore" });
|
|
161
|
+
} catch {
|
|
162
|
+
if (child.exitCode === null) child.kill("SIGTERM");
|
|
163
|
+
}
|
|
164
|
+
} else if (child.exitCode === null) {
|
|
165
|
+
child.kill("SIGTERM");
|
|
166
|
+
}
|
|
167
|
+
await new Promise((accept) => {
|
|
168
|
+
if (child.exitCode !== null) return accept();
|
|
169
|
+
const timeout = setTimeout(() => { child.kill("SIGKILL"); accept(); }, 3_000);
|
|
170
|
+
child.once("exit", () => { clearTimeout(timeout); accept(); });
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
await mkdir(workspace, { recursive: true });
|
|
175
|
+
await cp(resolve(agentRoot, "preset/odai"), sourceRoot, { recursive: true });
|
|
176
|
+
const developmentRuntime = resolve(repoRoot, "dsh/runtime/src");
|
|
177
|
+
const developmentSkill = resolve(repoRoot, "skills/odai");
|
|
178
|
+
if (existsSync(developmentRuntime) && existsSync(developmentSkill)) {
|
|
179
|
+
await Promise.all([
|
|
180
|
+
cp(developmentRuntime, resolve(sourceRoot, "runtime"), { recursive: true }),
|
|
181
|
+
cp(developmentSkill, resolve(sourceRoot, "skills/odai"), { recursive: true }),
|
|
182
|
+
]);
|
|
183
|
+
} else if (!existsSync(resolve(sourceRoot, "runtime/index.mjs"))
|
|
184
|
+
|| !existsSync(resolve(sourceRoot, "skills/odai/SKILL.md"))) {
|
|
185
|
+
throw new Error("Odai Agent verification requires either repository sources or packaged runtime and skill files");
|
|
186
|
+
}
|
|
187
|
+
await installAgentPreset({ dshHome: home, sourceRoot, dshVersion: targetDshVersion });
|
|
188
|
+
|
|
189
|
+
const probePlugin = `import { existsSync, writeFileSync } from "node:fs";\nimport { bindScopeParent } from ${JSON.stringify(pathToFileURL(scopeModule).href)};\n\nexport const name = "odai-agent-scope-probe";\nexport const inject = ["systemPrompt", "tools"];\n\nexport function apply(ctx, config) {\n const results = {};\n let writing = Promise.resolve();\n\n ctx.on("agent/created", ({ agent }) => {\n writing = writing.then(async () => {\n const preset = agent.session?.header?.agentPreset;\n if (preset !== "standard" && preset !== "odai") return;\n\n const assembly = await ctx.systemPrompt.assemble({ scope: agent });\n const canonicalSections = assembly.sections.filter((section) => section.name === "odai:canonical-governance");\n const writePath = preset === "odai" ? config.odaiWritePath : config.standardWritePath;\n const childSession = new Proxy(agent.session, {\n get(target, property) {\n if (property === "header") {\n return { ...target.header, origin: "subagent", delegationDepth: 1 };\n }\n return Reflect.get(target, property, target);\n },\n });\n const child = { id: agent.id, session: childSession };\n bindScopeParent(child, agent);\n const toolResult = await ctx.tools.execute({\n callId: \`scope-probe-\${preset}\`,\n name: "write",\n arguments: { file_path: writePath, content: \`\${preset} child write reached body\\n\` },\n agent: child,\n signal: new AbortController().signal,\n });\n\n let routingProtected;\n if (preset === "odai") {\n const routingResult = await ctx.tools.execute({\n callId: "scope-probe-odai-routing-config",\n name: "odai_routing_config",\n arguments: { action: "set", responsibility: "executor", provider: "probe-provider", model: "probe-executor", reasoningEffort: "high" },\n agent,\n signal: new AbortController().signal,\n });\n routingProtected = routingResult.isError === true\n && /NO_ADAPTER/u.test(routingResult.error?.message ?? "")\n && !existsSync(config.routingConfigPath);\n }\n\n results[preset] = {\n canonicalSectionCount: canonicalSections.length,\n toolIsError: toolResult.isError === true,\n toolError: toolResult.isError === true ? toolResult.error?.message : undefined,\n writeReachedBody: existsSync(writePath),\n ...(routingProtected === undefined ? {} : { routingProtected }),\n };\n if (results.standard && results.odai) {\n writeFileSync(config.markerPath, JSON.stringify(results, null, 2) + "\\n", "utf8");\n }\n }).catch((error) => {\n writeFileSync(config.markerPath, JSON.stringify({ probeError: error?.stack ?? String(error) }, null, 2) + "\\n", "utf8");\n });\n }, { global: true });\n}\n`;
|
|
190
|
+
await writeFile(probePluginPath, probePlugin, "utf8");
|
|
191
|
+
await writeFile(patchPath, [
|
|
192
|
+
"- insert:",
|
|
193
|
+
" - id: odai-agent-scope-probe",
|
|
194
|
+
` name: ${yaml(pathToFileURL(probePluginPath).href)}`,
|
|
195
|
+
" config:",
|
|
196
|
+
` markerPath: ${yaml(markerPath)}`,
|
|
197
|
+
` standardWritePath: ${yaml(resolve(workspace, "standard-child-write.txt"))}`,
|
|
198
|
+
` odaiWritePath: ${yaml(resolve(workspace, "odai-child-write.txt"))}`,
|
|
199
|
+
` routingConfigPath: ${yaml(resolve(home, "odai/routing.json"))}`,
|
|
200
|
+
"",
|
|
201
|
+
].join("\n"), "utf8");
|
|
202
|
+
|
|
203
|
+
const port = await freePort();
|
|
204
|
+
const baseUrl = `http://127.0.0.1:${port}`;
|
|
205
|
+
const child = spawnDsh(dsh, [
|
|
206
|
+
"--profile", "web",
|
|
207
|
+
"--patch", patchPath,
|
|
208
|
+
"--host", "127.0.0.1",
|
|
209
|
+
"--port", String(port),
|
|
210
|
+
], {
|
|
211
|
+
cwd: workspace,
|
|
212
|
+
env: {
|
|
213
|
+
...process.env,
|
|
214
|
+
DSH_HOME: home,
|
|
215
|
+
DSH_TELEMETRY_MODE: "DISABLED",
|
|
216
|
+
},
|
|
217
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
218
|
+
});
|
|
219
|
+
let output = "";
|
|
220
|
+
child.stdout.on("data", (chunk) => { output += chunk.toString(); });
|
|
221
|
+
child.stderr.on("data", (chunk) => { output += chunk.toString(); });
|
|
222
|
+
const capturedOutput = () => output;
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
await waitForServer(baseUrl, child, capturedOutput);
|
|
226
|
+
const roster = await rpc(baseUrl, "agentPreset.list", {});
|
|
227
|
+
const ids = roster.presets.map((preset) => preset.id);
|
|
228
|
+
if (!ids.includes("standard") || !ids.includes("odai")) {
|
|
229
|
+
throw new Error(`expected standard and odai presets, got ${JSON.stringify(ids)}`);
|
|
230
|
+
}
|
|
231
|
+
await rpc(baseUrl, "session.create", { cwd: workspace, agentPreset: "standard" });
|
|
232
|
+
await rpc(baseUrl, "session.create", { cwd: workspace, agentPreset: "odai" });
|
|
233
|
+
const results = await waitForMarker(child, capturedOutput);
|
|
234
|
+
if (results.probeError) throw new Error(results.probeError);
|
|
235
|
+
if (results.standard?.canonicalSectionCount !== 0) {
|
|
236
|
+
throw new Error(`standard saw odai canonical prompt: ${JSON.stringify(results)}`);
|
|
237
|
+
}
|
|
238
|
+
if (results.standard?.toolError?.startsWith("ODAI_SUBAGENT_BOUNDARY:")) {
|
|
239
|
+
throw new Error(`standard child call reached the odai guard: ${JSON.stringify(results)}`);
|
|
240
|
+
}
|
|
241
|
+
if (results.odai?.canonicalSectionCount !== 1) {
|
|
242
|
+
throw new Error(`odai canonical prompt count was not exactly one: ${JSON.stringify(results)}`);
|
|
243
|
+
}
|
|
244
|
+
if (results.odai?.writeReachedBody !== false || results.odai?.toolIsError !== true
|
|
245
|
+
|| !results.odai?.toolError?.startsWith("ODAI_SUBAGENT_BOUNDARY:")) {
|
|
246
|
+
throw new Error(`odai child write was not denied by odai guard: ${JSON.stringify(results)}`);
|
|
247
|
+
}
|
|
248
|
+
if (results.odai?.routingProtected !== true) {
|
|
249
|
+
throw new Error(`odai routing tool did not reject an unavailable route without mutation: ${JSON.stringify(results)}`);
|
|
250
|
+
}
|
|
251
|
+
process.stdout.write(`${JSON.stringify({ scratch, roster: ids, results }, null, 2)}\n`);
|
|
252
|
+
} finally {
|
|
253
|
+
await terminateChild(child);
|
|
254
|
+
if (process.env.KEEP_ODAI_SCOPE_PROBE !== "1") {
|
|
255
|
+
await rm(scratch, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
|
|
256
|
+
}
|
|
257
|
+
}
|
package/src/dsh-version.mjs
CHANGED
|
@@ -5,14 +5,18 @@ export function readDshVersion({
|
|
|
5
5
|
platform = process.platform,
|
|
6
6
|
execute = execFileSync,
|
|
7
7
|
} = {}) {
|
|
8
|
-
return execute(dsh, ["-V"], dshProcessOptions({ encoding: "utf8" }, platform)).trim();
|
|
8
|
+
return execute(normalizeDshCommand(dsh, platform), ["-V"], dshProcessOptions({ encoding: "utf8" }, platform)).trim();
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export function spawnDsh(command, args, options = {}, {
|
|
12
12
|
platform = process.platform,
|
|
13
13
|
execute = spawn,
|
|
14
14
|
} = {}) {
|
|
15
|
-
return execute(command, args, dshProcessOptions(options, platform));
|
|
15
|
+
return execute(normalizeDshCommand(command, platform), args, dshProcessOptions(options, platform));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function normalizeDshCommand(command, platform) {
|
|
19
|
+
return platform === "win32" && command === "dsh" ? "dsh.cmd" : command;
|
|
16
20
|
}
|
|
17
21
|
|
|
18
22
|
function dshProcessOptions(options, platform) {
|