dsh 0.2.0-rc.2: in-container update (experimental channel), path-agnostic compose, upstream patches adapted to 0.1.x/0.2.x

This commit is contained in:
hermes committed 2026-10-01 19:21:19 +00:00
commit e6dcef9bf5
302 files changed
+75178

No files matched your search

@@ -0,0 +1,297 @@
import { createHash, randomUUID } from 'node:crypto'
import { spawn } from 'node:child_process'
import { cp, lstat, mkdir, readFile, readdir, readlink, rename, rm, symlink, writeFile } from 'node:fs/promises'
import { dirname, join, resolve } from 'node:path'
import { canonicalJson } from '../../../platform/lib/canonical-json.mjs'
import { parseEnvironmentManifest } from '../../../platform/lib/contracts.mjs'
import { hashTree } from '../../../platform/lib/tree-hash.mjs'
const ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
const STATE_SCHEMA = 1
export const SYSTEM_SKILL_CATALOG_ARTIFACT_ID = 'system-skill-catalog'
export const SYSTEM_SKILL_CATALOG_MEDIA_TYPE = 'application/vnd.dsh-platform.system-skill-catalog.v1+tar+gzip'
function runTar(args, { capture = false } = {}) {
return new Promise((resolveTar, reject) => {
const child = spawn('tar', args, { stdio: ['ignore', capture ? 'pipe' : 'ignore', 'pipe'] })
const stdout = []
const stderr = []
child.stdout?.on('data', chunk => stdout.push(chunk))
child.stderr.on('data', chunk => stderr.push(chunk))
child.once('error', reject)
child.once('exit', code => code === 0
? resolveTar(Buffer.concat(stdout).toString('utf8'))
: reject(new Error(`System Skill archive failed: ${Buffer.concat(stderr).toString('utf8')}`)))
})
}
async function exists(path) {
return lstat(path).then(() => true, error => error?.code === 'ENOENT' ? false : Promise.reject(error))
}
export async function retainLegacySystemSkillCatalog({ sourceRoot, outputRoot }) {
const source = resolve(sourceRoot)
const destination = join(resolve(outputRoot), 'legacy-bootstrap')
if (await exists(join(destination, 'catalog.json'))) {
await readSystemSkillCatalog(destination)
return destination
}
await readSystemSkillCatalog(source)
const staging = `${destination}.${randomUUID()}.tmp`
try {
await cp(source, staging, { recursive: true, verbatimSymlinks: true })
try {
await rename(staging, destination)
} catch (error) {
if (error?.code !== 'EEXIST' && error?.code !== 'ENOTEMPTY') throw error
}
} finally {
await rm(staging, { recursive: true, force: true })
}
await readSystemSkillCatalog(destination)
return destination
}
export async function materializeSystemSkillCatalog({ environmentRoot, outputRoot }) {
const root = resolve(environmentRoot)
const manifest = parseEnvironmentManifest(await readFile(join(root, 'environment.manifest.json')))
const artifact = manifest.artifacts.find(entry => entry.id === SYSTEM_SKILL_CATALOG_ARTIFACT_ID)
if (artifact === undefined) return undefined
if (artifact.mediaType !== SYSTEM_SKILL_CATALOG_MEDIA_TYPE) {
throw new Error('System Skill catalog Artifact media type is invalid')
}
const archive = join(root, 'artifacts', artifact.id)
const bytes = await readFile(archive)
if (bytes.byteLength !== artifact.size || createHash('sha256').update(bytes).digest('hex') !== artifact.sha256) {
throw new Error('System Skill catalog Artifact differs from its Environment manifest')
}
const destination = join(resolve(outputRoot), artifact.sha256)
if (!await exists(destination)) {
const staging = `${destination}.${randomUUID()}.tmp`
await mkdir(staging, { recursive: true })
try {
const entries = (await runTar(['-tzf', archive], { capture: true })).split('\n').filter(Boolean)
if (entries.length === 0 || entries.some(entry => (
entry.startsWith('/') || !entry.startsWith('skills/') || entry.split('/').includes('..')
))) throw new Error('System Skill catalog archive contains an unsafe path')
await runTar(['-xzf', archive, '--no-same-owner', '--no-same-permissions', '--strip-components=1', '-C', staging])
await readSystemSkillCatalog(staging)
try {
await rename(staging, destination)
} catch (error) {
if (error?.code !== 'EEXIST' && error?.code !== 'ENOTEMPTY') throw error
}
} finally {
await rm(staging, { recursive: true, force: true })
}
}
await readSystemSkillCatalog(destination)
return destination
}
function exactKeys(value, keys, label) {
if (value === null || typeof value !== 'object' || Array.isArray(value)
|| Object.keys(value).sort().join(',') !== [...keys].sort().join(',')) {
throw new Error(`${label} is invalid`)
}
return value
}
function skillId(value) {
if (typeof value !== 'string' || !ID_PATTERN.test(value) || value.length > 64) {
throw new Error('System Skill ID is invalid')
}
return value
}
function description(value, label) {
exactKeys(value, ['en', 'zh'], label)
for (const language of ['en', 'zh']) {
if (typeof value[language] !== 'string' || value[language].length === 0 || value[language].length > 240) {
throw new Error(`${label}.${language} is invalid`)
}
}
return Object.freeze({ en: value.en, zh: value.zh })
}
export async function readSystemSkillCatalog(root) {
const sourceRoot = resolve(root)
const document = JSON.parse(await readFile(join(sourceRoot, 'catalog.json'), 'utf8'))
exactKeys(document, ['schema', 'skills'], 'System Skill catalog')
if (document.schema !== 1 || !Array.isArray(document.skills)) throw new Error('System Skill catalog is invalid')
const ids = new Set()
const skills = []
for (const entry of document.skills) {
exactKeys(entry, ['description', 'id', 'source'], 'System Skill catalog entry')
const id = skillId(entry.id)
if (ids.has(id)) throw new Error(`duplicate System Skill ID ${id}`)
ids.add(id)
if (typeof entry.source !== 'string' || entry.source !== id) throw new Error(`System Skill ${id} source is invalid`)
const source = resolve(sourceRoot, entry.source)
if (!source.startsWith(`${sourceRoot}/`)) throw new Error(`System Skill ${id} source escapes its catalog`)
const metadata = await lstat(join(source, 'SKILL.md'))
if (!metadata.isFile() || metadata.isSymbolicLink()) throw new Error(`System Skill ${id} SKILL.md is invalid`)
const bytes = await readFile(join(source, 'SKILL.md'), 'utf8')
if (!bytes.startsWith('---\n') || !bytes.includes(`\nname: ${id}\n`)) {
throw new Error(`System Skill ${id} frontmatter does not match its catalog`)
}
skills.push(Object.freeze({
id,
source,
sha256: await hashTree(source),
description: description(entry.description, `System Skill ${id} description`),
}))
}
return Object.freeze(skills)
}
function defaults(catalog) {
return Object.fromEntries(catalog.map(skill => [skill.id, { installed: true, enabled: true }]))
}
function normalizeState(value, catalog) {
const fallback = defaults(catalog)
if (value === undefined) return fallback
exactKeys(value, ['schema', 'skills'], 'System Skill selection state')
if (value.schema !== STATE_SCHEMA || value.skills === null || typeof value.skills !== 'object' || Array.isArray(value.skills)) {
throw new Error('System Skill selection state is invalid')
}
for (const skill of catalog) {
const saved = value.skills[skill.id]
if (saved === undefined) continue
exactKeys(saved, ['enabled', 'installed'], `System Skill ${skill.id} selection`)
if (typeof saved.installed !== 'boolean' || typeof saved.enabled !== 'boolean' || (saved.enabled && !saved.installed)) {
throw new Error(`System Skill ${skill.id} selection is invalid`)
}
fallback[skill.id] = { installed: saved.installed, enabled: saved.enabled }
}
return fallback
}
export class SystemSkillSelectionStore {
constructor(path) {
this.path = resolve(path)
}
async read(catalog) {
let value
try {
value = JSON.parse(await readFile(this.path, 'utf8'))
} catch (error) {
if (error?.code !== 'ENOENT') throw error
}
return normalizeState(value, catalog)
}
async write(catalog, state) {
const normalized = normalizeState({ schema: STATE_SCHEMA, skills: state }, catalog)
const document = {
schema: STATE_SCHEMA,
skills: Object.fromEntries(catalog.map(skill => [skill.id, normalized[skill.id]])),
}
await mkdir(dirname(this.path), { recursive: true })
const temporary = `${this.path}.${randomUUID()}.tmp`
try {
await writeFile(temporary, `${canonicalJson(document)}\n`, { flag: 'wx', mode: 0o600 })
await rename(temporary, this.path)
} finally {
await rm(temporary, { force: true })
}
return normalized
}
}
async function replaceSkillLink(viewRoot, skill) {
const path = join(viewRoot, skill.id)
const existing = await lstat(path).catch(error => error?.code === 'ENOENT' ? undefined : Promise.reject(error))
if (existing?.isSymbolicLink() && await readlink(path) === skill.source) return
const temporary = join(viewRoot, `.${skill.id}.${randomUUID()}.tmp`)
try {
await symlink(skill.source, temporary, 'dir')
await rename(temporary, path)
} finally {
await rm(temporary, { recursive: true, force: true })
}
}
export async function synchronizeSystemSkillView({ catalog, selection, viewRoot }) {
const root = resolve(viewRoot)
await mkdir(root, { recursive: true })
const expected = new Set(catalog.filter(skill => selection[skill.id].enabled).map(skill => skill.id))
for (const entry of await readdir(root, { withFileTypes: true })) {
if (!expected.has(entry.name)) await rm(join(root, entry.name), { recursive: true, force: true })
}
for (const skill of catalog) {
if (selection[skill.id].enabled) await replaceSkillLink(root, skill)
}
}
export class SystemSkillManager {
constructor({ sourceRoot, viewRoot, statePath }) {
this.sourceRoot = resolve(sourceRoot)
this.viewRoot = resolve(viewRoot)
this.store = new SystemSkillSelectionStore(statePath)
}
async initialize() {
const catalog = await readSystemSkillCatalog(this.sourceRoot)
const selection = await this.store.read(catalog)
await this.store.write(catalog, selection)
await synchronizeSystemSkillView({ catalog, selection, viewRoot: this.viewRoot })
return this.listFrom(catalog, selection)
}
async useSourceRoot(sourceRoot) {
const nextRoot = resolve(sourceRoot)
const catalog = await readSystemSkillCatalog(nextRoot)
const selection = await this.store.read(catalog)
await this.store.write(catalog, selection)
await synchronizeSystemSkillView({ catalog, selection, viewRoot: this.viewRoot })
this.sourceRoot = nextRoot
return this.listFrom(catalog, selection)
}
listFrom(catalog, selection) {
return Object.freeze(catalog.map(skill => Object.freeze({
id: skill.id,
sha256: skill.sha256,
description: skill.description,
installed: selection[skill.id].installed,
enabled: selection[skill.id].enabled,
})))
}
async list() {
const catalog = await readSystemSkillCatalog(this.sourceRoot)
return this.listFrom(catalog, await this.store.read(catalog))
}
async configure(idValue, action) {
const id = skillId(idValue)
if (!['install', 'uninstall', 'enable', 'disable'].includes(action)) throw new Error('System Skill action is invalid')
const catalog = await readSystemSkillCatalog(this.sourceRoot)
if (!catalog.some(skill => skill.id === id)) throw new Error(`System Skill ${id} is not provided by the current Environment`)
const before = await this.store.read(catalog)
const next = structuredClone(before)
const current = next[id]
if (action === 'install') next[id] = { installed: true, enabled: true }
else if (action === 'uninstall') next[id] = { installed: false, enabled: false }
else if (action === 'enable') {
if (!current.installed) throw new Error(`System Skill ${id} must be installed before it can be enabled`)
next[id] = { ...current, enabled: true }
} else next[id] = { ...current, enabled: false }
try {
await this.store.write(catalog, next)
await synchronizeSystemSkillView({ catalog, selection: next, viewRoot: this.viewRoot })
return this.listFrom(catalog, next)
} catch (error) {
try {
await this.store.write(catalog, before)
await synchronizeSystemSkillView({ catalog, selection: before, viewRoot: this.viewRoot })
} catch (rollbackError) {
throw new AggregateError([error, rollbackError], 'System Skill operation and rollback failed')
}
throw error
}
}
}