dsh-harbor-evolution 0.8.2 → 0.9.2

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/lib/evolution.js CHANGED
@@ -2,6 +2,8 @@ import { mkdir, readFile, rename, writeFile } from 'node:fs/promises'
2
2
  import path from 'node:path'
3
3
 
4
4
  import { MANIFEST_NAME, snapshotCandidate } from './candidate.js'
5
+ import { loadCandidateRuntime } from './candidate-runtime.js'
6
+ import { redactCredentialText, redactLocalPaths, redactOpaqueSecretText } from './credential-redaction.js'
5
7
  import { runProcess } from './process.js'
6
8
 
7
9
  export function resolveWithin(root, value, label) {
@@ -145,7 +147,8 @@ async function cliJson(config, args, { allowedExitCodes = [0], input } = {}) {
145
147
  })
146
148
  } catch (error) {
147
149
  const detail = error?.result?.stderr?.trim().split('\n').at(-1)?.replace(/^[A-Za-z]+Error:\s*/, '')
148
- throw new Error(detail || error.message)
150
+ const sanitized = redactDiagnostic(detail || error.message).slice(0, 512)
151
+ throw new Error(sanitized || 'HARBOR_DSH_COMMAND_FAILED: harbor-dsh did not return a safe error detail')
149
152
  }
150
153
  try {
151
154
  return JSON.parse(result.stdout)
@@ -154,6 +157,15 @@ async function cliJson(config, args, { allowedExitCodes = [0], input } = {}) {
154
157
  }
155
158
  }
156
159
 
160
+ export function historicalDockerBlockers(value) {
161
+ const findings = Array.isArray(value?.findings) ? value.findings : []
162
+ return findings.filter(item => (
163
+ item?.level === 'error'
164
+ && typeof item.code === 'string'
165
+ && item.code.startsWith('DOCKER_')
166
+ ))
167
+ }
168
+
157
169
  export async function inspectEvaluator(config, args = {}) {
158
170
  const stack = resolveWithin(config.projectRoot, args.stackPath ?? '.harbor/evaluation-stack.yml', 'stackPath')
159
171
  return cliJson(config, [
@@ -239,10 +251,91 @@ function candidateModelCliArgs(binding) {
239
251
  }
240
252
 
241
253
  export function redactDiagnostic(value) {
242
- return String(value ?? '')
243
- .replace(/(authorization\s*[:=]\s*(?:bearer\s+)?)[^\s,'"}]+/gi, '$1[redacted]')
244
- .replace(/(bearer\s+)[A-Za-z0-9._~+\/-]+/gi, '$1[redacted]')
245
- .replace(/((?:api[_-]?key|token|secret|password)\s*[:=]\s*)[^\s,'"}]+/gi, '$1[redacted]')
254
+ const credentials = redactCredentialText(String(value ?? ''), '[redacted]')
255
+ const opaque = redactOpaqueSecretText(credentials, kind => `[redacted ${kind}]`)
256
+ return redactLocalPaths(opaque)
257
+ }
258
+
259
+ const RUN_RECEIPT_TEXT_LIMIT = 256
260
+ const RUN_SUMMARY_COUNT_FIELDS = [
261
+ 'n_trials',
262
+ 'n_valid_scores',
263
+ 'n_invalid_scores',
264
+ 'n_exceptions',
265
+ 'n_unscored_trials',
266
+ 'n_discovered_trials',
267
+ ]
268
+
269
+ function boundedRunReceiptText(value, fallback = 'unknown') {
270
+ const sanitized = redactDiagnostic(value).replace(/[\r\n\t]+/g, ' ').trim()
271
+ return (sanitized || fallback).slice(0, RUN_RECEIPT_TEXT_LIMIT)
272
+ }
273
+
274
+ function safeNonNegativeInteger(value) {
275
+ return Number.isSafeInteger(value) && value >= 0 ? value : undefined
276
+ }
277
+
278
+ function safeFiniteNumber(value) {
279
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined
280
+ }
281
+
282
+ function narrowRunSummary(summary, { historical = false } = {}) {
283
+ const receipt = {
284
+ artifact_ref: 'evaluation-summary.json',
285
+ artifact_validation: { valid: summary?.artifact_validation?.valid === true },
286
+ }
287
+ const schemaVersion = safeNonNegativeInteger(summary?.schema_version)
288
+ if (schemaVersion !== undefined) receipt.schema_version = schemaVersion
289
+ for (const key of RUN_SUMMARY_COUNT_FIELDS) {
290
+ const value = safeNonNegativeInteger(summary?.[key])
291
+ if (value !== undefined) receipt[key] = value
292
+ }
293
+ if (historical) {
294
+ const coverage = {}
295
+ for (const key of HISTORICAL_COVERAGE_KEYS) {
296
+ const value = safeFiniteNumber(summary?.coverage?.[key])
297
+ if (value !== undefined) coverage[key] = value
298
+ }
299
+ receipt.coverage = coverage
300
+ }
301
+ return receipt
302
+ }
303
+
304
+ /**
305
+ * A mutating tool receipt is intentionally not an artifact read path. Return
306
+ * only bounded scalar status plus a reference that can be opened through
307
+ * harbor_eval_result, where untrusted artifact content receives its dedicated
308
+ * allowlist/redaction policy.
309
+ */
310
+ export function buildEvaluationRunReceipt({ jobName, mode, summary, processCode }) {
311
+ return {
312
+ schema_version: 1,
313
+ jobKind: 'candidate-evaluation',
314
+ mode: mode === 'promotion-eligible' ? 'promotion-eligible' : 'diagnostic',
315
+ status: 'completed',
316
+ job: boundedRunReceiptText(jobName),
317
+ summary: narrowRunSummary(summary),
318
+ process: { code: safeNonNegativeInteger(processCode) ?? null },
319
+ }
320
+ }
321
+
322
+ export function buildHistoricalRunReceipt({ jobName, summary, processCode }) {
323
+ return {
324
+ schema_version: 1,
325
+ jobKind: 'historical-generation-evaluation',
326
+ executionMode: 'observe-existing',
327
+ promotionEligible: false,
328
+ status: 'completed',
329
+ job: boundedRunReceiptText(jobName),
330
+ summary: narrowRunSummary(summary, { historical: true }),
331
+ completion: {
332
+ schema_version: 1,
333
+ status: 'completed',
334
+ valid: true,
335
+ artifact_ref: 'historical-evaluation-complete.json',
336
+ },
337
+ process: { code: safeNonNegativeInteger(processCode) ?? null },
338
+ }
246
339
  }
247
340
 
248
341
  export function classifyHarborFailure(value) {
@@ -288,7 +381,7 @@ export async function explainHarborFailure(error, jobDir) {
288
381
  const suggestions = classifyHarborFailure(detail || error?.message)
289
382
  const lines = [
290
383
  `HARBOR_JOB_FAILED: Harbor exited with code ${error?.result?.code ?? 'unknown'}.`,
291
- `jobPath: ${jobDir}`,
384
+ 'jobPath: [local path]',
292
385
  ...suggestions.map(item => `nextStep[${item.code}]: ${item.action}`),
293
386
  ]
294
387
  if (detail.trim()) lines.push('diagnosticTail:', detail.trim())
@@ -350,8 +443,11 @@ export async function previewContext(config, args) {
350
443
  }
351
444
 
352
445
  export async function runEvaluation(config, args, modelRuntime) {
353
- const manifest = await snapshot(config, args)
354
446
  const inputs = strictInputs(config, args)
447
+ // Do not rely on a possibly older Python Doctor to enforce a contract that
448
+ // its agent may not understand. Legacy snapshots remain readable, not runnable.
449
+ await loadCandidateRuntime(inputs.candidate, { required: true })
450
+ const manifest = await snapshot(config, args)
355
451
  const datasetValidation = await validateDataset(config, args)
356
452
  if (!datasetValidation.valid) {
357
453
  throw new Error(
@@ -360,10 +456,13 @@ export async function runEvaluation(config, args, modelRuntime) {
360
456
  )
361
457
  }
362
458
  const doctor = await runDoctor(config, args)
363
- const runtimeBlockers = doctor.findings.filter(item => item.level === 'error' && item.code.startsWith('DOCKER_'))
459
+ const runtimeBlockers = doctor.findings.filter(item => item.level === 'error' && (item.code.startsWith('DOCKER_') || item.code.startsWith('CANDIDATE_RUNTIME_')))
364
460
  if (runtimeBlockers.length) {
365
461
  throw new Error(`Runtime Doctor blocked Harbor Job:\n${runtimeBlockers.map(item => `${item.code}: ${item.message}`).join('\n')}`)
366
462
  }
463
+ if (!doctor.findings.some(item => item.level === 'info' && item.code === 'CANDIDATE_RUNTIME_VERIFIED')) {
464
+ throw new Error('CANDIDATE_RUNTIME_ADAPTER_UNSUPPORTED: update the Python Adapter; it has not verified the Candidate-owned local ACP runtime. No model lease or Harbor Job was started.')
465
+ }
367
466
  if (inputs.mode === 'promotion-eligible' && !doctor.promotion_ready) {
368
467
  throw new Error(`Architecture Doctor blocked promotion-eligible Job: ${doctor.findings.filter(item => item.level === 'error').map(item => item.code).join(', ')}`)
369
468
  }
@@ -427,15 +526,12 @@ export async function runEvaluation(config, args, modelRuntime) {
427
526
  throw await explainHarborFailure(error, jobDir)
428
527
  }
429
528
  const summary = JSON.parse(await readFile(path.join(jobDir, 'evaluation-summary.json'), 'utf8'))
430
- return {
431
- manifest,
432
- candidateModelBinding: args.candidateModelBinding,
433
- job: path.relative(inputs.projectRoot, jobDir),
529
+ return buildEvaluationRunReceipt({
530
+ jobName,
531
+ mode: inputs.mode,
434
532
  summary,
435
- doctor,
436
- contextPreview: preview,
437
- process: { code: processResult.code },
438
- }
533
+ processCode: processResult.code,
534
+ })
439
535
  } finally {
440
536
  await lease.close()
441
537
  }
@@ -467,6 +563,16 @@ export async function runHistoricalEvaluation(config, args, modelRuntime) {
467
563
  ])
468
564
  const dataset = resolveWithin(projectRoot, materialized.dataset_path ?? output, 'historicalDataset')
469
565
  const stack = resolveWithin(projectRoot, materialized.stack_path, 'historicalStack')
566
+ // Fail fast on Docker runtime blockers (e.g. an unresolvable credential
567
+ // helper) before Harbor creates a Job whose Trials would all fail during
568
+ // environment setup and only report missing downstream artifacts.
569
+ const dockerCheck = await cliJson(config, ['docker-check'], { allowedExitCodes: [0, 2] })
570
+ const dockerBlockers = historicalDockerBlockers(dockerCheck)
571
+ if (dockerBlockers.length) {
572
+ throw new Error(
573
+ `HISTORICAL_DOCKER_PREFLIGHT_FAILED: Docker runtime preflight blocked the Historical Job:\n${dockerBlockers.map(item => `${item.code}: ${item.message}`).join('\n')}`,
574
+ )
575
+ }
470
576
  const batch = JSON.parse(await readFile(batchPath, 'utf8'))
471
577
  const jobs = resolveWithin(projectRoot, config.jobsDir, 'jobsDir')
472
578
  const jobName = args.jobName ?? makeHistoricalJobName(batch)
@@ -541,23 +647,11 @@ export async function runHistoricalEvaluation(config, args, modelRuntime) {
541
647
  batchId: batch.batch_id,
542
648
  recordCount: Array.isArray(batch.records) ? batch.records.length : undefined,
543
649
  })
544
- return {
545
- judgeModelBinding: {
546
- provider: args.judgeBinding.provider,
547
- model: args.judgeBinding.model,
548
- ...(args.judgeBinding.reasoning_effort === undefined
549
- ? {}
550
- : { reasoning_effort: args.judgeBinding.reasoning_effort }),
551
- },
552
- job: path.relative(projectRoot, jobDir).split(path.sep).join('/'),
650
+ return buildHistoricalRunReceipt({
651
+ jobName,
553
652
  summary,
554
- completion,
555
- materialized: {
556
- dataset: path.relative(projectRoot, dataset).split(path.sep).join('/'),
557
- stack: path.relative(projectRoot, stack).split(path.sep).join('/'),
558
- },
559
- process: { code: processResult.code },
560
- }
653
+ processCode: processResult.code,
654
+ })
561
655
  } finally {
562
656
  await lease.close()
563
657
  }
@@ -0,0 +1,102 @@
1
+ import { realpathSync } from 'node:fs'
2
+ import path from 'node:path'
3
+
4
+ const LOCKED_MESSAGE = 'HISTORICAL_JOB_ALREADY_RUNNING: wait for the current Historical Session Job to finish'
5
+
6
+ function lockError(code, message) {
7
+ const error = new Error(`${code}: ${message}`)
8
+ error.code = code
9
+ return error
10
+ }
11
+
12
+ function realpathIfPresent(value) {
13
+ try {
14
+ return realpathSync.native(value)
15
+ } catch (error) {
16
+ if (error?.code === 'ENOENT') return undefined
17
+ throw lockError('HISTORICAL_LOCK_SCOPE_INVALID', 'the Historical workspace path could not be resolved safely')
18
+ }
19
+ }
20
+
21
+ function realpathFromDeepestExistingAncestor(value) {
22
+ let current = path.resolve(value)
23
+ const missingSegments = []
24
+ while (true) {
25
+ const physical = realpathIfPresent(current)
26
+ if (physical) return path.resolve(physical, ...missingSegments)
27
+ const parent = path.dirname(current)
28
+ if (parent === current) {
29
+ throw lockError('HISTORICAL_LOCK_SCOPE_INVALID', 'the Historical workspace path has no resolvable ancestor')
30
+ }
31
+ missingSegments.unshift(path.basename(current))
32
+ current = parent
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Resolve the write target used by Historical evaluation. The absolute Jobs
38
+ * directory, rather than a UI workspace label, is the collision boundary:
39
+ * nested project roots that point at the same directory must share a lock.
40
+ */
41
+ export function historicalRunScope(config = {}) {
42
+ const projectRoot = String(config.projectRoot ?? '')
43
+ const jobsDir = String(config.jobsDir ?? 'jobs')
44
+ if (!projectRoot || !path.isAbsolute(projectRoot)) {
45
+ throw lockError('HISTORICAL_LOCK_SCOPE_INVALID', 'projectRoot must be an absolute path')
46
+ }
47
+ if (!jobsDir) {
48
+ throw lockError('HISTORICAL_LOCK_SCOPE_INVALID', 'jobsDir must be a non-empty path inside projectRoot')
49
+ }
50
+ const root = path.resolve(projectRoot)
51
+ const target = path.resolve(root, jobsDir)
52
+ const relative = path.relative(root, target)
53
+ if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
54
+ throw lockError('HISTORICAL_LOCK_SCOPE_INVALID', 'jobsDir must stay inside projectRoot')
55
+ }
56
+ return realpathFromDeepestExistingAncestor(target)
57
+ }
58
+
59
+ /** Process-local, fail-closed lease registry for Historical Job writers. */
60
+ export class HistoricalRunLock {
61
+ constructor() {
62
+ this.active = new Map()
63
+ }
64
+
65
+ acquire(config, owner = {}) {
66
+ const scope = historicalRunScope(config)
67
+ if (this.active.has(scope)) {
68
+ const error = new Error(LOCKED_MESSAGE)
69
+ error.code = 'HISTORICAL_JOB_ALREADY_RUNNING'
70
+ throw error
71
+ }
72
+
73
+ const token = Symbol('historical-run-lease')
74
+ this.active.set(scope, { token, owner: { ...owner } })
75
+ let released = false
76
+ return Object.freeze({
77
+ scope,
78
+ release: () => {
79
+ if (released) return false
80
+ released = true
81
+ const current = this.active.get(scope)
82
+ if (current?.token === token) this.active.delete(scope)
83
+ return true
84
+ },
85
+ })
86
+ }
87
+
88
+ async runExclusive(config, task, owner = {}) {
89
+ if (typeof task !== 'function') {
90
+ throw lockError('HISTORICAL_LOCK_TASK_INVALID', 'the protected operation must be a function')
91
+ }
92
+ const lease = this.acquire(config, owner)
93
+ try {
94
+ return await task()
95
+ } finally {
96
+ lease.release()
97
+ }
98
+ }
99
+ }
100
+
101
+ /** Shared across every Harbor plugin/controller instance in this Node process. */
102
+ export const historicalRunLock = new HistoricalRunLock()
@@ -1,5 +1,8 @@
1
1
  import { randomUUID } from 'node:crypto'
2
2
 
3
+ import { redactDiagnostic } from './evolution.js'
4
+ import { historicalRunLock } from './historical-run-lock.js'
5
+
3
6
  const OPERATION_RETENTION_MS = 60 * 60 * 1000
4
7
 
5
8
  function timestamp(now) {
@@ -9,9 +12,7 @@ function timestamp(now) {
9
12
 
10
13
  function publicError(error) {
11
14
  const raw = error instanceof Error ? error.message : String(error)
12
- const message = raw
13
- .replace(/(?:\/[A-Za-z0-9._ -]+){2,}/g, '[local path]')
14
- .replace(/[A-Za-z]:\\[^\s]+/g, '[local path]')
15
+ const message = redactDiagnostic(raw).slice(0, 512)
15
16
  const match = message.match(/^([A-Z][A-Z0-9_]+):\s*(.*)$/s)
16
17
  return {
17
18
  code: match?.[1] ?? 'HISTORICAL_JOB_FAILED',
@@ -44,6 +45,7 @@ export class HistoricalWebController {
44
45
  randomId = () => randomUUID(),
45
46
  schedule = callback => queueMicrotask(callback),
46
47
  operationRetentionMs = OPERATION_RETENTION_MS,
48
+ runLock = historicalRunLock,
47
49
  }) {
48
50
  this.service = service
49
51
  this.sessionDiagnostic = sessionDiagnostic
@@ -51,6 +53,7 @@ export class HistoricalWebController {
51
53
  this.randomId = randomId
52
54
  this.schedule = schedule
53
55
  this.operationRetentionMs = operationRetentionMs
56
+ this.runLock = runLock
54
57
  this.previews = new Map()
55
58
  this.operations = new Map()
56
59
  this.consumedPreviews = new Map()
@@ -71,19 +74,27 @@ export class HistoricalWebController {
71
74
  }
72
75
  }
73
76
 
74
- _activeOperation(workspace) {
77
+ _activeWorkspaceOperation(workspace) {
75
78
  return [...this.operations.values()]
76
79
  .filter(item => item.workspace === workspace && ['queued', 'running'].includes(item.status))
77
80
  .sort((left, right) => right.createdAt.localeCompare(left.createdAt))[0]
78
81
  }
79
82
 
83
+ _activeOwnedOperation(workspace, ownerSessionId) {
84
+ return [...this.operations.values()]
85
+ .filter(item => item.workspace === workspace && item.ownerSessionId === ownerSessionId && ['queued', 'running'].includes(item.status))
86
+ .sort((left, right) => right.createdAt.localeCompare(left.createdAt))[0]
87
+ }
88
+
80
89
  async preview(args = {}) {
81
90
  this._cleanup()
82
- const resolved = await this.service.historicalWorkspace({ workspace: args.workspace })
91
+ const ownerSessionId = String(args.sessionId ?? '').trim()
92
+ if (!ownerSessionId) throw new Error('HISTORICAL_SESSION_REQUIRED: a live DSH Session is required')
93
+ const resolved = await this.service.historicalWorkspace({ workspace: args.workspace, sessionId: ownerSessionId })
83
94
  const previewId = this.randomId()
84
95
  const identity = {
85
96
  projectRoot: resolved.projectRoot,
86
- ownerSessionId: `web-historical:${this.randomId()}`,
97
+ ownerSessionId: `web-historical:${ownerSessionId}:${this.randomId()}`,
87
98
  }
88
99
  const preview = await this.sessionDiagnostic.previewWithIdentity({
89
100
  limit: args.limit === undefined ? 10 : args.limit,
@@ -94,6 +105,7 @@ export class HistoricalWebController {
94
105
  this.previews.set(previewId, {
95
106
  previewId,
96
107
  workspace: resolved.workspace,
108
+ ownerSessionId,
97
109
  identity,
98
110
  config: resolved.config,
99
111
  selectionToken,
@@ -110,11 +122,14 @@ export class HistoricalWebController {
110
122
 
111
123
  async run(args = {}) {
112
124
  this._cleanup()
125
+ const ownerSessionId = String(args.sessionId ?? '').trim()
126
+ if (!ownerSessionId) throw new Error('HISTORICAL_SESSION_REQUIRED: a live DSH Session is required')
113
127
  const previewId = String(args.previewId ?? '')
114
128
  if (!previewId) throw new Error('HISTORICAL_PREVIEW_REQUIRED: preview the recent Sessions before confirming the Job')
115
129
  const existingOperationId = this.consumedPreviews.get(previewId)
116
130
  if (existingOperationId) {
117
131
  const existing = this.operations.get(existingOperationId)
132
+ if (existing?.ownerSessionId !== ownerSessionId) throw new Error('HISTORICAL_PREVIEW_SESSION_MISMATCH: the preview belongs to another DSH Session')
118
133
  if (args.workspace && existing?.workspace !== args.workspace) {
119
134
  throw new Error('HISTORICAL_PREVIEW_WORKSPACE_MISMATCH: the workspace changed; preview again')
120
135
  }
@@ -122,33 +137,47 @@ export class HistoricalWebController {
122
137
  }
123
138
  const preview = this.previews.get(previewId)
124
139
  if (!preview) throw new Error('HISTORICAL_PREVIEW_INVALID: this preview expired or was already discarded; preview again')
140
+ if (preview.ownerSessionId !== ownerSessionId) throw new Error('HISTORICAL_PREVIEW_SESSION_MISMATCH: the preview belongs to another DSH Session')
125
141
  if (args.workspace && preview.workspace !== args.workspace) {
126
142
  throw new Error('HISTORICAL_PREVIEW_WORKSPACE_MISMATCH: the workspace changed; preview again')
127
143
  }
128
- const active = this._activeOperation(preview.workspace)
144
+ const active = this._activeWorkspaceOperation(preview.workspace)
129
145
  if (active) {
130
146
  throw new Error('HISTORICAL_JOB_ALREADY_RUNNING: wait for the current Historical Session Job to finish')
131
147
  }
148
+ const lease = this.runLock.acquire(preview.config, {
149
+ channel: 'web',
150
+ workspace: preview.workspace,
151
+ })
132
152
  const operationId = this.randomId()
133
153
  const createdAt = timestamp(this.now).toISOString()
134
154
  const operation = {
135
155
  operationId,
136
156
  workspace: preview.workspace,
157
+ ownerSessionId,
137
158
  status: 'queued',
138
159
  selectedCount: preview.selectedCount,
139
160
  createdAt,
140
161
  }
141
- this.previews.delete(previewId)
142
- this.operations.set(operationId, operation)
143
- this.consumedPreviews.set(previewId, operationId)
144
- this.schedule(() => { void this._execute(operation, preview) })
162
+ try {
163
+ this.previews.delete(previewId)
164
+ this.operations.set(operationId, operation)
165
+ this.consumedPreviews.set(previewId, operationId)
166
+ this.schedule(() => { void this._execute(operation, preview, lease) })
167
+ } catch (error) {
168
+ this.operations.delete(operationId)
169
+ this.consumedPreviews.delete(previewId)
170
+ this.previews.set(previewId, preview)
171
+ lease.release()
172
+ throw error
173
+ }
145
174
  return publicOperation(operation)
146
175
  }
147
176
 
148
- async _execute(operation, preview) {
149
- operation.status = 'running'
150
- operation.startedAt = timestamp(this.now).toISOString()
177
+ async _execute(operation, preview, lease) {
151
178
  try {
179
+ operation.status = 'running'
180
+ operation.startedAt = timestamp(this.now).toISOString()
152
181
  const result = await this.sessionDiagnostic.runWithIdentity({
153
182
  selectionToken: preview.selectionToken,
154
183
  }, preview.identity, { config: preview.config })
@@ -159,22 +188,29 @@ export class HistoricalWebController {
159
188
  operation.status = 'failed'
160
189
  operation.error = publicError(error)
161
190
  } finally {
162
- operation.finishedAt = timestamp(this.now).toISOString()
191
+ try {
192
+ operation.finishedAt = timestamp(this.now).toISOString()
193
+ } finally {
194
+ lease.release()
195
+ }
163
196
  }
164
197
  }
165
198
 
166
199
  operation(args = {}) {
167
200
  this._cleanup()
201
+ const ownerSessionId = String(args.sessionId ?? '').trim()
202
+ if (!ownerSessionId) throw new Error('HISTORICAL_SESSION_REQUIRED: a live DSH Session is required')
168
203
  const operationId = String(args.operationId ?? '')
169
204
  if (operationId) {
170
205
  const operation = this.operations.get(operationId)
171
206
  if (!operation) return { status: 'idle' }
207
+ if (operation.ownerSessionId !== ownerSessionId) throw new Error('HISTORICAL_OPERATION_SESSION_MISMATCH: the operation belongs to another DSH Session')
172
208
  if (args.workspace && operation.workspace !== args.workspace) {
173
209
  throw new Error('HISTORICAL_OPERATION_WORKSPACE_MISMATCH: the operation belongs to another workspace')
174
210
  }
175
211
  return publicOperation(operation)
176
212
  }
177
213
  if (!args.workspace) return { status: 'idle' }
178
- return publicOperation(this._activeOperation(String(args.workspace)))
214
+ return publicOperation(this._activeOwnedOperation(String(args.workspace), ownerSessionId))
179
215
  }
180
216
  }
@@ -0,0 +1,56 @@
1
+ import { createHash } from 'node:crypto'
2
+
3
+ export const LOCAL_OBJECT_KINDS = new Set(['hypothesis', 'gate-reason', 'metric', 'finding', 'attempt', 'exception', 'evaluator-source', 'trial-set'])
4
+
5
+ function canonical(value) {
6
+ if (Array.isArray(value)) return value.map(canonical)
7
+ if (value && typeof value === 'object') return Object.fromEntries(Object.keys(value).sort().map(key => [key, canonical(value[key])]))
8
+ return value
9
+ }
10
+
11
+ export function localObjectDigest(value) {
12
+ return `sha256:${createHash('sha256').update(JSON.stringify(canonical(value))).digest('hex')}`
13
+ }
14
+
15
+ /** Host-derived selectors. No paths or artifact prose enter message context. */
16
+ export function interactionObjectCatalog(job, jobState, trialState, governance) {
17
+ const artifacts = jobState?.artifacts ?? {}
18
+ const entries = []
19
+ const add = (kind, stage, value, fields = {}) => {
20
+ if (value === undefined || value === null) return
21
+ const sourceDigest = localObjectDigest(value)
22
+ const id = `${kind}-${sourceDigest.slice(7, 31)}`
23
+ entries.push({ ref: { kind, id, job, stage, sourceDigest, ...fields }, value })
24
+ }
25
+ for (const value of (artifacts.optimization?.hypotheses ?? []).slice(0, 100)) add('hypothesis', 'optimizer', value)
26
+ for (const value of (artifacts.promotion?.reasons ?? []).slice(0, 100)) add('gate-reason', 'gate', value)
27
+ for (const [metric, value] of Object.entries(artifacts.summary?.metrics ?? {}).slice(0, 100)) {
28
+ if (typeof value === 'number' && Number.isFinite(value)) add('metric', 'reporter', { metric, value })
29
+ }
30
+ if (trialState) {
31
+ const trial = trialState.lifecycle?.id ?? trialState.trial
32
+ for (const value of (trialState.assessment?.findings ?? []).slice(0, 100)) add('finding', 'judge', value, { trial })
33
+ for (const reason of (trialState.assessment?.score?.invalid_reasons ?? trialState.lifecycle?.score?.invalid_reasons ?? []).slice(0, 100)) add('exception', 'judge', { reason, scoreValid: false }, { trial })
34
+ if (trialState.lifecycle?.exception) add('exception', 'judge', trialState.lifecycle.exception, { trial })
35
+ if (trialState.lifecycle) add('attempt', 'judge', trialState.lifecycle, { trial })
36
+ }
37
+ for (const role of ['evaluator', 'rubric']) {
38
+ const source = governance?.components?.[role]?.source
39
+ if (typeof source?.text === 'string' && !source.error) {
40
+ add('evaluator-source', 'judge', { role, text: source.text }, { sourceRole: role })
41
+ }
42
+ }
43
+ return entries
44
+ }
45
+
46
+ export function resolveCatalogSelection(ref, catalog) {
47
+ const entry = catalog.find(item => item.ref.job === ref.job && item.ref.kind === ref.kind && item.ref.id === ref.id && item.ref.sourceDigest === ref.sourceDigest && item.ref.trial === ref.trial)
48
+ if (!entry) throw new Error('HARBOR_CONTEXT_STALE_SELECTION: Selected artifact item has changed or is unavailable; select it again.')
49
+ if (ref.kind !== 'evaluator-source') return entry
50
+ if (ref.sourceRole !== entry.ref.sourceRole) throw new Error('HARBOR_CONTEXT_STALE_SELECTION: Source role does not match the selected artifact.')
51
+ const lines = entry.value.text.split('\n')
52
+ const start = ref.startLine ?? 1
53
+ const end = ref.endLine ?? Math.min(lines.length, 200)
54
+ if (start < 1 || end < start || end > lines.length || end - start >= 200) throw new Error('HARBOR_CONTEXT_INVALID: Select between 1 and 200 saved source lines.')
55
+ return { ref: { ...entry.ref, startLine: start, endLine: end }, value: { role: ref.sourceRole, startLine: start, endLine: end, text: lines.slice(start - 1, end).join('\n') } }
56
+ }
@@ -9,6 +9,14 @@ function nonBlank(value) {
9
9
  return typeof value === 'string' && value.trim() ? value.trim() : undefined
10
10
  }
11
11
 
12
+ function leaseLimit(value, fallback, label) {
13
+ const selected = value ?? fallback
14
+ if (!Number.isSafeInteger(selected) || selected < 1) {
15
+ throw Object.assign(new Error(`HARBOR_MODEL_LIMIT_INVALID: ${label} must be a positive integer.`), { code: 'HARBOR_MODEL_LIMIT_INVALID' })
16
+ }
17
+ return selected
18
+ }
19
+
12
20
  function sameSecret(expected, actual) {
13
21
  const left = Buffer.from(expected)
14
22
  const right = Buffer.from(actual)
@@ -165,7 +173,27 @@ export class CandidateModelRuntime {
165
173
  }
166
174
  }
167
175
 
168
- async openLease(binding, scope) {
176
+ /** Read-only budget validation, shared by Preflight and the execution boundary. */
177
+ async assertLeaseLimits(binding, scope = {}) {
178
+ const globalRequests = leaseLimit(this.config.modelBrokerMaxRequests, 1000, 'modelBrokerMaxRequests')
179
+ const maxRequests = Math.min(globalRequests, leaseLimit(scope.maxRequests, globalRequests, 'maxRequests'))
180
+ let maxResponseBytes
181
+ if (scope.maxResponseBytes !== undefined) {
182
+ const globalBytes = leaseLimit(this.config.modelBrokerMaxResponseBytes, 4 * 1024 * 1024, 'modelBrokerMaxResponseBytes')
183
+ maxResponseBytes = Math.min(globalBytes, leaseLimit(scope.maxResponseBytes, globalBytes, 'maxResponseBytes'))
184
+ }
185
+ if (scope.maxOutputTokens !== undefined) {
186
+ leaseLimit(scope.maxOutputTokens, undefined, 'maxOutputTokens')
187
+ // The current public DSH model metadata does not prove provider-wire
188
+ // enforcement. In particular its Codex adapter can ignore maxTokens.
189
+ // Do not silently treat an API option as an actual billing/token cap.
190
+ throw Object.assign(new Error('HARBOR_MODEL_OUTPUT_LIMIT_UNSUPPORTED: This Host does not expose verified provider output-token enforcement. Use explicit request, time and response-byte budgets; these are not token or billing limits.'), { code: 'HARBOR_MODEL_OUTPUT_LIMIT_UNSUPPORTED' })
191
+ }
192
+ return { maxRequests, ...(maxResponseBytes === undefined ? {} : { maxResponseBytes }) }
193
+ }
194
+
195
+ async openLease(binding, scope = {}) {
196
+ const limits = await this.assertLeaseLimits(binding, scope)
169
197
  const token = randomBytes(32).toString('base64url')
170
198
  const route = `/harbor-model-gateway/v1/${randomBytes(18).toString('base64url')}`
171
199
  const controllers = new Set()
@@ -180,6 +208,7 @@ export class CandidateModelRuntime {
180
208
  protocol: MODEL_GATEWAY_PROTOCOL,
181
209
  candidate_digest: scope.candidateDigest,
182
210
  job: scope.jobName,
211
+ limits,
183
212
  binding: {
184
213
  provider: binding.provider,
185
214
  model: binding.model,
@@ -192,7 +221,7 @@ export class CandidateModelRuntime {
192
221
  sendJson(response, 405, { error: 'method not allowed' })
193
222
  return
194
223
  }
195
- if (requestCount >= this.config.modelBrokerMaxRequests) {
224
+ if (requestCount >= limits.maxRequests) {
196
225
  sendJson(response, 429, { error: 'model gateway request budget exhausted' })
197
226
  return
198
227
  }
@@ -209,8 +238,12 @@ export class CandidateModelRuntime {
209
238
  model: _model,
210
239
  reasoningEffort: _reasoningEffort,
211
240
  signal: _signal,
241
+ maxRequests: _maxRequests,
242
+ maxResponseBytes: _maxResponseBytes,
243
+ maxOutputTokens: _maxOutputTokens,
212
244
  ...requestOptions
213
245
  } = body
246
+ if (controller.signal.aborted) throw new Error('Candidate disconnected before model execution')
214
247
  response.writeHead(200, {
215
248
  'content-type': 'application/x-ndjson; charset=utf-8',
216
249
  'cache-control': 'no-store',
@@ -222,8 +255,17 @@ export class CandidateModelRuntime {
222
255
  ...(binding.reasoning_effort === undefined ? {} : { reasoningEffort: binding.reasoning_effort }),
223
256
  signal: controller.signal,
224
257
  })
258
+ let responseBytes = 0
225
259
  for await (const chunk of stream) {
226
- if (!response.write(`${JSON.stringify(chunk)}\n`)) await once(response, 'drain')
260
+ const line = `${JSON.stringify(chunk)}\n`
261
+ responseBytes += Buffer.byteLength(line, 'utf8')
262
+ if (limits.maxResponseBytes !== undefined && responseBytes > limits.maxResponseBytes) {
263
+ const error = new Error('model gateway response byte budget exhausted; this is not a provider token or billing limit')
264
+ controller.abort(error)
265
+ // Never publish the overflowing chunk or a successful finish.
266
+ throw error
267
+ }
268
+ if (!response.write(line)) await once(response, 'drain', { signal: controller.signal })
227
269
  }
228
270
  response.end()
229
271
  } catch (error) {
@@ -248,6 +290,9 @@ export class CandidateModelRuntime {
248
290
  token,
249
291
  candidateProvider: CANDIDATE_GATEWAY_PROVIDER,
250
292
  modelInfo: binding.model_info,
293
+ limits: { ...limits },
294
+ // Host-only observation: no gateway URL, token or request content escapes.
295
+ usage: () => ({ modelRequests: requestCount, maxModelRequests: limits.maxRequests }),
251
296
  async close() {
252
297
  if (closed) return
253
298
  closed = true