Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 30 additions & 136 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ name: ci

on:
pull_request:
types: [opened, synchronize, reopened, edited]
merge_group:
types: [checks_requested]
push:
branches:
- main
Expand All @@ -17,29 +20,42 @@ jobs:
timeout-minutes: 15
permissions:
contents: read
pull-requests: read
env:
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: init.defaultBranch
GIT_CONFIG_VALUE_0: main
NOEMA_LIVE_PR_BASE_SHA: ${{ github.event.merge_group.base_sha || '' }}
steps:
- name: checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.pull_request.head.sha || github.event.merge_group.head_sha || github.sha }}
fetch-depth: 0
persist-credentials: false

- name: verify exact checkout
shell: bash
env:
NOEMA_EXPECTED_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
NOEMA_EXPECTED_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.event.merge_group.head_sha || github.sha }}
NOEMA_EXPECTED_BASE_SHA: ${{ github.event.merge_group.base_sha || '' }}
run: |
set -euo pipefail
if [[ ! "$NOEMA_EXPECTED_HEAD_SHA" =~ ^[0-9a-f]{40}$ ]]; then
printf '::error::Invalid expected head SHA.\n'
exit 1
fi
test "$(git rev-parse HEAD)" = "$NOEMA_EXPECTED_HEAD_SHA"
if [ "$GITHUB_EVENT_NAME" = "merge_group" ]; then
if [[ ! "$NOEMA_EXPECTED_BASE_SHA" =~ ^[0-9a-f]{40}$ ]]; then
printf '::error::Invalid merge-group base SHA.\n'
exit 1
fi
git merge-base --is-ancestor "$NOEMA_EXPECTED_BASE_SHA" "$NOEMA_EXPECTED_HEAD_SHA"
elif [ -n "$NOEMA_EXPECTED_BASE_SHA" ]; then
printf '::error::Unexpected merge-group base SHA outside a merge-group event.\n'
exit 1
fi

- name: setup node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
Expand All @@ -54,89 +70,28 @@ jobs:
test "$(node --version)" = "v24.21.0"
test "$(npm --version)" = "11.19.0"

- name: verify live pull-request base before lockfile control
- name: capture live pull-request identity before lockfile control
if: github.event_name == 'pull_request'
shell: bash
env:
GH_TOKEN: ${{ github.token }}
NOEMA_PR_BASE_REF: ${{ github.event.pull_request.base.ref }}
NOEMA_PR_NUMBER: ${{ github.event.pull_request.number }}
NOEMA_EXPECTED_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
set -euo pipefail
if [[ ! "$NOEMA_EXPECTED_HEAD_SHA" =~ ^[0-9a-f]{40}$ ]]; then
printf '::error::Invalid expected head SHA.\n'
exit 1
fi
if [ -z "$NOEMA_PR_BASE_REF" ]; then
printf '::error::Pull-request base ref is unavailable.\n'
exit 1
fi
repository_name="${GITHUB_REPOSITORY#*/}"
resolve_live_base_sha() {
local output=""
local graphql_transient_exhausted="false"
for attempt in 1 2 3; do
if output="$(
gh api graphql \
-f query='query($owner:String!,$name:String!,$qualifiedName:String!){repository(owner:$owner,name:$name){ref(qualifiedName:$qualifiedName){target{oid}}}}' \
-F owner="$GITHUB_REPOSITORY_OWNER" \
-F name="$repository_name" \
-F qualifiedName="refs/heads/${NOEMA_PR_BASE_REF}" \
--jq '.data.repository.ref.target.oid' \
2>&1
)"; then
printf '%s\n' "$output"
return 0
fi
if printf '%s\n' "$output" | grep -Eq '\(HTTP (502|503|504)\)$'; then
if [ "$attempt" -lt 3 ]; then
sleep "$attempt"
continue
fi
graphql_transient_exhausted="true"
break
fi
printf '::error::Live pull-request base resolution failed after attempt %s.\n' "$attempt" >&2
return 1
done
if [ "$graphql_transient_exhausted" != "true" ]; then
return 1
fi
if output="$(
gh api --method GET "repos/${GITHUB_REPOSITORY}/git/ref/heads/${NOEMA_PR_BASE_REF}" \
--jq '.object.sha' \
2>&1
)"; then
printf '%s\n' "$output"
return 0
fi
printf '::error::Live pull-request base REST fallback failed.\n' >&2
return 1
}
live_base_sha="$(resolve_live_base_sha)"
if [[ ! "$live_base_sha" =~ ^[0-9a-f]{40}$ ]]; then
printf '::error::Live pull-request base ref did not resolve to a full commit SHA.\n'
exit 1
fi
if ! git merge-base --is-ancestor "$live_base_sha" "$NOEMA_EXPECTED_HEAD_SHA"; then
printf '::error::Pull-request head does not contain the current live base %s.\n' "$live_base_sha"
exit 1
fi
printf 'NOEMA_LIVE_BASE_SHA=%s\n' "$live_base_sha" >> "$GITHUB_ENV"
run: bash scripts/verify-live-pull-request-identity.sh capture

- name: verify lockfile change control
if: github.event_name == 'pull_request'
if: github.event_name == 'pull_request' || github.event_name == 'merge_group'
shell: bash
run: |
set -euo pipefail
if [[ ! "$NOEMA_LIVE_BASE_SHA" =~ ^[0-9a-f]{40}$ ]]; then
if [[ ! "$NOEMA_LIVE_PR_BASE_SHA" =~ ^[0-9a-f]{40}$ ]]; then
printf '::error::Invalid live pull-request base SHA.\n'
exit 1
fi
base_lock="$RUNNER_TEMP/noema-package-lock-base.json"
git show "${NOEMA_LIVE_BASE_SHA}:package-lock.json" >"$base_lock"
git show "${NOEMA_LIVE_PR_BASE_SHA}:package-lock.json" >"$base_lock"
NOEMA_LOCKFILE_BASE_PATH="$base_lock" \
NOEMA_LOCKFILE_BASE_SHA="$NOEMA_LIVE_BASE_SHA" \
NOEMA_LOCKFILE_BASE_SHA="$NOEMA_LIVE_PR_BASE_SHA" \
node --input-type=module <<'NODE'
import { runLockfileChangeControl } from "./scripts/lockfile-change-control.mjs";

Expand Down Expand Up @@ -182,7 +137,7 @@ jobs:
if: ${{ !cancelled() && steps.regenerate_lockfile.outcome != 'skipped' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: noema-lockfile-regeneration-${{ github.event.pull_request.head.sha || github.sha }}
name: noema-lockfile-regeneration-${{ github.event.pull_request.head.sha || github.event.merge_group.head_sha || github.sha }}
path: |
${{ runner.temp }}/noema-package-lock-regenerated.json
${{ runner.temp }}/noema-package-lock-regeneration.diff
Expand Down Expand Up @@ -272,72 +227,11 @@ jobs:
- name: release acquisition integrity
run: npm run acquisition:integrity

- name: refuse pull-request base drift after verification
- name: refuse pull-request identity drift after verification
if: github.event_name == 'pull_request'
shell: bash
env:
GH_TOKEN: ${{ github.token }}
NOEMA_PR_BASE_REF: ${{ github.event.pull_request.base.ref }}
run: |
set -euo pipefail
if [[ ! "$NOEMA_LIVE_BASE_SHA" =~ ^[0-9a-f]{40}$ ]]; then
printf '::error::Initial live pull-request base SHA is unavailable.\n'
exit 1
fi
if [ -z "$NOEMA_PR_BASE_REF" ]; then
printf '::error::Pull-request base ref is unavailable.\n'
exit 1
fi
repository_name="${GITHUB_REPOSITORY#*/}"
resolve_live_base_sha() {
local output=""
local graphql_transient_exhausted="false"
for attempt in 1 2 3; do
if output="$(
gh api graphql \
-f query='query($owner:String!,$name:String!,$qualifiedName:String!){repository(owner:$owner,name:$name){ref(qualifiedName:$qualifiedName){target{oid}}}}' \
-F owner="$GITHUB_REPOSITORY_OWNER" \
-F name="$repository_name" \
-F qualifiedName="refs/heads/${NOEMA_PR_BASE_REF}" \
--jq '.data.repository.ref.target.oid' \
2>&1
)"; then
printf '%s\n' "$output"
return 0
fi
if printf '%s\n' "$output" | grep -Eq '\(HTTP (502|503|504)\)$'; then
if [ "$attempt" -lt 3 ]; then
sleep "$attempt"
continue
fi
graphql_transient_exhausted="true"
break
fi
printf '::error::Live pull-request base resolution failed after attempt %s.\n' "$attempt" >&2
return 1
done
if [ "$graphql_transient_exhausted" != "true" ]; then
return 1
fi
if output="$(
gh api --method GET "repos/${GITHUB_REPOSITORY}/git/ref/heads/${NOEMA_PR_BASE_REF}" \
--jq '.object.sha' \
2>&1
)"; then
printf '%s\n' "$output"
return 0
fi
printf '::error::Live pull-request base REST fallback failed.\n' >&2
return 1
}
live_base_sha="$(resolve_live_base_sha)"
if [[ ! "$live_base_sha" =~ ^[0-9a-f]{40}$ ]]; then
printf '::error::Live pull-request base ref did not resolve to a full commit SHA.\n'
exit 1
fi
if [ "$live_base_sha" != "$NOEMA_LIVE_BASE_SHA" ]; then
printf '::error::Pull-request base branch advanced during verification from %s to %s.\n' \
"$NOEMA_LIVE_BASE_SHA" "$live_base_sha"
exit 1
fi
test "$live_base_sha" = "$NOEMA_LIVE_BASE_SHA"
NOEMA_PR_NUMBER: ${{ github.event.pull_request.number }}
NOEMA_EXPECTED_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: bash scripts/verify-live-pull-request-identity.sh check
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,33 @@
- Add a Noema-owned exact-claim evidence receipt contract whose execution and research producers serialize one canonical artifact that binds every receipt semantic field, including command/result/isolation/network or source revision/excerpt/retrieval policy. Admission accepts only a receipt ID from untrusted model output. The owner API first verifies the exact authenticated OpenCode-handoff manifest digest, canonical envelope bytes, reviewed producer-to-kind policy, and repository/head/workflow/run/attempt identity before it can construct an immutable typed index; admission then reconstructs each canonical artifact and verifies time/claim/artifact identity. The version-2 manifest now binds a separate producer-authenticated `ClaimEvidenceRequirement` containing the exact claim, independently required evidence kind, and `context` or `finding` publication authority. Raw current-head source lines are context only: they are withheld from finding-reference prompts and cannot publish a finding or `request_changes`; an explicitly producer-authorized source finding remains usable and retains exact path/line checks. Finding-free model `request_changes` and `blocked` verdicts cannot bypass receipt admission to publish a vacuous blocking review. Requirement/receipt kind mismatch, fixed-artifact semantic substitution, caller-supplied receipt dictionaries, model self-classification, stale identities, cross-kind receipts, marker-only sandbox output, noncanonical artifact bytes, and expired receipts fail closed before the GitHub publisher. This remains the owner prerequisite for ContextualWisdomLab/.github#1641 and issue #555. The reviewed `sandboxed_verify` adapter exists in owner source, but its actual central stdout/stderr/marker-to-manifest wiring and the trusted research producer are not yet integrated; exact-head hosted GREEN, immutable release, and the verified central consumer bump remain required.

## Unreleased
- PR #733 adds the missing `merge_group/checks_requested` application-CI
contract. Merge-queue runs now checkout the exact event `head_sha`, require
the exact event `base_sha` to be a canonical commit ancestor, and execute the
existing lockfile change control plus ordinary release verification on
GitHub's latest-base synthetic merge group.
This is source support, not merge authority by itself: the protected-main
ruleset must require the merge queue and this check before the P0 base-only
invalidation gap can be closed.
- PR #733 follow-up repairs an invalid TypeScript test fixture introduced by
the live base-ref verifier tests. The generated fake `gh` program no longer
nests unescaped template literals inside its enclosing fixture template;
exact API-path and unexpected-invocation checks use ordinary string
concatenation instead. `stripTypeScriptTypes` now parses the exact file and
the direct Node syntax check is green. The repository Vitest suite remains
unclaimed locally because the available Node 24.19.0 runtime is rejected by
the repository's exact Node 24.21.0 `devEngines` contract.
- PR #733 removes stale PR-snapshot authority from each started or re-run
application CI gate. The gate reads only `(head SHA, base ref)` from the live
pull-request endpoint and resolves the current base SHA independently through
the Git-ref endpoint; identity drift or missing exact ancestry fails closed.
The fake `gh` and `git` executables now verify exact methods, endpoints,
projections, and merge-base operands, including fail-closed non-ancestry.
This does not self-invalidate a terminal same-head Check after a later
base-only push because GitHub emits no corresponding PR-head synchronize
event. That P0 observer/merge-authority gap remains open, so the PR stays
Draft / Proposed / merge HOLD pending prerequisite #737, fresh hosted
exact-head Checks, independent approval, and protected-main revalidation.
- PR #737의 patch-validator static-runtime receipt verifier가 `process.versions.node`뿐 아니라 bundled Undici를 reviewed fixed version `7.29.1`로 exact-match한다. 서로 내부적으로 일치하더라도 Undici `7.29.0` inventory/component/PURL/raw scan evidence는 fail closed하며, simplified verifier fixtures도 실제 Node 24.21.0 runtime identity에 맞춘다. Source/test GREEN은 새 exact-head hosted image/SBOM/provenance/scanner, 독립 승인, ordinary merge, immutable release를 대신하지 않는다.
- `packages/noema-core/README.md`의 `build_agent` 서명을 코드와 ADR-0014에 맞춘다. caller-visible `retries=3` 표기를 제거하고, 이 경계의 model-attempt retry는 `Agent(..., retries=0)`으로 고정되며 orchestration retry/failover는 contextual-orchestrator 소유임을 명시한다. 구현·ADR 상태·provider 정책은 바꾸지 않는다.
- PR #724 makes private-vulnerability-reporting setting receipts a fail-closed strict-release prerequisite without turning setting evidence into release or deployment truth. The consumer validates bounded stable/no-follow receipt files with fatal UTF-8 and duplicate-key rejection, canonical repository/source/timestamp identity, bounded future skew and freshness, and explicit `PASS + enabled:true + failures:[]`; all observations are validated before ordering and the newest observation wins even when it is FAIL, so a historical PASS cannot mask later contradictory evidence. Protected CD recollects the setting in the same job from the exact immutable-release commit through a repository-scoped `Metadata: read` GitHub App capability, moves the short-lived bearer into an owner-only capability file, unsets the raw bootstrap value before Node execution, feeds the same-run receipt into `release:verify:strict`, and retains the receipt with deployment evidence. The ordinary pre-tag source gate remains `release:verify`; bare pre-publication `release:verify:strict` without a fresh matching receipt is not an authorized procedure. External reporter visibility, staffing/notification, benign private-case handling, immutable-release publication, deployment, KPI and acquisition authority remain independent evidence classes.
Expand Down
2 changes: 1 addition & 1 deletion docs/CONTEXT_MAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Noema owns an evidence-producing credential and maintenance control plane plus a

### Credential Exchange

Owns GitHub Actions OIDC verification, exact reusable-workflow source identity, replay/rate controls, repository-scoped GitHub App capability minting, and the `/health`, `/ready`, `/exchange` HTTP boundary. Credential evidence is not review, merge, release, deployment, or legal authority.
Owns GitHub Actions OIDC verification, exact reusable-workflow source identity, replay/rate controls, owner identity bound to the GitHub App installation rather than a fixed owner list (ADR-0019), repository-scoped GitHub App capability minting, and the `/health`, `/ready`, `/exchange` HTTP boundary. Credential evidence is not review, merge, release, deployment, or legal authority.

### Maintenance Control

Expand Down
18 changes: 18 additions & 0 deletions docs/adr/0003-exact-revision-and-live-base.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,16 @@ When a decision depends on current integration ancestry, dependency content, loc

A GitHub `refs/pull/*/merge` revision is valid integration evidence. It is not labelled immutable-head evidence unless the specific scanner/test separately proves the PR head it consumed.

For protected-main merge authority, application CI also subscribes to
`merge_group/checks_requested`. It checks out the event's exact synthetic
`head_sha`, validates the event's exact `base_sha`, and requires that base to be
an ancestor of the tested merge-group commit. The same exact base SHA feeds the
existing lockfile change-control comparison. GitHub rebuilds the temporary
merge group from the latest base before required checks authorize a queued
merge. Source support is non-authorizing until the protected-main ruleset
requires the merge queue and the application-CI check; a direct ordinary merge
must not be represented as carrying this base-binding guarantee.

### Stacked PRs

The immediate stack predecessor's live tip is an explicit dependency. A stacked PR is not early-retargeted merely to create a required check if doing so duplicates predecessor changes or corrupts dependency order.
Expand Down Expand Up @@ -68,6 +78,7 @@ Any movement between decision and write causes abort/re-plan rather than blind r
## Verification

- `test/ci-exact-head-contract.test.ts`.
- `test/ci-live-pull-request-identity.test.ts` merge-group head/base binding.
- live-base preflight/post-verification and package/lockfile contracts.
- stack/base, publisher and Security Scan trigger semantics.
- `ARCHITECTURE.md` exact-head/workflow-source invariants.
Expand All @@ -76,3 +87,10 @@ Any movement between decision and write causes abort/re-plan rather than blind r
## Rationale sources

`docs/doctoring/architecture-trust-boundaries.md` records SLSA Source Track and GitHub OIDC primary-source rationale with APA 7 references.

GitHub. (n.d.). *Events that trigger workflows: merge_group*. Retrieved
October 9, 2026, from
https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#merge_group

GitHub. (n.d.). *Managing a merge queue*. Retrieved October 9, 2026, from
https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/managing-a-merge-queue
Loading
Loading