dsh-harbor-evolution 0.7.1 → 0.7.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.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Installable DeepSeek Harness Plugin + Skill for running stable Harbor evaluation and controlled Agent evolution loops, with a native DSH Web dashboard.
4
4
 
5
- The package gives DSH twelve strict Harbor tools, dedicated Tool cards, a nine-stage Evaluation Workbench, an installation Doctor, and the model- and user-invocable `evolve-agent-with-harbor` Skill. The Skill starts with four user-facing concepts—Dataset (what to test), Generator (who answers), Evaluator plus criteria (what good means), and Optimizer (who improves it)—then compiles accepted choices into the strict Evaluation Stack. It validates Dataset identity, checks Trial Lifecycle and Score Validity, governs independent Ground Truth meta-evaluation, diagnoses evidence provenance, limits each iteration to one controlled Candidate change, and invokes the Promotion Gate only as an explicit action.
5
+ The package gives DSH fourteen strict Harbor tools, dedicated Tool cards, a nine-stage Evaluation Workbench, an installation Doctor, and the model- and user-invocable `evolve-agent-with-harbor` Skill. The Skill starts with four user-facing concepts—Dataset (what to test), Generator (who answers), Evaluator plus criteria (what good means), and Optimizer (who improves it)—then compiles accepted choices into the strict Evaluation Stack. A DSH Generator may explicitly pin the current default model as a non-secret Candidate identity while retaining the per-Job Host Broker credential boundary. The Plugin validates Dataset identity, checks Trial Lifecycle and Score Validity, governs independent Ground Truth meta-evaluation, diagnoses evidence provenance, limits each iteration to one controlled Candidate change, and invokes the Promotion Gate only as an explicit action.
6
6
 
7
7
  ## Install
8
8
 
@@ -14,10 +14,10 @@ npx --yes dsh-harbor-evolution@latest setup --project-root "$PWD"
14
14
 
15
15
  The setup command installs both required runtimes:
16
16
 
17
- - `harbor-dsh-evolution==0.7.1` in a managed Python environment.
18
- - `dsh-harbor-evolution@0.7.1` in the selected DSH profile.
17
+ - `harbor-dsh-evolution==0.7.3` in a managed Python environment.
18
+ - `dsh-harbor-evolution@0.7.3` in the selected DSH profile.
19
19
 
20
- It then stores the absolute Harbor executable paths and `projectRoot` in the profile's `harbor-evolution` block and verifies the integration. Existing unrelated profile entries are preserved, and rerunning setup updates the same block.
20
+ It then stores the absolute Harbor executable paths and a fallback `projectRoot` in the profile's `harbor-evolution` block and verifies the integration. Agent Tool calls always use the calling session's absolute working directory as their project root; the configured value remains the Web Workbench and non-Agent fallback. Existing unrelated profile entries are preserved, and rerunning setup updates the same block.
21
21
 
22
22
  The default profile is `web`. Use `--profile headless` only when that is the profile you actually run. See all options with:
23
23
 
@@ -37,6 +37,7 @@ Users may provide a single Query or Dataset path, a Generator curl or local Agen
37
37
  The Plugin registers:
38
38
 
39
39
  - `harbor_candidate_snapshot`
40
+ - `harbor_model_binding`
40
41
  - `harbor_evolution_init`
41
42
  - `harbor_evolution_doctor`
42
43
  - `harbor_dataset_validate`
@@ -51,7 +52,7 @@ In the `web` profile, the same package also registers:
51
52
  - descriptor-authorized Evaluator/Rubric source editing for `script` and `llm-as-judge` implementations, with optimistic concurrency and mandatory new identities;
52
53
  - a `harbor-dsh-evaluator/v1` interface shared by deterministic scripts and LLM-as-Judge implementations;
53
54
  - compact result cards for all Harbor Tool calls;
54
- - a `Harbor Evolution` Settings section that checks the configured project, Evaluation Stack, Jobs directory, and CLI paths.
55
+ - a `Harbor Evolution` Settings section that checks the configured project, Evaluation Stack, Jobs directory, and CLI paths, supports process-local `projectRoot` reload, and checks npm for a newer formal release without silently installing it.
55
56
 
56
57
  The Web UI is intentionally read-only. Starting an evaluation or deciding promotion remains an explicit Agent + Skill workflow, so a page refresh can never launch an expensive Job.
57
58
 
@@ -63,6 +64,10 @@ Before each Job, the Plugin snapshots the current DSH Agent selection—provider
63
64
 
64
65
  `harbor_eval_run`, `harbor_context_preview`, and `harbor_evolution_doctor` inherit that selection by default. Advanced callers can override `candidateProvider` and `candidateModel` only as a pair, plus an optional `candidateReasoningEffort`. `openai-codex` performs a GPT Auth sign-in check before Harbor starts. The resulting model binding is part of Context v2 comparison identity, so any provider/model/reasoning change requires a new baseline.
65
66
 
67
+ `harbor_model_binding` returns the current default selection as a credential-free `model-binding.json` draft. Once included before Candidate snapshot, it enters the Candidate digest and becomes the required Job model identity. Conflicting Job or Plugin overrides fail before Harbor starts. Even for `openai-codex`, the Candidate receives only the short-lived Broker capability—never the Host OAuth file or an upstream API key.
68
+
69
+ When Settings opens, the Host performs a bounded npm registry check and caches successful results. An available release is shown with its exact installer command and release link. The browser never installs, rewrites a DSH profile, or restarts DSH; registry failures are non-blocking.
70
+
66
71
  `harbor_eval_result` defaults to the stable Summary. Use `view=job`, `view=dataset`, `view=progress`, `view=trial` plus a returned `trialId`, or `view=governance` to inspect sanitized instructions, generated output, evidence, and evaluator source without coupling the Agent to artifact file paths.
67
72
 
68
73
  ## What setup writes
@@ -79,7 +84,7 @@ The selected profile receives one id-targeted override:
79
84
  pythonPath: ""
80
85
  ```
81
86
 
82
- Keep `pythonPath` empty for the published Python package. `candidatePath`, `datasetPath`, `jobPath`, and `policyPath` are constrained to `projectRoot`.
87
+ Keep `pythonPath` empty for the published Python package. For Agent Tool calls, `projectRoot` is replaced by the calling session's working directory for that call. `candidatePath`, `datasetPath`, `jobPath`, and `policyPath` remain constrained to that request-local root, so concurrent sessions cannot redirect each other's Harbor operations.
83
88
 
84
89
  For source development from the repository:
85
90
 
package/index.js CHANGED
@@ -6,6 +6,7 @@ import { fileURLToPath } from 'node:url'
6
6
 
7
7
  import { loadBundledSkill } from './lib/official-skill.js'
8
8
  import { CandidateModelRuntime } from './lib/model-runtime.js'
9
+ import { RUNTIME_POLICY } from './lib/runtime-identity.js'
9
10
  import { EvolutionService } from './lib/service.js'
10
11
  import { installDashboardWeb } from './lib/web.js'
11
12
 
@@ -26,7 +27,6 @@ export const Config = Schema.object({
26
27
  jobsDir: Schema.string().default('jobs'),
27
28
  harborBin: Schema.string().default(''),
28
29
  harborDshBin: Schema.string().default(''),
29
- dshVersion: Schema.string().default('0.1.0-rc.6'),
30
30
  agentImportPath: Schema.string().default('harbor_dsh_evolution.agent:DshCandidateAgent'),
31
31
  pluginImportPath: Schema.string().default('dsh-evolution'),
32
32
  pythonPath: Schema.string().default(''),
@@ -47,15 +47,30 @@ function jsonTool(definition, execute) {
47
47
  schema: { type: 'string' },
48
48
  render: (_args, value) => [{ type: 'text', text: value }],
49
49
  },
50
- async execute(args) {
51
- return JSON.stringify(await execute(args), null, 2)
50
+ async execute(args, exec) {
51
+ return JSON.stringify(await execute(args, exec), null, 2)
52
52
  },
53
53
  })
54
54
  }
55
55
 
56
+ function toolProjectRoot(exec) {
57
+ const cwd = exec?.agent?.session?.header?.cwd
58
+ if (typeof cwd !== 'string' || !path.isAbsolute(cwd)) {
59
+ throw new Error('Harbor tools require an Agent session with an absolute working directory')
60
+ }
61
+ return path.resolve(cwd)
62
+ }
63
+
64
+ export function synchronizeWorkbenchProjectRoot(service, exec) {
65
+ const projectRoot = toolProjectRoot(exec)
66
+ service.activateProjectRoot(projectRoot, 'agent-session')
67
+ return projectRoot
68
+ }
69
+
56
70
  export function apply(ctx, config) {
57
71
  const resolved = {
58
72
  ...config,
73
+ runtimePolicy: RUNTIME_POLICY,
59
74
  projectRoot: path.resolve(config.projectRoot),
60
75
  harborBin: config.harborBin || process.env.HARBOR_BIN || checkoutExecutable('harbor'),
61
76
  harborDshBin: config.harborDshBin || process.env.HARBOR_DSH_BIN || checkoutExecutable('harbor-dsh'),
@@ -66,7 +81,12 @@ export function apply(ctx, config) {
66
81
  ),
67
82
  }
68
83
  const modelRuntime = new CandidateModelRuntime(ctx, resolved)
69
- const service = new EvolutionService(resolved, { pluginVersion: packageJson.version }, modelRuntime)
84
+ const metadata = { pluginVersion: packageJson.version, projectRootSource: 'configured' }
85
+ const service = new EvolutionService(resolved, metadata, modelRuntime)
86
+ const serviceForTool = exec => {
87
+ const projectRoot = synchronizeWorkbenchProjectRoot(service, exec)
88
+ return new EvolutionService({ ...resolved, projectRoot }, metadata, modelRuntime)
89
+ }
70
90
 
71
91
  ctx.skills.register(loadBundledSkill())
72
92
  installDashboardWeb(ctx, service)
@@ -79,13 +99,20 @@ export function apply(ctx, config) {
79
99
  candidateId: { type: 'string' },
80
100
  version: { type: 'string' },
81
101
  },
82
- }, args => service.snapshot(args)))
102
+ }, (args, exec) => serviceForTool(exec).snapshot(args)))
103
+
104
+ ctx.tools.register(jsonTool({
105
+ name: 'harbor_model_binding',
106
+ description: 'Freeze the current DSH default provider, model, and reasoning identity into a non-secret model-binding.json draft. Runtime access still uses the short-lived Host Model Broker capability.',
107
+ parameters: {},
108
+ }, (_args, exec) => serviceForTool(exec).modelBinding()))
83
109
 
84
110
  ctx.tools.register(jsonTool({
85
111
  name: 'harbor_evolution_init',
86
112
  description: 'Compile an accepted Dataset, Generator, Evaluator/criteria, and Optimizer onboarding card into a strict, non-overwriting Evaluation Stack project. Detailed identity fields are internal tool inputs, not a user questionnaire.',
87
113
  parameters: {
88
114
  datasetPath: { type: 'string', required: true },
115
+ workspaceSubdir: { type: 'string', description: 'Optional namespace under the current project root. Defaults to the project root; use it to host multiple independent Harbor projects.' },
89
116
  stackId: { type: 'string', required: true },
90
117
  stackVersion: { type: 'string', required: true },
91
118
  datasetId: { type: 'string', required: true },
@@ -101,7 +128,7 @@ export function apply(ctx, config) {
101
128
  policyVersion: { type: 'string', required: true },
102
129
  minImprovement: { type: 'number', required: true },
103
130
  },
104
- }, args => service.initialize(args)))
131
+ }, (args, exec) => serviceForTool(exec).initialize(args)))
105
132
 
106
133
  ctx.tools.register(jsonTool({
107
134
  name: 'harbor_evolution_doctor',
@@ -116,7 +143,17 @@ export function apply(ctx, config) {
116
143
  candidateModel: { type: 'string' },
117
144
  candidateReasoningEffort: { type: 'string' },
118
145
  },
119
- }, args => service.doctor(args)))
146
+ }, (args, exec) => serviceForTool(exec).doctor(args)))
147
+
148
+ ctx.tools.register(jsonTool({
149
+ name: 'harbor_quick_diagnostic_init',
150
+ description: 'Create a non-overwriting Harbor 1.4 wiring diagnostic with one Query, a minimal Host-model Candidate, a runnable Task, and an explicit non-promotion Evaluator. The supplied Rubric is recorded as a draft but is not treated as executed.',
151
+ parameters: {
152
+ query: { type: 'string', required: true },
153
+ rubric: { type: 'string', required: true },
154
+ workspaceSubdir: { type: 'string', description: 'Defaults to harbor-diagnostic under the current Agent session directory.' },
155
+ },
156
+ }, (args, exec) => serviceForTool(exec).quickDiagnostic(args)))
120
157
 
121
158
  ctx.tools.register(jsonTool({
122
159
  name: 'harbor_dataset_validate',
@@ -124,7 +161,7 @@ export function apply(ctx, config) {
124
161
  parameters: {
125
162
  datasetPath: { type: 'string', required: true },
126
163
  },
127
- }, args => service.validateDataset(args)))
164
+ }, (args, exec) => serviceForTool(exec).validateDataset(args)))
128
165
 
129
166
  ctx.tools.register(jsonTool({
130
167
  name: 'harbor_context_preview',
@@ -140,7 +177,7 @@ export function apply(ctx, config) {
140
177
  candidateModel: { type: 'string' },
141
178
  candidateReasoningEffort: { type: 'string' },
142
179
  },
143
- }, args => service.previewContext(args)))
180
+ }, (args, exec) => serviceForTool(exec).previewContext(args)))
144
181
 
145
182
  ctx.tools.register(jsonTool({
146
183
  name: 'harbor_eval_run',
@@ -158,7 +195,7 @@ export function apply(ctx, config) {
158
195
  candidateModel: { type: 'string' },
159
196
  candidateReasoningEffort: { type: 'string' },
160
197
  },
161
- }, args => service.run(args)))
198
+ }, (args, exec) => serviceForTool(exec).run(args)))
162
199
 
163
200
  ctx.tools.register(jsonTool({
164
201
  name: 'harbor_eval_result',
@@ -170,7 +207,7 @@ export function apply(ctx, config) {
170
207
  compareJob: { type: 'string', description: 'Optional previous Job for view=governance impact analysis' },
171
208
  since: { type: 'string', description: 'Optional ISO timestamp for incremental progress changes' },
172
209
  },
173
- }, args => service.result(args)))
210
+ }, (args, exec) => serviceForTool(exec).result(args)))
174
211
 
175
212
  ctx.tools.register(jsonTool({
176
213
  name: 'harbor_evaluator_inspect',
@@ -178,7 +215,7 @@ export function apply(ctx, config) {
178
215
  parameters: {
179
216
  stackPath: { type: 'string', description: 'Defaults to .harbor/evaluation-stack.yml' },
180
217
  },
181
- }, args => service.evaluatorInspect(args)))
218
+ }, (args, exec) => serviceForTool(exec).evaluatorInspect(args)))
182
219
 
183
220
  ctx.tools.register(jsonTool({
184
221
  name: 'harbor_evaluator_update',
@@ -191,13 +228,14 @@ export function apply(ctx, config) {
191
228
  newEvaluatorVersion: { type: 'string', required: true },
192
229
  newStackVersion: { type: 'string', required: true },
193
230
  },
194
- }, args => service.evaluator(args)))
231
+ }, (args, exec) => serviceForTool(exec).evaluator(args)))
195
232
 
196
233
  ctx.tools.register(jsonTool({
197
234
  name: 'harbor_ground_truth_init',
198
235
  description: 'Create a non-overwriting Ground Truth draft for evaluator meta-evaluation. GT may be human, programmatic, consensus, model, or external, but must have explicit provenance and remain independent of the Candidate evaluator.',
199
236
  parameters: {
200
237
  outputPath: { type: 'string', description: 'Defaults to .harbor/ground-truth.json' },
238
+ evaluationRoot: { type: 'string', description: 'Optional evaluation workspace root used to register custom Ground Truth paths.' },
201
239
  groundTruthId: { type: 'string', required: true },
202
240
  version: { type: 'string', required: true },
203
241
  sourceKind: { type: 'string', required: true, description: 'human, programmatic, consensus, model, or external' },
@@ -205,7 +243,7 @@ export function apply(ctx, config) {
205
243
  provenance: { type: 'string', required: true },
206
244
  criteria: { type: 'string', required: true, description: 'Comma-separated criterion ids' },
207
245
  },
208
- }, args => service.groundTruthInitialize(args)))
246
+ }, (args, exec) => serviceForTool(exec).groundTruthInitialize(args)))
209
247
 
210
248
  ctx.tools.register(jsonTool({
211
249
  name: 'harbor_evaluator_meta_evaluate',
@@ -214,8 +252,9 @@ export function apply(ctx, config) {
214
252
  groundTruthPath: { type: 'string', description: 'Defaults to .harbor/ground-truth.json' },
215
253
  observationsPath: { type: 'string', required: true },
216
254
  outputPath: { type: 'string', description: 'Defaults to .harbor/meta-evaluation-report.json' },
255
+ evaluationRoot: { type: 'string', description: 'Optional evaluation workspace root used to register custom report paths.' },
217
256
  },
218
- }, args => service.evaluatorMetaEvaluate(args)))
257
+ }, (args, exec) => serviceForTool(exec).evaluatorMetaEvaluate(args)))
219
258
 
220
259
  ctx.tools.register(jsonTool({
221
260
  name: 'harbor_candidate_compare',
@@ -225,5 +264,5 @@ export function apply(ctx, config) {
225
264
  candidateJob: { type: 'string', required: true },
226
265
  policyPath: { type: 'string', required: true },
227
266
  },
228
- }, args => service.compare(args)))
267
+ }, (args, exec) => serviceForTool(exec).compare(args)))
229
268
  }
package/lib/candidate.js CHANGED
@@ -2,7 +2,10 @@ import { createHash } from 'node:crypto'
2
2
  import { mkdir, readFile, readdir, stat, writeFile } from 'node:fs/promises'
3
3
  import path from 'node:path'
4
4
 
5
+ import { CANDIDATE_ACP_PACKAGE, DSH_RUNTIME_VERSION, RUNTIME_POLICY } from './runtime-identity.js'
6
+
5
7
  export const MANIFEST_NAME = 'candidate-manifest.json'
8
+ export const MODEL_BINDING_NAME = 'model-binding.json'
6
9
  const DIGEST_PREFIX = Buffer.from('harbor-dsh-candidate-v1\0')
7
10
  const EXCLUDED_DIRS = new Set(['.git', 'node_modules', '__pycache__', '.harbor-runtime'])
8
11
  const EXCLUDED_FILES = new Set([MANIFEST_NAME, '.DS_Store'])
@@ -16,6 +19,7 @@ const CREDENTIAL_FILES = new Set([
16
19
  'id_rsa',
17
20
  'id_ed25519',
18
21
  ])
22
+ const MODEL_BINDING_KEYS = new Set(['schema_version', 'source', 'provider', 'model', 'reasoning_effort'])
19
23
 
20
24
  async function walk(root, current = root) {
21
25
  const entries = await readdir(current, { withFileTypes: true })
@@ -33,6 +37,45 @@ async function walk(root, current = root) {
33
37
  return files.sort((a, b) => Buffer.compare(Buffer.from(a.relative), Buffer.from(b.relative)))
34
38
  }
35
39
 
40
+ export async function loadModelBinding(candidateDir) {
41
+ const pathname = path.join(path.resolve(candidateDir), MODEL_BINDING_NAME)
42
+ let value
43
+ try {
44
+ value = JSON.parse(await readFile(pathname, 'utf8'))
45
+ } catch (error) {
46
+ if (error.code === 'ENOENT') return undefined
47
+ if (error instanceof SyntaxError) throw new Error(`${MODEL_BINDING_NAME} is not valid JSON`)
48
+ throw error
49
+ }
50
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
51
+ throw new Error(`${MODEL_BINDING_NAME} must be an object`)
52
+ }
53
+ const unknown = Object.keys(value).filter(key => !MODEL_BINDING_KEYS.has(key)).sort()
54
+ if (unknown.length) {
55
+ throw new Error(`${MODEL_BINDING_NAME} contains unsupported or secret-bearing fields: ${unknown.join(', ')}`)
56
+ }
57
+ if (value.schema_version !== 1) throw new Error(`${MODEL_BINDING_NAME} requires schema_version=1`)
58
+ const source = typeof value.source === 'string' ? value.source.trim() : ''
59
+ const provider = typeof value.provider === 'string' ? value.provider.trim() : ''
60
+ const model = typeof value.model === 'string' ? value.model.trim() : ''
61
+ if (!source || !provider || !model) {
62
+ throw new Error(`${MODEL_BINDING_NAME} requires non-empty source, provider, and model`)
63
+ }
64
+ const reasoningEffort = value.reasoning_effort === undefined
65
+ ? undefined
66
+ : typeof value.reasoning_effort === 'string' ? value.reasoning_effort.trim() : ''
67
+ if (value.reasoning_effort !== undefined && !reasoningEffort) {
68
+ throw new Error(`${MODEL_BINDING_NAME} reasoning_effort must be a non-empty string when present`)
69
+ }
70
+ return {
71
+ schema_version: 1,
72
+ source,
73
+ provider,
74
+ model,
75
+ ...(reasoningEffort ? { reasoning_effort: reasoningEffort } : {}),
76
+ }
77
+ }
78
+
36
79
  export async function computeCandidate(candidateDir) {
37
80
  const root = path.resolve(candidateDir)
38
81
  if (!(await stat(root)).isDirectory()) throw new Error(`Candidate path is not a directory: ${root}`)
@@ -83,6 +126,7 @@ async function validateCandidateContract(root) {
83
126
  if (credentialPaths.length > 0) {
84
127
  throw new Error(`Candidate contains credential-bearing files: ${credentialPaths.join(', ')}; inject credentials at runtime instead`)
85
128
  }
129
+ await loadModelBinding(root)
86
130
  }
87
131
 
88
132
  export async function snapshotCandidate(candidateDir, options = {}) {
@@ -96,11 +140,18 @@ export async function snapshotCandidate(candidateDir, options = {}) {
96
140
  }
97
141
  const candidateId = options.candidateId ?? packageJson.name
98
142
  const version = options.version ?? packageJson.version
99
- const runtimeVersion = options.runtimeVersion ?? '0.1.0-rc.6'
100
- if (!candidateId || !version || !runtimeVersion) {
101
- throw new Error('Candidate id, version, and runtime version must not be empty; set package.json name/version or pass explicit values')
143
+ if (!candidateId || !version) {
144
+ throw new Error('Candidate id and version must not be empty; set package.json name/version or pass explicit values')
102
145
  }
103
146
  const computed = await computeCandidate(root)
147
+ const metadata = { ...(options.metadata ?? {}) }
148
+ const modelBinding = await loadModelBinding(root)
149
+ if (modelBinding) {
150
+ if (metadata.model_binding !== undefined && JSON.stringify(metadata.model_binding) !== JSON.stringify(modelBinding)) {
151
+ throw new Error('Candidate metadata model_binding must match model-binding.json')
152
+ }
153
+ metadata.model_binding = modelBinding
154
+ }
104
155
  const manifest = {
105
156
  schema_version: 1,
106
157
  candidate_id: String(candidateId),
@@ -109,11 +160,13 @@ export async function snapshotCandidate(candidateDir, options = {}) {
109
160
  created_at: new Date().toISOString(),
110
161
  runtime: {
111
162
  kind: 'deepseek-harness',
112
- version: String(runtimeVersion),
163
+ policy: RUNTIME_POLICY,
164
+ version: DSH_RUNTIME_VERSION,
165
+ package: CANDIDATE_ACP_PACKAGE,
113
166
  transport: 'acp',
114
167
  },
115
168
  files: computed.files,
116
- metadata: options.metadata ?? {},
169
+ metadata,
117
170
  }
118
171
  await mkdir(root, { recursive: true })
119
172
  await writeFile(path.join(root, MANIFEST_NAME), `${JSON.stringify(manifest, null, 2)}\n`)