Skip to content

Commit 2d331e8

Browse files
neo-opus-adatobiu
andauthored
docs(incident): agent identity drift forensic record + runbook (#13239) (#13240)
* docs(incident): agent identity drift forensic record + runbook (#13239) Per @tobiu's friction->gold direction: convert this session's agent-identity-drift incident (GH_TOKEN + memory-core) into durable shared substrate + a diagnostic/fix runbook so the next occurrence is fast to recognize and repair instead of silent. learn/agentos/incidents/2026-06-14-agent-identity-drift.md documents: the incident (Euclid's harness drifted to neo-opus-ada across GitHub opener + memory-core A2A while git-config stayed correct), the two-identities/one-drifted-source mechanism, a copy-paste diagnostic runbook (gh api user vs git config vs expected, + a window-scoping loop), the fix approach (reset the session identity-config; the body @identity stopgap doesn't restore the formal review gate), and the prevention (a healthcheck + write-boundary identity assertion that fails loud -- code rides #13234). Forensic complement to #13234; sibling silent-failure anti-pattern to #12450. * docs(incident): add validated fix-that-worked + 3-part defense to identity-drift runbook (#13239) Fold in the post-restart verification facts (@neo-gpt recovered via harness restart; verify across gh api user / Memory-Core identity.bound / manage_issue_comment+pr_review COMMENT probes) and the contamination-is-permanent boundary into Fix Approach; add the three-part defense synthesis (GH_TOKEN isolation + canonical-emit + #13237 gate-hardening with the line-anchored parser gotcha) to Prevention. * docs(incident): cross-link the Codex identity-safe worktree discipline instance (#13239) Reference #13241/PR #13242 (the Codex-harness GH_TOKEN-isolation discipline: worktrees under the clone root + an identity preflight) in the 3-part-defense GH_TOKEN-isolation pillar, so the cross-cutting incident record and its per-harness prevention instance reference each other. Names the cross-harness generalization as the open follow-up surface. --------- Co-authored-by: tobiu <tobiasuhlig78@gmail.com>
1 parent 8086fd2 commit 2d331e8

1 file changed

Lines changed: 104 additions & 0 deletions

File tree

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# Incident: Agent Identity Drift — GH_TOKEN + Memory-Core (2026-06-14)
2+
3+
> Forensic record for [#13239](https://github.com/neomjs/neo/issues/13239); forensic complement to [#13234](https://github.com/neomjs/neo/issues/13234) (the canonical-`AgentIdentity` code repair). Captures how one agent's session identity drifted to *another* agent's across both GitHub and the Memory-Core A2A bus, the divergence signature that diagnoses it, and the fix + prevention approach so the next occurrence is fast to resolve instead of silent.
4+
5+
| Attribute | Value |
6+
|---|---|
7+
| **Severity** | Medium — no data loss; mis-attributed authorship + a broken cross-family review gate + an A2A identity collision; operator-visible |
8+
| **Detected** | 2026-06-14 — operator noticed PR [#13233](https://github.com/neomjs/neo/pull/13233)'s opener (`neo-opus-ada`) ≠ its stated author (`@neo-gpt`); confirmed by @neo-gpt's `gh api user` V-B-A from his worktree |
9+
| **Affected** | @neo-gpt (Euclid, GPT-5 / Codex Desktop)'s harness session: PRs [#13233](https://github.com/neomjs/neo/pull/13233) + [#13235](https://github.com/neomjs/neo/pull/13235) (opener mis-attributed) and his Memory-Core A2A from-identity |
10+
| **Acute Containment** | A *different* Claude identity's formal approval cleared #13233's cross-family gate; @neo-gpt added canonical `@identity` lines to the PR bodies as an attribution stopgap |
11+
| **Root Resolution** | ⏳ Pending: the affected harness's session identity-config reset (team-owned) + the detection seam in [#13234](https://github.com/neomjs/neo/issues/13234). Until detection lands, recurrence is **silent** until a human notices a mis-attributed artifact. |
12+
| **Author** | Claude Opus 4.8 [1M context] (Claude Code) as @neo-opus-ada |
13+
| **Origin Session ID** | `4c598c8f-d8a7-4288-9420-e825a45d310e` |
14+
15+
## Summary
16+
17+
During this session @neo-gpt (Euclid) opened PRs #13233 and #13235 whose **GitHub opener was `neo-opus-ada`** (my identity) while the **commit author was correctly `neo-gpt`**. The same drift extended to the **Memory-Core A2A bus**: his agent-to-agent messages arrived *from* `neo-opus-ada`, and `mark_read` rejected his attempts on `@neo-gpt`-addressed messages "as unauthorized for the recipient" — he and I were colliding as the same identity.
18+
19+
The failure was **silent**: nothing warned that a PR was being opened, or an A2A sent, under the wrong identity. It surfaced only when a human noticed the opener/author mismatch. Downstream harm: mis-attributed authorship; a broken cross-family review gate (GitHub blocks self-review, so I — the *real* `neo-opus-ada` — could not formally approve "my own" PRs that were actually Euclid's); and an A2A bus collision.
20+
21+
The git commit author was never wrong — which is the key diagnostic: **the drift is in the *identity token*, not the *git config*.**
22+
23+
## Mechanism — two identities, one drifted source
24+
25+
A PR (and an agent op generally) carries **two independent identities** from **two sources**:
26+
27+
| Identity | Source | This incident |
28+
|---|---|---|
29+
| **Commit author** | git config `user.name` / `user.email` (per checkout) | `neo-gpt` — correct |
30+
| **PR opener / GitHub API** | the gh auth **token** | `neo-opus-ada`**drifted** |
31+
| **Memory-Core A2A from-identity** | the session identity-source | `neo-opus-ada`**drifted** |
32+
33+
The GitHub-token resolution is the crux:
34+
35+
- `gh` resolves its token from the **`GH_TOKEN` env var first**, which **overrides** any `gh auth login` stored credential. A stray / inherited `GH_TOKEN` therefore silently wins.
36+
- The **github-workflow MCP shares this token**`GraphqlService.#getAuthToken()` runs `gh auth token` (the same gh resolution). So a wrong `GH_TOKEN` corrupts **both** the MCP tools (`manage_pr_review`, `create_issue`, …) **and** the raw `gh` CLI (`gh pr create`, `gh api`) **identically** — one assertion can therefore cover both.
37+
38+
So when the affected harness's `GH_TOKEN` (or the upstream env/config that set it) held a different agent's PAT, every GitHub write authenticated as that agent, while the commit — set by the separate git config — stayed correct. The Memory-Core A2A identity drifted from the same root identity-config, extending the mis-identification to the bus.
39+
40+
## Diagnostic Runbook — recognize + confirm
41+
42+
**Symptom signature** (any one is a flag; together they confirm):
43+
44+
- A PR's **opener login ≠ its commit author** (or ≠ the body's declared `@identity`).
45+
- A2A messages arrive **from the wrong agent identity**.
46+
- `mark_read` / recipient-scoped ops reject **"unauthorized for the recipient"** for messages addressed to the agent's *real* identity.
47+
48+
**Confirm (copy-paste, run in the affected checkout):**
49+
50+
```bash
51+
gh api user --jq .login # the GitHub *token* identity (= the PR opener)
52+
git config user.email # the *commit* identity
53+
gh auth status # shows "(GH_TOKEN)" when the env var is the live source
54+
```
55+
56+
**Verdict:** if `gh api user` ≠ the expected agent's GitHub login *while* `git config` is correct, the **`GH_TOKEN` has drifted** — that is the bug, not git config. The `gh auth status` `(GH_TOKEN)` annotation confirms the env var (not `gh auth login`) is the live source.
57+
58+
**Scope the window:** list PRs the wrong identity *authored* but whose commits are by the real agent — those are the mis-attributed set:
59+
60+
```bash
61+
for n in $(gh pr list --author <drifted-login> --state all --limit 12 --json number --jq '.[].number'); do
62+
echo "#$n commits: $(gh pr view $n --json commits --jq '[.commits[].authors[0].login]|unique|join(",")')"
63+
done
64+
# rows whose commit author != <drifted-login> are the mis-attributed PRs (here: exactly #13233, #13235)
65+
```
66+
67+
## Fix Approach
68+
69+
1. **The fix that resolved this incident: a harness restart.** Restarting the affected harness re-derived its identity from the correct source — empirically the fastest repair (`@neo-gpt` recovered this way). **Verify across surfaces afterward** — post-restart, every one must report the *true* agent, not the drifted one:
70+
71+
```bash
72+
gh api user --jq .login # == expected agent (the GitHub token identity)
73+
```
74+
- Memory-Core healthcheck: `identity.bound=true`, `identity.nodeId=@<agent>`.
75+
- Memory-Core self-DM probe: stamps `from @<agent>`, and `mark_read` works again (while drifted it rejected the agent's own messages "unauthorized for the recipient").
76+
- github-workflow MCP `manage_issue_comment` — and a `manage_pr_review` **`COMMENT`** probe (`COMMENT` does not mutate review state, unlike `APPROVED`/`CHANGES_REQUESTED`) — stamp as `@<agent>`.
77+
78+
2. **Root prevention (so the restart isn't needed again): per-agent `GH_TOKEN` isolation.** The drift vector is `GH_TOKEN` *precedence* — it overrides `gh auth login`, so a stray / inherited token silently wins. Ensure each harness exports only its own PAT and nothing upstream leaks a different one into its env. (Team / operator-owned, per harness.)
79+
80+
3. **Attribution stopgap** (apply *while* drifted): a canonical `@identity` line in the PR / review *body* keeps authorship readable to humans + the GoldenPath reporting layer (the [#13237](https://github.com/neomjs/neo/pull/13237) gate-fix resolves cross-family *eligibility* from it). **But it does not restore GitHub's native formal review gate** — the self-review check keys on the *opener login*, not the body, so a cross-family `reviewDecision: APPROVED` still needs a different-family identity (or a human merge) until the token is reset.
81+
82+
4. **Contamination is permanent for artifacts created while drifted.** A mis-attributed PR opener is fixed at create-time; an old review / A2A object stays stamped with the drifted identity even after the harness is repaired. Re-attribution is not cleanly possible — the body `@identity`, a shell-authored correction comment, and this record are the durable attribution markers for those artifacts.
83+
84+
## Prevention — make it loud, not silent
85+
86+
The root failure class is the same as the Chroma query swallow in [#12450](https://github.com/neomjs/neo/issues/12450): **a real failure erased into a clean-looking result instead of surfaced.** A wrong identity should not silently produce a valid-looking PR.
87+
88+
Recommended detection seam (code implementation rides [#13234](https://github.com/neomjs/neo/issues/13234)):
89+
90+
- **Session-start / healthcheck assertion** — the github-workflow healthcheck already verifies `gh auth status`; extend it to assert `gh api user --jq .login` **and** the Memory-Core self-identity **== the expected per-agent identity** (canonical, from `ai/graph/identityRoots.mjs`). On mismatch → `degraded` + an explicit `"identity drift: authed as X, expected Y"`. Catches a drifted token **before any write**.
91+
- **Write-boundary guard (defense-in-depth)** — the MCP write tools already gate on `viewerPermission`; add the same identity-match as a fail-closed precondition. For the `gh pr create` path (not routed through the MCP), a one-line pre-flight assertion before create.
92+
93+
One assertion covers GitHub + the MCP because they share the gh-token; the Memory-Core check is the second surface.
94+
95+
**The complete defense is three-part; the detection above is the loud-failure net under it:**
96+
97+
1. **`GH_TOKEN` isolation** — the root (Fix Approach §2): each harness exports only its own PAT. Stops the drift at the source. Per-harness *discipline* instance (codified for Codex in [#13241](https://github.com/neomjs/neo/issues/13241) / PR [#13242](https://github.com/neomjs/neo/pull/13242)): keep identity-sensitive worktrees under the harness clone root so the shell resolves the right per-agent `.env` / PAT, plus an identity preflight (`gh api user --jq .login == <agent>`) after a worktree/thread switch before any state-changing GitHub call. The cross-harness generalization (Claude / Gemini equivalents) is the open follow-up surface.
98+
2. **Canonical-emit** — every PR / review body carries the agent's `@identity` line (`pull-request-workflow.md` §5), so authorship is machine-readable even when the opener login is wrong.
99+
3. **Gate-hardening** — the cross-family review gate resolves author *family* from that body `@identity`, not the opener login ([#13237](https://github.com/neomjs/neo/pull/13237)), with a **line-anchored** parser (`/^Authored by[^\n]*?@([\w-]+)/m`). The `^…/m` anchor is load-bearing: a naive `/Authored by/` overmatches the `Co-Authored-By` trailer, and real bodies place the self-id mid-document (after `Resolves #N`), so a non-multiline match misses it entirely.
100+
101+
## Related
102+
103+
- [#13234](https://github.com/neomjs/neo/issues/13234) — the canonical-`AgentIdentity` code repair + the detection-seam implementation (where the prevention above lands as code).
104+
- [#12450](https://github.com/neomjs/neo/issues/12450) — sibling silent-failure anti-pattern (a real failure erased into a clean result instead of surfaced).

0 commit comments

Comments
 (0)