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.
@@ -0,0 +1,175 @@
1
+ import { constants } from 'node:fs'
2
+ import { lstat, open, readFile, readlink } from 'node:fs/promises'
3
+ import { createHash } from 'node:crypto'
4
+ import path from 'node:path'
5
+ import { resolveWithin } from './evolution.js'
6
+
7
+ const phases = new Set(['queued', 'preparing-environment', 'preparing-agent', 'loading-observation', 'running-agent', 'running-adapter', 'running-integration', 'rendering', 'evaluating', 'completed', 'completed-unscored', 'candidate-quality-failed', 'infrastructure-error', 'evaluation-error', 'cancelled'])
8
+ const fail = () => { throw Object.assign(new Error('HARBOR_DIAGNOSTIC_OBSERVATION_UNSAFE: Diagnostic evidence is missing or unsafe.'), { code: 'HARBOR_DIAGNOSTIC_OBSERVATION_UNSAFE' }) }
9
+
10
+ export async function pinDiagnosticEnvironment(runProcess, environment) {
11
+ const env = { ...environment }
12
+ let endpoint = env.DOCKER_HOST
13
+ if (env.DOCKER_CONTEXT || !endpoint) {
14
+ let context = env.DOCKER_CONTEXT
15
+ if (!context) {
16
+ const selected = await runProcess('docker', ['context', 'show'], { env, timeoutMs: 5000, maxOutputBytes: 1024 })
17
+ if (selected.code !== 0) fail()
18
+ context = selected.stdout.trim()
19
+ }
20
+ if (!/^[A-Za-z0-9][A-Za-z0-9_.-]{0,100}$/.test(context)) fail()
21
+ const resolved = await runProcess('docker', ['context', 'inspect', context, '--format', '{{.Endpoints.docker.Host}}'], { env, timeoutMs: 5000, maxOutputBytes: 4096 })
22
+ if (resolved.code !== 0) fail()
23
+ endpoint = resolved.stdout.trim()
24
+ }
25
+ if (!/^unix:\/\/\/[^\r\n\0]{1,4000}$/.test(endpoint)) throw Object.assign(new Error('HARBOR_DIAGNOSTIC_RUNTIME_UNSUPPORTED: Bounded diagnostics currently require a local Docker Unix socket. Remote/TLS contexts are not enabled because immutable transport and cleanup ownership cannot yet be proven; no run was started.'), { code: 'HARBOR_DIAGNOSTIC_RUNTIME_UNSUPPORTED' })
26
+ // Pin the endpoint, not merely its mutable context name. No TLS credentials
27
+ // are copied and editing Docker's context definition cannot redirect a run.
28
+ env.DOCKER_HOST = endpoint
29
+ delete env.DOCKER_CONTEXT
30
+ delete env.DOCKER_TLS
31
+ delete env.DOCKER_TLS_VERIFY
32
+ delete env.DOCKER_CERT_PATH
33
+ return env
34
+ }
35
+
36
+ export async function readDiagnosticRuntimeIdentity({ runProcess, platform = process.platform, env }) {
37
+ let machine, processDomain = ''
38
+ if (platform === 'darwin') {
39
+ const response = await runProcess('ioreg', ['-rd1', '-c', 'IOPlatformExpertDevice'], { env, timeoutMs: 5000, maxOutputBytes: 64 * 1024 })
40
+ if (response.code !== 0) fail()
41
+ machine = response.stdout.match(/"IOPlatformUUID"\s*=\s*"([a-f0-9-]{36})"/i)?.[1] ?? ''
42
+ } else if (platform === 'linux') {
43
+ const info = await lstat('/etc/machine-id')
44
+ if (!info.isFile() || info.size > 1024) fail()
45
+ machine = (await readFile('/etc/machine-id', 'utf8')).trim()
46
+ const namespace = await readlink('/proc/self/ns/pid')
47
+ const boot = (await readFile('/proc/sys/kernel/random/boot_id', 'utf8')).trim()
48
+ if (!/^pid:\[[0-9]+\]$/.test(namespace) || !/^[a-f0-9-]{36}$/.test(boot)) fail()
49
+ processDomain = `:${namespace}:${boot}`
50
+ } else fail()
51
+ if (!/^[a-f0-9-]{16,64}$/i.test(machine)) fail()
52
+ const docker = await runProcess('docker', ['info', '--format', '{{.ID}}'], { env, timeoutMs: 10_000, maxOutputBytes: 1024 })
53
+ if (docker.code !== 0 || !/^[A-Za-z0-9:_.-]{8,200}$/.test(docker.stdout.trim())) fail()
54
+ const digest = value => `sha256:${createHash('sha256').update(value).digest('hex')}`
55
+ // Do not persist device IDs, daemon endpoints, context names or credentials.
56
+ return { hostIdentity: digest(`${platform}:${machine.toLowerCase()}${processDomain}`), dockerIdentity: digest(docker.stdout.trim()) }
57
+ }
58
+
59
+ async function artifact(root, target) {
60
+ const file = resolveWithin(root, target, 'diagnostic observation')
61
+ let cursor = root
62
+ for (const part of path.relative(root, file).split(path.sep)) {
63
+ cursor = path.join(cursor, part)
64
+ if ((await lstat(cursor)).isSymbolicLink()) fail()
65
+ }
66
+ const handle = await open(file, constants.O_RDONLY | constants.O_NOFOLLOW)
67
+ try {
68
+ const info = await handle.stat()
69
+ if (!info.isFile() || info.size > 512 * 1024) fail()
70
+ return JSON.parse(await handle.readFile('utf8'))
71
+ } finally { await handle.close() }
72
+ }
73
+
74
+ async function evidence(config, operation, root, { tolerateLifecycleError = false } = {}) {
75
+ if (!/^hop_[A-Za-z0-9_-]{1,100}$/.test(operation.operationId ?? '')) fail()
76
+ const jobName = `diagnostic-${operation.operationId.slice(4)}`
77
+ const directory = resolveWithin(root, path.join(config.jobsDir ?? 'jobs', jobName), 'diagnostic Job')
78
+ const provenance = await artifact(root, path.join(directory, 'diagnostic-provenance.json'))
79
+ if (provenance?.protocol !== 'harbor-diagnostic-provenance/v1' || provenance.operationId !== operation.operationId || provenance.promotionEligible !== false || !Array.isArray(provenance.selection) || provenance.selection.length < 1 || provenance.selection.length > 12) fail()
80
+ const resultRef = { verified: true, jobName, ...(operation.target?.workspace ? { workspace: operation.target.workspace } : {}), partial: operation.status !== 'COMPLETED' }
81
+ let lifecycle, observationWarning
82
+ try {
83
+ lifecycle = await artifact(root, path.join(directory, 'trial-lifecycle.json'))
84
+ if (lifecycle.schema_version !== 1 || lifecycle.job !== jobName || lifecycle.dataset_total !== provenance.selection.length || !Array.isArray(lifecycle.trials) || lifecycle.trials.length !== provenance.selection.length || lifecycle.trials.some(trial => !phases.has(trial.phase) || typeof trial.terminal !== 'boolean' || !Number.isSafeInteger(trial.dataset_order) || trial.dataset_order < 0 || trial.dataset_order >= provenance.selection.length) || new Set(lifecycle.trials.map(trial => trial.dataset_order)).size !== provenance.selection.length) fail()
85
+ } catch (cause) {
86
+ if (cause.code !== 'ENOENT' && !tolerateLifecycleError) throw cause
87
+ lifecycle = undefined
88
+ if (cause.code !== 'ENOENT') observationWarning = 'HARBOR_DIAGNOSTIC_OBSERVATION_UNSAFE'
89
+ }
90
+ return { resultRef, lifecycle, total: provenance.selection.length, ...(observationWarning ? { observationWarning } : {}) }
91
+ }
92
+
93
+ /** Bounded local artifact reads only. Missing evidence is not manufactured progress. */
94
+ export async function observeDiagnostic(config, operation, { root }) {
95
+ try {
96
+ const { resultRef, lifecycle, total, observationWarning } = await evidence(config, operation, root, { tolerateLifecycleError: true })
97
+ if (!lifecycle) return { resultRef, ...(observationWarning ? { observationWarning } : {}) }
98
+ const counts = {}
99
+ for (const trial of lifecycle.trials) counts[trial.phase] = (counts[trial.phase] ?? 0) + 1
100
+ return { resultRef, progress: { source: 'harbor-lifecycle', total, completed: lifecycle.trials.filter(trial => trial.terminal).length, counts, ...(typeof lifecycle.updated_at === 'string' && Number.isFinite(Date.parse(lifecycle.updated_at)) ? { updatedAt: lifecycle.updated_at } : {}) } }
101
+ } catch (cause) {
102
+ return cause.code === 'ENOENT' ? {} : { observationWarning: 'HARBOR_DIAGNOSTIC_OBSERVATION_UNSAFE' }
103
+ }
104
+ }
105
+
106
+ function probeGroup(pid) {
107
+ try { process.kill(-pid, 0); return 'running' }
108
+ catch (cause) { if (cause.code !== 'ESRCH') return 'unknown' }
109
+ // A reused live PID is deliberately a blocker, never a process to signal.
110
+ try { process.kill(pid, 0); return 'running' }
111
+ catch (cause) { return cause.code === 'ESRCH' ? 'stopped' : 'unknown' }
112
+ }
113
+
114
+ const composeName = name => {
115
+ const lowered = name.toLowerCase()
116
+ return (/^[a-z0-9]/.test(lowered) ? lowered : `0${lowered}`).replace(/[^a-z0-9_-]/g, '-')
117
+ }
118
+
119
+ /** Read-only reconciliation. It does not kill processes or remove Docker resources. */
120
+ export async function inspectDiagnostic(config, operation, { root, runProcess, processProbe = probeGroup, platform = process.platform }) {
121
+ const blockers = []
122
+ const checkpoint = operation.events?.find(event => event.result?.process)?.result.process
123
+ let processState = 'unknown'
124
+ let runtimeMatches = false
125
+ let environment
126
+ if (/^sha256:[a-f0-9]{64}$/.test(checkpoint?.hostIdentity ?? '') && /^sha256:[a-f0-9]{64}$/.test(checkpoint?.dockerIdentity ?? '') && checkpoint.platform === platform && checkpoint.dockerTransport === 'pinned-local-unix/v1') {
127
+ try {
128
+ environment = await pinDiagnosticEnvironment(runProcess, process.env)
129
+ const current = await readDiagnosticRuntimeIdentity({ runProcess, platform, env: environment })
130
+ runtimeMatches = current.hostIdentity === checkpoint.hostIdentity && current.dockerIdentity === checkpoint.dockerIdentity
131
+ } catch {}
132
+ }
133
+ if (!runtimeMatches) blockers.push({ code: 'DIAGNOSTIC_RUNTIME_IDENTITY_UNVERIFIED', message: 'The recorded Host machine and Docker daemon cannot be matched. Restore the original runtime/context and inspect again; a different empty daemon is not proof of cleanup.' })
134
+ if (runtimeMatches && Number.isSafeInteger(checkpoint?.pid) && checkpoint.pid > 1 && checkpoint.groupId === checkpoint.pid && ['darwin', 'linux'].includes(checkpoint.platform)) {
135
+ try { processState = await processProbe(checkpoint.pid) } catch {}
136
+ if (!['stopped', 'running', 'unknown'].includes(processState)) processState = 'unknown'
137
+ }
138
+ const processInfo = { state: processState, ...(checkpoint?.pid ? { pid: checkpoint.pid, groupId: checkpoint.groupId } : {}) }
139
+ if (processState !== 'stopped') blockers.push({ code: processState === 'running' ? 'DIAGNOSTIC_PROCESS_PRESENT' : 'DIAGNOSTIC_PROCESS_OWNERSHIP_UNKNOWN', message: processState === 'running' ? 'The recorded process or process group still exists. Stop the original Host-owned run and inspect again; no signal was sent.' : 'A trustworthy stopped-process checkpoint is unavailable. An administrator must reconcile the original runner; no resources or lock were changed.' })
140
+ let resources = { state: 'unknown', items: [] }, resultRef
141
+ try {
142
+ const value = await evidence(config, operation, root)
143
+ resultRef = value.resultRef
144
+ if (!runtimeMatches) fail()
145
+ if (!value.lifecycle) fail()
146
+ const names = value.lifecycle.trials.map(trial => trial.trial_name).filter(Boolean)
147
+ // A separate-verifier name is truncated to 63 characters by Harbor. For
148
+ // longer Trial names that can truncate the ownership prefix itself, so
149
+ // refuse to infer cleanup instead of missing those resources.
150
+ if (names.some(name => typeof name !== 'string' || !/^[A-Za-z0-9_.-]{1,50}$/.test(name)) || value.lifecycle.trials.some(trial => trial.phase !== 'queued' && !trial.trial_name)) fail()
151
+ // Harbor 0.21's POSIX Docker adapter binds every environment and separate
152
+ // verifier project to `<trial_name>__…`. Trial-start is journaled before
153
+ // environment launch. We read labels only, never environment variables.
154
+ const prefixes = names.map(name => `${composeName(name)}__`)
155
+ const items = []
156
+ for (const kind of ['container', 'network', 'volume']) {
157
+ const args = [kind, 'ls', ...(kind === 'container' ? ['--all'] : []), '--filter', 'label=com.docker.compose.project', '--format', '{{.ID}}\t{{.Label "com.docker.compose.project"}}']
158
+ if (kind === 'volume') args[args.length - 1] = '{{.Name}}\t{{.Label "com.docker.compose.project"}}'
159
+ const response = await runProcess('docker', args, { cwd: root, env: environment, timeoutMs: 10_000, maxOutputBytes: 128 * 1024 })
160
+ if (response.code !== 0 || typeof response.stdout !== 'string') fail()
161
+ for (const line of response.stdout.split('\n').filter(Boolean)) {
162
+ const [id, project, extra] = line.split('\t')
163
+ if (extra !== undefined || !/^[a-zA-Z0-9_.-]{1,200}$/.test(id ?? '') || !/^[a-z0-9_-]{1,220}$/.test(project ?? '')) fail()
164
+ if (prefixes.some(prefix => project.startsWith(prefix))) items.push({ kind, id, project })
165
+ }
166
+ }
167
+ const finalIdentity = await readDiagnosticRuntimeIdentity({ runProcess, platform, env: environment })
168
+ if (finalIdentity.hostIdentity !== checkpoint.hostIdentity || finalIdentity.dockerIdentity !== checkpoint.dockerIdentity) fail()
169
+ resources = { state: items.length ? 'remaining' : 'clean', items: items.sort((a, b) => `${a.kind}:${a.id}`.localeCompare(`${b.kind}:${b.id}`)), checkedProjects: prefixes, boundary: 'Owned Compose containers, networks and volumes only; shared image/build caches are retained.' }
170
+ if (items.length) blockers.push({ code: 'DIAGNOSTIC_RESOURCES_REMAIN', message: 'The listed Compose resources remain. Have the workspace administrator inspect these exact resources, then check again. This action never deletes resources.' })
171
+ } catch {
172
+ blockers.push({ code: 'DIAGNOSTIC_RESOURCES_UNKNOWN', message: 'Job provenance/lifecycle or Docker inspection is unavailable. Restore access and inspect again; an empty or unreadable response is not proof of cleanup.' })
173
+ }
174
+ return { process: processInfo, resources, ...(resultRef ? { resultRef } : {}), blockers, canRecover: processState === 'stopped' && resources.state === 'clean' }
175
+ }
@@ -0,0 +1,206 @@
1
+ import { lstat, open, realpath } from 'node:fs/promises'
2
+ import { constants } from 'node:fs'
3
+ import path from 'node:path'
4
+ import { runBoundedProcess } from './bounded-process.js'
5
+ import { buildEvaluationRunReceipt, redactDiagnostic, resolveWithin } from './evolution.js'
6
+ import { inspectDiagnostic, observeDiagnostic, pinDiagnosticEnvironment, readDiagnosticRuntimeIdentity } from './diagnostic-observation.js'
7
+
8
+ export const DIAGNOSTIC_LIMITS = Object.freeze({ maxTrials: 12, concurrency: 2, attempts: 1, maxRetries: 0, wallTimeoutMs: 900_000, maxModelRequests: 96, maxResponseBytes: 1_048_576 })
9
+ const AGENT = 'harbor_dsh_evolution.agent:DshCandidateAgent'
10
+ const PLUGIN = 'harbor_dsh_evolution.diagnostic_plugin:BoundedDiagnosticPlugin'
11
+ const PLAN_PROTOCOL = 'harbor-bounded-diagnostic-plan/v1'
12
+ const OPERATION_ID = /^hop_[A-Za-z0-9_-]{1,100}$/
13
+
14
+ function failure(code, message) { return Object.assign(new Error(`${code}: ${message}`), { code }) }
15
+ function aborted(signal) { if (signal?.aborted) throw failure('HARBOR_PROCESS_ABORTED', 'Diagnostic cancelled before launch.') }
16
+ function bindingIdentity(binding) {
17
+ return Object.fromEntries(['provider', 'model', 'transport', 'protocol', 'reasoning_effort'].filter(key => binding?.[key] !== undefined).map(key => [key, binding[key]]))
18
+ }
19
+ function hasLockedRuntime(runtime) {
20
+ const safeSourcePath = value => typeof value === 'string' && value.length <= 1024 && value.trim() === value && !/[\\:\x00-\x1f\x7f]/.test(value) && !value.split('/').some(part => ['', '.', '..', 'node_modules', '.harbor-runtime', '.git'].includes(part))
21
+ return runtime?.kind === 'deepseek-harness' && runtime.policy === 'candidate-locked' && runtime.transport === 'acp'
22
+ && runtime.descriptor === 'candidate-runtime.json' && runtime.lockfile === 'package-lock.json'
23
+ && safeSourcePath(runtime.entrypoint) && /\.(?:js|mjs|cjs)$/.test(runtime.entrypoint)
24
+ && safeSourcePath(runtime.config_path) && typeof runtime.agent_entry_id === 'string' && /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(runtime.agent_entry_id)
25
+ && typeof runtime.node_version === 'string' && /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(runtime.node_version) && Number(runtime.node_version.split('.')[0]) >= 22
26
+ && ['descriptor_digest', 'entrypoint_digest', 'lockfile_digest'].every(key => /^sha256:[a-f0-9]{64}$/.test(runtime[key] ?? ''))
27
+ }
28
+ function assertPlan(plan) {
29
+ if (plan?.protocol !== PLAN_PROTOCOL || !/^sha256:[a-f0-9]{64}$/.test(plan.planDigest ?? '') || plan.mode !== 'diagnostic' || plan.promotionEligible !== false || !Array.isArray(plan.selection) || !plan.selection.length || plan.selection.length > DIAGNOSTIC_LIMITS.maxTrials || Object.entries(DIAGNOSTIC_LIMITS).some(([key, value]) => plan.limits?.[key] !== value)) {
30
+ throw failure('HARBOR_DIAGNOSTIC_PLAN_INVALID', 'The installed adapter did not produce the bounded diagnostic contract.')
31
+ }
32
+ if (!hasLockedRuntime(plan.identities?.candidate?.runtime)) {
33
+ throw failure('HARBOR_DIAGNOSTIC_RUNTIME_ADAPTER_UNSUPPORTED', 'Update the Python Adapter and review a new plan; it has not verified a locked Candidate-owned ACP runtime. No Job was started.')
34
+ }
35
+ }
36
+ function safeProcessError(error) {
37
+ const code = /^HARBOR_[A-Z_]+$/.test(error?.code ?? '') ? error.code : 'HARBOR_DIAGNOSTIC_EXECUTION_FAILED'
38
+ return failure(code, redactDiagnostic(error?.message ?? 'Diagnostic execution failed.').slice(0, 600))
39
+ }
40
+
41
+ async function safeJsonArtifact(root, file, maxBytes = 4 * 1024 * 1024) {
42
+ const target = resolveWithin(root, file, 'diagnostic artifact')
43
+ const relative = path.relative(root, target)
44
+ let cursor = root
45
+ for (const part of relative.split(path.sep)) {
46
+ cursor = path.join(cursor, part)
47
+ if ((await lstat(cursor)).isSymbolicLink()) throw failure('HARBOR_DIAGNOSTIC_ARTIFACT_INVALID', 'Diagnostic artifacts must not traverse symlinks.')
48
+ }
49
+ const handle = await open(target, constants.O_RDONLY | constants.O_NOFOLLOW)
50
+ try {
51
+ const stats = await handle.stat()
52
+ if (!stats.isFile() || stats.size > maxBytes) throw failure('HARBOR_DIAGNOSTIC_ARTIFACT_INVALID', 'Diagnostic artifacts exceeded the bounded regular-file contract.')
53
+ return JSON.parse(await handle.readFile('utf8'))
54
+ } finally { await handle.close() }
55
+ }
56
+
57
+ /** Fixed CLI adapter. Authorization, idempotency and persistent Operation state belong to the Host controller. */
58
+ export class DiagnosticRunner {
59
+ constructor(config, modelRuntime, { runProcess = runBoundedProcess, platform = process.platform, processProbe } = {}) {
60
+ this.config = config
61
+ this.modelRuntime = modelRuntime
62
+ this.runProcess = runProcess
63
+ this.platform = platform
64
+ this.processProbe = processProbe
65
+ }
66
+
67
+ async observe(operation, { owner } = {}) {
68
+ return observeDiagnostic(this.config, operation, { root: await this._root(owner) })
69
+ }
70
+
71
+ async inspect(operation, { owner } = {}) {
72
+ return inspectDiagnostic(this.config, operation, { root: await this._root(owner), runProcess: this.runProcess, processProbe: this.processProbe, platform: this.platform })
73
+ }
74
+
75
+ async _root(owner) {
76
+ if (!owner?.sessionId || !owner.projectRoot) throw failure('HARBOR_DIAGNOSTIC_OWNER_REQUIRED', 'A current Session project is required.')
77
+ const root = await realpath(owner.projectRoot)
78
+ if (root !== await realpath(this.config.projectRoot)) throw failure('HARBOR_DIAGNOSTIC_SOURCE_DENIED', 'The diagnostic runner belongs to a different project.')
79
+ if (this.platform === 'win32') throw failure('HARBOR_DIAGNOSTIC_RUNTIME_UNSUPPORTED', 'This runner requires POSIX process-group cancellation; Windows execution is not enabled.')
80
+ return root
81
+ }
82
+
83
+ _environment() { return { ...process.env, HARBOR_TELEMETRY: '0', ...(this.config.pythonPath ? { PYTHONPATH: this.config.pythonPath } : {}) } }
84
+
85
+ async _json(args, root, { input, signal, timeoutMs = 30_000 } = {}) {
86
+ let result
87
+ try {
88
+ result = await this.runProcess(this.config.harborDshBin, args, { cwd: root, env: this._environment(), timeoutMs, signal, allowedExitCodes: [0, 2], maxOutputBytes: 4 * 1024 * 1024, ...(input ? { input: JSON.stringify(input) } : {}) })
89
+ } catch (error) { throw safeProcessError(error) }
90
+ let value
91
+ try { value = JSON.parse(result.stdout) } catch { throw failure('HARBOR_DIAGNOSTIC_ADAPTER_UNAVAILABLE', 'Update the Python Adapter; its bounded diagnostic response is missing or invalid.') }
92
+ if (value?.error) {
93
+ const message = redactDiagnostic(value.error).slice(0, 600)
94
+ throw failure(message.match(/^(HARBOR_[A-Z_]+):/)?.[1] ?? 'HARBOR_DIAGNOSTIC_SOURCE_INVALID', message)
95
+ }
96
+ return value
97
+ }
98
+
99
+ async _binding(plan) {
100
+ if (!this.modelRuntime?.resolve || !this.modelRuntime?.openLease) throw failure('HARBOR_DIAGNOSTIC_MODEL_UNAVAILABLE', 'The Host model broker is not connected.')
101
+ const pinned = plan.candidateModelBinding
102
+ const binding = await this.modelRuntime.resolve({}, pinned, { ignoreConfigured: true })
103
+ if (JSON.stringify(bindingIdentity(binding)) !== JSON.stringify(bindingIdentity(pinned))) throw failure('HARBOR_DIAGNOSTIC_MODEL_CHANGED', 'The recorded Candidate model cannot be resolved exactly.')
104
+ return binding
105
+ }
106
+
107
+ async prepare({ owner, sourceJobDir, trialIds, signal }) {
108
+ const root = await this._root(owner)
109
+ const source = resolveWithin(root, sourceJobDir, 'source Job')
110
+ const plan = await this._json(['diagnostic-subset', 'plan'], root, { input: { projectRoot: root, sourceJobDir: source, trialIds }, signal })
111
+ assertPlan(plan)
112
+ const binding = await this._binding(plan)
113
+ if (typeof this.modelRuntime.assertLeaseLimits !== 'function') throw failure('HARBOR_DIAGNOSTIC_MODEL_LIMIT_UNSUPPORTED', 'The Host broker cannot prove the requested model budget; update the runtime before running.')
114
+ const budget = await this.modelRuntime.assertLeaseLimits(binding, { maxRequests: DIAGNOSTIC_LIMITS.maxModelRequests, maxResponseBytes: DIAGNOSTIC_LIMITS.maxResponseBytes })
115
+ if (!Number.isSafeInteger(budget?.maxRequests) || budget.maxRequests < 1 || budget.maxRequests > DIAGNOSTIC_LIMITS.maxModelRequests || !Number.isSafeInteger(budget?.maxResponseBytes) || budget.maxResponseBytes < 1 || budget.maxResponseBytes > DIAGNOSTIC_LIMITS.maxResponseBytes) throw failure('HARBOR_DIAGNOSTIC_MODEL_LIMIT_UNSUPPORTED', 'The Host broker returned an invalid effective budget.')
116
+ const runtime = await this._json(['docker-check'], root, { signal })
117
+ if (runtime?.valid !== true) {
118
+ const codes = (runtime?.findings ?? []).filter(item => item.level === 'error').map(item => String(item.code)).filter(code => /^DOCKER_[A-Z_]+$/.test(code))
119
+ throw failure('HARBOR_DIAGNOSTIC_RUNTIME_BLOCKED', `Docker is not ready (${codes.join(', ') || 'DOCKER_UNAVAILABLE'}). Fix the runtime and check parameters again; no Job was started.`)
120
+ }
121
+ // This is a read-only capability check. Unsupported remote/TLS transports
122
+ // must be visible in preflight, not discovered after user confirmation.
123
+ await pinDiagnosticEnvironment(this.runProcess, this._environment())
124
+ try {
125
+ const version = await this.runProcess(this.config.harborBin, ['--version'], { cwd: root, env: this._environment(), timeoutMs: 10_000, signal, maxOutputBytes: 4096 })
126
+ if (!/\b0\.21\.\d+(?:\b|[-+])/.test(version.stdout)) throw failure('HARBOR_DIAGNOSTIC_RUNTIME_UNSUPPORTED', 'The bounded runner requires the installed Harbor 0.21 adapter contract.')
127
+ } catch (error) { throw safeProcessError(error) }
128
+ return { ...plan, effectiveLimits: { ...plan.limits, maxModelRequests: budget.maxRequests, maxResponseBytes: budget.maxResponseBytes } }
129
+ }
130
+
131
+ async execute(plan, { owner, operationId, signal, onSpawn, onUsage } = {}) {
132
+ if (!OPERATION_ID.test(operationId ?? '')) throw failure('HARBOR_DIAGNOSTIC_OPERATION_INVALID', 'A stable Host Operation ID is required.')
133
+ assertPlan(plan)
134
+ aborted(signal)
135
+ const root = await this._root(owner)
136
+ const trialIds = plan.selection.map(item => item.trialId)
137
+ const fresh = await this.prepare({ owner, sourceJobDir: plan.sourceJob, trialIds, signal })
138
+ if (fresh.planDigest !== plan.planDigest) throw failure('HARBOR_DIAGNOSTIC_REVISION_CONFLICT', 'Inputs changed since preflight. Review a new preview; no Job was started.')
139
+ if (plan.effectiveLimits && JSON.stringify(plan.effectiveLimits) !== JSON.stringify(fresh.effectiveLimits)) throw failure('HARBOR_DIAGNOSTIC_REVISION_CONFLICT', 'The effective diagnostic budget changed. Review a new preview; no Job was started.')
140
+ const binding = await this._binding(fresh)
141
+ const jobName = `diagnostic-${operationId.slice(4)}`
142
+ if (jobName.length > 100) throw failure('HARBOR_DIAGNOSTIC_OPERATION_INVALID', 'The Operation ID exceeds the fixed Job-name bound.')
143
+ const jobs = resolveWithin(root, this.config.jobsDir ?? 'jobs', 'jobs directory')
144
+ let cursor = root
145
+ for (const part of path.relative(root, jobs).split(path.sep).filter(Boolean)) {
146
+ cursor = path.join(cursor, part)
147
+ try { if ((await lstat(cursor)).isSymbolicLink()) throw failure('HARBOR_DIAGNOSTIC_SOURCE_DENIED', 'The Job output directory must not traverse symlinks.') }
148
+ catch (error) { if (error.code !== 'ENOENT') throw error }
149
+ }
150
+ const jobDir = path.join(jobs, jobName)
151
+ try { await lstat(jobDir); throw failure('HARBOR_DIAGNOSTIC_ALREADY_STARTED', 'This Operation already has a Job; it cannot be launched again.') }
152
+ catch (error) { if (error.code !== 'ENOENT') throw error }
153
+ aborted(signal)
154
+ const materialized = await this._json(['diagnostic-subset', 'materialize'], root, { input: { projectRoot: root, sourceJobDir: plan.sourceJob, trialIds, expectedPlanDigest: plan.planDigest, operationId }, signal, timeoutMs: 60_000 })
155
+ assertPlan(materialized)
156
+ const candidate = resolveWithin(root, materialized.candidatePath, 'Candidate')
157
+ const dataset = resolveWithin(root, materialized.datasetPath, 'diagnostic Dataset')
158
+ const stack = resolveWithin(root, materialized.stackPath, 'Evaluation Stack')
159
+ const identity = materialized.identities.candidate
160
+ const args = [
161
+ 'run', '-p', dataset, '-a', AGENT,
162
+ '--ak', `candidate_path=${candidate}`, '--ak', `candidate_version=${identity.version}`, '--ak', `candidate_digest=${identity.digest}`,
163
+ '--ak', `candidate_model_provider=${binding.provider}`, '--ak', `candidate_model=${binding.model}`,
164
+ '--job-name', jobName, '--jobs-dir', jobs, '-n', String(DIAGNOSTIC_LIMITS.concurrency), '-k', '1', '--max-retries', '0', '-e', 'docker', '--delete',
165
+ '--plugin', PLUGIN, '--plugin-kwarg', `candidate_manifest=${path.join(candidate, 'candidate-manifest.json')}`,
166
+ '--plugin-kwarg', `dataset_path=${dataset}`, '--plugin-kwarg', `stack_path=${stack}`, '--plugin-kwarg', `project_root=${root}`, '--plugin-kwarg', 'mode=diagnostic',
167
+ '--plugin-kwarg', `candidate_model_provider=${binding.provider}`, '--plugin-kwarg', `candidate_model=${binding.model}`,
168
+ '--plugin-kwarg', `candidate_model_transport=${binding.transport}`, '--plugin-kwarg', `candidate_model_protocol=${binding.protocol}`,
169
+ '--plugin-kwarg', `expected_dataset_digest=${materialized.datasetIdentity.source_digest}`, '--plugin-kwarg', `expected_stack_digest=${materialized.identities.stack.digest}`,
170
+ '--plugin-kwarg', `operation_id=${operationId}`, '--plugin-kwarg', `source_plan_digest=${materialized.planDigest}`,
171
+ ]
172
+ if (binding.reasoning_effort !== undefined) args.push('--ak', `candidate_reasoning_effort=${binding.reasoning_effort}`, '--plugin-kwarg', `candidate_reasoning_effort=${binding.reasoning_effort}`)
173
+ aborted(signal)
174
+ const environment = await pinDiagnosticEnvironment(this.runProcess, this._environment())
175
+ const runtimeIdentity = await readDiagnosticRuntimeIdentity({ runProcess: this.runProcess, platform: this.platform, env: environment })
176
+ aborted(signal)
177
+ const lease = await this.modelRuntime.openLease(binding, { candidateDigest: identity.digest, jobName, maxRequests: fresh.effectiveLimits.maxModelRequests, maxResponseBytes: fresh.effectiveLimits.maxResponseBytes })
178
+ let processStarted = false
179
+ // A lightweight in-memory counter is sampled by the controller; unlike
180
+ // lifecycle evidence, request usage cannot be reconstructed after restart.
181
+ onUsage?.(() => lease.usage?.())
182
+ const executionError = error => {
183
+ const safe = safeProcessError(error)
184
+ if (processStarted) Object.assign(safe, { cleanupRequired: true, jobName, diagnosticOnly: true })
185
+ return safe
186
+ }
187
+ try {
188
+ const result = await this.runProcess(this.config.harborBin, args, {
189
+ cwd: root, env: { ...environment, HSE_MODEL_GATEWAY_URL: lease.endpoint, HSE_MODEL_GATEWAY_TOKEN: lease.token, HSE_MODEL_GATEWAY_PROVIDER: lease.candidateProvider, HSE_MODEL_GATEWAY_INFO: JSON.stringify(lease.modelInfo), HSE_MODEL_GATEWAY_PROTOCOL: lease.protocol },
190
+ timeoutMs: DIAGNOSTIC_LIMITS.wallTimeoutMs, maxOutputBytes: 2 * 1024 * 1024, killGraceMs: 30_000, signal,
191
+ onSpawn: pid => { processStarted = true; return onSpawn?.(pid, { job: jobName, dataset: materialized.datasetIdentity, operationId, process: { pid, groupId: pid, platform: this.platform, dockerTransport: 'pinned-local-unix/v1', ...runtimeIdentity } }) },
192
+ })
193
+ const summary = await safeJsonArtifact(root, path.join(jobDir, 'evaluation-summary.json'))
194
+ const context = await safeJsonArtifact(root, path.join(jobDir, 'evaluation-context.json'))
195
+ const count = plan.selection.length
196
+ if (summary?.schema_version !== 3 || summary.job !== jobName || summary.mode !== 'diagnostic' || summary.n_trials !== count || summary.n_discovered_trials !== count || summary.artifact_validation?.valid !== true || context.mode !== 'diagnostic' || context.candidate?.digest !== identity.digest || context.dataset?.source_digest !== materialized.datasetIdentity.source_digest || context.evaluation_stack?.digest !== materialized.identities.stack.digest || summary.evaluation_context?.full_digest !== context.full_digest) {
197
+ throw failure('HARBOR_DIAGNOSTIC_ARTIFACT_INVALID', 'The Job process ended without complete, identity-matching diagnostic artifacts. Inspect the Job; it is not a successful evaluation.')
198
+ }
199
+ return { ...buildEvaluationRunReceipt({ jobName, mode: 'diagnostic', summary, processCode: result.code }), schema: 'harbor-diagnostic-operation-result/v1', operationId, jobName, productionImpact: 'none', promotionEligible: false, sourceJob: path.basename(plan.sourceJob), selectionCount: count, freshBaselineRequired: true, limits: fresh.effectiveLimits }
200
+ } catch (error) { throw executionError(error) }
201
+ finally {
202
+ try { await lease.close() }
203
+ catch (error) { throw executionError(error) }
204
+ }
205
+ }
206
+ }
@@ -0,0 +1,129 @@
1
+ import { constants } from 'node:fs'
2
+ import { access, lstat, mkdir, open, rename, unlink } from 'node:fs/promises'
3
+ import { createHash, randomUUID } from 'node:crypto'
4
+ import path from 'node:path'
5
+
6
+ const SCHEMA = 'harbor-evaluator-save/v1'
7
+ const IDENTITY = /^[A-Za-z0-9][A-Za-z0-9._+-]{0,99}$/
8
+ const DIGEST = /^(?:sha256:)?[a-f0-9]{64}$/i
9
+ const MAX_RECORD_BYTES = 16 * 1024
10
+ const fail = () => { throw new Error('HARBOR_EVALUATOR_SAVE_HISTORY_UNAVAILABLE: Saved-version history could not be safely read or recorded.') }
11
+ const hash = value => createHash('sha256').update(JSON.stringify(value)).digest('hex')
12
+
13
+ function relativeFile(value) {
14
+ if (typeof value !== 'string' || !value || value.length > 1024 || /[\u0000-\u001f\u007f\\]/.test(value) || value.startsWith('/') || /^[a-z][a-z\d+.-]*:/i.test(value) || value.split('/').some(part => !part || part === '.' || part === '..')) fail()
15
+ return value
16
+ }
17
+
18
+ function identity(value) { if (typeof value !== 'string' || !IDENTITY.test(value)) fail(); return value }
19
+
20
+ function scopeFor(args) {
21
+ const scope = { sessionId: args.sessionId, workspace: args.workspace, job: args.job }
22
+ // Workspace IDs may begin with a dot (the project-root workspace label).
23
+ // These opaque strings are hashed, never interpolated into a file path.
24
+ if (Object.values(scope).some(value => typeof value !== 'string' || !value || value.length > 512 || /[\u0000-\u001f\u007f]/.test(value))) fail()
25
+ return scope
26
+ }
27
+
28
+ export function evaluatorSourceIdentity(governance) {
29
+ const evaluator = governance?.components?.evaluator
30
+ return { stack: governance?.stackIdentity, evaluator: { id: evaluator?.id, version: evaluator?.version, digest: evaluator?.digest, entry: evaluator?.entry }, contextDigest: governance?.contextDigest }
31
+ }
32
+
33
+ // The journal stores identities only, never browser-supplied receipts, source
34
+ // text, credentials, or evaluation claims. Live source is re-read by the Host.
35
+ function receiptIdentity(receipt) {
36
+ const evaluator = receipt?.evaluator
37
+ const stack = receipt?.stack
38
+ if (typeof evaluator?.digest !== 'string' || !DIGEST.test(evaluator.digest)) fail()
39
+ return {
40
+ stack: { id: identity(stack?.id), version: identity(stack?.version), path: relativeFile(stack?.path) },
41
+ evaluator: { evaluator_id: identity(evaluator?.evaluator_id), version: identity(evaluator?.version), descriptor_path: relativeFile(evaluator?.descriptor_path), digest: evaluator.digest },
42
+ requires_fresh_baseline: true, automatic_evaluation: false, automatic_gate: false,
43
+ }
44
+ }
45
+
46
+ async function directory(root, create) {
47
+ let current = path.resolve(root)
48
+ for (const segment of ['.harbor', 'workbench-evaluator-saves']) {
49
+ current = path.join(current, segment)
50
+ if (create) await mkdir(current, { mode: 0o700 }).catch(error => { if (error.code !== 'EEXIST') throw error })
51
+ const details = await lstat(current)
52
+ if (!details.isDirectory() || details.isSymbolicLink()) fail()
53
+ }
54
+ return current
55
+ }
56
+
57
+ async function stackDigest(config, relative) {
58
+ const segments = relativeFile(relative).split('/')
59
+ let file = path.resolve(config.projectRoot)
60
+ for (const segment of segments) {
61
+ file = path.join(file, segment)
62
+ if ((await lstat(file)).isSymbolicLink()) fail()
63
+ }
64
+ const handle = await open(file, constants.O_RDONLY | constants.O_NOFOLLOW)
65
+ try {
66
+ const details = await handle.stat()
67
+ if (!details.isFile() || details.size > 1024 * 1024) fail()
68
+ return `sha256:${createHash('sha256').update(await handle.readFile()).digest('hex')}`
69
+ } finally { await handle.close() }
70
+ }
71
+
72
+ // Check journal access before mutating the evaluator. A later disk failure must
73
+ // still be reported as a *saved* version with recovery unavailable, not a failed
74
+ // save which invites the user to repeat a successful source mutation.
75
+ export async function prepareEvaluatorSaveHistory(config, args) {
76
+ const scope = scopeFor(args)
77
+ const destination = await directory(config.projectRoot, true)
78
+ await access(destination, constants.W_OK)
79
+ return { scope, destination }
80
+ }
81
+
82
+ export async function recordEvaluatorSave(config, args, governance, receipt) {
83
+ if (receipt?.requires_fresh_baseline !== true || receipt?.automatic_evaluation !== false || receipt?.automatic_gate !== false) fail()
84
+ const { scope, destination } = await prepareEvaluatorSaveHistory(config, args)
85
+ const value = receiptIdentity(receipt)
86
+ const record = { schema: SCHEMA, scope, sourceDigest: hash(evaluatorSourceIdentity(governance)), stackDigest: await stackDigest(config, value.stack.path), savedAt: new Date().toISOString(), receipt: value }
87
+ const temporary = path.join(destination, `.save-${randomUUID()}.tmp`)
88
+ let handle
89
+ try {
90
+ handle = await open(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600)
91
+ await handle.writeFile(JSON.stringify(record))
92
+ await handle.sync()
93
+ await handle.close(); handle = undefined
94
+ await rename(temporary, path.join(destination, `${hash(scope)}.json`))
95
+ } finally {
96
+ await handle?.close()
97
+ await unlink(temporary).catch(error => { if (error.code !== 'ENOENT') throw error })
98
+ }
99
+ return { ...receipt, continuation: { verification: 'VERIFIED', savedAt: record.savedAt, durable: true } }
100
+ }
101
+
102
+ export async function readEvaluatorSave(config, args, governance, current, inspect) {
103
+ const scope = scopeFor(args)
104
+ let handle
105
+ let record
106
+ try {
107
+ const destination = await directory(config.projectRoot, false)
108
+ handle = await open(path.join(destination, `${hash(scope)}.json`), constants.O_RDONLY | constants.O_NOFOLLOW)
109
+ const details = await handle.stat()
110
+ if (!details.isFile() || details.size > MAX_RECORD_BYTES) fail()
111
+ record = JSON.parse(await handle.readFile('utf8'))
112
+ } catch (error) { if (error.code === 'ENOENT') return undefined; throw error }
113
+ finally { await handle?.close() }
114
+ if (record.schema !== SCHEMA || hash(record.scope) !== hash(scope) || record.sourceDigest !== hash(evaluatorSourceIdentity(governance)) || typeof record.stackDigest !== 'string' || !DIGEST.test(record.stackDigest) || typeof record.savedAt !== 'string' || !Number.isFinite(Date.parse(record.savedAt))) fail()
115
+ const receipt = receiptIdentity(record.receipt)
116
+ // A saved explicit Stack path can differ from the Job's default discovery
117
+ // path. Resolve only the validated recorded path through the Host inspector.
118
+ if (inspect && current?.stack?.path !== receipt.stack.path) {
119
+ try { current = await inspect(receipt.stack.path) } catch { current = undefined }
120
+ }
121
+ let verified = false
122
+ let available = Boolean(current)
123
+ try { verified = hash(receiptIdentity(current)) === hash(receipt) && await stackDigest(config, receipt.stack.path) === record.stackDigest } catch { available = false }
124
+ return {
125
+ ...receipt,
126
+ ...(verified ? { evaluator: { ...receipt.evaluator, editable_files: current.evaluator.editable_files } } : {}),
127
+ continuation: { verification: verified ? 'VERIFIED' : available ? 'DRIFTED' : 'UNAVAILABLE', savedAt: record.savedAt, durable: true, recovered: true },
128
+ }
129
+ }