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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
91 changes: 91 additions & 0 deletions .github/workflows/pr89-seam-receipt.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
name: pr89-seam-receipt

on:
pull_request:
paths:
- ".github/workflows/pr89-seam-receipt.yml"
- "design-notes/seam-spike/**"
- "openspec/changes/harden-pdpp-authorization-and-0-1-migration/**"
- "package.json"
- "packages/reference-contract/**"
- "pnpm-lock.yaml"
- "pnpm-workspace.yaml"
- "reference-implementation/**"
- "scripts/test-accounting/**"
- "test-accounting.manifest.json"
push:
branches: [main]
paths:
- ".github/workflows/pr89-seam-receipt.yml"
- "design-notes/seam-spike/**"
- "openspec/changes/harden-pdpp-authorization-and-0-1-migration/**"
- "package.json"
- "packages/reference-contract/**"
- "pnpm-lock.yaml"
- "pnpm-workspace.yaml"
- "reference-implementation/**"
- "scripts/test-accounting/**"
- "test-accounting.manifest.json"
workflow_dispatch: {}

permissions:
contents: read

env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"

jobs:
pr89-seam-receipt:
runs-on: ubuntu-latest
timeout-minutes: 30
services:
postgres:
image: postgres:16
env:
POSTGRES_DB: pdpp_pr89
POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U postgres -d pdpp_pr89"
--health-interval 5s
--health-timeout 5s
--health-retries 12
env:
PDPP_TEST_POSTGRES_URL: postgresql://postgres:postgres@127.0.0.1:5432/pdpp_pr89
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
cache: pnpm
node-version: "24"
- name: Install dependencies
env:
PATCHRIGHT_SKIP_BROWSER_DOWNLOAD: "1"
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1"
run: pnpm install --frozen-lockfile
- name: Test receipt derivation and fail-closed checks
run: pnpm --filter pdpp-reference-implementation test:seam:pr89:receipt
- name: Validate PR89 OpenSpec target
run: pnpm dlx @fission-ai/openspec@1.8.0 validate harden-pdpp-authorization-and-0-1-migration --strict
- name: Execute all seven cases and generate receipt
id: seam
continue-on-error: true
run: pnpm --filter pdpp-reference-implementation test:seam:pr89 -- --backend postgresql
- name: Validate generated receipt against executed evidence
id: receipt
if: always()
continue-on-error: true
run: pnpm --filter pdpp-reference-implementation check:seam:pr89:receipt
- name: Require execution and receipt validation
if: always()
env:
RECEIPT_OUTCOME: ${{ steps.receipt.outcome }}
SEAM_OUTCOME: ${{ steps.seam.outcome }}
run: |
test "$SEAM_OUTCOME" = "success"
test "$RECEIPT_OUTCOME" = "success"
2 changes: 2 additions & 0 deletions .github/workflows/reference-implementation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,8 @@ jobs:
if: steps.changes.outputs.reference_impacting == 'true'
env:
PDPP_TEST_CONCURRENCY: "2"
PDPP_TEST_FILE_HARD_TIMEOUT_MS: "900000"
PDPP_TEST_FILE_TIMEOUT_MS: "120000"
run: pnpm --dir reference-implementation run test

- name: Report non-reference skip
Expand Down
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ reference implementation, and supporting documentation.

The protocol is defined by the `spec-*.md` files at the repository root. Not
every root spec carries the same authority: each file states its own status in
a header near the top, and that header governs. Two of them are the normative
a header near the top, and that header governs. Three of them are the normative
protocol; the rest are informative rationale, illustrative examples, or
historical material superseded by the normative text. Where any downstream
document, example, or superseded spec disagrees with the normative specs, the
Expand All @@ -23,6 +23,7 @@ normative specs prevail.
**Normative** — the protocol itself. Read these to implement PDPP:

- [`spec-core.md`](spec-core.md) — core protocol: grants, sources, records, and the query surface (*Normative draft*). Core Section 8 is the authoritative definition of the resource-server query interface.
- [`spec-discovery-and-trust.md`](spec-discovery-and-trust.md): source onboarding, provider-native discovery, authority, and accepted declaration revisions (*Companion specification draft*)
- [`spec-collection-profile.md`](spec-collection-profile.md) — how a source is declared and collected; a companion profile to Core (*Companion profile draft*)

**Informative** — rationale and context. These explain and situate the
Expand Down Expand Up @@ -79,8 +80,9 @@ are not the protocol boundary. The durable boundary between them lives in the

This repository uses a strict authority order:

1. **Root PDPP specs** (`spec-*.md`) define the protocol. The two normative
specs (`spec-core.md`, `spec-collection-profile.md`) define protocol
1. **Root PDPP specs** (`spec-*.md`) define the protocol. The three normative
specs (`spec-core.md`, `spec-discovery-and-trust.md`, and
`spec-collection-profile.md`) define protocol
semantics; the other root specs are informative, illustrative, or
superseded, as each file's status header states.
2. **Code and tests** define what the reference implementation actually does.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ const FAILED_RUNS: RunSummary[] = [
const PENDING: PendingApproval[] = [
{
approval_id: "appr_atlas",
batch: false,
client_id: "Atlas Mortgage",
created_at: iso(0),
grant_preview: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ test("relDay produces calm relative labels", () => {
test("hero is DECIDE when an approval is pending", () => {
const pending: PendingApproval = {
approval_id: "a1",
batch: false,
client_id: "Atlas Mortgage",
created_at: NOW.toISOString(),
grant_preview: { streams: [{ name: "pay_statements" }, { name: "transactions" }] },
Expand Down Expand Up @@ -264,6 +265,7 @@ test("failed syncs/traces alone do NOT drive the alarm — only the rendered-ver
test("decide wins over alarm", () => {
const pending = {
approval_id: "a1",
batch: false,
created_at: NOW.toISOString(),
kind: "consent",
object: "approval",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
// Copyright The PDP-Connect Contributors
// SPDX-License-Identifier: Apache-2.0

import assert from "node:assert/strict";
import test from "node:test";
import React from "react";
import { renderToStaticMarkup } from "react-dom/server";
import type { ApprovalReview, ConsentApprovalReview, SingleConsentApprovalArtifact } from "../../../lib/ref-client.ts";
import { ApprovalReview as ApprovalReviewView } from "./approval-review.tsx";

(globalThis as { React?: typeof React }).React = React;

const APPROVAL_BUTTON_RE = /Approve and issue grant/;
const APPROVAL_CONFIRMATION_RE = /name="approval_confirmation"[^>]*value="approve"/;
const APPROVAL_REVISION_RE =
/name="approval_review_revision"[^>]*value="reference\.approval-review\.v1:sha256:reviewDigest"/;
const REQUEST_URI_RE = /name="request_uri"[^>]*value="urn:pdpp:pending-consent:dc_reviewed"/;
const NO_APPROVAL_BUTTON_RE = /<button[^>]*>Approve and issue grant<\/button>/;
const NO_DEVICE_SECRET_RE = /device_code|user_code|params_json/;
const OWNER_DEVICE_CONTROL_RE = /Owner device control/;
const OWNER_DEVICE_WARNING_RE = /not a scoped third-party data grant/;
const REVIEW_CONTINUE_RE = /Continue to approval/;
const BATCH_NON_ACTIONABLE_RE = /Batch approval is not available from this console review/;
const BATCH_ARTIFACT_VERSION_RE = /reference\.batch-approval-review\.v1/;
const NO_APPROVAL_ID_RE = /name="approval_id"[^>]*value="dc_/;
const NO_MUTABLE_CONSENT_RE = /name="subject_id"|name="ai_training_consented"/;
const NO_OWNER_DEVICE_SCOPE_RE = /Resolved streams|Purpose/;

function noAction(): void {
// Rendering proof only.
}

function artifact(overrides: Partial<SingleConsentApprovalArtifact> = {}): SingleConsentApprovalArtifact {
return {
access_mode: "continuous",
ai_training_consented: true,
client: {
client_display: {
name: "Concert Finder",
policy_uri: "https://concert.example/policy",
tos_uri: "data:text/html,owned",
uri: "javascript:alert(1)",
},
client_id: "concert_finder",
registration_mode: "pre_registered_public",
},
expires_at: "2026-08-11T12:10:00.000Z",
client_claims: { commitments: ["Use only for concert recommendations"] },
purpose_code: "https://pdpp.org/purpose/ai_training",
purpose_description: "Train a concert-ranking model.",
resolved_streams: [
{
fields: ["name", "popularity"],
instance_ids: ["cin_music_primary", "cin_music_backup"],
name: "top_artists",
resources: ["artist_1", "artist_2"],
time_constraint: { field: "played_at", since: "2026-01-01", until: "2026-02-01" },
},
],
retention: { max_duration: "P30D", on_expiry: "delete" },
selection_preset: "music-profile",
source: { id: "spotify", kind: "connector" },
source_declaration: {
accepted_revision_reference: "accepted-rev-1",
digest: "sha256:sourceDigest",
publisher_attribution: { id: "https://pdpp.dev/reference-implementation", status: "unverified" },
resource_authority: { authority_binding: "https://spotify.example/pdpp", status: "verified" },
version: "reference.source-declaration.v1",
},
subject: { id: "owner_local" },
version: "reference.approval-review.v1",
...overrides,
};
}

function consent(overrides: Partial<ConsentApprovalReview> = {}): ConsentApprovalReview {
return {
approval_id: "apr_review_safe",
approval_review: artifact(),
approval_review_revision: "reference.approval-review.v1:sha256:reviewDigest",
batch: false,
kind: "consent",
object: "approval_review",
request_uri: "urn:pdpp:pending-consent:dc_reviewed",
...overrides,
};
}

function render(detail: ApprovalReview, confirm = false): string {
return renderToStaticMarkup(ApprovalReviewView({ approveAction: noAction, confirm, denyAction: noAction, detail }));
}

test("single consent review renders PR114 artifact authority and no approval submit before confirmation", () => {
const html = render(consent());
for (const label of [
"reference.approval-review.v1",
"owner_local",
"Concert Finder",
"concert_finder",
"spotify",
"reference.source-declaration.v1 / sha256:sourceDigest",
"accepted revision accepted-rev-1",
"publisher https://pdpp.dev/reference-implementation (unverified)",
"resource authority verified: https://spotify.example/pdpp",
"Use only for concert recommendations",
"music-profile",
"Train a concert-ranking model.",
"continuous",
"true",
"P30D",
"cin_music_primary, cin_music_backup",
"name, popularity",
"artist_1, artist_2",
"played_at: 2026-01-01 to 2026-02-01",
"Exact reviewed artifact",
]) {
assert.match(html, new RegExp(label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
}
assert.match(html, REVIEW_CONTINUE_RE);
assert.doesNotMatch(html, NO_APPROVAL_BUTTON_RE);
assert.doesNotMatch(html, NO_DEVICE_SECRET_RE);
});

test("final consent confirmation carries the exact reviewed request uri and revision", () => {
const html = render(consent(), true);
assert.match(html, APPROVAL_BUTTON_RE);
assert.match(html, APPROVAL_CONFIRMATION_RE);
assert.match(html, APPROVAL_REVISION_RE);
assert.match(html, REQUEST_URI_RE);
assert.doesNotMatch(html, NO_APPROVAL_ID_RE);
assert.doesNotMatch(html, NO_MUTABLE_CONSENT_RE);
});

test("batch review is explicit and non-actionable in the console", () => {
const html = render(
consent({
approval_review: {
access_mode: null,
approved_source_indexes: [0],
client: artifact().client,
expires_at: "2026-08-11T12:10:00.000Z",
parent_package_id: "pkg_1",
source_narrowing: { "0": { streams: ["top_artists"] } },
sources: [
{
access_mode: "continuous",
client_claims: { commitments: ["Only use this approved source for batch recommendations"] },
index: 0,
purpose_code: "https://pdpp.org/purpose/ai_training",
purpose_description: "Train a concert-ranking model.",
resolved_streams: artifact().resolved_streams,
retention: { max_duration: "P30D" },
selection_preset: "music-profile",
source: { id: "spotify", kind: "connector" },
source_declaration: { digest: "sha256:sourceDigest", version: "reference.source-declaration.v1" },
},
],
subject: { id: "owner_local" },
version: "reference.batch-approval-review.v1",
},
batch: true,
}),
true
);
assert.match(html, BATCH_ARTIFACT_VERSION_RE);
assert.match(html, BATCH_NON_ACTIONABLE_RE);
assert.doesNotMatch(html, NO_APPROVAL_BUTTON_RE);
});

test("owner-device review warns about owner control without inventing grant scope or purpose", () => {
const html = render({
approval_id: "apr_owner",
client_id: "owner_cli",
created_at: "2026-08-11T12:00:00.000Z",
expires_at: "2026-08-11T12:10:00.000Z",
kind: "owner_device",
object: "approval_review",
});
assert.match(html, OWNER_DEVICE_CONTROL_RE);
assert.match(html, OWNER_DEVICE_WARNING_RE);
assert.doesNotMatch(html, NO_OWNER_DEVICE_SCOPE_RE);
});
Loading
Loading