Files
stack/scripts/foundation/resolve.mjs
T
jason.woltje 8ebddd6f93 feat(foundation): offline synthetic scope/permission inspector (FI-FILBERT-8 APPROVED r6)
Rocko-authored, Filbert-reviewed inspector (r6 manifest
a4a44930...) with full review/build/verdict evidence under
docs/plans/reviews. 43/0 selftests, oracle zero-disagreement,
foundation checker PASS. Owner A9 acceptance recorded separately.
2026-09-07 14:06:35 -05:00

1861 lines
86 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Foundation synthetic inspector — pure evaluation core.
*
* Implements charter candidate 3 (docs/plans/2026-09-06_foundation-inspector-charter.md,
* sha256 19b67211…) §§37 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 (L1L9)
* (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<node, node[]>. 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<rev, rec>, 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);
}
/**
* L1L9 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.110.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;
}