Skip to content

Follow-up: deferred review findings from PR #477 (ignored-workspace identity on newly-recorded rows; refusal warn fires every turn) #481

Description

@philcunliffe

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

Two findings whose subject code exists only on the unmerged fix/issue-476
branch
, so neither is actionable until PR #477 lands. Labelled
neutral:stuck for that reason. Blocking PR: #477.

Both were verified by execution against the real projector and the real shared
matcher (createUsagePolicyResolver), on PR #477's head 0db67e1 and on that
head merged with origin/master 1555f13, with origin/master as the control.


1. A row recorded where it used to drop carries an ignored workspace's identity

LOW-MEDIUM, privacy (identity, not content).
hypaware-core/plugins-workspace/codex/src/exchange-projector.js:715 with :733.

PR #477 takes the gate/stamp cwd back from the substituted workspaces key but
deliberately leaves the key's enrichment role intact. So the case the PR
newly records (clean in-band cwd, declared workspace that is itself
.hypignore-ignored) produces a row naming the opted-out tree:

tree verdict row
origin/master DROP (no row)
0db67e1 RECORDED cwd=/work/clean/real, attributes.codex.workspace=/work/ignored/proj, git_remote=git@github.com:acme/SECRET-REPO.git, head_sha=cafebabe

Only the identity of the ignored directory leaks (its path, its git remote, its
head sha), never its content, and the drop for the ignored directory's own
sessions is unaffected. It is also arguably correct by design: the gate is scoped
by cwd (LLP 0049 #scope), not by enrichment source. But on master this row
did not exist at all, so it is new surface.

The decision. Suppress enrichment sourced from a workspace key that resolves
to ignore, or accept it as documented. Suppressing costs a second resolver
lookup per exchange (cheap, the per-cwd TTL cache absorbs it) and loses
graph-bridge identity for genuinely multi-root sessions (LLP 0032 #capture).
Accepting is already written down as a stated limit in
llp/0083-codex-live-cwd-from-rollout.decision.md.

Note that PR #477's own test "a refused workspace substitution still enriches the
row from the workspace key (#476)" pins the opposite behaviour using a clean
workspace key, so it does not constrain this choice either way.


2. The refusal warn fires on every turn of an ordinary subdirectory session

LOW, observability. exchange-projector.js:125-137.

The refusal predicate is "a key was substituted and it is not byte-equal to the
in-band cwd":

refused_workspace_cwd: workspace && inBandCwd && !pathsEqual(workspace.path, inBandCwd)
  ? workspace.path
  : undefined,

pathsEqual is byte equality after a trailing-slash trim, so the completely
ordinary shape "session running in a subdirectory of its workspace" trips it.
Verified, both directories clean, no .hypignore anywhere, nothing privacy
relevant happening:

tree workspaces in-band cwd verdict refusal warns
origin/master {/work/proj} /work/proj/sub RECORDED 0
0db67e1 {/work/proj} /work/proj/sub RECORDED 1 per turn

That is a warn on every exchange of such a session. The PR argues symmetry with
#474's usage_policy_cwd_unusable sibling, but that signal fires only on
genuinely unusable values, so the symmetry does not extend to frequency, and a
warn that fires constantly on the common case devalues the other privacy warns
around it.

The decision. Two options, either is fine:

  • Demote to info. One word, plus moving the assertion in the test "a refused
    workspace substitution is logged with hashed paths, not silently applied
    (Codex live projector: the workspace-key substitution can feed the .hypignore gate a directory the session never ran in #476 case c)" to the info channel.
  • Keep warn but narrow the predicate to the cases that actually matter: skip
    it when the substituted key is an ancestor of the in-band cwd, which is
    the benign subdirectory shape (the ancestor walk from the subdirectory passes
    through the key anyway, so the verdict is unchanged and there is nothing to
    report). Reuse isEqualOrDescendant from src/core/usage-policy/matcher.js
    rather than writing a second copy of the path logic (LLP 0069 R8).

Round 2 left exchange-projector.js byte-identical on purpose: #462 and #474
both modify this file (#474 was mid conflict-resolution during the review), so a
one-word log-level change was not worth expanding the conflict surface for.


Also for whoever merges

PR #474's paragraph in llp/0083-codex-live-cwd-from-rollout.decision.md still
says the #476 gap is open ("an absolute-but-unrelated directory can still reach
the gate (#476)"). Whichever of #474 / #477 merges second should trim that
sentence.

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