Agents Shipgate keeps the release gate local and deterministic while adding organization-level evidence around it: pinned policy packs, exception hygiene, host-grant drift, release attestations, org evidence bundles, and an append-only local registry.
report.json.release_decision.decision remains the only merge/release gate.
agents-shipgate org status is an opt-in governance gate for scheduled CI and
platform-team audits.
organization:
id: acme
repo: github.com/acme/support-agent
service: support-agent
tier: production
teams:
agent-platform:
reviewers: ["@acme/agent-platform"]
security:
reviewers: ["@acme/security"]
exception_policy:
require_owner: true
require_reason: true
require_expiry: true
max_age_days: 180
audit:
registry: .agents-shipgate/registry.jsonlThe fields are structural only. Shipgate does not call GitHub, resolve teams, or fetch organization policy.
Treat organization packs like dependencies: vendor or sync them into the repo, then pin their content.
checks:
policy_packs:
- id: org-release
path: vendor/shipgate-policies/org-release.yaml
source: github.com/acme/shipgate-policies@v3
sha256: "<sha256>"org status reports sourced packs without a sha256 as
policy_pack_unpinned, and reports changed content as
policy_pack_pin_mismatch. Normal scan still fails closed on a pin mismatch,
for an optional: true pack too.
A declared pack is a release trust root wherever it lives: verify reports a
pull request that edits it with SHIP-VERIFY-TRUST-ROOT-TOUCHED (class
policy), so a human reviews the change, including one that also removes the
declaration. The local loop says the same before the pull request:
shipgate check routes the edit to review, and the Claude Code hook from
install-hooks asks before a coding agent edits the pack (#1128).
For a read-only policy-pack inventory, run:
agents-shipgate org policy-packs --config shipgate.yaml --jsonPolicy-pack rules may carry routing metadata:
owner: agent-platform
reviewers: [security]
approval:
required: true
teams: [agent-platform]
min_approvals: 1These fields do not change rule matching, approval enforcement, fingerprints, or
release decisions. They are emitted as findings[].policy_routing
reviewer/audit metadata and are validated against organization.teams when
teams are declared. Shipgate does not call GitHub or verify whether those
approvals happened; use deterministic predicates and block: true for release
gating.
Starter packs live in ../policies/templates/.
agents-shipgate org status --config shipgate.yaml --jsonThe command evaluates local artifacts:
- accepted debt in
.agents-shipgate/baseline.json - manifest suppressions
- severity overrides and override acknowledgements
human_ack- policy-pack pin state
- host-grant drift when
.agents-shipgate/host-grants.jsonexists - configured registry path presence
It exits 20 only when explicit organization governance violations exist. It
does not create a second release verdict.
When organization.exception_policy.require_owner or
require_expiry is enabled, suppressions and severity override entries can
carry owner and expires metadata alongside their existing reason.
Emit a deterministic local attestation from a verify run:
agents-shipgate attest \
--from agents-shipgate-reports/verifier.json \
--config shipgate.yaml \
--ci-context github-actions \
--out agents-shipgate-reports/attestation.jsonattest validates the verification-receipt.json beside verifier.json
before it copies request_id, receipt_id, decision_id and
artifact_set_id into the attestation, and org bundle does the same before
it publishes them. A run that states a verdict without a valid receipt from
the same verify run exits 3 with a Verification receipt error line naming
the receipt; rerun verify rather than editing either file. See
verification reproducibility.
org.actor is who triggered the run and human_ack is what the manifest
declares; neither is an approval by a second person. GitHub holds that as the
pull request's reviews. The workflow fetches them and attest --reviews
records them in the attestation's approval block (attestation 0.6):
on:
pull_request_review:
types: [submitted]
permissions:
contents: read
pull-requests: read # the reviews API only
# … after the steps that check out the pull request's head commit
# (ref: ${{ github.event.pull_request.head.sha }}), install agents-shipgate
# (a release with --reviews) and verify that commit into
# agents-shipgate-reports/ (--head ${{ github.event.pull_request.head.sha }}) …
- name: Fetch the pull request's reviews
env:
GH_TOKEN: ${{ github.token }}
run: |
gh api --paginate --slurp \
"repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/reviews" \
| jq 'add // []' > "$RUNNER_TEMP/reviews.json"
- name: Attest with the approval
run: |
agents-shipgate attest \
--from agents-shipgate-reports/verifier.json \
--ci-context github-actions \
--reviews "$RUNNER_TEMP/reviews.json" \
--out agents-shipgate-reports/attestation.jsonWhat the block establishes:
statusisapprovedwhen someone other than the pull request's author approved the attested head commit and no reviewer's latest review requests changes;approved_stalewhen the standing approvals are on an earlier commit;changes_requestedwhen any reviewer's latest decisive review requests changes;not_approvedotherwise; andnot_suppliedon every attestation made without--reviews. A reviewer's standing is their latestAPPROVED,CHANGES_REQUESTEDorDISMISSEDreview; comments do not change it.- An approval by the pull request's author or a bot account never counts. It
is listed under
ignored_approvalswith the reason. source_sha256hashes the reviews file, and every review in it must name this repository's pull request: a file from another pull request is refused (exit3), never partly used.- The file must be outside the repository: a path inside the Git work tree
around the current directory or
verifier.json, or insideGITHUB_WORKSPACE, is refused (exit3), because the change under review could write it. So is a link committed in the repository. --reviewsneeds--ci-context github-actionsand an event payload with a pull request (pull_requestorpull_request_review): the number, its author and its head commit come from the event.head_shais the commit the verify run evaluated, read from the plan itsverification-receipt.jsonbinds, and the reviews are evaluated against it. It must be the pull request's head: verify that commit, as above. A run of the defaultpull_requestcheckout evaluates GitHub's synthetic merge commit instead, and is refused (exit3), as are artifacts from another revision, a--head-shanaming another commit, a run over uncommitted working-tree changes, and averifier.jsonwith no receipt beside it. An approval of one commit is never recorded against another.
The approval is evidence. It never changes verdict, release_decision,
the exit code or the bypass report. registry ingest copies it into the row
(approval_status, approval), and registry summary counts rows by
approval_status. Rows from earlier attestations carry null there, and their
row ids and chain hashes are unchanged. Ingest validates the block as
attest writes it and refuses (exit 3) one whose status its own rows do
not support. A ledger row whose block does not validate, or whose
approval_status is not the block's status, is skipped by every registry
reader and reported by registry verify.
Append attestations to a local JSONL ledger:
agents-shipgate registry ingest \
--attestation agents-shipgate-reports/attestation.json \
--registry .agents-shipgate/registry.jsonl \
--repo acme/support-agent
agents-shipgate registry report --bypass --json
agents-shipgate registry summary --registry .agents-shipgate/registry.jsonl --json
agents-shipgate registry verify --registry .agents-shipgate/registry.jsonl --jsonThe bypass report lists rows where merge_verdict != "mergeable" and the
attestation does not carry satisfied human acknowledgement. It exits 0 by
default so scheduled jobs can archive the report; pass --fail-on-bypass when
CI should exit 20 if bypass_count > 0. Both registry query --json and
registry report --bypass --json include skipped_count and skipped_rows[]
for malformed ledger lines. registry summary --json answers fleet-level
counts by repo/service/tier/verdict plus bypass, trust-root, policy-weakening,
and human-acknowledgement counts. registry verify --json checks row IDs,
duplicates, malformed rows, and optional per-repo hash-chain links.
Build one compact artifact for CI upload or future read-only hosted ingestion:
agents-shipgate org bundle \
--config shipgate.yaml \
--from agents-shipgate-reports/verifier.json \
--out agents-shipgate-reports/org-evidence-bundle.json \
--jsonThe bundle includes organization metadata, attestation payload, registry-row
projection, org status, policy-pack pin records, host-grant summary, artifact
hashes, and privacy/redaction status. It names
gating_signal: release_decision.decision and never computes a verdict.
attest --redact is path-scoped. It shortens local artifact paths, but it does
not remove explicit organization or CI identity fields such as org.repo,
org.actor, org.workflow_run_id, or org.merge_sha. Omit --ci-context or
the corresponding explicit flags when those identities should not be recorded.
Use ../examples/github-actions/13-org-governance.yml
for a copyable scheduled workflow. The public Action can now emit
attestation.json, org-evidence-bundle.json, host-grants.json, and
org-status.json with:
with:
attestation: "true"
org_bundle: "true"
host_audit: "true"
org_status: "true"
registry_repo_label: acme/support-agentDo not put hosted scanning, dynamic policy fetching, GitHub review verification, or LLM policy decisions in this layer. The local scanner emits deterministic artifacts; any hosted or cross-repo view consumes those artifacts.