Skip to content

Rename durable publication identity → JournalProvenance and make preservation explicit #424

Description

@taras

Summary

Rename the durable publication-identity surface introduced in #415 to
JournalProvenance, and make provenance preservation across a trusted journal
wrapper explicit at the wrapping site.

The exact-backend mechanism remains load-bearing. The vocabulary and wrapping
seam change so the code says what the mechanism proves and makes every transfer
of that proof visible.

This is a security-boundary refactor, not only a symbol rename. It must preserve
the existing authority and transaction behavior exactly.

What JournalProvenance proves

When a live Workspace effect publishes, its filtered Yield must land in the
authoritative journal of the specific WorkflowRun whose mutation is being
transacted. Otherwise the filesystem mutation could commit to run A while the
journal result is sent to an InMemoryStream, another run's journal, or a
look-alike wrapper on another connection. That would break:

one expansion → one effect → one caller-owned transaction

JournalProvenance is a non-operational, equality-only witness that a live
publication stream descends from the exact journal backend selected by the
provider. It grants no append or read capability, does not mean that journal
contents are immutable, and is meaningful only because the provider retains
the witness it established and later requires exact equality.

The current implementation associates a fresh opaque witness with the
authoritative stream. The Deno provider retains it for the WorkflowRun journal
and refuses a Workspace publication whose provenance differs before opening the
transaction. WAC11 proves that an in-memory stream, another run's journal, a
copied property, and an unproven wrapper cannot reach mutation or publication.

Why change it

The current names describe the mechanism as a generic publication identity:

  • claim…Identity does not say that the call creates and associates a fresh
    provenance witness.
  • publicationIdentity does not say which concrete boundary the value proves.
  • guardDurableStream() currently preserves provenance for every canonical
    guard through an implicit inherit(source, target) side effect.

JournalProvenance names the narrower fact being proven: lineage from the
provider-selected journal backend. establish… describes minting and
associating the witness. preserve… makes propagation through the trusted
secret-filter wrapper visible without making the generic guard responsible for
provenance policy.

Vocabulary and API

Current Replacement
class PublicationIdentity { #opaque } class JournalProvenance { #opaque }
type DurablePublicationIdentity type JournalProvenance
claimDurablePublicationIdentity(stream) establishJournalProvenance(stream)
internal durablePublicationIdentity(stream) internal getJournalProvenance(stream)
internal blanket inherit(source, target) explicit preserveJournalProvenance(source, target)
RunConnectionLease.journalIdentity journalProvenance
WorkspaceCoordinationAuthority.publicationIdentity journalProvenance
validateJournal(database, identity) validateJournalProvenance(database, provenance)

Establishing the authoritative journal reads:

lease.journalProvenance = establishJournalProvenance(journal);

The coordination boundary reads:

connections.validateJournalProvenance(
  database,
  authority.journalProvenance,
);

The refusal should describe the failed fact rather than a forged capability:

the live Workspace journal does not have the provenance of the selected WorkflowRun.

These exported symbol renames are source-level breaking changes for direct
consumers of @executablemd/durable-streams. That is acceptable before release,
but it must be recorded accurately. Stable context API names such as
executablemd.workflow.workspace.coordination* do not change, so loaded-copy
composition keys remain compatible.

Explicit preservation at the secret boundary

guardDurableStream() remains the generic, policy-neutral pre-persistence
guard. An ordinary guard does not automatically preserve JournalProvenance.

The trusted secret-filter wrapping site explicitly preserves it:

const guarded = guardDurableStream(stream, gate);
return preserveJournalProvenance(stream, guarded);

Equivalently, it may be composed directly:

return preserveJournalProvenance(
  stream,
  guardDurableStream(stream, gate),
);

Preservation is visible composition, not new authority. It can preserve only
the provenance already associated with the exact source object. An unproven
source produces no proven target, and a separately loaded package copy cannot
read or transfer the canonical copy's association.

Nested trusted wrappers preserve the same exact witness. A generic wrapper,
custom look-alike, copied property, wrapper from another loaded copy, or wrapper
whose source has no established provenance remains unproven.

Architecture decision: retain a narrow weak-provenance exception

The association remains a canonical-module-local WeakMap keyed by exact
stream object identity. This is an approved, narrowly scoped exception to the
ordinary rule that execution state is operation-owned and module-scoped
registries are forbidden.

JournalProvenance is security metadata about an exact stream, not operational or
lifecycle state. The exception applies only when all of these constraints hold:

  • the association carries a non-operational, equality-only provenance witness
    and grants no append, read, execution, publication, or reconciliation
    capability;
  • keys are exact objects held weakly, the association cannot enumerate them,
    and it cannot keep a stream alive or outlive that stream;
  • public structural metadata would be forgeable or copyable and therefore
    cannot preserve the security property;
  • a separately loaded package copy must neither read nor transfer the canonical
    copy's association;
  • establishment and transfer occur only through the canonical module's explicit
    establishJournalProvenance() and preserveJournalProvenance() functions;
    and
  • the weak association retains no unrelated execution, lifecycle, journal
    content, or provider state.

This exception is not a general allowance for module-scoped caches or
registries. It exists because the exact-object, anti-forgery and loaded-copy
boundary is the provenance contract itself, while weak keys make the
association's lifetime subordinate to the stream objects that own it.

A scope-owned alternative would have to thread new authority through core,
durable execution, secret wrapping and Workspace coordination. A wrapper-private
alternative cannot also refuse duplicate establishment on an arbitrary stream
without retaining an association elsewhere or changing the stream contract.
Neither adds a security property the weak association lacks, and both enlarge
the trusted surface.

The implementation therefore preserves the current exact-object mechanism,
renames it to JournalProvenance, removes implicit propagation from
guardDurableStream(), and makes the trusted secret-filter boundary call
preserveJournalProvenance() explicitly. The Planner and Implementor inherit
this decision; they do not reopen it. The same PR must record the exception in
architecture.md before describing the implementation as complete.

Scope and blast radius

  • packages/durable-streams/guard.ts
  • packages/durable-streams/effect.ts
  • packages/durable-streams/live-coordinator.ts
  • packages/durable-streams/mod.ts
  • packages/core/src/secrets/policy.ts
  • packages/workflow/src/deno/connections.ts
  • packages/workflow/src/deno/workspace/effect.ts
  • packages/workflow/src/workspace/effect.ts
  • focused durable-stream, secret-policy, Workspace coordination and
    physical-loaded-copy tests
  • architecture.md, specs/workflow-spec.md,
    specs/workflow-workspace-spec.md, and the affected DLC/WAC conformance rows

This does not change the journal representation, filtering policy, transaction
protocol, replay protocol, stable context names, or public workflow behavior.

Sequencing

Implement as its own focused PR after #419 lands. Do not grow the #365 crash
proof slice.

Acceptance criteria

  • JournalProvenance is defined precisely in architecture terminology and
    used consistently in code, types, diagnostics and specifications.
  • Symbols are renamed according to the table, with no compatibility aliases
    unless explicitly approved for the pre-release API.
  • The architecture decision is settled here: retain the
    canonical-module-local weak provenance association under the narrow
    exception above.
  • architecture.md records that exception and its constraints in the
    implementation PR.
  • Establishing provenance creates one fresh witness, associates it with one
    exact authoritative stream, and refuses duplicate establishment.
  • guardDurableStream() no longer preserves provenance implicitly.
  • The trusted secret-filter wrapper explicitly preserves provenance.
  • An ordinary guard, arbitrary wrapper, copied field, foreign journal,
    in-memory stream and separately loaded package copy remain unproven.
  • Nested trusted wrappers preserve the same exact provenance.
  • An unproven source cannot make a proven target through the preservation
    seam.
  • Provider validation happens before transaction, savepoint, mutation or
    publication work.
  • Secret rejection and cancellation retain their existing pre-persistence
    behavior.
  • Complete replay remains independent of live provenance and provider
    coordination.
  • WAC1, WAC5, WAC11, DLC8 and the physical loaded-copy guard regressions
    pass without behavioral weakening.
  • deno task lint, deno task check, deno task check:jsr, affected Deno
    tests, and fresh Deno/Node/Bun CI pass.

Follow-up to #415. Related to the #365 stack.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions