Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 11 additions & 8 deletions docs/procore/STAGE3_CERTIFICATION_PLAN_V2.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,17 @@

## Status Update - 2026-06-18

This plan has now advanced beyond the original read-only planning state. `main` includes PRs #26-#29:
This plan has now advanced beyond the original read-only planning state. `main` includes PRs #26-#30, and Supabase migrations 007/008 have been applied and verified:

- **PR #26:** live write-back is gated by `PROCORE_LIVE_WRITEBACK_ENABLED=false` by default.
- **PR #27:** migration 008 is hardened with pinned `search_path` and a controlled delete guard.
- **PR #28:** PDF/JWT dependency smoke coverage exists for the upgraded extraction/auth dependencies.
- **PR #29:** metadata-only Procore audit payload builder and graceful Supabase RPC wiring are implemented.
- **PR #30:** Stage 3 evidence was refreshed after hardening and merged at `5e258a1`.
- **Supabase apply:** project `rpd-pims` / `nebdpofqglfyfyqqodni` records `20260618025557 / 007_procore_webhook_deliveries` and `20260618025643 / 008_procore_audit` (`docs/procore/SUPABASE_MIGRATION_APPLY_EVIDENCE_2026-06-18.md:5`, `docs/procore/SUPABASE_MIGRATION_APPLY_EVIDENCE_2026-06-18.md:17-18`).
- **Supabase verification:** private tables exist with RLS enabled (`docs/procore/SUPABASE_MIGRATION_APPLY_EVIDENCE_2026-06-18.md:40-44`); public RPC execute is allowed for `service_role` and denied for `anon`/`authenticated` (`docs/procore/SUPABASE_MIGRATION_APPLY_EVIDENCE_2026-06-18.md:73-119`); audit update/delete triggers exist (`docs/procore/SUPABASE_MIGRATION_APPLY_EVIDENCE_2026-06-18.md:137-155`); direct `private` schema `USAGE` is denied for `anon`/`authenticated`/`service_role` (`docs/procore/SUPABASE_MIGRATION_APPLY_EVIDENCE_2026-06-18.md:172-178`).

The remaining blockers are still real: Procore auth/signature scheme, OAuth/DMSA grant/scopes, write-back resource/path/API version, and `delivery_id`/`ulid` payload location remain `UNVERIFIED`; migrations 007/008 remain `UNAPPLIED`; production env/migration application remains `OPS-GATED`.
The remaining blockers are still real: Procore auth/signature scheme, OAuth/DMSA grant/scopes, write-back resource/path/API version, and `delivery_id`/`ulid` payload location remain `UNVERIFIED`; production env rollout remains `OPS-GATED`; Supabase runtime smoke remains optional and requires explicit approval because it writes synthetic rows.

---

Expand Down Expand Up @@ -147,30 +150,30 @@ Each phase lists objective, key evidence, and exit gate. Per `CLAUDE.md`, any ph
>
> SWMS document content is processed transiently by the Anthropic API for the review step. The applicable data-processing terms (including a Zero-Data-Retention arrangement) are **[status — Alan to confirm; ZDR is an open action item, `PROCORE_TECHNICAL_FEASIBILITY_REPORT.md:135,169`]** and must be locked before this statement is submitted.

**Engineering status for this language:** local JSONL persistence is gated off by default (`PROCORE_LOCAL_JSONL_ENABLED=false`), and PR #29 added metadata-only audit payload/RPC wiring. The statement is still conditional on applying migrations 007/008 after Supabase preflight and confirming final retention/ZDR terms.
**Engineering status for this language:** local JSONL persistence is gated off by default (`PROCORE_LOCAL_JSONL_ENABLED=false`), PR #29 added metadata-only audit payload/RPC wiring, and migrations 007/008 are applied and verified in Supabase (`docs/procore/SUPABASE_MIGRATION_APPLY_EVIDENCE_2026-06-18.md:17-18`, `docs/procore/SUPABASE_MIGRATION_APPLY_EVIDENCE_2026-06-18.md:40-44`). The statement is still conditional on confirming final retention/ZDR terms.

---

## Deliverable 6 — Implementation Tickets / Current Status

> Current status after PRs #24-#29. Several confirmation-independent tickets are implemented; the final T2/T7/T8 production wiring still depends on Procore answers and ops-controlled Supabase migration application.
> Current status after PRs #24-#30 and verified Supabase migration apply. Several confirmation-independent tickets are implemented; the final T2/T7/T8 production wiring still depends on Procore answers and production env rollout.

| ID | Title | Scope (evidence) | Depends on | Acceptance |
|---|---|---|---|---|
| T1 | Disable legacy `/procore` route registration | Implemented in PR #24; legacy route disabled by default behind `PROCORE_LEGACY_ROUTE_ENABLED` | Done | `POST /procore/webhook` returns 404 by default; direct-import legacy tests remain supported |
| T2 | Fail-closed webhook auth (**scheme-agnostic**) | Implemented in PR #24; default `PROCORE_AUTH_SCHEME=unverified` rejects before side effects | Final scheme UNVERIFIED | Candidate HMAC/Bearer verifiers exist; production scheme/header must be set only after Procore confirms |
| T3 | Durable idempotency | Implemented in PR #24; Supabase reserve RPC with raw-body hash fallback | Migrations UNAPPLIED; payload key UNVERIFIED | Duplicate processing is guarded; migration 007 must be applied after ops preflight |
| T3 | Durable idempotency | Implemented in PR #24; Supabase reserve RPC with raw-body hash fallback; migration 007 applied as `20260618025557` | Real payload key location UNVERIFIED; optional runtime smoke not run | Duplicate processing is guarded; verified RPC exists with service-role execute only |
| T4 | Async 202 + dead-letter/failure surface | Implemented in PR #24; `/v1` returns 202 and runs background pipeline with failure alerting | Done | Failures recorded/alerted; worker/queue can remain a later durability upgrade |
| T5 | Gate local JSONL in production | Implemented in PR #25; `PROCORE_LOCAL_JSONL_ENABLED=false` by default | Done | No local Procore-derived JSONL files in production default |
| T6 | Supabase audit tables w/ immutability + retention | Implemented in PR #25 and hardened in PR #27 | Migrations UNAPPLIED; ops preflight required | Migration 008 has private table, RLS, service-role RPCs, pinned trigger search path, controlled retention/delete guard |
| T6 | Supabase audit tables w/ immutability + retention | Implemented in PR #25, hardened in PR #27, applied as `20260618025643` | Optional runtime smoke not run; retention windows/ZDR still require final confirmation | Migration 008 has private table, RLS, service-role RPCs, pinned trigger search path, controlled retention/delete guard |
| T7 | OAuth / DMSA token model | Not implemented; static token model remains interim | Procore grant/scopes UNVERIFIED | Replace `PROCORE_ACCESS_TOKEN` with confirmed OAuth/DMSA model before certification |
| T8 | Advisory comment quality gate + write-back idempotency | Partially implemented: PR #26 gated live write-back; PR #29 added metadata-only audit builder/RPC wiring | Write-back resource/path UNVERIFIED | Final endpoint/path, write-back idempotency, and comment quality gate still require Procore confirmation |
| T9 | SBOM / vulnerability scan in CI | Implemented in PR #25; PR #28 added PDF/JWT dependency smoke coverage | Done | CI fails on vulnerable pinned deps; SBOM artifact produced; extraction/auth smoke coverage exists |
| T10 | Baseline review must not be gated by rule pack | Implemented in PR #24 | Done | Missing rule pack still produces baseline structural review with `project_review_status=UNAVAILABLE` |

---

## Residual UNVERIFIED items (must resolve before submission)
## Residual UNVERIFIED / Explicit-Approval Items (must resolve before submission)

1. Webhook signature/auth scheme — Procore confirmation (D2 Q1).
2. Write-back resource + REST path/version — Procore confirmation (D2 Q3); do **not** invent. Live write-back is safely gated off by default, but the final resource is still `UNVERIFIED`.
Expand All @@ -179,7 +182,7 @@ Each phase lists objective, key evidence, and exit gate. Per `CLAUDE.md`, any ph
5. `tests/test_procore_webhook.py` line citations in action_01 — not re-opened this pass; re-pin on edit.
6. Anthropic ZDR status — Alan (`report:135,169`).
7. Retention windows (12 mo / 30 day) — Alan to set.
8. Supabase migrations 007/008`UNAPPLIED`; apply only after project/schema/service-role preflight.
8. Optional Supabase runtime smokenot run; requires explicit approval because it writes synthetic delivery/audit rows.

---

Expand Down
181 changes: 181 additions & 0 deletions docs/procore/SUPABASE_MIGRATION_APPLY_EVIDENCE_2026-06-18.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
# Supabase Migration Apply Evidence - 2026-06-18

Purpose: non-secret certification evidence for the Procore Stage 3 migration apply and metadata-only verification.

Scope: Supabase project `rpd-pims` / `nebdpofqglfyfyqqodni`. This transcript contains only schema metadata, migration identifiers, function metadata, and privilege booleans. It contains no tokens, service keys, customer data, webhook payloads, SWMS text, or synthetic smoke rows.

Runtime smoke status: **not run**. The runtime smoke remains an explicit-approval checkpoint because it writes synthetic delivery/audit rows.

## Migration History

Source: Supabase MCP `_list_migrations`, project `nebdpofqglfyfyqqodni`.

Relevant result rows:

```json
[
{"version": "20260618025557", "name": "007_procore_webhook_deliveries"},
{"version": "20260618025643", "name": "008_procore_audit"}
]
```

## Table And RLS Metadata

Read-only SQL:

```sql
select n.nspname as schema_name, c.relname as table_name, c.relrowsecurity as rls_enabled
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where (n.nspname, c.relname) in (
('private','procore_webhook_deliveries'),
('private','procore_audit')
)
and c.relkind = 'r'
order by 1,2;
```

Result:

```json
[
{"schema_name": "private", "table_name": "procore_audit", "rls_enabled": true},
{"schema_name": "private", "table_name": "procore_webhook_deliveries", "rls_enabled": true}
]
```

## Public RPC Metadata And Execute Grants

Read-only SQL:

```sql
select n.nspname as schema_name, p.proname as function_name,
pg_get_function_identity_arguments(p.oid) as args,
pg_get_function_result(p.oid) as result_type,
p.prosecdef as security_definer,
p.proconfig as config,
has_function_privilege('service_role', p.oid, 'EXECUTE') as service_role_execute,
has_function_privilege('anon', p.oid, 'EXECUTE') as anon_execute,
has_function_privilege('authenticated', p.oid, 'EXECUTE') as authenticated_execute
from pg_proc p
join pg_namespace n on n.oid = p.pronamespace
where (n.nspname, p.proname) in (
('public','reserve_procore_webhook_delivery'),
('public','record_procore_audit'),
('public','purge_procore_audit'),
('public','delete_procore_audit_for_company')
)
order by schema_name, function_name;
```

Relevant result rows:

```json
[
{
"schema_name": "public",
"function_name": "delete_procore_audit_for_company",
"args": "p_company_id bigint",
"result_type": "integer",
"security_definer": true,
"config": ["search_path=private, public"],
"service_role_execute": true,
"anon_execute": false,
"authenticated_execute": false
},
{
"schema_name": "public",
"function_name": "purge_procore_audit",
"args": "",
"result_type": "integer",
"security_definer": true,
"config": ["search_path=private, public"],
"service_role_execute": true,
"anon_execute": false,
"authenticated_execute": false
},
{
"schema_name": "public",
"function_name": "record_procore_audit",
"args": "p_record jsonb",
"result_type": "bigint",
"security_definer": true,
"config": ["search_path=private, public"],
"service_role_execute": true,
"anon_execute": false,
"authenticated_execute": false
},
{
"schema_name": "public",
"function_name": "reserve_procore_webhook_delivery",
"args": "p_delivery_key text, p_correlation_id text",
"result_type": "boolean",
"security_definer": true,
"config": ["search_path=private, public"],
"service_role_execute": true,
"anon_execute": false,
"authenticated_execute": false
}
]
```

## Audit Trigger Metadata

Read-only SQL:

```sql
select event_object_schema, event_object_table, trigger_name,
action_timing, event_manipulation, action_statement
from information_schema.triggers
where event_object_schema = 'private'
and event_object_table in ('procore_webhook_deliveries','procore_audit')
order by event_object_table, trigger_name, event_manipulation;
```

Result:

```json
[
{
"event_object_schema": "private",
"event_object_table": "procore_audit",
"trigger_name": "no_delete_procore_audit",
"action_timing": "BEFORE",
"event_manipulation": "DELETE",
"action_statement": "EXECUTE FUNCTION private.prevent_procore_audit_mutation()"
},
{
"event_object_schema": "private",
"event_object_table": "procore_audit",
"trigger_name": "no_update_procore_audit",
"action_timing": "BEFORE",
"event_manipulation": "UPDATE",
"action_statement": "EXECUTE FUNCTION private.prevent_procore_audit_mutation()"
}
]
```

## Private Schema Direct Privileges

Read-only SQL:

```sql
select role_name,
has_schema_privilege(role_name, 'private', 'USAGE') as private_usage,
has_schema_privilege(role_name, 'private', 'CREATE') as private_create
from (values ('service_role'), ('anon'), ('authenticated'), ('postgres')) roles(role_name)
order by role_name;
```

Result:

```json
[
{"role_name": "anon", "private_usage": false, "private_create": false},
{"role_name": "authenticated", "private_usage": false, "private_create": false},
{"role_name": "postgres", "private_usage": true, "private_create": true},
{"role_name": "service_role", "private_usage": false, "private_create": false}
]
```

Interpretation: application access is through the public `SECURITY DEFINER` RPCs, not direct private schema access. The private trigger function is not a public API surface.
26 changes: 17 additions & 9 deletions docs/procore/TRACK_A_COMPLETION_REPORT.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# Procore Track A - Evidence Checkpoint

**Date:** 2026-06-18
**Current `main` HEAD:** `0d6160f` (merged PR #29)
**Current `main` HEAD:** `5e258a1` (merged PR #30)
**Source of truth / plan:** `docs/procore/STAGE3_CERTIFICATION_PLAN_V2.md`
**Scope:** Track A plus the confirmation-independent post-review hardening through PRs #26-#29.
**Scope:** Track A plus the confirmation-independent post-review hardening through PR #30 and the verified Supabase migration apply.

This report replaces the older Track A review snapshot that stopped at PR #24/#25 and `1447c27`. It is an evidence checkpoint for Stage 3 certification readiness. It does not claim the Procore-gated items are complete.

Expand Down Expand Up @@ -31,7 +31,7 @@ This report replaces the older Track A review snapshot that stopped at PR #24/#2
### PR #27 - migration 008 hardened before apply
- `private.prevent_procore_audit_mutation()` now pins `search_path`.
- Audit retention/customer deletion no longer depends on the migration owner being `postgres` or `audit_admin`; controlled delete functions use a transaction-local guard.
- Migration 008 remains **UNAPPLIED**.
- Migration 008 was applied after PR #30 alongside migration 007.

### PR #28 - PDF/JWT dependency smoke tests
- Added smoke coverage for real PDF extraction and textless-PDF fallback when `pypdf` is installed.
Expand All @@ -45,6 +45,15 @@ This report replaces the older Track A review snapshot that stopped at PR #24/#2
- Audit write degrades to no-op when Supabase is unconfigured or migration 008/RPC is absent.
- This is partial T8: audit-row wiring is done; final write-back resource/path is still Procore-gated.

### PR #30 + Supabase migration apply - evidence refresh and live schema state
- PR #30 refreshed the Stage 3 evidence after PRs #26-#29 and was merged at `5e258a1`.
- Supabase project `rpd-pims` / `nebdpofqglfyfyqqodni` now records `20260618025557 / 007_procore_webhook_deliveries` and `20260618025643 / 008_procore_audit` (`docs/procore/SUPABASE_MIGRATION_APPLY_EVIDENCE_2026-06-18.md:5`, `docs/procore/SUPABASE_MIGRATION_APPLY_EVIDENCE_2026-06-18.md:17-18`).
- Verified private tables now exist with RLS enabled: `private.procore_webhook_deliveries` and `private.procore_audit` (`docs/procore/SUPABASE_MIGRATION_APPLY_EVIDENCE_2026-06-18.md:40-44`).
- Verified public RPC execute is allowed for `service_role` and denied for `anon`/`authenticated` on reserve/audit/purge/company-delete RPCs (`docs/procore/SUPABASE_MIGRATION_APPLY_EVIDENCE_2026-06-18.md:73-119`).
- Verified audit triggers exist: `no_update_procore_audit` and `no_delete_procore_audit` (`docs/procore/SUPABASE_MIGRATION_APPLY_EVIDENCE_2026-06-18.md:137-155`).
- Verified direct `private` schema `USAGE` is denied for `anon`/`authenticated`/`service_role` (`docs/procore/SUPABASE_MIGRATION_APPLY_EVIDENCE_2026-06-18.md:172-178`).
- Supabase runtime smoke has **not** been run yet; it remains an optional explicit-approval checkpoint because it writes synthetic rows (`docs/procore/SUPABASE_MIGRATION_APPLY_EVIDENCE_2026-06-18.md:7`).

---

## 2. Verification Status
Expand Down Expand Up @@ -82,16 +91,15 @@ Do **not** use an unqualified "no data retained" claim. The correct position is
5. PM/SM directed visibility or two-tier comment support, if required.

### Ops-gated
1. Supabase migration preflight: verify project URL, schema state, existing functions/tables, and service-role access.
2. Apply migrations `007_procore_webhook_deliveries.sql` and `008_procore_audit.sql` only after the preflight is clean.
3. Set production env safely:
1. Set production env safely:
- `PROCORE_REQUIRE_AUTH=true`
- `PROCORE_AUTH_SCHEME` set only after Procore confirms the scheme
- `PROCORE_WEBHOOK_SECRET` / configured signing secret
- `PROCORE_LEGACY_ROUTE_ENABLED=false`
- `PROCORE_LIVE_WRITEBACK_ENABLED=false` until write-back resource is verified
- `PROCORE_LOCAL_JSONL_ENABLED=false`
- `SUPABASE_SERVICE_ROLE_KEY` for durable idempotency/audit
2. Optional Supabase runtime smoke requires explicit approval because it writes synthetic delivery/audit rows.

### Still not complete
1. T7 OAuth/DMSA token model; static `PROCORE_ACCESS_TOKEN` remains a non-certification-ready interim model.
Expand All @@ -106,13 +114,13 @@ Do **not** use an unqualified "no data retained" claim. The correct position is
Procore Submittal event
-> /v1/procore/webhook
-> fail-closed scheme-agnostic auth [scheme UNVERIFIED]
-> durable idempotency reservation, pre-side-effect [migrations UNAPPLIED until ops preflight]
-> durable idempotency reservation, pre-side-effect [migrations applied; payload key UNVERIFIED]
-> 202 Accepted
-> async pipeline: fetch -> extract -> review -> compare -> optional comment
-> live write-back gated off by default
-> failures recorded + alerted
-> metadata-only audit RPC attempted, no-op if Supabase/RPC absent
-> metadata-only audit RPC attempted through verified Supabase RPC
-> human Safety Manager decides in Procore
```

The confirmation-independent foundation is now in place. The remaining work is deliberately blocked on Procore answers and controlled Supabase operations, not more speculative implementation.
The confirmation-independent foundation is now in place. The remaining work is deliberately blocked on Procore answers, production env rollout, and any explicitly approved synthetic Supabase runtime smoke, not more speculative implementation.
Loading