dsh-harbor-evolution 0.7.2 → 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,8 +14,8 @@ 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.2` in a managed Python environment.
18
- - `dsh-harbor-evolution@0.7.2` 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
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
 
@@ -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
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(''),
@@ -61,9 +61,16 @@ function toolProjectRoot(exec) {
61
61
  return path.resolve(cwd)
62
62
  }
63
63
 
64
+ export function synchronizeWorkbenchProjectRoot(service, exec) {
65
+ const projectRoot = toolProjectRoot(exec)
66
+ service.activateProjectRoot(projectRoot, 'agent-session')
67
+ return projectRoot
68
+ }
69
+
64
70
  export function apply(ctx, config) {
65
71
  const resolved = {
66
72
  ...config,
73
+ runtimePolicy: RUNTIME_POLICY,
67
74
  projectRoot: path.resolve(config.projectRoot),
68
75
  harborBin: config.harborBin || process.env.HARBOR_BIN || checkoutExecutable('harbor'),
69
76
  harborDshBin: config.harborDshBin || process.env.HARBOR_DSH_BIN || checkoutExecutable('harbor-dsh'),
@@ -74,12 +81,12 @@ export function apply(ctx, config) {
74
81
  ),
75
82
  }
76
83
  const modelRuntime = new CandidateModelRuntime(ctx, resolved)
77
- const metadata = { pluginVersion: packageJson.version }
84
+ const metadata = { pluginVersion: packageJson.version, projectRootSource: 'configured' }
78
85
  const service = new EvolutionService(resolved, metadata, modelRuntime)
79
- const serviceForTool = exec => new EvolutionService({
80
- ...resolved,
81
- projectRoot: toolProjectRoot(exec),
82
- }, metadata, modelRuntime)
86
+ const serviceForTool = exec => {
87
+ const projectRoot = synchronizeWorkbenchProjectRoot(service, exec)
88
+ return new EvolutionService({ ...resolved, projectRoot }, metadata, modelRuntime)
89
+ }
83
90
 
84
91
  ctx.skills.register(loadBundledSkill())
85
92
  installDashboardWeb(ctx, service)
@@ -94,11 +101,18 @@ export function apply(ctx, config) {
94
101
  },
95
102
  }, (args, exec) => serviceForTool(exec).snapshot(args)))
96
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()))
109
+
97
110
  ctx.tools.register(jsonTool({
98
111
  name: 'harbor_evolution_init',
99
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.',
100
113
  parameters: {
101
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.' },
102
116
  stackId: { type: 'string', required: true },
103
117
  stackVersion: { type: 'string', required: true },
104
118
  datasetId: { type: 'string', required: true },
@@ -131,6 +145,16 @@ export function apply(ctx, config) {
131
145
  },
132
146
  }, (args, exec) => serviceForTool(exec).doctor(args)))
133
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)))
157
+
134
158
  ctx.tools.register(jsonTool({
135
159
  name: 'harbor_dataset_validate',
136
160
  description: 'Validate dataset-manifest.json, task uniqueness, instructions, paths, sensitive metadata, and the immutable source digest.',
@@ -211,6 +235,7 @@ export function apply(ctx, config) {
211
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.',
212
236
  parameters: {
213
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.' },
214
239
  groundTruthId: { type: 'string', required: true },
215
240
  version: { type: 'string', required: true },
216
241
  sourceKind: { type: 'string', required: true, description: 'human, programmatic, consensus, model, or external' },
@@ -227,6 +252,7 @@ export function apply(ctx, config) {
227
252
  groundTruthPath: { type: 'string', description: 'Defaults to .harbor/ground-truth.json' },
228
253
  observationsPath: { type: 'string', required: true },
229
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.' },
230
256
  },
231
257
  }, (args, exec) => serviceForTool(exec).evaluatorMetaEvaluate(args)))
232
258
 
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`)