odai-dsh-agent 0.0.3 → 0.0.5
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/README.md +14 -4
- package/bin/odai-dsh-agent.mjs +23 -3
- package/package.json +3 -3
- package/preset/odai/preset.yml +2 -2
- package/preset/odai/runtime/governance.mjs +2 -2
- package/preset/odai/runtime/index.mjs +166 -42
- package/preset/odai/runtime/router.mjs +76 -17
- package/preset/odai/runtime/routing-config.mjs +3 -2
- package/preset/odai/runtime/session-compat.mjs +387 -0
- package/preset/odai/runtime/session-evidence.mjs +344 -0
- package/preset/odai/runtime/skill-bundle.mjs +295 -0
- package/preset/odai/runtime/skill-selection-state.mjs +53 -0
- package/preset/odai/runtime/skill-selector.mjs +134 -0
- package/preset/odai/runtime/skill-source-config.mjs +239 -0
- package/preset/odai/skills/odai/SKILL.md +2 -1
- package/preset/odai/skills/odai/assets/routing-roles/controller.md +1 -1
- package/preset/odai/skills/odai/assets/routing-roles/reviewer.md +1 -1
- package/preset/odai/skills/odai/manifest.json +33 -0
- package/preset/odai/skills/odai/references/craft.md +1 -0
- package/preset/odai/skills/odai/references/leverage.md +1 -0
- package/preset/odai/skills/odai/references/planning.md +81 -0
- package/scripts/verify-agent-load.mjs +47 -19
- package/src/installer.mjs +60 -2
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
+
import {
|
|
4
|
+
closeSync,
|
|
5
|
+
constants as fsConstants,
|
|
6
|
+
copyFileSync,
|
|
7
|
+
existsSync,
|
|
8
|
+
fsyncSync,
|
|
9
|
+
openSync,
|
|
10
|
+
readFileSync,
|
|
11
|
+
readdirSync,
|
|
12
|
+
renameSync,
|
|
13
|
+
rmSync,
|
|
14
|
+
statSync,
|
|
15
|
+
writeSync,
|
|
16
|
+
} from "node:fs";
|
|
17
|
+
import { homedir } from "node:os";
|
|
18
|
+
import { dirname, resolve, sep } from "node:path";
|
|
19
|
+
import * as zlib from "node:zlib";
|
|
20
|
+
|
|
21
|
+
const ZSTD_MAGIC = 4247762216;
|
|
22
|
+
const ODAI_EVENT_PREFIX = "odai/";
|
|
23
|
+
const LEGACY_ODAI_AUDIT_EVENT_TYPES = new Set([
|
|
24
|
+
"odai/governance-denied",
|
|
25
|
+
"odai/route-config-missing",
|
|
26
|
+
"odai/route-decided",
|
|
27
|
+
"odai/route-protection",
|
|
28
|
+
"odai/route-result",
|
|
29
|
+
"odai/route-upgrade",
|
|
30
|
+
"odai/routing-configured",
|
|
31
|
+
"odai/tool-observed",
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
export function resolveDshHome(configured, env = process.env) {
|
|
35
|
+
const value = configured ?? env.DSH_HOME ?? resolve(homedir(), ".dsh");
|
|
36
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
37
|
+
throw new TypeError("DSH home must be a non-empty path");
|
|
38
|
+
}
|
|
39
|
+
const trimmed = value.trim();
|
|
40
|
+
if (trimmed === "~") return homedir();
|
|
41
|
+
if (trimmed.startsWith(`~${sep}`)) return resolve(homedir(), trimmed.slice(2));
|
|
42
|
+
return resolve(trimmed);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isDshCommand(commandLine) {
|
|
46
|
+
if (typeof commandLine !== "string") return false;
|
|
47
|
+
return /@deepseek-ai[\\/]dsh(?:[\\/]|$)/iu.test(commandLine)
|
|
48
|
+
|| /(?:^|[\s"'])[^\s"']*[\\/]dsh(?:\.cmd|\.mjs|\.js)?(?=$|[\s"'])/iu.test(commandLine)
|
|
49
|
+
|| /(?:^|[\s"'])dsh(?:\.cmd)?(?=$|[\s"'])/iu.test(commandLine);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function listWindowsProcesses() {
|
|
53
|
+
const command = "Get-CimInstance Win32_Process | Select-Object ProcessId,Name,CommandLine | ConvertTo-Json -Compress";
|
|
54
|
+
const output = execFileSync("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", command], {
|
|
55
|
+
encoding: "utf8",
|
|
56
|
+
windowsHide: true,
|
|
57
|
+
}).trim();
|
|
58
|
+
if (output === "") return [];
|
|
59
|
+
const parsed = JSON.parse(output);
|
|
60
|
+
return (Array.isArray(parsed) ? parsed : [parsed]).map((entry) => ({
|
|
61
|
+
pid: Number(entry.ProcessId),
|
|
62
|
+
name: typeof entry.Name === "string" ? entry.Name : "unknown",
|
|
63
|
+
commandLine: entry.CommandLine,
|
|
64
|
+
}));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function listPosixProcesses() {
|
|
68
|
+
const output = execFileSync("ps", ["-axo", "pid=,comm=,args="], { encoding: "utf8" });
|
|
69
|
+
return output.split(/\r?\n/u).flatMap((line) => {
|
|
70
|
+
const matched = /^\s*(\d+)\s+(\S+)\s+(.*)$/u.exec(line);
|
|
71
|
+
return matched ? [{ pid: Number(matched[1]), name: matched[2], commandLine: matched[3] }] : [];
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function listActiveDshProcesses(platform = process.platform) {
|
|
76
|
+
const processes = platform === "win32" ? listWindowsProcesses() : listPosixProcesses();
|
|
77
|
+
return processes
|
|
78
|
+
.filter((entry) => Number.isSafeInteger(entry.pid) && entry.pid > 0 && entry.pid !== process.pid)
|
|
79
|
+
.filter((entry) => isDshCommand(entry.commandLine))
|
|
80
|
+
.map(({ pid, name }) => Object.freeze({ pid, name }));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function assertDshStopped(options) {
|
|
84
|
+
let active;
|
|
85
|
+
try {
|
|
86
|
+
const scanner = options.processScanner ?? listActiveDshProcesses;
|
|
87
|
+
active = scanner();
|
|
88
|
+
} catch {
|
|
89
|
+
throw new Error("cannot verify that DSH is stopped; run the repair from a normal terminal with permission to inspect local process command lines");
|
|
90
|
+
}
|
|
91
|
+
if (!Array.isArray(active)) throw new Error("DSH process scanner returned an invalid result");
|
|
92
|
+
if (active.length > 0) {
|
|
93
|
+
const owners = active.map((entry) => `${entry.name ?? "dsh"} (pid ${entry.pid ?? "unknown"})`).join(", ");
|
|
94
|
+
throw new Error(`refusing to rewrite session artifacts while DSH is running: ${owners}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function listSessionArtifacts(root) {
|
|
99
|
+
if (!existsSync(root)) return [];
|
|
100
|
+
const found = [];
|
|
101
|
+
const pending = [root];
|
|
102
|
+
while (pending.length > 0) {
|
|
103
|
+
const current = pending.pop();
|
|
104
|
+
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
105
|
+
const path = resolve(current, entry.name);
|
|
106
|
+
if (entry.isDirectory()) pending.push(path);
|
|
107
|
+
else if (entry.isFile() && (entry.name === "session.jsonl" || entry.name === "session.jsonl.zstd")) {
|
|
108
|
+
found.push(path);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return found.sort();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function transformLine(line) {
|
|
116
|
+
if (line === "") return { line, repaired: 0 };
|
|
117
|
+
const record = JSON.parse(line);
|
|
118
|
+
if (typeof record?.type !== "string" || !record.type.startsWith(ODAI_EVENT_PREFIX) || record.ignorable === true) {
|
|
119
|
+
return { line, repaired: 0 };
|
|
120
|
+
}
|
|
121
|
+
if (!LEGACY_ODAI_AUDIT_EVENT_TYPES.has(record.type)) {
|
|
122
|
+
throw new Error(`refusing unknown Odai session event type ${JSON.stringify(record.type)}; use a compatible Odai repair version`);
|
|
123
|
+
}
|
|
124
|
+
return { line: JSON.stringify({ ...record, ignorable: true }), repaired: 1 };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function transformCompleteJsonl(text) {
|
|
128
|
+
const output = [];
|
|
129
|
+
let repaired = 0;
|
|
130
|
+
let offset = 0;
|
|
131
|
+
for (;;) {
|
|
132
|
+
const newline = text.indexOf("\n", offset);
|
|
133
|
+
if (newline < 0) break;
|
|
134
|
+
const raw = text.slice(offset, newline);
|
|
135
|
+
const carriageReturn = raw.endsWith("\r");
|
|
136
|
+
const line = carriageReturn ? raw.slice(0, -1) : raw;
|
|
137
|
+
const transformed = transformLine(line);
|
|
138
|
+
output.push(transformed.line, carriageReturn ? "\r\n" : "\n");
|
|
139
|
+
repaired += transformed.repaired;
|
|
140
|
+
offset = newline + 1;
|
|
141
|
+
}
|
|
142
|
+
const tornTail = text.slice(offset);
|
|
143
|
+
output.push(tornTail);
|
|
144
|
+
return { text: output.join(""), repaired, torn: tornTail.length > 0 };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function scanZstdFrames(buffer) {
|
|
148
|
+
const frames = [];
|
|
149
|
+
let offset = 0;
|
|
150
|
+
while (offset < buffer.length) {
|
|
151
|
+
const start = offset;
|
|
152
|
+
if (buffer.length - offset < 4) return { frames, tornStart: start };
|
|
153
|
+
if (buffer.readUInt32LE(offset) !== ZSTD_MAGIC) {
|
|
154
|
+
throw new Error(`invalid Zstandard frame magic at byte ${offset}`);
|
|
155
|
+
}
|
|
156
|
+
offset += 4;
|
|
157
|
+
if (offset === buffer.length) return { frames, tornStart: start };
|
|
158
|
+
const descriptor = buffer.readUInt8(offset);
|
|
159
|
+
offset += 1;
|
|
160
|
+
if ((descriptor & 24) !== 0) throw new Error(`reserved Zstandard frame-header bit at byte ${offset - 1}`);
|
|
161
|
+
const contentSizeFlag = descriptor >>> 6;
|
|
162
|
+
const singleSegment = (descriptor & 32) !== 0;
|
|
163
|
+
const checksum = (descriptor & 4) !== 0;
|
|
164
|
+
const dictionaryFlag = descriptor & 3;
|
|
165
|
+
const dictionaryBytes = dictionaryFlag === 3 ? 4 : dictionaryFlag;
|
|
166
|
+
const contentSizeBytes = contentSizeFlag === 0 ? (singleSegment ? 1 : 0) : 1 << contentSizeFlag;
|
|
167
|
+
const remainingHeaderBytes = (singleSegment ? 0 : 1) + dictionaryBytes + contentSizeBytes;
|
|
168
|
+
if (buffer.length - offset < remainingHeaderBytes) return { frames, tornStart: start };
|
|
169
|
+
offset += remainingHeaderBytes;
|
|
170
|
+
for (;;) {
|
|
171
|
+
if (buffer.length - offset < 3) return { frames, tornStart: start };
|
|
172
|
+
const blockHeader = buffer.readUIntLE(offset, 3);
|
|
173
|
+
offset += 3;
|
|
174
|
+
const lastBlock = (blockHeader & 1) !== 0;
|
|
175
|
+
const blockType = (blockHeader >>> 1) & 3;
|
|
176
|
+
const blockSize = blockHeader >>> 3;
|
|
177
|
+
if (blockType === 3) throw new Error(`reserved Zstandard block type at byte ${offset - 3}`);
|
|
178
|
+
const payloadBytes = blockType === 1 ? 1 : blockSize;
|
|
179
|
+
if (buffer.length - offset < payloadBytes) return { frames, tornStart: start };
|
|
180
|
+
offset += payloadBytes;
|
|
181
|
+
if (lastBlock) break;
|
|
182
|
+
}
|
|
183
|
+
if (checksum) {
|
|
184
|
+
if (buffer.length - offset < 4) return { frames, tornStart: start };
|
|
185
|
+
offset += 4;
|
|
186
|
+
}
|
|
187
|
+
frames.push({ start, end: offset });
|
|
188
|
+
}
|
|
189
|
+
return { frames };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function assertZstdAvailable() {
|
|
193
|
+
if (typeof zlib.zstdCompressSync !== "function" || typeof zlib.zstdDecompressSync !== "function") {
|
|
194
|
+
throw new Error("this Node.js build cannot migrate Zstandard session logs");
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function compressZstdFrame(plaintext) {
|
|
199
|
+
return zlib.zstdCompressSync(plaintext, {
|
|
200
|
+
params: { [zlib.constants.ZSTD_c_checksumFlag]: 1 },
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function transformZstd(source) {
|
|
205
|
+
assertZstdAvailable();
|
|
206
|
+
const { frames, tornStart } = scanZstdFrames(source);
|
|
207
|
+
if (frames.length === 0) throw new Error("Zstandard session log has no complete header frame");
|
|
208
|
+
const output = [];
|
|
209
|
+
let repaired = 0;
|
|
210
|
+
for (const [index, frame] of frames.entries()) {
|
|
211
|
+
const encoded = source.subarray(frame.start, frame.end);
|
|
212
|
+
if (index === 0) {
|
|
213
|
+
const header = zlib.zstdDecompressSync(encoded).toString("utf8");
|
|
214
|
+
if (!header.endsWith("\n") || JSON.parse(header.trimEnd())?.type !== "session") {
|
|
215
|
+
throw new Error("Zstandard session log has an invalid header frame");
|
|
216
|
+
}
|
|
217
|
+
output.push(encoded);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
const plaintext = zlib.zstdDecompressSync(encoded);
|
|
221
|
+
const transformed = transformCompleteJsonl(plaintext.toString("utf8"));
|
|
222
|
+
if (transformed.torn) throw new Error("complete Zstandard event frame has an unterminated JSONL record");
|
|
223
|
+
repaired += transformed.repaired;
|
|
224
|
+
if (transformed.repaired === 0) {
|
|
225
|
+
output.push(encoded);
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
const replacementPlaintext = Buffer.from(transformed.text, "utf8");
|
|
229
|
+
const replacement = compressZstdFrame(replacementPlaintext);
|
|
230
|
+
if (!zlib.zstdDecompressSync(replacement).equals(replacementPlaintext)) {
|
|
231
|
+
throw new Error("Zstandard migration verification failed");
|
|
232
|
+
}
|
|
233
|
+
output.push(replacement);
|
|
234
|
+
}
|
|
235
|
+
if (tornStart !== undefined) output.push(source.subarray(tornStart));
|
|
236
|
+
return {
|
|
237
|
+
buffer: repaired > 0 ? Buffer.concat(output) : source,
|
|
238
|
+
repaired,
|
|
239
|
+
torn: tornStart !== undefined,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function transformArtifact(path) {
|
|
244
|
+
const source = readFileSync(path);
|
|
245
|
+
if (path.endsWith(".zstd")) return { source, ...transformZstd(source) };
|
|
246
|
+
const transformed = transformCompleteJsonl(source.toString("utf8"));
|
|
247
|
+
return {
|
|
248
|
+
source,
|
|
249
|
+
buffer: transformed.repaired > 0 ? Buffer.from(transformed.text, "utf8") : source,
|
|
250
|
+
repaired: transformed.repaired,
|
|
251
|
+
torn: transformed.torn,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function writeDurably(path, content, mode) {
|
|
256
|
+
const handle = openSync(path, "wx", mode);
|
|
257
|
+
try {
|
|
258
|
+
let offset = 0;
|
|
259
|
+
while (offset < content.length) {
|
|
260
|
+
const written = writeSync(handle, content, offset, content.length - offset);
|
|
261
|
+
if (written <= 0) throw new Error(`short write while preparing session artifact: ${path}`);
|
|
262
|
+
offset += written;
|
|
263
|
+
}
|
|
264
|
+
fsyncSync(handle);
|
|
265
|
+
} finally {
|
|
266
|
+
closeSync(handle);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function syncDirectory(path) {
|
|
271
|
+
let handle;
|
|
272
|
+
try {
|
|
273
|
+
handle = openSync(path, "r");
|
|
274
|
+
fsyncSync(handle);
|
|
275
|
+
} catch {
|
|
276
|
+
// Windows does not expose directory fsync; the file itself was already synced.
|
|
277
|
+
} finally {
|
|
278
|
+
if (handle !== undefined) closeSync(handle);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function backupPathFor(path, source) {
|
|
283
|
+
const digest = createHash("sha256").update(source).digest("hex").slice(0, 16);
|
|
284
|
+
return `${path}.odai-compat-${digest}.bak`;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function createVerifiedBackup(path, source) {
|
|
288
|
+
const backupPath = backupPathFor(path, source);
|
|
289
|
+
if (!existsSync(backupPath)) {
|
|
290
|
+
copyFileSync(path, backupPath, fsConstants.COPYFILE_EXCL);
|
|
291
|
+
const handle = openSync(backupPath, "r+");
|
|
292
|
+
try {
|
|
293
|
+
fsyncSync(handle);
|
|
294
|
+
} finally {
|
|
295
|
+
closeSync(handle);
|
|
296
|
+
}
|
|
297
|
+
syncDirectory(dirname(path));
|
|
298
|
+
}
|
|
299
|
+
if (!readFileSync(backupPath).equals(source)) {
|
|
300
|
+
throw new Error(`session backup does not match the migration source: ${backupPath}`);
|
|
301
|
+
}
|
|
302
|
+
return backupPath;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function replaceAtomically(path, source, replacement, backupPath) {
|
|
306
|
+
if (!readFileSync(path).equals(source)) {
|
|
307
|
+
throw new Error("session artifact changed while compatibility migration was preparing it; stop every DSH process and retry");
|
|
308
|
+
}
|
|
309
|
+
const temporary = `${path}.odai-compat-${process.pid}-${randomUUID()}.tmp`;
|
|
310
|
+
const mode = statSync(path).mode & 0o777;
|
|
311
|
+
let published = false;
|
|
312
|
+
try {
|
|
313
|
+
writeDurably(temporary, replacement, mode);
|
|
314
|
+
renameSync(temporary, path);
|
|
315
|
+
published = true;
|
|
316
|
+
syncDirectory(dirname(path));
|
|
317
|
+
if (!readFileSync(path).equals(replacement)) throw new Error("published session artifact failed byte verification");
|
|
318
|
+
const verified = transformArtifact(path);
|
|
319
|
+
if (verified.repaired !== 0) throw new Error("published session artifact still contains required Odai events");
|
|
320
|
+
} catch (error) {
|
|
321
|
+
if (published) {
|
|
322
|
+
const restore = `${path}.odai-restore-${process.pid}-${randomUUID()}.tmp`;
|
|
323
|
+
try {
|
|
324
|
+
writeDurably(restore, readFileSync(backupPath), mode);
|
|
325
|
+
renameSync(restore, path);
|
|
326
|
+
syncDirectory(dirname(path));
|
|
327
|
+
} finally {
|
|
328
|
+
rmSync(restore, { force: true });
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
throw error;
|
|
332
|
+
} finally {
|
|
333
|
+
rmSync(temporary, { force: true });
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function processLegacySessionLogs(options, writeChanges) {
|
|
338
|
+
const dshHome = resolveDshHome(options.dshHome);
|
|
339
|
+
const sessionRoot = resolve(options.sessionRoot ?? resolve(dshHome, "sessions"));
|
|
340
|
+
const artifacts = listSessionArtifacts(sessionRoot);
|
|
341
|
+
const result = {
|
|
342
|
+
dshHome,
|
|
343
|
+
sessionRoot,
|
|
344
|
+
scannedArtifacts: artifacts.length,
|
|
345
|
+
matchedArtifacts: 0,
|
|
346
|
+
matchedEvents: 0,
|
|
347
|
+
repairedArtifacts: 0,
|
|
348
|
+
repairedEvents: 0,
|
|
349
|
+
backupPaths: [],
|
|
350
|
+
tornArtifacts: [],
|
|
351
|
+
failures: [],
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
for (const path of artifacts) {
|
|
355
|
+
try {
|
|
356
|
+
const transformed = transformArtifact(path);
|
|
357
|
+
if (transformed.torn) result.tornArtifacts.push(path);
|
|
358
|
+
if (transformed.repaired === 0) continue;
|
|
359
|
+
result.matchedArtifacts += 1;
|
|
360
|
+
result.matchedEvents += transformed.repaired;
|
|
361
|
+
if (!writeChanges) continue;
|
|
362
|
+
const backupPath = createVerifiedBackup(path, transformed.source);
|
|
363
|
+
replaceAtomically(path, transformed.source, transformed.buffer, backupPath);
|
|
364
|
+
result.backupPaths.push(backupPath);
|
|
365
|
+
result.repairedArtifacts += 1;
|
|
366
|
+
result.repairedEvents += transformed.repaired;
|
|
367
|
+
} catch (error) {
|
|
368
|
+
result.failures.push({
|
|
369
|
+
path,
|
|
370
|
+
error: error instanceof Error ? error.message : String(error),
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return result;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export function inspectLegacySessionLogs(options = {}) {
|
|
378
|
+
return processLegacySessionLogs(options, false);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export function repairLegacySessionLogs(options = {}) {
|
|
382
|
+
if (options.confirmDshStopped !== true) {
|
|
383
|
+
throw new Error("refusing to rewrite session artifacts until confirmDshStopped is true; stop every DSH process first");
|
|
384
|
+
}
|
|
385
|
+
assertDshStopped(options);
|
|
386
|
+
return processLegacySessionLogs(options, true);
|
|
387
|
+
}
|