352 lines
25 KiB
Markdown
352 lines
25 KiB
Markdown
---
|
|
status: draft
|
|
system: executors
|
|
requirements:
|
|
- REQ-EXECUTORS-EXECUTOR-PROFILE-ENV-PRECEDENCE-001
|
|
created: 2026-08-17
|
|
owners:
|
|
- tbd
|
|
---
|
|
# Executor-Profile Environment Precedence System Design Part 4
|
|
|
|
## Purpose and boundaries
|
|
|
|
This design preserves the technical source detail for `REQ-EXECUTORS-EXECUTOR-PROFILE-ENV-PRECEDENCE-001` during migration.
|
|
|
|
## Requirement mapping
|
|
|
|
| Requirement | Design section |
|
|
| --- | --- |
|
|
| `REQ-EXECUTORS-EXECUTOR-PROFILE-ENV-PRECEDENCE-001` | [Migrated source detail](#migrated-source-detail) |
|
|
|
|
## Migrated source detail
|
|
|
|
### Determinism and ordering
|
|
|
|
- **AC-27** WHEN the same definition set is resolved twice, THE SYSTEM SHALL return an identical
|
|
environment map and a deeply identical override-record sequence. "Deeply identical" includes the
|
|
contents and order of `WinningOrigins` and `LosingOrigins` inside every record, not just the
|
|
sequence of records — a test comparing records SHALL compare their nested slices element by
|
|
element.
|
|
- **AC-28** THE SYSTEM SHALL order override records by `OverrideRecord.Key` ascending. Because
|
|
AC-20 emits exactly one record per key, `Key` is unique across the sequence and is therefore a
|
|
total order on its own; no secondary record-level sort key exists or is needed. Ordering WITHIN a
|
|
record is specified separately and is what AC-27 depends on:
|
|
- `WinningOrigins` SHALL be sorted by origin string ascending;
|
|
- `LosingOrigins` SHALL be sorted by `Origin` ascending.
|
|
|
|
`Origin` alone TOTALLY orders both lists, and it does so only because AC-20 makes them
|
|
de-duplicated sets: with each origin appearing at most once, no two entries can compare equal,
|
|
so no tiebreak is needed or possible. This sentence depends on the set rule and would be FALSE
|
|
without it — under a bag reading, two discarded identities sharing an origin would produce two
|
|
entries that `Origin` cannot order, and AC-27's element-by-element comparison would rest on an
|
|
undefined order between them. `Tier` is additionally functionally determined by `Origin` through
|
|
`TierForOrigin`, so a tiebreak on `Tier` would carry no information even if entries could
|
|
collide.
|
|
- **AC-29** THE SYSTEM SHALL sort definitions by the NAMED columns `Key`, then `Origin`, then
|
|
`SecretID`, then `WorkspaceID`, then `Literal`, all ascending, before walking them. This is a
|
|
total order over every `Definition` field that can affect the outcome, which the current
|
|
three-column sort at `environment.go:97` is not. WHEN several definitions for one key share a
|
|
`definitionIdentity`, THE SYSTEM SHALL select the first under that order, and input slice order
|
|
SHALL NOT affect which one survives.
|
|
- **AC-29a** WHEN a definition set produced a deterministic result under the current three-column
|
|
sort, THE SYSTEM SHALL produce that same result under the five-column sort. Adding sort columns
|
|
can only break ties previously left to input order; it cannot reorder definitions that already
|
|
differed on an earlier column.
|
|
- **AC-29b** WHEN two definitions share `Key`, `Origin` and a non-empty `SecretID` but differ in
|
|
`WorkspaceID`, THE SYSTEM SHALL select the one with the lexicographically smaller `WorkspaceID`,
|
|
deterministically. This case merges rather than conflicts (both have identity
|
|
`"secret:" + SecretID`), and the surviving `WorkspaceID` decides whether
|
|
`resolveEnvironmentDefinition` reveals the secret globally or through `RevealForWorkspace`, so
|
|
leaving it to input order would make the revealed VALUE order-dependent.
|
|
- **AC-30** WHEN definitions are supplied in a different input order, THE SYSTEM SHALL produce the
|
|
same result: the same winner, the same revealed value, the same error, and the same override
|
|
records including their nested slices.
|
|
|
|
### Nil, empty, and error behaviour
|
|
|
|
- **AC-31** WHEN the definition list is empty or nil, THE SYSTEM SHALL return an empty (non-nil)
|
|
map, a nil error, and a nil `[]OverrideRecord` per AC-26's pinned convention. The empty map
|
|
matches today's behaviour, which allocates before the loop and returns it.
|
|
- **AC-32** WHEN a definition's `Key` is empty or whitespace-only, THE SYSTEM SHALL skip it,
|
|
unchanged from today (`environment.go:111`).
|
|
- **AC-33** WHEN a secret-backed definition is selected and the reveal callback is nil, THE SYSTEM
|
|
SHALL return a `*SecretError` naming the key and origin, unchanged from today.
|
|
- **AC-34** WHEN a secret-backed definition is selected and the reveal callback returns an error,
|
|
THE SYSTEM SHALL return a `*SecretError` naming the key and origin, unchanged from today.
|
|
- **AC-35** WHEN a definition carries an origin string that this spec's tier table does not name,
|
|
`TierForOrigin` SHALL return `TierAuthoritative` and THE SYSTEM SHALL apply peer-conflict
|
|
behaviour to it. This covers the empty origin string and any string that is not a repository
|
|
origin under the normative rule (for example `repositoryapp`), both of which fall through to
|
|
the unrecognised default rather than into the repository row.
|
|
- **AC-36** IF more than one key conflicts, THEN THE SYSTEM SHALL report the lexicographically
|
|
first conflicting key, unchanged from today (`environment.go:138`).
|
|
- **AC-37** WHEN precedence selects a winner, THE SYSTEM SHALL reveal secrets only for the
|
|
surviving definition. A discarded definition SHALL NOT be revealed. Because AC-9 blocks every
|
|
mixed-secret case, a discarded definition is always literal in practice; this criterion pins
|
|
that no reveal is attempted for a loser.
|
|
|
|
### Boundary values and defaults
|
|
|
|
- **AC-38** WHERE a profile entry has an empty `Value` and an empty `SecretID`, no definition for
|
|
that key SHALL reach the resolver from that profile, and its absence SHALL NOT count as an
|
|
override. This SHALL hold on ALL THREE assembly paths. Two already drop the entry and are
|
|
unchanged; the third does not drop it today and SHALL be changed to:
|
|
- the LIFECYCLE agent-profile path drops the entry explicitly (`appendProfileDefinitions`,
|
|
`environment_resolution.go:134`) — UNCHANGED;
|
|
- the LIFECYCLE executor-profile path drops it explicitly
|
|
(`executorProfileEnvironmentDefinitions`, the `value.SecretID == "" && value.Value == ""`
|
|
skip, `environment_resolution.go:257-259`) — UNCHANGED;
|
|
- the PRIMARY ORCHESTRATOR path (`Executor.taskEnvironmentSources`,
|
|
`task_environment.go:66-70`) appends every `ProfileEnvVar` unconditionally today and **SHALL
|
|
be changed to skip an entry whose `SecretID` and `Value` are both empty**, using the same
|
|
condition as the lifecycle executor-profile path.
|
|
|
|
**This reverses an instruction earlier drafts of this spec gave, and the reason is recorded so
|
|
it is not reverted again.** Those drafts asserted the orchestrator path "cannot receive such an
|
|
entry, because `validateEnvVarValue` (`profile_crud.go`) rejects it at profile save time", and
|
|
told the builder not to test for a drop there. That justification is FALSE: `validateEnvVarValue`
|
|
is agent-profile-only, the vars on this path are EXECUTOR-profile vars, and the executor-profile
|
|
save path enforces no value-or-secret rule at all (see "The two profile kinds are NOT validated
|
|
the same way"). A blank executor-profile value is therefore savable from the UI, HTTP, WS and
|
|
agent-driven MCP, and under tier precedence it would classify Tier 1, beat a real Tier 2
|
|
agent-profile value, and launch the agent with the key set to the empty string.
|
|
DO write a test asserting the drop at the orchestrator assembly step (AC-38b). The save-time
|
|
rejection test remains valid, but ONLY as a statement about agent profiles.
|
|
- **AC-38a** An override to the EMPTY STRING is therefore not expressible from a profile, on any
|
|
path. Lifting that limitation is out of scope.
|
|
The guarantee now rests on the ASSEMBLY-TIME SKIP being present on all three paths (AC-38), not
|
|
on save-time validation. That distinction is the whole of F25: save-time validation covers only
|
|
agent profiles, so an invariant resting on it was false for executor profiles.
|
|
- **AC-38b** WHEN `Executor.taskEnvironmentSources` receives a `ProfileEnvVar` whose `Value` and
|
|
`SecretID` are both empty, THE SYSTEM SHALL NOT emit an `environmentSource` for it, and the key
|
|
SHALL NOT appear in `req.EnvironmentDefinitions`.
|
|
A test SHALL assert this directly at the orchestrator assembly step. A test SHALL also assert
|
|
that the same profile produces the same definition set through BOTH assembly paths — the
|
|
orchestrator path and `executorProfileEnvironmentDefinitions` — for an input containing one
|
|
populated entry and one blank entry, because eliminating the divergence between those two paths
|
|
is the point of the change and a one-sided fix would satisfy the first test alone.
|
|
THE SKIP SHALL match the lifecycle condition exactly: `SecretID == "" && Value == ""`. An entry
|
|
with a blank `Value` but a non-empty `SecretID` is secret-backed and SHALL still be emitted; an
|
|
entry with a blank `SecretID` but a non-empty `Value` is a literal and SHALL still be emitted.
|
|
- **AC-38c** WHEN an executor profile carries a key whose `Value` and `SecretID` are both empty and
|
|
an agent profile defines the same key with a non-empty literal, THE SYSTEM SHALL resolve that key
|
|
to the AGENT-PROFILE value and SHALL NOT emit an override record, because after AC-38b only one
|
|
definition for that key reaches the resolver.
|
|
This is a deliberate BEHAVIOUR CHANGE and is the user-visible consequence of the AC-38 decision.
|
|
Today the same pair returns a `*ConflictError` and blocks the launch. It is recorded as an AC
|
|
rather than left implicit because a reader comparing before-and-after will otherwise read it as
|
|
a regression in AC-49's "no behaviour change for currently-succeeding tasks" — that AC is about
|
|
tasks that SUCCEED today, and this pair fails today.
|
|
Related: WHEN an executor profile carries such an entry and NO other origin defines that key, the
|
|
key SHALL be absent from the resolved environment. Today it resolves to the empty string on the
|
|
primary path and is absent on the recovery path; after this change both paths agree that it is
|
|
absent.
|
|
- **AC-39** WHEN a key is defined only by origin `executor profile` with a non-empty `Value` or a
|
|
non-empty `SecretID`, THE SYSTEM SHALL resolve it to that value and SHALL NOT emit an override
|
|
record, because nothing was overridden. The qualifier is required by AC-38b: an entry blank on
|
|
both fields no longer reaches the resolver at all, so there is no value to resolve it to.
|
|
- **AC-40** THE **AGENT**-PROFILE save path SHALL continue to reject `TASK_DESCRIPTION` and any
|
|
`KANDEV_*` key (`profile_crud.go:904`), unchanged by this spec. Precedence SHALL NOT create a
|
|
new path to define a reserved key from a profile.
|
|
THE SCOPE OF THAT FIRST CLAUSE IS THE AGENT PROFILE, AND SAYING SO IS THE POINT. The
|
|
executor-profile save path rejects no reserved key (see "The two profile kinds are NOT validated
|
|
the same way"), so a `KANDEV_FOO` executor-profile entry is savable today and reaches the agent
|
|
today whenever no managed-runtime definition claims that key. An earlier draft stated this AC
|
|
without the scope, which would send a builder to write a failing test against executor-profile
|
|
save and then invent either new validation or a silently narrowed test.
|
|
THE SECOND CLAUSE HOLDS AND IS OBSERVABLE, which is why this AC survives rather than being
|
|
deleted: precedence creates no NEW path. A reserved key that a managed-runtime definition also
|
|
sets puts two Tier 1 definitions against each other, so it conflicts exactly as it does today
|
|
(AC-5); a reserved-prefixed key that nothing else sets resolved before this change and resolves
|
|
after it. A test SHALL assert that an `executor profile` definition for a key also set by
|
|
`managed runtime` returns `*ConflictError` and is NOT resolved by tier precedence.
|
|
Closing the executor-profile validation gap itself is OUT OF SCOPE — see "Out of scope".
|
|
|
|
## Concurrency
|
|
|
|
Resolution is a pure function of its definition set. There is no shared row, no lock, and no
|
|
read-modify-write.
|
|
|
|
- **AC-41** WHEN two launches for different sessions resolve concurrently, each SHALL produce a
|
|
result determined solely by its own definition set.
|
|
- **AC-42** WHEN a profile is edited after a launch has captured that profile's definitions, the
|
|
in-flight launch SHALL keep what it captured and SHALL NOT re-read that profile. This inherits
|
|
ADR-2026-08-03: "An already-running process or open terminal does not change when a binding or
|
|
secret value changes." A later launch, resume, or Reset Environment re-resolves.
|
|
|
|
**ONE LAUNCH HAS TWO CAPTURE MOMENTS, NOT ONE, AND THEY ARE NAMED HERE** because a test written
|
|
against a single "the snapshot" fails against the actual code:
|
|
|
|
| Definitions | Captured at | Effect of an edit after that moment |
|
|
| --- | --- | --- |
|
|
| `executor profile`, `repository <name>`, `managed runtime` | ORCHESTRATOR assembly — `resolveLaunchEnvironment` → `taskEnvironmentSources`, writing `req.EnvironmentDefinitions` (`executor_execute.go:1100`, `executor_interaction.go:747`) | not seen by this launch |
|
|
| `agent profile` (and `AGENT_MODEL` / `AGENTCTL_AUTO_APPROVE_PERMISSIONS` derived from it) | LIFECYCLE — `resolveAgentProfile` (`manager_launch.go:1140`, `:1197`), appended in `resolveStrictEnvironment` (`environment_resolution.go:40`) | SEEN by this launch, because the read has not happened yet |
|
|
|
|
So an EXECUTOR-profile edit between the two moments is not picked up, and an AGENT-profile edit
|
|
between the two moments IS. THE SYSTEM SHALL preserve exactly that. This is today's behaviour and
|
|
this spec does not change it; it is written down because the asymmetry is invisible from either
|
|
call site alone and a reasonable reader assumes one snapshot covers both.
|
|
A test SHALL assert the executor-profile half: a launch whose `req.EnvironmentDefinitions` were
|
|
assembled from executor profile state E resolves against E even when the stored profile has since
|
|
changed to E'.
|
|
- **AC-43** WHEN the orchestrator preflight ACCEPTS a definition set, that verdict SHALL NOT be
|
|
treated as a guarantee that the launch resolves: the lifecycle resolve runs over a STRICTLY
|
|
LARGER definition set and its verdict is the one that decides the launch.
|
|
Observable outcome, and the whole content of "authoritative": THE SYSTEM SHALL fail the launch
|
|
when `Validate` returns nil at `task_environment.go:118` and `Resolve` subsequently returns an
|
|
error at `environment_resolution.go:51`. A test SHALL construct exactly that pair — a preflight
|
|
set of Tier 1 definitions that validates cleanly, plus an `agent profile` definition appended at
|
|
the lifecycle site that makes the key conflict — and assert the launch fails with the resolver's
|
|
error, not the preflight's success.
|
|
**THE CONVERSE SHALL NOT HOLD, and this is the clause that closes the ambiguity:** "authoritative"
|
|
describes WHICH VERDICT BINDS, not a re-read of the data. The lifecycle resolve SHALL NOT re-read
|
|
the executor profile, the repositories, or `req.Env`; it consumes `req.EnvironmentDefinitions` as
|
|
captured (AC-42) and appends to them. An implementation that makes the lifecycle site re-load the
|
|
executor profile satisfies a plain reading of the word "authoritative" and violates AC-42, and
|
|
because no precedence criterion observes the difference, every other test in this spec would
|
|
still pass. AC-19 remains true alongside this: the preflight never REJECTS a set the final
|
|
resolve would accept, so the two verdicts can only diverge in the accept-then-fail direction.
|
|
|
|
## Idempotency and retry
|
|
|
|
- **AC-44** WHEN a launch fails after environment resolution and is retried with the same profile
|
|
and repository state, resolution SHALL produce the same map and the same override records.
|
|
- **AC-45** WHEN a session is resumed, THE SYSTEM SHALL re-resolve from current definitions and
|
|
SHALL apply the same rules as an initial launch. Resume SHALL NOT inherit a stale precedence
|
|
decision.
|
|
- **AC-46** Emitting an override log record and incrementing the counter SHALL be tied to an
|
|
actual resolution. A retried launch that resolves again SHALL emit again; these are per-resolve
|
|
events, not deduplicated across retries.
|
|
|
|
## Migration
|
|
|
|
- **AC-47** THE SYSTEM SHALL NOT move, copy, or rewrite any environment variable between an agent
|
|
profile and an executor profile.
|
|
- **AC-48** THE SYSTEM SHALL NOT require a database migration for this feature.
|
|
- **AC-49** WHEN an existing install already has the same key on both an agent profile and an
|
|
executor profile with DIFFERING LITERAL-BACKED values, that install SHALL begin succeeding with
|
|
the executor-profile value on the next strict-resolution launch, with an override record emitted
|
|
per AC-20. The qualifier is load-bearing in four directions, and a migration test written
|
|
without it contradicts other ACs:
|
|
- if EITHER side is secret-backed, the veto still blocks the launch (AC-9, AC-10, AC-11). Such
|
|
an install does NOT begin succeeding, by design;
|
|
- if the two values are IDENTICAL, the install already succeeds today — they merge, and no
|
|
override record is emitted (AC-15). Nothing changes for it;
|
|
- if the EXECUTOR-profile side is blank on both `Value` and `SecretID`, no executor-profile
|
|
definition reaches the resolver at all (AC-38b), so the install begins succeeding with the
|
|
AGENT-profile value and NO override record (AC-38c). The empty string is a differing
|
|
literal-backed value on a plain reading, so without this bullet AC-49 would demand the
|
|
executor's empty value win and contradict AC-38a outright;
|
|
- "strict-resolution launch" excludes the legacy fill-missing path scoped out under
|
|
"The two assembly sites", which never receives executor-profile definitions at all.
|
|
|
|
Rationale for no automatic move: there is no safe general rule for classifying a key as
|
|
machine-scoped. `ANTHROPIC_BASE_URL` is obvious to a human and not to a matcher. Moving a value
|
|
off an agent profile would also change what every other task using that profile sees. And an
|
|
executor profile is frequently absent: in the observed install, 9 worktree sessions and 4 sessions
|
|
carried no `executor_profile_id`, and `Local Docker` had no executor profiles at all. The agent
|
|
profile must remain the fallback (AC-7, AC-8), so relocating values there would break more than it
|
|
fixes.
|
|
|
|
## Out of scope
|
|
|
|
Named exclusions. Each is a contract, not an omission.
|
|
|
|
- **The agent-profile matcher collision.** `buildAgentProfileMatcher` is a separate card and is
|
|
not changed here.
|
|
- **The legacy fill-missing branch of `buildEnvForExecution`** (`manager_startup.go:314-339`),
|
|
taken when neither `EnvironmentFinalized` nor `EnvironmentResolutionRequired` is set. It never
|
|
reaches the resolver and never receives executor-profile definitions, so the conflict this
|
|
feature resolves cannot arise in it. Full reasoning under "The two assembly sites".
|
|
- **A per-task or per-session environment override tier.** ADR-2026-08-03 rejected task-stored
|
|
bindings for v1; this spec does not reopen that.
|
|
- **Any change to `mergeEnvFillMissing`** (`profile_env.go:57`) or to the process-environment
|
|
merge it performs at agent launch.
|
|
- **Any change to which secrets a profile may reference.** Agent and executor profiles remain
|
|
Global-secrets-only per ADR-2026-08-03.
|
|
- **Making an override expressible as the empty string.** AC-38 states the limitation; lifting it
|
|
would require changing the assembly-time drop, which is out of scope.
|
|
- **A UI affordance for choosing a winner per key.** Precedence is by origin, not by user
|
|
selection.
|
|
- **Surfacing override records in the web UI.** AC-20 through AC-26 require the record, the log,
|
|
and the counter. Rendering them in a settings or task panel is deliberately deferred; no
|
|
frontend change is required by this spec.
|
|
- **Reordering the Tier 1 peer group.** Making `executor profile` beat `managed runtime`, or
|
|
`repository` beat `executor profile`, is explicitly not part of this change and would contradict
|
|
ADR-2026-08-03 and the tests `TestResolveEnvironmentSources_RejectsEveryConflictingPair`
|
|
(`task_environment_test.go:40`) and
|
|
`TestBuildEnvForExecution_RejectsLateManagedValuesThatConflictWithRepositorySecrets`
|
|
(`manager_launch_test.go:530`).
|
|
- **Adding reserved-key validation to the executor-profile save path.** `TASK_DESCRIPTION` and
|
|
`KANDEV_*` are rejected only on the agent-profile path (AC-40), so an executor profile can hold
|
|
such a key today. This spec does not close that: precedence creates no new path to it, closing it
|
|
would change behaviour on three save surfaces (HTTP, WS, MCP) that this card never asked to
|
|
touch, and it could reject profiles already persisted in existing installs on their next update.
|
|
A separate card owns it.
|
|
- **Adding duplicate-key validation to the executor-profile save path.** Same reasoning. AC-6a
|
|
pins that the resulting pair conflicts rather than silently picking a winner, which is the part
|
|
this feature is responsible for.
|
|
- **Adding value-or-secret validation to the executor-profile save path.** The blank-value problem
|
|
is solved at ASSEMBLY (AC-38b), not at save time, so an existing profile carrying a blank entry
|
|
keeps saving and loading exactly as it does now — the entry is simply dropped before resolution,
|
|
on both paths instead of one. Validating it at save time was considered and deliberately not
|
|
chosen, for the same back-compatibility reason as the two entries above.
|
|
- **Changing `definitionIdentity`.** Comparing decrypted plaintext remains rejected.
|
|
|
|
## Verification surfaces
|
|
|
|
Recorded as the E2E decision input.
|
|
|
|
- **Backend only.** Every acceptance criterion above is observable from Go tests against four
|
|
seams and three artefacts:
|
|
- seams: `runtimeenv.Resolve` (map, `[]OverrideRecord`, error), `runtimeenv.Validate` (error),
|
|
`Manager.resolveStrictEnvironment` (log + counter side effects),
|
|
`Executor.resolveLaunchEnvironment` (preflight verdict), and
|
|
`Executor.taskEnvironmentSources` (the emitted `environmentSource` list — this is where the
|
|
AC-38b blank-entry skip and the AC-6a duplicate-key case are observed);
|
|
- artefacts: the returned `[]OverrideRecord` (AC-20, AC-27, AC-28), the
|
|
`environment override applied` log record and its named fields (AC-23), and the
|
|
`environment_override_applied_total` expvar map read back by its label key (AC-24, AC-24a).
|
|
The expvar map is readable in-process via `expvar.Get("environment_override_applied_total")`,
|
|
so the counter ACs need no HTTP round trip to assert.
|
|
- **One Go signature changes; no data shape does.** `Resolve` gains a third return value
|
|
(`[]OverrideRecord`) and `Validate` is unchanged (AC-19b). `Definition`, `ConflictError` and
|
|
`SecretError` keep their exported fields, so no caller has to reinterpret existing data. The
|
|
signature change touches two call sites: `Manager.resolveStrictEnvironment`, which consumes the
|
|
records, and the test-only-today `resolveEnvironmentSources`, which discards them.
|
|
|
|
**One production behaviour changes outside the resolver**, and it is the only such change in this
|
|
spec: `Executor.taskEnvironmentSources` (`task_environment.go:66-70`) starts skipping profile
|
|
entries blank on both `Value` and `SecretID` (AC-38, AC-38b). No signature changes there and no
|
|
data shape changes; the function simply emits one fewer `environmentSource` for an input the
|
|
restart-recovery path already discards. Nothing else in this feature touches the orchestrator.
|
|
|
|
The `environment` package's complete new exported surface, all named rather than left to the
|
|
builder:
|
|
- three types: `Tier`, `OriginTier`, `OverrideRecord`;
|
|
- three `Tier` constants: `TierAuthoritative`, `TierProfileDefault`, `TierAgentRuntimeDefault`;
|
|
- six origin constants: `OriginManagedRuntime`, `OriginManagedCredentials`,
|
|
`OriginManagedAgentDefaults`, `OriginAgentProfile`, `OriginExecutorProfile`,
|
|
`OriginRepositoryPrefix`;
|
|
- four functions: `TierForOrigin`, `IsRepositoryOrigin` (AC-19c), `NormalizeOriginLabel`,
|
|
`JoinOriginLabels` (AC-25).
|
|
|
|
The tier table itself stays UNEXPORTED behind `TierForOrigin` (AC-19c). One new package,
|
|
`internal/agent/runtime/envmetrics`, is created with one exported function,
|
|
`RecordOverrideApplied` (AC-24a).
|
|
This is not a REST/WS/DTO change: nothing crosses the HTTP or WebSocket boundary.
|
|
- **No frontend change.** The agent-profile editor
|
|
(`apps/web/components/settings/profile-edit/`) and the executor-profile editor
|
|
(`apps/web/app/settings/executors/[profileId]/page.tsx`) already accept the env vars this spec
|
|
reconciles. No new copy, so no i18n work.
|
|
- **No database migration.**
|
|
- **E2E recommendation: none required.** The behaviour is a pure-function decision several layers
|
|
below any user-visible surface, and no user-visible surface changes. Existing backend tests plus
|
|
new unit tests at the resolver and both assembly sites give better coverage per unit of runtime
|
|
than a browser test could.
|
|
|
|
Note for Build: `apps/backend/internal/agent/runtime/environment/` currently has **no test file**.
|
|
The resolver is covered only indirectly through the orchestrator and lifecycle packages. This
|
|
feature changes that resolver's core decision, so package-level tests belong there.
|