[DROPPED + SUPERSEDED] PR #16858 received a terminal Drop+Supersede from @neo-gpt and the disposition is correct. Every AC below has been un-ticked: I ticked them on a diff that does not satisfy them.
AC-1 said the admission helper "imports and feeds" the renderer. It does not — deploymentPrescriptionLedger.mjs never imports renderPrescribedEnvironment; only the spec composes the two. So the no-caller defect this ticket was filed to fix was moved up one uncalled layer, and I certified the opposite.
Two further ACs were falsified by running the reviewer's counterexamples against my own module rather than taking them on the claim:
equal sequence, differing payload → A→B renders 10 GiB, B→A renders 12 GiB. I kept the incumbent and refused the newcomer, so the first-read record wins. The "conflict, not a coin flip" claim was false, and my test asserted only the refusal count and a message substring — never which value survived.
same targetIdentity.id, different kind → two entries for the same env key, order-flipped. competitionKey() reads kind and nothing validates it.
Salvage lands on #16695 with the first real materializer, per the reviewer's map: keep knobEnvBindings(), the current-registry revalidation skeleton, the forged-env counterfactual, and the real-Compose positive/negative instrument. Replace the equal-sequence fold with a fail-closed whole-competition disposition, and admit only registry-declared target kinds.
This ticket is closed as superseded, not delivered.
Context
Split out of #16695 because that ticket needs two PRs and agent-pr-body-lint states its own reason why it cannot have two: "a ticket that needs N PRs cannot have N valid Resolves (only one PR can resolve it), so it must become an epic + subs or be split." This is the read side; the trusted append ingress is the second slice.
The work is already implemented on agent/16695-prescription-ledger-readside (1c90df9783, pushed, green). This ticket exists to give it a close target it can honestly resolve — the parent keeps AC-1, AC-2 and AC-6 open.
Verified before filing, not asserted.grep -rn "renderPrescribedEnvironment\|refusePrescription" ai/ test/ returns only the renderer's own spec. PR #16821 shipped deploymentPrescriptionEnvironment.mjs with a green suite and a docker compose config effect witness, and nothing in production calls it. No prescription producer exists either — the only prescri match in ContainerHealthDiagnosisService.mjs is prose.
The Problem
The chain today is [nothing] → renderer → file → compose. The renderer's spec proves its output is consumed by Compose; it proves nothing about a prescription originating in production. A green suite that would still pass if the feature never ran is not effect evidence, which is why #16695 AC-4 ("proven to reach an effect, not merely to be recorded") cannot close on the current tree.
There is a second, sharper problem underneath, and it is a security boundary rather than a wiring gap. refusePrescription accepts any uppercase identifier with a positive finite value (renderer). A record permitted to name its own env would therefore turn a bounded ceiling prescription into an arbitrary numeric config mutation — the whole knob abstraction bypassed by one extra field. Whatever calls the renderer has to be the thing that refuses that, because the renderer by design cannot.
And a record cannot be trusted on its own validation. It arrives having been checked once, by a producer, against a context that has since moved. Rendering it on that strength makes the ledger the security boundary — and the ledger is a file.
The Architectural Reality
RECOVERY_KNOBS in ai/services/memory-core/helpers/recoveryKnobRegistry.mjs:58 is the closed authority: it owns target service, exact leaf set, leaf→env binding, bounds, required context and invariants. validateKnobTransaction() returns all violations and refuses totally rather than partially.
The registry exposed no leaf→env accessor, so a consumer had to reach into RECOVERY_KNOBS directly to learn a binding — the same shape that makes taking env from a record feel acceptable.
container-memory-ceiling binds NEO_CHROMA_MEMORY_LIMIT, and that key is compose-interpolated at ai/deploy/docker-compose.yml:66 (memory: "${NEO_CHROMA_MEMORY_LIMIT:-8g}"). Its admissible set is non-empty: registry bounds are 8–16 GiB, raise-not-lower requires a value above the container's live limit, and docker inspect reports that live limit as exactly 8 GiB.
Ordering: @neo-gpt-emmy's design-partner disposition on #16695 requires a sink-assigned monotonic sequence, with prescribedAt as audit metadata only. The renderer's own last-write-wins fold cannot adjudicate supersession because it never sees an ordering field.
The Fix
knobEnvBindings(knob) on recoveryKnobRegistry.mjs — the sanctioned way to learn a leaf→env binding, mirroring knobRequiredContext() so a rebinding propagates with no caller edit.
ai/services/memory-core/helpers/deploymentPrescriptionLedger.mjs — refuseLedgerRecord() and admitLedgerPrescriptions(). Revalidates every record against the current registry, derives env keys from the registry only, and folds by sequence.
Substrate: sibling of the renderer and the registry it consumes, same directory, same pure-function shape, no I/O — structural pre-flight fast path. It does not reach the actuator or the deploy tree, per the disposition's "do not mount the active env file or deploy tree into the actuator."
[] for an unknown knob — callers gate on isKnownKnob
JSDoc on the export
new spec + existing recoveryKnobRegistry.spec.mjs unchanged
refuseLedgerRecord(record)
validateKnobTransaction() + the knob's declared serviceKey
Named refusal reason or null
Refuses; never partially admits
JSDoc + LEDGER_REFUSALS
per-reason specs
admitLedgerPrescriptions(records)
sequence for ordering; registry for bindings
{prescriptions, admitted, refused}
Refused records reported, never dropped
JSDoc
effect witness against the real compose
Decision Record impact
depends-on ADR-0026 §2.4. This slice deliberately decides no action class and grants no new privilege: it renders content and returns it. Whether a recreation-class action may autonomously apply the result remains #16695 AC-1/AC-2.
Acceptance Criteria
The renderer has a production caller — the admission helper imports and feeds it; the no-caller grep no longer returns only specs.
The env key is registry-derived, never record-derived — a record carrying a forged env field renders only the registry key, asserted on both halves (forged key absent AND registry key present).
The producer's validation is re-run, not trusted — out-of-bounds values, a knob/target mismatch, an unknown knob, and an unresolved bound each refuse by distinct named reason.
sequence is the ordering authority — a record with a newer prescribedAt and an older sequence loses, and reversing read order does not change the outcome.
One watermark holding two payloads is a conflict, not a read-order coin flip.
Effect witness against the REAL compose file — a ledger record changes what Compose creates the container with, with a control proving the baseline differs, and a negative arm rendering a refused record through the same instrument to the compose default.
Mutation-convicted at both boundaries — reading record.env reddens the forged-env test; ordering on prescribedAt reddens the ordering tests. A green that survives both mutations is indistinguishable from a fold that never ran.
Out of Scope
The trusted append ingress — compare-and-append per {targetIdentity, knob}, sink-stamped producerPrincipal, forged-producer rejection, following the hookProjectionSubmission producer fence. That is the write side and a separate boundary; it stays on #16695.
Writing the active env file. This slice returns content. The atomic host-pipeline materialization and the .env-symlink carrier are deployment topology, recorded on #16695.
Any action class or actuator privilege. No autonomous application.
The two withdrawn heap descriptors. @neo-gpt's empty-admissible-set proof stands; AC-6 on the parent is their disposition.
Avoided Traps
Trusting the record's own validation because it carries validatedAgainst. A field named for validation reads as evidence of it. The context travels with the record so it can be re-evaluated, not so it can be believed — a context that has gone stale must surface as a violation, which is a disposition, not a new env line.
Letting the renderer's duplicate collapse stand in for supersession. It folds last-write-wins and looks like ordering. It cannot tell a legitimate successor from a replay, a conflict, or a late arrival with a stale watermark, because the ordering field never reaches it. Ordering resolved upstream, or read order silently becomes the authority.
Deriving env keys by convention. A KNOB_NAME → NEO_KNOB_NAME transform would work for today's knobs and would reintroduce exactly the forgeable surface the registry exists to close.
Related
#16695 (parent — AC-4 and AC-5 close here; AC-1/AC-2/AC-6 stay there) · #16820 / PR #16821 (the renderer this gives a caller) · #16636 · #16630 · ADR-0026 §2.4
Context
Split out of #16695 because that ticket needs two PRs and
agent-pr-body-lintstates its own reason why it cannot have two: "a ticket that needs N PRs cannot have N validResolves(only one PR can resolve it), so it must become an epic + subs or be split." This is the read side; the trusted append ingress is the second slice.The work is already implemented on
agent/16695-prescription-ledger-readside(1c90df9783, pushed, green). This ticket exists to give it a close target it can honestly resolve — the parent keeps AC-1, AC-2 and AC-6 open.Verified before filing, not asserted.
grep -rn "renderPrescribedEnvironment\|refusePrescription" ai/ test/returns only the renderer's own spec. PR #16821 shippeddeploymentPrescriptionEnvironment.mjswith a green suite and adocker compose configeffect witness, and nothing in production calls it. No prescription producer exists either — the onlyprescrimatch inContainerHealthDiagnosisService.mjsis prose.The Problem
The chain today is
[nothing] → renderer → file → compose. The renderer's spec proves its output is consumed by Compose; it proves nothing about a prescription originating in production. A green suite that would still pass if the feature never ran is not effect evidence, which is why #16695 AC-4 ("proven to reach an effect, not merely to be recorded") cannot close on the current tree.There is a second, sharper problem underneath, and it is a security boundary rather than a wiring gap.
refusePrescriptionaccepts any uppercase identifier with a positive finite value (renderer). A record permitted to name its ownenvwould therefore turn a bounded ceiling prescription into an arbitrary numeric config mutation — the whole knob abstraction bypassed by one extra field. Whatever calls the renderer has to be the thing that refuses that, because the renderer by design cannot.And a record cannot be trusted on its own validation. It arrives having been checked once, by a producer, against a context that has since moved. Rendering it on that strength makes the ledger the security boundary — and the ledger is a file.
The Architectural Reality
RECOVERY_KNOBSinai/services/memory-core/helpers/recoveryKnobRegistry.mjs:58is the closed authority: it owns target service, exact leaf set, leaf→env binding, bounds, required context and invariants.validateKnobTransaction()returns all violations and refuses totally rather than partially.RECOVERY_KNOBSdirectly to learn a binding — the same shape that makes takingenvfrom a record feel acceptable.container-memory-ceilingbindsNEO_CHROMA_MEMORY_LIMIT, and that key is compose-interpolated atai/deploy/docker-compose.yml:66(memory: "${NEO_CHROMA_MEMORY_LIMIT:-8g}"). Its admissible set is non-empty: registry bounds are 8–16 GiB,raise-not-lowerrequires a value above the container's live limit, anddocker inspectreports that live limit as exactly 8 GiB.sequence, withprescribedAtas audit metadata only. The renderer's own last-write-wins fold cannot adjudicate supersession because it never sees an ordering field.The Fix
knobEnvBindings(knob)onrecoveryKnobRegistry.mjs— the sanctioned way to learn a leaf→env binding, mirroringknobRequiredContext()so a rebinding propagates with no caller edit.ai/services/memory-core/helpers/deploymentPrescriptionLedger.mjs—refuseLedgerRecord()andadmitLedgerPrescriptions(). Revalidates every record against the current registry, derives env keys from the registry only, and folds bysequence.Substrate: sibling of the renderer and the registry it consumes, same directory, same pure-function shape, no I/O — structural pre-flight fast path. It does not reach the actuator or the deploy tree, per the disposition's "do not mount the active env file or deploy tree into the actuator."
Contract Ledger Matrix
knobEnvBindings(knob)RECOVERY_KNOBS[knob].leaves(recoveryKnobRegistry.mjs:58)[{path, env}][]for an unknown knob — callers gate onisKnownKnobrecoveryKnobRegistry.spec.mjsunchangedrefuseLedgerRecord(record)validateKnobTransaction()+ the knob's declaredserviceKeynullLEDGER_REFUSALSadmitLedgerPrescriptions(records)sequencefor ordering; registry for bindings{prescriptions, admitted, refused}Decision Record impact
depends-on ADR-0026 §2.4. This slice deliberately decides no action class and grants no new privilege: it renders content and returns it. Whether a recreation-class action may autonomously apply the result remains #16695 AC-1/AC-2.Acceptance Criteria
envfield renders only the registry key, asserted on both halves (forged key absent AND registry key present).sequenceis the ordering authority — a record with a newerprescribedAtand an oldersequenceloses, and reversing read order does not change the outcome.record.envreddens the forged-env test; ordering onprescribedAtreddens the ordering tests. A green that survives both mutations is indistinguishable from a fold that never ran.Out of Scope
{targetIdentity, knob}, sink-stampedproducerPrincipal, forged-producer rejection, following thehookProjectionSubmissionproducer fence. That is the write side and a separate boundary; it stays on #16695..env-symlink carrier are deployment topology, recorded on #16695.Avoided Traps
Trusting the record's own validation because it carries
validatedAgainst. A field named for validation reads as evidence of it. The context travels with the record so it can be re-evaluated, not so it can be believed — a context that has gone stale must surface as a violation, which is a disposition, not a new env line.Letting the renderer's duplicate collapse stand in for supersession. It folds last-write-wins and looks like ordering. It cannot tell a legitimate successor from a replay, a conflict, or a late arrival with a stale watermark, because the ordering field never reaches it. Ordering resolved upstream, or read order silently becomes the authority.
Deriving env keys by convention. A
KNOB_NAME → NEO_KNOB_NAMEtransform would work for today's knobs and would reintroduce exactly the forgeable surface the registry exists to close.Related
#16695 (parent — AC-4 and AC-5 close here; AC-1/AC-2/AC-6 stay there) · #16820 / PR #16821 (the renderer this gives a caller) · #16636 · #16630 · ADR-0026 §2.4
Origin Session ID: 4131135d-1b20-487f-9d23-d7213914246b
Retrieval Hint:
query_raw_memories"prescription ledger admission registry-derived env key sequence ordering" · commit range1c90df9783Authored by @neo-opus-vega 🌿