Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 39 additions & 11 deletions hypaware-core/plugins-workspace/codex/src/exchange-projector.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,23 @@ export function createCodexExchangeProjector(opts = {}) {
// rollout lookup LAZY (a fresh in-band cwd never scans), and it is keyed on
// a Codex thread id (only a real Codex thread has a rollout), so
// non-codex traffic never scans.
//
// @ref LLP 0083#workspace-key-ranks-last [implements]: three sources, in
// this order: in-band, rollout, workspace key (#480).
// The key ranks LAST because it is the only one of the three that can be a
// guess: `selectCodexWorkspace` substitutes the first `workspaces` entry
// when none matches, while `session_meta.cwd` is what Codex itself wrote
// at session start. Ranking the key above the rollout meant a declared
// `workspaces` map satisfied the first `??` and the rollout was never
// consulted, so on the subscription route (the route this fallback exists
// for) a first-key guess turned a correct `.hypignore` drop into a record.
// It is still a real source, not a discarded one: with no rollout it is
// the only cwd there is, and refusing it outright would remove `.hypignore`
// coverage from that route. Held to the same usability checks as the
// in-band value, which is where it used to be checked.
const cwd = usableInBandCwd(firstString(codexContext?.cwd, readRecordedCwd(reqBody)), ctx)
?? resolveRolloutCwd(rolloutCwd, codexContext)
?? usableInBandCwd(codexContext?.workspace_cwd, ctx)
// @ref LLP 0083#decision [implements]: a refused workspace substitution is
// observable, not silent - it means the key named a tree the session did
// not run in, so the location inference behind it is in doubt.
Expand Down Expand Up @@ -876,9 +891,18 @@ function resolveCodexContext(input, provider, path, reqBody) {
// substitutes the first `workspaces` key when none matches, which is a guess
// about a directory the session may never have run in, so it must not decide
// a `.hypignore` verdict. The key still enriches (`attributes.codex.workspace`,
// git_*) and still supplies the cwd on the subscription route, where the
// request states none and the key is the only in-band source there is.
cwd: firstString(inBandCwd, workspace?.path),
// git_*) and still supplies the cwd on the subscription route as a last
// resort, below: see `workspace_cwd`.
cwd: inBandCwd,
// The key as a LAST-RESORT cwd, carried apart from the in-band one so the
// call site can rank it BELOW the rollout (#480). Set only when the request
// stated no cwd at all: when it stated one, the key is a substitution over a
// value the client actually sent, and a substitution never decides the
// verdict (#476 case c, where the stated cwd is unusable and the key must
// still not stand in for it).
// @ref LLP 0083#workspace-key-ranks-last [implements]: in-band, then
// rollout, then the key
workspace_cwd: inBandCwd ? undefined : workspace?.path,
// Set only when the substitution was refused, so the caller can log it
// rather than let a discarded guess vanish. Refused is narrower than
// "different bytes": see `workspaceCoversCwd`.
Expand Down Expand Up @@ -1229,14 +1253,18 @@ function readRecordedCwd(reqBody) {
* `firstString` refuse the rest upstream), and over that whole domain the two
* former copies agreed.
*
* One thing this does NOT reach, so nobody reads it as the whole gate: when the
* request states no `cwd` at all, the value passed in is the workspace key
* `selectCodexWorkspace` picked for it, and that falls back to the first
* workspace when none matches, which is absolute and so accepted here even when
* the session ran elsewhere. A request that DOES state one no longer reaches
* here through the key (#476, closed), so the residue is the narrower ranking
* question: the key still outranks the rollout fallback (#480). The rollout
* fallback at the call site sits outside this call, but it is not unguarded:
* One thing this does NOT reach, so nobody reads it as the whole gate: it bounds
* the SHAPE of a value, never its provenance. The call site also passes the
* workspace key `selectCodexWorkspace` picked through here - a separate call
* since #480, where it used to arrive folded into the in-band value - and that
* key falls back to the first workspace when none matches, which is absolute and
* so accepted here even when the session ran elsewhere. A request that DOES
* state a `cwd` no longer reaches here through the key at all (#476, closed);
* the key is offered only when the request stated none. What keeps such a guess
* from deciding a verdict is then the RANKING at the call site (in-band,
* rollout, key), not this predicate: the key lost its precedence over the
* rollout in #480, which is where the residual ranking question used to sit. The
* rollout fallback sits between the two and is not unguarded either:
* `rollout-cwd.js` reads it through `readRolloutSessionMeta`, which applies
* `sessionMetaCwd`.
* @ref LLP 0160#corrections-0083 [constrained-by]: LLP 0083's own statement of
Expand Down
78 changes: 51 additions & 27 deletions llp/0083-codex-live-cwd-from-rollout.decision.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,9 @@ has the symmetric fallback.
against the **daemon's** own process cwd and return a confident verdict for an
unrelated directory (#471). The rollout fallback then still gets its turn, and
when nothing usable is found the row records `cwd = NULL` exactly as before, so
refusing does not make this path fail closed. Both cwd sources for this gate
answer that from **one** predicate: core's `sessionMetaCwd` refuses a blank or
relative value
refusing does not make this path fail closed. All three cwd sources for this
gate answer that from **one** predicate: core's `sessionMetaCwd` refuses a
blank or relative value
([LLP 0150 §usable-cwd](./0150-one-reader-for-codex-session-meta.decision.md#usable-cwd)),
`rollout-cwd.js` reads the rollout through it, and `usableInBandCwd` calls it
for the in-band value, so a refused in-band value falls through to a source
Expand All @@ -88,11 +88,14 @@ has the symmetric fallback.
declining to borrow it. What changed here is the weight given to drift, not
the scoping: the two readings were behaviourally identical over every input
that can reach the seam, so folding them together changed nothing except the
number of places the rule can drift from. One limit of the rule, stated rather
than implied: on the Codex route the value the predicate sees is usually not
the request's `cwd` but the workspace key `selectCodexWorkspace` selected for
it, which substitutes the first workspace when none matches, so an
absolute-but-unrelated directory can still reach the gate (#476).
number of places the rule can drift from. The workspace key is the third source
with the same trust problem, and `usableInBandCwd` is applied to it too, as a
**separate** call, because the key no longer arrives folded into the in-band
value (#480). One limit of the rule, stated rather than implied: it bounds the
*shape* of a value, never its provenance, so an absolute-but-unrelated
directory (the first workspace `selectCodexWorkspace` substitutes when none
matches) passes it. What keeps such a guess from deciding a verdict is the
**ranking** below, not this predicate (#476).
- **Keyed on the codex thread id, and the rollout must confirm it.** A rollout is
one **thread's** file: its name embeds `session_meta.payload.id` (the thread),
matched via the `sessionIdFromPath` helper shared with the backfill (a helper
Expand Down Expand Up @@ -233,30 +236,49 @@ has the symmetric fallback.
- **One resolved `cwd`, used twice.** The same value feeds the `.hypignore` drop
and the row's stamped `cwd`, so live rows now carry the cwd the backfill reads
and the two halves of the policy agree (closes the live/backfill inconsistency).
- **A substituted workspace key never decides the verdict** (amended, #476). The
Codex projector picks a `workspaces` turn-metadata key for enrichment, falling
- **A substituted workspace key never decides the verdict** (amended, #476,
#480). {#workspace-key-ranks-last} The Codex projector picks a `workspaces`
turn-metadata key for enrichment, falling
back to the *first* key when none matches the request's `cwd`. That substituted
key is a guess about a directory the session may never have run in, so it does
not supply the one resolved `cwd`: an explicit in-band `cwd` outranks it for
not supply the one resolved `cwd` while any better-founded source exists. The
order is **in-band, rollout, key**: an explicit in-band `cwd` outranks it for
both the gate and the stamp, and the refusal is reported as
`plugin.codex.usage_policy_workspace_cwd_refused`
(`error_kind: workspace_cwd_mismatch`, paths hashed). The key keeps its
(`error_kind: workspace_cwd_mismatch`, paths hashed); the rollout's
`session_meta.cwd` outranks it too, because that line is what Codex itself
wrote at session start rather than a guess this projector made. The key keeps its
enrichment role (`attributes.codex.workspace`, `git_remote`, `git_commit`,
`has_changes`) and still supplies the `cwd` on the subscription route, where
the request states none and the key is the only in-band source there is.
`has_changes`) and remains the **last resort** for the `cwd` on the
subscription route: when the request states none and no rollout is found, it is
the only source there is, and refusing it outright would remove `.hypignore`
coverage from that whole route.
Consequence: for a session running in a *subdirectory* of its workspace the
row now stamps the subdirectory rather than the workspace root, which is the
directory the policy is actually scoped to.
Three limits, stated rather than implied, and each one filed so it does not
live only here. The key still outranks the **rollout** fallback (it resolves
before the `??`), so a subscription-route session that declares a `workspaces`
map never consults `session_meta.cwd` and a first-key guess can still decide
its verdict (#480). That one is pre-existing, verified byte-identical before
and after this amendment; ranking the guess below the rollout is a separate
call, not taken in the lines PRs #467/#474 rewrite. Because the key keeps
enriching, a row recorded where it used to drop (clean in-band `cwd`, ignored
declared workspace) carries that workspace's identity even though the
directory it names is `.hypignore`-ignored: the gate is scoped by `cwd`
The key is offered as a last-resort `cwd` **only when the request stated no
`cwd` at all**, not merely when what it stated was unusable: a relative or
blank in-band value is still a statement about where the session ran, and
letting the key stand in for it is exactly the substitution this bullet
refuses (#476 case c).
Three limits, stated rather than implied. A key outranked by the **rollout**
is discarded silently: `usage_policy_workspace_cwd_refused` fires only for the
in-band contradiction, because that predicate compares the key against a value
the request stated, and the rollout arrives after it. A naive widening would
put a warn on every turn of an ordinary session whose `session_meta.cwd` is a
subdirectory of its workspace, which is the frequency objection
[LLP 0160](./0160-workspace-cwd-refusal-is-an-ancestor-test.decision.md#decision)
has since settled on the in-band side by making the refusal an **ancestor**
test rather than a byte test. Widening is therefore no longer blocked on that
objection, but it must reuse 0160's predicate and not a byte comparison, or it
reintroduces exactly the noise 0160 removed. The other two are filed so they
do not live only here. Because the
key keeps enriching, a row recorded where it used to drop (an ignored declared
workspace outranked by a clean `cwd`, in-band since #476 and, now that the key
ranks below it, from the **rollout** too: demoting the key widens this case to
the whole subscription route rather than leaving it to the in-band one) carries
that workspace's identity even though the directory it names is
`.hypignore`-ignored: the gate is scoped by `cwd`
([LLP 0049](./0049-hypignore-usage-policy.spec.md#scope)), not by enrichment
source (#481). And the gate does not canonicalize paths, so *which spelling*
reaches it decides the verdict: a symlinked spelling of an ignored directory
Expand All @@ -266,9 +288,11 @@ has the symmetric fallback.
misses symlinked spellings here). What this amendment changes is which of two
symmetric spellings trips it, by taking the client's honest `cwd` over the
key's: it closes the case where the *key* held the non-canonical spelling and
opens the case where the *request* does. The widest case is untouched, a
declared symlinked key on a subscription-route request that states no `cwd` at
all, which leaks the same before and after. Canonicalizing belongs in the
opens the case where the *request* does. The widest case is narrowed but not
closed: a declared symlinked key on a subscription-route request that states no
`cwd` at all now loses to the rollout, whose value came from `getcwd(2)` and is
therefore canonical, but it still reaches the gate uncanonicalized whenever no
rollout is found. Canonicalizing belongs in the
shared matcher ([LLP 0050](./0050-ignore-enforced-in-adapters.decision.md)),
where it must also canonicalize the `local-only` list entries or it un-governs
an entry a user marked by its symlink spelling.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,8 +178,11 @@ key `selectCodexWorkspace` selected for it ... so an absolute-but-unrelated
directory can still reach the gate (#476)"*. Since that document's own amendment
landed (PR #477), `usableInBandCwd` sees the request's `cwd` whenever the request
states one. The residual case is narrower than the sentence: **only** a request
that states no `cwd` at all lets a substituted key reach the gate, which is the
open #480 ranking question, not #476.
that states no `cwd` at all lets a substituted key reach the gate, which was the #480
ranking question, not #476. #480 has since been answered the other way, in LLP
0083's own [§workspace-key-ranks-last](./0083-codex-live-cwd-from-rollout.decision.md#workspace-key-ranks-last):
the key now ranks **below** the rollout, so it reaches the gate only when the
request states no `cwd` *and* no rollout is found.

## Consequences

Expand Down
Loading
Loading