Skip to content

fix(compliance): normalize anchor timestamps#925

Merged
ericodom merged 1 commit into
mainfrom
codex/fix-compliance-anchor-timestamp
May 7, 2026
Merged

fix(compliance): normalize anchor timestamps#925
ericodom merged 1 commit into
mainfrom
codex/fix-compliance-anchor-timestamp

Conversation

@ericodom
Copy link
Copy Markdown
Contributor

@ericodom ericodom commented May 7, 2026

Summary

  • normalize raw SQL recorded_at values from compliance chain-head reads before Drizzle writes them back to tenant_anchor_state
  • add regression coverage for timestamp strings returned from readerDb.execute

Root cause

The dev deploy failed in Compliance Anchor Smoke because readChainHeads typed raw SQL recorded_at as Date, but the deployed pg path returned it as a string. Drizzle then attempted to serialize that string into a timestamp column during the tenant_anchor_state upsert and threw e.toISOString is not a function.

Verification

  • pnpm --filter @thinkwork/lambda test -- compliance-anchor.test.ts
  • pnpm --filter @thinkwork/lambda typecheck
  • git diff --check
  • bash scripts/build-lambdas.sh compliance-anchor

@ericodom ericodom merged commit b540f37 into main May 7, 2026
5 checks passed
@ericodom ericodom deleted the codex/fix-compliance-anchor-timestamp branch May 7, 2026 22:31
ericodom added a commit that referenced this pull request May 7, 2026
`AnchorFn` is now `=> Promise<...>` in U8b. The timestamp-normalization
test added in #925 used a sync stub, which fails typecheck against the
new contract. Switch the stub to `async () => ({ anchored: false })` —
test still exercises the same path (recorded_at coercion → drainer
update) since runAnchorPass awaits the result either way.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ericodom added a commit that referenced this pull request May 7, 2026
…k retention) (#927)

* feat(compliance): U8b — anchor Lambda live (S3 PutObject + Object Lock retention)

Replaces `_anchor_fn_inert` with `_anchor_fn_live`, which performs the
actual S3 PutObject of per-tenant proof slices and the global anchor
JSON to the WORM-locked compliance bucket. The anchor object carries an
explicit `ObjectLockMode` + `ObjectLockRetainUntilDate` per-object
override (mirroring the bucket-default), so the retention contract is
portable across buckets and visible at the call site. Slices write
under `proofs/tenant-{id}/cadence-{cadence_id}.json` (no per-object
lock; bucket default applies); anchor writes last so a partial failure
never publishes a verifier-discoverable commit point.

Five guards land alongside the body swap:

  * **Deterministic cadence_id** — sha256 of canonical chain-head
    fingerprint, reshaped to UUIDv7 form. Same heads produce the same
    cadence_id, so a retry after a partial failure overwrites its own
    slice keys instead of orphaning slices for the full 365-day
    retention window.
  * **Merkle self-check** — `_anchor_fn_live` recomputes the root from
    the received leaves and asserts equality before any PutObject. Cheap
    insurance against latent runAnchorPass arithmetic bugs becoming
    WORM-locked poisoned evidence.
  * **Layer 2 body-swap test** — `compliance-anchor-s3-spy.test.ts`
    mocks S3Client.send and asserts the live function actually issues
    PutObjectCommand for both slices and anchor (with SHA256 checksum,
    SSE-KMS, and ObjectLock retention on the anchor key only). Pairs
    with the Layer 1 identity assertion (`getWiredAnchorFn() ===
    _anchor_fn_live`) in the integration test.
  * **Sibling watchdog IAM role** — watchdog moves OFF the shared
    lambda role onto a dedicated role with `kms:DescribeKey` only on
    the bucket CMK (NOT `kms:Decrypt` — the watchdog never reads
    object bodies), `s3:ListBucket` prefix-conditioned on `anchors/`,
    and an explicit Deny on every Delete + Bypass + Lock-mutation
    action so future role broadening cannot turn the watchdog into a
    deletion vector.
  * **Dev-COMPLIANCE precondition** — `var.allow_compliance_in_non_prod`
    (default false) blocks accidentally locking a dev bucket into
    irreversible COMPLIANCE bytes via a stage typo.

Watchdog flips to live: `mode: "live"`, ListObjectsV2 with 1000-key
truncation warning, max-LastModified pick, `ComplianceAnchorGap` metric
emission (suppressed on greenfield-empty bucket), heartbeat unchanged.
The CloudWatch alarm cuts over: gap → `treat_missing_data = breaching`
(catches both real gaps and a watchdog-down regression); a sibling
heartbeat-missing alarm is born `notBreaching` so deploy-time gaps
don't fire it before the first heartbeat lands (Decision #7).

Operator pre-merge step: `terraform state mv` the watchdog from the
for_each handler set to the new standalone resource address. Without
it, the next `terraform apply` fails with ResourceConflictException on
the function name. Plan documents the exact command.

Plan: docs/plans/2026-05-07-012-feat-compliance-u8b-anchor-lambda-live-plan.md
Master plan: docs/plans/2026-05-06-011-feat-compliance-audit-event-log-plan.md (U8b)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(review): apply autofix feedback

Drop unused drizzle-orm imports flagged by ce-code-review:
- compliance-anchor.ts: `and`, `eq`, `gt`, plus the `auditEvents` schema
  import (raw SQL via `` sql`...` `` is the actual codepath there)
- compliance-anchor.integration.test.ts: `and`, `gt`, `auditOutbox`

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(compliance): make compliance-anchor.test.ts stub anchorFn async

`AnchorFn` is now `=> Promise<...>` in U8b. The timestamp-normalization
test added in #925 used a sync stub, which fails typecheck against the
new contract. Switch the stub to `async () => ({ anchored: false })` —
test still exercises the same path (recorded_at coercion → drainer
update) since runAnchorPass awaits the result either way.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ericodom added a commit that referenced this pull request May 8, 2026
Comprehensive docs for the compliance audit-event-log feature shipped
via U1–U11. Eight files covering four audience cuts (operator,
auditor, developer, on-call) plus three structural docs (README,
overview, architecture) and a chronological changelog.

- docs/compliance/README.md — entry point + navigation table.
- docs/compliance/overview.md — what the module does + non-goals +
  U1–U11 roster with PR links + strategic context.
- docs/compliance/architecture.md — Mermaid substrate diagram + ASCII
  fallback + Aurora role split table + S3 prefix contract +
  RFC 6962 hash chain explainer + 14-event slate + tier semantics.
- docs/compliance/operator-runbook.md — 8 procedures (inspect events,
  request export, apply hand-rolled migration, rotate Aurora roles,
  GOVERNANCE→COMPLIANCE flip, drain anchor/exports DLQs, re-run
  failed export). Each procedure starts with "When to use this".
- docs/compliance/auditor-walkthrough.md — 10-step SOC2 Type 1
  narrative with screenshot placeholders to fill in during U11.U5
  rehearsal. Covers admin browse, walk-back-10-events, export +
  download, audit-verifier CLI run.
- docs/compliance/developer-guide.md — adding a new event type
  (6 numbered steps from schema to test), tier semantics table,
  cross-runtime emit path (Strands), adding a new compliance
  Lambda (5-step checklist), where the tests live (15-row table).
- docs/compliance/oncall.md — alarm-to-playbook quick reference +
  procedures for anchor DLQ, exports DLQ, watchdog heartbeat
  missing, anchor gap, drift gate failure, audit_outbox runaway,
  Strands env shadowing. Plus irreversibility warnings list.
- docs/compliance/changelog.md — chronological PR table for every
  compliance commit + bordering fixes (#895, #905, #925, #942).
- CLAUDE.md — one-line pointer to docs/compliance/README.md added
  to the existing docs/ bullet.

Plan: docs/plans/2026-05-08-008-docs-compliance-module-documentation-plan.md

Verification:
- All 26 cited file paths verified to resolve on origin/main HEAD.
- All 13 cited test paths verified.
- All 17 cited PR numbers verified MERGED via gh pr view.
- Aurora role GRANTs transcribed verbatim from
  packages/database-pg/drizzle/0070_compliance_aurora_roles.sql.
- 14-event slate transcribed verbatim from
  packages/database-pg/src/schema/compliance.ts COMPLIANCE_EVENT_TYPES.
- Markdown renders cleanly (no broken table syntax in GitHub preview).

Deferred: U11.U5 SOC2 rehearsal will capture the 10 screenshots
listed in auditor-walkthrough.md and validate the walkthrough
script against actual deployed-dev UI.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

1 participant