diff --git a/CHANGELOG.md b/CHANGELOG.md index 90dc7c1..b0259e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- **Portable Forge Infrastructure Contract (draft).** Add the + `forge-infra/v0` seven-lane contract, canonical capability catalog, + evidence-backed provider and authority profiles, digest-bound binding plans, + non-secret transition receipts, time-bounded verification receipts, object + references, examples, rejects, and cross-document controls. +- **ADR-0009: forge infrastructure contract.** Record the separation of + neutral forge capabilities, provider-native mappings, authority control, and + live assurance. +- **Forge-infra reader guide.** Add a mechanics companion covering the + information model, seven lanes, and sibling-contract boundaries. + ## [0.1.29] - 2026-08-24 ### Added diff --git a/Makefile b/Makefile index e553f3e..455addc 100644 --- a/Makefile +++ b/Makefile @@ -326,6 +326,8 @@ lint-config: lint-role-prompts lint-coverage-attestation ## Validate config data sh scripts/test-service-job-controls.sh || exit 1; \ echo " Project-work controls..."; \ sh scripts/test-project-work-controls.sh || exit 1; \ + echo " Forge-infra controls..."; \ + sh scripts/test-forge-infra-controls.sh || exit 1; \ echo " Validating contract manifests..."; \ sh scripts/validate-contract-manifests.sh \ schemas/data-artifact/v0/contract.json \ @@ -334,7 +336,8 @@ lint-config: lint-role-prompts lint-coverage-attestation ## Validate config data schemas/review-journal/v0/contract.json \ schemas/agent-wait/v0/contract.json \ schemas/service-job/v0/contract.json \ - schemas/project-work/v0/contract.json || exit 1; \ + schemas/project-work/v0/contract.json \ + schemas/forge-infra/v0/contract.json || exit 1; \ else \ echo "[!!] goneat not found, skipping config validation"; \ fi diff --git a/README.md b/README.md index d54f678..5dba0c1 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,7 @@ This is a **reference-based model**—we don't sync standards into repositories. │ ├── coverage-attestation/v0/ # Coverage attestation (proposed) │ ├── agent-wait/v0/ # Portable agent wait/poll contract │ ├── data-artifact/v0/ # Portable data artifact contract +│ ├── forge-infra/v0/ # Forge capabilities, provider profiles, authority bindings │ ├── foundation/v0/ # Lifecycle phases, release phases, types │ ├── process-run/v0/ # Local process telemetry/control (proposed) │ ├── project-work/v0/ # Ready packets, project state, work ledger (draft) diff --git a/docs/README.md b/docs/README.md index eb2f884..3b33571 100644 --- a/docs/README.md +++ b/docs/README.md @@ -42,6 +42,7 @@ Run-books and adoption guides — the mechanics companion to the standards. - [running-a-fierce-collaboration-review.md](guides/running-a-fierce-collaboration-review.md) - Executable steps for the fierce-collaboration review standard - [composing-a-review-panel.md](guides/composing-a-review-panel.md) - Assembling review seats and panels: the five-layer composition model, harness classes, framing blocks, and preflight +- [using-the-forge-infrastructure-contract.md](guides/using-the-forge-infrastructure-contract.md) - Reader tour of `forge-infra/v0`: information model, seven lanes, and sibling contracts - [agentic-interface-adoption.md](guides/agentic-interface-adoption.md) - Adopting the role catalog and attribution baseline - [multi-org-github-cli-auth.md](guides/multi-org-github-cli-auth.md) - GitHub CLI auth across orgs @@ -59,6 +60,7 @@ Cross-cutting classification standards for data and artifacts, plus contract sta - [agent-wait-contract.md](standards/agent-wait-contract.md) - Portable wait/poll contract for one aggregate waiter per consuming seat - [classifiers-framework.md](standards/classifiers-framework.md) - How the classifiers system fits together (docs + config + schemas) - [data-artifact-contract.md](standards/data-artifact-contract.md) - Portable artifact, representation, catalog, provenance, and protection contract +- [forge-infrastructure-contract.md](standards/forge-infrastructure-contract.md) - Seven-lane forge capability, authority, resolution, and assurance contract - [service-job-contract.md](standards/service-job-contract.md) - Portable catalog, digest-bound submit, and job lifecycle contract - [project-work-contract.md](standards/project-work-contract.md) - Portable ready-packet, project-state, control-record, and progress-ledger contract - [data-artifact-contract-examples.md](standards/data-artifact-contract-examples.md) - Source-neutral stress cases and producer adoption preview template @@ -81,6 +83,8 @@ Decision and governance records (ADR / PDR / …) — see the [decisions index]( - [ADR-0001](decisions/ADR-0001-schema-config-versioning.md) - Schema and config versioning - [ADR-0002](decisions/ADR-0002-keymaterial-fingerprint-portable-contract.md) - Key-material fingerprint contract as a portable schema (proposed) - [ADR-0003](decisions/ADR-0003-decision-record-taxonomy.md) - Decision & governance record taxonomy (the \*DR family) +- [ADR-0008](decisions/ADR-0008-project-work-contract.md) - Portable project-work companion contract (proposed) +- [ADR-0009](decisions/ADR-0009-forge-infrastructure-contract.md) - Neutral forge capabilities and curated provider profiles (proposed) - [PDR-0001](decisions/PDR-0001-adopt-data-pipeline-principles.md) - Adopt the data-pipeline engineering principles ### [Catalog](catalog/) @@ -122,6 +126,8 @@ Current release documentation. | CI/CD Baseline | `crucible.3leaps.dev/operations/ci-baseline` | | Stream Output | `crucible.3leaps.dev/sop/stream-output` | | Auth Session Artifact | `crucible.3leaps.dev/standards/auth-session-artifact` | +| Forge Infrastructure | `crucible.3leaps.dev/standards/forge-infrastructure-contract` | +| Using Forge Infra | `crucible.3leaps.dev/guides/using-the-forge-infrastructure-contract` | | Data Sensitivity | `crucible.3leaps.dev/standards/data-sensitivity-classification` | | Classifiers Framework | `crucible.3leaps.dev/standards/classifiers-framework` | | Volatility | `crucible.3leaps.dev/standards/volatility-classification` | diff --git a/docs/decisions/ADR-0009-forge-infrastructure-contract.md b/docs/decisions/ADR-0009-forge-infrastructure-contract.md new file mode 100644 index 0000000..18e60eb --- /dev/null +++ b/docs/decisions/ADR-0009-forge-infrastructure-contract.md @@ -0,0 +1,160 @@ +--- +id: "ADR-0009" +title: "Separate Neutral Forge Capabilities from Curated Provider Profiles" +status: "proposed" +date: "2026-08-27" +last_updated: "2026-08-27" +deciders: + - "@3leapsdave" +scope: "Crucible foundation / forge-infrastructure contracts" +tags: + - "schemas" + - "forge" + - "authorization" + - "interchange-contract" +relates-to: + - "crucible ADR-0001 (schema/config versioning)" + - "docs/standards/data-artifact-contract.md" + - "docs/standards/project-work-contract.md" +--- + +# ADR-0009: Separate Neutral Forge Capabilities from Curated Provider Profiles + +## Status + +**Current Status**: Proposed — the contract enters review at `v0`. + +## Context + +Git hosting providers expose useful infrastructure beyond repository storage: +issues, automation, runners, statuses, checks, packages, releases, deployment +governance, webhooks, and other event surfaces. + +The providers do not expose one shared feature or authorization model. +Similarly named features differ by operation, principal, resource scope, +grant vocabulary, offering, lifecycle, and event behavior. Application +registration, user consent, installation, token issuance, and runner +registration are distinct transitions. + +A single provider-by-feature matrix would either freeze the lowest common +denominator or claim parity that does not exist. A provider-specific contract +per implementation would prevent adopters from expressing portable +requirements. + +## Decision + +Establish `contract: forge-infra/v0` with: + +1. L1 requirement profiles and digest-bound binding plans; +2. L2 authority profiles, closed policy-input facts, and immutable non-secret + transition receipts; +3. an explicit L3–L6 neutral capability catalog for native git, forge + resources, automation, events, and telemetry; +4. curated provider profiles mapping native features and authority profiles + onto that catalog with dated evidence; +5. L7 time-bounded verification receipts for live conformance and drift; and +6. stable forge-object references that exclude access context. + +`contract.json` is the schema-registry entry point and exposes the canonical +catalog plus all public object schemas. The catalog is a versioned artifact, +not an illustrative example. + +### Operation and authority are the support key + +Provider profiles make availability claims per neutral operation and authority +profile. A feature-level boolean is insufficient. The same provider-native +feature may be available to a user token and unavailable to an application +installation. + +### Provider vocabulary remains visible + +Permission, scope, role, feature, and offering names remain provider-native +data in provider profiles. Neutral capability ids describe jobs and operations +without renaming vendor grants into a fictitious common permission model. + +### Consumer policy is an overlay + +Information stores, revision stores, long-running agent memory, and other +adopters express requirements against the neutral catalog. Their preferred or +forbidden uses do not become provider facts. + +### Evidence is part of a claim + +Provider profiles carry observation times and source references. Unknown or +stale evidence does not become support by inference. + +### Receipts are non-secret + +Binding receipts carry public identifiers and grant names. Credential material +remains outside the contract. Registering an application does not by itself +prove that an authorized principal or usable token exists. + +### Binding and verification are distinct + +A binding receipt proves that one controlled transition was attempted. It does +not prove current authority. Verification receipts are time-bounded and report +expected, observed, missing, and unexpected grants plus capability checks. +Receipts cite a specific digest-bound plan action; successor transitions cite a +successful predecessor so failed work cannot advance the authority chain. + +### Provider and instance identity are extensible + +Provider identity is not a closed vendor enum. A shared provider context +distinguishes hosted, self-managed, dedicated, and other instance or offering +contexts. + +### Events separate subscription from delivery + +Subscription management is a forge capability. Delivery envelopes, retry, +acknowledgement, signature verification, and local receiver execution remain +the responsibility of their respective contracts and integrations. + +## Consequences + +- Adopters can compare providers without erasing meaningful differences. +- Provider profiles can evolve independently of the neutral vocabulary. +- Authority acquisition and capability use remain connected without becoming + one credential blob. +- Provider-native limitations and feature names remain representable without + turning arbitrary extension keys into portable contract vocabulary. +- Cross-document validation is required in addition to JSON Schema. +- Curated provider profiles require ongoing evidence refresh. +- Implementations need a resolver rather than a one-row feature lookup. +- Side-effecting authority operations need digest and idempotency binding. +- Consumers need explicit verification and reconciliation policies. + +## Alternatives considered + +### One common provider API + +Rejected. It would either omit high-value provider capabilities or give +similarly named operations misleadingly identical semantics. + +### One provider-by-feature boolean matrix + +Rejected. Availability depends on operation, authority, resource scope, +offering, and grants. + +### Provider-specific contracts only + +Rejected. Adopters could not state portable requirements or evaluate fallback +paths. + +### Combine application registration and granted authority + +Rejected. Registration, installation, consent, and token issuance have +different principals, outputs, and security boundaries. + +### Put adopter use cases in provider rows + +Rejected. A provider fact should not change when an information store or +revision store changes policy. + +## Review-loop items + +1. Whether the canonical neutral catalog should later publish on a separate + registry release cadence from provider profiles. +2. Whether provider profiles need signed provenance or freshness policy + metadata beyond source URI and observation time. +3. Whether a later companion contract should normalize webhook delivery and + retry semantics. diff --git a/docs/decisions/README.md b/docs/decisions/README.md index c6f054d..618ef17 100644 --- a/docs/decisions/README.md +++ b/docs/decisions/README.md @@ -51,6 +51,7 @@ Examples: | [ADR-0006](ADR-0006-process-run-contract.md) | Local Process Telemetry & Control as a Companion Portable Contract | proposed | 2026-07-06 | | [ADR-0007](ADR-0007-separate-documentation-and-schema-registry-origins.md) | Separate Crucible Documentation and Schema Registry Origins | proposed | 2026-08-19 | | [ADR-0008](ADR-0008-project-work-contract.md) | Portable Project Work as a Companion Contract | proposed | 2026-08-21 | +| [ADR-0009](ADR-0009-forge-infrastructure-contract.md) | Separate Neutral Forge Capabilities from Curated Provider Profiles | proposed | 2026-08-27 | | [PDR-0001](PDR-0001-adopt-data-pipeline-principles.md) | Adopt the Data-Pipeline Engineering Principles | accepted | 2026-06-29 | | [PDR-0002](PDR-0002-worktree-per-task.md) | One git worktree per concurrent task | accepted | 2026-06-29 | | [PDR-0003](PDR-0003-role-portfolio-tiering.md) | Role portfolio tiering: core, supplemental, deprecated | accepted | 2026-06-29 | diff --git a/docs/guides/using-the-forge-infrastructure-contract.md b/docs/guides/using-the-forge-infrastructure-contract.md new file mode 100644 index 0000000..78cd196 --- /dev/null +++ b/docs/guides/using-the-forge-infrastructure-contract.md @@ -0,0 +1,235 @@ +--- +title: "Using the Forge Infrastructure Contract" +description: "Reader tour of contract: forge-infra/v0 — information model, seven lanes, and how it sits next to git, project-work, and other Crucible contracts" +author: "cxotech" +author_of_record: "Dave Thompson " +supervised_by: "@3leapsdave" +date: "2026-08-27" +last_updated: "2026-08-27" +status: "draft" +category: "guide" +tags: + - forge + - git + - contracts + - authorization + - how-to +--- + +# Using the Forge Infrastructure Contract + +The [portable forge-infrastructure standard](../standards/forge-infrastructure-contract.md) +is the law. This guide is the tour: what the contract is for, the objects a +reader will meet, how information flows, and what it deliberately is not. +Where this guide and the standard disagree, the standard wins. + +Companion machine objects live under +[`schemas/forge-infra/v0/`](../../schemas/forge-infra/v0/). Decision: +[ADR-0009](../decisions/ADR-0009-forge-infrastructure-contract.md). + +## One-sentence version + +An actor wants a git stratum with **capabilities** and **requirements**. +This contract **resolves and binds access honestly**. It does not wrap `gh`, +store secrets, or replace git. + +## Four heads (read this first) + +The contract looks like one SDK. It is four coupled concerns. Mixing them +is how a reader concludes we wrapped `gh`. + +| Head | Job | Lane | +| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- | +| **Access brokerage** | Mint apps / bind privileges; map grants the provider actually has | L1, L2, catalog | +| **Git-stratum CRUD** | Catalog both `forge.repository.remote` (hosted container) and `forge.repository.content` (read/create/update/delete via native git or a mapped contents API). No blob/tree/commit schema, git CLI, or provider proxy | L3 | +| **Async comms** | Infrastructure notifies us, or we notify it (statuses, checks, subscriptions) | L6 | +| **Health / management facts** | Dated evidence, live grant verification, drift, teardown. A live health _stream_ is `process-run`, not a fourth tree | L7 | + +A remote that is “just git” (including git stored on object storage) may need +only L3. Issues, runners, and webhooks are why L4–L6 exist. + +## What it is / is not + +| It is | It is not | +| -------------------------------------------------------------- | -------------------------------------------- | +| A capability-resolution and authority-control model | A common provider SDK | +| Two trees: **neutral catalog** + **curated provider profiles** | Fake feature parity | +| Non-secret receipts of mint/bind/verify | A credential or session store | +| Facts a policy engine may consume | The policy engine (no OPA/Rego inside) | +| A locator for forge-native objects | The published bag of bytes (`data-artifact`) | + +GitHub is far ahead of GitLab and Forgejo. Common jobs still exist (issues, +automation, runners, webhooks). Honesty means a cell may say `unsupported` +or `unknown` instead of inventing a shared verb. + +## Information model + +Eight public objects. Objects that directly cite neutral capabilities pin a +`catalog_ref` (catalog id + revision); receipts trace through their binding +plan so a resolver can fail closed if the vocabulary moved. + +```mermaid +flowchart TB + subgraph tree1 [Tree 1 — neutral] + CAT[Canonical capability catalog] + end + subgraph tree2 [Tree 2 — curated] + PP[Provider profile] + AP[Authority profile] + end + subgraph bridge [Acquisition bridge] + REQ[Requirement profile] + PLAN[Binding plan] + BR[Binding receipt] + VR[Verification receipt] + end + CTX[Provider context] + REF[Forge object reference] + CAT --> PP + CAT --> REQ + PP --> AP + REQ --> PLAN + PP --> PLAN + AP --> PLAN + PLAN --> BR + BR --> VR + CAT --> REF + CTX --> PP + CTX --> AP + CTX --> PLAN + CTX --> BR + CTX --> VR + CTX --> REF +``` + +| Object | Job | +| -------------------------- | ----------------------------------------------------------------------------------------------------- | +| **Capability catalog** | Stable `capability_id` values, lane, operations, portability class. No vendor names. | +| **Provider profile** | Native feature → catalog operation, under a named authority, with dated evidence. | +| **Authority profile** | One principal/credential _class_ (installation, PAT, job token, …). Acquisition mode. No secrets. | +| **Requirement profile** | What an adopter needs (`required` / `preferred` / `allowed` / `forbidden`) without choosing a vendor. | +| **Binding plan** | Resolver output: grants, ordered actions, gaps, RFC 8785 `plan_spec_digest`, policy-input facts. | +| **Binding receipt** | One immutable, non-secret **transition** (registered, installed, bound, revoked, …). | +| **Verification receipt** | Time-bounded observation that live grants still match the binding. | +| **Forge object reference** | Locator (provider context + capability + native id). Access context is not identity. | + +Mint is not authority. A GitHub App registration is not an installation token. +A Forgejo OAuth client is not a PAT. A binding receipt is not current +authority until a successful bound/rotated/reconciled receipt has a conformant +verification still in its validity interval. + +## Seven lanes + +```mermaid +flowchart LR + L1[L1 Intent] --> L2[L2 Authority] + L2 --> L3[L3 Native git] + L2 --> L4[L4 Forge resources] + L2 --> L5[L5 Automation] + L2 --> L6[L6 Events] + L2 --> L7[L7 Assurance] + L3 -.-> L7 + L4 -.-> L7 + L5 -.-> L7 + L6 -.-> L7 +``` + +| Lane | Concern | Lives in this contract as | +| ------ | ------------------- | ----------------------------------------------------------------------------------------------------------------------- | +| **L1** | Intent / resolution | Requirement profile → binding plan | +| **L2** | Authority control | Authority profile, policy-input facts, binding receipt | +| **L3** | Native git | Catalog: hosted remote administration plus content CRUD authority. **Execution is git / kit or a mapped contents API.** | +| **L4** | Forge resources | Issues, snippets, packages, releases, wikis | +| **L5** | Automation | Workflow definition vs run vs runner (separate capabilities) | +| **L6** | Events / telemetry | Subscriptions, statuses, checks. Delivery bus is _not_ here. | +| **L7** | Assurance | Evidence, verification, drift, revoke, teardown | + +L3–L6 are the catalog. L1, L2, and L7 are how a consumer selects, binds, and +checks those capabilities. + +Earlier F0–F5 draft labels are aliases only. They are not schema fields. + +## Required sequence + +```mermaid +sequenceDiagram + actor Actor + participant Resolver + participant PDP as Policy engine + participant Provider + Actor->>Resolver: requirement profile + Resolver->>Resolver: catalog + provider + authority + Resolver-->>Actor: binding plan plus digest + Actor->>PDP: policy-input facts + PDP-->>Actor: opaque decision ref + Actor->>Provider: digest-bound transition + Provider-->>Actor: secrets out of band + Note over Actor: binding receipt non-secret + Actor->>Provider: time-bounded verification + Provider-->>Actor: verification receipt + Actor->>Provider: use L3-L6 or observe drift +``` + +1. Requirement +2. Resolution plan +3. Opaque policy decision +4. Digest- and idempotency-bound execution +5. Non-secret binding receipt +6. Time-bounded verification +7. Use and observation +8. Drift reconcile, revoke, or teardown + +Do not report “we minted an app” as usable authority before bind + verify. +The policy target, capability-operation and grant tuples, and ordered authority +actions must exactly match the digest-bound plan; a digest does not repair +mismatched authorization facts. + +Side-effecting L2 work SHOULD run as `contract: service-job/v0` (or equivalent) +so digest and idempotency are enforced. + +## Non-code stores (why this exists besides CI) + +Two adopter overlays use the **same** catalog. They do not add provider rows. + +| Overlay | Git (L3) | Forge objects (L4) | Automation / events | +| ---------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------ | ---------------------------------------------------------- | +| Collaborative information store / agent memory | **Authority** — files humans can read; graph/RAG is a derived index | Issues _preferred_ for intake; snippets/gists **forbidden** as the store | Observe workflow runs; webhooks allowed with poll fallback | +| Daemon revision store | **Local authority** — expected-parent history | Forbid as live authority; optional sealed projection | Observe projection; forge is not the writer | + +The example requirement profile +[`requirement-profile.agent-memory.example.json`](../../schemas/forge-infra/v0/examples/requirement-profile.agent-memory.example.json) +is the agent-memory overlay in machine form. + +## Sibling contracts + +Do not fork their jobs into forge-infra. + +| Need | Contract | +| ---------------------------------------------- | ---------------------------------------------------------- | +| Published bags / indexes / logs | `data-artifact/v0` | +| Local producing process | `process-run/v0` | +| Wait / poll for a subject | `agent-wait/v0` | +| Digest-bound admission of a side-effecting job | `service-job/v0` | +| Dispatchable work packets | `project-work/v0` (an issue is not automatically a packet) | +| Relied-upon review evidence | `review-journal/v0` | +| Value-stripped session expiry/provenance | `auth-session-artifact/v0` (not a binding receipt) | + +L6: this contract says whether you may manage a provider subscription. +Delivery, retry, and signature verification stay with the waiter, the local +receiver, and the artifact bag. + +## Where to pin + +- Capability token: `contract: forge-infra/v0` +- Manifest: `schemas/forge-infra/v0/contract.json` +- Canonical catalog instance: `schemas/forge-infra/v0/forge-capability-catalog.json` +- Structural + semantic battery: `scripts/test-forge-infra-controls.sh` + +Pin a reviewed repository revision. Path `v0/` is unstable (ADR-0001). + +## Out of this tour + +- Implementing git or wrapping `gh` / `glab` / `tea` +- Putting PEMs, tokens, or webhook secrets in receipts +- A complete frozen vendor grant catalog +- Treating hosted issues or Actions as a revision store’s authority diff --git a/docs/standards/agent-wait-contract.md b/docs/standards/agent-wait-contract.md index 366c655..24b1a5e 100644 --- a/docs/standards/agent-wait-contract.md +++ b/docs/standards/agent-wait-contract.md @@ -176,6 +176,9 @@ identity reference, never a credential). Optional: `causation_id`, That is not a second waiter. - **`contract: data-artifact/v0`** — event payloads are artifact or message refs, not embedded bytes. +- **`contract: forge-infra/v0`** — forge event subscription is distinct from + delivery. A forge integration may surface a payload reference through one + aggregate waiter; this contract does not create the provider subscription. ## Validation Requirements diff --git a/docs/standards/auth-session-artifact.md b/docs/standards/auth-session-artifact.md index 6766430..9ff27a3 100644 --- a/docs/standards/auth-session-artifact.md +++ b/docs/standards/auth-session-artifact.md @@ -115,6 +115,10 @@ The schema lives under the `auth/v0/` path namespace; `v0` may change without no regardless of the repository's lifecycle phase. Pin to a specific commit (and SHA256) for stability. +An auth-session artifact is value-stripped expiry and provenance metadata. It +is not a `contract: forge-infra/v0` binding receipt and MUST NOT carry forge +binding, installation, repository, or provider-object identifiers. + ## Governance / validation split - **Crucible owns the contract.** Source of truth for the schema, `schema_version`, diff --git a/docs/standards/data-artifact-contract.md b/docs/standards/data-artifact-contract.md index 5cf0356..6489c28 100644 --- a/docs/standards/data-artifact-contract.md +++ b/docs/standards/data-artifact-contract.md @@ -558,6 +558,14 @@ by checking the emitted aggregate against the declared threshold. It MUST NOT accept a producer's self-asserted "suppression applied" flag as evidence. A self-claim that cannot be verified against the emitted values fails closed. +## Relationship To Forge Infrastructure + +`contract: forge-infra/v0` governs whether a provider can host, produce, or +deliver an artifact and which authority may perform that operation. This +contract governs the published bag of bytes, representations, provenance, and +protection. Forge object references and event payload references do not replace +a data-artifact descriptor. + ## Validation Requirements A validator MUST check: diff --git a/docs/standards/forge-infrastructure-contract.md b/docs/standards/forge-infrastructure-contract.md new file mode 100644 index 0000000..47c1c8f --- /dev/null +++ b/docs/standards/forge-infrastructure-contract.md @@ -0,0 +1,387 @@ +--- +title: "Portable Forge Infrastructure Contract" +description: "Seven-lane model for resolving forge requirements, authority, native features, automation, events, and assurance" +category: "standards" +status: "draft" +version: "0.0.0" +lastUpdated: "2026-08-27" +maintainer: "core-standards" +reviewers: ["architecture", "security", "information-architecture"] +approvers: ["lead-maintainer"] +tags: ["forge", "git", "capability", "authorization", "interoperability"] +content_license: "CC0" +relatedDocs: + - "docs/guides/using-the-forge-infrastructure-contract.md" + - "docs/standards/agent-wait-contract.md" + - "docs/standards/auth-session-artifact.md" + - "docs/standards/data-artifact-contract.md" + - "docs/standards/process-run-contract.md" + - "docs/standards/project-work-contract.md" + - "docs/standards/review-journal-contract.md" + - "docs/standards/service-job-contract.md" +audience: "implementers" +--- + +# Portable Forge Infrastructure Contract + +This draft defines `contract: forge-infra/v0`: a portable way to resolve what +a forge can do, which provider-native feature does it, which authority may +exercise it, and whether a live binding still conforms. + +The contract is a capability-resolution and control model. It is not a common +provider SDK, a `gh` proxy, or a credential envelope. + +## Seven-lane model + +The model separates facts and actions that have different authorities and +lifecycles: + +| Lane | Concern | Contract representation | +| ---- | --------------------- | --------------------------------------------------------------------------- | +| L1 | Intent and resolution | Requirement profile and digest-bound binding plan | +| L2 | Authority control | Authority profile, policy-input facts, transition receipt | +| L3 | Native git data | Hosted-container and content CRUD capability; native git execution boundary | +| L4 | Forge resources | Issues, snippets, packages, releases, wikis, and governance objects | +| L5 | Automation | Workflow definitions, workflow runs, jobs, and runners | +| L6 | Events and telemetry | Subscription, status, and check capabilities; delivery boundary | +| L7 | Assurance | Evidence, time-bounded verification, drift, reconciliation, and revocation | + +L3 through L6 form the neutral capability catalog. L1, L2, and L7 govern how a +consumer selects, authorizes, executes, and verifies those capabilities. + +Earlier F0–F5 labels are non-normative aliases and are not contract fields. + +## Design stance + +- **Neutral vocabulary, curated mappings.** Portable meanings and + provider-native facts remain separate. +- **Provider context is explicit.** A provider id is combined with an instance + or offering context; hosted and self-managed deployments are not assumed to + behave alike. +- **Operation-level honesty.** Support is asserted for an operation under a + named authority profile, not for a feature name in the abstract. +- **Mint is not authority.** Registration, installation, consent, token + issuance, binding, and verification are separate transitions. +- **Receipts are not credentials.** Contract artifacts carry identifiers, + facts, and evidence references, never secret values. +- **Binding is not current authority.** A binding receipt records a completed + transition; a time-bounded verification receipt reports live conformance. +- **Evidence expires.** Provider and acquisition claims carry source + references and observation timestamps. +- **Consumer policy stays downstream.** Adopters state requirements without + rewriting provider facts. + +## Contract discovery and catalog identity + +Contract documents carry: + +```json +{ + "capabilities": ["contract: forge-infra/v0"] +} +``` + +The schema-registry entry point is +`schemas/forge-infra/v0/contract.json`. It exposes: + +- the entry schema; +- the canonical neutral catalog; and +- every public object schema in this contract family. + +Consumers resolve the capability token through a trusted registry or vendored +schema set, verify the manifest capability, and load the relative targets. + +Every provider, requirement, plan, and object-reference document pins a +`catalog_ref` containing the catalog id and revision. A publisher MAY add a +SHA-256 digest. All document digests in this contract use RFC 8785 JSON +Canonicalization Scheme bytes. A resolver MUST reject documents whose catalog +reference does not match the selected catalog. + +Path versioning follows ADR-0001. The `v0/` family is unstable; consumers +should pin a reviewed repository revision. + +## L1: intent and resolution + +### Requirement profile + +A requirement profile expresses provider-neutral needs. Each row names: + +- a neutral capability; +- one or more operations; +- a disposition (`required`, `preferred`, `allowed`, or `forbidden`); and +- optional allowed authority classes. + +The profile does not select a provider or prescribe a provider grant. + +### Binding plan + +A resolver evaluates a requirement profile against a catalog, provider +profile, authority profiles, provider context, and target structure. Its +output is a binding plan containing: + +- satisfied, partial, unresolved, and unknown operation resolutions; +- exact provider-native grants and interaction modes; +- ordered acquisition, install, consent, provision, bind, verify, rotate, + revoke, or teardown actions; +- closed policy-input facts; and +- explicit gaps. + +The plan pins the requirement digest and provider-profile revision. Its +`plan_spec_digest` is the RFC 8785 SHA-256 digest of `plan_spec`. +Side-effecting execution MUST bind its idempotency key and policy decision to +that digest. + +Authority-action sequence numbers MUST be contiguous from one. Every action +other than `verify` declares the binding transition it produces. Verification +produces its own receipt and therefore declares no binding transition. + +The portable contract defines policy input, not the policy engine. OPA/Rego or +another PDP may evaluate the facts. The resulting decision remains an opaque +`policy_decision_ref`; it is not embedded as provider data. + +The policy-input target MUST match the execution target. Its requested +capability-operation, authority-profile, and provider-native grant tuples MUST +exactly match the plan's satisfiable resolutions. Its ordered authority-action +summaries MUST exactly match the plan's executable authority actions. These +facts are part of the same digest-bound plan; re-digesting internally +inconsistent facts does not authorize them. + +## L2: authority control + +### Authority profiles + +An authority profile describes one provider-native principal or credential +class without credential material. The vocabulary distinguishes: + +- anonymous access; +- application assertions and installations; +- application and OAuth user grants; +- personal, project, and group access tokens; +- deploy, job, workflow, and runner registration tokens; and +- service accounts. + +`native_name` preserves the provider term. `authority_class` supplies the +portable category. Provider context, principal scope, resource selection, +grant model, acquisition mechanism, and lifecycle operations remain distinct. + +Acquisition modes are: + +| Mode | Meaning | +| ------------------ | ---------------------------------------------------------- | +| `api` | A documented API creates or exchanges the authority object | +| `operator_handoff` | A human completes a documented provider workflow | +| `automatic` | The provider issues authority during another lifecycle | +| `unsupported` | The authority cannot be acquired in this context | + +An operator handoff MUST include an operator URI template. Implementations +MUST NOT scrape provider user interfaces. + +Every acquisition path cites evidence from the authority profile. A resolver +MUST verify those references and the provider context before using the +profile. + +### Binding transitions and receipts + +Authority changes are immutable transitions: + +`registered → installed → consented/provisioned → bound → rotated/reconciled → +revoked → torn_down` + +Not every provider uses every transition. A binding receipt records exactly +one attempted transition and its outcome. It carries: + +- provider context, authority profile, subject, and optional target; +- binding-plan id, plan-spec digest, action sequence, and idempotency key; +- an opaque policy-decision reference; +- public provider identifiers and provider-native grants; +- an optional non-secret reference to an auth session; and +- transition time and outcome. + +A receipt MUST NOT contain access or refresh tokens, client secrets, passwords, +private keys, PEM material, webhook secrets, signing tokens, or runner +registration tokens. Secret storage, delivery, and expiry are outside this +contract. + +`contract: auth-session-artifact/v0` may provide value-stripped provenance and +expiry for a session. It is not a binding receipt and carries no forge binding +identifiers. + +Side-effecting acquisition, bind, rotation, revocation, and teardown SHOULD +run through `contract: service-job/v0` or an equivalent boundary that enforces +digest binding and idempotency. + +A planned receipt at action sequence two or later MUST reference the receipt +for the immediately preceding receipt-producing action in the same plan, +provider context, authority, and target. Actions such as `verify` that declare +no binding transition are skipped when identifying that predecessor. The +predecessor MUST have succeeded. Sequence one may begin from a newly planned +action or declare `history_basis: imported` when the provider object predates +the portable receipt chain. A failed, partial, or unknown transition MUST NOT +be bypassed or advance the chain. + +## L3–L6: neutral catalog and provider profiles + +### Neutral capability catalog + +The canonical catalog defines stable capability ids, one lane, allowed +operations, and a portability class. It contains no provider products, +permission strings, token prefixes, or commercial offerings. + +The v0 operation vocabulary is: + +`discover | read | create | update | delete | execute | cancel | observe | +subscribe | receive | administer` + +Portability classes are: + +| Class | Meaning | +| -------------------- | ------------------------------------------------------------------ | +| `portable` | Providers expose substantially equivalent semantics | +| `convergent` | Providers share the job, but lifecycle or shape differs materially | +| `provider_extension` | Useful provider-native capability with no honest shared floor | + +Portability is not availability. Provider profiles make availability claims. + +### Curated provider profiles + +A provider profile maps native features to neutral capability operations. +Every operation cell names: + +- the neutral operation and authority profile; +- availability (`supported`, `partial`, `emulated`, `unsupported`, or + `unknown`); +- interaction modes; +- exact required provider grants, including an explicit empty set; +- evidence references and limitations; and +- an optional neutral fallback. + +Profiles carry a provider context and revision. Provider ids are extensible, +not a closed vendor enum. Context distinguishes instance, deployment kind, +base URI, offering, and version where relevant. + +A producer MUST NOT publish `supported`, `partial`, or `emulated` without +dated evidence. `unknown` means the mapping has not been established. + +### Lane boundaries + +- **L3 native git.** The catalog distinguishes the hosted repository container + from repository content. It can state that a principal may administer the + remote and read, create, update, or delete content through native git or a + mapped provider contents API. Repository and content execution stays in git + or repository tooling; this contract defines no blob/tree/commit schema, + command model, or generic forge proxy. +- **L4 forge resources.** Provider-native objects use `forge-object-ref`. + Published byte bags remain governed by `contract: data-artifact/v0`. +- **L5 automation.** A workflow definition and a workflow run are different + capabilities. Runs are observed or cancelled; execution is requested through + the definition or a provider-native dispatch. Forge automation may implement + service jobs, but does not replace their admission and result contract. +- **L6 events and telemetry.** Subscription management is a forge capability. + Event delivery is a separate integration. An agent may wait for a payload + reference through `contract: agent-wait/v0`; `contract: process-run/v0` + governs only a local receiver process; `contract: data-artifact/v0` governs + payload or log bags. This contract does not normalize delivery envelopes, + retries, acknowledgement, or signature verification. + +## L7: verification, drift, and evidence + +A binding receipt alone does not prove current authority. A verification +receipt observes one binding and reports: + +- verification and expiry times; +- methods used; +- expected, observed, missing, and unexpected grants; +- operation-level capability checks; and +- an outcome such as conformant, drift, denied, revoked, expired, or unknown. + +Verification MUST be time-bounded. Consumers MUST NOT treat an expired +verification as current authority. Drift SHOULD trigger an explicit reconcile, +rotate, revoke, or teardown plan; it MUST NOT silently escalate grants. +`verified_at` and `valid_until` are RFC 3339 instants and MUST be compared +chronologically after offset normalization, never lexically. + +A `conformant` outcome requires observed grants to match the resolved plan and +binding receipt, no missing or unexpected grants, and allowed results for every +reported capability check. It also requires a successful `bound`, `rotated`, or +`reconciled` receipt; registration, installation, consent, or provisioning is +not usable authority. A `drift` outcome requires a grant delta or a +denied/unknown capability check. Every satisfiable required operation in the +linked plan MUST have a capability check. + +Relied-upon human or automated review evidence remains governed by +`contract: review-journal/v0`. Provider checks and statuses may project an +outcome but are not the journal. + +## Forge object references + +`forge-object-ref` locates one provider-native object. Identity consists of: + +- catalog reference and provider context; +- neutral capability id; +- provider-native id; and +- optional repository path. + +Credentials, authority-profile ids, binding-receipt ids, and consumer record +ids are access context, not object identity. + +## Required operational sequence + +Implementations follow this sequence: + +1. requirement; +2. resolution plan; +3. opaque policy decision; +4. digest- and idempotency-bound execution; +5. non-secret binding receipt; +6. time-bounded verification; +7. use and observation; and +8. drift reconciliation, revocation, or teardown. + +Registration or token issuance MUST NOT be reported as usable authority before +the applicable bind and verification steps succeed. + +## Cross-document invariants + +JSON Schema validates individual documents. Producers and consumers MUST also +enforce: + +1. Catalog ids are unique; parent ids resolve within the same revision. +2. All `catalog_ref` values match the selected catalog id and revision. +3. Provider mappings, operations, and fallbacks resolve to the catalog. +4. Provider operation cells are unique and all evidence references resolve. +5. Authority-profile references exist in the same provider context. +6. Authority acquisition evidence references resolve. +7. Requirement operations exist on the neutral capability. +8. Binding-plan refs, revisions, digests, contexts, policy facts, resolutions, + contiguous action sequences, and action-to-transition mappings resolve. +9. Binding receipts match a specific plan action, target, authority, and + provider context; successor links cite the immediate receipt-producing + predecessor and never bypass or advance a non-successful receipt. +10. Verification receipts match their binding and plan action, have a finite + chronologically ordered validity interval, require a successful usable + binding for conformance, and substantiate conformant or drift outcomes. +11. Object references resolve to the selected catalog. + +The fixture and control battery is +`scripts/test-forge-infra-controls.sh`. Implementations publishing arbitrary +document sets MUST enforce equivalent invariants for their own registry. + +## Out of scope for v0 + +- A universal provider SDK or common endpoint set +- Credential storage, exchange, signing, or secret verification +- Provider UI automation or automatic permission escalation +- A complete vendor grant catalog frozen into Crucible +- Webhook payload normalization, retry queues, or poison-message handling +- Provider billing, quota, or commercial entitlement negotiation +- Treating hosted issues, automation, or callbacks as revision authority by + default + +## Schema identity and publication + +Schema `$id` values are hostless: +`contract:forge-infra/v0/`. + +Publication URLs are retrieval conveniences, not provider-instance identity. +Consumers resolve sibling schema ids through the selected trusted registry or +vendored schema set. diff --git a/docs/standards/process-run-contract.md b/docs/standards/process-run-contract.md index 261d25d..1536a07 100644 --- a/docs/standards/process-run-contract.md +++ b/docs/standards/process-run-contract.md @@ -256,6 +256,9 @@ dead-letter file rather than dropping silently. - **Logging baselines** — the envelope's severity enum and correlation-id conventions follow the common structured-logging practice so events and logs join cleanly downstream. +- **`contract: forge-infra/v0`** — a local webhook receiver or provider + integration may be represented as a process run. Forge subscription + management and provider delivery semantics remain outside process-run. ## Validation Requirements diff --git a/docs/standards/project-work-contract.md b/docs/standards/project-work-contract.md index c5bb2e1..7946dcd 100644 --- a/docs/standards/project-work-contract.md +++ b/docs/standards/project-work-contract.md @@ -193,6 +193,9 @@ classifier dimension. published bag. Do not invent a second ledger wrapper. - **review-journal** — relied-upon review events. Not the work ledger. - **agent-wait** — live wait/poll. Not a packet field. +- **forge-infra** — forge issues may project or provide an interaction lane + for project work, but a provider-native issue is not automatically a + portable work packet or the authoritative progress ledger. ## Out Of Scope For v0 diff --git a/docs/standards/service-job-contract.md b/docs/standards/service-job-contract.md index 1cc26a2..70d033b 100644 --- a/docs/standards/service-job-contract.md +++ b/docs/standards/service-job-contract.md @@ -228,6 +228,10 @@ stdlib-only materializer and refuses `jq -S` as the oracle. - **`contract: data-artifact/v0`** — inputs and outputs are artifact refs that may carry descriptor identity, representation, profile, media type, and digest. +- **`contract: forge-infra/v0`** — side-effecting authority acquisition, + binding, verification, rotation, revocation, and teardown may execute as + digest- and idempotency-bound service jobs. The forge plan and receipts + remain forge-infra artifacts. ## Validation Requirements diff --git a/schemas/forge-infra/v0/README.md b/schemas/forge-infra/v0/README.md new file mode 100644 index 0000000..f242e17 --- /dev/null +++ b/schemas/forge-infra/v0/README.md @@ -0,0 +1,88 @@ +# Forge Infrastructure Schemas v0 + +Machine-readable companion schemas for the portable forge-infrastructure +contract. + +The normative standard is +[`docs/standards/forge-infrastructure-contract.md`](../../../docs/standards/forge-infrastructure-contract.md). +The contract identity is the hostless capability token +`contract: forge-infra/v0`. + +## Contract shape + +The contract separates seven lanes that evolve at different rates: + +1. **L1 intent and resolution** uses requirement profiles and digest-bound + binding plans. +2. **L2 authority control** uses evidence-backed authority profiles and + immutable non-secret transition receipts. +3. **L3–L6 capabilities** use a canonical neutral catalog plus curated + provider mappings for native git, forge resources, automation, events, and + telemetry. +4. **L7 assurance** uses time-bounded verification receipts for live + conformance and drift. + +Provider differences remain first-class. A provider profile may mark an +operation `partial`, `emulated`, `unsupported`, or `unknown`; it must not claim +parity merely because another provider exposes a similarly named feature. + +## Objects + +| Schema | Governs | +| ---------------------------------- | --------------------------------------------------------------------- | +| `contract.json` | Registry entry point exposing the catalog and object schemas | +| `forge-capability-catalog.json` | Canonical neutral capability catalog | +| `capability-catalog.schema.json` | Neutral capability vocabulary and operations | +| `provider-profile.schema.json` | Evidence-backed native-to-neutral mappings | +| `authority-profile.schema.json` | Acquisition, scope, grant model, and lifecycle for one authority kind | +| `binding-plan.schema.json` | Digest-bound resolution and operation-complete policy facts | +| `binding-receipt.schema.json` | One immutable, non-secret authority transition | +| `verification-receipt.schema.json` | Time-bounded live grant and capability verification | +| `requirement-profile.schema.json` | Provider-neutral adopter requirements and prohibitions | +| `forge-object-ref.schema.json` | Stable locator for a provider-native forge object | +| `common.schema.json` | Shared definitions used by the object schemas | + +The schema-registry entry point is `contract.json`. Consumers resolve the +capability token to that manifest, verify its `capability`, and load its +relative `entry_schema`, `catalog`, and `object_schemas` targets. + +## Cross-document controls + +JSON Schema validates each document. The fixture and control battery at +[`scripts/test-forge-infra-controls.sh`](../../../scripts/test-forge-infra-controls.sh) +additionally checks that: + +- catalog references, neutral capability ids, and provider operation cells are + consistent and unique; +- provider mappings and fallbacks resolve to the neutral catalog; +- mapped operations exist on the neutral capability; +- provider and authority acquisition evidence references resolve; +- authority-profile references use the same provider context; +- requirement-profile operations exist on the neutral capability; +- binding-plan refs, revisions, digests, policy facts, resolutions, and actions + resolve; +- transition receipts match a specific contiguous plan action and the immediate + successful receipt-producing predecessor; +- verification receipts match the binding, plan grants, required operation + checks, usable-authority state, outcome semantics, and normalized validity + interval; and +- object references resolve to the canonical catalog. + +Implementations publishing multiple documents MUST enforce equivalent +cross-document checks. + +## Secret boundary + +Binding and verification receipts contain identifiers, grant names, and +observations, never credential material. Access tokens, refresh tokens, client +secrets, private keys, PEM material, webhook secrets, signing tokens, +passwords, and runner registration tokens remain out of band. + +The schema rejects recognizably secret property names under +`public_identifiers`, but that is defense in depth. Producers are responsible +for ensuring that values are non-secret before publication or persistence. + +## Examples and rejects + +Worked examples are under [`examples/`](examples/). Structural and semantic +negative controls are under [`rejects/`](rejects/). diff --git a/schemas/forge-infra/v0/authority-profile.schema.json b/schemas/forge-infra/v0/authority-profile.schema.json new file mode 100644 index 0000000..41dc581 --- /dev/null +++ b/schemas/forge-infra/v0/authority-profile.schema.json @@ -0,0 +1,165 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "contract:forge-infra/v0/authority-profile.schema.json", + "title": "Forge authority acquisition profile", + "description": "Provider-specific, non-secret description of an authority kind, its acquisition path, grant model, resource scope, and lifecycle.", + "type": "object", + "additionalProperties": false, + "required": [ + "capabilities", + "authority_profile_id", + "provider_context", + "revision", + "observed_at", + "evidence", + "authority_class", + "native_name", + "principal_scope", + "acquisition", + "grant_model", + "secret_delivery", + "lifecycle_operations" + ], + "properties": { + "capabilities": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/capabilities" + }, + "authority_profile_id": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/opaqueId" + }, + "provider_context": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/providerContext" + }, + "revision": { + "type": "string", + "minLength": 1 + }, + "observed_at": { + "type": "string", + "format": "date-time" + }, + "evidence": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/evidence" + } + }, + "authority_class": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/authorityClass" + }, + "native_name": { + "type": "string", + "minLength": 1 + }, + "principal_scope": { + "type": "string", + "enum": [ + "anonymous", + "user", + "organization", + "group", + "project", + "repository", + "installation", + "instance", + "job" + ] + }, + "resource_selection": { + "type": "string", + "enum": ["none", "single", "selected", "all_in_scope", "provider_defined"] + }, + "acquisition": { + "type": "object", + "additionalProperties": false, + "required": ["mode", "mechanism", "evidence_refs"], + "properties": { + "mode": { + "type": "string", + "enum": ["api", "operator_handoff", "automatic", "unsupported"] + }, + "mechanism": { + "type": "string", + "minLength": 1 + }, + "operator_uri_template": { + "type": "string", + "minLength": 1 + }, + "expected_public_receipt_fields": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "string", + "pattern": "^[a-z][a-z0-9_]*$" + } + }, + "limitations": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "evidence_refs": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/opaqueId" + } + } + }, + "allOf": [ + { + "if": { + "properties": { + "mode": { + "const": "operator_handoff" + } + }, + "required": ["mode"] + }, + "then": { + "required": ["operator_uri_template"] + } + } + ] + }, + "grant_model": { + "type": "string", + "enum": [ + "permission_map", + "oauth_scopes", + "token_scopes", + "role_binding", + "unscoped_principal", + "provider_defined" + ] + }, + "grant_catalog_uri": { + "type": "string", + "format": "uri" + }, + "secret_delivery": { + "const": "out_of_band" + }, + "lifecycle_operations": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "string", + "enum": ["authorize", "install", "issue", "refresh", "rotate", "introspect", "revoke"] + } + }, + "default_ttl_seconds": { + "type": "integer", + "minimum": 1 + }, + "notes": { + "type": "string" + } + } +} diff --git a/schemas/forge-infra/v0/binding-plan.schema.json b/schemas/forge-infra/v0/binding-plan.schema.json new file mode 100644 index 0000000..395c969 --- /dev/null +++ b/schemas/forge-infra/v0/binding-plan.schema.json @@ -0,0 +1,300 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "contract:forge-infra/v0/binding-plan.schema.json", + "title": "Forge authority binding plan", + "description": "Digest-bound resolution of provider-neutral requirements into provider capabilities, authority actions, and policy-input facts.", + "type": "object", + "additionalProperties": false, + "required": [ + "capabilities", + "binding_plan_id", + "catalog_ref", + "requirement_profile_ref", + "provider_profile_ref", + "provider_context", + "target", + "created_at", + "plan_spec", + "plan_spec_digest" + ], + "properties": { + "capabilities": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/capabilities" + }, + "binding_plan_id": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/opaqueId" + }, + "catalog_ref": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/catalogRef" + }, + "requirement_profile_ref": { + "type": "object", + "additionalProperties": false, + "required": ["requirement_profile_id", "digest"], + "properties": { + "requirement_profile_id": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/opaqueId" + }, + "digest": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/sha256" + } + } + }, + "provider_profile_ref": { + "type": "object", + "additionalProperties": false, + "required": ["provider_profile_id", "revision"], + "properties": { + "provider_profile_id": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/opaqueId" + }, + "revision": { + "type": "string", + "minLength": 1 + } + } + }, + "provider_context": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/providerContext" + }, + "target": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/structureRef" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "plan_spec": { + "type": "object", + "additionalProperties": false, + "required": ["resolutions", "authority_actions", "policy_input", "gaps"], + "properties": { + "resolutions": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "capability_id", + "operation", + "authority_profile_id", + "disposition", + "resolution", + "required_grants" + ], + "properties": { + "capability_id": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/capabilityId" + }, + "operation": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/operation" + }, + "authority_profile_id": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/opaqueId" + }, + "disposition": { + "type": "string", + "enum": ["required", "preferred", "allowed", "forbidden"] + }, + "resolution": { + "type": "string", + "enum": ["satisfied", "partial", "unsatisfied", "unknown"] + }, + "required_grants": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/grant" + } + }, + "interaction_modes": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/interactionMode" + } + }, + "limitations": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + } + } + } + }, + "authority_actions": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["seq", "action", "authority_profile_id", "execution_mode"], + "properties": { + "seq": { + "type": "integer", + "minimum": 1 + }, + "action": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/bindingAction" + }, + "authority_profile_id": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/opaqueId" + }, + "execution_mode": { + "type": "string", + "enum": ["api", "operator_handoff", "automatic", "service_job", "unsupported"] + }, + "service_offer_ref": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/opaqueId" + }, + "produces_transition": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/bindingTransition" + }, + "notes": { + "type": "string" + } + }, + "allOf": [ + { + "if": { + "properties": { + "execution_mode": { + "const": "service_job" + } + }, + "required": ["execution_mode"] + }, + "then": { + "required": ["service_offer_ref"] + } + }, + { + "if": { + "properties": { + "action": { + "const": "verify" + } + }, + "required": ["action"] + }, + "then": { + "not": { + "required": ["produces_transition"] + } + }, + "else": { + "required": ["produces_transition"] + } + } + ] + } + }, + "policy_input": { + "type": "object", + "additionalProperties": false, + "required": ["actor_ref", "target", "requested_operations", "authority_actions"], + "properties": { + "actor_ref": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/opaqueId" + }, + "target": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/structureRef" + }, + "requested_operations": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "capability_id", + "operation", + "authority_profile_id", + "required_grants" + ], + "properties": { + "capability_id": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/capabilityId" + }, + "operation": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/operation" + }, + "authority_profile_id": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/opaqueId" + }, + "required_grants": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/grant" + } + } + } + } + }, + "authority_actions": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["seq", "action", "authority_profile_id", "execution_mode"], + "properties": { + "seq": { + "type": "integer", + "minimum": 1 + }, + "action": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/bindingAction" + }, + "authority_profile_id": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/opaqueId" + }, + "execution_mode": { + "type": "string", + "enum": ["api", "operator_handoff", "automatic", "service_job", "unsupported"] + }, + "service_offer_ref": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/opaqueId" + }, + "produces_transition": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/bindingTransition" + } + } + } + } + } + }, + "gaps": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["capability_id", "operation", "reason"], + "properties": { + "capability_id": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/capabilityId" + }, + "operation": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/operation" + }, + "reason": { + "type": "string", + "minLength": 1 + } + } + } + } + } + }, + "plan_spec_digest": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/sha256", + "description": "RFC 8785 SHA-256 digest of plan_spec." + } + } +} diff --git a/schemas/forge-infra/v0/binding-receipt.schema.json b/schemas/forge-infra/v0/binding-receipt.schema.json new file mode 100644 index 0000000..1e6493a --- /dev/null +++ b/schemas/forge-infra/v0/binding-receipt.schema.json @@ -0,0 +1,142 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "contract:forge-infra/v0/binding-receipt.schema.json", + "title": "Portable non-secret forge authority binding receipt", + "description": "Immutable evidence of one registration, install, consent, bind, rotation, revocation, or teardown transition. Credential material is always out of band.", + "type": "object", + "additionalProperties": false, + "required": [ + "capabilities", + "binding_receipt_id", + "provider_context", + "authority_profile_id", + "binding_plan_ref", + "binding_plan_action_seq", + "history_basis", + "idempotency_key", + "policy_decision_ref", + "transition", + "outcome", + "subject", + "occurred_at" + ], + "properties": { + "capabilities": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/capabilities" + }, + "binding_receipt_id": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/opaqueId" + }, + "previous_receipt_ref": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/opaqueId" + }, + "provider_context": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/providerContext" + }, + "authority_profile_id": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/opaqueId" + }, + "binding_plan_ref": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/bindingPlanRef" + }, + "binding_plan_action_seq": { + "type": "integer", + "minimum": 1 + }, + "history_basis": { + "type": "string", + "enum": ["planned", "imported"] + }, + "idempotency_key": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/opaqueId" + }, + "policy_decision_ref": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/opaqueId" + }, + "transition": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/bindingTransition" + }, + "outcome": { + "type": "string", + "enum": ["succeeded", "partial", "failed", "unknown"] + }, + "subject": { + "type": "object", + "additionalProperties": false, + "required": ["kind", "id"], + "properties": { + "kind": { + "type": "string", + "enum": [ + "user", + "application", + "installation", + "organization", + "group", + "project", + "repository", + "job", + "runner", + "event_subscription" + ] + }, + "id": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/opaqueId" + } + } + }, + "target": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/structureRef" + }, + "public_identifiers": { + "type": "object", + "minProperties": 1, + "propertyNames": { + "pattern": "^[a-z][a-z0-9_]*$", + "not": { + "pattern": "(?:secret|token|password|private_key|pem)$" + } + }, + "additionalProperties": { + "type": "string", + "minLength": 1 + } + }, + "resource_bindings": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/structureRef" + } + }, + "grants": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/grant" + } + }, + "auth_session_ref": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/opaqueId" + }, + "occurred_at": { + "type": "string", + "format": "date-time" + } + }, + "allOf": [ + { + "if": { + "properties": { + "binding_plan_action_seq": { + "minimum": 2 + } + }, + "required": ["binding_plan_action_seq"] + }, + "then": { + "required": ["previous_receipt_ref"] + } + } + ] +} diff --git a/schemas/forge-infra/v0/capability-catalog.schema.json b/schemas/forge-infra/v0/capability-catalog.schema.json new file mode 100644 index 0000000..ea31910 --- /dev/null +++ b/schemas/forge-infra/v0/capability-catalog.schema.json @@ -0,0 +1,89 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "contract:forge-infra/v0/capability-catalog.schema.json", + "title": "Neutral forge capability catalog", + "description": "Provider-neutral vocabulary for infrastructure capabilities above git.", + "type": "object", + "additionalProperties": false, + "required": ["capabilities", "catalog_id", "revision", "neutral_capabilities"], + "properties": { + "capabilities": { + "type": "array", + "items": { + "type": "string" + }, + "contains": { + "const": "contract: forge-infra/v0" + }, + "minItems": 1, + "uniqueItems": true + }, + "catalog_id": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/opaqueId" + }, + "revision": { + "type": "string", + "minLength": 1 + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "neutral_capabilities": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "capability_id", + "title", + "description", + "lane", + "kind", + "portability", + "operations" + ], + "properties": { + "capability_id": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/capabilityId" + }, + "parent_id": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/capabilityId" + }, + "title": { + "type": "string", + "minLength": 1 + }, + "description": { + "type": "string", + "minLength": 1 + }, + "lane": { + "type": "string", + "enum": ["l3_native_git", "l4_forge_resources", "l5_automation", "l6_events_telemetry"] + }, + "kind": { + "type": "string", + "enum": ["resource", "automation", "event", "execution", "artifact", "governance"] + }, + "portability": { + "type": "string", + "enum": ["portable", "convergent", "provider_extension"] + }, + "operations": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/operation" + } + }, + "notes": { + "type": "string" + } + } + } + } + } +} diff --git a/schemas/forge-infra/v0/common.schema.json b/schemas/forge-infra/v0/common.schema.json new file mode 100644 index 0000000..df01661 --- /dev/null +++ b/schemas/forge-infra/v0/common.schema.json @@ -0,0 +1,271 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "contract:forge-infra/v0/common.schema.json", + "title": "Forge infrastructure common definitions", + "$defs": { + "capabilities": { + "type": "array", + "items": { + "type": "string" + }, + "contains": { + "const": "contract: forge-infra/v0" + }, + "minItems": 1, + "uniqueItems": true + }, + "opaqueId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "capabilityId": { + "type": "string", + "pattern": "^forge\\.[a-z][a-z0-9_]*(?:\\.[a-z][a-z0-9_]*)+$", + "maxLength": 128 + }, + "providerId": { + "type": "string", + "pattern": "^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$", + "maxLength": 64 + }, + "providerContext": { + "type": "object", + "additionalProperties": false, + "required": ["provider_id", "context_id", "deployment_kind"], + "properties": { + "provider_id": { + "$ref": "#/$defs/providerId" + }, + "context_id": { + "$ref": "#/$defs/opaqueId" + }, + "deployment_kind": { + "type": "string", + "enum": [ + "hosted", + "self_managed", + "dedicated", + "enterprise", + "instance", + "provider_defined" + ] + }, + "base_uri": { + "type": "string", + "format": "uri" + }, + "offering": { + "type": "string", + "minLength": 1 + }, + "version": { + "type": "string", + "minLength": 1 + } + } + }, + "catalogRef": { + "type": "object", + "additionalProperties": false, + "required": ["catalog_id", "revision"], + "properties": { + "catalog_id": { + "$ref": "#/$defs/opaqueId" + }, + "revision": { + "type": "string", + "minLength": 1 + }, + "digest": { + "$ref": "#/$defs/sha256" + } + } + }, + "sha256": { + "type": "string", + "pattern": "^sha256:[0-9a-f]{64}$" + }, + "structureRef": { + "type": "object", + "additionalProperties": false, + "required": ["structure_class", "structure_ref"], + "properties": { + "structure_class": { + "type": "string", + "enum": [ + "user", + "organization", + "group", + "project", + "repository", + "installation", + "instance", + "job", + "none", + "provider_defined" + ] + }, + "structure_ref": { + "$ref": "#/$defs/opaqueId" + }, + "parent_ref": { + "$ref": "#/$defs/opaqueId" + } + } + }, + "bindingPlanRef": { + "type": "object", + "additionalProperties": false, + "required": ["binding_plan_id", "plan_spec_digest"], + "properties": { + "binding_plan_id": { + "$ref": "#/$defs/opaqueId" + }, + "plan_spec_digest": { + "$ref": "#/$defs/sha256" + } + } + }, + "bindingAction": { + "type": "string", + "enum": [ + "register", + "install", + "consent", + "provision", + "bind", + "verify", + "rotate", + "reconcile", + "revoke", + "teardown" + ] + }, + "bindingTransition": { + "type": "string", + "enum": [ + "registered", + "installed", + "consented", + "provisioned", + "bound", + "rotated", + "reconciled", + "revoked", + "torn_down" + ] + }, + "operation": { + "type": "string", + "enum": [ + "discover", + "read", + "create", + "update", + "delete", + "execute", + "cancel", + "observe", + "subscribe", + "receive", + "administer" + ] + }, + "authorityClass": { + "type": "string", + "enum": [ + "anonymous", + "application_assertion", + "application_installation", + "application_user_grant", + "oauth_user_grant", + "personal_access_token", + "project_access_token", + "group_access_token", + "deploy_token", + "job_token", + "workflow_token", + "runner_registration_token", + "service_account" + ] + }, + "availability": { + "type": "string", + "enum": ["supported", "partial", "emulated", "unsupported", "unknown"] + }, + "interactionMode": { + "type": "string", + "enum": [ + "api", + "git_remote", + "registry_protocol", + "operator_handoff", + "webhook", + "poll", + "event_stream", + "runner", + "callback", + "provider_defined" + ] + }, + "grant": { + "type": "object", + "additionalProperties": false, + "required": ["kind", "name"], + "properties": { + "kind": { + "type": "string", + "enum": ["permission", "scope", "role", "provider_defined"] + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "access": { + "type": "string", + "enum": ["read", "write", "execute", "manage", "admin", "full", "provider_defined"] + }, + "resource_scope": { + "type": "string", + "enum": [ + "user", + "organization", + "group", + "project", + "repository", + "installation", + "instance", + "provider_defined" + ] + } + } + }, + "evidence": { + "type": "object", + "additionalProperties": false, + "required": ["evidence_id", "source_uri", "checked_at"], + "properties": { + "evidence_id": { + "$ref": "#/$defs/opaqueId" + }, + "source_uri": { + "type": "string", + "format": "uri" + }, + "checked_at": { + "type": "string", + "format": "date-time" + }, + "provider_version": { + "type": "string", + "minLength": 1 + }, + "notes": { + "type": "string" + } + } + } + } +} diff --git a/schemas/forge-infra/v0/contract.json b/schemas/forge-infra/v0/contract.json new file mode 100644 index 0000000..61d03e7 --- /dev/null +++ b/schemas/forge-infra/v0/contract.json @@ -0,0 +1,15 @@ +{ + "capability": "contract: forge-infra/v0", + "entry_schema": "capability-catalog.schema.json", + "catalog": "forge-capability-catalog.json", + "object_schemas": { + "authority_profile": "authority-profile.schema.json", + "binding_plan": "binding-plan.schema.json", + "binding_receipt": "binding-receipt.schema.json", + "common": "common.schema.json", + "forge_object_ref": "forge-object-ref.schema.json", + "provider_profile": "provider-profile.schema.json", + "requirement_profile": "requirement-profile.schema.json", + "verification_receipt": "verification-receipt.schema.json" + } +} diff --git a/schemas/forge-infra/v0/examples/authority-profile.github-app-installation.example.json b/schemas/forge-infra/v0/examples/authority-profile.github-app-installation.example.json new file mode 100644 index 0000000..6643425 --- /dev/null +++ b/schemas/forge-infra/v0/examples/authority-profile.github-app-installation.example.json @@ -0,0 +1,39 @@ +{ + "capabilities": ["contract: forge-infra/v0"], + "authority_profile_id": "github-app-installation", + "provider_context": { + "provider_id": "github", + "context_id": "github.com", + "deployment_kind": "hosted", + "base_uri": "https://github.com", + "offering": "github.com" + }, + "revision": "2026-08-27", + "observed_at": "2026-08-27T01:30:00Z", + "evidence": [ + { + "evidence_id": "github-app-auth", + "source_uri": "https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/about-authentication-with-a-github-app", + "checked_at": "2026-08-27T01:30:00Z" + } + ], + "authority_class": "application_installation", + "native_name": "GitHub App installation access token", + "principal_scope": "installation", + "resource_selection": "selected", + "acquisition": { + "mode": "api", + "mechanism": "app-manifest-conversion-then-maintainer-install", + "expected_public_receipt_fields": ["app_id", "client_id", "app_slug", "installation_id"], + "limitations": [ + "The maintainer approves installation and repository selection.", + "Account permissions such as gists are outside the installation principal." + ], + "evidence_refs": ["github-app-auth"] + }, + "grant_model": "permission_map", + "grant_catalog_uri": "https://docs.github.com/en/rest/authentication/permissions-required-for-github-apps", + "secret_delivery": "out_of_band", + "lifecycle_operations": ["install", "issue", "rotate", "introspect", "revoke"], + "default_ttl_seconds": 3600 +} diff --git a/schemas/forge-infra/v0/examples/authority-profile.github-pat.example.json b/schemas/forge-infra/v0/examples/authority-profile.github-pat.example.json new file mode 100644 index 0000000..d0699ea --- /dev/null +++ b/schemas/forge-infra/v0/examples/authority-profile.github-pat.example.json @@ -0,0 +1,35 @@ +{ + "capabilities": ["contract: forge-infra/v0"], + "authority_profile_id": "github-personal-access-token", + "provider_context": { + "provider_id": "github", + "context_id": "github.com", + "deployment_kind": "hosted", + "base_uri": "https://github.com", + "offering": "github.com" + }, + "revision": "2026-08-27", + "observed_at": "2026-08-27T01:30:00Z", + "evidence": [ + { + "evidence_id": "github-pat-auth", + "source_uri": "https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens", + "checked_at": "2026-08-27T01:30:00Z" + } + ], + "authority_class": "personal_access_token", + "native_name": "GitHub personal access token", + "principal_scope": "user", + "resource_selection": "provider_defined", + "acquisition": { + "mode": "operator_handoff", + "mechanism": "user-token-settings", + "operator_uri_template": "https://github.com/settings/tokens", + "expected_public_receipt_fields": ["token_fingerprint"], + "evidence_refs": ["github-pat-auth"] + }, + "grant_model": "provider_defined", + "grant_catalog_uri": "https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens", + "secret_delivery": "out_of_band", + "lifecycle_operations": ["issue", "rotate", "revoke"] +} diff --git a/schemas/forge-infra/v0/examples/binding-plan.github-installation.example.json b/schemas/forge-infra/v0/examples/binding-plan.github-installation.example.json new file mode 100644 index 0000000..0e847bd --- /dev/null +++ b/schemas/forge-infra/v0/examples/binding-plan.github-installation.example.json @@ -0,0 +1,152 @@ +{ + "capabilities": ["contract: forge-infra/v0"], + "binding_plan_id": "binding-plan-example-github-installation", + "catalog_ref": { + "catalog_id": "forge-neutral-v0", + "revision": "0.0.0-draft" + }, + "requirement_profile_ref": { + "requirement_profile_id": "long-running-agent-memory", + "digest": "sha256:8ab34f75b9d81dc8045c39656ba082b8a78d4149f2fc1cce79acd199cefde63b" + }, + "provider_profile_ref": { + "provider_profile_id": "github-public-docs-2026-08", + "revision": "2026-08-27" + }, + "provider_context": { + "provider_id": "github", + "context_id": "github.com", + "deployment_kind": "hosted", + "base_uri": "https://github.com", + "offering": "github.com" + }, + "target": { + "structure_class": "repository", + "structure_ref": "example/repository", + "parent_ref": "example" + }, + "created_at": "2026-08-27T01:30:00Z", + "plan_spec": { + "resolutions": [ + { + "capability_id": "forge.resource.issue", + "operation": "create", + "authority_profile_id": "github-app-installation", + "disposition": "preferred", + "resolution": "satisfied", + "required_grants": [ + { + "kind": "permission", + "name": "issues", + "access": "write", + "resource_scope": "installation" + } + ], + "interaction_modes": ["api"] + }, + { + "capability_id": "forge.automation.workflow_run", + "operation": "observe", + "authority_profile_id": "github-app-installation", + "disposition": "required", + "resolution": "satisfied", + "required_grants": [ + { + "kind": "permission", + "name": "actions", + "access": "read", + "resource_scope": "installation" + } + ], + "interaction_modes": ["api", "webhook", "poll"] + } + ], + "authority_actions": [ + { + "seq": 1, + "action": "install", + "produces_transition": "installed", + "authority_profile_id": "github-app-installation", + "execution_mode": "operator_handoff", + "notes": "A maintainer selects and approves the repository installation." + }, + { + "seq": 2, + "action": "bind", + "produces_transition": "bound", + "authority_profile_id": "github-app-installation", + "execution_mode": "service_job", + "service_offer_ref": "forge-authority-binding/v0" + }, + { + "seq": 3, + "action": "verify", + "authority_profile_id": "github-app-installation", + "execution_mode": "service_job", + "service_offer_ref": "forge-authority-verification/v0" + } + ], + "policy_input": { + "actor_ref": "operator-example", + "target": { + "structure_class": "repository", + "structure_ref": "example/repository", + "parent_ref": "example" + }, + "requested_operations": [ + { + "capability_id": "forge.resource.issue", + "operation": "create", + "authority_profile_id": "github-app-installation", + "required_grants": [ + { + "kind": "permission", + "name": "issues", + "access": "write", + "resource_scope": "installation" + } + ] + }, + { + "capability_id": "forge.automation.workflow_run", + "operation": "observe", + "authority_profile_id": "github-app-installation", + "required_grants": [ + { + "kind": "permission", + "name": "actions", + "access": "read", + "resource_scope": "installation" + } + ] + } + ], + "authority_actions": [ + { + "seq": 1, + "action": "install", + "produces_transition": "installed", + "authority_profile_id": "github-app-installation", + "execution_mode": "operator_handoff" + }, + { + "seq": 2, + "action": "bind", + "produces_transition": "bound", + "authority_profile_id": "github-app-installation", + "execution_mode": "service_job", + "service_offer_ref": "forge-authority-binding/v0" + }, + { + "seq": 3, + "action": "verify", + "authority_profile_id": "github-app-installation", + "execution_mode": "service_job", + "service_offer_ref": "forge-authority-verification/v0" + } + ] + }, + "gaps": [] + }, + "plan_spec_digest": "sha256:794f1e644edcb2704f428e4c336ce5ec3f50ba500bb3284116d0b174a0a15d25" +} diff --git a/schemas/forge-infra/v0/examples/binding-receipt.github-installation-installed.example.json b/schemas/forge-infra/v0/examples/binding-receipt.github-installation-installed.example.json new file mode 100644 index 0000000..effb0c1 --- /dev/null +++ b/schemas/forge-infra/v0/examples/binding-receipt.github-installation-installed.example.json @@ -0,0 +1,51 @@ +{ + "capabilities": ["contract: forge-infra/v0"], + "binding_receipt_id": "example-github-installation-installed", + "provider_context": { + "provider_id": "github", + "context_id": "github.com", + "deployment_kind": "hosted", + "base_uri": "https://github.com", + "offering": "github.com" + }, + "authority_profile_id": "github-app-installation", + "binding_plan_ref": { + "binding_plan_id": "binding-plan-example-github-installation", + "plan_spec_digest": "sha256:794f1e644edcb2704f428e4c336ce5ec3f50ba500bb3284116d0b174a0a15d25" + }, + "binding_plan_action_seq": 1, + "history_basis": "planned", + "idempotency_key": "github-installation-example-install-1", + "policy_decision_ref": "policy-decision-example-1", + "transition": "installed", + "outcome": "succeeded", + "subject": { + "kind": "installation", + "id": "12345678" + }, + "target": { + "structure_class": "repository", + "structure_ref": "example/repository", + "parent_ref": "example" + }, + "public_identifiers": { + "app_id": "24680", + "app_slug": "example-automation-app", + "installation_id": "12345678" + }, + "grants": [ + { + "kind": "permission", + "name": "issues", + "access": "write", + "resource_scope": "installation" + }, + { + "kind": "permission", + "name": "actions", + "access": "read", + "resource_scope": "installation" + } + ], + "occurred_at": "2026-08-27T01:29:00Z" +} diff --git a/schemas/forge-infra/v0/examples/binding-receipt.github-installation.example.json b/schemas/forge-infra/v0/examples/binding-receipt.github-installation.example.json new file mode 100644 index 0000000..8fb0ace --- /dev/null +++ b/schemas/forge-infra/v0/examples/binding-receipt.github-installation.example.json @@ -0,0 +1,60 @@ +{ + "capabilities": ["contract: forge-infra/v0"], + "binding_receipt_id": "example-github-installation-binding", + "provider_context": { + "provider_id": "github", + "context_id": "github.com", + "deployment_kind": "hosted", + "base_uri": "https://github.com", + "offering": "github.com" + }, + "authority_profile_id": "github-app-installation", + "binding_plan_ref": { + "binding_plan_id": "binding-plan-example-github-installation", + "plan_spec_digest": "sha256:794f1e644edcb2704f428e4c336ce5ec3f50ba500bb3284116d0b174a0a15d25" + }, + "binding_plan_action_seq": 2, + "history_basis": "planned", + "previous_receipt_ref": "example-github-installation-installed", + "idempotency_key": "github-installation-example-1", + "policy_decision_ref": "policy-decision-example-1", + "transition": "bound", + "outcome": "succeeded", + "subject": { + "kind": "installation", + "id": "12345678" + }, + "target": { + "structure_class": "repository", + "structure_ref": "example/repository", + "parent_ref": "example" + }, + "public_identifiers": { + "app_id": "24680", + "app_slug": "example-automation-app", + "installation_id": "12345678" + }, + "resource_bindings": [ + { + "structure_class": "repository", + "structure_ref": "example/repository", + "parent_ref": "example" + } + ], + "grants": [ + { + "kind": "permission", + "name": "issues", + "access": "write", + "resource_scope": "installation" + }, + { + "kind": "permission", + "name": "actions", + "access": "read", + "resource_scope": "installation" + } + ], + "auth_session_ref": "auth-session-example-1", + "occurred_at": "2026-08-27T01:30:00Z" +} diff --git a/schemas/forge-infra/v0/examples/forge-object-ref.issue.example.json b/schemas/forge-infra/v0/examples/forge-object-ref.issue.example.json new file mode 100644 index 0000000..8940690 --- /dev/null +++ b/schemas/forge-infra/v0/examples/forge-object-ref.issue.example.json @@ -0,0 +1,20 @@ +{ + "capabilities": ["contract: forge-infra/v0"], + "ref_id": "example-issue-reference", + "catalog_ref": { + "catalog_id": "forge-neutral-v0", + "revision": "0.0.0-draft" + }, + "provider_context": { + "provider_id": "github", + "context_id": "github.com", + "deployment_kind": "hosted", + "base_uri": "https://github.com", + "offering": "github.com" + }, + "capability_id": "forge.resource.issue", + "native_id": "42", + "repository_path": "example/repository", + "url": "https://github.com/example/repository/issues/42", + "created_at": "2026-08-27T01:30:00Z" +} diff --git a/schemas/forge-infra/v0/examples/provider-profile.github.example.json b/schemas/forge-infra/v0/examples/provider-profile.github.example.json new file mode 100644 index 0000000..fbe3771 --- /dev/null +++ b/schemas/forge-infra/v0/examples/provider-profile.github.example.json @@ -0,0 +1,195 @@ +{ + "capabilities": ["contract: forge-infra/v0"], + "provider_profile_id": "github-public-docs-2026-08", + "catalog_ref": { + "catalog_id": "forge-neutral-v0", + "revision": "0.0.0-draft" + }, + "provider_context": { + "provider_id": "github", + "context_id": "github.com", + "deployment_kind": "hosted", + "base_uri": "https://github.com", + "offering": "github.com" + }, + "revision": "2026-08-27", + "observed_at": "2026-08-27T01:30:00Z", + "evidence": [ + { + "evidence_id": "github-issues-rest", + "source_uri": "https://docs.github.com/en/rest/issues/issues", + "checked_at": "2026-08-27T01:30:00Z" + }, + { + "evidence_id": "github-gists-rest", + "source_uri": "https://docs.github.com/en/rest/gists/gists", + "checked_at": "2026-08-27T01:30:00Z" + }, + { + "evidence_id": "github-actions-rest", + "source_uri": "https://docs.github.com/en/rest/actions", + "checked_at": "2026-08-27T01:30:00Z" + }, + { + "evidence_id": "github-repository-contents", + "source_uri": "https://docs.github.com/en/rest/repos/contents", + "checked_at": "2026-08-27T01:30:00Z" + } + ], + "mappings": [ + { + "capability_id": "forge.repository.content", + "native_feature": { + "name": "Git smart protocol and repository contents", + "documentation_uri": "https://docs.github.com/en/rest/repos/contents", + "provider_key": "contents" + }, + "operation_cells": [ + { + "operation": "read", + "authority_profile_id": "github-app-installation", + "availability": "supported", + "interaction_modes": ["git_remote", "api"], + "required_grants": [ + { + "kind": "permission", + "name": "contents", + "access": "read", + "resource_scope": "installation" + } + ], + "evidence_refs": ["github-repository-contents"] + }, + { + "operation": "create", + "authority_profile_id": "github-app-installation", + "availability": "supported", + "interaction_modes": ["git_remote", "api"], + "required_grants": [ + { + "kind": "permission", + "name": "contents", + "access": "write", + "resource_scope": "installation" + } + ], + "evidence_refs": ["github-repository-contents"] + }, + { + "operation": "update", + "authority_profile_id": "github-app-installation", + "availability": "supported", + "interaction_modes": ["git_remote", "api"], + "required_grants": [ + { + "kind": "permission", + "name": "contents", + "access": "write", + "resource_scope": "installation" + } + ], + "evidence_refs": ["github-repository-contents"] + }, + { + "operation": "delete", + "authority_profile_id": "github-app-installation", + "availability": "supported", + "interaction_modes": ["git_remote", "api"], + "required_grants": [ + { + "kind": "permission", + "name": "contents", + "access": "write", + "resource_scope": "installation" + } + ], + "evidence_refs": ["github-repository-contents"] + } + ] + }, + { + "capability_id": "forge.resource.issue", + "native_feature": { + "name": "Issues", + "documentation_uri": "https://docs.github.com/en/rest/issues/issues", + "provider_key": "issues" + }, + "operation_cells": [ + { + "operation": "create", + "authority_profile_id": "github-app-installation", + "availability": "supported", + "interaction_modes": ["api"], + "required_grants": [ + { + "kind": "permission", + "name": "issues", + "access": "write", + "resource_scope": "installation" + } + ], + "evidence_refs": ["github-issues-rest"] + } + ] + }, + { + "capability_id": "forge.resource.snippet", + "native_feature": { + "name": "Gists", + "documentation_uri": "https://docs.github.com/en/rest/gists/gists", + "provider_key": "gists" + }, + "operation_cells": [ + { + "operation": "create", + "authority_profile_id": "github-app-installation", + "availability": "unsupported", + "required_grants": [], + "evidence_refs": ["github-gists-rest"], + "fallback_capability_id": "forge.resource.issue", + "limitations": ["Installation access tokens do not carry account-level gist authority."] + }, + { + "operation": "create", + "authority_profile_id": "github-personal-access-token", + "availability": "supported", + "interaction_modes": ["api"], + "required_grants": [ + { + "kind": "scope", + "name": "gist", + "access": "write", + "resource_scope": "user" + } + ], + "evidence_refs": ["github-gists-rest"] + } + ] + }, + { + "capability_id": "forge.automation.workflow_run", + "native_feature": { + "name": "GitHub Actions", + "documentation_uri": "https://docs.github.com/en/rest/actions", + "provider_key": "actions" + }, + "operation_cells": [ + { + "operation": "observe", + "authority_profile_id": "github-app-installation", + "availability": "supported", + "interaction_modes": ["api", "webhook", "poll"], + "required_grants": [ + { + "kind": "permission", + "name": "actions", + "access": "read", + "resource_scope": "installation" + } + ], + "evidence_refs": ["github-actions-rest"] + } + ] + } + ] +} diff --git a/schemas/forge-infra/v0/examples/requirement-profile.agent-memory.example.json b/schemas/forge-infra/v0/examples/requirement-profile.agent-memory.example.json new file mode 100644 index 0000000..75981ca --- /dev/null +++ b/schemas/forge-infra/v0/examples/requirement-profile.agent-memory.example.json @@ -0,0 +1,36 @@ +{ + "capabilities": ["contract: forge-infra/v0"], + "requirement_profile_id": "long-running-agent-memory", + "catalog_ref": { + "catalog_id": "forge-neutral-v0", + "revision": "0.0.0-draft" + }, + "title": "Long-running agent memory", + "description": "Portable forge requirements for a git-backed information archive with observable indexing automation.", + "requirements": [ + { + "capability_id": "forge.resource.issue", + "operations": ["read", "create", "update"], + "disposition": "preferred", + "notes": "Issues may provide collaborative intake and review around repository-backed records." + }, + { + "capability_id": "forge.automation.workflow_run", + "operations": ["observe"], + "disposition": "required", + "notes": "Indexing and validation must be observable and replayable." + }, + { + "capability_id": "forge.event.subscription", + "operations": ["create"], + "disposition": "allowed", + "notes": "Polling remains a valid fallback when callback operations are unavailable." + }, + { + "capability_id": "forge.resource.snippet", + "operations": ["create"], + "disposition": "forbidden", + "notes": "Detached snippets are not the authority store." + } + ] +} diff --git a/schemas/forge-infra/v0/examples/verification-receipt.github-installation.example.json b/schemas/forge-infra/v0/examples/verification-receipt.github-installation.example.json new file mode 100644 index 0000000..37c4de6 --- /dev/null +++ b/schemas/forge-infra/v0/examples/verification-receipt.github-installation.example.json @@ -0,0 +1,76 @@ +{ + "capabilities": ["contract: forge-infra/v0"], + "verification_receipt_id": "verification-example-github-installation", + "binding_receipt_ref": "example-github-installation-binding", + "binding_plan_ref": { + "binding_plan_id": "binding-plan-example-github-installation", + "plan_spec_digest": "sha256:794f1e644edcb2704f428e4c336ce5ec3f50ba500bb3284116d0b174a0a15d25" + }, + "binding_plan_action_seq": 3, + "catalog_ref": { + "catalog_id": "forge-neutral-v0", + "revision": "0.0.0-draft" + }, + "provider_context": { + "provider_id": "github", + "context_id": "github.com", + "deployment_kind": "hosted", + "base_uri": "https://github.com", + "offering": "github.com" + }, + "authority_profile_id": "github-app-installation", + "target": { + "structure_class": "repository", + "structure_ref": "example/repository", + "parent_ref": "example" + }, + "verified_at": "2026-08-27T01:31:00Z", + "valid_until": "2026-08-27T01:36:00Z", + "outcome": "conformant", + "methods": ["permission_introspection", "live_probe"], + "expected_grants": [ + { + "kind": "permission", + "name": "issues", + "access": "write", + "resource_scope": "installation" + }, + { + "kind": "permission", + "name": "actions", + "access": "read", + "resource_scope": "installation" + } + ], + "observed_grants": [ + { + "kind": "permission", + "name": "issues", + "access": "write", + "resource_scope": "installation" + }, + { + "kind": "permission", + "name": "actions", + "access": "read", + "resource_scope": "installation" + } + ], + "missing_grants": [], + "unexpected_grants": [], + "capability_checks": [ + { + "capability_id": "forge.resource.issue", + "operation": "create", + "result": "allowed", + "evidence_ref": "provider-permission-probe-example-1" + }, + { + "capability_id": "forge.automation.workflow_run", + "operation": "observe", + "result": "allowed", + "evidence_ref": "provider-permission-probe-example-2" + } + ], + "auth_session_ref": "auth-session-example-1" +} diff --git a/schemas/forge-infra/v0/forge-capability-catalog.json b/schemas/forge-infra/v0/forge-capability-catalog.json new file mode 100644 index 0000000..ead034c --- /dev/null +++ b/schemas/forge-infra/v0/forge-capability-catalog.json @@ -0,0 +1,134 @@ +{ + "capabilities": ["contract: forge-infra/v0"], + "catalog_id": "forge-neutral-v0", + "revision": "0.0.0-draft", + "updated_at": "2026-08-27T01:30:00Z", + "neutral_capabilities": [ + { + "capability_id": "forge.resource.issue", + "title": "Issue or work item", + "description": "Provider-native issue or work-item resource.", + "lane": "l4_forge_resources", + "kind": "resource", + "portability": "portable", + "operations": ["discover", "read", "create", "update"] + }, + { + "capability_id": "forge.resource.snippet", + "title": "Snippet or gist", + "description": "Provider-native short-form content resource outside the repository tree.", + "lane": "l4_forge_resources", + "kind": "resource", + "portability": "convergent", + "operations": ["discover", "read", "create", "update", "delete"] + }, + { + "capability_id": "forge.repository.remote", + "title": "Hosted repository container", + "description": "Provider-hosted repository lifecycle and governance container. Git object and ref operations remain native git.", + "lane": "l3_native_git", + "kind": "governance", + "portability": "portable", + "operations": ["discover", "read", "create", "update", "delete", "administer"] + }, + { + "capability_id": "forge.repository.content", + "title": "Repository content", + "description": "Read and mutate repository tree content through native git or a provider contents API. The contract resolves authority and interaction mode; it does not model git objects or commands.", + "lane": "l3_native_git", + "kind": "resource", + "portability": "portable", + "operations": ["read", "create", "update", "delete"] + }, + { + "capability_id": "forge.automation.workflow_definition", + "title": "Workflow or pipeline definition", + "description": "Provider-hosted automation definition that may be dispatched into a run.", + "lane": "l5_automation", + "kind": "automation", + "portability": "convergent", + "operations": ["discover", "read", "create", "update", "delete", "execute"] + }, + { + "capability_id": "forge.automation.workflow_run", + "title": "Workflow or pipeline run", + "description": "One provider-hosted automation execution and its observable result.", + "lane": "l5_automation", + "kind": "automation", + "portability": "convergent", + "operations": ["discover", "read", "cancel", "observe"] + }, + { + "capability_id": "forge.execution.runner", + "title": "Automation runner", + "description": "Execution capacity registered with the forge automation system.", + "lane": "l5_automation", + "kind": "execution", + "portability": "convergent", + "operations": ["discover", "read", "administer"] + }, + { + "capability_id": "forge.signal.commit_status", + "title": "Commit status", + "description": "Provider-native status associated with a source revision.", + "lane": "l6_events_telemetry", + "kind": "automation", + "portability": "portable", + "operations": ["read", "create", "update"] + }, + { + "capability_id": "forge.signal.check", + "title": "Structured revision check", + "description": "Rich check result with annotations and lifecycle distinct from a commit status.", + "lane": "l6_events_telemetry", + "kind": "automation", + "portability": "provider_extension", + "operations": ["read", "create", "update"] + }, + { + "capability_id": "forge.artifact.package", + "title": "Package", + "description": "Package or container artifact hosted by the provider.", + "lane": "l4_forge_resources", + "kind": "artifact", + "portability": "convergent", + "operations": ["discover", "read", "create", "delete"] + }, + { + "capability_id": "forge.artifact.release", + "title": "Release", + "description": "Named release and associated assets.", + "lane": "l4_forge_resources", + "kind": "artifact", + "portability": "portable", + "operations": ["discover", "read", "create", "update", "delete"] + }, + { + "capability_id": "forge.resource.wiki", + "title": "Wiki", + "description": "Forge-hosted documentation collection associated with a repository or project.", + "lane": "l4_forge_resources", + "kind": "resource", + "portability": "convergent", + "operations": ["discover", "read", "create", "update", "delete"] + }, + { + "capability_id": "forge.governance.environment", + "title": "Deployment environment", + "description": "Named environment and its deployment governance controls.", + "lane": "l4_forge_resources", + "kind": "governance", + "portability": "convergent", + "operations": ["discover", "read", "create", "update", "administer"] + }, + { + "capability_id": "forge.event.subscription", + "title": "Provider event subscription", + "description": "Provider-side configuration that routes selected forge events to a callback or other delivery mechanism.", + "lane": "l6_events_telemetry", + "kind": "event", + "portability": "convergent", + "operations": ["discover", "read", "create", "update", "delete", "administer"] + } + ] +} diff --git a/schemas/forge-infra/v0/forge-object-ref.schema.json b/schemas/forge-infra/v0/forge-object-ref.schema.json new file mode 100644 index 0000000..7b3487f --- /dev/null +++ b/schemas/forge-infra/v0/forge-object-ref.schema.json @@ -0,0 +1,50 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "contract:forge-infra/v0/forge-object-ref.schema.json", + "title": "Portable forge object reference", + "description": "Stable locator for one provider-native forge object. Access credentials and consumer-specific record links are not object identity.", + "type": "object", + "additionalProperties": false, + "required": [ + "capabilities", + "ref_id", + "catalog_ref", + "provider_context", + "capability_id", + "native_id" + ], + "properties": { + "capabilities": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/capabilities" + }, + "ref_id": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/opaqueId" + }, + "catalog_ref": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/catalogRef" + }, + "provider_context": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/providerContext" + }, + "capability_id": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/capabilityId" + }, + "native_id": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/opaqueId" + }, + "repository_path": { + "type": "string", + "minLength": 1, + "description": "Provider-native namespace/repository path when the object is repository-scoped." + }, + "url": { + "type": "string", + "format": "uri", + "description": "Optional human-facing retrieval URL; never the object identity." + }, + "created_at": { + "type": "string", + "format": "date-time" + } + } +} diff --git a/schemas/forge-infra/v0/provider-profile.schema.json b/schemas/forge-infra/v0/provider-profile.schema.json new file mode 100644 index 0000000..b46f9ab --- /dev/null +++ b/schemas/forge-infra/v0/provider-profile.schema.json @@ -0,0 +1,154 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "contract:forge-infra/v0/provider-profile.schema.json", + "title": "Curated forge provider profile", + "description": "Evidence-backed mapping from provider-native infrastructure to neutral forge capabilities.", + "type": "object", + "additionalProperties": false, + "required": [ + "capabilities", + "provider_profile_id", + "catalog_ref", + "provider_context", + "revision", + "observed_at", + "evidence", + "mappings" + ], + "properties": { + "capabilities": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/capabilities" + }, + "provider_profile_id": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/opaqueId" + }, + "catalog_ref": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/catalogRef" + }, + "provider_context": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/providerContext" + }, + "revision": { + "type": "string", + "minLength": 1 + }, + "observed_at": { + "type": "string", + "format": "date-time" + }, + "evidence": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/evidence" + } + }, + "mappings": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["capability_id", "native_feature", "operation_cells"], + "properties": { + "capability_id": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/capabilityId" + }, + "native_feature": { + "type": "object", + "additionalProperties": false, + "required": ["name"], + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "documentation_uri": { + "type": "string", + "format": "uri" + }, + "provider_key": { + "type": "string", + "minLength": 1 + } + } + }, + "operation_cells": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "operation", + "authority_profile_id", + "availability", + "required_grants", + "evidence_refs" + ], + "properties": { + "operation": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/operation" + }, + "authority_profile_id": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/opaqueId" + }, + "availability": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/availability" + }, + "interaction_modes": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/interactionMode" + } + }, + "required_grants": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/grant" + } + }, + "evidence_refs": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/opaqueId" + } + }, + "limitations": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "fallback_capability_id": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/capabilityId" + } + }, + "allOf": [ + { + "if": { + "properties": { + "availability": { + "enum": ["supported", "partial", "emulated"] + } + }, + "required": ["availability"] + }, + "then": { + "required": ["interaction_modes"] + } + } + ] + } + } + } + } + } + } +} diff --git a/schemas/forge-infra/v0/rejects/README.md b/schemas/forge-infra/v0/rejects/README.md new file mode 100644 index 0000000..740d6be --- /dev/null +++ b/schemas/forge-infra/v0/rejects/README.md @@ -0,0 +1,13 @@ +# Rejects + +| Fixture | Kind | Why | +| ----------------------------------------------------- | ---------- | ---------------------------------------------------------------------- | +| `authority-profile.handoff-without-uri.json` | structural | operator handoff lacks its required URI template | +| `binding-receipt.secret-field.json` | structural | secret-bearing field appears under public identifiers | +| `forge-object-ref.missing-provider.json` | structural | provider context is absent | +| `provider-profile.supported-without-mode.json` | structural | supported operation has no interaction mode | +| `provider-profile.unknown-capability.semantic.json` | semantic | schema-valid mapping cites a capability absent from the catalog | +| `requirement-profile.unknown-operation.semantic.json` | semantic | schema-valid requirement cites an operation absent from its capability | + +Semantic fixtures deliberately pass their object schema and fail the +cross-document controls in `scripts/test-forge-infra-controls.sh`. diff --git a/schemas/forge-infra/v0/rejects/authority-profile.handoff-without-uri.json b/schemas/forge-infra/v0/rejects/authority-profile.handoff-without-uri.json new file mode 100644 index 0000000..8663735 --- /dev/null +++ b/schemas/forge-infra/v0/rejects/authority-profile.handoff-without-uri.json @@ -0,0 +1,29 @@ +{ + "capabilities": ["contract: forge-infra/v0"], + "authority_profile_id": "reject-handoff-without-uri", + "provider_context": { + "provider_id": "gitlab", + "context_id": "gitlab.com", + "deployment_kind": "hosted" + }, + "revision": "reject", + "observed_at": "2026-08-27T01:30:00Z", + "evidence": [ + { + "evidence_id": "oauth-doc", + "source_uri": "https://docs.gitlab.com/integration/oauth_provider/", + "checked_at": "2026-08-27T01:30:00Z" + } + ], + "authority_class": "oauth_user_grant", + "native_name": "OAuth user grant", + "principal_scope": "user", + "acquisition": { + "mode": "operator_handoff", + "mechanism": "user-application-settings", + "evidence_refs": ["oauth-doc"] + }, + "grant_model": "oauth_scopes", + "secret_delivery": "out_of_band", + "lifecycle_operations": ["authorize", "refresh", "revoke"] +} diff --git a/schemas/forge-infra/v0/rejects/binding-receipt.secret-field.json b/schemas/forge-infra/v0/rejects/binding-receipt.secret-field.json new file mode 100644 index 0000000..5699a93 --- /dev/null +++ b/schemas/forge-infra/v0/rejects/binding-receipt.secret-field.json @@ -0,0 +1,29 @@ +{ + "capabilities": ["contract: forge-infra/v0"], + "binding_receipt_id": "reject-secret-field", + "provider_context": { + "provider_id": "github", + "context_id": "github.com", + "deployment_kind": "hosted" + }, + "authority_profile_id": "github-app-installation", + "binding_plan_ref": { + "binding_plan_id": "binding-plan-reject-secret", + "plan_spec_digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "binding_plan_action_seq": 1, + "history_basis": "imported", + "idempotency_key": "reject-secret-field-1", + "policy_decision_ref": "policy-decision-reject-1", + "transition": "bound", + "outcome": "succeeded", + "subject": { + "kind": "installation", + "id": "12345678" + }, + "public_identifiers": { + "installation_id": "12345678", + "access_token": "must-not-appear" + }, + "occurred_at": "2026-08-27T01:30:00Z" +} diff --git a/schemas/forge-infra/v0/rejects/forge-object-ref.missing-provider.json b/schemas/forge-infra/v0/rejects/forge-object-ref.missing-provider.json new file mode 100644 index 0000000..88f04d0 --- /dev/null +++ b/schemas/forge-infra/v0/rejects/forge-object-ref.missing-provider.json @@ -0,0 +1,10 @@ +{ + "capabilities": ["contract: forge-infra/v0"], + "ref_id": "reject-missing-provider", + "catalog_ref": { + "catalog_id": "forge-neutral-v0", + "revision": "0.0.0-draft" + }, + "capability_id": "forge.resource.issue", + "native_id": "1" +} diff --git a/schemas/forge-infra/v0/rejects/provider-profile.supported-without-mode.json b/schemas/forge-infra/v0/rejects/provider-profile.supported-without-mode.json new file mode 100644 index 0000000..085c8ea --- /dev/null +++ b/schemas/forge-infra/v0/rejects/provider-profile.supported-without-mode.json @@ -0,0 +1,39 @@ +{ + "capabilities": ["contract: forge-infra/v0"], + "provider_profile_id": "reject-supported-without-mode", + "catalog_ref": { + "catalog_id": "forge-neutral-v0", + "revision": "0.0.0-draft" + }, + "provider_context": { + "provider_id": "github", + "context_id": "github.com", + "deployment_kind": "hosted" + }, + "revision": "reject", + "observed_at": "2026-08-27T01:30:00Z", + "evidence": [ + { + "evidence_id": "issues-doc", + "source_uri": "https://docs.github.com/en/rest/issues/issues", + "checked_at": "2026-08-27T01:30:00Z" + } + ], + "mappings": [ + { + "capability_id": "forge.resource.issue", + "native_feature": { + "name": "Issues" + }, + "operation_cells": [ + { + "operation": "create", + "authority_profile_id": "github-app-installation", + "availability": "supported", + "required_grants": [], + "evidence_refs": ["issues-doc"] + } + ] + } + ] +} diff --git a/schemas/forge-infra/v0/rejects/provider-profile.unknown-capability.semantic.json b/schemas/forge-infra/v0/rejects/provider-profile.unknown-capability.semantic.json new file mode 100644 index 0000000..e2dd54a --- /dev/null +++ b/schemas/forge-infra/v0/rejects/provider-profile.unknown-capability.semantic.json @@ -0,0 +1,40 @@ +{ + "capabilities": ["contract: forge-infra/v0"], + "provider_profile_id": "reject-unknown-capability", + "catalog_ref": { + "catalog_id": "forge-neutral-v0", + "revision": "0.0.0-draft" + }, + "provider_context": { + "provider_id": "github", + "context_id": "github.com", + "deployment_kind": "hosted" + }, + "revision": "reject", + "observed_at": "2026-08-27T01:30:00Z", + "evidence": [ + { + "evidence_id": "example-doc", + "source_uri": "https://docs.github.com/", + "checked_at": "2026-08-27T01:30:00Z" + } + ], + "mappings": [ + { + "capability_id": "forge.resource.unknown", + "native_feature": { + "name": "Unknown feature" + }, + "operation_cells": [ + { + "operation": "read", + "authority_profile_id": "github-app-installation", + "availability": "supported", + "interaction_modes": ["api"], + "required_grants": [], + "evidence_refs": ["example-doc"] + } + ] + } + ] +} diff --git a/schemas/forge-infra/v0/rejects/requirement-profile.unknown-operation.semantic.json b/schemas/forge-infra/v0/rejects/requirement-profile.unknown-operation.semantic.json new file mode 100644 index 0000000..baaf31a --- /dev/null +++ b/schemas/forge-infra/v0/rejects/requirement-profile.unknown-operation.semantic.json @@ -0,0 +1,16 @@ +{ + "capabilities": ["contract: forge-infra/v0"], + "requirement_profile_id": "reject-unknown-operation", + "catalog_ref": { + "catalog_id": "forge-neutral-v0", + "revision": "0.0.0-draft" + }, + "title": "Reject operation outside neutral definition", + "requirements": [ + { + "capability_id": "forge.resource.issue", + "operations": ["execute"], + "disposition": "required" + } + ] +} diff --git a/schemas/forge-infra/v0/requirement-profile.schema.json b/schemas/forge-infra/v0/requirement-profile.schema.json new file mode 100644 index 0000000..6050e16 --- /dev/null +++ b/schemas/forge-infra/v0/requirement-profile.schema.json @@ -0,0 +1,64 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "contract:forge-infra/v0/requirement-profile.schema.json", + "title": "Forge capability requirement profile", + "description": "Provider-neutral requirements and prohibitions for an adopter use case.", + "type": "object", + "additionalProperties": false, + "required": ["capabilities", "requirement_profile_id", "catalog_ref", "title", "requirements"], + "properties": { + "capabilities": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/capabilities" + }, + "requirement_profile_id": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/opaqueId" + }, + "catalog_ref": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/catalogRef" + }, + "title": { + "type": "string", + "minLength": 1 + }, + "description": { + "type": "string" + }, + "requirements": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["capability_id", "operations", "disposition"], + "properties": { + "capability_id": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/capabilityId" + }, + "operations": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/operation" + } + }, + "disposition": { + "type": "string", + "enum": ["required", "preferred", "allowed", "forbidden"] + }, + "allowed_authority_classes": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/authorityClass" + } + }, + "notes": { + "type": "string" + } + } + } + } + } +} diff --git a/schemas/forge-infra/v0/verification-receipt.schema.json b/schemas/forge-infra/v0/verification-receipt.schema.json new file mode 100644 index 0000000..87c0679 --- /dev/null +++ b/schemas/forge-infra/v0/verification-receipt.schema.json @@ -0,0 +1,216 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "contract:forge-infra/v0/verification-receipt.schema.json", + "title": "Forge binding verification receipt", + "description": "Time-bounded observation of live grants, capability checks, and drift for one authority binding.", + "type": "object", + "additionalProperties": false, + "required": [ + "capabilities", + "verification_receipt_id", + "binding_receipt_ref", + "binding_plan_ref", + "binding_plan_action_seq", + "catalog_ref", + "provider_context", + "authority_profile_id", + "target", + "verified_at", + "valid_until", + "outcome", + "methods", + "expected_grants", + "observed_grants", + "missing_grants", + "unexpected_grants", + "capability_checks" + ], + "properties": { + "capabilities": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/capabilities" + }, + "verification_receipt_id": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/opaqueId" + }, + "binding_receipt_ref": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/opaqueId" + }, + "binding_plan_ref": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/bindingPlanRef" + }, + "binding_plan_action_seq": { + "type": "integer", + "minimum": 1 + }, + "catalog_ref": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/catalogRef" + }, + "provider_context": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/providerContext" + }, + "authority_profile_id": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/opaqueId" + }, + "target": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/structureRef" + }, + "verified_at": { + "type": "string", + "format": "date-time" + }, + "valid_until": { + "type": "string", + "format": "date-time" + }, + "outcome": { + "type": "string", + "enum": ["conformant", "drift", "denied", "revoked", "expired", "unknown"] + }, + "methods": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "string", + "enum": [ + "permission_introspection", + "provider_claim", + "dry_run", + "live_probe", + "operator_attestation" + ] + } + }, + "expected_grants": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/grant" + } + }, + "observed_grants": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/grant" + } + }, + "missing_grants": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/grant" + } + }, + "unexpected_grants": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/grant" + } + }, + "capability_checks": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["capability_id", "operation", "result"], + "properties": { + "capability_id": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/capabilityId" + }, + "operation": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/operation" + }, + "result": { + "type": "string", + "enum": ["allowed", "denied", "unknown"] + }, + "evidence_ref": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/opaqueId" + } + } + } + }, + "auth_session_ref": { + "$ref": "contract:forge-infra/v0/common.schema.json#/$defs/opaqueId" + }, + "notes": { + "type": "string" + } + }, + "allOf": [ + { + "if": { + "properties": { + "outcome": { + "const": "conformant" + } + }, + "required": ["outcome"] + }, + "then": { + "properties": { + "missing_grants": { + "maxItems": 0 + }, + "unexpected_grants": { + "maxItems": 0 + }, + "capability_checks": { + "minItems": 1, + "items": { + "properties": { + "result": { + "const": "allowed" + } + } + } + } + } + } + }, + { + "if": { + "properties": { + "outcome": { + "const": "drift" + } + }, + "required": ["outcome"] + }, + "then": { + "anyOf": [ + { + "properties": { + "missing_grants": { + "minItems": 1 + } + } + }, + { + "properties": { + "unexpected_grants": { + "minItems": 1 + } + } + }, + { + "properties": { + "capability_checks": { + "contains": { + "properties": { + "result": { + "enum": ["denied", "unknown"] + } + }, + "required": ["result"] + } + } + } + } + ] + } + } + ] +} diff --git a/scripts/test-forge-infra-controls.sh b/scripts/test-forge-infra-controls.sh new file mode 100644 index 0000000..a2e857a --- /dev/null +++ b/scripts/test-forge-infra-controls.sh @@ -0,0 +1,786 @@ +#!/bin/sh +# Structural and cross-document controls for contract: forge-infra/v0. +# Uses the repository RFC 8785 and RFC 3339 helpers for digest and instant +# comparisons; lexical JSON/timestamp ordering is not a security oracle. + +set -eu + +base="schemas/forge-infra/v0" +examples="$base/examples" +rejects="$base/rejects" +catalog="$base/forge-capability-catalog.json" +tmpd="$(mktemp -d)" +authority_index="$tmpd/authority-index.json" +trap 'rm -rf "$tmpd"' EXIT HUP INT TERM + +jq -s ' + map({key: .authority_profile_id, value: .provider_context}) + | from_entries +' "$examples"/authority-profile.*.example.json >"$authority_index" + +validate() { + schema="$1" + data="$2" + goneat validate data \ + --schema-file "$base/$schema" \ + --ref-dir "$base" \ + --data "$data" >/dev/null +} + +expect_rejected() { + schema="$1" + data="$2" + if validate "$schema" "$data" 2>/dev/null; then + echo " [!!] forge-infra reject unexpectedly passed: $data" >&2 + exit 1 + fi + echo " [ok] rejected: $data" +} + +redigest_plan() { + source_plan="$1" + output_plan="$2" + updated_digest="$(jq -c .plan_spec "$source_plan" | + python3 scripts/rfc8785-canonicalize.py | + sha256sum | + awk '{print "sha256:" $1}')" + jq --arg digest "$updated_digest" \ + '.plan_spec_digest = $digest' \ + "$source_plan" >"$output_plan" +} + +check_catalog() { + data="$1" + jq -e ' + .neutral_capabilities as $capabilities + | [$capabilities[].capability_id] as $ids + | (($ids | length) == ($ids | unique | length)) + and all( + $capabilities[]; + . as $capability + | (($capability.parent_id == null) + or ($ids | index($capability.parent_id) != null)) + ) + ' "$data" >/dev/null +} + +check_catalog_ref() { + data="$1" + jq -e --slurpfile catalog_data "$catalog" ' + .catalog_ref.catalog_id == $catalog_data[0].catalog_id + and .catalog_ref.revision == $catalog_data[0].revision + ' "$data" >/dev/null +} + +check_authority_profile() { + data="$1" + jq -e ' + [.evidence[].evidence_id] as $evidence_ids + | (($evidence_ids | length) == ($evidence_ids | unique | length)) + and all( + .acquisition.evidence_refs[]; + . as $evidence_ref + | ($evidence_ids | index($evidence_ref)) != null + ) + ' "$data" >/dev/null +} + +check_provider_profile() { + data="$1" + jq -e \ + --slurpfile catalog_data "$catalog" \ + --slurpfile authority_data "$authority_index" ' + . as $profile + | ($catalog_data[0].neutral_capabilities + | map({key: .capability_id, value: .operations}) + | from_entries) as $catalog + | $authority_data[0] as $authorities + | [.evidence[].evidence_id] as $evidence_ids + | [ + .mappings[] as $mapping + | $mapping.operation_cells[] + | [ + $mapping.capability_id, + .operation, + .authority_profile_id + ] + | join("|") + ] as $cell_keys + | ($profile.catalog_ref.catalog_id == $catalog_data[0].catalog_id) + and ($profile.catalog_ref.revision == $catalog_data[0].revision) + and (($evidence_ids | length) == ($evidence_ids | unique | length)) + and (($cell_keys | length) == ($cell_keys | unique | length)) + and all( + .mappings[]; + . as $mapping + | ($catalog | has($mapping.capability_id)) + and all( + $mapping.operation_cells[]; + . as $cell + | (($catalog[$mapping.capability_id] + | index($cell.operation)) != null) + and ($authorities | has($cell.authority_profile_id)) + and ($authorities[$cell.authority_profile_id] + == $profile.provider_context) + and all( + $cell.evidence_refs[]; + . as $evidence_ref + | (($evidence_ids | index($evidence_ref)) != null) + ) + and (($cell.fallback_capability_id == null) + or ($catalog | has($cell.fallback_capability_id))) + ) + ) + ' "$data" >/dev/null +} + +check_requirement_profile() { + data="$1" + jq -e --slurpfile catalog_data "$catalog" ' + ($catalog_data[0].neutral_capabilities + | map({key: .capability_id, value: .operations}) + | from_entries) as $catalog + | (.catalog_ref.catalog_id == $catalog_data[0].catalog_id) + and (.catalog_ref.revision == $catalog_data[0].revision) + and all( + .requirements[]; + . as $requirement + | ($catalog | has($requirement.capability_id)) + and all( + $requirement.operations[]; + . as $operation + | (($catalog[$requirement.capability_id] + | index($operation)) != null) + ) + ) + ' "$data" >/dev/null +} + +check_capability_ref() { + data="$1" + jq -e --slurpfile catalog_data "$catalog" ' + . as $reference + | ($catalog_data[0].neutral_capabilities + | map(.capability_id)) as $capability_ids + | ($reference.catalog_ref.catalog_id == $catalog_data[0].catalog_id) + and ($reference.catalog_ref.revision == $catalog_data[0].revision) + and (($capability_ids | index($reference.capability_id)) != null) + ' "$data" >/dev/null +} + +check_binding_plan() { + data="$1" + requirement="$2" + provider="$3" + requirement_digest="$(jq -c . "$requirement" | + python3 scripts/rfc8785-canonicalize.py | + sha256sum | + awk '{print "sha256:" $1}')" + plan_digest="$(jq -c .plan_spec "$data" | + python3 scripts/rfc8785-canonicalize.py | + sha256sum | + awk '{print "sha256:" $1}')" + jq -e \ + --arg requirement_digest "$requirement_digest" \ + --arg plan_digest "$plan_digest" \ + --slurpfile catalog_data "$catalog" \ + --slurpfile requirement_data "$requirement" \ + --slurpfile provider_data "$provider" \ + --slurpfile authority_data "$authority_index" ' + . as $plan + | $requirement_data[0] as $requirement + | $provider_data[0] as $provider + | $authority_data[0] as $authorities + | ([ + $provider.mappings[] as $mapping + | $mapping.operation_cells[] + | { + key: ([$mapping.capability_id, .operation, .authority_profile_id] + | join("|")), + value: . + } + ] | from_entries) as $provider_cells + | ([ + $requirement.requirements[] as $requirement_row + | $requirement_row.operations[] + | { + key: ([$requirement_row.capability_id, .] | join("|")), + value: $requirement_row.disposition + } + ] | from_entries) as $requirement_cells + | [ + $plan.plan_spec.resolutions[] + | [.capability_id, .operation] + | join("|") + ] as $resolution_keys + | [ + $plan.plan_spec.gaps[] + | [.capability_id, .operation] + | join("|") + ] as $gap_keys + | ([ + $plan.plan_spec.resolutions[] + | select(.resolution == "satisfied" or .resolution == "partial") + | select(.disposition != "forbidden") + | { + capability_id, + operation, + authority_profile_id, + required_grants: (.required_grants + | sort_by([.kind, .name, .access, .resource_scope])) + } + ] | sort_by([.capability_id, .operation, .authority_profile_id])) + as $resolved_policy_operations + | ([ + $plan.plan_spec.authority_actions[] + | del(.notes) + ] | sort_by(.seq)) as $planned_policy_actions + | def transition_for($action): + { + register: "registered", + install: "installed", + consent: "consented", + provision: "provisioned", + bind: "bound", + rotate: "rotated", + reconcile: "reconciled", + revoke: "revoked", + teardown: "torn_down" + }[$action]; + ($plan.catalog_ref.catalog_id == $catalog_data[0].catalog_id) + and ($plan.catalog_ref.revision == $catalog_data[0].revision) + and ($plan.requirement_profile_ref.requirement_profile_id + == $requirement.requirement_profile_id) + and ($plan.requirement_profile_ref.digest == $requirement_digest) + and ($plan.provider_profile_ref.provider_profile_id + == $provider.provider_profile_id) + and ($plan.provider_profile_ref.revision == $provider.revision) + and ($plan.provider_context == $provider.provider_context) + and ($plan.plan_spec_digest == $plan_digest) + and (([$plan.plan_spec.authority_actions[].seq] | sort) + == [range(1; ($plan.plan_spec.authority_actions | length) + 1)]) + and all( + $plan.plan_spec.resolutions[]; + . as $resolution + | ([$resolution.capability_id, $resolution.operation] + | join("|")) as $requirement_key + | ([$resolution.capability_id, $resolution.operation, + $resolution.authority_profile_id] | join("|")) as $provider_key + | ($requirement_cells | has($requirement_key)) + and ($requirement_cells[$requirement_key] + == $resolution.disposition) + and ($authorities | has($resolution.authority_profile_id)) + and ($authorities[$resolution.authority_profile_id] + == $plan.provider_context) + and ($provider_cells | has($provider_key)) + and ($provider_cells[$provider_key].required_grants + == $resolution.required_grants) + and (if $resolution.disposition == "forbidden" + then $resolution.resolution == "unsatisfied" + elif $resolution.resolution == "satisfied" + then $provider_cells[$provider_key].availability == "supported" + elif $resolution.resolution == "partial" + then ($provider_cells[$provider_key].availability == "partial" + or $provider_cells[$provider_key].availability == "emulated") + elif $resolution.resolution == "unsatisfied" + then $provider_cells[$provider_key].availability == "unsupported" + else $provider_cells[$provider_key].availability == "unknown" + end) + ) + and all( + $requirement_cells | to_entries[] + | select(.value == "required"); + .key as $required_key + | (($resolution_keys | index($required_key)) != null) + or (($gap_keys | index($required_key)) != null) + ) + and all( + $plan.plan_spec.authority_actions[]; + . as $action + | ($authorities | has($action.authority_profile_id)) + and (if $action.action == "verify" + then $action.produces_transition == null + else $action.produces_transition + == transition_for($action.action) + end) + ) + and ($plan.plan_spec.policy_input.target == $plan.target) + and (($plan.plan_spec.policy_input.requested_operations + | map(.required_grants |= + sort_by([.kind, .name, .access, .resource_scope])) + | sort_by([.capability_id, .operation, .authority_profile_id])) + == $resolved_policy_operations) + and (($plan.plan_spec.policy_input.authority_actions | sort_by(.seq)) + == $planned_policy_actions) + ' "$data" >/dev/null +} + +check_binding_receipt_set() { + plan="$1" + shift + jq -s -e \ + --slurpfile authority_data "$authority_index" \ + --slurpfile plan_data "$plan" ' + . as $receipts + | $authority_data[0] as $authorities + | $plan_data[0] as $plan + | ($receipts + | map({key: .binding_receipt_id, value: .}) + | from_entries) as $receipt_index + | ($plan.plan_spec.authority_actions + | map({key: (.seq | tostring), value: .}) + | from_entries) as $actions + | (($receipts | map(.binding_receipt_id) | length) + == ($receipts | map(.binding_receipt_id) | unique | length)) + and (($receipts | map(.idempotency_key) | length) + == ($receipts | map(.idempotency_key) | unique | length)) + and all( + $receipts[]; + . as $receipt + | ([ + $plan.plan_spec.authority_actions[] + | select(.produces_transition != null) + | select(.seq < $receipt.binding_plan_action_seq) + | .seq + ] | max) as $expected_previous_action_seq + | ($receipt.binding_plan_ref.binding_plan_id + == $plan.binding_plan_id) + and ($receipt.binding_plan_ref.plan_spec_digest + == $plan.plan_spec_digest) + and ($actions | has($receipt.binding_plan_action_seq | tostring)) + and ($actions[$receipt.binding_plan_action_seq | tostring] + .produces_transition == $receipt.transition) + and ($actions[$receipt.binding_plan_action_seq | tostring] + .authority_profile_id == $receipt.authority_profile_id) + and ($authorities | has($receipt.authority_profile_id)) + and ($authorities[$receipt.authority_profile_id] + == $receipt.provider_context) + and ($receipt.target == $plan.target) + and (if $receipt.binding_plan_action_seq == 1 + then ($receipt.history_basis == "imported") + or ($receipt.previous_receipt_ref == null) + else + ($receipt.previous_receipt_ref != null) + and ($receipt_index + | has($receipt.previous_receipt_ref)) + and ($receipt_index[$receipt.previous_receipt_ref].outcome + == "succeeded") + and ($receipt_index[$receipt.previous_receipt_ref] + .binding_plan_action_seq + == $expected_previous_action_seq) + and ($receipt_index[$receipt.previous_receipt_ref] + .binding_plan_ref == $receipt.binding_plan_ref) + and ($receipt_index[$receipt.previous_receipt_ref] + .provider_context == $receipt.provider_context) + and ($receipt_index[$receipt.previous_receipt_ref] + .authority_profile_id == $receipt.authority_profile_id) + and ($receipt_index[$receipt.previous_receipt_ref] + .target == $receipt.target) + end) + ) + ' "$@" >/dev/null +} + +check_verification_receipt() { + data="$1" + binding="$2" + plan="$3" + verified_at="$(jq -r '.verified_at' "$data")" + valid_until="$(jq -r '.valid_until' "$data")" + instant_order="$(python3 scripts/rfc3339-instant.py \ + --compare "$verified_at" "$valid_until")" || return 1 + [ "$instant_order" = "-1" ] || return 1 + jq -e \ + --slurpfile binding_data "$binding" \ + --slurpfile plan_data "$plan" \ + --slurpfile catalog_data "$catalog" ' + . as $verification + | $binding_data[0] as $binding + | $plan_data[0] as $plan + | ($plan.plan_spec.authority_actions + | map({key: (.seq | tostring), value: .}) + | from_entries) as $actions + | ($plan.plan_spec.resolutions + | map(select(.resolution == "satisfied" or .resolution == "partial")) + | map(.required_grants[]) + | unique + | sort_by([.kind, .name, .access, .resource_scope])) as $planned_grants + | ($plan.plan_spec.resolutions + | map(select(.resolution == "satisfied" or .resolution == "partial")) + | map([.capability_id, .operation] | join("|")) + | unique) as $planned_checks + | ($verification.capability_checks + | map([.capability_id, .operation] | join("|"))) as $observed_checks + | ($verification.binding_receipt_ref == $binding.binding_receipt_id) + and ($verification.binding_plan_ref.binding_plan_id + == $plan.binding_plan_id) + and ($verification.binding_plan_ref.plan_spec_digest + == $plan.plan_spec_digest) + and ($verification.binding_plan_ref == $binding.binding_plan_ref) + and ($actions | has($verification.binding_plan_action_seq | tostring)) + and ($actions[$verification.binding_plan_action_seq | tostring] + .action == "verify") + and ($actions[$verification.binding_plan_action_seq | tostring] + .authority_profile_id == $verification.authority_profile_id) + and ($verification.catalog_ref.catalog_id + == $catalog_data[0].catalog_id) + and ($verification.catalog_ref.revision + == $catalog_data[0].revision) + and ($verification.provider_context == $binding.provider_context) + and ($verification.authority_profile_id + == $binding.authority_profile_id) + and ($verification.target == $binding.target) + and (($verification.expected_grants + | sort_by([.kind, .name, .access, .resource_scope])) + == $planned_grants) + and (($binding.grants + | sort_by([.kind, .name, .access, .resource_scope])) + == $planned_grants) + and (($observed_checks | length) + == ($observed_checks | unique | length)) + and all( + $observed_checks[]; + . as $observed_check + | ($planned_checks | index($observed_check)) != null + ) + and all( + $plan.plan_spec.resolutions[] + | select(.disposition == "required" + and .resolution == "satisfied"); + ([.capability_id, .operation] | join("|")) as $required_check + | ($observed_checks | index($required_check)) != null + ) + and (if $verification.outcome == "conformant" + then ($binding.outcome == "succeeded") + and ($binding.transition == "bound" + or $binding.transition == "rotated" + or $binding.transition == "reconciled") + and (($verification.observed_grants + | sort_by([.kind, .name, .access, .resource_scope])) + == $planned_grants) + and ($verification.missing_grants | length) == 0 + and ($verification.unexpected_grants | length) == 0 + and all($verification.capability_checks[]; + .result == "allowed") + elif $verification.outcome == "drift" + then (($verification.missing_grants | length) > 0) + or (($verification.unexpected_grants | length) > 0) + or any($verification.capability_checks[]; + .result == "denied" or .result == "unknown") + else true + end) + ' "$data" >/dev/null +} + +echo " Forge-infra positive examples..." +validate capability-catalog.schema.json \ + "$catalog" +validate authority-profile.schema.json \ + "$examples/authority-profile.github-app-installation.example.json" +validate authority-profile.schema.json \ + "$examples/authority-profile.github-pat.example.json" +validate provider-profile.schema.json \ + "$examples/provider-profile.github.example.json" +validate binding-plan.schema.json \ + "$examples/binding-plan.github-installation.example.json" +validate binding-receipt.schema.json \ + "$examples/binding-receipt.github-installation-installed.example.json" +validate binding-receipt.schema.json \ + "$examples/binding-receipt.github-installation.example.json" +validate verification-receipt.schema.json \ + "$examples/verification-receipt.github-installation.example.json" +validate forge-object-ref.schema.json \ + "$examples/forge-object-ref.issue.example.json" +validate requirement-profile.schema.json \ + "$examples/requirement-profile.agent-memory.example.json" + +echo " Forge-infra structural rejects..." +expect_rejected authority-profile.schema.json \ + "$rejects/authority-profile.handoff-without-uri.json" +expect_rejected binding-receipt.schema.json \ + "$rejects/binding-receipt.secret-field.json" +expect_rejected forge-object-ref.schema.json \ + "$rejects/forge-object-ref.missing-provider.json" +expect_rejected provider-profile.schema.json \ + "$rejects/provider-profile.supported-without-mode.json" +jq '.missing_grants = [.expected_grants[0]]' \ + "$examples/verification-receipt.github-installation.example.json" \ + >"$tmpd/conformant-with-missing-grant.json" +expect_rejected verification-receipt.schema.json \ + "$tmpd/conformant-with-missing-grant.json" +jq '.outcome = "drift"' \ + "$examples/verification-receipt.github-installation.example.json" \ + >"$tmpd/drift-without-delta.json" +expect_rejected verification-receipt.schema.json \ + "$tmpd/drift-without-delta.json" + +echo " Forge-infra cross-document controls..." +check_catalog "$catalog" +check_authority_profile \ + "$examples/authority-profile.github-app-installation.example.json" +check_authority_profile \ + "$examples/authority-profile.github-pat.example.json" +check_provider_profile "$examples/provider-profile.github.example.json" +check_requirement_profile \ + "$examples/requirement-profile.agent-memory.example.json" +check_capability_ref "$examples/forge-object-ref.issue.example.json" +check_binding_plan \ + "$examples/binding-plan.github-installation.example.json" \ + "$examples/requirement-profile.agent-memory.example.json" \ + "$examples/provider-profile.github.example.json" + +jq '.plan_spec.policy_input.target.structure_ref = "other/repository"' \ + "$examples/binding-plan.github-installation.example.json" \ + >"$tmpd/policy-target-mismatch.raw.json" +redigest_plan \ + "$tmpd/policy-target-mismatch.raw.json" \ + "$tmpd/policy-target-mismatch.json" +validate binding-plan.schema.json "$tmpd/policy-target-mismatch.json" +if check_binding_plan \ + "$tmpd/policy-target-mismatch.json" \ + "$examples/requirement-profile.agent-memory.example.json" \ + "$examples/provider-profile.github.example.json"; then + echo " [!!] policy target diverged from execution target" >&2 + exit 1 +fi +echo " [ok] rejected policy/execution target mismatch" + +jq '.plan_spec.policy_input.requested_operations[0].operation = "delete"' \ + "$examples/binding-plan.github-installation.example.json" \ + >"$tmpd/policy-operation-mismatch.raw.json" +redigest_plan \ + "$tmpd/policy-operation-mismatch.raw.json" \ + "$tmpd/policy-operation-mismatch.json" +validate binding-plan.schema.json "$tmpd/policy-operation-mismatch.json" +if check_binding_plan \ + "$tmpd/policy-operation-mismatch.json" \ + "$examples/requirement-profile.agent-memory.example.json" \ + "$examples/provider-profile.github.example.json"; then + echo " [!!] policy operation diverged from resolved set" >&2 + exit 1 +fi +echo " [ok] rejected policy/resolution operation mismatch" + +jq '.plan_spec.policy_input.requested_operations[0].required_grants[0].access = "admin"' \ + "$examples/binding-plan.github-installation.example.json" \ + >"$tmpd/policy-grant-escalation.raw.json" +redigest_plan \ + "$tmpd/policy-grant-escalation.raw.json" \ + "$tmpd/policy-grant-escalation.json" +validate binding-plan.schema.json "$tmpd/policy-grant-escalation.json" +if check_binding_plan \ + "$tmpd/policy-grant-escalation.json" \ + "$examples/requirement-profile.agent-memory.example.json" \ + "$examples/provider-profile.github.example.json"; then + echo " [!!] policy grant exceeded resolved provider grants" >&2 + exit 1 +fi +echo " [ok] rejected policy/provider grant escalation" + +jq '.plan_spec.policy_input.authority_actions[1].action = "revoke"' \ + "$examples/binding-plan.github-installation.example.json" \ + >"$tmpd/policy-action-mismatch.raw.json" +redigest_plan \ + "$tmpd/policy-action-mismatch.raw.json" \ + "$tmpd/policy-action-mismatch.json" +validate binding-plan.schema.json "$tmpd/policy-action-mismatch.json" +if check_binding_plan \ + "$tmpd/policy-action-mismatch.json" \ + "$examples/requirement-profile.agent-memory.example.json" \ + "$examples/provider-profile.github.example.json"; then + echo " [!!] policy action diverged from planned authority action" >&2 + exit 1 +fi +echo " [ok] rejected policy/authority action mismatch" + +check_binding_receipt_set \ + "$examples/binding-plan.github-installation.example.json" \ + "$examples/binding-receipt.github-installation-installed.example.json" \ + "$examples/binding-receipt.github-installation.example.json" +check_verification_receipt \ + "$examples/verification-receipt.github-installation.example.json" \ + "$examples/binding-receipt.github-installation.example.json" \ + "$examples/binding-plan.github-installation.example.json" + +for unusable_outcome in failed partial unknown; do + jq --arg outcome "$unusable_outcome" \ + '.outcome = $outcome' \ + "$examples/binding-receipt.github-installation.example.json" \ + >"$tmpd/unusable-binding-$unusable_outcome.json" + validate binding-receipt.schema.json \ + "$tmpd/unusable-binding-$unusable_outcome.json" + if check_verification_receipt \ + "$examples/verification-receipt.github-installation.example.json" \ + "$tmpd/unusable-binding-$unusable_outcome.json" \ + "$examples/binding-plan.github-installation.example.json"; then + echo " [!!] conformant verification accepted $unusable_outcome binding" >&2 + exit 1 + fi + echo " [ok] rejected conformant verification of $unusable_outcome binding" +done + +jq '.binding_receipt_ref = "example-github-installation-installed"' \ + "$examples/verification-receipt.github-installation.example.json" \ + >"$tmpd/pre-bind-verification.json" +validate verification-receipt.schema.json "$tmpd/pre-bind-verification.json" +if check_verification_receipt \ + "$tmpd/pre-bind-verification.json" \ + "$examples/binding-receipt.github-installation-installed.example.json" \ + "$examples/binding-plan.github-installation.example.json"; then + echo " [!!] conformant verification accepted pre-bind receipt" >&2 + exit 1 +fi +echo " [ok] rejected conformant verification of pre-bind receipt" + +jq ' + .verified_at = "2026-08-27T02:00:00-04:00" + | .valid_until = "2026-08-27T03:00:00Z" +' "$examples/verification-receipt.github-installation.example.json" \ + >"$tmpd/reversed-offset-instants.json" +validate verification-receipt.schema.json "$tmpd/reversed-offset-instants.json" +if check_verification_receipt \ + "$tmpd/reversed-offset-instants.json" \ + "$examples/binding-receipt.github-installation.example.json" \ + "$examples/binding-plan.github-installation.example.json"; then + echo " [!!] verification accepted reversed RFC3339 instants" >&2 + exit 1 +fi +echo " [ok] rejected reversed mixed-offset validity interval" + +jq '.outcome = "failed"' \ + "$examples/binding-receipt.github-installation-installed.example.json" \ + >"$tmpd/failed-predecessor.json" +if check_binding_receipt_set \ + "$examples/binding-plan.github-installation.example.json" \ + "$tmpd/failed-predecessor.json" \ + "$examples/binding-receipt.github-installation.example.json"; then + echo " [!!] receipt advanced a failed predecessor" >&2 + exit 1 +fi +echo " [ok] rejected advancement from failed predecessor" + +jq '.transition = "installed"' \ + "$examples/binding-receipt.github-installation.example.json" \ + >"$tmpd/action-transition-mismatch.json" +if check_binding_receipt_set \ + "$examples/binding-plan.github-installation.example.json" \ + "$examples/binding-receipt.github-installation-installed.example.json" \ + "$tmpd/action-transition-mismatch.json"; then + echo " [!!] receipt transition did not match planned action" >&2 + exit 1 +fi +echo " [ok] rejected action/transition mismatch" + +jq ' + .plan_spec.authority_actions += [{ + "seq": 4, + "action": "rotate", + "produces_transition": "rotated", + "authority_profile_id": "github-app-installation", + "execution_mode": "service_job", + "service_offer_ref": "forge-authority-rotation/v0" + }] + | .plan_spec.policy_input.authority_actions += [{ + "seq": 4, + "action": "rotate", + "produces_transition": "rotated", + "authority_profile_id": "github-app-installation", + "execution_mode": "service_job", + "service_offer_ref": "forge-authority-rotation/v0" + }] +' "$examples/binding-plan.github-installation.example.json" \ + >"$tmpd/extended-chain-plan.raw.json" +redigest_plan \ + "$tmpd/extended-chain-plan.raw.json" \ + "$tmpd/extended-chain-plan.json" +validate binding-plan.schema.json "$tmpd/extended-chain-plan.json" +check_binding_plan \ + "$tmpd/extended-chain-plan.json" \ + "$examples/requirement-profile.agent-memory.example.json" \ + "$examples/provider-profile.github.example.json" +extended_chain_digest="$(jq -r '.plan_spec_digest' \ + "$tmpd/extended-chain-plan.json")" +jq --arg digest "$extended_chain_digest" \ + '.binding_plan_ref.plan_spec_digest = $digest' \ + "$examples/binding-receipt.github-installation-installed.example.json" \ + >"$tmpd/extended-installed.json" +jq --arg digest "$extended_chain_digest" \ + '.binding_plan_ref.plan_spec_digest = $digest | .outcome = "failed"' \ + "$examples/binding-receipt.github-installation.example.json" \ + >"$tmpd/extended-bound.json" +jq --arg digest "$extended_chain_digest" ' + .binding_receipt_id = "example-github-installation-rotated" + | .binding_plan_ref.plan_spec_digest = $digest + | .binding_plan_action_seq = 4 + | .previous_receipt_ref = "example-github-installation-installed" + | .idempotency_key = "github-installation-example-rotate-1" + | .transition = "rotated" + | .occurred_at = "2026-08-27T01:32:00Z" +' "$examples/binding-receipt.github-installation.example.json" \ + >"$tmpd/bypass-failed-intermediate.json" +validate binding-receipt.schema.json "$tmpd/extended-installed.json" +validate binding-receipt.schema.json "$tmpd/extended-bound.json" +validate binding-receipt.schema.json "$tmpd/bypass-failed-intermediate.json" +if check_binding_receipt_set \ + "$tmpd/extended-chain-plan.json" \ + "$tmpd/extended-installed.json" \ + "$tmpd/extended-bound.json" \ + "$tmpd/bypass-failed-intermediate.json"; then + echo " [!!] receipt skipped the immediate prior transition" >&2 + exit 1 +fi +echo " [ok] rejected skipped intermediate transition" + +jq '.plan_spec.authority_actions[2].seq = 4' \ + "$examples/binding-plan.github-installation.example.json" \ + >"$tmpd/noncontiguous-plan.raw.json" +noncontiguous_digest="$(jq -c .plan_spec "$tmpd/noncontiguous-plan.raw.json" | + python3 scripts/rfc8785-canonicalize.py | + sha256sum | + awk '{print "sha256:" $1}')" +jq --arg digest "$noncontiguous_digest" \ + '.plan_spec_digest = $digest' \ + "$tmpd/noncontiguous-plan.raw.json" \ + >"$tmpd/noncontiguous-plan.json" +if check_binding_plan \ + "$tmpd/noncontiguous-plan.json" \ + "$examples/requirement-profile.agent-memory.example.json" \ + "$examples/provider-profile.github.example.json"; then + echo " [!!] noncontiguous binding-plan actions passed" >&2 + exit 1 +fi +echo " [ok] rejected noncontiguous binding-plan actions" + +jq '.expected_grants = [] | .observed_grants = []' \ + "$examples/verification-receipt.github-installation.example.json" \ + >"$tmpd/verification-plan-grant-mismatch.json" +validate verification-receipt.schema.json \ + "$tmpd/verification-plan-grant-mismatch.json" +if check_verification_receipt \ + "$tmpd/verification-plan-grant-mismatch.json" \ + "$examples/binding-receipt.github-installation.example.json" \ + "$examples/binding-plan.github-installation.example.json"; then + echo " [!!] verification grants diverged from the plan" >&2 + exit 1 +fi +echo " [ok] rejected verification/plan grant mismatch" + +validate provider-profile.schema.json \ + "$rejects/provider-profile.unknown-capability.semantic.json" +if check_provider_profile \ + "$rejects/provider-profile.unknown-capability.semantic.json"; then + echo " [!!] unknown provider capability unexpectedly passed" >&2 + exit 1 +fi +echo " [ok] rejected unknown provider capability" + +validate requirement-profile.schema.json \ + "$rejects/requirement-profile.unknown-operation.semantic.json" +if check_requirement_profile \ + "$rejects/requirement-profile.unknown-operation.semantic.json"; then + echo " [!!] unknown requirement operation unexpectedly passed" >&2 + exit 1 +fi +echo " [ok] rejected unknown requirement operation" + +echo " [ok] forge-infra controls passed" diff --git a/scripts/validate-contract-manifests.sh b/scripts/validate-contract-manifests.sh index dc063de..801472d 100755 --- a/scripts/validate-contract-manifests.sh +++ b/scripts/validate-contract-manifests.sh @@ -71,6 +71,65 @@ for manifest_path in "$@"; do if [ "$advertised_capability" != "$capability" ]; then fail "$manifest_path" "capability mismatch: manifest has $capability, entry schema advertises $advertised_capability" fi + + manifest_dir="$(dirname "$manifest_path")" + catalog=$(jq -r 'if (.catalog | type) == "string" then .catalog else "" end' "$manifest_path") + if [ -n "$catalog" ]; then + case "$catalog" in + /* | *../* | *'\'* | */*) + fail "$manifest_path" "catalog must be a file relative to the manifest directory" + ;; + *) + catalog_path="$manifest_dir/$catalog" + if [ ! -f "$catalog_path" ]; then + fail "$manifest_path" "catalog target is missing: $catalog" + elif ! jq -e --arg capability "$capability" \ + '.capabilities | index($capability) != null' \ + "$catalog_path" >/dev/null 2>&1; then + fail "$manifest_path" "catalog does not advertise $capability: $catalog" + fi + ;; + esac + fi + + if ! object_schemas=$(jq -r ' + if .object_schemas == null then empty + elif ((.object_schemas | type) == "object" + and all(.object_schemas[]; type == "string")) + then .object_schemas[] | select(type == "string") + else error("object_schemas must be an object") + end + ' "$manifest_path"); then + fail "$manifest_path" "object_schemas must be an object of relative schema filenames" + continue + fi + + for object_schema in $object_schemas; do + case "$object_schema" in + /* | *../* | *'\'* | */*) + fail "$manifest_path" "object schema must be a file relative to the manifest directory: $object_schema" + continue + ;; + esac + object_path="$manifest_dir/$object_schema" + if [ ! -f "$object_path" ]; then + fail "$manifest_path" "object schema target is missing: $object_schema" + continue + fi + if ! object_capability=$(jq -r '.properties.capabilities.contains.const // ""' "$object_path"); then + fail "$manifest_path" "object schema is invalid JSON: $object_schema" + continue + fi + object_schema_id=$(jq -r 'if (."$id" | type) == "string" then ."$id" else "" end' "$object_path") + capability_path=${capability#contract: } + case "$object_schema_id" in + "contract:$capability_path/"*) object_id_matches=true ;; + *) object_id_matches=false ;; + esac + if [ "$object_capability" != "$capability" ] && [ "$object_id_matches" != true ]; then + fail "$manifest_path" "capability mismatch in object schema: $object_schema" + fi + done done if [ "$failures" -gt 0 ]; then