@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.
@@ -7,17 +7,18 @@ import { executeReviewExperiment } from '../../experiment/review.mjs'
7
7
  import { CLI_RELATIVE_PATH } from '../../cli.mjs'
8
8
  import { overlayDisableRows } from '../../overlay.mjs'
9
9
  import {
10
- resolveRealDshHome, stageSandboxHome, teardownSandbox, spawnHeadlessDsh,
10
+ resolveRealDshHome, stageSandboxHome, stagedPluginRows, teardownSandbox, spawnHeadlessDsh,
11
11
  } from '../../sandbox.mjs'
12
12
  import { loadTraceDir } from '../../trace.mjs'
13
13
  import { validateToolBoundary, renderToolBoundaryEvidence } from '../../tool-validation.mjs'
14
14
 
15
15
  /**
16
16
  * Model-facing tool rows every shipped dsh profile mounts from `dsh-base`.
17
- * The overlay disables all host tool rows as a supplementary guard; the
18
- * primary isolation comes from the sterile review profile (dsh-base +
19
- * dsh-headless only, no out-of-tree plugins). Post-run tool boundary
20
- * validation (see `validateToolBoundary`) detects any residual drift.
17
+ * A static supplementary guard on top of the blank-environment enumeration
18
+ * (host template tool rows never appear in `stagedPluginRows` they are the
19
+ * sterile baseline, so their ids are enumerated here instead). Post-run
20
+ * tool boundary validation (see `validateToolBoundary`) detects any residual
21
+ * drift.
21
22
  */
22
23
  const REVIEW_DISABLED_TOOL_ROWS = [
23
24
  'tool-bash',
@@ -39,9 +40,34 @@ const REVIEW_DISABLED_TOOL_ROWS = [
39
40
  'tool-workflow',
40
41
  ]
41
42
 
42
- /** Serialize the tool-less overlay: disable every host model-facing tool row. */
43
- function buildReviewOverlayYaml() {
44
- return overlayDisableRows(REVIEW_DISABLED_TOOL_ROWS)
43
+ /**
44
+ * Rows the reviewer session must KEEP even when an out-of-tree bundle or the
45
+ * profile patch touches them: model wiring and the session log — without
46
+ * these the reviewer cannot answer at all and the trace (tool boundary
47
+ * check, run artifacts) never materializes. These ids are host-template
48
+ * rows; an out-of-tree patch row targeting one of them only overrides its
49
+ * config, so keeping it enabled stays safe.
50
+ */
51
+ const REVIEW_REQUIRED_ROWS = new Set([
52
+ 'agent-default-model',
53
+ 'session-title-llm',
54
+ 'system-prompt',
55
+ 'session-persistence-jsonl',
56
+ ])
57
+
58
+ /**
59
+ * Serialize the blank-environment overlay: disable every plugin row the
60
+ * staged profile composes beyond the host templates (`stagedPluginRows` —
61
+ * out-of-tree bundle rows + profile patch rows), UNION the static host tool
62
+ * rows, MINUS the model/session wiring the reviewer needs. With
63
+ * `keepPluginRows` (explicit opt-in to study the host plugin face itself)
64
+ * the enumeration is skipped and only the static tool lockdown remains.
65
+ */
66
+ function buildReviewOverlayYaml(pluginRows, { keepPluginRows }) {
67
+ const disabled = new Set(keepPluginRows ? [] : pluginRows)
68
+ for (const row of REVIEW_DISABLED_TOOL_ROWS) disabled.add(row)
69
+ for (const row of REVIEW_REQUIRED_ROWS) disabled.delete(row)
70
+ return overlayDisableRows([...disabled])
45
71
  }
46
72
 
47
73
  /** Resolve and validate the compiled dsh CLI entry point. */
@@ -66,16 +92,22 @@ function executorCli(options) {
66
92
  /**
67
93
  * Create an executor compatible with executeReviewExperiment.
68
94
  *
69
- * The executor boots a sterile review profile (default: the host's
70
- * `headless` template, bundles = dsh-base + dsh-headless, no out-of-tree
71
- * plugins) in an isolated DSH_HOME (staging/teardown mechanics shared with
72
- * the behavior runner via sandbox.mjs), disables every host tool row via
73
- * overlay, and validates the tool boundary after the run. `options.profile`
74
- * must name a profile whose installed plugin set is empty or review-safe.
95
+ * The executor boots a blank review environment in an isolated DSH_HOME
96
+ * (staging/teardown mechanics shared with the behavior runner via
97
+ * sandbox.mjs): the staged profile's every out-of-tree plugin row and every
98
+ * host model-facing tool row is disabled via overlay (blank =
99
+ * dsh-base/dsh-headless templates + model wiring, nothing else regardless
100
+ * of what the host profile carries), and the tool boundary is validated
101
+ * after the run.
75
102
  *
76
103
  * @param {object} options
77
- * @param {string} [options.profile='headless'] - the sterile review profile.
104
+ * @param {string} [options.profile='headless'] - the review profile (host
105
+ * templates + whatever the host machine carries; plugin rows are disabled
106
+ * by the overlay anyway).
78
107
  * @param {Set<string>} [options.allowedTools] - tool names permitted in the reviewer's session (default: empty).
108
+ * @param {boolean} [options.keepPluginRows=false] - opt back into the host
109
+ * profile's plugin face (e.g. to review a plugin's own gate behavior);
110
+ * only the static tool lockdown remains.
79
111
  */
80
112
  export function createDshHeadlessReviewExecutor(options) {
81
113
  const cli = executorCli(options)
@@ -85,6 +117,7 @@ export function createDshHeadlessReviewExecutor(options) {
85
117
  }
86
118
  const timeoutMs = options.timeoutMs ?? 300_000
87
119
  const allowedTools = options.allowedTools ?? new Set()
120
+ const keepPluginRows = options.keepPluginRows ?? false
88
121
 
89
122
  return async function executeWithDsh(task) {
90
123
  // A fresh process alone is not enough: dsh also stores settings, titles,
@@ -93,9 +126,18 @@ export function createDshHeadlessReviewExecutor(options) {
93
126
  // while retaining the selected profile's model config and plugin links.
94
127
  const runDir = mkdtempSync(join(tmpdir(), 'dsh-review-'))
95
128
  const dshHome = join(runDir, 'dsh-home')
96
- const overlayPath = join(runDir, 'review-overlay.yml')
97
- writeFileSync(overlayPath, buildReviewOverlayYaml(), 'utf8')
98
129
  stageSandboxHome(options.dshHome ?? resolveRealDshHome(), dshHome, profile)
130
+ // Blank environment by DEFAULT (review-blank-environment TODO): disable
131
+ // every row the staged profile composes beyond the host templates, so a
132
+ // host profile carrying out-of-tree plugins (gates included) can no
133
+ // longer steer or crash the reviewer. `keepPluginRows` opts back into
134
+ // the host plugin face deliberately. Enumeration happens on the STAGED
135
+ // copy, after staging — the staged home is what actually boots.
136
+ const overlayPath = join(runDir, 'review-overlay.yml')
137
+ writeFileSync(overlayPath, buildReviewOverlayYaml(
138
+ stagedPluginRows(dshHome, profile),
139
+ { keepPluginRows },
140
+ ), 'utf8')
99
141
 
100
142
  try {
101
143
  const { stdout, stderr, exitCode, timedOut } = await spawnHeadlessDsh({
@@ -141,6 +183,17 @@ export function createDshHeadlessReviewExecutor(options) {
141
183
  boundaryError.result = result
142
184
  throw boundaryError
143
185
  }
186
+ // The ANSWER to the task, not the last message: stdout carries the
187
+ // headless CLI's final assistant message — whatever the reviewer
188
+ // said LAST. If any tail interaction intervened (a turn-close gate
189
+ // splice that slipped past the blank environment, an infra
190
+ // complaint), stdout holds that instead of the analysis. The
191
+ // trace's answerText (last assistant text before the first
192
+ // plugin-sourced injection) IS the analysis; stdout remains the
193
+ // fallback for trace-less runs.
194
+ result.answer = trace.answerText !== '' ? trace.answerText : stdout
195
+ } else {
196
+ result.answer = stdout
144
197
  }
145
198
 
146
199
  return result