Skip to content

Codex live projector: the workspace-key substitution can feed the .hypignore gate a directory the session never ran in #476

Description

@philcunliffe

Deferred from PR #474's review (round 2 of 2)

PR #474 closed #471 by refusing an unusable in-band Codex cwd before it
reaches the .hypignore gate (usableInBandCwd: non-blank and path.isAbsolute).
Round-2 review established by execution that an adjacent door to the same
hazard is still open
, one layer earlier: the value the predicate sees on the
Codex route is usually not the request's cwd at all, it is a workspace key
substituted for it
.

The finding

hypaware-core/plugins-workspace/codex/src/exchange-projector.js:773

const workspacePath = workspacePaths.find((key) => pathsEqual(key, cwd)) ?? workspacePaths[0]

resolveCodexContext (:653) calls selectCodexWorkspace(metadata, <in-band cwd>)
and stamps the result as codexContext.cwd (:722). That value is the first
argument to the gate expression at :122, so when the request's cwd matches no
key in the workspaces turn-metadata map, workspacePaths[0] becomes both the
.hypignore gate input and the row's stamped cwd (LLP 0083, "One resolved
cwd, used twice").

Because workspacePaths[0] is normally an absolute path, usableInBandCwd
accepts it. PR #474's predicate does not and cannot see the problem.

Verified by execution

Real projector, real shared matcher (ignoringResolver), one governing
.hypignore of class ignore at /work/ignored:

workspaces map request cwd result row cwd
{'/work/ignored/proj':{}} /work/ignored/proj DROP (correct) n/a
{'/work/ignored/proj':{}} /work/clean/real DROP n/a
{'/work/clean/proj':{}} /work/ignored/real RECORDED /work/clean/proj
{'/work/ignored/proj':{}} sub DROP, no cwd_unusable warn n/a

Rows 2 and 3 are the two halves of #471's defect reached through a different
door: row 2 is a false drop for a session no .hypignore covers, row 3 is
the failure the control exists to prevent, a session whose real directory is
ignored being recorded, with a cwd from an unrelated tree stamped on it.
Row 4 shows the workspace substitution runs ahead of #474's predicate, so the
in-band value never reaches it and no usage_policy_cwd_unusable is logged.

Realistic exposure

The single-workspace case, which is the common one, is usually benign: the one
key is the session root, an ancestor of the real cwd, so the substitution is
ancestor-conservative rather than wrong-tree (a deeper .hypignore under the
cwd is still missed, which is fail-open). The wrong-tree verdicts need either a
multi-root session whose cwd sits under a root that is not workspacePaths[0],
or a body-cwd / metadata-key normalization disagreement (pathsEqual only trims
trailing slashes, so symlinked or otherwise non-identical spellings miss). Narrow,
but the same class as #471 and it survives #474 intact.

Why PR #474 did not fix it

The fallback is deliberate and covered by an existing test
(test/plugins/codex-exchange-projector.test.js, "Codex workspace selection
prefers recorded cwd over first metadata key"). It is also load-bearing for
coverage: on the ChatGPT-subscription route the request often carries no cwd
at all, and workspacePaths[0] is then the only in-band source of one, so simply
deleting the substitution removes real .hypignore coverage rather than fixing
anything.

Fixing it therefore requires deciding what codexContext.cwd means when the
workspace map and the request's cwd disagree, which changes row content
(cwd, attributes.codex.workspace), not only the drop decision. That is the
same reason #466 deferred #471 to #471. PR #474 was at its 2-round review budget
and is deliberately one-line-surgical because exchange-projector.js is rewritten
heavily by held PRs #467 and #462.

The decision to take

Pick one:

  1. Separate the two roles. Keep workspacePaths[0] for workspace
    enrichment (attributes.codex.workspace, git_remote, git_commit,
    has_changes) but let it supply the gate/stamp cwd only when it is not
    contradicted by an explicit request cwd. Requires resolveCodexContext to
    return the two separately.
  2. Prefer the request's own cwd on disagreement. One-expression change at
    :722; changes the recorded cwd for the cwd-is-a-subdir-of-the-workspace
    case from the workspace root to the subdir, so it needs an LLP 0083 amendment
    and probably a test update.
  3. Accept it and say so. Record in LLP 0083 that on the Codex route the
    gate's cwd is workspace-mediated and may be an ancestor of, or a sibling
    tree to, the session's actual directory. Round-2 review already added a short
    note to LLP 0083's decision list pointing here; option 3 is to make that the
    final answer.

Either way this should land after #467 and #462 are resolved, since it is in
the neighbourhood they rewrite.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    neutral:fixDelegate this issue to neutral for an autonomous fix attempt (reproduce -> fix -> PR)

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions