/** * Foundation synthetic inspector — pure evaluation core. * * Implements charter candidate 3 (docs/plans/2026-09-06_foundation-inspector-charter.md, * sha256 19b67211…) §§3–7 and §10 over an already-parsed bundle value. This module * never touches the filesystem, process, environment, clock or network; it imports * only ./canonical.mjs (which imports node:crypto for SHA-256). * * Global stage order (§10.4): * 2 closed shapes / profile / supported kinds * 3 identity index and duplicate checks * 4 structural reference existence * 5 cycles: task.dependencies, mission.parentMissionRef, supersedes * 6 revision-chain continuity * 7 per-record ownership / parent-scope * 8 supported operation selection * 9 context binding, current-head, work-scope guards, ordered admission (L1–L9) * (Stage 1, bounds/lexical parsing, lives in strict-json.mjs and the CLI.) * * Every refusal names a FIXED rule identifier from RULES; rules are never derived * from input text. Internal `detail` locators exist for unit tests only and are * never part of the closed result. */ import { canonicalize, digestOf, CanonicalError } from "./canonical.mjs"; // --------------------------------------------------------------------------- // Constants // --------------------------------------------------------------------------- export const DISCLAIMER = "SYNTHETIC PREVIEW — NO LIVE EFFECTS"; export const PREVIEW = "preview: no live registrations or permission grants"; export const OPERATION_CATALOG = Object.freeze([ "work.read", "file.read", "file.change", "command.execute", "work.propose", "task.create", "assignment.change", "result.submit", "task.accept", "registration.manage", "conversation.read", "conversation.observe", "execution.launch", "execution.control", "execution.transfer", "workspace.retire", "workspace.reopen", "message.send", "audit.read", "project.create", "project.change", "workspace.create", "workspace.change", "mission.create", "mission.change", "mission.accept", "task.change", "decision.issue", "session.adopt", ]); export const SUPPORTED_OPERATIONS = Object.freeze(["work.read", "file.read", "file.change", "assignment.change"]); export const TASKLESS_OPERATIONS = Object.freeze(["work.read", "file.read"]); export const RECORD_KINDS = Object.freeze([ "agent-definition", "project", "workspace", "registration", "mission", "task", "assignment", "decision", "session", "context-source", "adapter-capability", ]); export const SUPPORTED_KINDS = Object.freeze([ "agent-definition", "project", "workspace", "registration", "mission", "task", "assignment", "decision", ]); export const REGISTRIES = Object.freeze([ "harness", "settings", "context-content", "scope-role", "agent-policy", "project-policy", "endpoint", ]); export const CONTENT_REGISTRIES = Object.freeze(["scope-role", "agent-policy", "project-policy"]); export const RESULTS = Object.freeze(["allowed", "refused", "unresolved", "invalid"]); export const REASONS = Object.freeze([ "allowed", "invalid-request", "missing-state", "stale-revision", "not-authorized", "unsupported-capability", "unsupported-kind", "unknown-effects", "io-failure", ]); /** Fixed, documented rule identifiers. Nothing else is ever emitted as `rule`. */ export const RULES = Object.freeze([ // CLI / lexical (stage 1) "usage-invalid", "input-too-large", "input-parse-failed", "open-flags-unavailable", "input-open-failed", "input-not-regular", "input-size-changed", // shapes / profile (stage 2) "shape-unknown-field", "shape-missing-field", "shape-type-mismatch", "shape-enum-mismatch", "shape-pattern-mismatch", "shape-bound-exceeded", "shape-unique-violation", "shape-conditional-mismatch", "calendar-invalid", "path-invalid", "record-kind-unsupported", "profile-pattern-mismatch", "registry-digest-mismatch", "mock-content-unsupported", // identity (stage 3) "duplicate-record-identity", "duplicate-registry-identity", "duplicate-artifact-identity", "duplicate-authorization-id", "duplicate-delegation-input", // structure (stage 4) "record-reference-missing", // cycles (stage 5) "dependency-cycle", "mission-parent-cycle", "supersedes-cycle", // continuity (stage 6) "revision-chain-gap", "supersedes-mismatch", // ownership (stage 7) "project-id-scope-mismatch", "workspace-id-scope-mismatch", "workspace-project-missing", "registration-parent-mismatch", "mission-parent-scope-mismatch", "mission-owning-project-mismatch", "assignment-task-scope-mismatch", // operation selection (stage 8) "operation-unknown", "operation-unsupported", "operation-target-mismatch", "proposal-required", "proposal-not-applicable", // admission (stage 9) "selected-agent-missing", "selected-project-missing", "selected-workspace-missing", "authorization-undeclared", "artifact-undeclared", "registry-declaration-missing", "agent-disabled", "agent-policy-missing", "project-not-active", "project-policy-missing", "workspace-not-active", "workspace-policy-missing", "policy-ceiling-stale", "registration-ambiguous", "registration-revoked", "project-registration-missing", "workspace-registration-missing", "registration-parent-stale", "registration-parent-not-current", "scope-role-missing", "registration-delegation-stale", "registration-delegation-not-approved", "registration-delegation-scope-not-modelled", "registration-delegation-subject-form", "registration-delegation-bounds", "delegation-input-missing", "delegation-exceeds-issuer-ceiling", "assignment-required", "assignment-stale", "assignment-not-selected", "assignment-agent-mismatch", "assignment-workspace-mismatch", "task-ref-stale", "assignment-intent-stale", "assignment-intent-not-applicable", "task-not-ready", "task-intent-stale", "task-intent-not-approved", "cross-workspace-work-reference-not-modelled", "cross-project-work-reference-not-modelled", "mission-stale", "mission-not-active", "dependency-stale", "consulted-work-not-readable", "dependency-not-accepted", "operation-not-permitted", "path-not-permitted", // proposal (§5, r2 §8) "proposal-subject-mismatch", "proposal-selection-mismatch", "requester-agent-missing", "requester-context-mismatch", "requester-lacks-original-scope-authority", "requester-lacks-target-scope-authority", "message-is-not-authority", "delegation-missing", "delegation-stale", "delegation-not-approved", "delegation-operation-not-covered", "delegation-bounds", "delegation-not-applicable", "change-not-recorded", "change-decision-stale", "change-not-approved", "change-subjects-mismatch", "intent-not-current", "runtime-reconciliation-required", ]); const RULE_SET = new Set(RULES); /** Proposal step identifiers and their only possible results (unit-tested: no "allowed"). */ export const PROPOSAL_STEPS = Object.freeze({ "requester-lacks-original-scope-authority": "refused", "requester-lacks-target-scope-authority": "refused", "delegation-not-applicable": "refused", "message-is-not-authority": "refused", "change-not-recorded": "refused", "intent-not-current": "refused", "runtime-reconciliation-required": "unresolved", }); const MAX_INVENTORY = 256; const MAX_ARRAY = 1024; const ID_RE = /^[a-z0-9][a-z0-9._-]{0,63}$/; const RUNTIME_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/; const DIGEST_RE = /^sha256:[0-9a-f]{64}$/; const TIME_RE = /^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})\.[0-9]{3}Z$/; const FORBIDDEN_CATEGORY_RE = /[\p{Cc}\p{Cf}\p{Cs}]/u; const MAX_SAFE = 9007199254740991; /** Typed string grammars shared with the pinned records.schema.json ($defs/id, $defs/runtimeId, $defs/digest). */ export const PINNED_PATTERNS = Object.freeze({ id: ID_RE, runtimeId: RUNTIME_ID_RE, digest: DIGEST_RE }); /** * Schema column (pinned-checker compatibility). The pinned checker evaluates "pattern" * through Python re.search, whose "$" (no MULTILINE) also matches immediately before * exactly one final "\n"; the inspector's schema verdict reproduces that verdict for the * three typed grammars. The value itself is never trimmed or normalized: the second test * runs on a copy and only the verdict is derived from it. */ export function pinnedPatternMatches(re, v) { return re.test(v) || (v.endsWith("\n") && re.test(v.slice(0, -1))); } /** Profile column (addendum FI-C2-1): ECMAScript "$" — the whole string must satisfy the grammar. */ export function strictPatternMatches(re, v) { return re.test(v); } // --------------------------------------------------------------------------- // Refusal // --------------------------------------------------------------------------- export class Refusal extends Error { constructor(exit, reason, rule, detail) { super(`${reason}/${rule}${detail ? ` at ${detail}` : ""}`); this.name = "Refusal"; if (!RULE_SET.has(rule)) throw new Error(`internal: unknown rule ${rule}`); if (!REASONS.includes(reason)) throw new Error(`internal: unknown reason ${reason}`); this.exit = exit; this.reason = reason; this.rule = rule; this.detail = detail || null; this.step = null; } } function refuse(exit, reason, rule, detail) { throw new Refusal(exit, reason, rule, detail); } // --------------------------------------------------------------------------- // Generic helpers (prototype-agnostic: values may be null-prototype objects) // --------------------------------------------------------------------------- function isObject(v) { return typeof v === "object" && v !== null && !Array.isArray(v); } function hasKey(o, k) { return Object.prototype.hasOwnProperty.call(o, k); } function codePointLength(s) { let n = 0; for (const _ of s) n += 1; // eslint-disable-line no-unused-vars return n; } function utf8ByteLength(s) { let n = 0; for (const ch of s) { const cp = ch.codePointAt(0); if (cp >= 0xd800 && cp <= 0xdfff) return -1; // lone surrogate: not encodable n += cp < 0x80 ? 1 : cp < 0x800 ? 2 : cp < 0x10000 ? 3 : 4; } return n; } /** JSON-value deep equality; objects compare as unordered key sets (jsonschema uniqueItems semantics). */ export function deepEqual(a, b) { if (a === b) return true; if (typeof a !== typeof b) return false; if (typeof a !== "object" || a === null || b === null) return false; if (Array.isArray(a) !== Array.isArray(b)) return false; if (Array.isArray(a)) { if (a.length !== b.length) return false; for (let i = 0; i < a.length; i += 1) if (!deepEqual(a[i], b[i])) return false; return true; } const ka = Object.keys(a); const kb = Object.keys(b); if (ka.length !== kb.length) return false; for (const k of ka) { if (!hasKey(b, k)) return false; if (!deepEqual(a[k], b[k])) return false; } return true; } function hasDuplicates(items) { for (let i = 0; i < items.length; i += 1) { for (let j = i + 1; j < items.length; j += 1) { if (deepEqual(items[i], items[j])) return true; } } return false; } // --------------------------------------------------------------------------- // Format checks (pinned check.py semantics, §8) // --------------------------------------------------------------------------- function daysInMonth(year, month) { if (month === 2) { const leap = (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; return leap ? 29 : 28; } return [4, 6, 9, 11].includes(month) ? 30 : 31; } /** * $defs/time: pattern plus the pinned checker's calendar semantics (check.py: * strptime then strftime round-trip). On the measured platform (CPython 3.12.8, * glibc 2.44) strftime("%Y") is not zero-padded below year 1000, so the pinned * checker refuses years 0001..0999; this validator refuses them identically. * verify-schema.py witnesses that platform behaviour before comparing. */ export function isValidTime(value) { if (typeof value !== "string") return false; const m = TIME_RE.exec(value); if (!m) return false; const [year, month, day, hour, minute, second] = m.slice(1, 7).map(Number); if (year < 1000 || year > 9999) return false; if (month < 1 || month > 12) return false; if (day < 1 || day > daysInMonth(year, month)) return false; if (hour > 23 || minute > 59 || second > 59) return false; return true; } /** * $defs/relativePath: schema pattern + minLength/maxLength + mosaic-relative-path * format (check.py), implemented procedurally: no leading "/", no backslash, * no empty/"."/".." segment, 1..4096 UTF-8 bytes, no Cc/Cf/Cs code points. */ export function isValidRelativePath(value) { if (typeof value !== "string") return false; const bytes = utf8ByteLength(value); if (bytes < 1 || bytes > 4096) return false; if (value.startsWith("/")) return false; if (value.includes("\\")) return false; if (FORBIDDEN_CATEGORY_RE.test(value)) return false; for (const segment of value.split("/")) { if (segment === "" || segment === "." || segment === "..") return false; } return true; } // --------------------------------------------------------------------------- // Shape validator (stage 2). Throws ShapeError; first failure wins. // --------------------------------------------------------------------------- export class ShapeError extends Error { constructor(rule, path) { super(`${rule} at ${path}`); this.name = "ShapeError"; this.rule = rule; this.path = path; } } function shapeFail(rule, path) { throw new ShapeError(rule, path); } function checkObject(v, path, allowed) { if (!isObject(v)) shapeFail("shape-type-mismatch", path); for (const k of Object.keys(v)) { if (!allowed.includes(k)) shapeFail("shape-unknown-field", `${path}.${k}`); } return v; } function requireKeys(v, path, keys) { for (const k of keys) if (!hasKey(v, k)) shapeFail("shape-missing-field", `${path}.${k}`); } function checkString(v, path, min, max) { if (typeof v !== "string") shapeFail("shape-type-mismatch", path); const n = codePointLength(v); if (n < min || n > max) shapeFail("shape-bound-exceeded", path); return v; } // Profile violation sink (addendum FI-C2-1). Inside a validation scope // (validateRecordShape / validateBundleShape) every schema-valid typed value that fails // the strict end-of-string grammar is recorded by path, in validation order, and refused // as profile-pattern-mismatch after the shape pass and before identity, graph or // admission. Outside a scope the same value is refused immediately (fail closed). let profileSink = null; function checkPattern(v, path, re) { if (typeof v !== "string") shapeFail("shape-type-mismatch", path); if (!pinnedPatternMatches(re, v)) shapeFail("shape-pattern-mismatch", path); if (!strictPatternMatches(re, v)) { if (profileSink === null) shapeFail("profile-pattern-mismatch", path); profileSink.push(path); } return v; } function checkId(v, path) { return checkPattern(v, path, ID_RE); } function checkRuntimeId(v, path) { return checkPattern(v, path, RUNTIME_ID_RE); } function checkDigest(v, path) { return checkPattern(v, path, DIGEST_RE); } function checkEnum(v, path, values) { if (!values.includes(v)) shapeFail("shape-enum-mismatch", path); return v; } function checkConst(v, path, c) { if (v !== c) shapeFail("shape-enum-mismatch", path); return v; } function checkInteger(v, path, min, max) { if (typeof v !== "number" || !Number.isInteger(v)) shapeFail("shape-type-mismatch", path); if (v < min || v > max) shapeFail("shape-bound-exceeded", path); return v; } function checkRevision(v, path) { return checkInteger(v, path, 1, MAX_SAFE); } function checkArray(v, path, min, max, unique, itemCheck) { if (!Array.isArray(v)) shapeFail("shape-type-mismatch", path); if (v.length < min || v.length > max) shapeFail("shape-bound-exceeded", path); v.forEach((item, i) => itemCheck(item, `${path}[${i}]`)); if (unique && hasDuplicates(v)) shapeFail("shape-unique-violation", path); return v; } function checkTime(v, path) { if (typeof v !== "string") shapeFail("shape-type-mismatch", path); if (!TIME_RE.test(v)) shapeFail("shape-pattern-mismatch", path); if (!isValidTime(v)) shapeFail("calendar-invalid", path); return v; } function checkRelativePath(v, path) { if (typeof v !== "string") shapeFail("shape-type-mismatch", path); if (!isValidRelativePath(v)) shapeFail("path-invalid", path); return v; } function checkScope(v, path, kinds) { if (!isObject(v)) shapeFail("shape-type-mismatch", path); if (!hasKey(v, "kind")) shapeFail("shape-missing-field", `${path}.kind`); const kind = v.kind; if (!["system", "project", "workspace"].includes(kind)) shapeFail("shape-enum-mismatch", `${path}.kind`); if (kind === "system") { checkObject(v, path, ["kind"]); } else if (kind === "project") { checkObject(v, path, ["kind", "projectId"]); requireKeys(v, path, ["projectId"]); checkId(v.projectId, `${path}.projectId`); } else { checkObject(v, path, ["kind", "projectId", "workspaceId"]); requireKeys(v, path, ["projectId", "workspaceId"]); checkId(v.projectId, `${path}.projectId`); checkId(v.workspaceId, `${path}.workspaceId`); } if (kinds && !kinds.includes(kind)) shapeFail("shape-enum-mismatch", `${path}.kind`); return v; } function checkRecordRef(v, path, kind) { checkObject(v, path, ["kind", "id", "scope", "revision"]); requireKeys(v, path, ["kind", "id", "scope", "revision"]); checkEnum(v.kind, `${path}.kind`, RECORD_KINDS); if (kind && v.kind !== kind) shapeFail("shape-enum-mismatch", `${path}.kind`); checkId(v.id, `${path}.id`); checkScope(v.scope, `${path}.scope`, null); checkRevision(v.revision, `${path}.revision`); return v; } function checkRegistryRef(v, path, registry) { checkObject(v, path, ["registry", "id", "revision", "digest"]); requireKeys(v, path, ["registry", "id", "revision", "digest"]); checkEnum(v.registry, `${path}.registry`, REGISTRIES); if (registry && v.registry !== registry) shapeFail("shape-enum-mismatch", `${path}.registry`); checkId(v.id, `${path}.id`); checkRevision(v.revision, `${path}.revision`); checkDigest(v.digest, `${path}.digest`); return v; } function checkArtifactRef(v, path) { checkObject(v, path, ["runId", "artifactId", "digest"]); requireKeys(v, path, ["runId", "artifactId", "digest"]); checkId(v.runId, `${path}.runId`); checkId(v.artifactId, `${path}.artifactId`); checkDigest(v.digest, `${path}.digest`); return v; } function checkActor(v, path) { checkObject(v, path, ["kind", "principalId", "executionId"]); requireKeys(v, path, ["kind", "principalId", "executionId"]); checkEnum(v.kind, `${path}.kind`, ["human", "service", "agent"]); checkId(v.principalId, `${path}.principalId`); if (v.kind === "agent") { if (v.executionId === null) shapeFail("shape-conditional-mismatch", `${path}.executionId`); checkRuntimeId(v.executionId, `${path}.executionId`); } else if (v.executionId !== null) { shapeFail("shape-conditional-mismatch", `${path}.executionId`); } return v; } function checkPathGrant(v, path) { checkObject(v, path, ["root", "path"]); requireKeys(v, path, ["root", "path"]); checkConst(v.root, `${path}.root`, "workspace"); if (v.path !== null) checkRelativePath(v.path, `${path}.path`); return v; } export function checkRestrictions(v, path) { checkObject(v, path, ["operations", "readPaths", "writePaths", "network", "endpointRefs"]); requireKeys(v, path, ["operations", "readPaths", "writePaths", "network", "endpointRefs"]); checkArray(v.operations, `${path}.operations`, 0, MAX_ARRAY, true, (op, p) => checkEnum(op, p, OPERATION_CATALOG)); checkArray(v.readPaths, `${path}.readPaths`, 0, MAX_ARRAY, true, checkPathGrant); checkArray(v.writePaths, `${path}.writePaths`, 0, MAX_ARRAY, true, checkPathGrant); checkEnum(v.network, `${path}.network`, ["none", "approved-endpoints"]); checkArray(v.endpointRefs, `${path}.endpointRefs`, 0, MAX_ARRAY, true, (r, p) => checkRegistryRef(r, p, "endpoint")); if (v.network === "none") { if (v.endpointRefs.length !== 0) shapeFail("shape-conditional-mismatch", `${path}.endpointRefs`); } else if (v.endpointRefs.length < 1) { shapeFail("shape-conditional-mismatch", `${path}.endpointRefs`); } return v; } function checkNullable(v, path, check) { if (v === null) return v; return check(v, path); } function checkCriteria(v, path) { return checkArray(v, path, 1, MAX_ARRAY, true, (c, p) => { checkObject(c, p, ["id", "text"]); requireKeys(c, p, ["id", "text"]); checkId(c.id, `${p}.id`); checkString(c.text, `${p}.text`, 1, 4000); }); } function checkContentRefs(v, path) { return checkArray(v, path, 0, MAX_ARRAY, true, (r, p) => checkRegistryRef(r, p, "context-content")); } /** * The envelope pass has already validated rec.scope in full (including its typed ids); * payload checks only constrain which scope kinds the record kind admits. Re-validating * the scope here would double-count profile violations under a bare path. */ function requireScopeKind(rec, payloadPath, kinds) { if (!kinds.includes(rec.scope.kind)) shapeFail("shape-enum-mismatch", `${payloadPath.slice(0, -".payload".length)}.scope.kind`); } const PAYLOAD_CHECKS = { "agent-definition": (p, path, rec) => { requireScopeKind(rec, path, ["system"]); const keys = ["displayName", "agentType", "harnessRef", "settingsRef", "soulRef", "instructionRefs", "skillRefs", "policyRef", "status"]; checkObject(p, path, keys); requireKeys(p, path, keys); checkString(p.displayName, `${path}.displayName`, 1, 128); checkId(p.agentType, `${path}.agentType`); checkRegistryRef(p.harnessRef, `${path}.harnessRef`, "harness"); checkRegistryRef(p.settingsRef, `${path}.settingsRef`, "settings"); checkRegistryRef(p.soulRef, `${path}.soulRef`, "context-content"); checkContentRefs(p.instructionRefs, `${path}.instructionRefs`); checkContentRefs(p.skillRefs, `${path}.skillRefs`); checkRegistryRef(p.policyRef, `${path}.policyRef`, "agent-policy"); checkEnum(p.status, `${path}.status`, ["enabled", "disabled"]); }, project: (p, path, rec) => { requireScopeKind(rec, path, ["project"]); const keys = ["displayName", "policyRef", "status"]; checkObject(p, path, keys); requireKeys(p, path, keys); checkString(p.displayName, `${path}.displayName`, 1, 128); checkRegistryRef(p.policyRef, `${path}.policyRef`, "project-policy"); checkEnum(p.status, `${path}.status`, ["active", "retired"]); }, workspace: (p, path, rec) => { requireScopeKind(rec, path, ["workspace"]); const keys = ["displayName", "policyRef", "status", "fileRootId"]; checkObject(p, path, keys); requireKeys(p, path, keys); checkString(p.displayName, `${path}.displayName`, 1, 128); checkRegistryRef(p.policyRef, `${path}.policyRef`, "project-policy"); checkEnum(p.status, `${path}.status`, ["active", "retiring", "retired"]); checkId(p.fileRootId, `${path}.fileRootId`); }, registration: (p, path, rec) => { requireScopeKind(rec, path, ["project", "workspace"]); const keys = ["agentId", "scopeRoleRef", "restrictions", "projectRegistrationRef", "status", "delegationRef"]; checkObject(p, path, keys); requireKeys(p, path, keys); checkId(p.agentId, `${path}.agentId`); checkRegistryRef(p.scopeRoleRef, `${path}.scopeRoleRef`, "scope-role"); checkNullable(p.restrictions, `${path}.restrictions`, checkRestrictions); checkNullable(p.projectRegistrationRef, `${path}.projectRegistrationRef`, (v, q) => checkRecordRef(v, q, "registration")); checkEnum(p.status, `${path}.status`, ["active", "revoked"]); checkNullable(p.delegationRef, `${path}.delegationRef`, (v, q) => checkRecordRef(v, q, "decision")); if (rec.scope.kind === "project") { if (p.projectRegistrationRef !== null) shapeFail("shape-conditional-mismatch", `${path}.projectRegistrationRef`); } else if (p.projectRegistrationRef === null) { shapeFail("shape-conditional-mismatch", `${path}.projectRegistrationRef`); } }, mission: (p, path, rec) => { requireScopeKind(rec, path, ["project", "workspace"]); const keys = ["objective", "criteria", "parentMissionRef", "restrictions", "status"]; checkObject(p, path, keys); requireKeys(p, path, keys); checkString(p.objective, `${path}.objective`, 1, 4000); checkCriteria(p.criteria, `${path}.criteria`); checkNullable(p.parentMissionRef, `${path}.parentMissionRef`, (v, q) => checkRecordRef(v, q, "mission")); checkNullable(p.restrictions, `${path}.restrictions`, checkRestrictions); checkEnum(p.status, `${path}.status`, ["proposed", "active", "blocked", "ready-for-review", "accepted", "canceled"]); if (rec.scope.kind === "project" && p.parentMissionRef !== null) { shapeFail("shape-conditional-mismatch", `${path}.parentMissionRef`); } }, task: (p, path, rec) => { requireScopeKind(rec, path, ["workspace"]); const keys = ["purpose", "criteria", "missionRef", "intentRef", "dependencies", "restrictions", "status"]; checkObject(p, path, keys); requireKeys(p, path, keys); checkString(p.purpose, `${path}.purpose`, 1, 4000); checkCriteria(p.criteria, `${path}.criteria`); checkNullable(p.missionRef, `${path}.missionRef`, (v, q) => checkRecordRef(v, q, "mission")); checkRecordRef(p.intentRef, `${path}.intentRef`, "decision"); checkArray(p.dependencies, `${path}.dependencies`, 0, MAX_ARRAY, true, (v, q) => checkRecordRef(v, q, "task")); checkNullable(p.restrictions, `${path}.restrictions`, checkRestrictions); checkEnum(p.status, `${path}.status`, ["proposed", "ready", "active", "blocked", "ready-for-review", "accepted", "canceled"]); }, assignment: (p, path, rec) => { requireScopeKind(rec, path, ["workspace"]); const keys = ["taskRef", "agentId", "intentRef", "status", "endReason"]; checkObject(p, path, keys); requireKeys(p, path, keys); checkRecordRef(p.taskRef, `${path}.taskRef`, "task"); checkId(p.agentId, `${path}.agentId`); checkRecordRef(p.intentRef, `${path}.intentRef`, "decision"); checkEnum(p.status, `${path}.status`, ["selected", "paused", "ended"]); checkEnum(p.endReason, `${path}.endReason`, [null, "completed", "abandoned", "revoked", "reassigned", "canceled"]); if (p.status === "ended") { if (typeof p.endReason !== "string") shapeFail("shape-conditional-mismatch", `${path}.endReason`); } else if (p.endReason !== null) { shapeFail("shape-conditional-mismatch", `${path}.endReason`); } }, decision: (p, path, rec) => { requireScopeKind(rec, path, ["system", "project", "workspace"]); const keys = ["decisionKind", "subjectRefs", "outcome", "basisRef", "evidenceRefs", "delegatedOperations"]; checkObject(p, path, keys); requireKeys(p, path, keys); checkEnum(p.decisionKind, `${path}.decisionKind`, ["plan-approval", "delegation", "assignment-change", "acceptance", "owner-checkpoint", "reconciliation"]); checkArray(p.subjectRefs, `${path}.subjectRefs`, 1, MAX_ARRAY, true, (v, q) => checkRecordRef(v, q, null)); checkEnum(p.outcome, `${path}.outcome`, ["approved", "rejected"]); checkArtifactRef(p.basisRef, `${path}.basisRef`); checkArray(p.evidenceRefs, `${path}.evidenceRefs`, 0, MAX_ARRAY, true, checkArtifactRef); checkArray(p.delegatedOperations, `${path}.delegatedOperations`, 0, MAX_ARRAY, true, (op, q) => checkEnum(op, q, OPERATION_CATALOG)); if (p.decisionKind === "delegation" && p.outcome === "approved") { if (p.delegatedOperations.length < 1) shapeFail("shape-conditional-mismatch", `${path}.delegatedOperations`); } else if (p.delegatedOperations.length !== 0) { shapeFail("shape-conditional-mismatch", `${path}.delegatedOperations`); } }, }; const ENVELOPE_KEYS = ["schemaVersion", "kind", "id", "scope", "revision", "createdAt", "createdBy", "supersedes", "authorizationRef", "payload"]; /** * Validate one candidate record against the pinned records.schema.json for the * eight supported kinds. Returns {verdict: "valid", profileViolations: [path, ...]} * | {verdict: "invalid", rule, path} | {verdict: "unsupported-kind", rule, path} * (schema-known kind not modelled here). The verdict is the schema column; the * profileViolations list (empty when the strict profile holds) is the independent * profile column: typed id/runtimeId/digest values the pinned checker accepts only * through its end-of-string newline tolerance, in validation order. */ export function validateRecordShape(rec, path = "record") { const outer = profileSink; profileSink = []; try { checkObject(rec, path, ENVELOPE_KEYS); requireKeys(rec, path, ENVELOPE_KEYS); checkConst(rec.schemaVersion, `${path}.schemaVersion`, 1); checkEnum(rec.kind, `${path}.kind`, RECORD_KINDS); if (!SUPPORTED_KINDS.includes(rec.kind)) return { verdict: "unsupported-kind", rule: "record-kind-unsupported", path: `${path}.kind` }; checkId(rec.id, `${path}.id`); checkScope(rec.scope, `${path}.scope`, null); checkRevision(rec.revision, `${path}.revision`); checkTime(rec.createdAt, `${path}.createdAt`); checkActor(rec.createdBy, `${path}.createdBy`); if (rec.revision === 1) { if (rec.supersedes !== null) shapeFail("shape-conditional-mismatch", `${path}.supersedes`); } else { if (rec.supersedes === null) shapeFail("shape-conditional-mismatch", `${path}.supersedes`); checkRecordRef(rec.supersedes, `${path}.supersedes`, null); } checkRuntimeId(rec.authorizationRef, `${path}.authorizationRef`); if (!isObject(rec.payload)) shapeFail("shape-type-mismatch", `${path}.payload`); PAYLOAD_CHECKS[rec.kind](rec.payload, `${path}.payload`, rec); return { verdict: "valid", profileViolations: profileSink }; } catch (e) { if (e instanceof ShapeError) return { verdict: "invalid", rule: e.rule, path: e.path }; throw e; } finally { profileSink = outer; } } function checkExecution(v, path) { if (!isObject(v)) shapeFail("shape-type-mismatch", path); if (!hasKey(v, "kind")) shapeFail("shape-missing-field", `${path}.kind`); if (v.kind === "unrestricted-simulation") { checkObject(v, path, ["kind"]); } else if (v.kind === "restrictions") { checkObject(v, path, ["kind", "restrictions"]); requireKeys(v, path, ["restrictions"]); checkRestrictions(v.restrictions, `${path}.restrictions`); } else { shapeFail("shape-enum-mismatch", `${path}.kind`); } return v; } function checkRegistryEntry(v, path) { if (!isObject(v)) shapeFail("shape-type-mismatch", path); requireKeys(v, path, ["registry", "id", "revision", "digest"]); checkEnum(v.registry, `${path}.registry`, REGISTRIES); const contentBearing = CONTENT_REGISTRIES.includes(v.registry); checkObject(v, path, contentBearing ? ["registry", "id", "revision", "digest", "content"] : ["registry", "id", "revision", "digest"]); checkId(v.id, `${path}.id`); checkRevision(v.revision, `${path}.revision`); checkDigest(v.digest, `${path}.digest`); if (contentBearing) { requireKeys(v, path, ["content"]); checkObject(v.content, `${path}.content`, ["restrictions"]); requireKeys(v.content, `${path}.content`, ["restrictions"]); checkRestrictions(v.content.restrictions, `${path}.content.restrictions`); } return v; } function checkRequesterContext(v, path) { checkObject(v, path, ["assignmentRef", "execution"]); requireKeys(v, path, ["assignmentRef", "execution"]); checkRecordRef(v.assignmentRef, `${path}.assignmentRef`, "assignment"); checkExecution(v.execution, `${path}.execution`); return v; } const BUNDLE_KEYS = ["bundleVersion", "kind", "simulation", "records", "registries", "artifacts", "authorizations", "selection", "operation", "proposal", "delegationInputs"]; const PROPOSAL_KEYS = ["kind", "requesterAgentId", "subjectAssignmentRef", "targetTaskRef", "delegationRef", "changeDecisionRef", "requesterContexts", "message"]; /** * Stage 2: closed bundle shape (pinned-schema column), then the strict pattern profile * (addendum FI-C2-1) over every typed field of records, wrapper and declared inputs, * then the mock-content digest profile. Throws Refusal; nothing later (identity, * graph, admission) sees a bundle that failed any of the three. */ export function validateBundleShape(b) { const outer = profileSink; profileSink = []; try { validateBundleShapeScoped(b); } finally { profileSink = outer; } } /** * Total ordering form over the strict-JSON input domain (§10.4 content tie-break). * Deliberately distinct from canonical.mjs, whose ASCII-only safe-integer domain is * the MOCK content-digest domain and refuses everything else: every value the strict * parser can produce (null, booleans, safe integers, any Unicode string, arrays, * objects) has exactly one form here, and distinct values have distinct forms — * object keys sorted by UTF-16 code unit, compact separators, array order preserved, * strings and keys as well-formed JSON string literals, no normalization or * case-folding. It is used only to order traversal; it is never a digest, never * echoed and never a validity judgement (FI-FILBERT-7 R5-1). */ export function orderingForm(value) { if (value === null || typeof value === "boolean" || typeof value === "number") return String(value); if (typeof value === "string") return JSON.stringify(value); if (Array.isArray(value)) return `[${value.map(orderingForm).join(",")}]`; if (typeof value === "object") { const keys = Object.keys(value).sort((x, y) => (x < y ? -1 : x > y ? 1 : 0)); return `{${keys.map((k) => `${JSON.stringify(k)}:${orderingForm(value[k])}`).join(",")}}`; } throw new Error("internal: ordering form of a non-JSON value"); } /** * §10.4: within a stage, records and declared inventories are traversed in stable * typed-key order, never input order. Values may still be unvalidated (shape stage), * so keys are read tolerantly: `keyOf` returns an array of typed components * (string | integer | null, compared component-wise, a null component after every * readable value) or null when the item has no readable identity at all. Items without * a readable identity sort after every keyed item; equal keys (duplicates, malformed * entries) break ties on the total ordering form of the whole entry, then on input * index — which only decides between byte-identical values — so no permutation of the * input can change the first reported failure. Returns input indices in traversal * order; locator paths keep the input index. */ export function stableOrder(items, keyOf) { const memo = new Map(); const form = (i) => { if (!memo.has(i)) memo.set(i, orderingForm(items[i])); return memo.get(i); }; const keys = items.map((item, i) => ({ i, key: keyOf(item) })); keys.sort((a, b) => { if ((a.key === null) !== (b.key === null)) return a.key === null ? 1 : -1; if (a.key !== null) { const c = compareTypedKeys(a.key, b.key); if (c !== 0) return c; } const fa = form(a.i); const fb = form(b.i); if (fa !== fb) return fa < fb ? -1 : 1; return a.i - b.i; }); return keys.map((k) => k.i); } function compareTypedKeys(a, b) { const n = Math.max(a.length, b.length); for (let k = 0; k < n; k += 1) { const x = k < a.length ? a[k] : null; const y = k < b.length ? b[k] : null; if (x === y) continue; if (x === null) return 1; if (y === null) return -1; if (typeof x !== typeof y) return typeof x === "string" ? -1 : 1; return x < y ? -1 : 1; } return 0; } const str = (v) => (typeof v === "string" ? v : null); const int = (v) => (Number.isInteger(v) ? v : null); /** Tolerant scope key: the scope kind must be readable, ids default to "". */ function tolerantScopeKey(sc) { if (!isObject(sc) || typeof sc.kind !== "string") return null; return scopeKey({ kind: sc.kind, projectId: typeof sc.projectId === "string" ? sc.projectId : "", workspaceId: typeof sc.workspaceId === "string" ? sc.workspaceId : "" }); } /** Tolerant kind/id/scope/revision key of a record or recordRef; null unless kind, id and scope kind are readable. */ function tolerantRefKey(v) { if (!isObject(v) || typeof v.kind !== "string" || typeof v.id !== "string") return null; const sk = tolerantScopeKey(v.scope); if (sk === null) return null; return [v.kind, v.id, sk, int(v.revision)]; } /** Typed identity keys per traversal family (§10.4): kind/id/scope/revision for records. */ export const ORDER_KEYS = Object.freeze({ records: tolerantRefKey, registries: (e) => (isObject(e) && typeof e.registry === "string" && typeof e.id === "string" ? [e.registry, e.id, int(e.revision), str(e.digest)] : null), artifacts: (a) => (isObject(a) && typeof a.runId === "string" && typeof a.artifactId === "string" ? [a.runId, a.artifactId, str(a.digest)] : null), authorizations: (a) => (typeof a === "string" ? [a] : null), delegationInputs: (d) => (isObject(d) ? tolerantRefKey(d.decisionRef) : null), }); function shapeValidationOrder(records) { return stableOrder(records, ORDER_KEYS.records); } /** checkArray for a declared inventory: bounds first, then every item in stable order with its input-index path. */ function checkInventory(v, path, itemCheck, keyOf) { if (!Array.isArray(v)) shapeFail("shape-type-mismatch", path); if (v.length > MAX_INVENTORY) shapeFail("shape-bound-exceeded", path); for (const i of stableOrder(v, keyOf)) itemCheck(v[i], `${path}[${i}]`); return v; } function validateBundleShapeScoped(b) { try { checkObject(b, "bundle", BUNDLE_KEYS); requireKeys(b, "bundle", BUNDLE_KEYS); checkConst(b.bundleVersion, "bundle.bundleVersion", 1); checkConst(b.kind, "bundle.kind", "foundation-inspector-bundle"); checkConst(b.simulation, "bundle.simulation", true); if (!Array.isArray(b.records)) shapeFail("shape-type-mismatch", "bundle.records"); if (b.records.length > MAX_INVENTORY) shapeFail("shape-bound-exceeded", "bundle.records"); for (const i of shapeValidationOrder(b.records)) { const v = validateRecordShape(b.records[i], `bundle.records[${i}]`); if (v.verdict === "unsupported-kind") refuse(2, "unsupported-kind", "record-kind-unsupported", v.path); if (v.verdict !== "valid") shapeFail(v.rule, v.path); profileSink.push(...v.profileViolations); } checkInventory(b.registries, "bundle.registries", checkRegistryEntry, ORDER_KEYS.registries); checkInventory(b.artifacts, "bundle.artifacts", checkArtifactRef, ORDER_KEYS.artifacts); checkInventory(b.authorizations, "bundle.authorizations", checkRuntimeId, ORDER_KEYS.authorizations); const s = b.selection; checkObject(s, "bundle.selection", ["agentId", "projectId", "workspaceId", "assignmentRef", "execution"]); requireKeys(s, "bundle.selection", ["agentId", "projectId", "workspaceId", "assignmentRef", "execution"]); checkId(s.agentId, "bundle.selection.agentId"); checkId(s.projectId, "bundle.selection.projectId"); checkId(s.workspaceId, "bundle.selection.workspaceId"); checkNullable(s.assignmentRef, "bundle.selection.assignmentRef", (v, q) => checkRecordRef(v, q, "assignment")); checkExecution(s.execution, "bundle.selection.execution"); const o = b.operation; checkObject(o, "bundle.operation", ["name", "target"]); requireKeys(o, "bundle.operation", ["name", "target"]); checkString(o.name, "bundle.operation.name", 1, 4096); checkNullable(o.target, "bundle.operation.target", (v, q) => { checkObject(v, q, ["root", "path"]); requireKeys(v, q, ["root", "path"]); checkConst(v.root, `${q}.root`, "workspace"); checkRelativePath(v.path, `${q}.path`); }); const p = b.proposal; if (p !== null) { checkObject(p, "bundle.proposal", PROPOSAL_KEYS); requireKeys(p, "bundle.proposal", PROPOSAL_KEYS.filter((k) => k !== "message")); checkConst(p.kind, "bundle.proposal.kind", "assignment-change"); checkId(p.requesterAgentId, "bundle.proposal.requesterAgentId"); checkRecordRef(p.subjectAssignmentRef, "bundle.proposal.subjectAssignmentRef", "assignment"); checkRecordRef(p.targetTaskRef, "bundle.proposal.targetTaskRef", "task"); checkNullable(p.delegationRef, "bundle.proposal.delegationRef", (v, q) => checkRecordRef(v, q, "decision")); checkNullable(p.changeDecisionRef, "bundle.proposal.changeDecisionRef", (v, q) => checkRecordRef(v, q, "decision")); checkObject(p.requesterContexts, "bundle.proposal.requesterContexts", ["original", "target"]); requireKeys(p.requesterContexts, "bundle.proposal.requesterContexts", ["original", "target"]); checkRequesterContext(p.requesterContexts.original, "bundle.proposal.requesterContexts.original"); checkRequesterContext(p.requesterContexts.target, "bundle.proposal.requesterContexts.target"); if (hasKey(p, "message")) checkString(p.message, "bundle.proposal.message", 0, 4000); } checkInventory(b.delegationInputs, "bundle.delegationInputs", (d, q) => { checkObject(d, q, ["decisionRef", "mode", "issuerCeiling"]); requireKeys(d, q, ["decisionRef", "mode", "issuerCeiling"]); checkRecordRef(d.decisionRef, `${q}.decisionRef`, "decision"); checkConst(d.mode, `${q}.mode`, "direct-declared"); checkRestrictions(d.issuerCeiling, `${q}.issuerCeiling`); }, ORDER_KEYS.delegationInputs); } catch (e) { if (e instanceof ShapeError) refuse(2, "invalid-request", e.rule, e.path); throw e; } // Profile: strict typed-string grammar (addendum FI-C2-1). Schema-valid under the // pinned checker's newline tolerance, refused here before any identity lookup; the // first violating path in validation order is the internal locator, never the value. if (profileSink.length > 0) refuse(2, "invalid-request", "profile-pattern-mismatch", profileSink[0]); // Profile: mock registry content digests (inspector-content-digest/1), in stable // registry/id/revision/digest order (§10.4), never input order. for (const i of stableOrder(b.registries, ORDER_KEYS.registries)) { const entry = b.registries[i]; if (!CONTENT_REGISTRIES.includes(entry.registry)) continue; let digest; try { digest = digestOf(entry.content); } catch (e) { if (e instanceof CanonicalError) refuse(2, "unsupported-capability", "mock-content-unsupported", `bundle.registries[${i}].content`); throw e; } if (digest !== entry.digest) refuse(2, "invalid-request", "registry-digest-mismatch", `bundle.registries[${i}].digest`); } } // --------------------------------------------------------------------------- // Identity helpers // --------------------------------------------------------------------------- export function scopeKey(scope) { if (scope.kind === "system") return "system"; if (scope.kind === "project") return `project:${scope.projectId}`; return `workspace:${scope.projectId}/${scope.workspaceId}`; } export function identityKey(kind, id, scope) { return `${kind}|${id}|${scopeKey(scope)}`; } function refIdentityKey(ref) { return identityKey(ref.kind, ref.id, ref.scope); } function refKey(ref) { return `${refIdentityKey(ref)}#${ref.revision}`; } function recordKey(rec) { return refKey(rec); } function plainScope(scope) { if (scope.kind === "system") return { kind: "system" }; if (scope.kind === "project") return { kind: "project", projectId: scope.projectId }; return { kind: "workspace", projectId: scope.projectId, workspaceId: scope.workspaceId }; } /** Ordinary-prototype copy of a recordRef with fixed key order. */ export function plainRef(ref) { if (ref === null) return null; return { kind: ref.kind, id: ref.id, scope: plainScope(ref.scope), revision: ref.revision }; } function refEquals(a, b) { return a.kind === b.kind && a.id === b.id && a.revision === b.revision && scopeKey(a.scope) === scopeKey(b.scope); } function sameIdentity(a, b) { return a.kind === b.kind && a.id === b.id && scopeKey(a.scope) === scopeKey(b.scope); } function compareRecords(a, b) { const ka = [a.kind, a.id, scopeKey(a.scope)]; const kb = [b.kind, b.id, scopeKey(b.scope)]; for (let i = 0; i < 3; i += 1) { if (ka[i] < kb[i]) return -1; if (ka[i] > kb[i]) return 1; } return a.revision - b.revision; } // --------------------------------------------------------------------------- // Cycle detection (stage 5) — exported for detector unit tests // --------------------------------------------------------------------------- /** * edges: Map. Nodes are visited in sorted order; returns the first * node found on a cycle (including self-loops) or null. */ export function findCycle(edges) { const WHITE = 0; const GREY = 1; const BLACK = 2; const state = new Map(); const nodes = [...edges.keys()].sort(); for (const start of nodes) { if (state.get(start)) continue; const stack = [[start, 0]]; state.set(start, GREY); while (stack.length) { const frame = stack[stack.length - 1]; const [node] = frame; const next = (edges.get(node) || []); if (frame[1] < next.length) { const target = next[frame[1]]; frame[1] += 1; const st = state.get(target) || WHITE; if (st === GREY) return target; if (st === WHITE) { state.set(target, GREY); stack.push([target, 0]); } } else { state.set(node, BLACK); stack.pop(); } } } return null; } // --------------------------------------------------------------------------- // Restrictions calculus (§4): component-prefix paths, set intersection // --------------------------------------------------------------------------- function grantSegments(grant) { return grant.path === null ? [] : grant.path.split("/"); } /** Is grant `a` a segment-prefix of grant `b` (a covers b)? */ function grantCovers(a, b) { const sa = grantSegments(a); const sb = grantSegments(b); if (sa.length > sb.length) return false; for (let i = 0; i < sa.length; i += 1) if (sa[i] !== sb[i]) return false; return true; } export function intersectGrants(A, B) { const out = []; const seen = new Set(); for (const a of A) { for (const b of B) { let keep = null; if (grantCovers(a, b)) keep = b; else if (grantCovers(b, a)) keep = a; if (keep) { const key = keep.path === null ? " root" : keep.path; if (!seen.has(key)) { seen.add(key); out.push({ root: "workspace", path: keep.path }); } } } } return out; } export function pathPermitted(path, grants) { const target = { root: "workspace", path }; return grants.some((g) => grantCovers(g, target)); } /** * A layer is {operations, readPaths, writePaths, network, endpointRefs}; any member * may be null meaning "no narrowing" (used for delegated-operation sets). A full * candidate restrictions object is a layer with every member present. */ export function layerFromRestrictions(r) { return { operations: [...r.operations], readPaths: r.readPaths.map((g) => ({ root: "workspace", path: g.path })), writePaths: r.writePaths.map((g) => ({ root: "workspace", path: g.path })), network: r.network, endpointRefs: r.endpointRefs.map((e) => ({ registry: e.registry, id: e.id, revision: e.revision, digest: e.digest })), }; } export const UNRESTRICTED_LAYER = Object.freeze({ operations: null, readPaths: null, writePaths: null, network: null, endpointRefs: null, }); export function intersectLayers(a, b) { const ops = a.operations === null ? b.operations : b.operations === null ? a.operations : a.operations.filter((op) => b.operations.includes(op)); const readPaths = a.readPaths === null ? b.readPaths : b.readPaths === null ? a.readPaths : intersectGrants(a.readPaths, b.readPaths); const writePaths = a.writePaths === null ? b.writePaths : b.writePaths === null ? a.writePaths : intersectGrants(a.writePaths, b.writePaths); let network; let endpointRefs; if (a.network === null) { network = b.network; endpointRefs = b.endpointRefs; } else if (b.network === null) { network = a.network; endpointRefs = a.endpointRefs; } else if (a.network === "none" || b.network === "none") { network = "none"; endpointRefs = []; } else { endpointRefs = a.endpointRefs.filter((e) => b.endpointRefs.some((f) => deepEqual(e, f))); network = endpointRefs.length ? "approved-endpoints" : "none"; } return { operations: ops === null ? null : [...ops], readPaths, writePaths, network, endpointRefs }; } function layerHasOperation(layer, op) { return layer.operations === null || layer.operations.includes(op); } function layerPermitsPath(layer, member, path) { const grants = layer[member]; return grants === null || pathPermitted(path, grants); } // --------------------------------------------------------------------------- // Evaluation // --------------------------------------------------------------------------- class Model { constructor(bundle) { this.bundle = bundle; this.records = [...bundle.records].sort(compareRecords); this.byKey = new Map(); // refKey -> record this.identities = new Map(); // identityKey -> {records: Map, head} this.registries = bundle.registries; this.artifacts = bundle.artifacts; this.authorizations = new Set(bundle.authorizations); this.delegationInputs = bundle.delegationInputs; // §10.4: declared inventories are traversed in stable typed-key order (input indices). this.order = { registries: stableOrder(bundle.registries, ORDER_KEYS.registries), artifacts: stableOrder(bundle.artifacts, ORDER_KEYS.artifacts), authorizations: stableOrder(bundle.authorizations, ORDER_KEYS.authorizations), delegationInputs: stableOrder(bundle.delegationInputs, ORDER_KEYS.delegationInputs), }; } // ---- stage 3 indexIdentities() { for (const rec of this.records) { const key = recordKey(rec); if (this.byKey.has(key)) refuse(2, "invalid-request", "duplicate-record-identity", key); this.byKey.set(key, rec); const ik = identityKey(rec.kind, rec.id, rec.scope); if (!this.identities.has(ik)) this.identities.set(ik, { records: new Map(), head: null }); this.identities.get(ik).records.set(rec.revision, rec); } const regSeen = new Set(); for (const i of this.order.registries) { const e = this.registries[i]; const k = `${e.registry}|${e.id}|${e.revision}`; if (regSeen.has(k)) refuse(2, "invalid-request", "duplicate-registry-identity", `bundle.registries[${i}]`); regSeen.add(k); } const artSeen = new Set(); for (const i of this.order.artifacts) { const a = this.artifacts[i]; const k = `${a.runId}|${a.artifactId}`; if (artSeen.has(k)) refuse(2, "invalid-request", "duplicate-artifact-identity", `bundle.artifacts[${i}]`); artSeen.add(k); } const authSeen = new Set(); for (const i of this.order.authorizations) { const a = this.bundle.authorizations[i]; if (authSeen.has(a)) refuse(2, "invalid-request", "duplicate-authorization-id", `bundle.authorizations[${i}]`); authSeen.add(a); } const delSeen = new Set(); for (const i of this.order.delegationInputs) { const k = refKey(this.delegationInputs[i].decisionRef); if (delSeen.has(k)) refuse(2, "invalid-request", "duplicate-delegation-input", `bundle.delegationInputs[${i}]`); delSeen.add(k); } } exact(ref) { return this.byKey.get(refKey(ref)); } requireExact(ref, where) { const rec = this.exact(ref); if (!rec) refuse(2, "missing-state", "record-reference-missing", where); return rec; } // ---- stage 4 checkStructuralReferences() { for (const rec of this.records) { const where = recordKey(rec); if (rec.supersedes !== null) this.requireExact(rec.supersedes, `${where}.supersedes`); const p = rec.payload; switch (rec.kind) { case "registration": if (p.projectRegistrationRef !== null) this.requireExact(p.projectRegistrationRef, `${where}.projectRegistrationRef`); if (p.delegationRef !== null) this.requireExact(p.delegationRef, `${where}.delegationRef`); break; case "mission": if (p.parentMissionRef !== null) this.requireExact(p.parentMissionRef, `${where}.parentMissionRef`); break; case "task": if (p.missionRef !== null) this.requireExact(p.missionRef, `${where}.missionRef`); this.requireExact(p.intentRef, `${where}.intentRef`); p.dependencies.forEach((d, i) => this.requireExact(d, `${where}.dependencies[${i}]`)); break; case "assignment": this.requireExact(p.taskRef, `${where}.taskRef`); this.requireExact(p.intentRef, `${where}.intentRef`); break; case "decision": p.subjectRefs.forEach((s, i) => this.requireExact(s, `${where}.subjectRefs[${i}]`)); break; default: break; } } const b = this.bundle; if (b.selection.assignmentRef !== null) this.requireExact(b.selection.assignmentRef, "bundle.selection.assignmentRef"); if (b.proposal !== null) { const p = b.proposal; this.requireExact(p.subjectAssignmentRef, "bundle.proposal.subjectAssignmentRef"); this.requireExact(p.targetTaskRef, "bundle.proposal.targetTaskRef"); if (p.delegationRef !== null) this.requireExact(p.delegationRef, "bundle.proposal.delegationRef"); if (p.changeDecisionRef !== null) this.requireExact(p.changeDecisionRef, "bundle.proposal.changeDecisionRef"); this.requireExact(p.requesterContexts.original.assignmentRef, "bundle.proposal.requesterContexts.original.assignmentRef"); this.requireExact(p.requesterContexts.target.assignmentRef, "bundle.proposal.requesterContexts.target.assignmentRef"); } for (const i of this.order.delegationInputs) this.requireExact(b.delegationInputs[i].decisionRef, `bundle.delegationInputs[${i}].decisionRef`); } // ---- stage 5 checkCycles() { const deps = new Map(); const parents = new Map(); const supersedes = new Map(); // Exact four-field nodes and edges (§3, r2 §6.2): every revision is its own node, // so temporally distinct edges of one identity are never unioned into a cycle. for (const rec of this.records) { const rk = recordKey(rec); if (rec.kind === "task") deps.set(rk, rec.payload.dependencies.map(refKey).sort()); if (rec.kind === "mission") parents.set(rk, rec.payload.parentMissionRef === null ? [] : [refKey(rec.payload.parentMissionRef)]); supersedes.set(rk, rec.supersedes === null ? [] : [refKey(rec.supersedes)]); } let c = findCycle(deps); if (c) refuse(2, "invalid-request", "dependency-cycle", c); c = findCycle(parents); if (c) refuse(2, "invalid-request", "mission-parent-cycle", c); c = findCycle(supersedes); if (c) refuse(2, "invalid-request", "supersedes-cycle", c); } // ---- stage 6 checkRevisionChains() { const keys = [...this.identities.keys()].sort(); for (const ik of keys) { const entry = this.identities.get(ik); const revs = [...entry.records.keys()].sort((a, b) => a - b); for (let i = 0; i < revs.length; i += 1) { if (revs[i] !== i + 1) refuse(2, "invalid-request", "revision-chain-gap", `${ik}#${revs[i]}`); const rec = entry.records.get(revs[i]); if (revs[i] > 1) { const s = rec.supersedes; if (!sameIdentity(s, rec) || s.revision !== revs[i] - 1) { refuse(2, "invalid-request", "supersedes-mismatch", `${ik}#${revs[i]}`); } } } entry.head = entry.records.get(revs[revs.length - 1]); } } headOf(kind, id, scope) { const entry = this.identities.get(identityKey(kind, id, scope)); return entry ? entry.head : undefined; } headFor(ref) { return this.headOf(ref.kind, ref.id, ref.scope); } // ---- stage 7 checkOwnership() { for (const rec of this.records) { const where = recordKey(rec); const p = rec.payload; switch (rec.kind) { case "project": if (rec.id !== rec.scope.projectId) refuse(2, "invalid-request", "project-id-scope-mismatch", where); break; case "workspace": { if (rec.id !== rec.scope.workspaceId) refuse(2, "invalid-request", "workspace-id-scope-mismatch", where); const projectScope = { kind: "project", projectId: rec.scope.projectId }; if (!this.identities.has(identityKey("project", rec.scope.projectId, projectScope))) { refuse(2, "missing-state", "workspace-project-missing", where); } break; } case "registration": if (rec.scope.kind === "workspace") { const parent = this.exact(p.projectRegistrationRef); if (parent.scope.kind !== "project" || parent.scope.projectId !== rec.scope.projectId || parent.payload.agentId !== p.agentId) { refuse(2, "invalid-request", "registration-parent-mismatch", where); } } break; case "mission": if (rec.scope.kind === "workspace" && p.parentMissionRef !== null) { const parent = this.exact(p.parentMissionRef); if (parent.scope.kind !== "project" || parent.scope.projectId !== rec.scope.projectId) { refuse(2, "invalid-request", "mission-parent-scope-mismatch", where); } } break; case "task": if (p.missionRef !== null) { const mission = this.exact(p.missionRef); if (mission.scope.projectId !== rec.scope.projectId) { refuse(2, "invalid-request", "mission-owning-project-mismatch", where); } } break; case "assignment": if (scopeKey(p.taskRef.scope) !== scopeKey(rec.scope)) { refuse(2, "invalid-request", "assignment-task-scope-mismatch", where); } break; default: break; } } } // ---- stage 8 checkOperation() { const { operation, proposal } = this.bundle; if (!OPERATION_CATALOG.includes(operation.name)) refuse(2, "invalid-request", "operation-unknown", "bundle.operation.name"); if (!SUPPORTED_OPERATIONS.includes(operation.name)) refuse(2, "unsupported-capability", "operation-unsupported", "bundle.operation.name"); if (operation.name === "work.read" || operation.name === "assignment.change") { if (operation.target !== null) refuse(2, "invalid-request", "operation-target-mismatch", "bundle.operation.target"); } else if (operation.target === null) { refuse(2, "invalid-request", "operation-target-mismatch", "bundle.operation.target"); } if (operation.name === "assignment.change") { if (proposal === null) refuse(2, "invalid-request", "proposal-required", "bundle.proposal"); } else if (proposal !== null) { refuse(2, "invalid-request", "proposal-not-applicable", "bundle.proposal"); } } // ---- stage 9 helpers requireAuthorization(rec) { if (!this.authorizations.has(rec.authorizationRef)) { refuse(3, "missing-state", "authorization-undeclared", recordKey(rec)); } } /** * §3 / r2 §6.2: every consulted registry reference must name a declared entry by * exact registry/id/revision/digest. An absent declared input is an admission * failure (exit 3 missing-state), never the ordinary-record structural rule. */ requireRegistryDeclaration(ref, where) { const declared = this.registries.some((e) => e.registry === ref.registry && e.id === ref.id && e.revision === ref.revision && e.digest === ref.digest); if (!declared) refuse(3, "missing-state", "registry-declaration-missing", where); } /** A restrictions layer whose endpoint references are all declared. */ declaredLayer(r, where) { r.endpointRefs.forEach((e, i) => this.requireRegistryDeclaration(e, `${where}.endpointRefs[${i}]`)); return layerFromRestrictions(r); } requireArtifacts(decision) { const declared = (a) => this.artifacts.some((d) => d.runId === a.runId && d.artifactId === a.artifactId && d.digest === a.digest); const p = decision.payload; if (!declared(p.basisRef)) refuse(3, "missing-state", "artifact-undeclared", `${recordKey(decision)}.basisRef`); p.evidenceRefs.forEach((e, i) => { if (!declared(e)) refuse(3, "missing-state", "artifact-undeclared", `${recordKey(decision)}.evidenceRefs[${i}]`); }); } /** Consult a decision head: authorization + artifacts declared. */ consultDecision(decision) { this.requireAuthorization(decision); this.requireArtifacts(decision); } /** Resolve a current-admission reference: must exist (structural) and be the head. */ resolveCurrent(ref, staleRule, where) { const head = this.headFor(ref); if (!head) refuse(2, "missing-state", "record-reference-missing", where); if (head.revision !== ref.revision) refuse(3, "stale-revision", staleRule, where); return head; } registryContent(ref, missingRule, where) { for (const e of this.registries) { if (e.registry === ref.registry && e.id === ref.id && e.revision > ref.revision) { refuse(3, "stale-revision", "policy-ceiling-stale", where); } } const entry = this.registries.find((e) => e.registry === ref.registry && e.id === ref.id && e.revision === ref.revision && e.digest === ref.digest); if (!entry) refuse(3, "missing-state", missingRule, where); return this.declaredLayer(entry.content.restrictions, `${where}.content.restrictions`); } /** * Active registration for agentId at scope (L2/L3) with its ceiling layers, * including the §10.1 delegation checks. */ registrationLayer(agentId, agent, scope, parent) { const sk = scopeKey(scope); const heads = []; for (const ik of [...this.identities.keys()].sort()) { const head = this.identities.get(ik).head; if (head.kind === "registration" && scopeKey(head.scope) === sk && head.payload.agentId === agentId) heads.push(head); } const active = heads.filter((r) => r.payload.status === "active"); const missingRule = scope.kind === "project" ? "project-registration-missing" : "workspace-registration-missing"; if (active.length > 1) refuse(2, "invalid-request", "registration-ambiguous", sk); if (active.length === 0) { if (heads.length > 0) refuse(3, "not-authorized", "registration-revoked", sk); refuse(3, "not-authorized", missingRule, sk); } const reg = active[0]; const where = recordKey(reg); this.requireAuthorization(reg); if (parent) { const pref = reg.payload.projectRegistrationRef; if (!refEquals(pref, parent)) { if (sameIdentity(pref, parent)) refuse(3, "stale-revision", "registration-parent-stale", where); refuse(3, "not-authorized", "registration-parent-not-current", where); } } const layers = [this.registryContent(reg.payload.scopeRoleRef, "scope-role-missing", `${where}.scopeRoleRef`)]; if (reg.payload.restrictions !== null) layers.push(this.declaredLayer(reg.payload.restrictions, `${where}.restrictions`)); if (reg.payload.delegationRef !== null) { const dwhere = `${where}.delegationRef`; const d = this.resolveCurrent(reg.payload.delegationRef, "registration-delegation-stale", dwhere); this.consultDecision(d); const dp = d.payload; if (dp.decisionKind !== "delegation" || dp.outcome !== "approved") { refuse(3, "not-authorized", "registration-delegation-not-approved", dwhere); } if (scopeKey(d.scope) !== sk) refuse(3, "unsupported-capability", "registration-delegation-scope-not-modelled", dwhere); const agentSubjects = dp.subjectRefs.filter((s) => s.kind === "agent-definition"); const regSubjects = dp.subjectRefs.filter((s) => s.kind === "registration"); if (dp.subjectRefs.length !== 2 || agentSubjects.length !== 1 || regSubjects.length !== 1) { refuse(3, "unsupported-capability", "registration-delegation-subject-form", dwhere); } if (!refEquals(agentSubjects[0], agent) || !refEquals(regSubjects[0], reg)) { refuse(3, "not-authorized", "registration-delegation-bounds", dwhere); } const input = this.delegationInputs.find((di) => refEquals(di.decisionRef, d)); if (!input) refuse(3, "missing-state", "delegation-input-missing", dwhere); for (const op of dp.delegatedOperations) { if (!input.issuerCeiling.operations.includes(op)) refuse(3, "not-authorized", "delegation-exceeds-issuer-ceiling", dwhere); } layers.push({ ...UNRESTRICTED_LAYER, operations: [...dp.delegatedOperations] }); layers.push(this.declaredLayer(input.issuerCeiling, `${dwhere}.issuerCeiling`)); } return { registration: reg, layers }; } /** §10.2 scope guard for a consulted work reference relative to a workspace scope. */ workScopeGuard(ref, contextScope, where) { const s = ref.scope; if (s.kind === "workspace" && s.projectId === contextScope.projectId && s.workspaceId === contextScope.workspaceId) return; if (s.kind === "project" && s.projectId === contextScope.projectId) return; if (s.projectId === contextScope.projectId) refuse(3, "unsupported-capability", "cross-workspace-work-reference-not-modelled", where); refuse(3, "unsupported-capability", "cross-project-work-reference-not-modelled", where); } /** * L1–L9 for one calculation context. Returns {layer, task, missions, dependencies}. * Throws Refusal at the first failing gate in fixed order. */ computeContext(ctx) { const { agentId, projectId, workspaceId, assignmentRef, execution, requiredOperation, purpose } = ctx; const systemScope = { kind: "system" }; const projectScope = { kind: "project", projectId }; const workspaceScope = { kind: "workspace", projectId, workspaceId }; const agent = this.headOf("agent-definition", agentId, systemScope); if (!agent) refuse(2, "missing-state", purpose === "requester" ? "requester-agent-missing" : "selected-agent-missing", agentId); this.requireAuthorization(agent); if (agent.payload.status !== "enabled") refuse(3, "not-authorized", "agent-disabled", recordKey(agent)); const agentPayload = agent.payload; const agentWhere = recordKey(agent); this.requireRegistryDeclaration(agentPayload.harnessRef, `${agentWhere}.harnessRef`); this.requireRegistryDeclaration(agentPayload.settingsRef, `${agentWhere}.settingsRef`); this.requireRegistryDeclaration(agentPayload.soulRef, `${agentWhere}.soulRef`); agentPayload.instructionRefs.forEach((r, i) => this.requireRegistryDeclaration(r, `${agentWhere}.instructionRefs[${i}]`)); agentPayload.skillRefs.forEach((r, i) => this.requireRegistryDeclaration(r, `${agentWhere}.skillRefs[${i}]`)); const l1 = this.registryContent(agentPayload.policyRef, "agent-policy-missing", `${agentWhere}.policyRef`); const project = this.headOf("project", projectId, projectScope); if (!project) refuse(2, "missing-state", "selected-project-missing", projectId); this.requireAuthorization(project); if (project.payload.status !== "active") refuse(3, "not-authorized", "project-not-active", recordKey(project)); const l4 = this.registryContent(project.payload.policyRef, "project-policy-missing", `${recordKey(project)}.policyRef`); const workspace = this.headOf("workspace", workspaceId, workspaceScope); if (!workspace) refuse(2, "missing-state", "selected-workspace-missing", workspaceId); this.requireAuthorization(workspace); if (workspace.payload.status !== "active") refuse(3, "not-authorized", "workspace-not-active", recordKey(workspace)); const l5 = this.registryContent(workspace.payload.policyRef, "workspace-policy-missing", `${recordKey(workspace)}.policyRef`); const l2 = this.registrationLayer(agentId, agent, projectScope, null); const l3 = this.registrationLayer(agentId, agent, workspaceScope, l2.registration); const layers = [l1, ...l2.layers, ...l3.layers, l4, l5]; let task = null; let assignment = null; const missions = []; const dependencies = []; let consultedWork = false; if (assignmentRef === null) { if (!TASKLESS_OPERATIONS.includes(requiredOperation)) refuse(3, "not-authorized", "assignment-required", "bundle.selection.assignmentRef"); } else { assignment = this.resolveCurrent(assignmentRef, "assignment-stale", "assignmentRef"); this.requireAuthorization(assignment); const ap = assignment.payload; if (ap.status !== "selected") refuse(3, "not-authorized", "assignment-not-selected", recordKey(assignment)); if (ap.agentId !== agentId) refuse(3, "not-authorized", "assignment-agent-mismatch", recordKey(assignment)); if (scopeKey(assignment.scope) !== scopeKey(workspaceScope)) refuse(3, "not-authorized", "assignment-workspace-mismatch", recordKey(assignment)); task = this.resolveCurrent(ap.taskRef, "task-ref-stale", `${recordKey(assignment)}.taskRef`); const intent = this.resolveCurrent(ap.intentRef, "assignment-intent-stale", `${recordKey(assignment)}.intentRef`); this.consultDecision(intent); const ip = intent.payload; const covers = ip.subjectRefs.some((s) => refEquals(s, task) || refEquals(s, assignment)); if (ip.outcome !== "approved" || !["plan-approval", "delegation", "assignment-change"].includes(ip.decisionKind) || !covers) { refuse(3, "not-authorized", "assignment-intent-not-applicable", `${recordKey(assignment)}.intentRef`); } // L7 — the assigned task itself is consulted work (§10.2): work.read is required // in the full intersection even when it has no mission or dependency references. consultedWork = true; this.requireAuthorization(task); const tp = task.payload; if (!["ready", "active"].includes(tp.status)) refuse(3, "not-authorized", "task-not-ready", recordKey(task)); const taskIntent = this.resolveCurrent(tp.intentRef, "task-intent-stale", `${recordKey(task)}.intentRef`); this.consultDecision(taskIntent); const tip = taskIntent.payload; if (tip.outcome !== "approved" || tip.decisionKind !== "plan-approval" || !tip.subjectRefs.some((s) => refEquals(s, task))) { refuse(3, "not-authorized", "task-intent-not-approved", `${recordKey(task)}.intentRef`); } // §10.2 scope guards, then L8 mission chain and dependency heads if (tp.missionRef !== null) this.workScopeGuard(tp.missionRef, workspaceScope, `${recordKey(task)}.missionRef`); tp.dependencies.forEach((d, i) => this.workScopeGuard(d, workspaceScope, `${recordKey(task)}.dependencies[${i}]`)); if (tp.restrictions !== null) layers.push(this.declaredLayer(tp.restrictions, `${recordKey(task)}.restrictions`)); if (tp.missionRef !== null) { let mref = tp.missionRef; let mwhere = `${recordKey(task)}.missionRef`; while (mref !== null) { const mission = this.resolveCurrent(mref, "mission-stale", mwhere); this.requireAuthorization(mission); if (mission.payload.status !== "active") refuse(3, "not-authorized", "mission-not-active", recordKey(mission)); missions.push(mission); if (mission.payload.restrictions !== null) layers.push(this.declaredLayer(mission.payload.restrictions, `${recordKey(mission)}.restrictions`)); mref = mission.payload.parentMissionRef; mwhere = `${recordKey(mission)}.parentMissionRef`; } } tp.dependencies.forEach((d, i) => { const dep = this.resolveCurrent(d, "dependency-stale", `${recordKey(task)}.dependencies[${i}]`); this.requireAuthorization(dep); dependencies.push(dep); }); } // L9 if (execution.kind === "restrictions") layers.push(this.declaredLayer(execution.restrictions, "execution.restrictions")); let layer = UNRESTRICTED_LAYER; for (const l of layers) layer = intersectLayers(layer, l); if (consultedWork && !layerHasOperation(layer, "work.read")) { refuse(3, "not-authorized", "consulted-work-not-readable", recordKey(task)); } if (requiredOperation === "file.change" || purpose === "requester") { dependencies.forEach((dep) => { if (dep.payload.status !== "accepted") refuse(3, "not-authorized", "dependency-not-accepted", recordKey(dep)); }); } return { layer, agent, project, workspace, task, assignment, missions, dependencies }; } /** * §10.2 for a proposal's subject/target task under a requester context: scope guards * on its references, then work.read in the given intersection. The task is consulted * work in itself, so work.read is required even without mission/dependency references. */ checkWorkAccess(task, contextScope, layer) { const tp = task.payload; if (tp.missionRef !== null) this.workScopeGuard(tp.missionRef, contextScope, `${recordKey(task)}.missionRef`); tp.dependencies.forEach((d, i) => this.workScopeGuard(d, contextScope, `${recordKey(task)}.dependencies[${i}]`)); if (!layerHasOperation(layer, "work.read")) refuse(3, "not-authorized", "consulted-work-not-readable", recordKey(task)); } checkOperationPermitted(layer, operation) { if (!layerHasOperation(layer, operation.name)) refuse(3, "not-authorized", "operation-not-permitted", operation.name); if (operation.name === "file.read" && !layerPermitsPath(layer, "readPaths", operation.target.path)) { refuse(3, "not-authorized", "path-not-permitted", "bundle.operation.target"); } if (operation.name === "file.change" && !layerPermitsPath(layer, "writePaths", operation.target.path)) { refuse(3, "not-authorized", "path-not-permitted", "bundle.operation.target"); } } // ---- stage 9: main permission preview evaluateMain() { const { selection, operation } = this.bundle; const ctx = this.computeContext({ agentId: selection.agentId, projectId: selection.projectId, workspaceId: selection.workspaceId, assignmentRef: selection.assignmentRef, execution: selection.execution, requiredOperation: operation.name, purpose: "selection", }); this.checkOperationPermitted(ctx.layer, operation); } // ---- stage 9: proposal negative check (§5, r2 §8, §10.1–10.3) evaluateProposal() { const { selection, proposal: p } = this.bundle; const step = (id, refusal) => { if (!(id in PROPOSAL_STEPS)) throw new Error(`internal: unknown proposal step ${id}`); refusal.step = id; throw refusal; }; const attempt = (id, fn) => { try { return fn(); } catch (e) { if (e instanceof Refusal && !e.step) step(id, e); throw e; } }; if (selection.assignmentRef === null || !refEquals(p.subjectAssignmentRef, selection.assignmentRef)) { refuse(2, "invalid-request", "proposal-subject-mismatch", "bundle.proposal.subjectAssignmentRef"); } const subject = this.exact(selection.assignmentRef); const selectedScope = { kind: "workspace", projectId: selection.projectId, workspaceId: selection.workspaceId }; if (subject.payload.agentId !== selection.agentId || scopeKey(subject.scope) !== scopeKey(selectedScope)) { refuse(2, "invalid-request", "proposal-selection-mismatch", "bundle.selection"); } const requesterAgent = this.headOf("agent-definition", p.requesterAgentId, { kind: "system" }); if (!requesterAgent) refuse(2, "missing-state", "requester-agent-missing", "bundle.proposal.requesterAgentId"); const targetTaskRecord = this.exact(p.targetTaskRef); const originalScope = plainScope(subject.scope); const targetScope = plainScope(targetTaskRecord.scope); const bindContext = (ctx, scope, where) => { const a = this.exact(ctx.assignmentRef); if (a.payload.agentId !== p.requesterAgentId || scopeKey(a.scope) !== scopeKey(scope)) { refuse(2, "invalid-request", "requester-context-mismatch", where); } }; bindContext(p.requesterContexts.original, originalScope, "bundle.proposal.requesterContexts.original"); bindContext(p.requesterContexts.target, targetScope, "bundle.proposal.requesterContexts.target"); // §5: once structural validation succeeds, a missing delegation accompanied by a // message yields message-is-not-authority before any requester admission step. if (p.delegationRef === null && hasKey(p, "message")) { step("message-is-not-authority", new Refusal(3, "not-authorized", "message-is-not-authority", "bundle.proposal.message")); } // current-head requirements on proposal refs const subjectHead = this.resolveCurrent(selection.assignmentRef, "assignment-stale", "bundle.selection.assignmentRef"); const targetTask = this.resolveCurrent(p.targetTaskRef, "task-ref-stale", "bundle.proposal.targetTaskRef"); const subjectTask = this.resolveCurrent(subjectHead.payload.taskRef, "task-ref-stale", `${recordKey(subjectHead)}.taskRef`); // Step 1: original scope const original = attempt("requester-lacks-original-scope-authority", () => { const c = this.computeContext({ agentId: p.requesterAgentId, projectId: originalScope.projectId, workspaceId: originalScope.workspaceId, assignmentRef: p.requesterContexts.original.assignmentRef, execution: p.requesterContexts.original.execution, requiredOperation: "assignment.change", purpose: "requester", }); if (!layerHasOperation(c.layer, "assignment.change")) refuse(3, "not-authorized", "operation-not-permitted", "original"); this.checkWorkAccess(subjectTask, originalScope, c.layer); return c; }); // Step 2: target scope const target = attempt("requester-lacks-target-scope-authority", () => { const c = this.computeContext({ agentId: p.requesterAgentId, projectId: targetScope.projectId, workspaceId: targetScope.workspaceId, assignmentRef: p.requesterContexts.target.assignmentRef, execution: p.requesterContexts.target.execution, requiredOperation: "assignment.change", purpose: "requester", }); if (!layerHasOperation(c.layer, "assignment.change")) refuse(3, "not-authorized", "operation-not-permitted", "target"); this.checkWorkAccess(targetTask, targetScope, c.layer); return c; }); // Step 3: delegation if (p.delegationRef === null) { step("delegation-not-applicable", new Refusal(3, "not-authorized", "delegation-missing", "bundle.proposal.delegationRef")); } const issuer = attempt("delegation-not-applicable", () => { const where = "bundle.proposal.delegationRef"; const d = this.resolveCurrent(p.delegationRef, "delegation-stale", where); this.consultDecision(d); const dp = d.payload; if (dp.decisionKind !== "delegation" || dp.outcome !== "approved") refuse(3, "not-authorized", "delegation-not-approved", where); if (!dp.delegatedOperations.includes("assignment.change")) refuse(3, "not-authorized", "delegation-operation-not-covered", where); const recipient = dp.subjectRefs.some((s) => refEquals(s, requesterAgent)); const bounded = dp.subjectRefs.some((s) => refEquals(s, subjectHead) || refEquals(s, subjectTask)); if (!recipient || !bounded) refuse(3, "not-authorized", "delegation-bounds", where); const input = this.delegationInputs.find((di) => refEquals(di.decisionRef, d)); if (!input) refuse(3, "missing-state", "delegation-input-missing", where); for (const op of dp.delegatedOperations) { if (!input.issuerCeiling.operations.includes(op)) refuse(3, "not-authorized", "delegation-exceeds-issuer-ceiling", where); } return this.declaredLayer(input.issuerCeiling, `${where}.issuerCeiling`); }); // §10.1/§10.2: the issuer ceiling additionally narrows both requester calculations, // so every layer-dependent access condition is rechecked on the narrowed intersection // in the order of steps 1 and 2: consulted own work, the operation, subject/target work. const recheck = (id, c, work, scope, label) => attempt(id, () => { const narrowed = intersectLayers(c.layer, issuer); if (!layerHasOperation(narrowed, "work.read")) refuse(3, "not-authorized", "consulted-work-not-readable", recordKey(c.task)); if (!layerHasOperation(narrowed, "assignment.change")) refuse(3, "not-authorized", "operation-not-permitted", `${label}+issuer`); this.checkWorkAccess(work, scope, narrowed); }); recheck("requester-lacks-original-scope-authority", original, subjectTask, originalScope, "original"); recheck("requester-lacks-target-scope-authority", target, targetTask, targetScope, "target"); // Step 4: recorded change attempt("change-not-recorded", () => { const where = "bundle.proposal.changeDecisionRef"; if (p.changeDecisionRef === null) refuse(3, "not-authorized", "change-not-recorded", where); const c = this.resolveCurrent(p.changeDecisionRef, "change-decision-stale", where); this.consultDecision(c); const cp = c.payload; if (cp.decisionKind !== "assignment-change" || cp.outcome !== "approved") refuse(3, "not-authorized", "change-not-approved", where); const hasSubject = cp.subjectRefs.some((s) => refEquals(s, subjectHead)); const hasTarget = cp.subjectRefs.some((s) => refEquals(s, targetTask)); if (!hasSubject || !hasTarget) refuse(3, "not-authorized", "change-subjects-mismatch", where); }); // Step 5: current intent/status of subject assignment and target task attempt("intent-not-current", () => { this.requireAuthorization(subjectHead); const sp = subjectHead.payload; if (sp.status !== "selected") refuse(3, "not-authorized", "assignment-not-selected", recordKey(subjectHead)); const intent = this.resolveCurrent(sp.intentRef, "assignment-intent-stale", `${recordKey(subjectHead)}.intentRef`); this.consultDecision(intent); const ip = intent.payload; const covers = ip.subjectRefs.some((s) => refEquals(s, subjectTask) || refEquals(s, subjectHead)); if (ip.outcome !== "approved" || !["plan-approval", "delegation", "assignment-change"].includes(ip.decisionKind) || !covers) { refuse(3, "not-authorized", "assignment-intent-not-applicable", `${recordKey(subjectHead)}.intentRef`); } this.requireAuthorization(subjectTask); this.requireAuthorization(targetTask); const tp = targetTask.payload; if (!["ready", "active"].includes(tp.status)) refuse(3, "not-authorized", "task-not-ready", recordKey(targetTask)); const ti = this.resolveCurrent(tp.intentRef, "task-intent-stale", `${recordKey(targetTask)}.intentRef`); this.consultDecision(ti); const tip = ti.payload; if (tip.outcome !== "approved" || tip.decisionKind !== "plan-approval" || !tip.subjectRefs.some((s) => refEquals(s, targetTask))) { refuse(3, "not-authorized", "task-intent-not-approved", `${recordKey(targetTask)}.intentRef`); } }); // Step 6: every simulation check passed — still not allowed. step("runtime-reconciliation-required", new Refusal(3, "unknown-effects", "runtime-reconciliation-required", null)); } } // --------------------------------------------------------------------------- // Result assembly // --------------------------------------------------------------------------- function resultFor(exit, reason) { if (exit === 0) return "allowed"; if (exit === 3) return reason === "unknown-effects" ? "unresolved" : "refused"; return "invalid"; } /** * Process exit status for a closed result (charter §7 exits). The exit status is * process metadata derived from the emitted result, never a field of it: * 0 allowed; 3 refused/unresolved; 4 invalid with io-failure; 2 any other invalid. */ export function exitFor(result) { if (result.result === "allowed") return 0; if (result.result === "refused" || result.result === "unresolved") return 3; return result.reason === "io-failure" ? 4 : 2; } /** The closed emitted result fields of charter §7, in emission order. */ export const RESULT_KEYS = Object.freeze([ "disclaimer", "preview", "bundleVersion", "authentication", "declarations", "result", "reason", "rule", "selection", "operation", "proposal", "diagnostic", ]); /** Build the closed result object (§7). Key order is fixed; no exit field. */ export function buildResult({ exit, reason, rule, selection, operation, proposal, diagnostic }) { if (rule !== null && !RULE_SET.has(rule)) throw new Error(`internal: unknown rule ${rule}`); if (!REASONS.includes(reason)) throw new Error(`internal: unknown reason ${reason}`); if ((exit === 0) !== (reason === "allowed")) throw new Error(`internal: exit ${exit} inconsistent with reason ${reason}`); const built = { disclaimer: DISCLAIMER, preview: PREVIEW, bundleVersion: 1, authentication: "not-modelled", declarations: "unverified-simulation", result: resultFor(exit, reason), reason, rule, selection: selection === null ? null : { agentId: selection.agentId, projectId: selection.projectId, workspaceId: selection.workspaceId, assignmentRef: plainRef(selection.assignmentRef), }, operation: operation === null ? null : { name: operation.name, target: operation.target === null ? null : { root: "workspace", path: operation.target.path }, }, proposal: proposal === null ? null : { result: proposal.result, reason: proposal.reason, rule: proposal.rule, selectedAssignmentRef: plainRef(proposal.selectedAssignmentRef), }, diagnostic: diagnostic === null ? null : { byteOffset: diagnostic.byteOffset === undefined ? null : diagnostic.byteOffset, inputPath: diagnostic.inputPath === undefined ? null : diagnostic.inputPath, }, }; if (exitFor(built) !== exit) throw new Error(`internal: exit ${exit} inconsistent with ${built.result}/${reason}`); return built; } /** * Evaluate a parsed bundle value. Returns {result, exit, detail}: result is the * closed §7 object, exit the derived process status (never emitted inside the * result), and detail an internal locator (or null) that the CLI must never emit. */ export function evaluate(bundle) { let selection = null; let operation = null; try { validateBundleShape(bundle); selection = bundle.selection; operation = bundle.operation; const model = new Model(bundle); model.indexIdentities(); model.checkStructuralReferences(); model.checkCycles(); model.checkRevisionChains(); model.checkOwnership(); model.checkOperation(); if (operation.name === "assignment.change") { model.evaluateProposal(); throw new Error("internal: proposal evaluation must not fall through"); } model.evaluateMain(); const result = buildResult({ exit: 0, reason: "allowed", rule: null, selection, operation, proposal: null, diagnostic: null }); return { result, exit: exitFor(result), detail: null }; } catch (e) { if (!(e instanceof Refusal)) throw e; let proposal = null; if (e.step) { proposal = { result: PROPOSAL_STEPS[e.step], reason: e.reason, rule: e.step, selectedAssignmentRef: selection.assignmentRef, }; } const result = buildResult({ exit: e.exit, reason: e.reason, rule: e.rule, selection, operation, proposal, diagnostic: null }); return { result, exit: exitFor(result), detail: e.detail }; } } /** Closed result only (no internal detail). */ export function inspect(bundle) { return evaluate(bundle).result; }