Skip to content

Add least-privilege cross-repository dispatch capability contract #735

Open

Description

@seonghobae

Problem

Consumer required workflows exchange a GitHub Actions OIDC assertion at /exchange_github_app_token, but the returned installation token is not proven to authorize a continuation dispatch into the central ContextualWisdomLab/.github repository.

This is a canonical Noema capability-boundary defect, not a consumer-workflow defect. The consumer must not compensate with a PAT, inherited repository secret, mutable source read, or broader GitHub token.

Exact RED evidence

A retained protected run exercises the existing exchange followed by central dispatch:

  • consumer: ContextualWisdomLab/late-life-anxiety-reanalysis
  • source ref: 34032cff52e8522db6ea0aad9f68ae0217e86ee3
  • retained log: local/supervisor-audit-20260912/current-ci/103571810868.log
  • evidence blob: cb91b0c0bfe1ff9001cab9e1675d97b00cfcef32
  • exchange call: line 141
  • central POST repos/ContextualWisdomLab/.github/dispatches: line 158
  • terminal result: line 173, Resource not accessible by integration (HTTP 403)

The protected .github doctoring note docs/doctoring/codeql-pr-private-consumer-read-permissions.md:21 also records that this central-dispatch use of a consumer-origin exchanged token was unexercised and might be out of scope. Proposed consumer PR ContextualWisdomLab/.github#2540 repeats that authority pattern and is Draft pending this owner contract.

Required contract

Implement and document a versioned API/client/schema that issues or delegates a short-lived, least-privilege capability for an explicit allowed central target repository and action. The contract must:

  • validate GitHub OIDC issuer, audience, organization, source repository, ref, reusable-workflow identity, and current exact revision;
  • bind source repository/PR/head and the explicit target repository/action;
  • permit only the allowlisted central repository_dispatch event(s), without repository contents or administration write authority;
  • reject arbitrary target repositories, event types, mutable heads, stale heads, forks, replay, malformed claims, and whitespace/non-string credential fields;
  • return no long-lived token and never log the assertion or capability;
  • expose provenance, expiry, and exact-revision evidence sufficient for the consumer to fail closed;
  • distinguish exchange/authorization failure from provider capacity and user cancellation.

Do not infer request fields or retrofit an undocumented query/body shape onto the existing endpoint. Publish the selected contract as a versioned owner API/client/schema.

Test-first acceptance

  1. RED integration fixture reproduces the retained 403 from a consumer OIDC identity targeting ContextualWisdomLab/.github.
  2. Unit/contract tests cover claim binding, target/action allowlists, replay/expiry, malformed JSON/typed credentials, and authorization denial.
  3. Hosted integration proves the exact consumer workflow identity can create only the intended central event.
  4. Owner CI is GREEN for API/schema, security, SBOM, and provenance on one exact SHA.
  5. Release an immutable Noema version containing the contract and evidence.
  6. Only then bump/pin ContextualWisdomLab/.github to that released contract and revalidate #2540 plus affected consumer PR heads.

Boundary

No PAT fallback, inherited central secret, cross-repository source read, or broad GitHub App installation token is acceptable. Until an immutable owner release exists, consumers must remain Draft/feature-gated and fail closed.

Permission-model correction — broker, not delegated GitHub token

GitHub's official Create a repository dispatch event contract requires Contents: write for GitHub App installation tokens and other fine-grained tokens: https://docs.github.com/en/rest/repos/repos#create-a-repository-dispatch-event. GitHub does not expose a narrower repository_dispatch-only token permission. Therefore a consumer-visible GitHub token cannot simultaneously create the event and satisfy this issue's “no repository contents write authority” invariant.

Selected owner shape: Noema validates the exact OIDC/source/PR/head/target/action/payload contract, performs the single allowlisted dispatch server-side with a target-repository installation token that never leaves Noema, and returns a short-lived signed receipt bound to the request and resulting dispatch attempt. The consumer receives no GitHub credential. Rejected alternatives are a broad installation token returned to the consumer, PAT/central-secret inheritance, and an undocumented extension of the legacy OpenCode endpoint.

Additional RED/GREEN acceptance:

  • prove the response contains no GitHub token or reusable credential;
  • prove arbitrary REST paths, target repositories, event types, and payload keys cannot be selected by the caller;
  • prove the broker installation token is scoped to the explicit target repository and is used only inside the dispatch adapter;
  • prove the signed receipt binds source repository, PR, head SHA, workflow SHA, target repository, event type, payload digest, outcome, expiry, and replay nonce;
  • prove GitHub denial is returned as authorization failure without misclassifying it as provider capacity or user cancellation.

Activity

  1. added
    area: ci-cdCI, GitHub Actions, checks, release, or supply chain
    area: securitySecurity boundary, hardening, or vulnerability prevention
    bugSomething isn't working
    on Sep 30, 2026
  2. seonghobae commented on Sep 30, 2026

    @seonghobae
    ContributorAuthor

    Protected-main boundary confirmation:

    • Noema already owns the documented POST /exchange API with an explicit target_repository body field (README.md, openapi.json, docs/api-spec.md).
    • Protected architecture says src/index.ts verifies OIDC and exchanges a repository-scoped installation token.
    • The documented current GitHub App permission set is Pull requests read/write, Checks read, Contents read. It does not establish a purpose-bound central repository_dispatch capability, and the existing .github candidate does not use this released Noema contract; it calls the legacy external /exchange_github_app_token endpoint without a target/action body.
    • The repository currently has no GitHub releases (live releases API returned an empty list on 2026-09-30), so there is no immutable owner version that .github can pin.

    Therefore the owner repair should extend/version Noema's own /exchange contract (or a separately versioned dispatch capability) rather than inventing parameters on the legacy OpenCode endpoint. It must bind the explicit ContextualWisdomLab/.github target and allowlisted continuation event/action while avoiding a reusable broad installation token. Publish an immutable release and consumer example/schema; then .github#2540 can replace its legacy endpoint call and pin the owner contract.

  3. seonghobae commented on Sep 30, 2026

    @seonghobae
    ContributorAuthor

    Proposed v1 owner contract — design checkpoint (no implementation claim)

    Fresh protected-source review on 2026-09-30 confirms:

    • authority remains main@1204a075ac15b924e2f907804ba99c337d1a3ac7;
    • the only public mutation contract is POST /exchange, which returns a reusable installation token;
    • current documented App permissions are Pull requests read/write, Checks read, Contents read;
    • no open Noema PR implements this issue (the search hit on fix: require canonical governance and normal-merge authority #730 is an unrelated governance lane);
    • GitHub's current official API contract still requires Contents: write for Create a repository dispatch event, so returning a token to the consumer cannot meet this issue's least-privilege invariant.

    Selected endpoint and responsibility

    Add a separate versioned endpoint:

    POST /v1/continuation-dispatches

    Do not widen or silently reinterpret POST /exchange. The endpoint is a synchronous broker: Noema validates the caller and live PR state, performs exactly one internally mapped central repository_dispatch, and returns a signed evidence receipt. It never returns a GitHub credential.

    Closed request schema

    Reject duplicate JSON keys, unknown keys, non-string typed lookalikes, whitespace-bearing identifiers, sparse/oversized payloads, and multiple JSON values before credential use.

    Required fields:

    • contract_version: exact published v1 identifier;
    • dispatch_action: enum noema_review_continuation or strix_scan_continuation;
    • central_repository: const ContextualWisdomLab/.github;
    • source_repository: exact owner/name, required to equal the verified OIDC repository claim;
    • pull_request_number: positive integer;
    • expected_head_sha: lowercase 40-hex;
    • expected_base_sha: lowercase 40-hex;
    • expected_base_ref: canonical nonempty Git ref name;
    • transport_retry_attempt: integer 1 or 2.

    The caller does not supply an arbitrary event type, REST path, client_payload, installation id, permissions object, or target URL. Noema maps the two action enums internally:

    • noema_review_continuation → event_type=noema-review;
    • strix_scan_continuation → event_type=strix-scan.

    The emitted client_payload is constructed only from the validated fields above; no additional caller-selected keys are copied.

    Trust and live-state sequence

    1. Apply existing bounded body/deadline/rate controls before any private-key use.
    2. Verify issuer, audience, organization, source repository, exact reusable-workflow path, immutable job_workflow_sha, time claims, and OIDC jti replay boundary.
    3. Require OIDC repository identity to equal source_repository; reject forks and mutable/ambiguous workflow identity.
    4. With the existing read-only source capability, fresh-read the PR immediately before dispatch and require open, non-draft, same-repository head/base, exact head SHA, exact base SHA, and exact base ref.
    5. Resolve a separate dispatch-broker GitHub App installation that is installed only on ContextualWisdomLab/.github. Its Contents-write installation token stays inside Noema, is scoped to that single repository, and is passed only to the fixed repository-dispatch adapter.
    6. Revalidate the request digest/idempotency record, create the fixed dispatch, destroy the in-memory token reference, persist the result, and return the receipt.

    This permission split avoids upgrading the existing consumer-installed credential-exchange App to Contents write across consumer repositories.

    Exactly-once/idempotency boundary

    Compute a canonical request digest from contract version, verified caller/workflow SHA, source repository, PR number, head/base/ref, action, central repository, and retry attempt. Atomically reserve that digest before dispatch:

    • first exact request may dispatch once;
    • exact replay returns the already committed receipt without a second GitHub call;
    • same logical continuation identity with different fields fails closed;
    • an incomplete/unknown upstream outcome is recorded as indeterminate and is not silently retried as success.

    Signed receipt (evidence, not authority)

    Return no token or reusable credential. The response contains:

    • receipt/version/key id and signature algorithm;
    • canonical request digest and replay/idempotency identity;
    • verified source repository, PR, head/base/ref;
    • verified workflow path and immutable workflow SHA;
    • fixed central repository and mapped event type;
    • exact emitted payload digest;
    • GitHub outcome (accepted, denied, indeterminate) and bounded upstream status;
    • issued/expiry timestamps and trace id;
    • detached signature from a dedicated Noema receipt key (not the GitHub App key).

    The receipt is audit evidence only; it does not authorize merge, approval, release, deployment, or a later dispatch.

    Error taxonomy

    Use dedicated error codes so transport authorization is never misreported as model/provider capacity or user cancellation:

    • validation/unknown field → ERR_DISPATCH_REQUEST_INVALID;
    • OIDC/workflow/source denial → ERR_DISPATCH_IDENTITY_DENIED;
    • moved/closed/draft/fork/live mismatch → ERR_DISPATCH_LIVE_STATE_STALE;
    • conflicting replay → ERR_DISPATCH_REPLAY_CONFLICT;
    • broker installation or GitHub 403 → ERR_GITHUB_DISPATCH_AUTHORIZATION;
    • GitHub 5xx/network/unknown outcome → ERR_GITHUB_DISPATCH_UPSTREAM.

    No secret, bearer, App JWT, installation token, or raw OIDC assertion may appear in the response, receipt, trace, or retained log.

    RED → GREEN acceptance and release order

    Start with executable RED contracts for: retained consumer→central 403; arbitrary target/event/path/payload-key attempts; wrong workflow SHA; source-claim mismatch; moved head/base/ref; closed/draft/fork PR; duplicate/unknown JSON members; exact replay and conflicting replay; GitHub 403/404/422/5xx/timeout; response/log secret absence; and exact receipt verification. Then implement, update OpenAPI/PRD/TRD/ADR/UML/threat model/runbook/CHANGELOG/gap baseline, obtain exact-head owner CI/review/security/SBOM/provenance evidence, and publish an immutable release.

    Only after that release exists should .github#2540 replace the legacy /exchange_github_app_token + local gh api sequence with this endpoint and pin the released contract. The current consumer Draft remains correctly blocked.

  4. seonghobae commented on Sep 30, 2026

    @seonghobae
    ContributorAuthor

    Canonical owner work is now staged in Draft PR #736.

    • exact head: 1f2465aac8b2ef16400b329eb050e17910d2a901
    • exact tree: 2db47f3e7cebfe1ebe8c3645564b350a722a2bd5
    • protected parent: 1204a075ac15b924e2f907804ba99c337d1a3ac7

    The PR contains only the Proposed broker design and a task-by-task RED→GREEN plan. It intentionally makes no implementation, release, deployment, or downstream-repair claim. .github#2540 remains Draft/blocked until the owner endpoint is implemented, verified at exact head, released immutably, and pinned by the consumer.

  5. seonghobae commented on Sep 30, 2026

    @seonghobae
    ContributorAuthor

    CodeQL owner-contract gap added to the existing acceptance — 2026-10-01

    Fresh downstream evidence shows that #736 is a valid least-privilege broker for the Noema/Strix continuations, but it does not yet carry the CodeQL failure that originally established this issue's RED.

    • Downstream exact head: ContextualWisdomLab/.github#2040@38a1692bd4419d4be3fb800abe70dd44644ac006.
    • Exact run/job: 36784069940 / 110121731055.
    • Failure after successful OIDC exchange: central repository_dispatch returned HTTP 403.
    • Current feat(dispatch): add least-privilege continuation broker #736 exact head 5c6d2f82821ad3039038d1102b225d018d472b1f admits only noema_review_continuation and strix_scan_continuation, mapped to noema-review and strix-scan.
    • The CodeQL path requires a separately closed, fixed mapping to codeql-scan-v2 and preserves additional evidence (matrix, required_run_id, required_jobs, producer_source_sha) that the current v1 request schema cannot represent.

    Keep #736's current two-action delta intact; do not widen the consumer token or copy the owner source. Extend the Noema owner stack with RED contracts for the CodeQL action, exact closed payload construction/digest/idempotency/receipt coverage, and the same no-token-egress guarantee. Then obtain exact-head CI/security/review, publish an immutable versioned release, and let .github#2040 adopt that released contract before rerunning its exact head. #2040 has been returned to Draft; no predecessor or sibling dispatch success is counted.

  6. seonghobae commented on Sep 30, 2026

    @seonghobae
    ContributorAuthor

    Fresh exact-head RED evidence from canonical consumer ContextualWisdomLab/.github#2040:

    • consumer head: 38a1692bd4419d4be3fb800abe70dd44644ac006
    • protected base: 37b10243cec3d160ecc9c1be75c71428b160a703
    • CodeQL required run: 36784069940
    • coordinator job: 110121731055
    • exact failure time: 2026-09-30T22:13:14Z
    • result: POST repos/ContextualWisdomLab/.github/dispatches returned Resource not accessible by integration (HTTP 403)

    The coordinator had already validated live PR state/head/base/merge identity, bound both required shard job IDs, obtained GitHub OIDC, and received a nonempty installation token from /exchange_github_app_token. Seven sibling exact-head workflows are terminal GREEN; only the owner capability boundary prevents CodeQL continuation. This independently reproduces the issue on the central repository itself, not only on a distinct consumer repository.

    No consumer workaround was introduced. ContextualWisdomLab/.github#2040 remains Ready/Proposed and fail-closed; it has no independent exact-head approval and cannot qualify for ordinary merge or circular-admission bypass. The required owner sequence remains RED fixture → broker API/schema/security CI GREEN → immutable Noema release → pinned consumer bump and fresh exact-head hosted verification.

  7. seonghobae commented on Sep 30, 2026

    @seonghobae
    ContributorAuthor

    Canonical owner repair snapshot:

    • noema#736 exact head 1bd5fce7e8df5a3ecbda109071d9b2bbd7a5917b, tree 5228844956962bacad76cc2c572cb324acca96c0, base 1204a075ac15b924e2f907804ba99c337d1a3ac7; open, Ready-for-review, mergeable.
    • Repaired expired-reservation same-transaction reuse, fixed-epoch test expiry, TypeScript narrowing, Worker expiry fixtures, and the 503 schema overlap (anyOf, with both real envelopes executed by contract tests).
    • Exact-head CI 36790157474, reviewer-ci 36790157475, and Security Scan 36790157585: SUCCESS.
    • patch-validator-image 36790157437: still running; not counted as GREEN.
    • Review threads: 0 unresolved. Qualifying approvals: 0. GitHub Releases: 0.

    Therefore the owner source is repaired but merge/release authority is not complete. .github#2540 remains correctly Draft and must not replace its legacy exchange/dispatch sequence or pin this contract until ordinary merge plus immutable Noema release publishes the receipt schema and key-id→SPKI artifact/digest.

  8. seonghobae commented on Sep 30, 2026

    @seonghobae
    ContributorAuthor

    Current exact-head evidence for the continuation admission cycle:

    • owner PR feat(dispatch): add least-privilege continuation broker #736: 81568910dccff168681b9b923e2c7667861b1063 (tree cb2e60ceb4632ae381f245c24070ad000b1946bb), open/Ready/mergeable;
    • RED 2a4488c6210921b9fa1af254868c0dfb764d5be9 → GREEN 350a36656343caf7730d107625e695575ef7e238 repairs only the proven post-commit/pre-request token-expiry path; exact receipt + original reservation capability are required for release, while request-started ambiguity remains terminal;
    • exact-head Checks: ci 36790870778: completed/success; patch-validator-image 36790870794: in_progress/pending; Security Scan 36790870914: completed/success; reviewer-ci 36790870754: completed/success;
    • unresolved review threads: 0; qualifying APPROVED reviews: 0; GitHub Releases: 0;
    • downstream ContextualWisdomLab/.github#2540 remains Draft at 48dbe4e7a080a0235cb0a244820c87232dada1f2, mechanically mergeable, and explicitly blocked on an immutable Noema release plus consumer pin.

    The running image/SBOM/provenance lane is not counted GREEN. No merge, bypass, release, consumer source copy, force update, destructive rebase, or manual rerun is authorized.

  9. seonghobae commented on Oct 1, 2026

    @seonghobae
    ContributorAuthor

    Fresh consumer recurrence — 2026-10-02

    The owner boundary remains live on ContextualWisdomLab/mcp-shared-gateway#28@ba0f67d2f02e3ae635f988fb95119c3108709aee.

    • Noema required run 36901701673, job 110502265658, classified all three orchestrator/free candidates as HTTP 429 and emitted bounded continuation eligibility.
    • Continuation job 110502605570 waited the emitted 170 seconds, revalidated the same open PR/head/base, then failed POST repos/ContextualWisdomLab/.github/dispatches with Resource not accessible by integration (HTTP 403).
    • The parallel Strix continuation reproduced the same authority defect in run 36901701684, jobs 110502262379 and 110502636142.
    • Consumer functional, coverage, Security, and Semgrep evidence was otherwise green; no consumer source repair can grant the missing central authority.

    This is a current consumer proof of the existing RED contract, not a new completion claim. .github#2540 must continue to avoid returning a broad GitHub installation token to the consumer. The Noema broker/release path in #736 remains the canonical owner repair: ordinary prerequisite integration, qualifying review, immutable release with receipt-verification material, then a released-contract pin and unchanged-head hosted continuation proof in .github#2540.

  10. seonghobae commented on Oct 2, 2026

    @seonghobae
    ContributorAuthor

    New exact-head consumer recurrence and offline boundary execution; not dispatch recovery or merge approval.

    ContextualWisdomLab/semantic-data-portal#107 remains at 742e7a7e1d4d0096a4fe46954d51c4b012f07dd3, base e48aa13c4af7a4875d4b53e6a60b50405c265a2f. Required Noema run 36970586032 model job 110723568569 ended in gateway HTTP429 after 2615.5 seconds; the single caller request does not count the gateway's provider routes. Continuation job 110737322328 then waited the emitted95 seconds, verified unchanged head/base, and failed the central repository-dispatch POST with Resource not accessible by integration (HTTP403). This workflow-token denial is distinct from an observer API quota. Strix job 110723570305 was canceled at six hours and supplies no authoritative completed verdict.

    I executed the unchanged published continuation shell from protected central 37b10243cec3d160ecc9c1be75c71428b160a703 and existing consumer-adapter #2540 d8bc25753dcec4cb83b816835568c0e11a27f4e5. Their dispatch-shell bytes are identical. Sixteen synthetic CLI-boundary cases pass with warnings fatal and zero skips/errors: valid matching identity dispatches exactly once; central403 propagates failure without claiming scheduled success; changed head/base, fork and closed PR never dispatch; invalid retry and excessive delay perform no API call. No real token/exchange/provider/network dispatch was used.

    This confirms preservation and causal scope, not that exchanged credentials grant central authority. Reuse Noema#735/#736's existing broker/release responsibility and .github#2540 consumer. Fresh owner read: #736 is still unmerged at dbe22c03651f878690691221595fcaaae2ebfadf, prerequisite#737 at bf2cd03dd817e0db8ec67aa9eb91458aaca36c14; immutable release count0 and no qualifying current-head approval observed. Required next transition remains ordinary prerequisite/broker integration plus immutable receipt-verification release, then released-contract adoption and actual exact-head continuation proof. No PAT fallback, inherited-secret change, broad token, unpublished source pin, manual redispatch, duplicate writer, Ready churn or protection bypass was performed. Semantic-data-portal#82/#107 remain open and #108 remains unstarted.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: ci-cdCI, GitHub Actions, checks, release, or supply chainarea: securitySecurity boundary, hardening, or vulnerability preventionbugSomething isn't workingpriority: highHigh-priority or P1 work

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions