From 67f5634eacced2f34625510aa7b8a4c4f970b881 Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Mon, 7 Sep 2026 09:17:51 -0400 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=94=92=20Preserve=20protected=20compo?= =?UTF-8?q?nent=20authority=20through=20generated=20wrappers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- architecture.md | 44 +++- packages/core/src/components/Evaluate.ts | 12 +- .../core/src/components/import-authority.ts | 8 + packages/core/src/evaluation-profile.ts | 23 +- packages/core/src/execute.ts | 1 + packages/core/src/expand.ts | 46 ++-- packages/core/src/generated-xmd.ts | 84 ++++--- packages/core/src/invocation-identity.ts | 107 +++++++- packages/core/src/syntax-admitted.ts | 10 +- .../core/tests/evaluate-component.test.ts | 230 ++++++++++++++++++ .../core/tests/evaluation-profile.test.ts | 152 ++++++++++++ packages/core/tests/generated-xmd.test.ts | 18 ++ .../core/tests/syntax-loaded-copy.test.ts | 61 ++++- specs/executable-mdx-spec.md | 31 ++- 14 files changed, 752 insertions(+), 75 deletions(-) diff --git a/architecture.md b/architecture.md index 99f35f838..724660c4a 100644 --- a/architecture.md +++ b/architecture.md @@ -3714,7 +3714,7 @@ assembled the run. Above them sits the engine's own, and a name in it means the same thing in every execution: whichever host built it, whichever package registered what, and whatever the repository holds. -One component is in it. `` describes the vocabulary of the site it is +`` and `` are in it. `` describes the vocabulary of the site it is written at, and a description of a run's vocabulary that anything in the run could answer for is a description of nothing — the value of the answer is exactly that nobody but core produced it. @@ -3743,14 +3743,40 @@ and verified where the component is invoked, and core's own copy is what runs. Every other name in the execution stays the ordinary open import it has always been. -**Protection is about the answer, not about power.** A protected implementation -is handed the lexical syntax reference for its site and nothing else: no -component definitions, no import witness, no invocation capability, no policy -table, no provider and no registration handle. The body itself is kept in a table -private to the copy of core that built the implementation and reached only by -canonical expansion, so an implementation another loaded copy created — which is -an ordinary arrangement, because a component can be loaded from disk beside its -own copy — has no body here and no answer to give. +**Protection is about the answer, not about power.** Canonical expansion hands a +protected body its lexical syntax reference and the internal operations its +site owns. `` receives the captured evaluation profile, its paired +content projection and an operation that derives a child protected route for +exact admitted definitions. None is published through a context, component API, +import answer or public host input. + +Protected lookup uses exact function identity. The execution's route projects +an already routed source onto a wrapper without exposing the body; an +unprotected source projects nothing. Profile sealing projects the first wrapper +onto the full execution route. `` derives a fresh child route seeded +only with its selected sealed definitions, and passes that route and its narrowed +lexical `SyntaxReference` internally to generated expansion. Generated import +projects its form/result wrapper only within the child route. Canonical dispatch +issues the invocation in the routed body's original domain and applies generated +form checks and read-result collection around either kind of body. Authored +imports still require their own settled selection frame; a routed function +cannot repair a missing or multiply selected domain. Paired producers retain +their lexical site route and operational authority. + +A trusted provider may therefore delegate and claim canonical self-closing +`` as a component answer. Its bare form reports the admitted vocabulary; +its named form renders the enclosing authoring documentation with narrowed +availability. Verified protected documentation provenance survives sealing even +when the delegating provider has a different origin. That provenance is +descriptive data and never a callable lookup key. + +Routes, projection edges, bodies, domains, callable sets and site operations +belong to one live execution. Child closure and execution teardown invalidate +retained operations. A same-name function, structural clone, independent wrapper, +unadmitted definition or implementation another loaded copy built gains no +route. Durable records keep their existing structural identities, forms, source, +profile inputs and results; they contain no callable authority. Continuation +rebuilds routes from the resuming execution's own verified answers. **The named form is a second question.** Bare `` answers *what may I write here*. `` answers *how do I use this one*, and diff --git a/packages/core/src/components/Evaluate.ts b/packages/core/src/components/Evaluate.ts index 438bf0858..f2c9df85b 100644 --- a/packages/core/src/components/Evaluate.ts +++ b/packages/core/src/components/Evaluate.ts @@ -71,7 +71,7 @@ import type { Operation } from "effection"; import { getExpansion } from "../expansion.ts"; import { NO_PROFILE, REVOKED } from "../evaluation-profile.ts"; import type { CapturedEntry, CapturedProfile } from "../evaluation-profile.ts"; -import { evaluateGeneratedXmd } from "../generated-xmd.ts"; +import { evaluateProtectedGeneratedXmd } from "../generated-xmd.ts"; import type { GeneratedEffectClass, GeneratedMutation, @@ -224,8 +224,8 @@ function evaluate(claim: IdentityClaimant): ProtectedBody { // producer is told about is the vocabulary the fragment is admitted for — // and reported as availability against the enclosing reference, which keeps // the authoring documentation the site already had. - const narrowed = narrow(site.syntax, entries); - const source = stated === undefined ? yield* project(site, narrowed) : stated; + const narrowedSyntax = narrow(site.syntax, entries); + const source = stated === undefined ? yield* project(site, narrowedSyntax) : stated; // Read after the producer has rendered, and exactly once per occurrence: a // producer may itself commit mutations, and the basis this admission is @@ -254,9 +254,13 @@ function evaluate(claim: IdentityClaimant): ProtectedBody { // fragment produced inside another fragment's producer leaves the outer one // where it was, and a failed one leaves nothing behind. const leave = yield* profile.enterFragment(); + const narrowedBodies = site.narrowProtectedBodies( + entries.admitted.map((entry) => entry.definition.fn), + ); try { - return answer(yield* evaluateGeneratedXmd(request)); + return answer(yield* evaluateProtectedGeneratedXmd(request, narrowedBodies, narrowedSyntax)); } finally { + narrowedBodies?.close(); leave(); } }; diff --git a/packages/core/src/components/import-authority.ts b/packages/core/src/components/import-authority.ts index a1d98837e..d6e70849c 100644 --- a/packages/core/src/components/import-authority.ts +++ b/packages/core/src/components/import-authority.ts @@ -19,6 +19,8 @@ */ import type { ComponentDefinition, FunctionComponentDefinition, SourcePosition } from "../types.ts"; +import type { Operation } from "effection"; +import type { ComponentInvocation } from "../invocation-identity.ts"; import type { FormSelections, InvocationIdentities, @@ -147,6 +149,12 @@ export interface ExpansionAuthority { * kept past this execution's teardown reaches a table that is gone. */ readonly protectedBodies?: ProtectedBodies; + /** The generated import's form check and result collection, around either body kind. */ + readonly invoke?: ( + fn: unknown, + invocation: ComponentInvocation, + body: Operation, + ) => Operation; } /** Why an answer is not the one canonical execution produced for this name. */ diff --git a/packages/core/src/evaluation-profile.ts b/packages/core/src/evaluation-profile.ts index 7e45a1486..ff8eb027a 100644 --- a/packages/core/src/evaluation-profile.ts +++ b/packages/core/src/evaluation-profile.ts @@ -54,7 +54,7 @@ import type { FragmentFileAccess, } from "./fragment-capabilities.ts"; import { isFormDispatcher } from "./invocation-identity.ts"; -import type { ComponentInvocation } from "./invocation-identity.ts"; +import type { ComponentInvocation, ProtectedBodies } from "./invocation-identity.ts"; import type { FetchRequest } from "./fetch-request.ts"; import { normalizeFetchRequest, requestRecord } from "./fetch-request.ts"; import { CORE_REVISION } from "./generated-xmd.ts"; @@ -411,6 +411,8 @@ export type ResolvedAnswers = ReadonlyMap; * own operation or a provider's answer, and the two are different grants. */ export interface CapturedEntry { + /** Documentation provenance of an exact routed answer; grants no callable authority. */ + readonly protectedOrigin?: string; readonly name: string; readonly identity: FragmentIdentity; readonly forms: readonly FragmentForm[]; @@ -553,8 +555,8 @@ export interface PreparedProfile { readonly live: () => boolean; /** End them. Registered by canonical execution before any installation runs. */ readonly revoke: () => void; - /** The completed profile, from the answers this execution resolved. */ - seal(answers: ResolvedAnswers): Operation; + /** Seal answers, projecting lifetime wrappers through the execution's private operation. */ + seal(answers: ResolvedAnswers, project?: ProtectedBodies["project"]): Operation; } /** @@ -605,14 +607,17 @@ export function* prepareEvaluationProfile( capabilities.revoke(); }, // deno-lint-ignore require-yield - *seal(answers: ResolvedAnswers): Operation { + *seal( + answers: ResolvedAnswers, + project?: ProtectedBodies["project"], + ): Operation { // One sealed implementation per name, built before either table is // sealed. A name that holds two entries — the self-closing spelling in // `read` and the paired one in `write` — is one component seen from two // sides, so both entries carry the same object: two guards over one // answer would be two lifetimes for one implementation, and which of // them a fragment reached would depend on which table admitted it. - const sealed = sealAnswers(answered, answers, capabilities); + const sealed = sealAnswers(answered, answers, capabilities, project); return Object.freeze({ read: sealEntries(read, sealed), write: sealEntries(write, sealed), @@ -877,6 +882,7 @@ function spelling(identity: FragmentIdentity): string { /** One provider-backed name's sealed implementation, shared by every entry. */ interface SealedAnswer { + readonly protectedOrigin?: string; readonly props: PropsSchema; readonly definition: FunctionComponentDefinition; readonly dispatch?: unknown; @@ -894,6 +900,7 @@ function sealAnswers( answered: ReadonlyMap, answers: ResolvedAnswers, capabilities: CapturedCapabilities, + project: ProtectedBodies["project"] | undefined, ): ReadonlyMap { const sealed = new Map(); for (const name of answered.keys()) { @@ -908,11 +915,14 @@ function sealAnswers( } const answer = resolved.definition; const inner = answer.fn; + const guard = bounded(inner, capabilities); + const protectedOrigin = project?.(inner, guard); sealed.set( name, Object.freeze({ props: detach(answer.props), - definition: Object.freeze({ ...answer, fn: bounded(inner, capabilities) }), + definition: Object.freeze({ ...answer, fn: guard }), + ...(protectedOrigin === undefined ? {} : { protectedOrigin }), // The provider's own dispatcher, when its answer has one. The // definition above runs behind core's lifetime guard, so what a // selection would read off it is core's function rather than the @@ -950,6 +960,7 @@ function sealEntry(entry: PreparedEntry, sealed: ReadonlyMap) => + authority?.invoke === undefined + ? body + : authority.invoke(definition.fn, issued.invocation, body); const projectionState: ProjectionState = { invocation, projecting: issued.projecting, @@ -3267,23 +3279,29 @@ function* expandFunctionComponent( } return renderSegments(outcome.segments); }); + let active = true; try { - return yield* guarded(validatedProps, issued.invocation, { - syntax: authority?.syntax, - evaluation: authority?.evaluation, - projectContent: lease?.project, - }); + return yield* dispatchBody( + guarded(validatedProps, issued.invocation, { + syntax: authority?.syntax, + evaluation: authority?.evaluation, + projectContent: lease?.project, + narrowProtectedBodies: (implementations: Iterable) => + active ? authority?.protectedBodies?.narrow(implementations) : undefined, + }), + ); } finally { // Closed in the same breath the issuance is: a projector a body // kept authorizes nothing once that body has finished. lease?.close(); + active = false; issued.close(); } } // Ended in the same breath the body is: an issuance a wrapper kept // from a finished element authorizes nothing when it is routed here. try { - return yield* definition.fn(validatedProps, issued.invocation); + return yield* dispatchBody(definition.fn(validatedProps, issued.invocation)); } finally { issued.close(); } diff --git a/packages/core/src/generated-xmd.ts b/packages/core/src/generated-xmd.ts index f3d442cf3..bc05979ca 100644 --- a/packages/core/src/generated-xmd.ts +++ b/packages/core/src/generated-xmd.ts @@ -129,8 +129,9 @@ import { scanSegments } from "./scanner.ts"; import { sourceDescription } from "./source-position.ts"; import { RESERVED_STRUCTURAL } from "./structural.ts"; import { installFormSelections, invocationForm } from "./invocation-identity.ts"; -import type { FormSelections } from "./invocation-identity.ts"; +import type { FormSelections, ProtectedBodies } from "./invocation-identity.ts"; import type { ComponentInvocation } from "./invocation-identity.ts"; +import type { SyntaxReference } from "./syntax-reference.ts"; import type { FunctionComponentDefinition, Json, @@ -887,6 +888,8 @@ class GeneratedImportAuthority implements ImportAuthority { readonly #forms = installFormSelections(); /** The form authority under each admitted name's wrapper. */ readonly #dispatchers = new Map(); + readonly #protectedBodies: ProtectedBodies | undefined; + readonly #invocations = new WeakMap(); /** * A generated fragment may invoke only what the host admitted for it, so @@ -898,7 +901,7 @@ class GeneratedImportAuthority implements ImportAuthority { return true; } - constructor(named: readonly Planned[]) { + constructor(named: readonly Planned[], protectedBodies?: ProtectedBodies) { const planned = new Map(); for (const invocation of named) { const queue = planned.get(invocation.name); @@ -909,6 +912,7 @@ class GeneratedImportAuthority implements ImportAuthority { queue.push(invocation); } this.#planned = planned; + this.#protectedBodies = protectedBodies; } /** What each admitted read returned, in invocation order. */ @@ -937,30 +941,14 @@ class GeneratedImportAuthority implements ImportAuthority { // invocation to it — a wrapper that collected results is trusted host code // and takes no part in deciding which form-specific body runs. - // The value is taken where the component produced it. Reading it back from - // the rendered fragment would lose every observation that renders nothing, - // which is most of them. A mutation collects nothing: its own durable - // record is the account of it. - const values = entry.effect === "read" ? this.#values : undefined; const admitted: FunctionComponentDefinition = { ...copy, *fn(props, invocation) { - // Before the component runs, and therefore before it reaches a - // provider: the form that chose this identity must be the form this - // invocation is of. holdForm(form, invocation); - const value = yield* implementation(props, invocation); - values?.push({ - name: entry.name, - // Parsed rather than asserted: this value is retained and handed back - // to a trusted host, and a component that returned something with no - // JSON shape has broken the contract an observation runs under. A - // component that returned nothing observed nothing, which is `null`. - value: value === undefined ? null : parseJson(value), - }); - return value; + return yield* implementation(props, invocation); }, }; + this.#invocations.set(admitted.fn, planned); // The wrapper above is the answer to the import; the dispatcher underneath // it is the form authority. Remembered by name so `authorize` can record it // against core's own copy — the object expansion actually invokes — because @@ -970,9 +958,32 @@ class GeneratedImportAuthority implements ImportAuthority { // is core's guard around somebody else's would otherwise offer the guard, // and a form authority read off the guard selects no body at all. this.#dispatchers.set(name, entry.dispatch ?? implementation); + this.#protectedBodies?.project(implementation, admitted.fn); return this.#imports.issue(name, admitted); } + // Protected dispatch bypasses the public wrapper function, so collection and + // form checking surround canonical dispatch rather than that function alone. + *invoke( + fn: unknown, + invocation: ComponentInvocation, + body: Operation, + ): Operation { + const planned = typeof fn === "function" ? this.#invocations.get(fn) : undefined; + if (planned === undefined) { + throw new GeneratedXmdError(CONSTRUCT.component); + } + holdForm(planned.form, invocation); + const value = yield* body; + if (planned.entry.effect === "read") { + this.#values.push({ + name: planned.entry.name, + value: value === undefined ? null : parseJson(value), + }); + } + return value; + } + /** The frames this fragment's own imports record into. */ get forms(): FormSelections { return this.#forms; @@ -1935,10 +1946,12 @@ function expand( id: string, segments: Segment[], named: readonly Planned[], + protectedBodies: ProtectedBodies | undefined, + syntax: SyntaxReference | undefined, ): Operation { return scoped(function* () { yield* ErrorMode.set("throw"); - const authority = new GeneratedImportAuthority(named); + const authority = new GeneratedImportAuthority(named, protectedBodies); yield* Component.around( { // deno-lint-ignore require-yield @@ -1958,12 +1971,16 @@ function expand( extendPath("", { f: "gen", id }), 0, undefined, - // No identity domains: a generated fragment names no durable work of its - // own, and what it may invoke is this table and nothing else. The - // selection frames are this fragment's own, so what its admitted imports - // select is not the enclosing document's business and cannot be reached - // from it. - { imports: authority, forms: authority.forms }, + // No enclosing identity table: protected invocation domains travel only + // through the narrowed route. Import and form selection belong to this + // fragment, while the reference preserves the admitting site's documentation. + { + imports: authority, + forms: authority.forms, + invoke: (fn, invocation, body) => authority.invoke(fn, invocation, body), + ...(protectedBodies === undefined ? {} : { protectedBodies }), + ...(syntax === undefined ? {} : { syntax }), + }, // A generated fragment is the engine's own text, so it owns no value body // and a written into it satisfies no declaration. undefined, @@ -1983,8 +2000,17 @@ function expand( * the same sequence and restores the admission and every observation that * already committed rather than performing them again. */ -export function* evaluateGeneratedXmd( +export function evaluateGeneratedXmd( + request: GeneratedXmdRequest, +): Operation { + return evaluateProtectedGeneratedXmd(request, undefined, undefined); +} + +/** Canonical Evaluate's internal handoff; absent from the public host surface. */ +export function* evaluateProtectedGeneratedXmd( request: GeneratedXmdRequest, + protectedBodies: ProtectedBodies | undefined, + syntax: SyntaxReference | undefined, ): Operation { const allow = selection(request.allow); const entries = selectedEntries(request, allow); @@ -2027,5 +2053,5 @@ export function* evaluateGeneratedXmd( // The retained source is what expands, so a continuation runs exactly the // bytes this run admitted rather than a caller's copy of them. const restored = yield* preflight(decided.source, table, ceilings); - return yield* expand(request.id, restored.segments, restored.named); + return yield* expand(request.id, restored.segments, restored.named, protectedBodies, syntax); } diff --git a/packages/core/src/invocation-identity.ts b/packages/core/src/invocation-identity.ts index 624b5ce96..f7e6399da 100644 --- a/packages/core/src/invocation-identity.ts +++ b/packages/core/src/invocation-identity.ts @@ -220,6 +220,8 @@ export interface ProtectedSite { readonly evaluation: CapturedProfile | undefined; /** How this body renders its own paired content, when it has any. */ readonly projectContent: ProjectProtectedContent | undefined; + /** Derive a child route from this site's exact admitted implementations. */ + narrowProtectedBodies(implementations: Iterable): ProtectedBodies | undefined; } /** @@ -236,6 +238,21 @@ export interface ProtectedSite { export interface ProtectedBodies { /** The body canonical expansion may enter for this exact implementation. */ body(fn: unknown): ProtectedBody | undefined; + /** Project an already routed source; report its documentation origin, never its body. */ + project(source: unknown, wrapper: unknown): string | undefined; + /** A fresh route seeded only with exact implementations this route holds. */ + narrow(implementations: Iterable): ProtectedBodies; + /** Issue in the routed body's domain without publishing that domain. */ + issue( + fn: unknown, + id: string, + component: string, + frame: Scope, + content: boolean, + selection?: FunctionComponent, + ): IssuedInvocation | undefined; + /** Close this route and its descendants. */ + close(): void; } interface ProtectedInstallation extends ProtectedBodies { @@ -250,13 +267,87 @@ interface ProtectedInstallation extends ProtectedBodies { name: string, build: (claim: IdentityClaimant) => ProtectedBody, claim: IdentityClaimant, + domain: IdentityDomain, + origin: string, ): FunctionComponent; + /** + * End the route, and every route narrowed from it. + * + * Called at the execution's teardown. A route or a projection callback kept + * past the execution answers for nothing afterwards even though the WeakMap it + * closed over is still reachable, and a route from one execution can never + * authorize another. + */ + revoke(): void; } function createProtectedBodies(): ProtectedInstallation { - const bodies = new WeakMap(); + // One liveness flag for the execution's whole route and every route narrowed + // from it: teardown revokes the lot at once. A retained route still holds its + // WeakMap, so liveness rather than reachability is what invalidates it. + let live = true; + interface Entry { + readonly body: ProtectedBody; + readonly domain: IdentityDomain; + readonly origin: string; + } + function route(bodies: WeakMap, parent: () => boolean): ProtectedBodies { + let open = true; + const active = () => open && parent(); + return { + body(fn): ProtectedBody | undefined { + const entry = active() && typeof fn === "function" ? bodies.get(fn) : undefined; + if (entry === undefined) { + return undefined; + } + return function* (props, invocation, site) { + if (!active()) { + throw new ComponentInvocationError("this protected-body route has closed"); + } + return yield* entry.body(props, invocation, site); + }; + }, + project(source, wrapper): string | undefined { + if (!active() || typeof source !== "function" || typeof wrapper !== "function") { + return; + } + const body = bodies.get(source); + if (body !== undefined) { + bodies.set(wrapper, body); + } + return body?.origin; + }, + narrow(implementations): ProtectedBodies { + const narrowed = new WeakMap(); + if (active()) { + for (const implementation of implementations) { + if (typeof implementation !== "function") { + continue; + } + const body = bodies.get(implementation); + if (body !== undefined) { + narrowed.set(implementation, body); + } + } + } + return route(narrowed, active); + }, + issue(fn, id, component, frame, content, selection): IssuedInvocation | undefined { + const entry = active() && typeof fn === "function" ? bodies.get(fn) : undefined; + return entry === undefined || entry.domain.component !== component + ? undefined + : issueInvocation(id, component, entry.domain, frame, content, selection); + }, + close(): void { + open = false; + }, + }; + } + const bodies = new WeakMap(); + const base = route(bodies, () => live); return { - implementation(name, build, claim): FunctionComponent { + ...base, + implementation(name, build, claim, domain, origin): FunctionComponent { // deno-lint-ignore require-yield function* unreachable(): Operation { throw new ComponentInvocationError( @@ -264,11 +355,11 @@ function createProtectedBodies(): ProtectedInstallation { "observes nothing", ); } - bodies.set(unreachable, build(claim)); + bodies.set(unreachable, { body: build(claim), domain, origin }); return unreachable; }, - body(fn): ProtectedBody | undefined { - return typeof fn === "function" ? bodies.get(fn) : undefined; + revoke(): void { + live = false; }, }; } @@ -990,6 +1081,8 @@ export function installIdentities( component.name, component.build, domain.claim, + domain.domain, + component.origin, ); domain.implementation = implementation; // Not marked private: a protected implementation is resolved by canonical @@ -1092,6 +1185,10 @@ export function installIdentities( for (const domain of minted.values()) { domain.revoke(); } + // The route goes with the domains: a wrapper projected into it, or a + // route narrowed from it, answers for nothing once the execution that + // minted the bodies is gone. + protectedBodies.revoke(); }, }, registrations, diff --git a/packages/core/src/syntax-admitted.ts b/packages/core/src/syntax-admitted.ts index c2dcf0027..30bff3d46 100644 --- a/packages/core/src/syntax-admitted.ts +++ b/packages/core/src/syntax-admitted.ts @@ -30,9 +30,8 @@ const UNDECLARED: ReturnsSchema = { type: "string" }; * * The identity's origin is what each entry reports it came from, because that * is what the host stated and what a continuation is compared against. Reported - * as a registration rather than as core's own protected tier even for core's - * pinned ``: inside a fragment it is an identity the *host* admitted, and - * a fragment cannot re-register or shadow anything at all. + * as a registration for ordinary entries. An exact protected answer keeps its + * canonical documentation origin even when a different provider delegates it. */ export function admittedSymbols(entries: readonly CapturedEntry[]): SyntaxSymbols { return { @@ -50,7 +49,10 @@ function describe(entry: CapturedEntry): CompleteComponentSyntaxEntry { return { kind: "component", name: entry.name, - origin: { kind: "registered", origin: entry.identity.origin, reserved: false }, + origin: + entry.protectedOrigin === undefined + ? { kind: "registered", origin: entry.identity.origin, reserved: false } + : { kind: "protected", origin: entry.protectedOrigin }, sourceKind: "registered", inspectability: "complete", // The forms the *host admitted this entry for*, which is narrower than the diff --git a/packages/core/tests/evaluate-component.test.ts b/packages/core/tests/evaluate-component.test.ts index ef89d7526..ac7edb42b 100644 --- a/packages/core/tests/evaluate-component.test.ts +++ b/packages/core/tests/evaluate-component.test.ts @@ -31,6 +31,236 @@ import { recordedFiles } from "./support/fragment-files.ts"; import type { RecordedFiles } from "./support/fragment-files.ts"; import { answerProvider, implementation } from "./support/answer-provider.ts"; import type { Implementation, ProviderOptions } from "./support/answer-provider.ts"; +import type { FunctionComponentDefinition } from "../src/types.ts"; +import { ActiveProjection } from "../src/projection.ts"; + +function independentWrapper(answer: FunctionComponentDefinition): FunctionComponentDefinition { + const fn = answer.fn; + if (typeof fn !== "function") { + throw new Error("expected an ordinary function"); + } + return { ...answer, fn: (props, invocation) => fn(props, invocation) }; +} + +function syntaxProfile( + options: { + transform?: (answer: FunctionComponentDefinition) => FunctionComponentDefinition; + retained?: FunctionComponentDefinition[]; + unclaimed?: boolean; + revision?: string; + } = {}, +): ExecutionInstallation { + let captured = false; + return { + evaluation: { + read: [ + { + kind: "component-answer", + name: "Syntax", + identity: { origin: "test://syntax-provider", key: "Syntax", revision: "1" }, + forms: ["self-closing"], + }, + ], + }, + componentAnswers: [ + { + origin: "test://syntax-provider", + *install(registrar) { + yield* registrar.around(function* (request, next) { + const answer = yield* next(); + if (request.name === "Syntax" && !captured) { + captured = true; + if (answer.kind !== "function") { + throw new Error("expected canonical Syntax"); + } + options.retained?.push(answer); + if (options.unclaimed) { + return answer; + } + request.claim(answer, { key: "Syntax", revision: options.revision ?? "1" }); + return options.transform?.(answer) ?? answer; + } + return answer; + }); + }, + }, + ], + }; +} + +describe("protected generated component answers", () => { + it("routes delegated Syntax through both wrappers and collects its actual read value", function* () { + const source = ''; + const result = yield* run( + `---\nreturns:\n type: object\n---\n\n`, + [syntaxProfile()], + ); + expect(result).toMatchObject({ observations: [{ name: "Syntax" }] }); + if (typeof result !== "object" || result === null || Array.isArray(result)) { + throw new Error("expected an evaluation result"); + } + expect(result.output).toContain("**Available in this evaluation:** yes"); + expect(result.output).toContain("**Available in this evaluation:** no"); + expect(result.observations).toEqual([{ name: "Syntax", value: result.output }]); + }); + + it("keeps the producer's route while the generated child admits only Syntax", function* () { + const told: string[] = []; + const stream = new InMemoryStream(); + yield* scoped(function* () { + yield* registerComponents([ + { + name: "Producer", + origin: "test://producer", + props: { type: "object", properties: { text: { type: "string" } }, required: ["text"] }, + // deno-lint-ignore require-yield + *fn(props): Operation { + told.push(String(props.text)); + return ''; + }, + }, + ]); + const output = yield* run( + '\n\n\n\n', + [syntaxProfile()], + stream, + ); + expect(String(output)).toContain("**Available in this evaluation:** no"); + }); + expect(told).toHaveLength(1); + expect(told[0]).toContain("### ``"); + expect(told[0]).not.toContain("### ``"); + expect(told[0]).not.toContain("### ``"); + const reads = (yield* stream.readAll()).filter( + (event) => event.type === "yield" && event.description.type === "syntax_symbols", + ); + expect(reads).toHaveLength(2); + expect(reads[0]).not.toEqual(reads[1]); + for (const text of ["", "", ""]) { + expect( + yield* refusal(run(``, [syntaxProfile()])), + ).toContain("did not admit"); + } + }); + + it("refuses copied, mutated, wrapped, unclaimed, misidentified and wrong-form answers", function* () { + const source = `'} />`; + const ran: string[] = []; + const transforms: ((answer: FunctionComponentDefinition) => FunctionComponentDefinition)[] = [ + (answer) => ({ ...answer }), + (answer) => ({ + ...answer, + *fn(): Operation { + ran.push("same name"); + return "forged"; + }, + }), + independentWrapper, + (answer) => { + answer.props = { type: "object" }; + return answer; + }, + ]; + for (const transform of transforms) { + const stream = new InMemoryStream(); + expect(yield* refusal(run(source, [syntaxProfile({ transform })], stream))).toBeTruthy(); + expect(admissions(yield* stream.readAll())).toHaveLength(0); + } + for (const options of [{ unclaimed: true }, { revision: "2" }]) { + const stream = new InMemoryStream(); + expect(yield* refusal(run(source, [syntaxProfile(options)], stream))).toBeTruthy(); + expect(admissions(yield* stream.readAll())).toHaveLength(0); + } + expect( + yield* refusal(run(`'} />`, [syntaxProfile()])), + ).toContain("self-closing"); + expect(ran).toEqual([]); + }); + + it("exposes no route to middleware and rejects a generated-import wrapper replacement", function* () { + const seen: FunctionComponentDefinition[] = []; + const surfaces: string[][] = []; + const stream = new InMemoryStream(); + const failed = yield* refusal( + scoped(function* () { + yield* Component.around({ + *importComponent([name, position], next) { + const answer = yield* next(name, position); + if (name === "Syntax" && answer.kind === "function") { + seen.push(answer); + surfaces.push(Reflect.ownKeys(answer).map(String)); + surfaces.push(Reflect.ownKeys(answer.fn).map(String)); + const projection = yield* ActiveProjection.get(); + surfaces.push( + projection === undefined ? [] : Reflect.ownKeys(projection).map(String), + ); + if (seen.length === 2) { + return independentWrapper(answer); + } + } + return answer; + }, + }); + return yield* run(`'} />`, [syntaxProfile()], stream); + }), + ); + expect(failed).toContain("canonical execution"); + expect(seen).toHaveLength(2); + for (const keys of surfaces) { + expect(keys).not.toContain("protectedBodies"); + expect(keys).not.toContain("narrowProtectedBodies"); + expect(keys).not.toContain("syntax"); + } + expect( + (yield* stream.readAll()).some( + (event) => event.type === "yield" && event.description.type === "syntax_symbols", + ), + ).toBe(false); + }); + + it("rebuilds routes on continuation and cannot resurrect a retained callable", function* () { + const source = `'} as="answer" />\n`; + const stream = new InMemoryStream(); + const retained: FunctionComponentDefinition[] = []; + const original = yield* run(source, [syntaxProfile({ retained })], stream); + const events = yield* stream.readAll(); + const admission = events.findIndex( + (event) => event.type === "yield" && event.description.type === "generated_xmd", + ); + expect(admission).toBeGreaterThanOrEqual(0); + const partial = events.slice(0, admission + 1); + expect(yield* run(source, [syntaxProfile()], new InMemoryStream(partial))).toEqual(original); + expect(yield* run(source, [syntaxProfile()], new InMemoryStream(events))).toEqual(original); + const held = retained[0]; + if (held === undefined || typeof held.fn !== "function") { + throw new Error("no delegated answer was retained"); + } + expect(yield* refusal(held.fn({}, { hasContent: () => false }))).toContain("canonical core"); + expect( + yield* refusal( + run(source, [syntaxProfile({ transform: () => held })], new InMemoryStream(partial)), + ), + ).toContain("carries no identity"); + expect( + yield* refusal(run(source, [syntaxProfile({ unclaimed: true })], new InMemoryStream(events))), + ).toContain("carries no identity"); + function inspect(value: unknown): void { + expect(typeof value).not.toBe("function"); + if (typeof value === "object" && value !== null) { + for (const key of Reflect.ownKeys(value)) { + expect([ + "protectedBodies", + "narrowProtectedBodies", + "projectContent", + "protectedOrigin", + ]).not.toContain(String(key)); + inspect(Reflect.get(value, key)); + } + } + } + inspect(events); + }); +}); const ROOT_PATH = "evaluate.md"; diff --git a/packages/core/tests/evaluation-profile.test.ts b/packages/core/tests/evaluation-profile.test.ts index 41d93a486..243196bc5 100644 --- a/packages/core/tests/evaluation-profile.test.ts +++ b/packages/core/tests/evaluation-profile.test.ts @@ -15,6 +15,9 @@ import { describe, it } from "@executablemd/test-support/bdd"; import { expect } from "@executablemd/test-support/expect"; import type { Operation } from "effection"; +import { scoped, useScope } from "effection"; +import { installIdentities } from "../src/invocation-identity.ts"; +import type { ProtectedBodies, ProtectedSite } from "../src/invocation-identity.ts"; import { prepareEvaluationProfile } from "../src/evaluation-profile.ts"; import type { @@ -28,6 +31,155 @@ import type { import type { Json } from "../src/types.ts"; import { recordedFiles } from "./support/fragment-files.ts"; +describe("protected route projection", () => { + function installation() { + return installIdentities( + [], + [], + ["Admitted", "Hidden"].map((name) => ({ + name, + origin: "test://protected", + props: { type: "object" }, + build: (claim) => + function* (_props, invocation): Operation { + return `${name}:${yield* claim(invocation)}`; + }, + })), + ); + } + + function invoke(route: ProtectedBodies, fn: unknown): Operation { + return scoped(function* () { + const body = route.body(fn); + const issued = route.issue(fn, "occurrence", "Admitted", yield* useScope(), false); + if (body === undefined || issued === undefined) { + return "unrouted"; + } + try { + const site: ProtectedSite = { + syntax: undefined, + evaluation: undefined, + projectContent: undefined, + narrowProtectedBodies: route.narrow, + }; + return yield* body({}, issued.invocation, site); + } finally { + issued.close(); + } + }); + } + + it("projects exact sealed functions and cannot widen a child from any other source", function* () { + const owner = installation(); + const other = installation(); + owner.activate(); + other.activate(); + try { + const original = owner.protected.get("Admitted"); + if (original === undefined) { + throw new Error("missing protected definition"); + } + const prepared = yield* prepareEvaluationProfile({ + read: [ + { + kind: "component-answer", + name: "Admitted", + identity: { origin: "test://provider", key: "Admitted", revision: "1" }, + forms: ["self-closing"], + }, + ], + }); + const captured = yield* prepared.seal( + new Map([["Admitted", { definition: original }]]), + owner.protectedBodies.project, + ); + const sealed = captured.read[0]?.definition.fn; + expect(yield* invoke(owner.protectedBodies, sealed)).toBe("Admitted:occurrence"); + const child = owner.protectedBodies.narrow([sealed]); + const wrapper = function* (): Operation { + throw new Error("unrouted wrapper ran"); + }; + child.project(sealed, wrapper); + expect(yield* invoke(child, wrapper)).toBe("Admitted:occurrence"); + expect(yield* invoke(owner.protectedBodies, wrapper)).toBe("unrouted"); + expect(yield* invoke(other.protectedBodies, sealed)).toBe("unrouted"); + const independent = function* Admitted(): Operation { + return "independent"; + }; + for (const source of [ + original.fn, + owner.protected.get("Hidden")?.fn, + other.protected.get("Admitted")?.fn, + independent, + { ...original, fn: independent }, + ]) { + const attempted = function* (): Operation { + return "attempted"; + }; + child.project(source, attempted); + expect(yield* invoke(child, source)).toBe("unrouted"); + expect(yield* invoke(child, attempted)).toBe("unrouted"); + } + child.close(); + expect(yield* invoke(child, wrapper)).toBe("unrouted"); + expect(yield* invoke(owner.protectedBodies, sealed)).toBe("Admitted:occurrence"); + prepared.revoke(); + } finally { + owner.identities.revoke(); + other.identities.revoke(); + } + }); + + it("revokes retained lookup, body, projection and narrowing operations at teardown", function* () { + const owner = installation(); + owner.activate(); + const original = owner.protected.get("Admitted")?.fn; + const child = owner.protectedBodies.narrow([original]); + const project = child.project; + const narrow = child.narrow; + const wrapper = function* (): Operation { + return "wrapper"; + }; + project(original, wrapper); + expect(yield* invoke(child, wrapper)).toBe("Admitted:occurrence"); + const body = child.body(wrapper); + if (body === undefined) { + throw new Error("expected a live body"); + } + owner.identities.revoke(); + project(original, wrapper); + expect(yield* invoke(child, wrapper)).toBe("unrouted"); + expect(yield* invoke(narrow([original, wrapper]), wrapper)).toBe("unrouted"); + let message = ""; + try { + yield* body( + {}, + { hasContent: () => false }, + { + syntax: undefined, + evaluation: undefined, + projectContent: undefined, + narrowProtectedBodies: narrow, + }, + ); + } catch (error) { + message = error instanceof Error ? error.message : String(error); + } + expect(message).toContain("route has closed"); + const later = installation(); + later.activate(); + try { + later.protectedBodies.project(wrapper, later.protected.get("Admitted")?.fn); + expect(yield* invoke(later.protectedBodies, wrapper)).toBe("unrouted"); + expect(yield* invoke(later.protectedBodies, later.protected.get("Admitted")?.fn)).toBe( + "Admitted:occurrence", + ); + } finally { + later.identities.revoke(); + } + }); +}); + function entry(overrides: Partial = {}): CapabilityEntry { const name = overrides.name ?? "File"; return { diff --git a/packages/core/tests/generated-xmd.test.ts b/packages/core/tests/generated-xmd.test.ts index 84c22f469..7f4c621bd 100644 --- a/packages/core/tests/generated-xmd.test.ts +++ b/packages/core/tests/generated-xmd.test.ts @@ -106,6 +106,24 @@ function probe(): GeneratedObservation { return pinnedComponent("Probe", hostIdentity("test://probe", "Probe"), PROBE); } +describe("generated authority stays internal", () => { + it("ignores route and lexical-reference injection on the public request", function* () { + const candidate = { + ...request("", [probe()]), + get protectedBodies(): never { + throw new Error("public route was read"); + }, + get syntax(): never { + throw new Error("public syntax was read"); + }, + }; + const result = yield* evaluate(candidate); + expect(result.failure).toBeUndefined(); + expect(result.output).toBe("probed"); + expect(result.values).toEqual([{ name: "Probe", value: "probed" }]); + }); +}); + function useWorkspace(): Operation { return resource(function* (provide) { const root = yield* until(realpath(yield* until(mkdtemp(join(tmpdir(), "generated-xmd-"))))); diff --git a/packages/core/tests/syntax-loaded-copy.test.ts b/packages/core/tests/syntax-loaded-copy.test.ts index a93e059f5..3b13674ed 100644 --- a/packages/core/tests/syntax-loaded-copy.test.ts +++ b/packages/core/tests/syntax-loaded-copy.test.ts @@ -58,7 +58,11 @@ interface LoadedCopy { protectedComponents: readonly unknown[], ): { protected: ReadonlyMap; - protectedBodies: { body(fn: unknown): unknown }; + protectedBodies: { + body(fn: unknown): unknown; + project(source: unknown, wrapper: unknown): unknown; + }; + identities: { revoke(): void }; activate(): void; }; } @@ -139,12 +143,15 @@ function answering(definition: unknown): ExecutionInstallation { }; } -function runRoot(installations: readonly ExecutionInstallation[]): Operation { +function runRoot( + installations: readonly ExecutionInstallation[], + source = "\n", +): Operation { return scoped(function* () { return yield* collect( yield* executeInstalled( { - ...retainedSource("documents/root.md", "\n"), + ...retainedSource("documents/root.md", source), stream: new InMemoryStream(), includes: [], }, @@ -188,6 +195,7 @@ describe("Tier SYN — a separately loaded protected implementation", () => { }, ], ); + yield* ensure(() => installed.identities.revoke()); installed.activate(); const foreign = installed.protected.get(SYNTAX_COMPONENT); if (foreign === undefined) { @@ -218,5 +226,52 @@ describe("Tier SYN — a separately loaded protected implementation", () => { ]); expect(String(output)).toContain("### ``"); expect(String(output)).not.toContain("a foreign catalog"); + + const forged = function* (): Operation { + return "foreign projection"; + }; + const projected: unknown[] = []; + const observed: string[] = []; + let captured = false; + const profile: ExecutionInstallation = { + evaluation: { + read: [ + { + kind: "component-answer", + name: "Syntax", + identity: { origin: "test://delegate", key: "Syntax", revision: "1" }, + forms: ["self-closing"], + }, + ], + }, + componentAnswers: [ + { + origin: "test://delegate", + *install(registrar) { + yield* registrar.around(function* (request, next) { + const answer = yield* next(); + if (request.name === "Syntax" && answer.kind === "function") { + observed.push(request.name); + projected.push(installed.protectedBodies.project(answer.fn, forged)); + if (!captured) { + captured = true; + return request.claim(answer, { key: "Syntax", revision: "1" }); + } + } + return answer; + }); + }, + }, + ], + }; + const generated = yield* runRoot( + [profile], + `'} as="answer" />\n`, + ); + expect(String(generated)).toContain("### ``"); + expect(String(generated)).not.toContain("foreign projection"); + expect(observed).toEqual(["Syntax", "Syntax"]); + expect(projected).toEqual([undefined, undefined]); + expect(installed.protectedBodies.body(forged)).toBeUndefined(); }); }); diff --git a/specs/executable-mdx-spec.md b/specs/executable-mdx-spec.md index 35d134826..48aae6df0 100644 --- a/specs/executable-mdx-spec.md +++ b/specs/executable-mdx-spec.md @@ -3920,6 +3920,35 @@ keeps its own imports, declarations, bindings, providers, working directory and error mode; and it is not published through `ActiveProjection`, so nothing else in the execution can obtain or influence it. +**Protected answers survive only canonical wrapping.** A trusted provider may +delegate canonical self-closing `` and claim that exact answer for a +component-answer profile entry. Profile sealing projects its lifetime wrapper +from the exact protected function. `` derives a fresh child route +containing only the protected bodies among its selected sealed definitions and +passes it, with the already narrowed lexical `SyntaxReference`, internally to +generated evaluation. Generated import projects its form/result wrapper within +that child route. Dispatch preserves the protected body's invocation domain, +checks the admitted form and collects the read's value just as for an ordinary +generated component. + +The producer's authored children retain the lexical site route. The generated +fragment receives the child route, so a component the producer can invoke is not +thereby admitted to the fragment. Generated `` lists only the admitted +vocabulary; its named form reads the enclosing documentation and reports +narrowed availability. The exact protected answer retains its canonical +documentation origin across provider delegation; this descriptive provenance +authorizes nothing. + +Projection succeeds only from an exact function already in the live route and +exposes no body. Names, structural copies and independent wrappers confer no +authority. Neither route nor projection operations appear on public host, +profile, document, middleware or generated-request inputs. Another loaded copy +cannot observe or inject them. Routes and descendants close with their owner; +retained operations fail after closure, and one execution cannot authorize +another. Durable admission retains existing stable identities, forms, source, +profile inputs and results only. Replay reconstructs callable routing from the +current execution's verified answers, never from a retained function or route. + **`allow` narrows; it never grants.** It names an effect *class* — `read` or `write` — and the class resolves to a table the host already installed. Omitting it asks for `read`. A class the host installed nothing for is refused before the @@ -11076,7 +11105,7 @@ through the captured capability because there is no other way to reach it. | FE5 | `write` permits only the named host-profile write forms and never creates authority from text. | | FE6 | Root frontmatter, root props, `returns`, and independent `` selection refuse before effects. | | FE7 | With `as`, the result object is captured; without `as`, it is discarded; neither form emits fragment output. | -| FE8 | Public `` and a directly nested Plan see the enclosing Evaluate vocabulary for the selected `allow`; generated text is validated against that same vocabulary. No private Syntax implementation is involved. **Receives #758's SY19**, which #759 cannot prove: it installs no narrower syntax reference, and `` is not in the generated-XMD pinned identity table until this issue admits it. | +| FE8 | Public `` and a directly nested Plan see the enclosing Evaluate vocabulary for the selected `allow`; generated text is validated against that same vocabulary. A trusted provider's delegated canonical `` component answer also renders inside generated XMD, through both canonical wrappers and the narrowed lexical reference. Bare output lists only admitted entries; named documentation retains the enclosing reference and reports narrowed availability. | | FE9 | A deferred Plan's public `` sees its own ordinary vocabulary; a later narrower Evaluate rejects incompatible text before effects. **Receives #758's SY20**, for the same reason. | | FE10 | Exact text and policy survive journal/continuation; changed text or policy refuses before effects. | | FE11 | Completed Plan work and completed fragment effects replay without repetition. | From 33a45a31558765ed9a647bd4a5add4c6c6b200e9 Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Mon, 7 Sep 2026 09:26:57 -0400 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=90=9B=20Align=20admitted=20protected?= =?UTF-8?q?=20symbols=20source=20provenance?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/core/src/syntax-admitted.ts | 2 +- .../core/tests/evaluate-component.test.ts | 44 ++++++++++++++++++- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/packages/core/src/syntax-admitted.ts b/packages/core/src/syntax-admitted.ts index 30bff3d46..d7ad7fdb3 100644 --- a/packages/core/src/syntax-admitted.ts +++ b/packages/core/src/syntax-admitted.ts @@ -53,7 +53,7 @@ function describe(entry: CapturedEntry): CompleteComponentSyntaxEntry { entry.protectedOrigin === undefined ? { kind: "registered", origin: entry.identity.origin, reserved: false } : { kind: "protected", origin: entry.protectedOrigin }, - sourceKind: "registered", + sourceKind: entry.protectedOrigin === undefined ? "registered" : "protected", inspectability: "complete", // The forms the *host admitted this entry for*, which is narrower than the // forms the implementation accepts whenever one name holds two identities: diff --git a/packages/core/tests/evaluate-component.test.ts b/packages/core/tests/evaluate-component.test.ts index ac7edb42b..641c7d1ba 100644 --- a/packages/core/tests/evaluate-component.test.ts +++ b/packages/core/tests/evaluate-component.test.ts @@ -33,6 +33,10 @@ import { answerProvider, implementation } from "./support/answer-provider.ts"; import type { Implementation, ProviderOptions } from "./support/answer-provider.ts"; import type { FunctionComponentDefinition } from "../src/types.ts"; import { ActiveProjection } from "../src/projection.ts"; +import { installIdentities } from "../src/invocation-identity.ts"; +import { SYNTAX_PROTECTED } from "../src/components/Syntax.ts"; +import { prepareEvaluationProfile } from "../src/evaluation-profile.ts"; +import { admittedSymbols } from "../src/syntax-admitted.ts"; function independentWrapper(answer: FunctionComponentDefinition): FunctionComponentDefinition { const fn = answer.fn; @@ -90,10 +94,41 @@ function syntaxProfile( describe("protected generated component answers", () => { it("routes delegated Syntax through both wrappers and collects its actual read value", function* () { + const profile = syntaxProfile(); + if (profile.evaluation === undefined) { + throw new Error("expected a Syntax evaluation profile"); + } + // syntax_symbols retains rendered text, so sourceKind must be checked on + // the sealed profile's structured symbols before rendering drops that field. + const identity = installIdentities([], [], [SYNTAX_PROTECTED]); + const prepared = yield* prepareEvaluationProfile(profile.evaluation); + try { + const definition = identity.protected.get("Syntax"); + if (definition === undefined) { + throw new Error("expected canonical Syntax"); + } + const captured = yield* prepared.seal( + new Map([["Syntax", { definition }]]), + identity.protectedBodies.project, + ); + const symbols = admittedSymbols(captured.read); + expect(symbols.categories[2].entries).toMatchObject([ + { + name: "Syntax", + origin: { kind: "protected", origin: "@executablemd/core" }, + sourceKind: "protected", + }, + ]); + } finally { + prepared.revoke(); + identity.identities.revoke(); + } + const stream = new InMemoryStream(); const source = ''; const result = yield* run( `---\nreturns:\n type: object\n---\n\n`, - [syntaxProfile()], + [profile], + stream, ); expect(result).toMatchObject({ observations: [{ name: "Syntax" }] }); if (typeof result !== "object" || result === null || Array.isArray(result)) { @@ -102,6 +137,13 @@ describe("protected generated component answers", () => { expect(result.output).toContain("**Available in this evaluation:** yes"); expect(result.output).toContain("**Available in this evaluation:** no"); expect(result.observations).toEqual([{ name: "Syntax", value: result.output }]); + const retained = (yield* stream.readAll()).filter( + (event) => event.type === "yield" && event.description.type === "syntax_symbols", + ); + expect(retained).toHaveLength(1); + expect(retained[0]).toMatchObject({ + result: { status: "ok", value: { symbols: result.output } }, + }); }); it("keeps the producer's route while the generated child admits only Syntax", function* () {