diff --git a/README.md b/README.md index ce9f5a0..c74a317 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ This repository is the public open-source UADS product. Releases are published f - Context routing, repository map, dependency/impact map - Token budget manager and cache-first prompt architecture - Provider-neutral model routing, evidence protocol, review ZIP workflow -- Global specialist registry with 25 bounded built-in profiles, deterministic domain/gate/evidence obligation coverage, independent assurance, and semantic stale-plan binding +- Global specialist registry with 26 bounded built-in profiles, deterministic domain/gate/evidence obligation coverage, independent assurance, and semantic stale-plan binding - Common Cursor/Codex/Generic Agent Skills adapters with ownership-safe global installation, sidecar-only Host Dispatch Bundles, and the bounded Host Execution handoff/Receipt Boundary with current-authority revalidation and 32-entry history retention - Two-stage GitHub Direct Review Evidence with exact-SHA CI receipt, canonical workflow artifact, and release cross-checks - Staged implementation roadmap diff --git a/agents/README.md b/agents/README.md index d727a1a..93acdb8 100644 --- a/agents/README.md +++ b/agents/README.md @@ -4,6 +4,6 @@ Canonical UADS specialist definitions for the current core registry. Copied to ` Host-invokable core roles: repo-inspector, requirements-engineer, software-architect, implementation-planner, implementation-agent, test-engineer, independent-reviewer, security-reviewer, performance-reviewer, reliability-reviewer, checkpoint-manager. -The bounded domain catalog includes product-requirements-specialist, frontend-specialist, backend-api-specialist, database-specialist, mobile-client-specialist, platform-cloud-specialist, reliability-specialist, data-ai-specialist, web3-contract-specialist, finance-math-specialist, game-systems-specialist, documentation-dx-specialist, release-specialist, and quality-specialist. These descriptors are lean host-facing contracts; they do not invoke providers or grant production authority. +The bounded domain catalog includes product-requirements-specialist, frontend-specialist, backend-api-specialist, database-specialist, mobile-client-specialist, systems-runtime-specialist, platform-cloud-specialist, reliability-specialist, data-ai-specialist, web3-contract-specialist, finance-math-specialist, game-systems-specialist, documentation-dx-specialist, release-specialist, and quality-specialist. These descriptors are lean host-facing contracts; they do not invoke providers or grant production authority. The TypeScript kernel emits routing; hosts/adapters invoke these roles. Do not launch provider APIs from the kernel. diff --git a/agents/uads-systems-runtime-specialist.md b/agents/uads-systems-runtime-specialist.md new file mode 100644 index 0000000..81a34a9 --- /dev/null +++ b/agents/uads-systems-runtime-specialist.md @@ -0,0 +1,11 @@ +--- +name: uads-systems-runtime-specialist +description: Review systems-language, kernel/runtime, contract, event-driven, recovery, and offline-first engineering concerns. +--- +# UADS systems runtime specialist + +Analyze only the Work Order domains assigned by UADS for systems/runtime work. + +Focus on Rust or systems-language boundaries, kernel/runtime lifecycle, contracts, event-driven semantics, state/recovery, offline-first behavior, deterministic failure handling, and platform/runtime compatibility. + +Produce bounded implementation guidance and verification obligations. Do not replace the implementation agent, focused test execution, security assurance, reliability assurance, or independent final review. Do not broaden scope beyond the admitted Work Order. diff --git a/docs/03-SCOPE.md b/docs/03-SCOPE.md index 4956e8a..6647c45 100644 --- a/docs/03-SCOPE.md +++ b/docs/03-SCOPE.md @@ -76,7 +76,7 @@ Every Work Order is classified before expansion: Only **NECESSARY** work enters the current Work Order. IMPORTANT/FUTURE items are recommendations. OUT_OF_SCOPE stays excluded. Execution classifies changed paths as in-scope, supporting, out-of-scope, or sensitive. # Specialist scope boundary (Prompt 009) -The built-in catalog contains 11 core roles and 14 domain profiles. Security, performance, and reliability assurance remain distinct from implementation and support. UGAS and game-assets are explicitly outside this catalog and do not receive automatic integration or project footprint. +The built-in catalog contains 11 core roles and 15 domain profiles. Security, performance, and reliability assurance remain distinct from implementation and support. The additional systems-runtime profile covers systems-language, kernel/runtime, contracts, event-driven systems, state recovery, and offline-first engineering without weakening fail-closed routing. UGAS and game-assets are explicitly outside this catalog and do not receive automatic integration or project footprint. Selection uses a bounded minimum-sufficient greedy algorithm. It covers only domains, affected areas, risk signals, gates, dependencies, and evidence obligations present in the Work Order. Missing required coverage blocks dispatch rather than causing an unrelated specialist expansion. diff --git a/package-lock.json b/package-lock.json index 854b405..b1c03cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.12.1", "license": "Apache-2.0", "dependencies": { - "adm-zip": "^0.6.0", + "adm-zip": "^0.6.1", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "commander": "^12.1.0" @@ -490,9 +490,9 @@ } }, "node_modules/adm-zip": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.6.0.tgz", - "integrity": "sha512-XleryMhbuksdKtofnWZ9Sk+4CUTbms4Mb/EU32SZwToAyZ5RgVos/ki8n+yr0LWHOGKuakbXTuuYNHLQjhddgg==", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.6.1.tgz", + "integrity": "sha512-Xwrja8nx9e5o2N1my4DsKCeKpdrnACyr1wtbPxBDgGzKzKyE9kRtBFA8mWldI+RVlD7CBZNWY/wQ2+ydwOR6kQ==", "license": "MIT", "engines": { "node": ">=14.0" diff --git a/package.json b/package.json index b81a559..9c9fa55 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "uads": "node dist/cli.js" }, "dependencies": { - "adm-zip": "^0.6.0", + "adm-zip": "^0.6.1", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "commander": "^12.1.0" diff --git a/schemas/specialist-profile.schema.json b/schemas/specialist-profile.schema.json index db6b75c..a327efc 100644 --- a/schemas/specialist-profile.schema.json +++ b/schemas/specialist-profile.schema.json @@ -12,8 +12,8 @@ "kind": { "enum": ["core", "domain", "assurance"] }, "status": { "enum": ["enabled", "disabled", "experimental"] }, "purpose": { "type": "string", "minLength": 1, "maxLength": 240 }, - "coveredDomains": { "type": "array", "minItems": 1, "maxItems": 32, "uniqueItems": true, "items": { "enum": ["general", "requirements", "architecture", "frontend", "backend", "api", "database", "saas", "mobile", "cloud-devops", "security", "quality", "performance", "reliability", "data-ai", "web3", "smart-contracts", "wallets", "finance-economics", "tokenomics", "mathematics-simulation", "game-systems", "documentation", "release"] } }, - "functions": { "type": "array", "minItems": 1, "maxItems": 32, "uniqueItems": true, "items": { "enum": ["inspect", "requirements", "architecture", "planning", "implementation", "testing", "independent-review", "security-assurance", "performance-assurance", "reliability-assurance", "checkpoint", "frontend", "backend-api", "database", "mobile", "platform-cloud", "data-ai", "web3-contract", "finance-math", "game-systems", "documentation", "release", "quality"] } }, + "coveredDomains": { "type": "array", "minItems": 1, "maxItems": 32, "uniqueItems": true, "items": { "enum": ["general", "requirements", "architecture", "frontend", "backend", "api", "database", "saas", "mobile", "cloud-devops", "security", "quality", "performance", "reliability", "data-ai", "web3", "smart-contracts", "wallets", "finance-economics", "tokenomics", "mathematics-simulation", "game-systems", "rust", "kernel-runtime", "contracts", "event-driven-systems", "state-recovery", "offline-first", "testing", "certification", "documentation", "release"] } }, + "functions": { "type": "array", "minItems": 1, "maxItems": 32, "uniqueItems": true, "items": { "enum": ["inspect", "requirements", "architecture", "planning", "implementation", "testing", "independent-review", "security-assurance", "performance-assurance", "reliability-assurance", "checkpoint", "frontend", "backend-api", "database", "mobile", "platform-cloud", "data-ai", "web3-contract", "finance-math", "game-systems", "documentation", "release", "quality", "systems-runtime"] } }, "mayImplement": { "type": "boolean" }, "reviewOnly": { "type": "boolean" }, "independenceClass": { "enum": ["implementation", "support", "independent-review", "assurance"] }, diff --git a/src/eval/adapters.ts b/src/eval/adapters.ts index 7864dfc..9e60b4f 100644 --- a/src/eval/adapters.ts +++ b/src/eval/adapters.ts @@ -241,7 +241,7 @@ function prepare(fixture: ReturnType): HostDispatchBundle function runCase(id: string): void { if (id === "AD1") { const fixture = install("cursor"); - assertEval(fixture.resources.length === 25, "Cursor catalog was not installed globally"); + assertEval(fixture.resources.length === 26, "Cursor catalog was not installed globally"); assertEval(!fs.existsSync(path.join(fixture.project, ".cursor")), "Cursor install touched the project"); assertEval(fs.existsSync(getHostAdapterStatePath("cursor", fixture.uadsHome)), "Cursor ownership state is missing"); } else if (id === "AD2") { diff --git a/src/kernel/domains.ts b/src/kernel/domains.ts index 4e2a144..331e500 100644 --- a/src/kernel/domains.ts +++ b/src/kernel/domains.ts @@ -21,6 +21,14 @@ export const DOMAIN_IDS = [ "tokenomics", "mathematics-simulation", "game-systems", + "rust", + "kernel-runtime", + "contracts", + "event-driven-systems", + "state-recovery", + "offline-first", + "testing", + "certification", "documentation", "release", ] as const; diff --git a/src/kernel/policy.ts b/src/kernel/policy.ts index b3feb28..8f22b5c 100644 --- a/src/kernel/policy.ts +++ b/src/kernel/policy.ts @@ -28,11 +28,14 @@ export function classifyScopeSize(intake: NormalizedIntake): ScopeDecision { scopeClass = "trivial"; reasons.push("isolated presentation change"); } else if ( - includesAny(text, ["architecture", "auth architecture", "storage model", "change the public contract", "public api contract"]) + includesAny(text, ["architecture", "auth architecture", "storage model", "change the public contract", "public api contract"]) || + intake.domainSignals.includes("kernel-runtime") || + (intake.domainSignals.includes("state-recovery") && intake.domainSignals.includes("offline-first")) ) { scopeClass = "architectural"; - reasons.push("public contract or core architecture is in play"); + reasons.push("public contract, kernel/runtime, or core architecture is in play"); } else if ( + intake.domainSignals.length >= 3 || intake.domainSignals.includes("database") || intake.riskSignals.includes("database-migration") || (intake.domainSignals.includes("api") && intake.domainSignals.includes("security")) || @@ -121,6 +124,16 @@ export function classifyRisk(intake: NormalizedIntake, map: RepositoryMap): { le } return { level: "HIGH", reasons }; } + if ( + signals.has("kernel-runtime") || + signals.has("state-recovery") || + signals.has("offline-first") || + signals.has("certification") || + (signals.has("event-driven-systems") && signals.has("contracts")) + ) { + reasons.push("foundational runtime, recovery, offline, contract, or certification surface"); + return { level: "HIGH", reasons }; + } if (signals.has("authentication") || signals.has("public-api") || signals.has("infrastructure")) { reasons.push("auth, public contract, or infrastructure signal"); return { level: "MEDIUM", reasons }; diff --git a/src/kernel/routing.ts b/src/kernel/routing.ts index 015d812..5b2c108 100644 --- a/src/kernel/routing.ts +++ b/src/kernel/routing.ts @@ -71,7 +71,7 @@ export const SPECIALISTS: SpecialistDef[] = [ id: "test-engineer", purpose: "Design and run focused tests", activation: "non-docs non-style plans", - domains: ["quality"], + domains: ["quality", "testing"], mayImplement: false, reviewOnly: true, expectedInput: "Work Order + gates", @@ -81,7 +81,7 @@ export const SPECIALISTS: SpecialistDef[] = [ id: "independent-reviewer", purpose: "Independently review implementation", activation: "any plan that includes implementation-agent", - domains: ["quality"], + domains: ["quality", "certification"], mayImplement: false, reviewOnly: true, expectedInput: "diff + gates", @@ -118,6 +118,16 @@ export const SPECIALISTS: SpecialistDef[] = [ expectedInput: "dependencies + rollback evidence", expectedOutput: "reliability review evidence", }, + { + id: "systems-runtime-specialist", + purpose: "Handle systems, kernel/runtime, contracts, event-driven, recovery, and offline-first concerns", + activation: "systems/runtime domain signals", + domains: ["rust", "kernel-runtime", "contracts", "event-driven-systems", "state-recovery", "offline-first"], + mayImplement: false, + reviewOnly: true, + expectedInput: "runtime architecture + contracts + recovery obligations", + expectedOutput: "systems/runtime verification guidance", + }, { id: "checkpoint-manager", purpose: "Persist resume state", @@ -162,6 +172,9 @@ export function selectSpecialists(input: { if (input.intake.riskSignals.includes("performance-hot-path") || input.domains.includes("performance")) { assurance.push("performance-reviewer"); } + if (input.domains.some((id) => ["rust", "kernel-runtime", "contracts", "event-driven-systems", "state-recovery", "offline-first"].includes(id))) { + specialists.push("systems-runtime-specialist"); + } const uniqueSpecialists = unique(specialists); const uniqueAssurance = unique(assurance); @@ -242,6 +255,12 @@ export function selectGates(input: { if (input.intake.riskSignals.includes("performance-hot-path") || input.domains.includes("performance")) { add("performance-check", "hot-path regression check"); } + if (input.domains.includes("contracts") || input.domains.includes("event-driven-systems")) { + add("contract-test", "systems contracts and event boundaries require contract evidence"); + } + if (input.domains.includes("state-recovery") || input.domains.includes("offline-first")) { + add("rollback-validation", "state recovery and offline behavior require rollback/recovery evidence"); + } if (input.risk === "HIGH" || input.risk === "CRITICAL") { add("security-review", "high/critical plans require security assurance"); } diff --git a/src/kernel/specialist-catalog.ts b/src/kernel/specialist-catalog.ts index 32e04ec..63042b1 100644 --- a/src/kernel/specialist-catalog.ts +++ b/src/kernel/specialist-catalog.ts @@ -53,8 +53,8 @@ export const BUILTIN_SPECIALIST_PROFILES: BuiltinProfile[] = [ profile({ specialistId: "software-architect", kind: "core", purpose: "Bound cross-cutting architecture decisions", coveredDomains: ["architecture"], functions: ["architecture"], independenceClass: "support", requiredInputs: ["intake and repository map"], producesEvidence: ["architecture bounds"], priority: 30, notes: "Must not approve its own implementation or invent external capabilities." }), profile({ specialistId: "implementation-planner", kind: "core", purpose: "Produce a bounded executable Work Order plan", coveredDomains: ["general"], functions: ["planning"], independenceClass: "support", requiredInputs: ["intake and routing evidence"], producesEvidence: ["Work Order plan"], priority: 40, notes: "Must not dispatch work outside the selected scope." }), profile({ specialistId: "implementation-agent", kind: "core", purpose: "Apply necessary in-scope product edits", coveredDomains: ["general"], functions: ["implementation"], mayImplement: true, reviewOnly: false, independenceClass: "implementation", requiredInputs: ["Work Order and execution packet"], producesEvidence: ["in-scope change"], priority: 50, notes: "Must not perform approval-gated actions or serve as the sole final reviewer." }), - profile({ specialistId: "test-engineer", kind: "core", purpose: "Design and run focused verification", coveredDomains: ["quality"], functions: ["testing"], independenceClass: "support", requiredInputs: ["Work Order and selected gates"], producesEvidence: ["test evidence"], priority: 60, notes: "Must not self-approve implementation changes." }), - profile({ specialistId: "independent-reviewer", kind: "core", purpose: "Independently review the implementation", coveredDomains: ["quality"], functions: ["independent-review"], independenceClass: "independent-review", requiredInputs: ["diff and gate evidence"], producesEvidence: ["independent review"], priority: 70, notes: "Must use an independent session and must not implement the reviewed change." }), + profile({ specialistId: "test-engineer", kind: "core", purpose: "Design and run focused verification", coveredDomains: ["quality", "testing"], functions: ["testing"], independenceClass: "support", requiredInputs: ["Work Order and selected gates"], producesEvidence: ["test evidence"], priority: 60, notes: "Must not self-approve implementation changes." }), + profile({ specialistId: "independent-reviewer", kind: "core", purpose: "Independently review the implementation", coveredDomains: ["quality", "certification"], functions: ["independent-review"], independenceClass: "independent-review", requiredInputs: ["diff and gate evidence"], producesEvidence: ["independent review"], priority: 70, notes: "Must use an independent session and must not implement the reviewed change." }), profile({ specialistId: "security-reviewer", kind: "assurance", purpose: "Provide security assurance for sensitive changes", coveredDomains: ["security"], functions: ["security-assurance"], independenceClass: "assurance", activation: {}, requiredInputs: ["risk signals and relevant diff"], producesEvidence: ["security review"], priority: 80, notes: "Must not expose secrets or approve by implementing." }), profile({ specialistId: "performance-reviewer", kind: "assurance", purpose: "Provide performance assurance for hot paths", coveredDomains: ["performance"], functions: ["performance-assurance"], independenceClass: "assurance", activation: { domainAny: ["performance"], riskSignalsAny: ["performance-hot-path"] }, requiredInputs: ["hot-path scope and benchmarks"], producesEvidence: ["performance evidence"], priority: 90, notes: "Must not trade correctness or scope controls for a local benchmark result." }), profile({ specialistId: "reliability-reviewer", kind: "assurance", purpose: "Provide reliability and rollback assurance", coveredDomains: ["reliability"], functions: ["reliability-assurance"], independenceClass: "assurance", activation: { domainAny: ["reliability", "database", "cloud-devops"], riskSignalsAny: ["database-migration", "destructive", "infrastructure"] }, requiredInputs: ["dependency and rollback evidence"], producesEvidence: ["reliability review"], priority: 95, notes: "Must not approve irreversible operations without rollback evidence." }), @@ -64,6 +64,7 @@ export const BUILTIN_SPECIALIST_PROFILES: BuiltinProfile[] = [ profile({ specialistId: "backend-api-specialist", kind: "domain", purpose: "Handle backend, API, and SaaS boundaries", coveredDomains: ["backend", "api", "saas"], functions: ["backend-api"], independenceClass: "support", activation: { domainAny: ["backend", "api", "saas"] }, requiredInputs: ["API or service contract"], producesEvidence: ["backend/API verification"], priority: 130, notes: "Must preserve authentication, contract, and data-boundary obligations." }), profile({ specialistId: "database-specialist", kind: "domain", purpose: "Handle database schema and migration safety", coveredDomains: ["database"], functions: ["database"], independenceClass: "support", activation: { domainAny: ["database"] }, requiredInputs: ["schema impact and migration plan"], producesEvidence: ["migration and rollback evidence"], priority: 140, notes: "Must not execute destructive production operations." }), profile({ specialistId: "mobile-client-specialist", kind: "domain", purpose: "Handle mobile client behavior and compatibility", coveredDomains: ["mobile"], functions: ["mobile"], independenceClass: "support", activation: { domainAny: ["mobile"] }, requiredInputs: ["mobile scope and supported clients"], producesEvidence: ["mobile compatibility evidence"], priority: 150, notes: "Must not assume desktop or server behavior is equivalent to mobile behavior." }), + profile({ specialistId: "systems-runtime-specialist", kind: "domain", purpose: "Handle systems language, kernel/runtime, contract, event-driven, state recovery, and offline-first concerns", coveredDomains: ["rust", "kernel-runtime", "contracts", "event-driven-systems", "state-recovery", "offline-first"], functions: ["systems-runtime"], independenceClass: "support", activation: { domainAny: ["rust", "kernel-runtime", "contracts", "event-driven-systems", "state-recovery", "offline-first"] }, requiredInputs: ["runtime architecture, contracts, and state/recovery obligations"], producesEvidence: ["systems/runtime verification guidance"], priority: 155, notes: "Must not replace focused testing, security assurance, or independent certification." }), profile({ specialistId: "platform-cloud-specialist", kind: "domain", purpose: "Handle cloud, DevOps, and release platform concerns", coveredDomains: ["cloud-devops", "release"], functions: ["platform-cloud"], independenceClass: "support", activation: { domainAny: ["cloud-devops"] }, requiredInputs: ["platform scope and deployment constraints"], producesEvidence: ["platform readiness"], priority: 160, notes: "Must not deploy or change paid infrastructure without approval." }), profile({ specialistId: "reliability-specialist", kind: "domain", purpose: "Handle service reliability and operational resilience", coveredDomains: ["reliability"], functions: ["reliability-assurance"], independenceClass: "support", activation: { domainAny: ["reliability"] }, requiredInputs: ["failure modes and dependencies"], producesEvidence: ["reliability plan"], priority: 165, notes: "Must not replace independent assurance for high-risk changes." }), profile({ specialistId: "data-ai-specialist", kind: "domain", purpose: "Handle data and AI pipeline behavior", coveredDomains: ["data-ai"], functions: ["data-ai"], independenceClass: "support", activation: { domainAny: ["data-ai"] }, requiredInputs: ["data flow and quality obligations"], producesEvidence: ["data/AI validation"], priority: 170, notes: "Must not infer model or data-provider behavior without evidence." }), diff --git a/src/kernel/specialist-registry.ts b/src/kernel/specialist-registry.ts index 37a13ba..9cc296c 100644 --- a/src/kernel/specialist-registry.ts +++ b/src/kernel/specialist-registry.ts @@ -33,7 +33,7 @@ const FUNCTIONS: SpecialistFunction[] = [ "inspect", "requirements", "architecture", "planning", "implementation", "testing", "independent-review", "security-assurance", "performance-assurance", "reliability-assurance", "checkpoint", "frontend", "backend-api", "database", "mobile", "platform-cloud", "data-ai", "web3-contract", "finance-math", "game-systems", - "documentation", "release", "quality", + "documentation", "release", "quality", "systems-runtime", ]; const PROFILE_KEYS = [ "schema", "schemaVersion", "specialistId", "kind", "status", "purpose", "coveredDomains", "functions", diff --git a/src/kernel/specialist-types.ts b/src/kernel/specialist-types.ts index 69b69b2..cbff3aa 100644 --- a/src/kernel/specialist-types.ts +++ b/src/kernel/specialist-types.ts @@ -32,7 +32,8 @@ export type SpecialistFunction = | "game-systems" | "documentation" | "release" - | "quality"; + | "quality" + | "systems-runtime"; export type SpecialistActivation = { scopeClasses?: ScopeClass[]; diff --git a/src/lib/inspect-review.ts b/src/lib/inspect-review.ts index 0e86aef..0b6e786 100644 --- a/src/lib/inspect-review.ts +++ b/src/lib/inspect-review.ts @@ -5,7 +5,7 @@ import addFormatsImport from "ajv-formats"; import { EVIDENCE_FILE_NAMES } from "./evidence.js"; import { isUnsafeZipEntryName } from "./exclusions.js"; import { findPackageRoot } from "./version.js"; -import { readZip } from "./zip-read.js"; +import { readZip, readZipCentralDirectoryNames } from "./zip-read.js"; import { containsAbsoluteHostPath, containsUnredactedSecret } from "./secrets.js"; import { validateCanonicalReleaseEvidence } from "./release-review.js"; @@ -94,18 +94,34 @@ export async function inspectReviewBundle( const errors: string[] = []; const requireEvidence = options.requireEvidence ?? true; - let entries; + let rawNames: string[]; try { - entries = await readZip(zipPath); + rawNames = readZipCentralDirectoryNames(zipPath) + .filter((name) => !name.endsWith("/")) + .map((name) => name.replace(/\\/g, "/")); } catch (error) { return { ok: false, errors: [`zip-unreadable: ${error instanceof Error ? error.message : String(error)}`] }; } - const names = entries.map((entry) => entry.name.replace(/\\/g, "/")); - if (new Set(names).size !== names.length) { + if (new Set(rawNames).size !== rawNames.length) { errors.push("duplicate-entry"); } + for (const name of rawNames) { + if (isUnsafeZipEntryName(name)) errors.push("unsafe-entry-path"); + for (const marker of EXCLUDED_ENTRY_MARKERS) { + if (name.includes(marker)) errors.push(`excluded-path-present:${name}`); + } + } + if (errors.length > 0) return { ok: false, errors: [...new Set(errors)] }; + let entries; + try { + entries = await readZip(zipPath); + } catch (error) { + return { ok: false, errors: [`zip-unreadable: ${error instanceof Error ? error.message : String(error)}`] }; + } + + const names = entries.map((entry) => entry.name.replace(/\\/g, "/")); const nameSet = new Set(names); const required = requireEvidence ? REQUIRED_REVIEW_ENTRIES @@ -117,17 +133,6 @@ export async function inspectReviewBundle( } } - for (const name of names) { - if (isUnsafeZipEntryName(name)) { - errors.push("unsafe-entry-path"); - } - for (const marker of EXCLUDED_ENTRY_MARKERS) { - if (name.includes(marker)) { - errors.push(`excluded-path-present:${name}`); - } - } - } - const manifestEntry = entries.find((entry) => entry.name.replace(/\\/g, "/") === "review-manifest.json"); if (!manifestEntry) { errors.push("missing-entry:review-manifest.json"); diff --git a/src/lib/zip-read.ts b/src/lib/zip-read.ts index f9f77da..48c8239 100644 --- a/src/lib/zip-read.ts +++ b/src/lib/zip-read.ts @@ -6,6 +6,73 @@ export type ZipEntry = { content: Buffer; }; +const EOCD_SIGNATURE = 0x06054b50; +const CENTRAL_SIGNATURE = 0x02014b50; +const MAX_EOCD_SEARCH = 22 + 0xffff; + +function findEocd(buffer: Buffer): number { + const start = Math.max(0, buffer.length - MAX_EOCD_SEARCH); + for (let offset = buffer.length - 22; offset >= start; offset -= 1) { + if (buffer.readUInt32LE(offset) === EOCD_SIGNATURE) return offset; + } + throw new Error("ZIP end-of-central-directory record is missing"); +} + +/** + * Reads raw central-directory entry names without relying on adm-zip's + * in-memory entry normalization/deduplication. Final-byte inspection uses + * this to preserve duplicate-name and unsafe-path detection across library + * upgrades. + * + * UADS review bundles are intentionally bounded standard ZIP archives. + * ZIP64 is rejected fail-closed here rather than guessed. + */ +export function readZipCentralDirectoryNames(zipPath: string): string[] { + const buffer = fs.readFileSync(zipPath); + if (buffer.length < 22) throw new Error("ZIP is truncated"); + + const eocd = findEocd(buffer); + const diskEntries = buffer.readUInt16LE(eocd + 8); + const totalEntries = buffer.readUInt16LE(eocd + 10); + const centralSize = buffer.readUInt32LE(eocd + 12); + const centralOffset = buffer.readUInt32LE(eocd + 16); + + if ( + diskEntries === 0xffff || + totalEntries === 0xffff || + centralSize === 0xffffffff || + centralOffset === 0xffffffff + ) { + throw new Error("ZIP64 review bundles are not supported"); + } + if (diskEntries !== totalEntries) throw new Error("multi-disk ZIP review bundles are not supported"); + if (centralOffset + centralSize > eocd || centralOffset > buffer.length) { + throw new Error("ZIP central directory bounds are invalid"); + } + + const names: string[] = []; + let offset = centralOffset; + const centralEnd = centralOffset + centralSize; + + for (let index = 0; index < totalEntries; index += 1) { + if (offset + 46 > centralEnd || buffer.readUInt32LE(offset) !== CENTRAL_SIGNATURE) { + throw new Error("ZIP central directory entry is invalid"); + } + const nameLength = buffer.readUInt16LE(offset + 28); + const extraLength = buffer.readUInt16LE(offset + 30); + const commentLength = buffer.readUInt16LE(offset + 32); + const recordLength = 46 + nameLength + extraLength + commentLength; + if (recordLength < 46 || offset + recordLength > centralEnd) { + throw new Error("ZIP central directory entry exceeds declared bounds"); + } + names.push(buffer.subarray(offset + 46, offset + 46 + nameLength).toString("utf8")); + offset += recordLength; + } + + if (offset !== centralEnd) throw new Error("ZIP central directory size does not match parsed entries"); + return names; +} + export function readZip(zipPath: string): Promise { const zip = new AdmZip(zipPath); const entries = zip diff --git a/tests/orchestrator-kernel.test.ts b/tests/orchestrator-kernel.test.ts index db6920a..8034971 100644 --- a/tests/orchestrator-kernel.test.ts +++ b/tests/orchestrator-kernel.test.ts @@ -229,4 +229,42 @@ describe("orchestrator kernel", { timeout: 90_000 }, () => { }); expect(inspected.map.projectId).toBe("aaaaaaaaaaaaaaaa"); }); + it("classifies foundational Forge-style kernel work as architectural HIGH risk", () => { + const { repo, home } = tempDirs(); + seedRepo(repo); + const planned = runPlan({ + cwd: repo, + uadsHome: home, + intake: normalizeIntake({ + schema: "uads.intake", + schemaVersion: "0.2.0", + objective: "Implement the complete sovereign Forge kernel and contract runtime", + domainSignals: [ + "rust", + "kernel-runtime", + "contracts", + "event-driven-systems", + "state-recovery", + "offline-first", + "testing", + "certification", + ], + riskSignals: [], + affectedAreas: ["kernel", "runtime", "state"], + classifier: "host-structured", + }), + }); + expect(planned.workOrder.scopeClass).toBe("architectural"); + expect(planned.workOrder.riskLevel).toBe("HIGH"); + expect(planned.specialistPlan.status).toBe("SELECTED"); + expect(planned.workOrder.specialists).toContain("systems-runtime-specialist"); + expect(planned.workOrder.assuranceReviewers).toEqual( + expect.arrayContaining(["independent-reviewer", "security-reviewer"]), + ); + expect(planned.workOrder.qualityGates).toEqual( + expect.arrayContaining(["static", "unit-test", "architecture-conformance", "contract-test", "rollback-validation", "security-review"]), + ); + }); + + }); diff --git a/tests/specialist-routing.test.ts b/tests/specialist-routing.test.ts index cd591d8..1580977 100644 --- a/tests/specialist-routing.test.ts +++ b/tests/specialist-routing.test.ts @@ -414,4 +414,34 @@ describe("specialist registry and deterministic routing", () => { selectSpecialistPlan({ ...current, requiredEvidence: ["gate:unit-test"] }).selectionDigest, ); }); + it("covers Forge-style systems runtime domains without NO_DOMAIN_COVERAGE", () => { + const domains = [ + "rust", + "kernel-runtime", + "contracts", + "event-driven-systems", + "state-recovery", + "offline-first", + "testing", + "certification", + ]; + const plan = selectSpecialistPlan(input({ + objective: "Implement a sovereign Rust kernel runtime with contracts, events, recovery, offline operation, testing, and certification", + domains, + scopeClass: "architectural", + riskLevel: "HIGH", + gates: ["unit-test"], + requiredEvidence: ["gate:unit-test"], + })); + expect(plan.status).toBe("SELECTED"); + expect(plan.blockedReasonCodes).not.toContain("NO_DOMAIN_COVERAGE"); + expect(plan.selected.map((item) => item.specialistId)).toEqual( + expect.arrayContaining(["systems-runtime-specialist", "test-engineer"]), + ); + expect(plan.assurance.map((item) => item.specialistId)).toContain("independent-reviewer"); + const covered = new Set(plan.coveredObligations.filter((item) => item.coverageKind === "domain").map((item) => item.obligationId.replace(/^domain:/, ""))); + for (const domain of domains) expect(covered.has(domain)).toBe(true); + }); + + });