Skip to content

fix: reject generated evidence quotes using source provenance - #237

Open
WTF-Am-ID wants to merge 11 commits into
kunchenguid:mainfrom
WTF-Am-ID:fm/up-backpass-229
Open

WTF-Am-ID wants to merge 11 commits into
kunchenguid:mainfrom
WTF-Am-ID:fm/up-backpass-229

Conversation

@WTF-Am-ID

@WTF-Am-ID WTF-Am-ID commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Closes #229

Intent

Start all upstream opportunties on codex only

Context: this is one of the picks in the combined upstream-opportunities report (data/fm-scan-synthesis/report.md, Part 1, in the primary Firstmate home). The contribution is: Reject evidence quotes that overlap distiller-generated spans, using structural span provenance. Upstream issue: #229. The captain approved contributing all the picks upstream, Codex only: a Codex writer and a Codex reviewer, with no Anthropic model, because the captain is short on Opus until Friday 2026-10-30 and has waived cross-model review for upstream work until then.

What Changed

  • Validate evidence quotes against source event spans, rejecting distiller-generated text even when usedRawTranscript is claimed.
  • Map redacted and rendered tool quotes back to source offsets, and redact evidence before storage.
  • Preserve verified provenance in the gap ledger, exclude unverified legacy observations from corroboration, and invalidate cached analysis.

Risk Assessment

✅ Low: The changes stay within the authorized evidence-provenance fix, and source tracing found no material correctness, privacy, or scope defects.

Testing

Review was single-family (Codex) by the owner's temporary choice. All 89 focused offline tests passed with node --import ./test/helpers/hermetic-config.js --test test/distill.test.js test/analyze-reuse.test.js test/gap-ledger.test.js. An independent baseline comparison reproduced the rendered-tool regression and confirmed the fix. The owner approved proceeding with this test evidence and the live-testing limitation.

Live follow-up is complete at unchanged head f393b8607d900c0907e3b5e3e8543619ed400762. I ran the actual backpass analyze CLI with signed-in Codex through real acpx 0.15.0 and codex-cli 0.165.0, using gpt-6.1-sol with low effort. CODEX_PATH=/opt/homebrew/bin/codex selects the working installed CLI. No source change or push was needed.

The inputs were newly authored, isolated synthetic Pi-format sessions. Diagnostic instructions in each fixture's AGENTS.md requested the specific quote cases. The production analysis prompt, permission controls, schema and validator were unchanged. No canned model response or fake acpx was used.

The commands ran from the respective adversarial/ and genuine/ fixture repos:

CODEX_PATH=/opt/homebrew/bin/codex \
PI_CODING_AGENT_SESSION_DIR=/private/tmp/pr-followups-lWsJvE/bp237/adversarial-sessions \
NO_COLOR=1 node /private/tmp/up-backpass-229.jxmdSF/gate/bin/backpass.js analyze \
  --harness pi --since all --host none --analysis-agent codex \
  --analysis-model gpt-6.1-sol --analysis-effort low --jobs 1 --json
# Same command for genuine, with PI_CODING_AGENT_SESSION_DIR ending in genuine-sessions.
Case Actual CLI result
Fresh generated elision and boundary overlap Exit 0, analyzed 1, failed 0, rejected quotes 2. No evidence items persisted.
Fresh genuine repeated, Unicode and tool-field quotes Exit 0, analyzed 1, failed 0, rejected quotes 0. All four requested quotes persisted with measured source provenance.
Legacy index and ledger upgrade Exit 0, analyzed 1, cached 0. Seeded v4 cache upgraded to v8. Two generated legacy observations became unverified, while the genuine message and redacted rendered-command observations gained source provenance. All other historical fields and timestamps remained unchanged.
Unchanged reruns before and after upgrade Both exit 0, analyzed 0, cached 1, with no model usage.

The fresh model response returned these two exact quotes, both present only because distillation generated the marker:

["[... 2042 chars elided ...]",
 "xxxxx\n\n[... 2042 chars elided ...]\n\nxxxxx"]

The CLI reported quotesNotInTrace: 2, and its stored positive, negative and gaps arrays were empty. The response used usedRawTranscript: false.

The genuine run retained:

Quote Measured source span
The synthetic build skipped lint before packaging. event 1, text, UTF-16 offsets [0,50). The source sentence occurs twice.
🙂 café 数据: the synthetic contract was overlooked. event 4, text, [0,50)
echo \"before\" echo PASSWORD=[redacted] event 5, input.command, [0,49), covering the original two-line command
Build passed for synthetic module. event 5, result, [0,34)

The seeded upgrade uses one real analyzed fixture session and four explicitly synthetic legacy ledger observations. All four remain stored. A separate call to the candidate's ledgerGapObservations interface admitted exactly the two source-backed observations and excluded both generated ones. The synthetic password value was absent from persisted evidence and ledger state. This is cache/ledger revalidation coverage, not a claim of live two-session corroboration or ledger merging.

The two fresh final responses were recovered losslessly from their real ACP agent_message_chunk final-answer events, excluding thought events. SHA-256 of the final-response capture artifacts:

adversarial.final.json 2051b28a248e27c242638545e891b440bb69fca0cfe2c64f1f40467273585dd2
genuine.final.json    3cb04ea3a7d3ae8900b99d6ab771bb82dc1ecdaab58501de8a48054b5893f769

The focused command node --import ./test/helpers/hermetic-config.js --test test/distill.test.js test/analyze-reuse.test.js test/gap-ledger.test.js also exited 0 with 89/89 passing.

Limits: the model did not claim raw access or supply forged source offsets. Those assertions remain covered offline. Growing-session, multi-session ledger merges and full propose/apply were not run live. The historical no-mistakes Test exception remains recorded below. This follow-up supersedes the earlier acpx live-blocked status.

Evidence: Rendered command admission and adversarial rejection
{
  "mode": "offline executable-interface check; no live model",
  "previousRevision": "0ad91bd5e9c386548a97e304127a39166e45bfb1",
  "candidate": "7c4630b1dcfcbe45dbd005e332a5ad7ebc7b73a5",
  "renderedTrace": "# session disposable-provenance-check\nharness: pi\nassociation: tier undefined (undefined)\n\ntool: Bash \"echo \\\"before\\\" echo PASSWORD=[redacted]\"\n\n---\nraw transcript: undefined\nTool calls above are one-line summaries and tool output is truncated. Open the raw\ntranscript only if a specific claim needs the full text.\n",
  "previousRenderedQuoteProvenance": null,
  "candidateEvidence": {
    "positive": [],
    "negative": [],
    "gaps": [
      {
        "mistake": "",
        "proposedInstruction": "Keep genuine rendered evidence.",
        "recurrenceRisk": "medium",
        "quote": "echo \\\"before\\\" echo PASSWORD=[redacted]",
        "quoteProvenance": {
          "kind": "source",
          "event": 0,
          "field": [
            "input",
            "command"
          ],
          "start": 0,
          "end": 51
        },
        "domain": "project"
      }
    ],
    "usedRawTranscript": true,
    "quotesNotInTrace": 0
  },
  "adversarialEvidence": {
    "positive": [],
    "negative": [],
    "gaps": [
      {
        "mistake": "",
        "proposedInstruction": "Accept prefix.",
        "recurrenceRisk": "medium",
        "quote": "echo \\\"before\\\" xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
        "quoteProvenance": {
          "kind": "source",
          "event": 0,
          "field": [
            "input"
          ],
          "start": 0,
          "end": 160
        },
        "domain": "project"
      }
    ],
    "usedRawTranscript": true,
    "quotesNotInTrace": 2
  },
  "secretAbsentFromDistillationAndEvidence": true
}
- Outcome: ⚠️ 2 warnings across 1 run (2m44s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 1 issue found → auto-fixed ✅
  • 🚨 src/distill.js:147 - The requirement says “Genuine quotes copied from a redacted trace must keep passing,” but tool-input rendering still loses provenance. Concrete case: a Bash command contains two lines, echo "before" and echo PASSWORD=abcdefgh. The trace flattens the newline and JSON-escapes the quotes. Copying that command body verbatim matches neither the raw string stored at src/distill.js:147 nor its object-JSON representation at src/distill.js:150. Fresh validation at src/analyze.js:153 and :158 discards the evidence, and legacy revalidation at src/analyze.js:280 marks the same genuine quote unverified. Map rendered tool-input spans through whitespace flattening and JSON escaping to genuine source offsets at the shared provenance boundary, while excluding generated truncation text.

🔧 Fix applied.
✅ Re-checked - no issues remain.

⚠️ **Test** - 2 warnings
  • ⚠️ .test-provenance-baseline/ - The command guard rejected scratch cleanup. Remove the untracked .test-provenance-baseline/ directory containing distill.js, redact.js, and tokens.js before delivery. No alternate deletion route was attempted.
  • ⚠️ live validation verdict: inconclusive (0 of 5 scenarios were driven live against the product); untested: Fresh analysis rejects generated elisions and boundary-overlapping quotes despite claimed raw access or supplied provenance., Fresh analysis preserves genuine redacted, Unicode, repeated, and flattened escaped tool quotes with measured source offsets., Upgrading cached analysis revalidates legacy ledger observations, preserves legitimate history, and excludes generated quotes from corroboration., Growing sessions beyond the capped trace rejects new and persisted generated quotes while retaining genuine corroboration., Reanalysis and ledger merges preserve redacted genuine evidence and provenance without persisting synthetic secrets.
  • Live validation: ⚠️ inconclusive - 0 of 5 scenarios driven live against the product
Scenario Result Live Evidence
Fresh analysis rejects generated elisions and boundary-overlapping quotes despite claimed raw access or supplied provenance. ⏸️ untested no Live model execution was NOT RUN under inbox 018, which prohibits retrying the previously rejected signed-in Codex gpt-6.1-sol environment. Offline executable checks passed. Live validation requires a…
Fresh analysis preserves genuine redacted, Unicode, repeated, and flattened escaped tool quotes with measured source offsets. ⏸️ untested no Live model execution was NOT RUN under inbox 018. Focused offline checks passed, including the two-line Bash regression. Live validation requires a supported model environment and authorization to ret…
Upgrading cached analysis revalidates legacy ledger observations, preserves legitimate history, and excludes generated quotes from corroboration. ⏸️ untested no The prior payload recorded only passing offline CLI integration tests using a fake model response and explicitly marked live=false. It did not establish a live result for upgrading cached analysis.
Growing sessions beyond the capped trace rejects new and persisted generated quotes while retaining genuine corroboration. ⏸️ untested no The prior payload recorded only a passing offline session-growth regression and explicitly marked live=false. It did not establish a live result for session growth.
Reanalysis and ledger merges preserve redacted genuine evidence and provenance without persisting synthetic secrets. ⏸️ untested no The prior payload recorded only passing offline CLI integration and ledger behavior tests and explicitly marked live=false. It did not establish a live result for reanalysis and ledger merges.
  • node --import ./test/helpers/hermetic-config.js --test test/distill.test.js test/analyze-reuse.test.js test/gap-ledger.test.js exited 0; 89 tests passed.
  • Executed an independent distill/sanitizeEvidence probe comparing revision 0ad91bd with the candidate. Reproduced baseline rejection and confirmed candidate admission, generated-overlap rejection, and secret exclusion.
  • git status --short confirmed only the disposable baseline directory remains untracked.
  • Attempted scratch removal; the command guard rejected deletion.
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

@greptile-apps

greptile-apps Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; no new blocking issue was found.

Reviews (4) · Last reviewed commit: "no-mistakes(document): Document rendered..." · Reviewed by Greptile

Comment thread src/distill.js Resolved
…red offset mapping. Regressions cover fresh and legacy quotes, elision rejection, secret exclusion, and provenance through growth and merges. All 86 focused tests, targeted lint/format checks, and typecheck pass. Full suite remains upstream. Cleanup blocked: the command guard refused deletion of scratch file `.ci-redaction-fix.patch`; outer executor cleanup is needed
@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate: thanks @WTF-Am-ID for taking on #229. The structural direction (validating quotes against genuine source events with recorded spans, rather than a marker-text classifier) is what the issue asked for. I reviewed the diff (no workflow, network, or exec changes) and approved fork CI.

This is waiting on you. A few things before it can move:

  1. CI is red on both Ubuntu and macOS: a fatal analysis error stops the run from starting more model calls fails with "the healthy call's evidence survives shutdown" (run). The healthy call's quote probably no longer passes the new source-provenance check in that test's fake transcript.
  2. A stray .ci-redaction-fix.patch (202 lines) is committed at the repo root. Please drop it.
  3. Please drop the VISION.md edit. VISION is Kun's document and isn't changed inside a fix PR; the README and AGENTS.md updates are the right place to describe the new check.
  4. Live evidence. Two of five scenarios are untested because Codex auth was blocked: fresh analysis that returns generated elisions or boundary-overlapping quotes, and fresh analysis with genuine repeated, Unicode, and tool-field quotes. Could you run those live with a signed-in harness and post the output?

VISION per-section verdict

  • Evidence is the only currency: aligns. Distiller markers can no longer stand in for session evidence, and unverified ledger history no longer counts toward the two-session bar.
  • The human owns the weights: aligns. No change to the review gate.
  • Nothing the model says is taken on faith: aligns. A model's usedRawTranscript claim no longer bypasses the check, and spans are measured.
  • The budget is the constraint: not affected.
  • It reads what you already have: aligns.
  • Failure is loud and named: aligns. Discarded quotes are still reported on stderr; legacy observations stay on disk.
  • Scope: aligns.

Contract class: new-default. The README currently specifies that the quote check is skipped when the analysis reports usedRawTranscript, and existing ledger observations count toward corroboration. This PR removes that bypass and stops unverified legacy observations from counting, so an unconfigured run behaves differently on a specified path. Once the items above are cleared, this goes to Kun for a merge decision rather than auto-merge. No rebase needed from you.

…by both CI failures to expect measured source provenance. All 36 focused tests pass, along with targeted ESLint, Prettier, and diff checks. Production behavior is unchanged. Full suite remains upstream
@WTF-Am-ID WTF-Am-ID changed the title fix: require source provenance for evidence quotes fix: reject generated evidence quotes using source provenance Oct 28, 2026
@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate: thanks @WTF-Am-ID. The stray .ci-redaction-fix.patch and the VISION.md edit are gone, and CI is now green on Ubuntu and macOS at f393b86 (I approved the fork runs after checking the new commits).

This is still waiting on you for live evidence. The PR body now shows 0 of 5 scenarios driven live. Before this goes to Kun, could you run at least fresh analysis that returns generated elisions or boundary-overlapping quotes, and fresh analysis with genuine repeated, Unicode, and tool-field quotes, with a signed-in harness, and post the output? The cache-upgrade and ledger scenarios would be good too.

VISION per-rule verdict is unchanged from my last comment (aligns on every rule). Contract-class: new-default (removes the documented usedRawTranscript bypass and stops unverified legacy observations from counting), so once live evidence is in, this goes to Kun for a merge decision rather than auto-merge. No rebase needed from you.

@WTF-Am-ID

Copy link
Copy Markdown
Contributor Author

Live follow-up is complete at unchanged head f393b8607d900c0907e3b5e3e8543619ed400762. I ran the actual backpass analyze CLI with signed-in Codex through real acpx 0.15.0 and codex-cli 0.165.0, using gpt-6.1-sol with low effort. CODEX_PATH=/opt/homebrew/bin/codex selects the working installed CLI. No source change or push was needed.

The inputs were newly authored, isolated synthetic Pi-format sessions. Diagnostic instructions in each fixture's AGENTS.md requested the specific quote cases. The production analysis prompt, permission controls, schema and validator were unchanged. No canned model response or fake acpx was used.

The commands ran from the respective adversarial/ and genuine/ fixture repos:

CODEX_PATH=/opt/homebrew/bin/codex \
PI_CODING_AGENT_SESSION_DIR=/private/tmp/pr-followups-lWsJvE/bp237/adversarial-sessions \
NO_COLOR=1 node /private/tmp/up-backpass-229.jxmdSF/gate/bin/backpass.js analyze \
  --harness pi --since all --host none --analysis-agent codex \
  --analysis-model gpt-6.1-sol --analysis-effort low --jobs 1 --json
# Same command for genuine, with PI_CODING_AGENT_SESSION_DIR ending in genuine-sessions.
Case Actual CLI result
Fresh generated elision and boundary overlap Exit 0, analyzed 1, failed 0, rejected quotes 2. No evidence items persisted.
Fresh genuine repeated, Unicode and tool-field quotes Exit 0, analyzed 1, failed 0, rejected quotes 0. All four requested quotes persisted with measured source provenance.
Legacy index and ledger upgrade Exit 0, analyzed 1, cached 0. Seeded v4 cache upgraded to v8. Two generated legacy observations became unverified, while the genuine message and redacted rendered-command observations gained source provenance. All other historical fields and timestamps remained unchanged.
Unchanged reruns before and after upgrade Both exit 0, analyzed 0, cached 1, with no model usage.

The fresh model response returned these two exact quotes, both present only because distillation generated the marker:

["[... 2042 chars elided ...]",
 "xxxxx\n\n[... 2042 chars elided ...]\n\nxxxxx"]

The CLI reported quotesNotInTrace: 2, and its stored positive, negative and gaps arrays were empty. The response used usedRawTranscript: false.

The genuine run retained:

Quote Measured source span
The synthetic build skipped lint before packaging. event 1, text, UTF-16 offsets [0,50). The source sentence occurs twice.
🙂 café 数据: the synthetic contract was overlooked. event 4, text, [0,50)
echo \"before\" echo PASSWORD=[redacted] event 5, input.command, [0,49), covering the original two-line command
Build passed for synthetic module. event 5, result, [0,34)

The seeded upgrade uses one real analyzed fixture session and four explicitly synthetic legacy ledger observations. All four remain stored. A separate call to the candidate's ledgerGapObservations interface admitted exactly the two source-backed observations and excluded both generated ones. The synthetic password value was absent from persisted evidence and ledger state. This is cache/ledger revalidation coverage, not a claim of live two-session corroboration or ledger merging.

The two fresh final responses were recovered losslessly from their real ACP agent_message_chunk final-answer events, excluding thought events. SHA-256 of the final-response capture artifacts:

adversarial.final.json 2051b28a248e27c242638545e891b440bb69fca0cfe2c64f1f40467273585dd2
genuine.final.json    3cb04ea3a7d3ae8900b99d6ab771bb82dc1ecdaab58501de8a48054b5893f769

The focused command node --import ./test/helpers/hermetic-config.js --test test/distill.test.js test/analyze-reuse.test.js test/gap-ledger.test.js also exited 0 with 89/89 passing.

Limits: the model did not claim raw access or supply forged source offsets. Those assertions remain covered offline. Growing-session, multi-session ledger merges and full propose/apply were not run live. The historical no-mistakes Test exception remains recorded in the PR body. This follow-up supersedes the earlier acpx live-blocked status.

@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate: thanks @WTF-Am-ID, the live runs cover what I asked for (generated elision and boundary-overlap quotes rejected, genuine repeated, Unicode, and tool-field quotes kept with measured spans, and the legacy cache and ledger upgrade). CI is green at f393b86.

This is now waiting on Kun, not on you. VISION per-rule verdict is unchanged (aligns on every rule). Contract-class: new-default, since it removes the documented usedRawTranscript bypass and stops unverified legacy observations from counting, so it needs Kun's merge decision rather than auto-merge. Nothing more is needed from you, including no rebase.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Distiller elision markers are accepted as verbatim evidence quotes

2 participants