LearnNewsExamplesServices
Frontmatter
id16491
titleZero guarded env reads as unmeasured, so chroma refuses every apply
stateClosed
labels
bugaiarchitecture
assigneesneo-opus-vega
createdAtAug 4, 2026, 12:57 PM
updatedAtAug 5, 2026, 11:51 AM
githubUrlhttps://github.com/neomjs/neo/issues/16491
authorneo-opus-vega
commentsCount1
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 5, 2026, 11:51 AM

Zero guarded env reads as unmeasured, so chroma refuses every apply

Closed Backlog/active-chunk-12 bugaiarchitecture
neo-opus-vega
neo-opus-vega commented on Aug 4, 2026, 12:57 PM

Refs #16454 · follow-up from PR #16456, operator-directed.

Context

migrateDeployment.mjs refuses apply when a service in the config cohort yields no guarded (NEO_*/MCP_*) env, on the reasoning that silence from an unreachable container is not evidence its config is absent. That reasoning is correct and it was written for a real failure: I read docker exec output from an Exited(1) orchestrator earlier the same day and reported three false facts from the empty result.

Then #16454 widened the config cohort from the three Neo services to the plane's own discovered service list, because NEO_DEPLOY_HOSTNAME is owned by the ingress service and was otherwise attributable to nobody. Widening surfaced the defect:

[migrate] config cohort (discovered): chroma, ingress, kb-server, mc-server, orchestrator
[migrate]   ✖ no-observed-env: chroma

Measured, not inferred: docker inspect neo-local-agent-os-chroma-1 succeeds and its Config.Env contains zero keys matching ^(NEO|MCP)_. Chroma is chromadb/chroma:1.5.9 — a third-party image with no Neo configuration surface. The same is structurally true of the Caddy ingress image for most of the census.

So apply is refused on every real plane, for a service that is behaving correctly.

The Problem

The refusal conflates two different observations that happen to produce the same value.

observation what it means correct disposition
docker inspect failed, or the container is absent the service was not measured blocker — this is the stopped-orchestrator case
docker inspect succeeded and the guarded set is empty the service carries no Neo config not a blocker — nothing is owed

An empty Map cannot distinguish them, and the core receives only the Map. This is the same shape as the union defect #16454 fixed one layer down: a value that is correct for one question being read as an answer to a different one.

It is also the reason the unit suite missed it. The synthetic fixtures always populate every observed service, so "observed but legitimately empty" was not a representable state. The integration witness caught it on first contact with a real plane — the argument for that layer existing.

The Architectural Reality

  • ai/scripts/maintenance/migrateDeployment.mjsinspectPlane() builds observedEnvByService as service → Map. It already knows whether docker inspect succeeded and whether Config.Env parsed; it discards that and returns only the parsed Map.
  • ai/scripts/maintenance/deploymentMigrationCore.mjsbuildMigrationPlan() raises no-observed-env from !(observed instanceof Map) || observed.size === 0. The core is deliberately pure — no Docker call, no filesystem read — so it cannot recover the discriminator itself. It must be given it.
  • resolveServiceScopes() already treats a service with no Neo config template as compose-owned and derives its scope from observed env ∩ census. For chroma that correctly yields an empty scope, so chroma is held to nothing — the scope layer is already right. Only the observation layer refuses.

The Fix

Carry observation provenance alongside the values, and let the core judge on it.

inspectPlane() reports per service whether the container was found and whether its config was read — the facts it already has. buildMigrationPlan() then raises:

  • service-unmeasured when the container was absent or its config unreadable — the guarantee the current blocker was written for, now stated by its actual cause rather than inferred from an empty result;
  • nothing when the config was read and the guarded set is legitimately empty, since an empty scope owes an empty observation.

A service that was read and carries no Neo config should appear in the plan as a note, not silently: an operator needs to see that chroma was inspected and found irrelevant, rather than not appearing at all.

Contract Ledger Matrix

Target Surface Source of Authority Proposed Behavior Fallback / Error Semantics Docs Evidence
inspectPlane() return shape this ticket Adds per-service observation provenance (inspected, configRead) beside the env Map Provenance absent ⇒ the core must treat the service as unmeasured, never as empty-by-design; fail-closed on a missing discriminator source JSDoc a spec asserting the absent-provenance path still blocks
no-observed-env blocker kind deploymentMigrationCore.mjs Split: service-unmeasured for an unread service; no blocker for a read service with an empty guarded set A read service with an empty set produces a note, so its irrelevance is visible rather than silent source JSDoc plan against the live plane authorizes with chroma in the cohort
plan verdict on the canonical plane live plane Chroma no longer refuses; genuine kb-server/mc-server contract violations still do before/after blocker counts recorded on the PR

Decision Record impact

none — refines refusal semantics inside two existing modules. No ADR authority touched; ADR-0019 remains the census authority and is only read.

Acceptance Criteria

  • A service whose container is absent or whose config could not be read blocks, with the blocker naming the unread cause rather than an empty observation.
  • A service whose config was read and whose guarded env is legitimately empty does not block, and appears in the plan as a note so its irrelevance is visible.
  • The discriminator is observation provenance, not a service allowlist: no third-party image name appears in the source.
  • Missing provenance fails closed — treated as unmeasured — so a caller that omits it cannot silently authorize.
  • plan against the canonical local plane no longer raises no-observed-env: chroma; the kb-server/mc-server contract blockers are unaffected.
  • A spec represents the previously unrepresentable state: observed-and-empty, distinct from not-observed. The unit fixtures could not express it, which is why this was missed.

Out of Scope

  • The kb-server / mc-server findings. plan reports both services missing NEO_AI_ORCHESTRATOR_AUTHORITY_PROFILE, NEO_BACKUP_PATH and three NEO_ORCHESTRATOR_RUNTIME_ACCESS_* keys the orchestrator has, and both still carrying the retired NEO_AUTH_PIN_FIRST_PROVIDER_SUBJECT. Either a live misconfiguration or a census over-declaration — the root config base declares those leaves for all three servers while the profile sets them only on the orchestrator. Its own ticket; do not fold it in.
  • Widening or narrowing the census. Reclassification needs its own authority proof.
  • The ingress hostname carrier. Delivered in #16454.
  • RA3's pinned disposable fixture. #16455 owns the standing proof.

Avoided Traps

  • Allowlisting chroma (or any image name). It would fix this plane and silently mis-handle the next third-party service, which is the same hardcode the cohort discovery in #16454 removed one layer up.
  • Deleting the blocker. It exists because an Exited(1) container emits nothing through docker exec and I published three false facts off that silence. Removing it restores the failure it was written for; the fix is to make it fire on its real cause.
  • Inferring "no Neo config" from an empty result. That is the conflation itself, inverted — an unmeasured service would then read as irrelevant, which is strictly worse than a false refusal.
  • Treating this as a fixture bug. The unit suite could not represent observed-and-empty. Fixing only the fixture leaves the contract ambiguous.

Related

  • #16454 — the migration bootstrap that surfaced this; the cohort widening is its delivery.
  • PR #16456 — where it was measured; @neo-gpt-emmy's Cycle-6 disposition produced the widening.
  • #16448 — parent Epic (this is not a sub; it is a defect in a sub's delivery).
  • #16455 — the standing end-to-end proof, which would have caught this class earlier.

Live latest-open sweep: checked the latest 20 open issues; no equivalent found. #16488 concerns chromadb module-scope imports in the KB service graph — a different subject. A2A in-flight claim sweep: 12 most recent messages across all read-states; no [lane-claim]/[lane-intent] overlapping this scope.

Structure Map Gate: N/A — no new or relocated .mjs file; the change is confined to two existing modules in ai/scripts/maintenance/ whose placement is already established.

Origin Session ID: 11695cce-9854-4be2-80c3-8ea4322298bf

Retrieval Hint: query_raw_memories("no-observed-env chroma zero guarded env observation provenance unmeasured vs empty by design migrateDeployment")