Refs #16454 · found while re-verifying a finding I had flagged three times and never filed.
Context
migrateDeployment.mjs judges the census per service, which #16454 delivered to replace a fail-open union. The per-service scope — which keys a given service is answerable for — is resolved through buildConfigEnvDefaultsForTemplate, i.e. the leaves that service's config template declares.
Declaring a leaf and reading it are different things, and the root config base is inherited by every Neo server. So a leaf that only one service consumes is declared for all three, and the gate holds all three to it.
Measured on the canonical local plane, freshly:
orchestrator 3 of 3 present · retired key absent
kb-server 0 of 3 present · retired key PRESENT
mc-server 0 of 3 present · retired key PRESENT
(NEO_AI_ORCHESTRATOR_AUTHORITY_PROFILE, NEO_BACKUP_PATH, NEO_ORCHESTRATOR_RUNTIME_ACCESS_ENABLED.)
I had been reporting this as "either a live misconfiguration or a census over-declaration." It is neither: kb-server and mc-server are correct to lack these, and the gate is wrong to demand them.
The Problem
Consumer sweep, excluding templates and specs:
| leaf |
consumers |
orchestrator.authorityProfile |
ai/daemons/orchestrator/daemon.mjs, ai/daemons/orchestrator/Orchestrator.mjs — orchestrator only |
runtimeAccess.* |
six files, all under ai/daemons/orchestrator/ — orchestrator only |
backup.path |
ai/daemons/orchestrator/services/DeploymentStateBridgeService.mjs plus host-side ai/scripts/maintenance/* and ai/examples/* |
So plan against the live plane reports 12 blockers of which 10 are false: two missing-required-input-boot-blocking (authority profile on kb-server and mc-server) and eight missing-required-input across the two services, for leaves neither reads. The two genuine findings are the forbidden-env-present rows — the retired NEO_AUTH_PIN_FIRST_PROVIDER_SUBJECT still set on both.
That is worse than noise. The boot-blocking kind exists to put the one key an operator must see first at the top of a long list, and it is now firing on two services that boot fine. A gate whose loudest signal is false trains its reader to skip it.
Bounded claim: authorityProfile and runtimeAccess.* are unambiguously orchestrator-only. For backup.path I verified only that the orchestrator carries the explicit SERVICE_ENTRYPOINT (ai/deploy/docker-compose.yml:244) while kb/mc use their image defaults — a maintenance script exec'd ad hoc into another container would still read it, so that one leaf's disposition deserves its own check rather than my inference.
The Architectural Reality
ai/scripts/maintenance/migrateDeployment.mjs — resolveServiceScopes() builds each scope from the template's declared leaves. Correct question, wrong predicate.
ai/scripts/maintenance/deploymentMigrationCore.mjs — consumes serviceScopes and raises missing-required-input / missing-required-input-boot-blocking per service. The core is pure and only as right as the scope handed to it.
ai/scripts/lint/config-leaf-parity.json — $composeDefaultParity.census.requiredDeploymentInputs is a flat, profile-wide list with no per-service attribution. That flatness is what forces the resolver to invent an attribution, and the declared-leaf set was the only one available.
- This is the same class as
#16491 one layer over: there, an empty observation stood for two different facts; here, a declared leaf stands for a consumed one.
The Fix
Make the scope predicate consumption, not declaration — and if consumption is not derivable, make the census carry the attribution rather than having the resolver guess.
Two candidate shapes, and this ticket does not pick between them:
- Per-service required lists in the census. The parity document gains attribution, and the resolver reads it instead of deriving one. Authoritative and static; costs a census change with its own review.
- Derive consumption. Resolve each service's entrypoint and walk its reachable config reads. Precise and self-maintaining; materially more machinery, and it must fail closed when the walk cannot resolve.
Either way the refusal must keep its current fail-closed posture: an unattributable required key blocks rather than being silently dropped, which is the property #16454 established and this ticket must not weaken while fixing the false positives.
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback / Error Semantics |
Docs |
Evidence |
resolveServiceScopes() |
this ticket |
Scope = leaves the service consumes |
Consumption unresolvable ⇒ block, never silently narrow the scope — a narrower scope is fail-open |
source JSDoc |
plan on the live plane drops to the 2 genuine blockers |
$composeDefaultParity.census |
config-leaf-parity.json |
Either gains per-service attribution, or is documented as profile-wide with attribution derived elsewhere |
Attribution absent and underivable ⇒ the existing required-input-unattributable refusal stands |
census docs |
the flat list is no longer read as per-service truth |
missing-required-input-boot-blocking |
deploymentMigrationCore.mjs |
Fires only for a service that actually reads the leaf |
— |
source JSDoc |
no boot-blocking row for a service that boots |
Decision Record impact
none — corrects a predicate inside the migration bootstrap. ADR-0019 stays the census authority and is only read; nothing here relaxes the no-default requiredness it establishes.
Acceptance Criteria
Out of Scope
- The retired-key rows.
NEO_AUTH_PIN_FIRST_PROVIDER_SUBJECT being set on kb-server and mc-server is a genuine finding this ticket leaves standing, not a defect it fixes.
- Loosening the census. Reclassifying required inputs needs its own authority proof.
#16491's observation provenance — adjacent class, separate surface, already in flight.
Avoided Traps
- Dropping unattributable keys to make the plan clean. That trades ten false positives for an unknown number of false negatives, and a repair tool that under-reports is worse than one that over-reports.
- Hand-listing which service owns which key in the resolver. A hand-list stales exactly as the flat census did, and
#16453's AC already forbids that pattern for the sibling predicate.
- Assuming a name implies ownership.
NEO_AI_ORCHESTRATOR_* reads orchestrator-only and is — but I confirmed it by consumer sweep, because the same reasoning applied to NEO_DEPLOY_HOSTNAME would have put it on a Neo service rather than on ingress, where it actually lives.
Related
#16454 — delivered the per-service judgement this corrects; merged as fbef7375d4.
#16491 — the sibling class: one value standing for two different facts.
#16505 / #16453 — the admissibility predicate derives from the same flat census, so it likely inherits this attribution gap. Flagged for @neo-opus-grace rather than assumed.
Live latest-open sweep: latest 20 open issues checked; no equivalent. A2A claim sweep: recent messages across all read-states; no [lane-claim] on scope attribution.
Origin Session ID: 11695cce-9854-4be2-80c3-8ea4322298bf
Retrieval Hint: query_raw_memories("declared leaves versus consumed leaves per-service scope false boot-blocking migrateDeployment root config base inheritance")
Refs #16454 · found while re-verifying a finding I had flagged three times and never filed.
Context
migrateDeployment.mjsjudges the census per service, which #16454 delivered to replace a fail-open union. The per-service scope — which keys a given service is answerable for — is resolved throughbuildConfigEnvDefaultsForTemplate, i.e. the leaves that service's config template declares.Declaring a leaf and reading it are different things, and the root config base is inherited by every Neo server. So a leaf that only one service consumes is declared for all three, and the gate holds all three to it.
Measured on the canonical local plane, freshly:
(
NEO_AI_ORCHESTRATOR_AUTHORITY_PROFILE,NEO_BACKUP_PATH,NEO_ORCHESTRATOR_RUNTIME_ACCESS_ENABLED.)I had been reporting this as "either a live misconfiguration or a census over-declaration." It is neither: kb-server and mc-server are correct to lack these, and the gate is wrong to demand them.
The Problem
Consumer sweep, excluding templates and specs:
orchestrator.authorityProfileai/daemons/orchestrator/daemon.mjs,ai/daemons/orchestrator/Orchestrator.mjs— orchestrator onlyruntimeAccess.*ai/daemons/orchestrator/— orchestrator onlybackup.pathai/daemons/orchestrator/services/DeploymentStateBridgeService.mjsplus host-sideai/scripts/maintenance/*andai/examples/*So
planagainst the live plane reports 12 blockers of which 10 are false: twomissing-required-input-boot-blocking(authority profile on kb-server and mc-server) and eightmissing-required-inputacross the two services, for leaves neither reads. The two genuine findings are theforbidden-env-presentrows — the retiredNEO_AUTH_PIN_FIRST_PROVIDER_SUBJECTstill set on both.That is worse than noise. The boot-blocking kind exists to put the one key an operator must see first at the top of a long list, and it is now firing on two services that boot fine. A gate whose loudest signal is false trains its reader to skip it.
Bounded claim:
authorityProfileandruntimeAccess.*are unambiguously orchestrator-only. Forbackup.pathI verified only that the orchestrator carries the explicitSERVICE_ENTRYPOINT(ai/deploy/docker-compose.yml:244) while kb/mc use their image defaults — a maintenance script exec'd ad hoc into another container would still read it, so that one leaf's disposition deserves its own check rather than my inference.The Architectural Reality
ai/scripts/maintenance/migrateDeployment.mjs—resolveServiceScopes()builds each scope from the template's declared leaves. Correct question, wrong predicate.ai/scripts/maintenance/deploymentMigrationCore.mjs— consumesserviceScopesand raisesmissing-required-input/missing-required-input-boot-blockingper service. The core is pure and only as right as the scope handed to it.ai/scripts/lint/config-leaf-parity.json—$composeDefaultParity.census.requiredDeploymentInputsis a flat, profile-wide list with no per-service attribution. That flatness is what forces the resolver to invent an attribution, and the declared-leaf set was the only one available.#16491one layer over: there, an empty observation stood for two different facts; here, a declared leaf stands for a consumed one.The Fix
Make the scope predicate consumption, not declaration — and if consumption is not derivable, make the census carry the attribution rather than having the resolver guess.
Two candidate shapes, and this ticket does not pick between them:
Either way the refusal must keep its current fail-closed posture: an unattributable required key blocks rather than being silently dropped, which is the property
#16454established and this ticket must not weaken while fixing the false positives.Contract Ledger Matrix
resolveServiceScopes()planon the live plane drops to the 2 genuine blockers$composeDefaultParity.censusconfig-leaf-parity.jsonrequired-input-unattributablerefusal standsmissing-required-input-boot-blockingdeploymentMigrationCore.mjsDecision Record impact
none— corrects a predicate inside the migration bootstrap. ADR-0019 stays the census authority and is only read; nothing here relaxes the no-default requiredness it establishes.Acceptance Criteria
planagainst the canonical local plane reports the 2 genuine blockers (retiredNEO_AUTH_PIN_FIRST_PROVIDER_SUBJECTon kb-server and mc-server) and none of the 10 false ones.missing-required-input-boot-blockingnever fires for a service that boots without the key.backup.path's disposition is settled by its own check rather than inherited from this ticket's inference.Out of Scope
NEO_AUTH_PIN_FIRST_PROVIDER_SUBJECTbeing set on kb-server and mc-server is a genuine finding this ticket leaves standing, not a defect it fixes.#16491's observation provenance — adjacent class, separate surface, already in flight.Avoided Traps
#16453's AC already forbids that pattern for the sibling predicate.NEO_AI_ORCHESTRATOR_*reads orchestrator-only and is — but I confirmed it by consumer sweep, because the same reasoning applied toNEO_DEPLOY_HOSTNAMEwould have put it on a Neo service rather than on ingress, where it actually lives.Related
#16454— delivered the per-service judgement this corrects; merged asfbef7375d4.#16491— the sibling class: one value standing for two different facts.#16505/#16453— the admissibility predicate derives from the same flat census, so it likely inherits this attribution gap. Flagged for @neo-opus-grace rather than assumed.Live latest-open sweep: latest 20 open issues checked; no equivalent. A2A claim sweep: recent messages across all read-states; no
[lane-claim]on scope attribution.Origin Session ID: 11695cce-9854-4be2-80c3-8ea4322298bf
Retrieval Hint:
query_raw_memories("declared leaves versus consumed leaves per-service scope false boot-blocking migrateDeployment root config base inheritance")