@catheadowl/dsh-eval 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/sandbox.mjs CHANGED
@@ -17,7 +17,7 @@
17
17
 
18
18
  import { spawn } from 'node:child_process'
19
19
  import {
20
- chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, readdirSync, readlinkSync, rmSync, symlinkSync, unlinkSync,
20
+ chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, readlinkSync, rmSync, symlinkSync, unlinkSync,
21
21
  } from 'node:fs'
22
22
  import { homedir } from 'node:os'
23
23
  import { join } from 'node:path'
@@ -104,6 +104,67 @@ function copyProfileEntry(from, to, junctions) {
104
104
  copyFileSync(from, to)
105
105
  }
106
106
 
107
+ /**
108
+ * Enumerate every loader row id the STAGED profile composes BEYOND the host
109
+ * templates (`@deepseek-ai/*` bundles) — the data source for the review
110
+ * adapter's default blank environment (blank = dsh-base/dsh-headless only,
111
+ * no out-of-tree plugin face regardless of what the host profile carries).
112
+ *
113
+ * Composition-aware, per the host's ordered-layer model: the profile root
114
+ * `cordis.yml` ships as an EMPTY entry list — plugins enter either through
115
+ * the profile's own `cordis.patch.yml` rows or through out-of-tree bundles
116
+ * listed in `package.json`'s `dsh.profile.bundles`, each bundle contributing
117
+ * rows from its own `cordis.patch.yml`/`cordis.yml` under the profile's
118
+ * (junctioned) `node_modules`. All three sources live inside the staged home.
119
+ *
120
+ * A minimal token scan (`- id: <token>` at any indentation, which also
121
+ * covers rows nested under `- insert:`), not a YAML parse (same precedent as
122
+ * the file-history minimal session-log decoder). Entries without an `id` are
123
+ * invisible to id-targeted disables by construction and thus not collected.
124
+ * Unreadable sources contribute nothing — callers keep their static
125
+ * fallback rows.
126
+ * @param {string} tmpHome - the staged temporary home.
127
+ * @param {string} profileName - the staged profile name.
128
+ * @returns {string[]} deduplicated loader row ids beyond the host templates.
129
+ */
130
+ export function stagedPluginRows(tmpHome, profileName) {
131
+ const profileDir = join(tmpHome, 'profiles', profileName)
132
+ const rows = []
133
+ const collectIds = (text) => {
134
+ for (const match of text.matchAll(/^[ \t]*-[ \t]+id:[ \t]*"?'?([^\s"']+)/gm)) {
135
+ if (!rows.includes(match[1])) rows.push(match[1])
136
+ }
137
+ }
138
+ const readText = (file) => {
139
+ try {
140
+ return readFileSync(file, 'utf8')
141
+ } catch {
142
+ return undefined
143
+ }
144
+ }
145
+ // Source 1+2: the profile's own composition files (root list is normally
146
+ // the shipped empty `[]`, but a non-empty one is scanned all the same).
147
+ for (const name of ['cordis.patch.yml', 'cordis.yml']) {
148
+ const text = readText(join(profileDir, name))
149
+ if (text !== undefined) collectIds(text)
150
+ }
151
+ // Source 3: every NON-host bundle in dsh.profile.bundles — host template
152
+ // bundles (`@deepseek-ai/*`) define the sterile baseline and stay.
153
+ let bundles
154
+ try {
155
+ bundles = JSON.parse(readFileSync(join(profileDir, 'package.json'), 'utf8'))?.dsh?.profile?.bundles
156
+ } catch { /* no package.json → no bundle rows to collect */ }
157
+ for (const bundle of Array.isArray(bundles) ? bundles : []) {
158
+ if (typeof bundle !== 'string' || bundle.startsWith('@deepseek-ai/')) continue
159
+ const bundleDir = join(profileDir, 'node_modules', ...bundle.split('/'))
160
+ for (const name of ['cordis.patch.yml', 'cordis.yml']) {
161
+ const text = readText(join(bundleDir, name))
162
+ if (text !== undefined) collectIds(text)
163
+ }
164
+ }
165
+ return rows
166
+ }
167
+
107
168
  /**
108
169
  * Copy the managed credential document into the temporary home (copy, not
109
170
  * junction: single file, best-effort owner-only 0o600 — a no-op beyond the
@@ -1,77 +1,77 @@
1
- /**
2
- * Post-run tool boundary validation for blind review.
3
- *
4
- * After a review run completes, this module inspects the session trace's
5
- * `request/header` events to verify that no unexpected tools were mounted
6
- * in the reviewer's session. This is the detection half of the
7
- * sterile-profile strategy: the profile prevents plugin tools from being
8
- * installed, the overlay disables every host tool row, and this check
9
- * makes any residual drift (a bundle leaking tools through a patch, a host
10
- * regression, a misconfigured profile) an explicit adapter failure.
11
- *
12
- * The module is intentionally pure: it takes an already-parsed trace and
13
- * returns a plain result object. File I/O (reading the session log,
14
- * writing evidence) stays in the calling adapter.
15
- */
16
-
17
- /**
18
- * Collect every distinct tool name mounted across all `request/header`
19
- * events in a trace.
20
- * @param {import('./trace.mjs').EvalTrace} trace
21
- * @returns {string[]} sorted tool names.
22
- */
23
- function collectMountedToolNames(trace) {
24
- const names = new Set()
25
- for (const header of trace.requestHeaders ?? []) {
26
- for (const name of header.toolNames ?? []) {
27
- names.add(name)
28
- }
29
- }
30
- return [...names].sort()
31
- }
32
-
33
- /**
34
- * Check a review trace for tool leakage.
35
- *
36
- * Every tool name appearing in any `request/header` event is compared
37
- * against the allowed set. An empty allowed set (the review default)
38
- * means the reviewer must see no tools at all.
39
- *
40
- * @param {import('./trace.mjs').EvalTrace | undefined} trace - the parsed trace; `undefined` skips validation.
41
- * @param {{ allowedTools?: Set<string> }} [options]
42
- * @returns {{ ok: boolean, unexpected: string[], actual: string[], allowed: string[] }}
43
- */
44
- export function validateToolBoundary(trace, options = {}) {
45
- const allowed = options.allowedTools ?? new Set()
46
- const allowedNames = [...allowed].sort()
47
- if (trace === undefined || trace === null) {
48
- return { ok: true, unexpected: [], actual: [], allowed: allowedNames }
49
- }
50
- const actual = collectMountedToolNames(trace)
51
- const unexpected = actual.filter(name => !allowed.has(name))
52
- return {
53
- ok: unexpected.length === 0,
54
- unexpected,
55
- actual,
56
- allowed: allowedNames,
57
- }
58
- }
59
-
60
- /**
61
- * Render a diagnostic evidence document for a tool boundary failure.
62
- * Suitable for writing to `.runs/<id>/tool-boundary-evidence.json`.
63
- *
64
- * @param {{ ok: boolean, unexpected: string[], actual: string[], allowed: string[] }} validation
65
- * @param {{ runDir: string, profile: string }} context
66
- * @returns {string}
67
- */
68
- export function renderToolBoundaryEvidence(validation, context) {
69
- return JSON.stringify({
70
- status: 'tool-boundary-violation',
71
- runDir: context.runDir,
72
- profile: context.profile,
73
- unexpectedTools: validation.unexpected,
74
- actualMounted: validation.actual,
75
- allowedTools: validation.allowed,
76
- }, null, 2) + '\n'
77
- }
1
+ /**
2
+ * Post-run tool boundary validation for blind review.
3
+ *
4
+ * After a review run completes, this module inspects the session trace's
5
+ * `request/header` events to verify that no unexpected tools were mounted
6
+ * in the reviewer's session. This is the detection half of the
7
+ * blank-environment strategy: the overlay disables every staged out-of-tree
8
+ * plugin row plus every host tool row, and this check makes any residual
9
+ * drift (a bundle leaking tools through a patch, a host regression, a
10
+ * misconfigured profile) an explicit adapter failure.
11
+ *
12
+ * The module is intentionally pure: it takes an already-parsed trace and
13
+ * returns a plain result object. File I/O (reading the session log,
14
+ * writing evidence) stays in the calling adapter.
15
+ */
16
+
17
+ /**
18
+ * Collect every distinct tool name mounted across all `request/header`
19
+ * events in a trace.
20
+ * @param {import('./trace.mjs').EvalTrace} trace
21
+ * @returns {string[]} sorted tool names.
22
+ */
23
+ function collectMountedToolNames(trace) {
24
+ const names = new Set()
25
+ for (const header of trace.requestHeaders ?? []) {
26
+ for (const name of header.toolNames ?? []) {
27
+ names.add(name)
28
+ }
29
+ }
30
+ return [...names].sort()
31
+ }
32
+
33
+ /**
34
+ * Check a review trace for tool leakage.
35
+ *
36
+ * Every tool name appearing in any `request/header` event is compared
37
+ * against the allowed set. An empty allowed set (the review default)
38
+ * means the reviewer must see no tools at all.
39
+ *
40
+ * @param {import('./trace.mjs').EvalTrace | undefined} trace - the parsed trace; `undefined` skips validation.
41
+ * @param {{ allowedTools?: Set<string> }} [options]
42
+ * @returns {{ ok: boolean, unexpected: string[], actual: string[], allowed: string[] }}
43
+ */
44
+ export function validateToolBoundary(trace, options = {}) {
45
+ const allowed = options.allowedTools ?? new Set()
46
+ const allowedNames = [...allowed].sort()
47
+ if (trace === undefined || trace === null) {
48
+ return { ok: true, unexpected: [], actual: [], allowed: allowedNames }
49
+ }
50
+ const actual = collectMountedToolNames(trace)
51
+ const unexpected = actual.filter(name => !allowed.has(name))
52
+ return {
53
+ ok: unexpected.length === 0,
54
+ unexpected,
55
+ actual,
56
+ allowed: allowedNames,
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Render a diagnostic evidence document for a tool boundary failure.
62
+ * Suitable for writing to `.runs/<id>/tool-boundary-evidence.json`.
63
+ *
64
+ * @param {{ ok: boolean, unexpected: string[], actual: string[], allowed: string[] }} validation
65
+ * @param {{ runDir: string, profile: string }} context
66
+ * @returns {string}
67
+ */
68
+ export function renderToolBoundaryEvidence(validation, context) {
69
+ return JSON.stringify({
70
+ status: 'tool-boundary-violation',
71
+ runDir: context.runDir,
72
+ profile: context.profile,
73
+ unexpectedTools: validation.unexpected,
74
+ actualMounted: validation.actual,
75
+ allowedTools: validation.allowed,
76
+ }, null, 2) + '\n'
77
+ }