Skip to content
Draft
Show file tree
Hide file tree
Changes from 54 commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
e7c776a
test: define fail-closed workflow registry audit contract
seonghobae Aug 14, 2026
578b101
feat: add fail-closed workflow registry audit
seonghobae Aug 14, 2026
59b6a94
fix: keep workflow audit token on fixed GitHub API
seonghobae Aug 14, 2026
be28004
test: require protected-ref movement detection
seonghobae Aug 14, 2026
f84b5c6
fix: fail closed when protected ref moves
seonghobae Aug 14, 2026
6f6eaa0
test: lock authenticated audit reads to GitHub API
seonghobae Aug 14, 2026
a4f9dc7
test(workflows): expose same-count pagination drift
seonghobae Aug 14, 2026
8d0c6ed
fix(workflows): verify multi-page registry stability
seonghobae Aug 14, 2026
d1b8449
test(workflows): verify stable multi-page registry pass
seonghobae Aug 14, 2026
c33759e
test(workflows): require fixed HTTPS transport boundary
seonghobae Aug 14, 2026
580b757
fix(workflows): pin audit transport to HTTPS host
seonghobae Aug 14, 2026
93ba3ba
test(workflows): require fixed aiohttp origin boundary
seonghobae Aug 14, 2026
ab948ba
fix(workflows): use fixed-origin aiohttp transport
seonghobae Aug 14, 2026
3c6ae8a
test(workflows): expose commit-tree identity mismatch
seonghobae Aug 14, 2026
df2d4de
fix(workflows): resolve commit tree before registry audit
seonghobae Aug 14, 2026
fabaad7
test(workflows): separate protected commit and tree fixtures
seonghobae Aug 14, 2026
4dd0979
test(workflows): resolve protected commit tree in ref movement
seonghobae Aug 14, 2026
1514b16
test(workflows): resolve commit tree in stability fixtures
seonghobae Aug 14, 2026
1197fd4
test(workflows): expose rate-limit diagnostic ambiguity
seonghobae Aug 14, 2026
9d54970
fix(workflows): classify GitHub rate-limit failures
seonghobae Aug 14, 2026
a2920a5
test(workflows): prove slash ref path regression
seonghobae Aug 14, 2026
c2ca8b5
fix(workflows): preserve slash refs in GitHub paths
seonghobae Aug 14, 2026
8a1215c
test(workflows): reproduce dynamic registry identity gap
seonghobae Aug 14, 2026
7aa1ab9
fix(workflows): classify dynamic registry identities safely
seonghobae Aug 14, 2026
3cd6896
test(workflows): reproduce protected-ref namespace confusion
seonghobae Aug 14, 2026
5faf4be
fix(workflows): reject protected-ref namespace prefixes
seonghobae Aug 14, 2026
7618fc4
test(workflows): reject nonfinite audit timeouts
seonghobae Aug 15, 2026
2f6788c
fix(workflows): require finite audit timeout
seonghobae Aug 15, 2026
842610c
test(security): reject repository dot segments before transport
seonghobae Aug 15, 2026
bdb0fb3
fix(security): reject repository dot segments
seonghobae Aug 15, 2026
a50d803
test(audit): cover negative infinite timeout
seonghobae Aug 15, 2026
5c7c3e2
test(workflows): prove GitHub audit response is unbounded
seonghobae Aug 15, 2026
8770ad8
fix(workflows): bound GitHub audit response memory
seonghobae Aug 15, 2026
c54978c
test(workflows): exercise bounded aiohttp response stream
seonghobae Aug 15, 2026
09fc0de
test(workflows): prove recursive JSON fails closed
seonghobae Aug 15, 2026
5defb01
fix(workflows): normalize recursive JSON failure
seonghobae Aug 15, 2026
156f134
test(workflows): bound registry pagination cardinality
seonghobae Aug 15, 2026
77732cd
fix(workflows): bound registry audit cardinality
seonghobae Aug 15, 2026
150b71d
test(workflows): reject unknown workflow registry state
seonghobae Aug 15, 2026
6a48171
fix(workflows): reject unknown workflow registry states
seonghobae Aug 15, 2026
ca65227
test(workflows): require exact registry primitive types
seonghobae Aug 16, 2026
6140e45
test(workflows): reject hostile registry mappings
seonghobae Aug 16, 2026
124e5b3
fix(workflows): bound malformed registry primitive types
seonghobae Aug 16, 2026
7a71400
test(workflows): reject hostile audit boundary subclasses
seonghobae Aug 16, 2026
b336272
revert(workflows): restore pre-run audit branch after blocked fix
seonghobae Aug 16, 2026
4e87d9e
test(workflows): reject hostile audit boundary subclasses
seonghobae Aug 16, 2026
1127c75
fix(workflows): reject hostile audit boundary subclasses
seonghobae Aug 16, 2026
cc4362a
Merge branch 'main' into fix/workflow-registry-audit-d0a4b30
opencode-agent[bot] Aug 16, 2026
4b1ca74
feat(workflows): package fail-closed registry audit
cursoragent Aug 16, 2026
5957c3b
test(workflows): bound audit receipt timestamps
seonghobae Aug 16, 2026
0e3fa51
fix(workflows): validate canonical audit timestamps
seonghobae Aug 16, 2026
0714188
test(workflows): require collision-free registry-audit ADR
cursoragent Aug 16, 2026
84866b5
fix(workflows): allocate ADR 0021 after recovery inventory
cursoragent Aug 16, 2026
a3ccb8c
Merge branch 'main' into cursor/bc-df73ecc5-7f76-47cd-9c06-483e539fc6…
seonghobae Aug 16, 2026
eef4641
fix(workflow-audit): reconcile detector onto current protected main
seonghobae Sep 12, 2026
46a34ec
fix(workflow-audit): reject RFC3339 hour 24 across Python versions
seonghobae Sep 12, 2026
e3ea237
chore(pr222): reconcile workflow auditor through recovery root
seonghobae Sep 12, 2026
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
14 changes: 14 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,20 @@ add CODEOWNERS-based merge gates until multiple independent maintainers exist.
realistic tenant-isolation, migration, rollback, compatibility, and
concurrency tests.

## Workflow registry audit

- Keep the detector read-only. It may classify active identities that are
absent from an exact protected tree as candidates; it must never disable,
enable, edit, or rerun workflows.
- Derive audit identity from an independently resolved 40-hex protected SHA
plus a live protected-ref pre/post check. Registry rows, `dynamic/` paths,
and transport headers are not source authority.
- Require exact JSON-decoder types for containers and for `ref` / commit
`sha` / tree `sha` members before any equality test.
- Keep the production module under `pg_llm_batch`, the console script
`pg-llm-batch-workflow-audit`, and the operator/ADR/doctoring/CHANGELOG
contract synchronized.

## Provider retry invariant

Automatic provider retries are restricted to idempotent GET operations. The
Expand Down
14 changes: 14 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,20 @@ does not require either host and retains standalone operation. When embedded,
tenant scope is a local control-plane identity and not model- or
provider-returned data.

## Workflow registry audit

`pg-llm-batch-workflow-audit` is a packaged read-only control-plane detector
(ADR 0021).
It compares the GitHub Actions registry to one exact protected commit and
reports active repository-backed identities that are absent from that tree as
`active_absent_workflows` candidates. Platform-managed `dynamic/` identities
are receipted and never treated as deleted YAML. The tool does not mutate
workflow state, select tenant scope, or perform provider I/O.

Operators install the package and run the console script. A moving protected
ref, truncated tree, pagination drift, rate limit, or hostile identity-member
type fails closed. Candidate review remains a human control-plane decision.

## Verification boundary

Deterministic gates cover strict tenant validation, standalone compatibility,
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Read-only workflow-registry audit (`pg-llm-batch-workflow-audit`, ADR 0021) that binds one receipt to an exact protected commit, verifies the live protected ref before and after the read, classifies active repository-backed identities missing from that tree as `active_absent_workflows` candidates only, receipts GitHub-managed `dynamic/` identities without treating them as orphans, and fails closed on hostile identity-member types, non-canonical RFC 3339 `captured_at` values, truncated trees, pagination drift, and rate limits. The tool never disables or edits workflows.
- Read-only exact-head release acceptance that builds wheel and source distribution artifacts twice from clean Git archives, proves byte-identical SHA-256 identity, records bounded canonical evidence, and keeps publication and attestation authority separate.
- Optional bounded provider output/error-file lifetime controls for batch creation, with exact local validation before credential resolution and backward-compatible omission for provider-neutral callers.
- Trusted tenant-scoped durable lifecycle identities for shared-table MSA deployments, including `TenantDurableBatchAPIClient`, tenant-qualified persistence and read helpers, transaction-local PostgreSQL context, forced default-deny row-level security, and explicit standalone compatibility.
Expand Down
4 changes: 4 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,7 @@
- Maintain 100% production statement, branch, and public-docstring coverage.
Add realistic migration, rollback, compatibility, security, and
tenant-isolation tests before implementation changes.
- Keep the workflow-registry auditor read-only, packaged as
`pg_llm_batch.workflow_registry_audit` / `pg-llm-batch-workflow-audit`,
exact-type fail-closed on identity members, and documented in README,
architecture, ADR, doctoring, and CHANGELOG.
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ than a second database-side network authority.
| DDL subset | `pg_llm_batch/schema.sql` |
| Readiness (`/healthz`) | `pg_llm_batch/health.py` |
| CLI | `pg_llm_batch/cli.py` |
| Read-only workflow registry audit | `pg_llm_batch/workflow_registry_audit.py` |

## Requirements

Expand Down Expand Up @@ -154,6 +155,28 @@ python -m pg_llm_batch health # prints the report, exit 0 ready / 1 not ready
The Docker `HEALTHCHECK` and the compose `postgres` service both gate on the
same `pg_llm_batch_health_check()` SQL function.

### Workflow registry audit

Detect active GitHub Actions identities that are missing from an exact
protected source tree. The command is read-only: it never disables, edits, or
reruns workflows. Review `active_absent_workflows` yourself. GitHub-managed
`dynamic/` identities are listed but are not orphan candidates.

```bash
export GITHUB_TOKEN="$(gh auth token)" # contents:read + actions:read
pg-llm-batch-workflow-audit \
--repository ContextualWisdomLab/pg-llm-batch \
--protected-ref main \
--protected-sha "$(git rev-parse origin/main)"
```

Exit `0` means the protected ref stayed on the supplied SHA and no
repository-backed orphan candidates were found. Exit `2` prints a JSON receipt
with candidates for a separate review. Exit `1` is a fail-closed audit error.
See [`docs/doctoring/workflow-registry-audit.md`](docs/doctoring/workflow-registry-audit.md)
and [ADR 0021](docs/adr/0021-workflow-registry-audit.md) for token scope,
rate-limit evidence, and recovery.

---

## Durable lifecycle modes
Expand Down
99 changes: 99 additions & 0 deletions docs/adr/0021-workflow-registry-audit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# ADR 0021: Read-only exact-SHA workflow registry audit

- **Status:** Accepted for the read-only detector slice
- **Date:** 2026-08-16

## Context

GitHub Actions can retain active workflow identities after their repository
YAML is removed, or can register platform-managed `dynamic/` identities that
never had a protected-tree blob. Operators need a fail-closed way to list those
identities against one immutable protected commit. Automatic disable, rerun, or
YAML rewrite would change production control-plane state from a detector.

GitHub's Git database and Actions registry are separate evidence sources. A
tree response `sha` is a tree object identity, not a commit SHA. A multi-page
registry read can splice two registry generations even when `total_count` is
unchanged. JSON subclasses can lie about equality if identity members are
compared before an exact-type check.

Protected `main` currently ends at ADR 0015. Concurrent recovery branches
already claim 0016 (logical restore seek), 0017 (recovery evidence binding),
0018 (restore catalog acceptance), 0019 (physical PITR profile), and 0020
(recovery receipt verification). This decision therefore uses 0021 so a later
merge cannot silently file two different records under one numeric prefix.

NIST SP 800-218 (SSDF) and SLSA v1.0 treat build-system integrity as a
provenance control: verify the source that is supposed to produce automation
before changing that automation. in-toto's farm-to-table argument is the same
shape: bind an observation to an exact artifact identity rather than to a
moving name. Receipt timestamps follow the RFC 3339 UTC profile so an operator
or library caller cannot inject an unbounded or non-calendar value into
otherwise successful evidence.

## Decision

`pg_llm_batch.workflow_registry_audit` is a packaged, installable, read-only
auditor.

1. Transport is GET-only to `https://api.github.com`, path-only, no redirects,
finite timeout, default TLS verification, no automatic retry.
2. The CLI always verifies the live protected ref before and after the SHA
audit. Library callers may classify one SHA without live-ref checks.
3. Commit objects are resolved first; tree reads use `commit.tree.sha`.
4. Multi-page registries require a second identical pass.
5. Active repository-backed paths absent from the exact tree are candidates
only. `dynamic/` identities are receipted and never orphaned.
6. Exact decoder types are required for containers and for identity members
before any `==` / `!=`.
7. Optional caller `captured_at` values must be exact built-in strings in the
finite canonical UTC RFC 3339 shape `YYYY-MM-DDTHH:MM:SSZ` with a real
calendar instant. Invalid, non-UTC, non-canonical, or oversized values fail
before any GitHub read. Omitted values are generated by the auditor clock.
8. Exit codes are `0` (clean), `1` (failed closed), and `2` (candidates).
9. The tool never disables, enables, edits, or reruns workflows.

## Consequences

Operators can install `pg-llm-batch` and run
`pg-llm-batch-workflow-audit` without a special PYTHONPATH. Coverage,
docstring, and compile gates now see the production module because it lives
under `pg_llm_batch`. A buyer can distinguish a moving branch, a truncated
tree, a rate-limit, and a true orphan candidate.

The detector does not replace branch protection, required reviews, or a human
decision to disable a workflow. False confidence from a stale PR body or from
comparing a tree SHA to a commit SHA is rejected.

## Rollback

Rollback is code-only: remove the package module, console script, checkout
shim, ADR, doctoring, changelog, and regression files. No database migration
or Actions mutation is required. Adding mutation to this tool requires a
separate reviewed control-plane contract.

## References

GitHub. (n.d.). *REST API endpoints for GitHub Actions workflows*. GitHub Docs.
Retrieved August 16, 2026, from
https://docs.github.com/en/rest/actions/workflows

GitHub. (n.d.). *REST API endpoints for Git trees*. GitHub Docs. Retrieved
August 16, 2026, from
https://docs.github.com/en/rest/git/trees

Klyne, G., & Newman, C. (2002). *Date and time on the internet: Timestamps*
(RFC 3339). https://doi.org/10.17487/RFC3339

National Institute of Standards and Technology. (2022). *Secure software
development framework (SSDF) version 1.1: Recommendations for mitigating the
risk of software vulnerabilities* (NIST SP 800-218).
https://doi.org/10.6028/NIST.SP.800-218

The Linux Foundation. (2023). *Supply-chain Levels for Software Artifacts
(SLSA) v1.0*. https://slsa.dev/spec/v1.0/

Torres-Arias, S., Afzali, H., Kuppusamy, T. K., Curtmola, R., & Cappos, J.
(2019). in-toto: Providing farm-to-table guarantees for bits and bytes. In
*Proceedings of the 28th USENIX Security Symposium* (pp. 1393–1410). USENIX
Association. https://www.usenix.org/conference/usenixsecurity19/presentation/torres-arias
141 changes: 141 additions & 0 deletions docs/doctoring/workflow-registry-audit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# Workflow registry audit doctoring

## Purpose

This record tells an operator how to detect GitHub Actions identities that are
active in a repository registry but absent from an exact protected source tree,
without disabling, editing, rerunning, or recreating any workflow.

Use the installed command after `pip install pg-llm-batch` or an editable
checkout. Do not treat a missing checkout-root module as the supported
interface. The detector follows NIST SP 800-218 and SLSA v1.0 by binding the
receipt to an exact source commit before any operator reviews a candidate.

```bash
export GITHUB_TOKEN="$(gh auth token)" # contents:read + actions:read is enough
pg-llm-batch-workflow-audit \
--repository ContextualWisdomLab/pg-llm-batch \
--protected-ref main \
--protected-sha "$(git rev-parse origin/main)"
```

Equivalent module form:

```bash
python -m pg_llm_batch.workflow_registry_audit \
--repository ContextualWisdomLab/pg-llm-batch \
--protected-ref main \
--protected-sha "$(git rev-parse origin/main)"
```

## What to do with the result

- Exit `0`: the live protected ref still matches the supplied SHA, the registry
was stable across a complete pagination pass, and no active
repository-backed identity is missing from that exact tree. Keep operating.
- Exit `2`: stdout is a JSON receipt whose `active_absent_workflows` lists
candidates only. Open a separate operator review. Do not disable those
workflows from this tool.
- Exit `1`: the audit failed closed. Read the single stderr line, then correct
the repository selector, SHA, ref, token scope, rate-limit budget, or
truncated-tree condition and run the same command again.

GitHub-managed `dynamic/` identities appear in `workflow_records` with
`source_kind=platform_dynamic` and `source_present=null`. They are never
orphans. Do not delete platform-managed identities because they lack a
`.github/workflows/` blob.

The library function `audit_repository_workflows` can classify one exact SHA
without live-ref pre/post checks. The CLI always uses
`audit_live_protected_ref_workflows` so a moving protected branch cannot
certify a stale head.

If a library caller supplies `captured_at`, pass one exact built-in string in
the canonical UTC RFC 3339 shape `YYYY-MM-DDTHH:MM:SSZ` with a real calendar
instant. Empty strings, offsets such as `+00:00`, fractional seconds, lowercase
`z`, invalid dates, and oversized values fail before any GitHub read. Omit the
argument to let the auditor write the clock instant. See ADR 0021.

## Trust and security boundary

The client sends path-only GET requests to the fixed origin
`https://api.github.com`, disables redirects, and uses the default TLS
verifier. It never retries automatically. Non-success bodies are not read.
Rate-limit evidence is limited to HTTP 429 or HTTP 403 plus
`X-RateLimit-Remaining: 0` or `Retry-After`.

JSON objects, arrays, strings, integers, and booleans are accepted only as
exact decoder built-in types. Identity members (`ref`, commit `sha`, tree
`sha`) are type-checked before equality. A hostile subclass cannot certify the
caller SHA while resolving a different tree.

`GITHUB_TOKEN` is used only as an `Authorization` header. It is never copied
into stdout, stderr, receipts, or exception cause chains.

This tool does not select tenant scope, touch PostgreSQL, or mutate Actions
state. It is a read-only control-plane detector for this repository and for
hosts that embed `pg-llm-batch` as a module.

## Recovery and rollback

No schema, secret, or workflow mutation is performed. Recovery is to rerun the
same read after correcting the failed input or GitHub condition. Rollback is
removing the console script, package module, ADR, and this record. Reintroducing
workflow disable/enable from this tool is not a routine rollback step.

## Verification

Permanent regression coverage requires that:

- hostile `str` subclasses cannot satisfy commit, tree, or ref identity by
lying about equality;
- raising identity subclasses become `WorkflowRegistryAuditError` without
leaking custom exception text;
- `dynamic/` identities are receipted and never orphaned;
- unknown workflow states fail closed;
- multi-page registries are accepted only after a second identical pass;
- truncated trees, moving protected refs, and oversize responses fail closed;
- the console script `pg-llm-batch-workflow-audit` is declared in package
metadata; and
- README, architecture, ADR 0021, changelog, and this doctoring record all tell
the operator to review candidates instead of disabling workflows; and
- caller-supplied receipt timestamps are rejected unless they are finite
canonical UTC RFC 3339 values.

## References

GitHub. (n.d.). *REST API endpoints for GitHub Actions workflows*. GitHub Docs.
Retrieved August 16, 2026, from
https://docs.github.com/en/rest/actions/workflows

GitHub. (n.d.). *REST API endpoints for Git commits*. GitHub Docs. Retrieved
August 16, 2026, from
https://docs.github.com/en/rest/git/commits

GitHub. (n.d.). *REST API endpoints for Git trees*. GitHub Docs. Retrieved
August 16, 2026, from
https://docs.github.com/en/rest/git/trees

GitHub. (n.d.). *REST API endpoints for Git references*. GitHub Docs. Retrieved
August 16, 2026, from
https://docs.github.com/en/rest/git/refs

GitHub. (n.d.). *Rate limits for the REST API*. GitHub Docs. Retrieved August
16, 2026, from
https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api

Klyne, G., & Newman, C. (2002). *Date and time on the internet: Timestamps*
(RFC 3339). https://doi.org/10.17487/RFC3339

National Institute of Standards and Technology. (2022). *Secure software
development framework (SSDF) version 1.1: Recommendations for mitigating the
risk of software vulnerabilities* (NIST SP 800-218).
https://doi.org/10.6028/NIST.SP.800-218

The Linux Foundation. (2023). *Supply-chain Levels for Software Artifacts
(SLSA) v1.0*. https://slsa.dev/spec/v1.0/

Torres-Arias, S., Afzali, H., Kuppusamy, T. K., Curtmola, R., & Cappos, J.
(2019). in-toto: Providing farm-to-table guarantees for bits and bytes. In
*Proceedings of the 28th USENIX Security Symposium* (pp. 1393–1410). USENIX
Association. https://www.usenix.org/conference/usenixsecurity19/presentation/torres-arias
Loading
Loading