Skip to content

Project evidence artifacts into the durable evidence_artifacts table (Epic 6) - #15

Merged
Lvvphole merged 1 commit into
mainfrom
claude/epic-6-evidence-artifacts
Jun 24, 2026
Merged

Project evidence artifacts into the durable evidence_artifacts table (Epic 6)#15
Lvvphole merged 1 commit into
mainfrom
claude/epic-6-evidence-artifacts

Conversation

@Lvvphole

Copy link
Copy Markdown
Owner

What & why

The evidence_artifacts table (with an attempt_id FK into run_attempts) has existed since Epic 2 but was never written. Epic 3 populated run_attempts and scoped artifacts per attempt on disk — leaving a populated parent table, real on-disk evidence, and an empty child table. This PR closes that loop: each run's hashed evidence artifacts are projected into evidence_artifacts, linked to the producing attempt.

The core obstacle

The rich Artifact records (sha256 + path) accumulate in ChainContext.artifacts, which never left the executor — ChainExecutionResult only carried artifact names. So the change surfaces those records out of execution and into the record/projection, mirroring how attempts already works.

Changes

  • Artifact.attempt_id added; ArtifactStore.write stamps it from the store's per-attempt scope (Epic 3) — every artifact is self-describing about which attempt produced it.
  • ChainExecutionResult.evidence_artifacts in-process carrier, Field(..., exclude=True) so it is never in an API response. Artifact.path is an absolute host path — same disclosure class as the workspace_id leak fixed earlier — so it stays server-side only.
  • RunRecord.artifacts embedded and round-tripped via the snapshot (like attempts); the route captures result.evidence_artifacts after each attempt, so retries accumulate.
  • Postgres projection: delete + reinsert evidence_artifacts per save (same idiom as handler_results), inserted after the run_attempts upsert so the FK target always exists. Path stored in object_uri (operator-queryable, never API-exposed); used_as_evidence = TRUE (quarantined agent narrative is never added to record.artifacts, so it never lands here).
  • Docs updated: persistence.md, workspace_isolation.md, README "Deferred" entry.

Tests (276 passed, 10 skipped — Postgres tests skip without a DSN)

  • ArtifactStore.write stamps / omits attempt_id per scope.
  • Record round-trip carries artifacts (memory + serialization).
  • Postgres-gated: evidence_artifacts projects with the attempt link, sha256, used_as_evidence, and is idempotent across re-saves.
  • Route captures artifacts tagged with the attempt; the carrier is excluded from /execute and /results responses.

Scope / safety

No auth/RBAC, no object-storage backend (that's slice b), no change to verifier/gate authority, no merge/deploy/bypass. git diff --check clean; security review clean.

Note: while testing I found a pre-existing absolute-path leak in HandlerResult.artifacts_created/artifacts (untouched by this PR) — being addressed in a separate follow-up.

🤖 Generated with Claude Code


Generated by Claude Code

The evidence_artifacts table (with an attempt_id FK into run_attempts) has
existed since Epic 2 but was never written. Epic 3 populated run_attempts
and scoped artifacts per attempt on disk; this closes the loop by
projecting each run's hashed evidence artifacts into evidence_artifacts,
linked to the producing attempt.

The Artifact records (sha256 + path) accumulate in ChainContext.artifacts,
which never left the executor. Surface them via an in-process carrier on
ChainExecutionResult (evidence_artifacts, exclude=True so it is never in an
API response — Artifact.path is an absolute host path, same leak class as
the workspace_id fix), capture them onto RunRecord.artifacts in the route,
and project them in the Postgres adapter (delete + reinsert, mirroring
handler_results; inserted after run_attempts so the FK target exists).

- Artifact gains attempt_id; ArtifactStore.write stamps it from its scope
- RunRecord.artifacts embedded + round-trips via snapshot (like attempts)
- evidence_artifacts rows store the path in object_uri (server-side only);
  used_as_evidence=TRUE (quarantined narrative never enters record.artifacts)
- Docs: persistence.md, workspace_isolation.md, README updated

Tests: ArtifactStore stamps attempt_id; record round-trip carries artifacts;
Postgres-gated projection + idempotency; route captures artifacts with the
attempt link; the carrier is excluded from execute/results responses.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015R7wjqoCZDF1pqNp8AiUJ6
@Lvvphole
Lvvphole merged commit e99eccd into main Jun 24, 2026
1 check passed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 93798d86d1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

agent_self_certification_used=context.agent_self_certification_used,
auto_merge=False,
auto_deploy=False,
evidence_artifacts=list(context.artifacts),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include ledgered LLM invocation artifacts in the projection

For runs that invoke a configured agent (the default runtime uses the fake analyst), LLMInvocationRecorder writes llm_invocation_*.json and appends it to the evidence ledger, but it writes directly to the store/evidence writer rather than through ChainContext.record_artifact, so it never enters context.artifacts. Since the durable carrier is populated only from context.artifacts here, those ledgered LLM_INVOCATION artifacts are omitted from RunRecord.artifacts and therefore never projected into evidence_artifacts, leaving the Postgres audit table incomplete for agent-backed executions.

Useful? React with 👍 / 👎.

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.

2 participants