Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
7206aa8
test(oidc): require immutable reusable-workflow source SHA
seonghobae Aug 18, 2026
7fce673
fix(oidc): bind reusable workflow source revision
seonghobae Aug 18, 2026
b12b961
test(oidc): exercise production source-SHA gate
seonghobae Aug 18, 2026
8810175
config(oidc): pin allowed reusable workflow source SHA
seonghobae Aug 18, 2026
0ccd62e
refactor(oidc): keep source-trust response minimal
seonghobae Aug 18, 2026
3abaefa
fix(readiness): require immutable workflow source SHA
seonghobae Aug 18, 2026
0204d2f
test(readiness): cover reusable workflow source SHA
seonghobae Aug 18, 2026
71bee5f
test(readiness): supply workflow source SHA in cache fixtures
seonghobae Aug 18, 2026
c7dd2fc
test(oidc): authenticate before workflow source authorization
seonghobae Aug 18, 2026
55c064c
test(oidc): bind fallback workflow identity to source SHA
seonghobae Aug 18, 2026
7963bcc
fix(oidc): bind fallback workflow source identity
seonghobae Aug 18, 2026
6418174
test(oidc): cover immutable workflow prefilter boundaries
seonghobae Aug 18, 2026
c80b705
test(oidc): align delegated rejection contract
seonghobae Aug 18, 2026
8cea43c
test(oidc): cover source-prefilter fallback branches
seonghobae Aug 18, 2026
db33413
chore(oidc): roll trusted workflow SHA to current central main
seonghobae Aug 18, 2026
8b9e61f
test(oidc): preserve delegated workflow-config failure contract
seonghobae Aug 18, 2026
4808980
test(oidc): reject oversized signed SHA mismatch
seonghobae Aug 18, 2026
3387198
test(oidc): drop invalid oversized SHA expectation
seonghobae Aug 18, 2026
53a8daf
test(oidc): require signed workflow SHA enforcement
seonghobae Aug 18, 2026
3c1361a
fix(oidc): bind verifier to workflow source SHA
seonghobae Aug 18, 2026
afed42e
chore(oidc): roll trusted workflow source SHA
seonghobae Aug 18, 2026
587d3e5
test(oidc): exercise verifier behind a valid limiter
seonghobae Aug 18, 2026
ed8b972
test(readiness): include immutable workflow source binding
seonghobae Aug 18, 2026
2c11c34
test(readiness): require workflow source configuration
seonghobae Aug 18, 2026
67974e3
test(readiness): keep ref fixtures source-bound
seonghobae Aug 18, 2026
5d7f806
docs(architecture): describe candidate immutable OIDC source binding
seonghobae Aug 18, 2026
f7058a0
test(architecture): track active immutable workflow binding
seonghobae Aug 18, 2026
c82f8b5
style(core): restore canonical newline
seonghobae Aug 18, 2026
394c33b
chore(oidc): roll immutable workflow source binding
seonghobae Aug 18, 2026
74f2073
test(oidc): prove signed exact source pairs
seonghobae Aug 18, 2026
ca1447b
test(oidc): require source SHA in authoritative verifier
seonghobae Aug 18, 2026
33f59e3
fix(oidc): fail closed without source SHA
seonghobae Aug 18, 2026
067d2c8
test(oidc): align residual fixtures with source SHA trust
seonghobae Aug 18, 2026
db1839a
test(oidc): bind cache fixtures to trusted source SHA
seonghobae Aug 18, 2026
3fcee0d
test(oidc): bind success fixture to workflow source SHA
seonghobae Aug 18, 2026
5c5cb89
test(oidc): bind GitHub App runtime fixtures to source SHA
seonghobae Aug 18, 2026
36f5e73
chore(oidc): roll trusted workflow source SHA
seonghobae Aug 18, 2026
bc10594
test(oidc): bind worker fixtures to source SHA trust
seonghobae Aug 18, 2026
1f8b312
test(oidc): bind target authorization fixtures to source SHA
seonghobae Aug 18, 2026
bcfb8ee
test(oidc): bind operational fixtures to source SHA
seonghobae Aug 18, 2026
2a4e4e4
test(oidc): bind replay core fixtures to source SHA
seonghobae Aug 18, 2026
a6cf714
test(oidc): bind request helper fixtures to source SHA
seonghobae Aug 18, 2026
101bf5e
test(oidc): align denial prefilter expectation
seonghobae Aug 18, 2026
6638b88
docs(architecture): remove stale documentation owner
seonghobae Aug 18, 2026
41d615e
docs(traceability): record active workflow SHA owner
seonghobae Aug 18, 2026
f918399
test(docs): bind active workflow trust maturity
seonghobae Aug 18, 2026
9e1ee7a
fix(oidc): roll trusted workflow source revision
seonghobae Aug 18, 2026
44fca56
test(coverage): exercise authless runtime workflow prefilter
seonghobae Aug 18, 2026
a777e1f
test(oidc): cover whitespace-only bearer prefilter
seonghobae Aug 18, 2026
3236b5f
test(oidc): cover invalid authoritative workflow SHA config
seonghobae Aug 18, 2026
9a96b15
chore(oidc): roll trusted workflow source SHA
seonghobae Aug 19, 2026
eef2690
chore(oidc): advance trusted workflow source pin
seonghobae Aug 19, 2026
aa8b89d
test(oidc): provide distributed limiter for prefilter delegation
seonghobae Aug 19, 2026
e30d40e
fix(oidc): roll trusted workflow source to current central main
seonghobae Aug 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Noema Architecture & Trust Boundaries

**Status: Proposed canonical documentation — In review on PR #71.** Until #71 integrates, protected source and live GitHub governance remain implementation authority. This document describes protected behavior unless explicitly marked **Planned** or **External evidence**.
**Status: Canonical documentation on protected `main`; this branch records Active PR #426 candidate behavior separately.** Protected source and live GitHub governance remain implementation authority. This document describes protected behavior unless explicitly marked **Active PR**, **Planned**, or **External evidence**.

Noema is a bounded credential-exchange and automation service. Its core rule is: **verify GitHub Actions OIDC identity, mint a repository-scoped GitHub App installation token, and keep model judgement, review evidence, merge authority, release authority, and deployment authority separate.**

Expand All @@ -26,11 +26,11 @@ Routes have different meanings: `/health` is liveness, `/ready` is offline confi

## 2. Current workflow trust contract

Protected runtime exposes `ALLOWED_WORKFLOW_REF_PREFIX`. Despite the legacy name, `src/worker.ts` parses it as one **exact full workflow ref** and compares decoded `job_workflow_ref` or `workflow_ref` for exact equality. Wildcard, comma, whitespace, and prefix-sharing configuration forms are rejected.
Protected `main` exposes `ALLOWED_WORKFLOW_REF_PREFIX`. Despite the legacy name, `src/worker.ts` parses it as one **exact full workflow ref** and compares decoded `job_workflow_ref` or `workflow_ref` for exact equality. Wildcard, comma, whitespace, and prefix-sharing configuration forms are rejected.

The protected runtime does not expose `ALLOWED_WORKFLOW_SHA`, `workflow_sha`, or `job_workflow_sha` in this wrapper contract. Therefore canonical documentation must not claim paired SHA validation is deployed. Cryptographic OIDC verification in `src/index.ts` is a separate authority from the outer exact-ref precheck.
**Active PR #426:** the candidate runtime adds `ALLOWED_WORKFLOW_SHA` as a required readiness binding and pairs it with GitHub OIDC `job_workflow_sha` or fallback `workflow_sha`. `src/runtime-entrypoint.ts` provides an early denial-only check for the configured exact workflow identity, while `src/index.ts` independently enforces the same exact ref/repository plus immutable source SHA after cryptographic verification. A missing, malformed, or mismatched configured source SHA fails closed. The candidate `wrangler.toml` pins `ALLOWED_WORKFLOW_SHA` to a specific central `.github` commit; that candidate configuration is not deployed truth until the PR integrates and deployment evidence proves the binding was rolled forward.

**Planned:** a stronger immutable workflow-source binding may be introduced only by a separately reviewed source change with realistic RED/GREEN regressions and deployment/configuration evidence. That stronger immutable workflow-source binding is **not implemented on protected main** merely because historical branch prose described it.
The source-SHA prefilter is not an authorization substitute for cryptographic verification. Tokens not rejected at the wrapper continue through the existing distributed rate-limit, exact-ref trust, signature, issuer, audience, repository, time-window, replay, and GitHub App boundaries.

## 3. Runtime data flow

Expand All @@ -40,10 +40,11 @@ flowchart LR
B --> C{route}
C -->|/health| H[Liveness]
C -->|/ready| R[Readiness]
C -->|/exchange| E[src/entrypoint.ts]
C -->|/exchange| S{Active PR #426\nexact ref + source SHA prefilter}
S --> E[src/entrypoint.ts]
E --> L[NoemaRateLimiter]
L --> W[src/worker.ts\nexact workflow ref]
W --> O[src/index.ts\nOIDC verification]
W --> O[src/index.ts\ncryptographic OIDC + exact source binding]
O --> G[GitHub App token exchange]
G --> P[NoemaOidcReplayGuard]
P --> T[Repository-scoped token]
Expand Down Expand Up @@ -89,7 +90,7 @@ Repository automation must:
6. reject stale/predecessor evidence as current success;
7. avoid self-modifying repair workflows and never weaken gates to manufacture green evidence.

These control-plane invariants are separate from the OIDC runtime's current exact-ref trust contract.
These control-plane invariants are separate from the runtime OIDC trust contract. Active PR #426 strengthens that runtime contract with immutable workflow-source identity but does not alter the evidence-authority rules above.

## 7. Credential and network boundaries

Expand Down Expand Up @@ -119,7 +120,7 @@ Durable Object alarms are at-least-once. Handlers reread current deadline/expiry
| Change | Minimum proof |
| --- | --- |
| `/exchange` | typecheck, realistic public/API regressions, exact owned-production coverage, security scan |
| OIDC/GitHub App | issuer/audience/repository/workflow-ref, malformed token/JWKS, replay, redirect/egress, secret non-disclosure regressions |
| OIDC/GitHub App | issuer/audience/repository/workflow-ref, immutable workflow-source SHA when configured, malformed token/JWKS, replay, redirect/egress, secret non-disclosure regressions |
| Durable Objects | cross-instance semantics, delayed/retried alarm, current-state reschedule, malformed backend/storage-failure tests |
| GitHub Actions/control plane | least privilege, exact-head/live-base binding, full pagination, stale-head refusal, evidence-class separation |
| LLM integration | gateway contract, provider-key isolation, deterministic gates independent of model judgement |
Expand Down
7 changes: 4 additions & 3 deletions docs/TRACEABILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

This document maps requirements and architecture decisions to executable Noema surfaces and to the evidence that can legitimately prove them. File presence, PR prose, model output, queued checks, or predecessor results are never promoted into implementation, approval, merge, release, deployment, or acquisition authority.

Current protected-main reference for this refresh: `1ac1ccb7829a87f3e13c531db0765a7ed1e00002`.
Current protected-main reference for this refresh: `bcef225f1cf1a640a78a7c5b55b662cc5deb8ef4`.

Noema's execution rule is:

Expand Down Expand Up @@ -42,7 +42,7 @@ Each arrow is a separate authority. Success at an earlier stage cannot fabricate
| Requirement family | Canonical decision / boundary | Protected or active implementation surface | Executable proof | Residual evidence | Maturity |
| --- | --- | --- | --- | --- | --- |
| Credential exchange and readiness | Architecture, runtime threat model | `src/index.ts`, runtime entrypoints, OIDC/replay/rate-limit modules | runtime/API/security tests and exact configured coverage | deployed protected-main smoke where applicable | Implemented on protected main; operational evidence remains separate |
| Workflow/repository authority | Runtime threat model and protected Worker contract | configured exact workflow-ref and repository-owner validation plus cryptographic OIDC verification | issuer/audience/repository/ref and hostile-token tests | current central workflow/deployment binding evidence | Implemented family; do not invent a separate SHA binding that protected runtime does not expose |
| Workflow/repository authority | Runtime threat model and Worker trust contract | protected exact workflow-ref and repository-owner validation plus cryptographic OIDC verification; Active PR #426 adds immutable `ALLOWED_WORKFLOW_SHA` binding to `job_workflow_sha` / `workflow_sha` | issuer/audience/repository/ref hostile-token tests plus #426 source-SHA mismatch/missing/configuration regressions | exact-head #426 CI/security evidence, current central workflow identity, protected deployment binding evidence | Exact-ref family implemented on protected main; immutable source-SHA binding Implemented on active PR / In review |
| Fail-closed outbound GitHub boundary | Architecture + security docs | outbound fetch/request/response validation | origin/redirect/timeout/body/schema tests | production telemetry/incident evidence | Implemented family |
| Delegated GitHub credential capability | AGENTS secret policy + closed issue #111 | `scripts/lib/delegated-github-token.mjs`, maintainer/reviewer workflow ingress | token-capability and workflow-ingress tests covering `NOEMA_MAINTAINER_TOKEN_PATH`, owner-only `0600`, symlink/race/size/content rejection, minimal child env | live App installation/key-custody/rotation/permission evidence under #29/#227 | Capability-file policy alignment is protected; external identity evidence remains separate |
| Distributed rate/replay state | Architecture data boundary | Durable Object rate/replay state | concurrency/alarm/replay tests | deployed binding/storage evidence | Implemented family |
Expand Down Expand Up @@ -72,6 +72,7 @@ Historical PR numbers are deliberately omitted unless they are still open and ma

| Workstream | Current owner | Evidence boundary |
| --- | --- | --- |
| Immutable OIDC workflow-source binding | PR #426 | Active candidate only; exact-head application/reviewer/Security evidence and later protected deployment configuration must prove the source-SHA binding before it becomes protected/deployed truth. |
| Patch-validator image verification | issue #66 / PR #407 | Current image owner; standard and dedicated image evidence must pass on one unchanged exact head before integration. |
| Historical validator-image stack | PR #67 | Stale predecessor retained only until #407 integration and unique-delta preservation/supersession are proven. |

Expand Down Expand Up @@ -185,4 +186,4 @@ After every material product, governance, persistence, stack, release, or operat
3. update the single canonical graph on its current owner branch;
4. remove stale owner tables and obsolete SHAs rather than accumulating them;
5. keep transient check conclusions out of timeless claims unless explicitly observation-scoped;
6. convert any newly discovered executable defect to its real source/test/API/operator owner before considering the documentation refresh complete.
6. convert any newly discovered executable defect to its real source/test/API/operator owner before considering the documentation refresh complete.
22 changes: 22 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export interface Env extends OidcReplayProtectionEnv {
ALLOWED_REPOSITORY_OWNER: string;
ALLOWED_WORKFLOW_REPOSITORY: string;
ALLOWED_WORKFLOW_REF_PREFIX: string;
ALLOWED_WORKFLOW_SHA?: string;
GITHUB_API_BASE: string;
GITHUB_APP_ID: string;
GITHUB_APP_PRIVATE_KEY_PEM: string;
Expand All @@ -32,7 +33,9 @@ type JwtPayload = {
repository?: string;
repository_owner?: string;
workflow_ref?: string;
workflow_sha?: string;
job_workflow_ref?: string;
job_workflow_sha?: string;
sub?: string;
ref?: string;
jti?: string;
Expand Down Expand Up @@ -137,6 +140,7 @@ const errorHints: Record<ErrorCode, string> = {

const trustedHeaderValuePattern = /^[A-Za-z0-9._:-]+$/;
const clientIdentifierPattern = /^[A-Za-z0-9.:%_,-]+$/;
const exactWorkflowSourceShaPattern = /^[0-9a-f]{40}$/;
const maxTrustedHeaderLength = 128;

function jsonResponse(body: StandardErrorResponse | StandardSuccessResponse<unknown>, status = 200): Response {
Expand Down Expand Up @@ -429,6 +433,24 @@ async function verifyGithubOidcJwt(token: string, env: Env): Promise<JwtPayload>
if (!workflowRef.startsWith(`${env.ALLOWED_WORKFLOW_REPOSITORY}/.github/workflows/`)) {
throw new ApiError("ERR_WORKFLOW_NOT_ALLOWED", 403, "OIDC workflow repository is not allowed");
}
const configuredWorkflowSha = env.ALLOWED_WORKFLOW_SHA?.trim();
if (!configuredWorkflowSha || !exactWorkflowSourceShaPattern.test(configuredWorkflowSha)) {
throw new ApiError(
"ERR_WORKFLOW_NOT_ALLOWED",
503,
"Workflow source trust configuration unavailable",
{ match_policy: "exact-ref-and-source-sha" },
);
}
const workflowSha = payload.job_workflow_ref ? payload.job_workflow_sha : payload.workflow_sha;
if (workflowSha !== configuredWorkflowSha) {
throw new ApiError(
"ERR_WORKFLOW_NOT_ALLOWED",
403,
"OIDC workflow source revision is not allowed",
{ match_policy: "exact-ref-and-source-sha" },
);
}
if (typeof payload.nbf === "number" && payload.nbf > now + 30) {
throw new ApiError("ERR_AUTH_INVALID", 401, "OIDC token is not valid yet");
}
Expand Down
145 changes: 141 additions & 4 deletions src/runtime-entrypoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,128 @@ export { NoemaOidcReplayGuard, NoemaRateLimiter };
/**
* Runtime bindings required by Noema's production worker entrypoint.
* This interface inherits the credential-exchange, replay-guard, and rate-limit bindings
* consumed by the delegated application entrypoint and the readiness evaluation path.
* consumed by the delegated application entrypoint and adds the immutable source revision
* expected for the configured central reusable workflow.
*/
export interface Env extends BaseEnv {}
export interface Env extends BaseEnv {
ALLOWED_WORKFLOW_SHA?: string;
}

const exactCommitShaPattern = /^[0-9a-f]{40}$/;
const MAX_OIDC_PAYLOAD_SEGMENT_LENGTH = 8_192;

type ReusableWorkflowClaims = {
workflow_ref?: unknown;
workflow_sha?: unknown;
job_workflow_ref?: unknown;
job_workflow_sha?: unknown;
};

type WorkflowSourceDecision =
| { allowed: true }
| {
allowed: false;
status: 403 | 503;
message: string;
hint: string;
outcome: "blocked" | "misconfigured";
};

function decodedReusableWorkflowClaims(request: Request): ReusableWorkflowClaims | undefined {
const authorization = request.headers.get("authorization") ?? "";
const match = authorization.match(/^Bearer\s+(\S+)$/i);
if (!match) return undefined;

const parts = match[1].split(".");
if (parts.length !== 3 || parts[1].length > MAX_OIDC_PAYLOAD_SEGMENT_LENGTH) {
return undefined;
}

try {
const normalized = parts[1].replace(/-/g, "+").replace(/_/g, "/");
const padded = normalized + "===".slice((normalized.length + 3) % 4);
const binary = atob(padded);
const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
const decoded: unknown = JSON.parse(new TextDecoder().decode(bytes));
if (!decoded || typeof decoded !== "object" || Array.isArray(decoded)) {
return undefined;
}
return decoded as ReusableWorkflowClaims;
} catch {
return undefined;
}
}

function workflowSourceDecision(request: Request, env: Env): WorkflowSourceDecision {
const claims = decodedReusableWorkflowClaims(request);
if (!claims) return { allowed: true };

const usingReusableWorkflowIdentity = typeof claims.job_workflow_ref === "string";
const workflowRef = usingReusableWorkflowIdentity
? claims.job_workflow_ref
: typeof claims.workflow_ref === "string"
? claims.workflow_ref
: undefined;
if (!workflowRef) return { allowed: true };

const configuredRef = env.ALLOWED_WORKFLOW_REF_PREFIX?.trim();
if (!configuredRef || workflowRef !== configuredRef) {
// The delegated hardened worker owns the exact workflow-ref error contract.
return { allowed: true };
}

const configuredSha = env.ALLOWED_WORKFLOW_SHA?.trim();
if (!configuredSha || !exactCommitShaPattern.test(configuredSha)) {
return {
allowed: false,
status: 503,
message: "Workflow source trust configuration unavailable",
hint: "Configure the exact 40-character lowercase commit SHA for the allowed workflow source.",
outcome: "misconfigured",
};
}

const workflowSha = usingReusableWorkflowIdentity
? claims.job_workflow_sha
: claims.workflow_sha;
if (workflowSha !== configuredSha) {
return {
allowed: false,
status: 403,
message: "OIDC workflow source revision is not allowed",
hint: "Run the request from the configured workflow source revision; mutable-ref identity alone is insufficient.",
outcome: "blocked",
};
}

return { allowed: true };
}

function workflowSourceResponse(
decision: Exclude<WorkflowSourceDecision, { allowed: true }>,
): Response {
const traceId = crypto.randomUUID();
return new Response(JSON.stringify({
ok: false,
error_code: "ERR_WORKFLOW_NOT_ALLOWED",
message: decision.message,
details: {
hint: decision.hint,
match_policy: "exact-ref-and-source-sha",
},
trace_id: traceId,
}), {
status: decision.status,
headers: {
"content-type": "application/json; charset=utf-8",
"cache-control": "no-store",
pragma: "no-cache",
"x-content-type-options": "nosniff",
"x-trace-id": traceId,
"x-latency-ms": "0",
},
});
}

function readinessHeaders(
traceId: string,
Expand Down Expand Up @@ -92,15 +211,33 @@ async function runtimeReadinessResponse(request: Request, env: Env): Promise<Res

/**
* Cloudflare Worker entrypoint for Noema's public runtime surface.
* Routes `/ready` probes through configuration readiness checks and delegates every other
* request to the hardened credential-exchange entrypoint without altering its response.
* Routes `/ready` probes through configuration readiness checks, rejects a configured workflow
* source revision that does not match the immutable SHA associated with its OIDC workflow
* identity, and delegates every remaining request to the hardened credential-exchange
* entrypoint. The delegated worker still performs the authoritative cryptographic JWT
* verification before any credential mint.
*/
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === "/ready") {
return runtimeReadinessResponse(request, env);
}
if (url.pathname === "/exchange") {
const sourceDecision = workflowSourceDecision(request, env);
if (!sourceDecision.allowed) {
console.log(JSON.stringify({
event: "workflow_source_trust",
route: url.pathname,
method: request.method,
status_code: sourceDecision.status,
error_code: "ERR_WORKFLOW_NOT_ALLOWED",
outcome: sourceDecision.outcome,
match_policy: "exact-ref-and-source-sha",
}));
return workflowSourceResponse(sourceDecision);
}
}
return entrypoint.fetch(request, env);
},
};
Loading
Loading