Wire Antigravity session logs via a runtime conversation→worktree map (CROW-1107) - #1109
Conversation
dgershman
left a comment
There was a problem hiding this comment.
Code & Security Review
Critical Issues (if any)
None.
Architecture / Existing Patterns
- Existing pathway:
CodingAgent.logSources→LogSyncCollector/BackfillScanner.assemble. Codex attributes a global store viacwdFilter; Grok partitions by encoded cwd. CROW-1097 already identified that Antigravity is neither, and named the follow-up: a runtimeconversationId → worktreemap filled from Crow's own hooks. This PR extends that pathway rather than inventing a second collector. - The map lives in CrowCore (
AntigravityConversationMap) soCrowEnginecan write it andCrowAntigravitycan read it without a new package edge. Worktree is taken fromprimaryWorktree(for:), notworkspacePaths— that matches the hook-event ownership model and is covered byAntigravityHookMapCaptureTests. - No architecture finding. The new "runtime-map" attribution mode is the right shape for a global store with no cwd.
Security Review
Strengths:
- Worktree attribution is Crow-owned, not payload-owned; a map miss drops the file instead of guessing (same invariant as Codex's cwd filter).
.antigravityis first-class internally and collapsed tounknownon the wire, so the server CHECK (corveil#2426) still accepts the upload.- Official hook docs confirm
conversationIdis a top-level common field on every event, so the extractor inEngineRouter.swiftis the right field (camelCase), not a guess at a nested object. hook-eventremains local-only; logsync stays opt-in / default-off.
Concerns:
-
Yellow — payload
transcriptPathis treated as an authoritative filesystem locator, not a hint. The PR's own invariant is that onlyconversationIdis trusted from the unverified hook payload, and that the durable file is "derived from the conversation id regardless" (AntigravityConversationMap.swift:100-102). The implementation does the opposite whenever a hint is present:- Every Antigravity hook carries
transcriptPath(Google's common-input table). The "optional" hint is therefore set on everyrecord(...)call (EngineRouter.swift:88-95,1447-1453). preferredTranscript(AntigravityConversationMap.swift:174-181) usesdirname(hint)/transcript_full.jsonland ignoresbrainDir. Confirmed against the existing testpreferredTranscriptRedirectsToFullBesideARecordedTruncatedFile, which asserts/brain/C/.../transcript_full.jsonlwhilebrainDiris/other/brain.transcripts()existence-checks that candidate andcontinues with no fallback toAntigravityHome.transcriptPath(conversationID:brainDir:)(AntigravityConversationMap.swift:158-161).- Official docs illustrate
transcriptPathas~/.gemini/antigravity/brain/<uuid>/.../transcript.jsonl.FileManager.fileExists(atPath:)does not expand~(reproduced: returns false). A tilde-prefixed hint silently drops a conversation whose derived path would have hit. - The hint is not confined to
brainDir, so a payload path of/tmp/whatever/transcript.jsonlselects/tmp/whatever/transcript_full.jsonlfor collection. Local-only RPC + opt-in logsync keep this from being a remote file-read, but it still contradicts "only conversationId is trusted."
Should-fix: derive the collectable file from
conversationId+brainDir(reject/,.., and empty ids). If the truncated→full rewrite is worth keeping, expand tildes, require the resolved path to be underbrainDir, and fall back to the derived path when the hint misses. - Every Antigravity hook carries
Code Quality
- Green —
recordedCacheis updated before a successful write (AntigravityConversationMap.swift:219-241). A failed first persist (permissions, disk) poisons the cache for that conversation until daemon restart, so later hooks never retry. Move the cache assignment into thedoafterwrite. - Green — stale scanner copy.
BackfillScanner.swift:7-15still says "Three harnesses are wired" and that all of them recover cwd from the file/path; Antigravity is a fourth and recovers cwd only from the map. - Green —
BackfillServiceTests.harnessMappingHelpersstill omits.antigravityeven thoughagentKindRawValuegained a case. A missingcasewould silently stamp Claude's kind on the sidecar; a one-line expect would lock it. - Tests for the intended happy path are solid (map hit / miss / other-worktree / owned worktree vs
workspacePaths/ backfill high vs orphan).swift buildpassed; CrowCore Antigravity tests,AntigravityAgentLogSourcesTests, andAntigravityHookMapCaptureTestsall passed. The docs-derivedtranscript_full.jsonlvs officialtranscript.jsonlfilename split is an accepted risk and is not graded as a blocker.
Summary Table
| Color | Meaning | Verdict effect |
|---|---|---|
| Red | Must fix | Request changes |
| Yellow | Should fix | Request changes |
| Green | Consider | Approve allowed |
Recommendation: Request Changes — driven by 0 Red, 1 Yellow, 3 Green findings.
1471866 to
7d5eb86
Compare
… (CROW-1107) Antigravity's durable transcript records no cwd, so the shared cwdFilter path can't attribute it — CROW-1097 deferred it on those grounds. Wire it instead via a runtime conversation→worktree map: the hook-event handler records `conversationId → worktree` as Crow's Antigravity hooks fire (with the worktree taken from Crow's OWN session ownership, never the payload), and `AntigravityAgent.logSources` reads it back to return exactly that worktree's `transcript_full.jsonl` files. A transcript with no map entry is dropped, never guessed — the same invariant as Codex's cwd filter. - CrowCore: new `AntigravityConversationMap` + `AntigravityHome` (map store + path resolution), and a first-class `LogSyncHarness.antigravity` case wire-collapsed to `unknown`, mirroring `.grok` (corveil#2426). - CrowEngine: hook-event handler captures the pairing (best-effort, deduped, never blocks the hook); `conversationId` extracted defensively across candidate key spellings. - CrowAntigravity: `AntigravityAgent.logSources` override (`.file`/`.jsonl` — no normalizer, already NDJSON). - Backfill reuses the same map: `reconstructAntigravity` looks each transcript's `conversationId` up for its worktree; a map miss → low/orphan. - Docs: session-log-collector.md + harness-transcript-locations.md, Deferred → Wired. Hard-gate deviation (user-approved): `agy` is not installable on the dev machines (closed-source, Google-Sign-In/GCP-authed), so the ticket's Step-1 "live-verify the hook payload fields" gate could not be met. The design mitigates the un-verifiable payload rather than trusting it — the worktree comes from session ownership (not the payload's workspacePaths), and only `conversationId` is read from the payload, so a wrong field name yields "no map entry" (nothing uploaded), never a misattribution. Payload field names remain docs-derived and must be confirmed against a live `agy` before Tier-2 promotion; flagged throughout code + docs. Tests: swift test green (CrowCore 786, CrowAntigravity 44, CrowEngine hook suites, CrowDaemon LogSync/Backfill); `make build` passes. 🐦⬛ Generated with Claude Code, orchestrated by Crow Co-Authored-By: Claude <noreply@anthropic.com> Crow-Session: 7BD2FBF5-AC06-4E44-80DA-E9D17EB5C904
7d5eb86 to
493a534
Compare
…sationId (CROW-1107) Review by @dgershman (1 Yellow + 3 Green). Fixes: - **Yellow — payload `transcriptPath` was an authoritative locator, not a hint.** The collectable file is now ALWAYS derived from `conversationId` + `brainDir` (`AntigravityHome.transcriptPath`), never from the recorded `transcriptPath`. This confines every collected path to `brainDir`, sidesteps the payload's `~` (which `fileExists` won't expand) and any out-of-tree location, and matches the stated invariant "only conversationId is trusted." Added `isPathSafeConversationID` — empty / `.` / `..` / path-separator / NUL ids are rejected at write time and never interpolated into a path at read time. `transcriptPath` is retained purely as untrusted provenance (documented as such) so the pending live-verify can compare agy's reported path against the derived template; `preferredTranscript` (which ignored `brainDir` and had no fallback) is removed. - **Green — dedupe cache poisoned on a failed write.** `recordedCache` is now set only after a durable write (and on the no-change-needed path), so a failed first persist no longer short-circuits later hooks for that conversation into a cache hit that never retries. - **Green — stale scanner doc.** `BackfillScanner` header now says "Six harnesses". - **Green — harness-mapping test.** `BackfillServiceTests.harnessMappingHelpers` now asserts `.antigravity` for both `uploadFormat` (`.jsonl`) and `agentKindRawValue`. New tests: hint-is-ignored derivation, unsafe-conversation-id rejection (write + read + the predicate), and a failed-write-does-not-poison-cache retry. `swift test` green across CrowCore / CrowDaemon / CrowAntigravity / CrowEngine; `make build` passes. 🐦⬛ Generated with Claude Code, orchestrated by Crow Co-Authored-By: Claude <noreply@anthropic.com> Crow-Session: 7BD2FBF5-AC06-4E44-80DA-E9D17EB5C904
|
Thanks @dgershman — all four findings addressed in 69eb1eb. Yellow — Green — cache poisoning. Green — stale scanner doc. Header now reads "Six harnesses" (the body already described Antigravity's map-based attribution). Green — harness-mapping test. New tests cover hint-is-ignored derivation, unsafe-id rejection, and the failed-write-doesn't-poison-cache retry. (Note: the review ran against the pre-rebase commit; the branch has since been rebased over the Cursor #1105 and OpenCode #1104 merges.) |
dgershman
left a comment
There was a problem hiding this comment.
Code & Security Review
Follow-up to the prior request-changes review. The Yellow (payload transcriptPath used as an authoritative locator) and the three Greens are addressed in 69eb1eb.
Critical Issues (if any)
None.
Architecture / Existing Patterns
- Existing pathway:
CodingAgent.logSources→LogSyncCollector/BackfillScanner.assemble. Codex attributes a global store viacwdFilter; Grok partitions by encoded cwd. CROW-1097 already named this follow-up: a runtimeconversationId → worktreemap filled from Crow's own hooks. This PR extends that pathway rather than inventing a second collector. - The map lives in CrowCore (
AntigravityConversationMap) soCrowEnginecan write it andCrowAntigravitycan read it without a new package edge. Worktree is taken fromprimaryWorktree(for:), notworkspacePaths(EngineRouter.swift,AntigravityHookMapCaptureTests). - No architecture finding. The "runtime-map" attribution mode is the right shape for a global store with no cwd.
Security Review
Strengths:
- Worktree attribution is Crow-owned, not payload-owned; a map miss drops the file instead of guessing (same invariant as Codex's cwd filter).
- Previous Yellow, verified fixed.
transcripts(forWorktreePath:brainDir:)always derives the collectable file fromconversationId+brainDirviaAntigravityHome.transcriptPath(AntigravityConversationMap.swift). RecordedtranscriptPathis stored only as untrusted provenance and is never interpolated. Confirmed against the implementation and bytranscriptsIgnoreRecordedHintAndDeriveFromBrainDir(a~/ out-of-tree hint still returns the brain-dir path). isPathSafeConversationIDrejects empty /./..///\/ NUL at write and read time (AntigravityConversationMap.swift), so an untrusted hook id cannot escapebrainDir. Covered byunsafeConversationIDIsNeverInterpolatedIntoAPath..antigravityis first-class internally and collapsed tounknownon the wire, so the server CHECK (corveil#2426) still accepts the upload.hook-eventremains local-only; logsync stays opt-in / default-off.
Concerns:
- None remaining. The docs-derived
conversationIdfield name and brain-dir template are an accepted, clearly caveated risk (wrong field ⇒ no map entry ⇒ nothing uploaded, never a misattribution) and are not re-graded as a blocker.
Code Quality
- Previous Green, verified fixed.
recordedCacheis assigned only after a durable write (and on the no-op path). A failed first persist no longer traps later hooks. Covered byaFailedWriteDoesNotPoisonTheDedupeCache. - Previous Green, verified fixed.
BackfillScannerheader now reads "Six harnesses" and describes Antigravity's map-based attribution. - Previous Green, verified fixed.
BackfillServiceTests.harnessMappingHelpersasserts.antigravityforuploadFormat(.jsonl) andagentKindRawValue. - Tests for map hit / miss / other-worktree / owned worktree vs
workspacePaths/ backfill high vs orphan all passed.swift buildsucceeded; CrowCore Antigravity suites,AntigravityAgentLogSourcesTests,AntigravityHookMapCaptureTests, andharnessMappingHelpersare green.
Summary Table
| Color | Meaning | Verdict effect |
|---|---|---|
| Red | Must fix | Request changes |
| Yellow | Should fix | Request changes |
| Green | Consider | Approve allowed |
Recommendation: Approve — driven by 0 Red, 0 Yellow, 0 Green findings.
Closes #1107
Wiring follow-up to the CROW-1097 research spike (PR #1101). Antigravity's durable transcript (
~/.gemini/antigravity-cli/brain/<conv-id>/.system_generated/logs/transcript_full.jsonl) records no cwd on any line, so the sharedcwdFilterpath can't attribute it — #1097 kept[]on those grounds. This PR wires it via the attribution path #1097 identified: a runtime conversation→worktree map Crow builds from its own hooks.How it works
EngineRouterhook-event handler): when an Antigravity hook fires, recordconversationId → worktreeintoAntigravityConversationMap. The worktree is taken from Crow's own session ownership (primaryWorktree(for:)), never the payload — so there is no possibility of misattribution. Best-effort, deduped, never blocks or fails the hook.AntigravityAgent.logSources): load the map, return exactly this worktree'stranscript_full.jsonlfiles as.file/.jsonlsources (already NDJSON — no normalizer). A transcript with no map entry is dropped, never guessed (same invariant as Codex's cwd filter).LogSyncHarness.antigravity, wire-collapsed tounknown(mirrors.grok; server DB CHECK — corveil#2426 — doesn't accept it yet).reconstructAntigravitylooks each transcript'sconversationIdup for its worktree (map hit → attributed like Codex/Grok; map miss →low/orphan, never guessed). No SQLite-protobuf reader.session-log-collector.md+harness-transcript-locations.mdmoved from Deferred to Wired (adds a "runtime-map" attribution mode).The ticket's Step-1 hard gate is "install + live-verify the hook payload fields before wiring — do not wire against docs alone."
agycannot be installed or run on this machine (crow agents list→available: false; not on PATH; brain dir absent; running it needs Google-Sign-In/GCP auth). Per the ticket owner's decision ("Wire defensively + caveat"), the design mitigates the un-verifiable payload rather than trusting it:workspacePaths— so the ticket's "workspacePaths reliably equals the launch worktree" verification is moot.conversationIdis read from the payload (across candidate spellingsconversationId/conversation_id); the transcript location is otherwise derived from the documented brain-dir template. A wrong payload field name yields "no map entry" (⇒ nothing uploaded), never a misattribution.agybefore Tier-2 promotion."Remaining pre-Tier-2 step: confirm the hook payload field names + brain-dir template against a live
agy.Tests
AntigravityConversationMapTests,AntigravityHomeTests,BackfillScannerTests(map hit → attributed, miss → low), updated harness-typing tests.AntigravityAgentLogSourcesTests(map hit → one source; empty/other-worktree →[]).AntigravityHookMapCaptureTests(records the owned worktree, not the payload'sworkspacePaths; non-antigravity records nothing).LogSyncCollectorTests/BackfillServiceTestsgreen.make buildpasses.🤖 Generated with Claude Code