229 lines
10 KiB
TypeScript
229 lines
10 KiB
TypeScript
// lib/goal-policy.ts — process-local, one-writer Goal policy bridge (T165 WP1).
|
|
//
|
|
// Goal registers the read-only consumer before Mosaic Core starts a session.
|
|
// A trusted launcher or role adapter registers exactly one resolver, then
|
|
// Mosaic Core publishes one validated attestation for the incarnation. This
|
|
// module has no filesystem, network, provider, or model-visible surface.
|
|
|
|
export const GOAL_REPORT_FORMAT = "ms-executive-update/v1";
|
|
export const GOAL_REPORT_CONTRACT_PATH = "skills-local/ms-executive-update/SKILL.md";
|
|
export const GOAL_REPORT_CONTRACT_SECTION = "Machine contract (for `goal_report` payloads and any parser)";
|
|
export const GOAL_REPORT_CONTRACT_BLOB = "df30c6fbb54b4a65a298c9e51c07f742610d171c";
|
|
export const GOAL_REPORT_CONTRACT_SHA256 = "bbea48a46b1f8da7bc759f86856fb52830b7dde456b826317163c6dc6ccab319";
|
|
export const GOAL_POLICY_ENFORCEMENT = "pre-state-change-fail-closed";
|
|
export const GOAL_POLICY_IDENTIFIER_RESOLUTION = "consumer-fail-closed";
|
|
|
|
export type GoalPolicyRole = "gate-merge-ng" | "plan-ng" | "review-ng";
|
|
export type GoalPolicyRoleRevision = 3 | 4;
|
|
|
|
export type GoalPolicyAttestationV1 = Readonly<{
|
|
schemaVersion: 1;
|
|
role: GoalPolicyRole;
|
|
roleRevision: GoalPolicyRoleRevision;
|
|
manifestSha256: string;
|
|
format: typeof GOAL_REPORT_FORMAT;
|
|
contractPath: typeof GOAL_REPORT_CONTRACT_PATH;
|
|
contractSection: typeof GOAL_REPORT_CONTRACT_SECTION;
|
|
contractBlob: typeof GOAL_REPORT_CONTRACT_BLOB;
|
|
contractSha256: typeof GOAL_REPORT_CONTRACT_SHA256;
|
|
enforcement: typeof GOAL_POLICY_ENFORCEMENT;
|
|
identifierResolution: typeof GOAL_POLICY_IDENTIFIER_RESOLUTION;
|
|
launchGeneration: number;
|
|
incarnationId: string;
|
|
}>;
|
|
|
|
export type GoalItemResolutionV1 =
|
|
| Readonly<{
|
|
outcome: "resolved";
|
|
objectId: string;
|
|
objectSha256: string;
|
|
changedSincePreviousAcceptedReport: boolean;
|
|
completionEvidenceId: string | null;
|
|
}>
|
|
| Readonly<{ outcome: "zero" | "multiple" | "unavailable" | "stale" }>;
|
|
|
|
export type GoalTrackedItem = Readonly<{
|
|
token: string;
|
|
section: "Just Completed" | "Next Step" | "Blocked";
|
|
}>;
|
|
|
|
export type GoalTrackedItemResolverV1 = (parsedItem: GoalTrackedItem) => Promise<GoalItemResolutionV1>;
|
|
|
|
export type GoalPolicyReason =
|
|
| "consumer-unavailable"
|
|
| "resolver-unavailable"
|
|
| "resolver-already-registered"
|
|
| "already-published"
|
|
| "malformed-attestation"
|
|
| "attestation-mismatch"
|
|
| "incarnation-mismatch";
|
|
|
|
export interface GoalPolicyPublication {
|
|
attestation: GoalPolicyAttestationV1;
|
|
resolver: GoalTrackedItemResolverV1;
|
|
/** Trusted Core journal bridge. Receives a stable code only, never payload text. */
|
|
onDenial?: (reason: string) => void;
|
|
}
|
|
|
|
interface GoalPolicySlot {
|
|
consumerRegistered: boolean;
|
|
resolver?: GoalTrackedItemResolverV1;
|
|
publication?: GoalPolicyPublication;
|
|
}
|
|
|
|
const REGISTRY_KEY = Symbol.for("mosaic.goal-policy.v1.registry");
|
|
type GlobalWithGoalPolicy = typeof globalThis & { [REGISTRY_KEY]?: Map<string, GoalPolicySlot> };
|
|
|
|
function registry(): Map<string, GoalPolicySlot> {
|
|
const global = globalThis as GlobalWithGoalPolicy;
|
|
if (!global[REGISTRY_KEY]) {
|
|
Object.defineProperty(global, REGISTRY_KEY, {
|
|
value: new Map<string, GoalPolicySlot>(),
|
|
configurable: false,
|
|
enumerable: false,
|
|
writable: false,
|
|
});
|
|
}
|
|
return global[REGISTRY_KEY]!;
|
|
}
|
|
|
|
function utf8Length(value: string): number {
|
|
return Buffer.byteLength(value, "utf8");
|
|
}
|
|
|
|
export function isOpaqueGoalPolicyId(value: unknown): value is string {
|
|
return typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9._:-]*$/.test(value) && utf8Length(value) >= 1 && utf8Length(value) <= 128;
|
|
}
|
|
|
|
export function isLowerSha256(value: unknown): value is string {
|
|
return typeof value === "string" && /^[0-9a-f]{64}$/.test(value);
|
|
}
|
|
|
|
function roleRevisionMatches(role: GoalPolicyRole, revision: GoalPolicyRoleRevision): boolean {
|
|
return (role === "gate-merge-ng" && revision === 3) || ((role === "plan-ng" || role === "review-ng") && revision === 4);
|
|
}
|
|
|
|
function hasExactKeys(value: Record<string, unknown>, keys: readonly string[]): boolean {
|
|
const actual = Object.keys(value).sort();
|
|
const expected = [...keys].sort();
|
|
return actual.length === expected.length && actual.every((key, index) => key === expected[index]);
|
|
}
|
|
|
|
export function validateGoalPolicyAttestation(value: unknown): value is GoalPolicyAttestationV1 {
|
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
const attestation = value as Record<string, unknown>;
|
|
if (!hasExactKeys(attestation, [
|
|
"schemaVersion", "role", "roleRevision", "manifestSha256", "format", "contractPath",
|
|
"contractSection", "contractBlob", "contractSha256", "enforcement", "identifierResolution",
|
|
"launchGeneration", "incarnationId",
|
|
])) return false;
|
|
if (attestation.schemaVersion !== 1) return false;
|
|
if (attestation.role !== "gate-merge-ng" && attestation.role !== "plan-ng" && attestation.role !== "review-ng") return false;
|
|
if (attestation.roleRevision !== 3 && attestation.roleRevision !== 4) return false;
|
|
if (!roleRevisionMatches(attestation.role, attestation.roleRevision)) return false;
|
|
if (!isLowerSha256(attestation.manifestSha256)) return false;
|
|
if (attestation.format !== GOAL_REPORT_FORMAT) return false;
|
|
if (attestation.contractPath !== GOAL_REPORT_CONTRACT_PATH) return false;
|
|
if (attestation.contractSection !== GOAL_REPORT_CONTRACT_SECTION) return false;
|
|
if (attestation.contractBlob !== GOAL_REPORT_CONTRACT_BLOB) return false;
|
|
if (attestation.contractSha256 !== GOAL_REPORT_CONTRACT_SHA256) return false;
|
|
if (attestation.enforcement !== GOAL_POLICY_ENFORCEMENT) return false;
|
|
if (attestation.identifierResolution !== GOAL_POLICY_IDENTIFIER_RESOLUTION) return false;
|
|
if (typeof attestation.launchGeneration !== "number" || !Number.isSafeInteger(attestation.launchGeneration) || attestation.launchGeneration < 1) return false;
|
|
return isOpaqueGoalPolicyId(attestation.incarnationId);
|
|
}
|
|
|
|
function freezeAttestation(value: GoalPolicyAttestationV1): GoalPolicyAttestationV1 {
|
|
return Object.freeze({ ...value });
|
|
}
|
|
|
|
/** Goal calls this during extension initialization, before Mosaic Core publishes. */
|
|
export function registerGoalPolicyConsumer(incarnationId: string): { ok: true } | { ok: false; reason: "incarnation-mismatch" } {
|
|
if (!isOpaqueGoalPolicyId(incarnationId)) return { ok: false, reason: "incarnation-mismatch" };
|
|
const slots = registry();
|
|
const slot = slots.get(incarnationId) ?? { consumerRegistered: false };
|
|
slot.consumerRegistered = true;
|
|
slots.set(incarnationId, slot);
|
|
return { ok: true };
|
|
}
|
|
|
|
/**
|
|
* A trusted launcher or role adapter supplies a resolver before Core publishes.
|
|
* WP1 deliberately supplies no fallback resolver: missing resolver authority
|
|
* keeps a version-4 role fail-closed until the later broker/adapter work exists.
|
|
*/
|
|
export function registerGoalTrackedItemResolver(
|
|
incarnationId: string,
|
|
resolver: GoalTrackedItemResolverV1,
|
|
): { ok: true } | { ok: false; reason: GoalPolicyReason } {
|
|
if (!isOpaqueGoalPolicyId(incarnationId)) return { ok: false, reason: "incarnation-mismatch" };
|
|
if (typeof resolver !== "function") return { ok: false, reason: "resolver-unavailable" };
|
|
const slot = registry().get(incarnationId);
|
|
if (!slot?.consumerRegistered) return { ok: false, reason: "consumer-unavailable" };
|
|
if (slot.resolver) return { ok: false, reason: "resolver-already-registered" };
|
|
slot.resolver = resolver;
|
|
return { ok: true };
|
|
}
|
|
|
|
/** Mosaic Core is the only policy publisher and may publish once per incarnation. */
|
|
export function publishGoalPolicy(
|
|
value: GoalPolicyAttestationV1,
|
|
onDenial?: (reason: string) => void,
|
|
): { ok: true; attestation: GoalPolicyAttestationV1 } | { ok: false; reason: GoalPolicyReason } {
|
|
if (!validateGoalPolicyAttestation(value)) return { ok: false, reason: "malformed-attestation" };
|
|
const slot = registry().get(value.incarnationId);
|
|
if (!slot?.consumerRegistered) return { ok: false, reason: "consumer-unavailable" };
|
|
if (!slot.resolver) return { ok: false, reason: "resolver-unavailable" };
|
|
if (slot.publication) return { ok: false, reason: "already-published" };
|
|
const attestation = freezeAttestation(value);
|
|
slot.publication = Object.freeze({ attestation, resolver: slot.resolver, onDenial });
|
|
return { ok: true, attestation };
|
|
}
|
|
|
|
/** Read-only consumer view. No caller can replace a published pair. */
|
|
export function goalPolicyPublication(incarnationId: string): GoalPolicyPublication | undefined {
|
|
return registry().get(incarnationId)?.publication;
|
|
}
|
|
|
|
/** Goal records a stable, payload-free denial through the Core-owned journal bridge. */
|
|
export function recordGoalPolicyDenial(incarnationId: string, reason: string): void {
|
|
try {
|
|
registry().get(incarnationId)?.publication?.onDenial?.(reason);
|
|
} catch {
|
|
// A journal outage cannot turn a fail-closed report denial into an allow.
|
|
}
|
|
}
|
|
|
|
/** Core pre-dispatch check: attestation must still be the exact published pair. */
|
|
export function verifyGoalPolicyPublication(
|
|
expected: GoalPolicyAttestationV1,
|
|
): { ok: true } | { ok: false; reason: GoalPolicyReason } {
|
|
if (!validateGoalPolicyAttestation(expected)) return { ok: false, reason: "malformed-attestation" };
|
|
const published = goalPolicyPublication(expected.incarnationId);
|
|
if (!published) return { ok: false, reason: "resolver-unavailable" };
|
|
const actual = published.attestation;
|
|
const same = JSON.stringify(actual) === JSON.stringify(expected);
|
|
return same ? { ok: true } : { ok: false, reason: "attestation-mismatch" };
|
|
}
|
|
|
|
/** Validate the closed resolver result before Goal uses it. */
|
|
export function validateGoalItemResolution(value: unknown): value is GoalItemResolutionV1 {
|
|
if (typeof value !== "object" || value === null || Array.isArray(value) || !Object.isFrozen(value)) return false;
|
|
const result = value as Record<string, unknown>;
|
|
if (result.outcome === "zero" || result.outcome === "multiple" || result.outcome === "unavailable" || result.outcome === "stale") {
|
|
return hasExactKeys(result, ["outcome"]);
|
|
}
|
|
if (result.outcome !== "resolved" || !hasExactKeys(result, [
|
|
"outcome", "objectId", "objectSha256", "changedSincePreviousAcceptedReport", "completionEvidenceId",
|
|
])) return false;
|
|
return isOpaqueGoalPolicyId(result.objectId)
|
|
&& isLowerSha256(result.objectSha256)
|
|
&& typeof result.changedSincePreviousAcceptedReport === "boolean"
|
|
&& (result.completionEvidenceId === null || isOpaqueGoalPolicyId(result.completionEvidenceId));
|
|
}
|
|
|
|
/** Test-only registry reset. Production code has no reset or replacement path. */
|
|
export function resetGoalPolicyRegistryForTest(): void {
|
|
registry().clear();
|
|
}
|