Skip to content

🔒 Make canonical core authoritative for execution - #432

Draft
taras wants to merge 10 commits into
mainfrom
agent/capability-backed-execution
Draft

🔒 Make canonical core authoritative for execution#432
taras wants to merge 10 commits into
mainfrom
agent/capability-backed-execution

Conversation

@taras

@taras taras commented Aug 10, 2026

Copy link
Copy Markdown
Owner

Why

Execution.execute middleware used to be handed the finished DocumentExecution
and have its return value taken as the result. That made every handler
authoritative by construction: one could answer without delegating and its
invented completion was the execution, and one could wrap the returned handle
and rewrite what the document had already settled.

That is also why #426 had no correct place to put workflow-run identity. Two
review rounds moved it — into ReplayGuard, then into an Execution-installed
stream wrapper — and both were defeated by registration order, because both
surfaces are composable policy.

This PR makes canonical core authoritative for execution and gives a trusted
host an explicit way to attach requirements. It is a prerequisite: it contains
no workflow-run recognition, no workflow filesystem, and no CLI lifecycle.

Stack

main
  → this PR
  → #426 (rebases onto this, replaces its ambient installers with ExecutionInstallation)
  → #428

Public API

@executablemd/core — the policy surface:

interface ExecutionApi {
  execute(request: ExecutionRequest): Operation<void>;   // was (options) => Operation<DocumentExecution>
  document(props): Operation<DocumentResult>;            // unchanged
}

interface ExecutionRequest {
  readonly options: ExecuteOptions;
  withOptions(options: ExecuteOptions): ExecutionRequest;
  addCompletionFailure(failure: () => Error | undefined): void;
}

execute(options): Operation<DocumentExecution>           // unchanged for callers
ExecutionProtocolError                                   // new

@executablemd/core/host — the infrastructure boundary:

type JournalAdmission = (retained: readonly DurableEvent[]) => Operation<void>;

interface ExecutionInstallation {
  readonly admissions?: readonly JournalAdmission[];
  install?(): Operation<void>;
}

executeInstalled(options, installations): Operation<DocumentExecution>

No admitJournal(), no requiredJournalAdmissions(), no JournalAdmissions
Context. An admission travels as a function the host holds and passes, which is
why a separately loaded package composes here without agreeing on a name.

How it works

executeInstalled(options, installations)
  1. copy + freeze every installation's admissions      ← before anything runs
  2. run each install()
  3. issue one opaque ExecutionRequest
  4. invoke public Execution.execute middleware
     … handlers inspect / withOptions / addCompletionFailure / refuse / delegate
     … the canonical terminal records the options and consumes the request
  5. require the request to have been consumed exactly once
  6. executeDocument(finalOptions, capturedAdmissions, completions)
       retainEvents(...)            ← the history is settled once, here
       every admission, in order    ← stops at the first refusal
       admitRootHistory(...)
       ReplayGuard → terminal reuse → Execution.document

Middleware return values are never read. execute(options) is the same call
with no installations, so ordinary execution is unchanged.

Migrated consumers

Consumer Before After
packages/testing useTesting() one-execution refusal, then decorateCompletion(inner, …) refusal stays pre-delegation; completion becomes addCompletionFailure()
packages/testing <Testing> replay hydration off executeOptions.stream, then decorateCompletion hydration inspects request.options.stream; completion becomes addCompletionFailure()
packages/core Agent bridgeRootProvider() manufactured a DocumentExecution with its own channel provider lifetime moves to Execution.document; prompt + teardown failures become addCompletionFailure()

decorateCompletion() is deleted from both packages — core's completion facility
replaces it. bridgeRootProvider() is deleted: nothing manufactures a
DocumentExecution any more.

Test-installed handlers needed no migration: they either use document (which
is unchanged) or refuse before delegating.

Verification

c7aa595c98ff6d8e5bf08113b8efe0dd73dfd9e3, gates read by exit code:

deno task lint                         0 errors
deno task check                        exit 0
deno task check:jsr                    exit 0
deno task build                        exit 0
deno task test --changed=origin/main   464 passed (3263 steps), 0 failed
git diff --check                       clean

Focused: Tier EP (28 unique steps), root-provider + agent-components (18 steps),
use-testing, document-target-execution, secret-detection.

Mutations

Each applied, the suite re-run, the source restored from HEAD and confirmed
byte-identical before the next.

# Mutation Tests killed
R1 middleware's returned value is accepted as the execution EP4
R2 execute without verifying the request was consumed EP3
R3 a consumed request may be delegated again EP6
R4 a reconstructed look-alike is accepted EP8
R5 admissions copied after install() EP11
R6 only the first admission runs EP12, EP13
R7b a later completion policy replaces an earlier failure EP17

Two honest notes:

  • R7a does not discriminate. Running completion policies against a failed
    document changes nothing, because the loop's !settled.ok guard already
    returns the existing failure. The two guards are redundant with each other by
    design; each alone is covered by the other, and neither can be removed without
    R7b or the call-site shape catching it.
  • EP8 had to be strengthened to catch R4. As first written it asserted only
    ExecutionProtocolError, which a silently-ignored look-alike also produces via
    "returned without delegating". It now asserts the refusal names the look-alike.

Behavior changes

  • A completion policy can no longer aggregate onto a failed document. The old
    Agent path combined a document failure with provider teardown into an
    AggregateError; under additive precedence an execution that already failed
    keeps its own failure. Prompt-failure aggregation against a successful
    document is unchanged and RP3/AC13 still pass on their existing assertions.
  • Provider teardown that raises after the document completed is recorded and
    added, not thrown — same precedence the bridge had, now without a manufactured
    handle.

Intentionally not here

  • No workflow-run recognition, workflow filesystem, or CLI lifecycle.
  • specs/workflow-spec.md is untouched; 📁 Give a workflow run's document its own filesystem (#366 PR 1) #426 owns the concrete installation and
    host handoff after it rebases.
  • JournalProvenance stays at its two existing trusted wrapping sites — this
    adds no wrapping site, because requirements are contributed into core's
    existing admission read rather than wrapped around it.
  • workflow-spec §6 is not resolved and core's root-import authority is not
    relaxed.

Scope confirmation

  • Every changed file supports the purpose described above.
  • Unrelated cleanup and formatting changes are excluded.
  • Generated or mechanical changes are clearly identified.
  • The description matches the final diff and test results.

@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown

PR #432: 🔒 Make canonical core authoritative for execution

14 files, +1873 / -198

Scope

🔴 PR has 2071 lines changed. Split into focused PRs.

🟡 2071 lines changed. PRs under 400 receive more thorough review.

🟡 PR mixes config and source changes.

🟡 package.json changed without dependency justification.

Structural

Oxlint structural signals:

  • no-unused-vars ×12: packages/core/src/agent/components.ts, packages/testing/src/components.ts, packages/core/src/execute.ts
  • no-unnecessary-type-assertion ×3: packages/core/src/execute.ts
  • no-empty-function ×2: packages/core/src/execute.ts

Slop

  • packages/testing/src/components.ts:139 (removed)
  • packages/core/src/execute.ts:626// The events themselves are the retained graph's own, already sealed.
  • packages/core/src/execute.ts:1595// does nothing, which is what lets a completed handle be read again.
  • packages/core/src/execute.ts:1675// Whatever a handler returns is not an execution, so it is not read.
  • packages/core/src/execution-request.ts:115// is guarded here, so nothing native or planted escapes.
  • packages/core/src/execution-request.ts:129// detach leaves nothing consumed either.
  • packages/core/src/execution-request.ts:143// A revoked proxy, or one whose has trap refuses. Not one of ours.

Static Analysis

Oxlint: 27 diagnostics across 3 files (9 rules)
Density: 0.014 violations/added-line

no-unused-vars (12): packages/core/src/agent/components.ts, packages/testing/src/components.ts, packages/core/src/execute.ts
no-unsafe-type-assertion (4): packages/core/src/execute.ts
no-unnecessary-type-assertion (3): packages/core/src/execute.ts
no-empty-function (2): packages/core/src/execute.ts
consistent-return (2): packages/core/src/execute.ts
no-array-sort (1): packages/core/src/agent/components.ts
no-shadow (1): packages/core/src/execute.ts
unbound-method (1): packages/core/src/execute.ts
no-floating-promises (1): packages/core/src/execute.ts

Correctness

No extraneous code patterns detected.

A stable Api name shares the middleware context; a createApi() instance
owns its own default handler. Canonical core invokes the public chain
through a private same-name instance whose default is closed over this
invocation, so every public handler composes exactly as before while the
terminal belongs to one execution and no other.

The exported Execution.execute default now always refuses, so calling it
with a captured live request settles nothing.

Consumption moved behind the issued invocation: it accepts only a
request created for that exact invocation, at the current generation,
unconsumed, validating everything before writing anything — so a
rejected foreign delegation consumes neither, and both requests stay
usable. Null, primitives, reconstructed values and hostile proxies are
refused with a fresh cause-free error rather than a native one.

Each invocation owns a child scope, so contextual behavior an
installation establishes reaches document teardown, stays out of a
concurrent invocation, and is gone from the next execution.

Also in this revision: the retained history every admission is held to
is frozen before anyone is offered it; completion precedence is
first-failure across prompts, teardown and test policies; and the
conformance table, terminology and ACP contract are reconciled with
what the code does.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 2 redundant comments. Inline suggestions to remove them below.

// is a compile-time claim: without this an admission could splice, reorder
// or empty the history in place, and every later admission, root-history
// validation and the replay itself would consume what it left behind.
// The events themselves are the retained graph's own, already sealed.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// The events themselves are the retained graph's own, already sealed.

Comment thread packages/testing/src/components.ts Outdated
// operation.
const replayed = yield* readCompletedRun(executeOptions.stream);
// The request's candidate stream, inspected and not acted on: hydration
// restores what the journal already recorded and decides nothing.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// restores what the journal already recorded and decides nothing.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 4 redundant comments. Inline suggestions to remove them below.

return yield* bridgeRootProvider(rootProvider, executeOptions, failures, next);
// Additive: prompt failures and a provider teardown failure turn a
// successful document into a failure. A document that already failed
// keeps its own failure — the completion policy adds, it does not replace.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// keeps its own failure — the completion policy adds, it does not replace.

// is a compile-time claim: without this an admission could splice, reorder
// or empty the history in place, and every later admission, root-history
// validation and the replay itself would consume what it left behind.
// The events themselves are the retained graph's own, already sealed.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// The events themselves are the retained graph's own, already sealed.

return retained;
// The same objects the admissions were held to. `readAll` is declared
// mutable by the protocol, so this is a fresh array over the identical
// sealed events rather than a second reading of the backend.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// sealed events rather than a second reading of the backend.

throw new ExecutionProtocolError("delegated a request that a later withOptions() superseded");
}
// Snapshotted before the invocation is marked consumed, so a failure to
// detach leaves nothing consumed either.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// detach leaves nothing consumed either.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 6 redundant comments. Inline suggestions to remove them below.

// is a compile-time claim: without this an admission could splice, reorder
// or empty the history in place, and every later admission, root-history
// validation and the replay itself would consume what it left behind.
// The events themselves are the retained graph's own, already sealed.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// The events themselves are the retained graph's own, already sealed.

*[Symbol.iterator]() {
// Registered in the *consumer's* scope, so halting a consumer that is
// still waiting takes the invocation down with it. After settlement this
// does nothing, which is what lets a completed handle be read again.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// does nothing, which is what lets a completed handle be read again.

},
});

// Whatever a handler returns is not an execution, so it is not read.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// Whatever a handler returns is not an execution, so it is not read.

// `#invocation in request` recognizes a value this class constructed
// without reading anything off it and without a registry to consult. It is
// also total: `in` on a primitive, on null, or on a proxy whose traps throw
// is guarded here, so nothing native or planted escapes.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// is guarded here, so nothing native or planted escapes.

throw new ExecutionProtocolError("delegated a request that a later withOptions() superseded");
}
// Snapshotted before the invocation is marked consumed, so a failure to
// detach leaves nothing consumed either.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// detach leaves nothing consumed either.

try {
return #invocation in value;
} catch {
// A revoked proxy, or one whose `has` trap refuses. Not one of ours.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant comment — restates what the code does.

Suggested change
// A revoked proxy, or one whose `has` trap refuses. Not one of ours.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant