dsh-harbor-evolution 0.8.3 → 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/README.md +28 -6
- package/index.js +120 -19
- package/lib/action-drafts.js +443 -0
- package/lib/bounded-process.js +190 -0
- package/lib/candidate-runtime.js +160 -0
- package/lib/candidate.js +7 -11
- package/lib/client.js +4110 -292
- package/lib/composer-context.js +56 -0
- package/lib/credential-redaction.js +155 -0
- package/lib/dashboard.js +417 -98
- package/lib/diagnostic-observation.js +175 -0
- package/lib/diagnostic-runner.js +206 -0
- package/lib/evaluator-saves.js +129 -0
- package/lib/evolution.js +107 -32
- package/lib/historical-run-lock.js +102 -0
- package/lib/historical-web.js +52 -16
- package/lib/interaction-objects.js +56 -0
- package/lib/model-runtime.js +48 -3
- package/lib/runtime-identity.js +0 -1
- package/lib/service.js +1427 -28
- package/lib/session-diagnostic.js +0 -1
- package/lib/session-redaction.js +17 -31
- package/lib/session-selection.js +5 -3
- package/lib/trial-selection.js +46 -0
- package/lib/ui-context.js +518 -0
- package/lib/web.js +32 -6
- package/lib/workbench-health.js +27 -0
- package/package.json +4 -4
- package/skills/evolve-agent-with-harbor/SKILL.md +33 -4
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import { lstat, readFile } from 'node:fs/promises'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
|
|
5
|
+
export const CANDIDATE_RUNTIME_NAME = 'candidate-runtime.json'
|
|
6
|
+
const DESCRIPTOR_KEYS = ['schema_version', 'transport', 'entrypoint', 'config_path', 'agent_entry_id', 'node_version']
|
|
7
|
+
const RESERVED_PATHS = new Set(['node_modules', '.harbor-runtime', '.git'])
|
|
8
|
+
const EXACT_VERSION = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/
|
|
9
|
+
const DEPENDENCY_FIELDS = ['dependencies', 'optionalDependencies', 'devDependencies', 'peerDependencies']
|
|
10
|
+
|
|
11
|
+
const object = value => value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
12
|
+
const sha256 = bytes => `sha256:${createHash('sha256').update(bytes).digest('hex')}`
|
|
13
|
+
|
|
14
|
+
function safeRelative(value, label) {
|
|
15
|
+
if (typeof value !== 'string' || !value || value.length > 1024 || value.trim() !== value || /[\\:\x00-\x1f\x7f]/.test(value) || path.posix.isAbsolute(value)) {
|
|
16
|
+
throw new Error(`${label} must be a safe Candidate-relative path`)
|
|
17
|
+
}
|
|
18
|
+
const parts = value.split('/')
|
|
19
|
+
if (parts.some(part => !part || part === '.' || part === '..' || RESERVED_PATHS.has(part))) {
|
|
20
|
+
throw new Error(`${label} must not traverse or use reserved Candidate paths`)
|
|
21
|
+
}
|
|
22
|
+
return parts
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function sourceFile(root, relative, { optional = false } = {}) {
|
|
26
|
+
const parts = safeRelative(relative, relative)
|
|
27
|
+
let absolute = root
|
|
28
|
+
for (let index = 0; index < parts.length; index++) {
|
|
29
|
+
absolute = path.join(absolute, parts[index])
|
|
30
|
+
let info
|
|
31
|
+
try {
|
|
32
|
+
info = await lstat(absolute)
|
|
33
|
+
} catch (error) {
|
|
34
|
+
if (optional && index === parts.length - 1 && error.code === 'ENOENT') return undefined
|
|
35
|
+
if (error.code === 'ENOENT') throw new Error(`Candidate runtime requires a regular source file: ${relative}`)
|
|
36
|
+
throw error
|
|
37
|
+
}
|
|
38
|
+
if (info.isSymbolicLink() || (index === parts.length - 1 ? !info.isFile() : !info.isDirectory())) {
|
|
39
|
+
throw new Error(`Candidate runtime requires a regular source file without symlinks: ${relative}`)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return readFile(absolute)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseObject(bytes, label) {
|
|
46
|
+
if (bytes.length > 4 * 1024 * 1024) throw new Error(`${label} exceeds the runtime metadata size limit`)
|
|
47
|
+
let value
|
|
48
|
+
try {
|
|
49
|
+
const source = bytes.toString('utf8')
|
|
50
|
+
if (!Buffer.from(source, 'utf8').equals(bytes)) throw new Error('Invalid UTF-8')
|
|
51
|
+
value = JSON.parse(source)
|
|
52
|
+
} catch {
|
|
53
|
+
throw new Error(`${label} is not valid JSON`)
|
|
54
|
+
}
|
|
55
|
+
if (!object(value)) throw new Error(`${label} must be an object`)
|
|
56
|
+
return value
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function dependencies(value, field, label) {
|
|
60
|
+
const entries = Object.hasOwn(value, field) ? value[field] : {}
|
|
61
|
+
if (!object(entries)) throw new Error(`${label} ${field} must be an object`)
|
|
62
|
+
for (const [name, version] of Object.entries(entries)) {
|
|
63
|
+
if (typeof version !== 'string' || !EXACT_VERSION.test(version)) {
|
|
64
|
+
throw new Error(`${label} ${field}.${name} must use an exact semver, not a tag, range, alias, or local dependency`)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return Object.entries(entries).sort(([left], [right]) => left.localeCompare(right))
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function validateLockfile(packageJson, lockfile) {
|
|
71
|
+
if (lockfile.lockfileVersion !== 3 || !object(lockfile.packages) || !object(lockfile.packages[''])) {
|
|
72
|
+
throw new Error('Candidate runtime requires package-lock.json v3 with packages[\'\'] root metadata')
|
|
73
|
+
}
|
|
74
|
+
const root = lockfile.packages['']
|
|
75
|
+
for (const field of ['name', 'version']) {
|
|
76
|
+
if (typeof packageJson[field] !== 'string' || !packageJson[field] || root[field] !== packageJson[field]) {
|
|
77
|
+
throw new Error(`package-lock.json root ${field} must match package.json ${field}`)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
for (const field of DEPENDENCY_FIELDS) {
|
|
81
|
+
const expected = dependencies(packageJson, field, 'package.json')
|
|
82
|
+
const actual = dependencies(root, field, 'package-lock.json root')
|
|
83
|
+
if (JSON.stringify(expected) !== JSON.stringify(actual)) {
|
|
84
|
+
throw new Error(`package-lock.json root ${field} must match package.json ${field}`)
|
|
85
|
+
}
|
|
86
|
+
for (const [name, version] of expected) {
|
|
87
|
+
const target = lockfile.packages[`node_modules/${name}`]
|
|
88
|
+
if (!object(target) || target.version !== version) {
|
|
89
|
+
throw new Error(`package-lock.json root ${field} dependency must resolve to its exact locked version`)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
for (const [location, entry] of Object.entries(lockfile.packages)) {
|
|
94
|
+
if (!location) continue
|
|
95
|
+
if (!location.startsWith('node_modules/') || /[\\:\x00-\x1f\x7f]/.test(location) || location.split('/').some(part => !part || part === '.' || part === '..' || part === '.git' || part === '.harbor-runtime')) {
|
|
96
|
+
throw new Error(`package-lock.json contains an unsupported package location: ${location}`)
|
|
97
|
+
}
|
|
98
|
+
if (!object(entry) || entry.link || entry.inBundle || typeof entry.version !== 'string' || !EXACT_VERSION.test(entry.version)) {
|
|
99
|
+
throw new Error(`package-lock.json ${location} requires an exact registry version without links or bundled dependencies`)
|
|
100
|
+
}
|
|
101
|
+
let resolved
|
|
102
|
+
try {
|
|
103
|
+
resolved = new URL(entry.resolved)
|
|
104
|
+
} catch {
|
|
105
|
+
throw new Error(`package-lock.json ${location} requires an HTTPS resolved package URL`)
|
|
106
|
+
}
|
|
107
|
+
if (typeof entry.resolved !== 'string' || !entry.resolved.startsWith('https://') || /[\x00-\x20\x7f\\]/.test(entry.resolved) || resolved.protocol !== 'https:' || !resolved.hostname || resolved.username || resolved.password || resolved.search || resolved.hash || entry.resolved.includes('?') || entry.resolved.includes('#')) {
|
|
108
|
+
throw new Error(`package-lock.json ${location} requires an HTTPS resolved package URL without credentials, query, or fragment`)
|
|
109
|
+
}
|
|
110
|
+
const integrity = entry.integrity
|
|
111
|
+
if (typeof integrity !== 'string' || !/^sha512-[A-Za-z0-9+/]{86}==$/.test(integrity) || Buffer.from(integrity.slice(7), 'base64').toString('base64') !== integrity.slice(7)) {
|
|
112
|
+
throw new Error(`package-lock.json ${location} requires a valid sha512 integrity digest`)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Resolve only Candidate-owned, source-digested runtime metadata; never select a registry default. */
|
|
118
|
+
export async function loadCandidateRuntime(candidateDir, { required = false } = {}) {
|
|
119
|
+
const root = path.resolve(candidateDir)
|
|
120
|
+
const bytes = await sourceFile(root, CANDIDATE_RUNTIME_NAME, { optional: true })
|
|
121
|
+
if (bytes === undefined) {
|
|
122
|
+
if (required) throw new Error(`Candidate runtime is unbound; migrate this Candidate by adding ${CANDIDATE_RUNTIME_NAME} and a locked ACP entrypoint, then create a new snapshot`)
|
|
123
|
+
return { kind: 'deepseek-harness', policy: 'unbound', transport: 'acp' }
|
|
124
|
+
}
|
|
125
|
+
const descriptor = parseObject(bytes, CANDIDATE_RUNTIME_NAME)
|
|
126
|
+
if (Object.keys(descriptor).some(key => !DESCRIPTOR_KEYS.includes(key)) || DESCRIPTOR_KEYS.some(key => !Object.hasOwn(descriptor, key))) {
|
|
127
|
+
throw new Error(`${CANDIDATE_RUNTIME_NAME} requires exactly: ${DESCRIPTOR_KEYS.join(', ')}`)
|
|
128
|
+
}
|
|
129
|
+
if (descriptor.schema_version !== 1 || descriptor.transport !== 'acp') {
|
|
130
|
+
throw new Error(`${CANDIDATE_RUNTIME_NAME} requires schema_version=1 and transport=acp`)
|
|
131
|
+
}
|
|
132
|
+
safeRelative(descriptor.entrypoint, 'entrypoint')
|
|
133
|
+
if (!/\.(?:js|mjs|cjs)$/.test(descriptor.entrypoint)) throw new Error('Candidate runtime entrypoint must be a .js, .mjs, or .cjs source file')
|
|
134
|
+
safeRelative(descriptor.config_path, 'config_path')
|
|
135
|
+
if (typeof descriptor.agent_entry_id !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(descriptor.agent_entry_id)) {
|
|
136
|
+
throw new Error('Candidate runtime agent_entry_id must be a non-empty safe identifier')
|
|
137
|
+
}
|
|
138
|
+
if (typeof descriptor.node_version !== 'string' || !/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(descriptor.node_version) || Number(descriptor.node_version.split('.')[0]) < 22) {
|
|
139
|
+
throw new Error('Candidate runtime node_version must be an exact x.y.z release with major >=22')
|
|
140
|
+
}
|
|
141
|
+
const entrypointBytes = await sourceFile(root, descriptor.entrypoint)
|
|
142
|
+
await sourceFile(root, descriptor.config_path)
|
|
143
|
+
const packageBytes = await sourceFile(root, 'package.json')
|
|
144
|
+
const lockfileBytes = await sourceFile(root, 'package-lock.json')
|
|
145
|
+
validateLockfile(parseObject(packageBytes, 'package.json'), parseObject(lockfileBytes, 'package-lock.json'))
|
|
146
|
+
return {
|
|
147
|
+
kind: 'deepseek-harness',
|
|
148
|
+
policy: 'candidate-locked',
|
|
149
|
+
transport: 'acp',
|
|
150
|
+
descriptor: CANDIDATE_RUNTIME_NAME,
|
|
151
|
+
entrypoint: descriptor.entrypoint,
|
|
152
|
+
config_path: descriptor.config_path,
|
|
153
|
+
agent_entry_id: descriptor.agent_entry_id,
|
|
154
|
+
node_version: descriptor.node_version,
|
|
155
|
+
lockfile: 'package-lock.json',
|
|
156
|
+
descriptor_digest: sha256(bytes),
|
|
157
|
+
entrypoint_digest: sha256(entrypointBytes),
|
|
158
|
+
lockfile_digest: sha256(lockfileBytes),
|
|
159
|
+
}
|
|
160
|
+
}
|
package/lib/candidate.js
CHANGED
|
@@ -2,7 +2,7 @@ import { createHash } from 'node:crypto'
|
|
|
2
2
|
import { mkdir, readFile, readdir, stat, writeFile } from 'node:fs/promises'
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { loadCandidateRuntime } from './candidate-runtime.js'
|
|
6
6
|
|
|
7
7
|
export const MANIFEST_NAME = 'candidate-manifest.json'
|
|
8
8
|
export const MODEL_BINDING_NAME = 'model-binding.json'
|
|
@@ -11,6 +11,7 @@ const EXCLUDED_DIRS = new Set(['.git', 'node_modules', '__pycache__', '.harbor-r
|
|
|
11
11
|
const EXCLUDED_FILES = new Set([MANIFEST_NAME, '.DS_Store'])
|
|
12
12
|
const LOCKFILES = ['package-lock.json', 'pnpm-lock.yaml', 'yarn.lock', 'bun.lock', 'bun.lockb']
|
|
13
13
|
const CREDENTIAL_FILES = new Set([
|
|
14
|
+
'.npmrc',
|
|
14
15
|
'credentials.json',
|
|
15
16
|
'service-account.json',
|
|
16
17
|
'secrets.json',
|
|
@@ -99,14 +100,14 @@ export async function computeCandidate(candidateDir) {
|
|
|
99
100
|
return { digest: `sha256:${digest.digest('hex')}`, files }
|
|
100
101
|
}
|
|
101
102
|
|
|
102
|
-
async function validateCandidateContract(root) {
|
|
103
|
+
async function validateCandidateContract(root, runtime) {
|
|
103
104
|
try {
|
|
104
105
|
await stat(path.join(root, '.harbor-runtime'))
|
|
105
106
|
throw new Error('Candidate must not contain the reserved .harbor-runtime path')
|
|
106
107
|
} catch (error) {
|
|
107
108
|
if (error.code !== 'ENOENT') throw error
|
|
108
109
|
}
|
|
109
|
-
for (const required of ['cordis.yml', 'package.json']) {
|
|
110
|
+
for (const required of [runtime.config_path ?? 'cordis.yml', 'package.json']) {
|
|
110
111
|
try {
|
|
111
112
|
if (!(await stat(path.join(root, required))).isFile()) throw new Error()
|
|
112
113
|
} catch {
|
|
@@ -131,7 +132,8 @@ async function validateCandidateContract(root) {
|
|
|
131
132
|
|
|
132
133
|
export async function snapshotCandidate(candidateDir, options = {}) {
|
|
133
134
|
const root = path.resolve(candidateDir)
|
|
134
|
-
await
|
|
135
|
+
const runtime = await loadCandidateRuntime(root)
|
|
136
|
+
await validateCandidateContract(root, runtime)
|
|
135
137
|
let packageJson
|
|
136
138
|
try {
|
|
137
139
|
packageJson = JSON.parse(await readFile(path.join(root, 'package.json'), 'utf8'))
|
|
@@ -158,13 +160,7 @@ export async function snapshotCandidate(candidateDir, options = {}) {
|
|
|
158
160
|
version: String(version),
|
|
159
161
|
digest: computed.digest,
|
|
160
162
|
created_at: new Date().toISOString(),
|
|
161
|
-
runtime
|
|
162
|
-
kind: 'deepseek-harness',
|
|
163
|
-
policy: RUNTIME_POLICY,
|
|
164
|
-
version: DSH_RUNTIME_VERSION,
|
|
165
|
-
package: CANDIDATE_ACP_PACKAGE,
|
|
166
|
-
transport: 'acp',
|
|
167
|
-
},
|
|
163
|
+
runtime,
|
|
168
164
|
files: computed.files,
|
|
169
165
|
metadata,
|
|
170
166
|
}
|