dsh-harbor-evolution 0.7.0 → 0.7.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/README.md CHANGED
@@ -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.0` in a managed Python environment.
18
- - `dsh-harbor-evolution@0.7.0` in the selected DSH profile.
17
+ - `harbor-dsh-evolution==0.7.1` in a managed Python environment.
18
+ - `dsh-harbor-evolution@0.7.1` in the selected DSH profile.
19
19
 
20
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.
21
21
 
@@ -57,6 +57,12 @@ The Web UI is intentionally read-only. Starting an evaluation or deciding promot
57
57
 
58
58
  A direct evaluation requires `candidatePath`, `datasetPath`, `stackPath`, and explicit `mode`; `promotion-eligible` additionally requires `policyPath`. Prefer the Skill because it will not run or compare Jobs until the material identities and evaluation contract are resolved.
59
59
 
60
+ ## Candidate model binding
61
+
62
+ Before each Job, the Plugin snapshots the current DSH Agent selection—provider, model, and reasoning effort—then starts a per-Job local Model Broker. The Candidate uses the temporary `dsh-host` adapter through `dsh-host-broker` / `dsh-host-model-gateway/v1`; it receives only a short-lived Job capability file, never GPT Auth, Codex OAuth, or an upstream API key.
63
+
64
+ `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
+
60
66
  `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.
61
67
 
62
68
  ## What setup writes
package/index.js CHANGED
@@ -5,11 +5,12 @@ import path from 'node:path'
5
5
  import { fileURLToPath } from 'node:url'
6
6
 
7
7
  import { loadBundledSkill } from './lib/official-skill.js'
8
+ import { CandidateModelRuntime } from './lib/model-runtime.js'
8
9
  import { EvolutionService } from './lib/service.js'
9
10
  import { installDashboardWeb } from './lib/web.js'
10
11
 
11
12
  export const name = 'harbor-evolution'
12
- export const inject = ['tools', 'skills']
13
+ export const inject = ['tools', 'skills', 'llm', 'agentDefaultModel']
13
14
 
14
15
  const packageDir = path.dirname(fileURLToPath(import.meta.url))
15
16
  const checkoutPythonPackage = path.resolve(packageDir, '../harbor-plugin')
@@ -30,6 +31,13 @@ export const Config = Schema.object({
30
31
  pluginImportPath: Schema.string().default('dsh-evolution'),
31
32
  pythonPath: Schema.string().default(''),
32
33
  timeoutMs: Schema.number().default(1800000),
34
+ candidateProvider: Schema.string().default(''),
35
+ candidateModel: Schema.string().default(''),
36
+ candidateReasoningEffort: Schema.string().default(''),
37
+ modelBrokerBindHost: Schema.string().default('127.0.0.1'),
38
+ modelBrokerAdvertisedHost: Schema.string().default('host.docker.internal'),
39
+ modelBrokerMaxRequests: Schema.number().min(1).default(1000),
40
+ modelBrokerMaxRequestBytes: Schema.number().min(1024).default(33554432),
33
41
  })
34
42
 
35
43
  function jsonTool(definition, execute) {
@@ -57,7 +65,8 @@ export function apply(ctx, config) {
57
65
  : ''
58
66
  ),
59
67
  }
60
- const service = new EvolutionService(resolved, { pluginVersion: packageJson.version })
68
+ const modelRuntime = new CandidateModelRuntime(ctx, resolved)
69
+ const service = new EvolutionService(resolved, { pluginVersion: packageJson.version }, modelRuntime)
61
70
 
62
71
  ctx.skills.register(loadBundledSkill())
63
72
  installDashboardWeb(ctx, service)
@@ -103,6 +112,9 @@ export function apply(ctx, config) {
103
112
  stackPath: { type: 'string', required: true },
104
113
  policyPath: { type: 'string' },
105
114
  mode: { type: 'string', required: true },
115
+ candidateProvider: { type: 'string', description: 'Optional Candidate provider. Supply it together with candidateModel; defaults to the current DSH Agent model.' },
116
+ candidateModel: { type: 'string' },
117
+ candidateReasoningEffort: { type: 'string' },
106
118
  },
107
119
  }, args => service.doctor(args)))
108
120
 
@@ -124,6 +136,9 @@ export function apply(ctx, config) {
124
136
  datasetPath: { type: 'string', required: true },
125
137
  stackPath: { type: 'string', required: true },
126
138
  mode: { type: 'string', required: true },
139
+ candidateProvider: { type: 'string', description: 'Optional Candidate provider. Supply it together with candidateModel; defaults to the current DSH Agent model.' },
140
+ candidateModel: { type: 'string' },
141
+ candidateReasoningEffort: { type: 'string' },
127
142
  },
128
143
  }, args => service.previewContext(args)))
129
144
 
@@ -139,6 +154,9 @@ export function apply(ctx, config) {
139
154
  mode: { type: 'string', required: true },
140
155
  policyPath: { type: 'string' },
141
156
  jobName: { type: 'string' },
157
+ candidateProvider: { type: 'string', description: 'Optional Candidate provider. Supply it together with candidateModel; defaults to the current DSH Agent model and is frozen before the Job starts.' },
158
+ candidateModel: { type: 'string' },
159
+ candidateReasoningEffort: { type: 'string' },
142
160
  },
143
161
  }, args => service.run(args)))
144
162
 
package/lib/candidate.js CHANGED
@@ -4,7 +4,7 @@ import path from 'node:path'
4
4
 
5
5
  export const MANIFEST_NAME = 'candidate-manifest.json'
6
6
  const DIGEST_PREFIX = Buffer.from('harbor-dsh-candidate-v1\0')
7
- const EXCLUDED_DIRS = new Set(['.git', 'node_modules', '__pycache__'])
7
+ const EXCLUDED_DIRS = new Set(['.git', 'node_modules', '__pycache__', '.harbor-runtime'])
8
8
  const EXCLUDED_FILES = new Set([MANIFEST_NAME, '.DS_Store'])
9
9
  const LOCKFILES = ['package-lock.json', 'pnpm-lock.yaml', 'yarn.lock', 'bun.lock', 'bun.lockb']
10
10
  const CREDENTIAL_FILES = new Set([
@@ -57,6 +57,12 @@ export async function computeCandidate(candidateDir) {
57
57
  }
58
58
 
59
59
  async function validateCandidateContract(root) {
60
+ try {
61
+ await stat(path.join(root, '.harbor-runtime'))
62
+ throw new Error('Candidate must not contain the reserved .harbor-runtime path')
63
+ } catch (error) {
64
+ if (error.code !== 'ENOENT') throw error
65
+ }
60
66
  for (const required of ['cordis.yml', 'package.json']) {
61
67
  try {
62
68
  if (!(await stat(path.join(root, required))).isFile()) throw new Error()
package/lib/evolution.js CHANGED
@@ -119,6 +119,19 @@ function strictInputs(config, args) {
119
119
  return { projectRoot, candidate, dataset, stack, jobs, mode, policy }
120
120
  }
121
121
 
122
+ function candidateModelCliArgs(binding) {
123
+ if (!binding?.provider || !binding?.model) throw new Error('Candidate model binding is required')
124
+ return [
125
+ '--candidate-model-provider', binding.provider,
126
+ '--candidate-model', binding.model,
127
+ ...(binding.reasoning_effort === undefined
128
+ ? []
129
+ : ['--candidate-reasoning-effort', binding.reasoning_effort]),
130
+ '--candidate-model-transport', binding.transport,
131
+ '--candidate-model-protocol', binding.protocol,
132
+ ]
133
+ }
134
+
122
135
  export async function validateDataset(config, args) {
123
136
  const dataset = resolveWithin(config.projectRoot, args.datasetPath, 'datasetPath')
124
137
  return cliJson(config, ['dataset', 'validate', dataset, '--project-root', config.projectRoot], { allowedExitCodes: [0, 2] })
@@ -157,11 +170,12 @@ export async function previewContext(config, args) {
157
170
  '--stack', inputs.stack,
158
171
  '--jobs-dir', inputs.jobs,
159
172
  '--mode', inputs.mode,
173
+ ...candidateModelCliArgs(args.candidateModelBinding),
160
174
  ])
161
175
  return { manifest, ...preview }
162
176
  }
163
177
 
164
- export async function runEvaluation(config, args) {
178
+ export async function runEvaluation(config, args, modelRuntime) {
165
179
  const manifest = await snapshot(config, args)
166
180
  const inputs = strictInputs(config, args)
167
181
  const datasetValidation = await validateDataset(config, args)
@@ -176,6 +190,7 @@ export async function runEvaluation(config, args) {
176
190
  'context', 'preview', '--project-root', inputs.projectRoot,
177
191
  '--candidate', inputs.candidate, '--dataset', inputs.dataset,
178
192
  '--stack', inputs.stack, '--jobs-dir', inputs.jobs, '--mode', inputs.mode,
193
+ ...candidateModelCliArgs(args.candidateModelBinding),
179
194
  ])
180
195
  const jobName = args.jobName ?? makeJobName(manifest)
181
196
  if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/.test(jobName)) throw new Error('jobName contains unsupported characters')
@@ -186,6 +201,8 @@ export async function runEvaluation(config, args) {
186
201
  '--ak', `candidate_path=${inputs.candidate}`,
187
202
  '--ak', `candidate_version=${manifest.version}`,
188
203
  '--ak', `candidate_digest=${manifest.digest}`,
204
+ '--ak', `candidate_model_provider=${args.candidateModelBinding.provider}`,
205
+ '--ak', `candidate_model=${args.candidateModelBinding.model}`,
189
206
  '--job-name', jobName,
190
207
  '--jobs-dir', inputs.jobs,
191
208
  '--plugin', config.pluginImportPath,
@@ -194,22 +211,47 @@ export async function runEvaluation(config, args) {
194
211
  '--plugin-kwarg', `stack_path=${inputs.stack}`,
195
212
  '--plugin-kwarg', `project_root=${inputs.projectRoot}`,
196
213
  '--plugin-kwarg', `mode=${inputs.mode}`,
214
+ '--plugin-kwarg', `candidate_model_provider=${args.candidateModelBinding.provider}`,
215
+ '--plugin-kwarg', `candidate_model=${args.candidateModelBinding.model}`,
216
+ '--plugin-kwarg', `candidate_model_transport=${args.candidateModelBinding.transport}`,
217
+ '--plugin-kwarg', `candidate_model_protocol=${args.candidateModelBinding.protocol}`,
197
218
  ]
219
+ if (args.candidateModelBinding.reasoning_effort !== undefined) {
220
+ harborArgs.push('--ak', `candidate_reasoning_effort=${args.candidateModelBinding.reasoning_effort}`)
221
+ harborArgs.push('--plugin-kwarg', `candidate_reasoning_effort=${args.candidateModelBinding.reasoning_effort}`)
222
+ }
198
223
  if (inputs.policy) harborArgs.push('--plugin-kwarg', `policy_path=${inputs.policy}`)
199
- const processResult = await runProcess(config.harborBin, harborArgs, {
200
- cwd: config.projectRoot,
201
- timeoutMs: config.timeoutMs,
202
- env: { ...process.env, ...(config.pythonPath ? { PYTHONPATH: config.pythonPath } : {}) },
224
+ const lease = await modelRuntime.openLease(args.candidateModelBinding, {
225
+ candidateDigest: manifest.digest,
226
+ jobName,
203
227
  })
204
- const jobDir = path.join(inputs.jobs, jobName)
205
- const summary = JSON.parse(await readFile(path.join(jobDir, 'evaluation-summary.json'), 'utf8'))
206
- return {
207
- manifest,
208
- job: path.relative(inputs.projectRoot, jobDir),
209
- summary,
210
- doctor,
211
- contextPreview: preview,
212
- process: { code: processResult.code },
228
+ try {
229
+ const processResult = await runProcess(config.harborBin, harborArgs, {
230
+ cwd: config.projectRoot,
231
+ timeoutMs: config.timeoutMs,
232
+ env: {
233
+ ...process.env,
234
+ ...(config.pythonPath ? { PYTHONPATH: config.pythonPath } : {}),
235
+ HSE_MODEL_GATEWAY_URL: lease.endpoint,
236
+ HSE_MODEL_GATEWAY_TOKEN: lease.token,
237
+ HSE_MODEL_GATEWAY_PROVIDER: lease.candidateProvider,
238
+ HSE_MODEL_GATEWAY_INFO: JSON.stringify(lease.modelInfo),
239
+ HSE_MODEL_GATEWAY_PROTOCOL: lease.protocol,
240
+ },
241
+ })
242
+ const jobDir = path.join(inputs.jobs, jobName)
243
+ const summary = JSON.parse(await readFile(path.join(jobDir, 'evaluation-summary.json'), 'utf8'))
244
+ return {
245
+ manifest,
246
+ candidateModelBinding: args.candidateModelBinding,
247
+ job: path.relative(inputs.projectRoot, jobDir),
248
+ summary,
249
+ doctor,
250
+ contextPreview: preview,
251
+ process: { code: processResult.code },
252
+ }
253
+ } finally {
254
+ await lease.close()
213
255
  }
214
256
  }
215
257
 
@@ -0,0 +1,216 @@
1
+ import { randomBytes, timingSafeEqual } from 'node:crypto'
2
+ import { once } from 'node:events'
3
+ import { createServer } from 'node:http'
4
+
5
+ export const MODEL_GATEWAY_PROTOCOL = 'dsh-host-model-gateway/v1'
6
+ export const CANDIDATE_GATEWAY_PROVIDER = 'dsh-host'
7
+
8
+ function nonBlank(value) {
9
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined
10
+ }
11
+
12
+ function sameSecret(expected, actual) {
13
+ const left = Buffer.from(expected)
14
+ const right = Buffer.from(actual)
15
+ return left.length === right.length && timingSafeEqual(left, right)
16
+ }
17
+
18
+ async function readJsonBody(request, maxBytes) {
19
+ const chunks = []
20
+ let size = 0
21
+ for await (const chunk of request) {
22
+ size += chunk.length
23
+ if (size > maxBytes) {
24
+ throw Object.assign(new Error('model gateway request is too large'), { statusCode: 413 })
25
+ }
26
+ chunks.push(chunk)
27
+ }
28
+ let value
29
+ try {
30
+ value = JSON.parse(Buffer.concat(chunks).toString('utf8'))
31
+ } catch {
32
+ throw Object.assign(new Error('model gateway request must be valid JSON'), { statusCode: 400 })
33
+ }
34
+ if (value === null || typeof value !== 'object' || Array.isArray(value) || !Array.isArray(value.messages)) {
35
+ throw Object.assign(new Error('model gateway request requires a messages array'), { statusCode: 400 })
36
+ }
37
+ return value
38
+ }
39
+
40
+ function sendJson(response, statusCode, value) {
41
+ const body = `${JSON.stringify(value)}\n`
42
+ response.writeHead(statusCode, {
43
+ 'content-type': 'application/json; charset=utf-8',
44
+ 'content-length': Buffer.byteLength(body),
45
+ 'cache-control': 'no-store',
46
+ })
47
+ response.end(body)
48
+ }
49
+
50
+ function authorized(request, token) {
51
+ const value = request.headers.authorization
52
+ return typeof value === 'string' && value.startsWith('Bearer ')
53
+ && sameSecret(token, value.slice('Bearer '.length))
54
+ }
55
+
56
+ async function listen(server, host) {
57
+ server.listen(0, host)
58
+ await once(server, 'listening')
59
+ const address = server.address()
60
+ if (address === null || typeof address === 'string') {
61
+ throw new Error('model gateway did not bind a TCP port')
62
+ }
63
+ return address.port
64
+ }
65
+
66
+ /**
67
+ * Freeze the current DSH Agent model per Job, then proxy only that model to an
68
+ * immutable Candidate. The Candidate receives a short-lived capability, never
69
+ * the Host provider's credential or route selection.
70
+ */
71
+ export class CandidateModelRuntime {
72
+ constructor(ctx, config) {
73
+ this.ctx = ctx
74
+ this.config = config
75
+ }
76
+
77
+ async resolve(args = {}) {
78
+ const explicitProvider = nonBlank(args.candidateProvider)
79
+ const explicitModel = nonBlank(args.candidateModel)
80
+ if (Boolean(explicitProvider) !== Boolean(explicitModel)) {
81
+ throw new Error('candidateProvider and candidateModel must be supplied together')
82
+ }
83
+ const configuredProvider = nonBlank(this.config.candidateProvider)
84
+ const configuredModel = nonBlank(this.config.candidateModel)
85
+ if (Boolean(configuredProvider) !== Boolean(configuredModel)) {
86
+ throw new Error('Harbor candidateProvider and candidateModel configuration must be supplied together')
87
+ }
88
+
89
+ const inherited = this.ctx.agentDefaultModel.currentSelection()
90
+ const provider = explicitProvider ?? configuredProvider ?? inherited.provider
91
+ const model = explicitModel ?? configuredModel ?? inherited.model
92
+ const explicitReasoning = nonBlank(args.candidateReasoningEffort)
93
+ const configuredReasoning = nonBlank(this.config.candidateReasoningEffort)
94
+ const canInheritReasoning = provider === inherited.provider && model === inherited.model
95
+ const reasoningEffort = explicitReasoning ?? configuredReasoning
96
+ ?? (canInheritReasoning ? inherited.reasoningEffort : undefined)
97
+
98
+ if (!this.ctx.llm.listProviders().some(item => item.id === provider)) {
99
+ throw new Error(`Candidate model provider "${provider}" is not registered in DeepSeek Harness`)
100
+ }
101
+ const modelInfo = await this.ctx.llm.resolveModelInfo(provider, model)
102
+ if (provider === 'openai-codex') {
103
+ const auth = this.ctx.get('codexAuth')
104
+ if (auth === undefined || typeof auth.status !== 'function') {
105
+ throw new Error('Candidate model openai-codex requires the dsh-codex-auth service')
106
+ }
107
+ const status = await auth.status()
108
+ if (!status.configured) {
109
+ throw new Error('Candidate model openai-codex is not signed in; complete GPT Auth in Settings before starting Harbor')
110
+ }
111
+ }
112
+
113
+ return {
114
+ provider,
115
+ model,
116
+ ...(reasoningEffort === undefined ? {} : { reasoning_effort: String(reasoningEffort) }),
117
+ transport: 'dsh-host-broker',
118
+ protocol: MODEL_GATEWAY_PROTOCOL,
119
+ model_info: modelInfo,
120
+ }
121
+ }
122
+
123
+ async openLease(binding, scope) {
124
+ const token = randomBytes(32).toString('base64url')
125
+ const route = `/harbor-model-gateway/v1/${randomBytes(18).toString('base64url')}`
126
+ const controllers = new Set()
127
+ let requestCount = 0
128
+ const server = createServer(async (request, response) => {
129
+ if (request.url !== route || !authorized(request, token)) {
130
+ sendJson(response, 404, { error: 'not found' })
131
+ return
132
+ }
133
+ if (request.method === 'GET') {
134
+ sendJson(response, 200, {
135
+ protocol: MODEL_GATEWAY_PROTOCOL,
136
+ candidate_digest: scope.candidateDigest,
137
+ job: scope.jobName,
138
+ binding: {
139
+ provider: binding.provider,
140
+ model: binding.model,
141
+ ...(binding.reasoning_effort === undefined ? {} : { reasoning_effort: binding.reasoning_effort }),
142
+ },
143
+ })
144
+ return
145
+ }
146
+ if (request.method !== 'POST') {
147
+ sendJson(response, 405, { error: 'method not allowed' })
148
+ return
149
+ }
150
+ if (requestCount >= this.config.modelBrokerMaxRequests) {
151
+ sendJson(response, 429, { error: 'model gateway request budget exhausted' })
152
+ return
153
+ }
154
+ requestCount += 1
155
+ const controller = new AbortController()
156
+ controllers.add(controller)
157
+ response.on('close', () => {
158
+ if (!response.writableEnded) controller.abort(new Error('Candidate disconnected'))
159
+ })
160
+ try {
161
+ const body = await readJsonBody(request, this.config.modelBrokerMaxRequestBytes)
162
+ const {
163
+ provider: _provider,
164
+ model: _model,
165
+ reasoningEffort: _reasoningEffort,
166
+ signal: _signal,
167
+ ...requestOptions
168
+ } = body
169
+ response.writeHead(200, {
170
+ 'content-type': 'application/x-ndjson; charset=utf-8',
171
+ 'cache-control': 'no-store',
172
+ })
173
+ const stream = this.ctx.llm.stream({
174
+ ...requestOptions,
175
+ provider: binding.provider,
176
+ model: binding.model,
177
+ ...(binding.reasoning_effort === undefined ? {} : { reasoningEffort: binding.reasoning_effort }),
178
+ signal: controller.signal,
179
+ })
180
+ for await (const chunk of stream) {
181
+ if (!response.write(`${JSON.stringify(chunk)}\n`)) await once(response, 'drain')
182
+ }
183
+ response.end()
184
+ } catch (error) {
185
+ if (!response.headersSent) {
186
+ const statusCode = Number.isInteger(error?.statusCode) ? error.statusCode : 502
187
+ const message = error instanceof Error ? error.message : String(error)
188
+ sendJson(response, statusCode, { error: message })
189
+ } else if (!response.writableEnded) {
190
+ response.destroy(error instanceof Error ? error : new Error(String(error)))
191
+ }
192
+ } finally {
193
+ controllers.delete(controller)
194
+ }
195
+ })
196
+
197
+ const port = await listen(server, this.config.modelBrokerBindHost)
198
+ const endpoint = `http://${this.config.modelBrokerAdvertisedHost}:${port}${route}`
199
+ let closed = false
200
+ return {
201
+ protocol: MODEL_GATEWAY_PROTOCOL,
202
+ endpoint,
203
+ token,
204
+ candidateProvider: CANDIDATE_GATEWAY_PROVIDER,
205
+ modelInfo: binding.model_info,
206
+ async close() {
207
+ if (closed) return
208
+ closed = true
209
+ for (const controller of controllers) controller.abort(new Error('Harbor Job ended'))
210
+ const close = new Promise((resolve, reject) => server.close(error => error ? reject(error) : resolve()))
211
+ server.closeAllConnections()
212
+ await close
213
+ },
214
+ }
215
+ }
216
+ }
package/lib/service.js CHANGED
@@ -48,9 +48,10 @@ export async function resolveEvaluatorStackPath(config, governance, explicitPath
48
48
 
49
49
  /** One Host-side boundary shared by Agent tools and the Web dashboard. */
50
50
  export class EvolutionService {
51
- constructor(config, metadata = {}) {
51
+ constructor(config, metadata = {}, modelRuntime) {
52
52
  this.config = config
53
53
  this.metadata = metadata
54
+ this.modelRuntime = modelRuntime
54
55
  }
55
56
 
56
57
  snapshot(args) {
@@ -61,8 +62,9 @@ export class EvolutionService {
61
62
  return initializeProject(this.config, args)
62
63
  }
63
64
 
64
- run(args) {
65
- return runEvaluation(this.config, args)
65
+ async run(args) {
66
+ const candidateModelBinding = await this.modelRuntime.resolve(args)
67
+ return runEvaluation(this.config, { ...args, candidateModelBinding }, this.modelRuntime)
66
68
  }
67
69
 
68
70
  result(args) {
@@ -82,16 +84,19 @@ export class EvolutionService {
82
84
  return compareCandidates(this.config, args)
83
85
  }
84
86
 
85
- doctor(args) {
86
- return runDoctor(this.config, args)
87
+ async doctor(args) {
88
+ const candidateModelBinding = await this.modelRuntime.resolve(args)
89
+ const result = await runDoctor(this.config, args)
90
+ return { ...result, candidate_model_binding: candidateModelBinding }
87
91
  }
88
92
 
89
93
  validateDataset(args) {
90
94
  return validateDataset(this.config, args)
91
95
  }
92
96
 
93
- previewContext(args) {
94
- return previewContext(this.config, args)
97
+ async previewContext(args) {
98
+ const candidateModelBinding = await this.modelRuntime.resolve(args)
99
+ return previewContext(this.config, { ...args, candidateModelBinding })
95
100
  }
96
101
 
97
102
  dashboard() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-harbor-evolution",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "DeepSeek Harness plugin and bundled Skill for safely evolving Cordis Candidates with Harbor.",
5
5
  "type": "module",
6
6
  "main": "index.js",