Overview
Signed, offline-verifiable authorization for API-contract changes. Only a granted change can proceed.
An agent or a pipeline sends a change set (OpenAPI, GraphQL, protobuf, AsyncAPI or an MCP manifest, before and after) to POST /api/v1/preflight or to the MCP tool preflight_change_set. In authorize mode it gets an execution_action (CONTINUE, CONTINUE_WITH_MONITORING, REQUEST_APPROVAL, STOP) and a signed receipt that verify_receipt checks offline. In analyze mode, without a key, it gets information and no permission (may_execute: false).
The older report endpoints documented below (/v1/diff and the protocol diffs) are still served for existing integrations. Their numeric risk_score is information, not a decision.
Every route the app serves is listed in /openapi.json: generated from the route inventory, with each route's auth class and, where the route validates one, its request schema. Routes this page does not describe are tagged undocumented. It lists; this page explains.
Authentication
Include your API key in the Authorization header as a Bearer token:
Authorization: Bearer cr_live_your_key_here
Don't have a key? Get one free → No credit card required. Free tier includes 1,000 requests per month.
Rate Limits
Enforced buckets (source: src/middleware/rateLimit.js):
| Bucket | Limit | Applies when |
|---|---|---|
| API key / minute | 100 | Authenticated request (X-API-Key or Bearer) |
| IP / minute | 30 | Anonymous non-agent public endpoints |
| IP / minute (agent public) | 60 | Anonymous calls to demo / public preflight / public actionguard-check / migraguard-check |
| Anonymous daily | 50 / day (UTC) | Anonymous non-agent public endpoints only (not agent-public paths) |
On every rate-limited response, and on any 429, these headers describe the bucket that counted the request (or rejected it) — source: src/middleware/rateLimit.js. Not every route is rate-limited: POST /api/v1/verify-receipt runs no limiter and carries no X-RateLimit-* header — verification is free, keyless and unmetered. POST /mcp returns the limit headers of the route a tool call was proxied to, so an MCP verify_receipt carries none either.
| Header | Description |
|---|---|
X-RateLimit-Limit | Limit of the active bucket |
X-RateLimit-Remaining | Remaining in that bucket |
X-RateLimit-Reset | Unix epoch seconds when that bucket resets |
Retry-After | Seconds to wait (on 429 only) |
Self-service API keys (keys issued via signup / dashboard — not env-configured unlimited keys, not anonymous) additionally receive a monthly quota header from src/middleware/auth.js after usage tracking, and — on an authorize preflight that reaches a conclusive verdict on a supported artifact — the authorization-case count (src/routes/preflight-change-set.js):
| Header | Description | Who receives it |
|---|---|---|
X-RateLimit-Remaining-Monthly | Requests remaining in the current UTC calendar month (free-tier cap 1,000) | Self-service keys only |
X-RateLimit-Cases-Limit | Authorization cases included this UTC calendar month (Free 1,000 · Team 10,000) | Self-service keys, authorize with a conclusive verdict |
X-RateLimit-Cases-Remaining | Authorization cases left this month | Self-service keys, authorize with a conclusive verdict |
The authorization-case count is reported, not enforced: during the beta no request is refused because X-RateLimit-Cases-Remaining reached 0.
Anonymous IP buckets and env-key (unlimited) requests do not get monthly headers. There is no X-RateLimit-Remaining-Minute header — the minute bucket uses the shared X-RateLimit-Remaining name above.
Endpoint: POST /v1/diff
Analyze the difference between two OpenAPI specifications and return a full governance report.
Request
curl -X POST \
https://app.coderifts.com/api/v1/diff \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"old_spec": "openapi: 3.0.0\ninfo:\n title: My API\n version: 1.0.0\npaths:\n /users:\n get:\n summary: List users\n responses:\n \"200\":\n description: OK",
"new_spec": "openapi: 3.0.0\ninfo:\n title: My API\n version: 2.0.0\npaths: {}"
}'
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
old_spec |
string |
required | The base (old) OpenAPI specification as a YAML or JSON string |
new_spec |
string |
required | The head (new) OpenAPI specification as a YAML or JSON string |
config |
object |
optional | Configuration options (see below) |
Config Options
| Field | Type | Default | Description |
|---|---|---|---|
format |
string |
"json" |
Response format: "json" or "markdown" |
risk_threshold |
number |
50 |
Threshold on risk_score (0-100) for should_block |
block_on |
array |
[] |
Change types that should trigger blocking (e.g., ["removed-endpoint", "auth-removed"]) |
Response
{
"decision": "BLOCK",
"safe_for_agent": false,
"execution_action": "STOP",
"risk_score": 60,
"breaking_changes": 4,
"requires_migration": true,
"evidence_quality": "MEDIUM",
"patterns": ["AUTH_SCHEME_REMOVAL", "TYPE_NARROWING", "ENUM_NARROWING", "ENDPOINT_REMOVAL"],
"coderifts_version": "1.0",
"coderifts_governance": { "mcp_config": { "url": "https://app.coderifts.com/mcp", "transport": "streamable-http" }, "registry": "io.github.coderifts/api-governance", "manifest": "https://coderifts.com/mcp.json" },
"detected_patterns": [ /* ... */ ],
"breaking_changes_details": [ /* ... */ ],
"compatibility_suggestions": [ /* ... */ ],
"security_findings": [ /* ... */ ],
"token_cost_impact": { /* ... */ }
}
Response Fields (Decision Spec core)
| Field | Type | Description |
|---|---|---|
decision | string | Overall verdict: ALLOW, WARN, REQUIRE_APPROVAL, or BLOCK |
safe_for_agent | boolean | false if any agent-breaking change is present; agents MUST NOT call the API when false |
execution_action | string | Agent action derived from decision: CONTINUE (ALLOW), CONTINUE_WITH_MONITORING (WARN), REQUEST_APPROVAL (REQUIRE_APPROVAL), STOP (BLOCK) |
risk_score | number | Overall risk from 0 (safe) to 100 (critical) |
breaking_changes | number | Count of breaking changes detected |
patterns | string[] | Named break patterns (e.g. AUTH_SCHEME_REMOVAL, TYPE_NARROWING) |
requires_migration | boolean | Whether consumers must migrate before adopting |
evidence_quality | string | Confidence band: LOW, MEDIUM, or HIGH |
coderifts_version | string | Decision Spec version (currently 1.0) |
timestamp | string | ISO 8601 timestamp of the analysis |
Extended Fields
Extended fields are additive and enrich the verdict. The frozen v1.0 core above never changes to accommodate them; an agent may read extended fields opportunistically and must not treat their absence as an error.
| Field | Type | Description |
|---|---|---|
detected_patterns | array | Named break patterns, each with a plain-English consequence |
breaking_changes_details | array | Per-change blast radius: type, path/field, severity, description |
coderifts_governance | object | Native integration info: MCP url, registry id, manifest url |
confidence_score | number | Debug-only — returned only with ?debug=true: numeric verdict confidence (0-100); pairs with evidence_quality |
compatibility_suggestions | array | Concrete non-breaking remediation steps (change, field, suggestion, effort) |
security_findings | array | Auth/scope/security findings (e.g. auth scheme removed) |
token_cost_impact | object | Per-call token and dollar delta of the change |
pii_findings | array | PII exposure findings; populated only when PII is detected |
spec_extension | object | CodeRifts x- spec extension data when present, else { "found": false } |
MCP Tool Poisoning Gate
When an MCP manifest is supplied to POST /v1/mcp-diff (via old_mcp_manifest / new_mcp_manifest), the tool schemas are scanned for poisoning. Detected poisoning escalates the verdict; it never lowers it, and it does not change risk_score. When present, a poison_gate object is added to the response listing the finding types and tier.
| Tier | Fires on | Effect |
|---|---|---|
block | Unambiguous, co-signal-gated poison: suspicious_instruction, exfiltration_url_cosignal, file_read_exfiltration, text_injection | decision is forced to BLOCK and safe_for_agent to false |
ra | Heuristic anomalies: structural_anomaly, hidden_encoding | decision is raised to REQUIRE_APPROVAL only if currently lower; safe_for_agent is left unchanged |
Precedence: any block-tier finding wins over ra-tier. A benign description edit with no sensitive co-signal produces no poison_gate and leaves the verdict unchanged.
Endpoint: POST /v1/graphql/diff
Analyze the difference between two GraphQL schemas and return a full governance report.
Request
curl -X POST \
https://app.coderifts.com/api/v1/graphql/diff \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"base_schema": "schema { query: Query } type Query { hello: String }",
"head_schema": "schema { query: Query } type Query { hello: String goodbye: String }"
}'
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
base_schema |
string |
required | The base (old) GraphQL schema as a string |
head_schema |
string |
required | The head (new) GraphQL schema as a string |
Response
{
"decision": "ALLOW",
"risk_score": 0,
"safe_for_agent": true,
"correlation_id": "a1b2c3d4-e5f6-7890-1234-567890abcdef",
"breaking_changes": 0,
"changes": [],
"non_breaking_changes": [
{
"type": "FIELD_ADDED",
"severity": "NON_BREAKING",
"name": "Query.goodbye",
"description": "Field 'goodbye' was added to type 'Query'.",
"impact": "Low"
}
],
"schema_type": "graphql",
"analyzed_at": "2026-06-12T10:00:00.000Z",
"coderifts_version": "1.0",
"timestamp": "2026-06-12T10:00:00.000Z"
}
Response Fields
| Field | Type | Description |
|---|---|---|
decision | string | Overall decision: ALLOW, WARN, REQUIRE_APPROVAL, or BLOCK |
risk_score | number | Numeric risk signal, 0 to 100. Information, not a decision — branch on execution_action |
safe_for_agent | boolean | true if no breaking changes, false otherwise |
correlation_id | string | Unique identifier for the request |
breaking_changes | number | Count of breaking changes detected |
changes | array | List of detected changes (breaking and non-breaking) |
non_breaking_changes | array | List of non-breaking changes |
schema_type | string | Type of schema analyzed (e.g., graphql) |
analyzed_at | string | ISO 8601 timestamp of analysis |
coderifts_version | string | Version of the CodeRifts API |
timestamp | string | ISO 8601 timestamp of the response |
Endpoint: POST /v1/grpc/diff
Analyze the difference between two gRPC/Protobuf schemas and return a full governance report.
Request
curl -X POST \
https://app.coderifts.com/api/v1/grpc/diff \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"base_schema": "syntax = \"proto3\";\npackage helloworld;\nmessage HelloRequest { string name = 1; }\nservice Greeter { rpc SayHello (HelloRequest) returns (HelloReply); }",
"head_schema": "syntax = \"proto3\";\npackage helloworld;\nmessage HelloRequest { string name = 1; }\nmessage HelloReply { string message = 1; }\nservice Greeter { rpc SayHello (HelloRequest) returns (HelloReply); rpc SayGoodbye (HelloRequest) returns (HelloReply); }"
}'
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
base_schema |
string |
required | The base (old) Protobuf schema as a string |
head_schema |
string |
required | The head (new) Protobuf schema as a string |
Response
{
"decision": "ALLOW",
"risk_score": 0,
"safe_for_agent": true,
"correlation_id": "a1b2c3d4-e5f6-7890-1234-567890abcdef",
"breaking_changes": 0,
"changes": [],
"non_breaking_changes": [
{
"type": "SERVICE_METHOD_ADDED",
"severity": "NON_BREAKING",
"name": "Greeter.SayGoodbye",
"description": "RPC method 'SayGoodbye' was added to service 'Greeter'.",
"impact": "Low"
},
{
"type": "MESSAGE_ADDED",
"severity": "NON_BREAKING",
"name": "HelloReply",
"description": "Message 'HelloReply' was added.",
"impact": "Low"
}
],
"schema_type": "grpc",
"analyzed_at": "2026-06-12T10:00:00.000Z",
"coderifts_version": "1.0",
"timestamp": "2026-06-12T10:00:00.000Z"
}
Response Fields
| Field | Type | Description |
|---|---|---|
decision | string | Overall decision: ALLOW, WARN, REQUIRE_APPROVAL, or BLOCK |
risk_score | number | Numeric risk signal, 0 to 100. Information, not a decision — branch on execution_action |
safe_for_agent | boolean | true if no breaking changes, false otherwise |
correlation_id | string | Unique identifier for the request |
breaking_changes | number | Count of breaking changes detected |
changes | array | List of detected changes (breaking and non-breaking) |
non_breaking_changes | array | List of non-breaking changes |
schema_type | string | Type of schema analyzed (e.g., grpc) |
analyzed_at | string | ISO 8601 timestamp of analysis |
coderifts_version | string | Version of the CodeRifts API |
timestamp | string | ISO 8601 timestamp of the response |
Endpoint: POST /v1/asyncapi/diff
Analyze the difference between two AsyncAPI specifications and return a full governance report.
Request
curl -X POST \
https://app.coderifts.com/api/v1/asyncapi/diff \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"base_schema": "asyncapi: 2.0.0\ninfo:\n title: My API\n version: 1.0.0\nchannels:\n user/signedup:\n publish:\n message:\n messageId: UserSignedUp\n payload:\n type: object\n properties:\n userId:\n type: string",
"head_schema": "asyncapi: 2.0.0\ninfo:\n title: My API\n version: 1.0.0\nchannels:\n user/signedup:\n publish:\n message:\n messageId: UserSignedUp\n payload:\n type: object\n properties:\n userId:\n type: string\n newOptionalField:\n type: string"
}'
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
base_schema |
string |
required | The base (old) AsyncAPI specification as a YAML or JSON string |
head_schema |
string |
required | The head (new) AsyncAPI specification as a YAML or JSON string |
Response
{
"decision": "ALLOW",
"risk_score": 0,
"safe_for_agent": true,
"correlation_id": "a1b2c3d4-e5f6-7890-1234-567890abcdef",
"breaking_changes": 0,
"changes": [],
"non_breaking_changes": [
{
"type": "PAYLOAD_FIELD_ADDED",
"severity": "NON_BREAKING",
"name": "user/signedup.publish.UserSignedUp.newOptionalField",
"description": "New payload field 'newOptionalField' was added to message 'UserSignedUp'.",
"impact": "Low"
}
],
"schema_type": "asyncapi",
"analyzed_at": "2026-06-12T10:00:00.000Z",
"coderifts_version": "1.0",
"timestamp": "2026-06-12T10:00:00.000Z"
}
Response Fields
| Field | Type | Description |
|---|---|---|
decision | string | Overall decision: ALLOW, WARN, REQUIRE_APPROVAL, or BLOCK |
risk_score | number | Numeric risk signal, 0 to 100. Information, not a decision — branch on execution_action |
safe_for_agent | boolean | true if no breaking changes, false otherwise |
correlation_id | string | Unique identifier for the request |
breaking_changes | number | Count of breaking changes detected |
changes | array | List of detected changes (breaking and non-breaking) |
non_breaking_changes | array | List of non-breaking changes |
schema_type | string | Type of schema analyzed (e.g., asyncapi) |
analyzed_at | string | ISO 8601 timestamp of analysis |
coderifts_version | string | Version of the CodeRifts API |
timestamp | string | ISO 8601 timestamp of the response |
Endpoint: GET /v1/public/preflight
The zero-auth entry point for AI agents. Before calling or trusting an API, an agent passes the spec URL and receives a CodeRifts decision. Results are cached; if a spec has not been analyzed yet the endpoint returns PENDING and analysis is triggered in the background. A POST variant accepts an agent workflow body.
Request
curl "https://app.coderifts.com/api/v1/public/preflight?spec_url=https://example.com/openapi.json&debug=true"
Query Parameters
| Param | Type | Required | Description |
|---|---|---|---|
spec_url | string | required | URL of the OpenAPI spec to evaluate |
debug | boolean | optional | When true, adds a debug block (cache lookup, score components, decision path) |
Response
{
"decision": "PENDING",
"safe_for_agent": false,
"correlation_id": "8c9ca61d-b0bd-4ae3-8117-3e3b8ad612db",
"debug": {
"cache_lookup": { "spec_hash": "sha256:9f523d36...", "cache_hit": false, "lookup_time_ms": 244 },
"analysis": { "patterns_checked": 33, "score_components": { "S_contract": 0, "P_break": 0, "S_blast": 0, "S_agent": 0 } },
"decision_path": ["analysis not yet complete \u2192 PENDING"]
}
}
Endpoint: POST /v1/public/actionguard-check
Zero-auth, stateless check for a single GitHub Actions workflow diff. An agent passes the workflow file's before/after content and receives WARN-tier findings for risky uses: action references (unpinned refs, pin drift, major-version jumps, new third-party actions). This endpoint is advisory only: it contributes no risk_score and its decision is always ALLOW (no findings) or WARN (one or more findings) — never REQUIRE_APPROVAL or BLOCK. Nothing is stored.
Request
curl -X POST \
https://app.coderifts.com/api/v1/public/actionguard-check \
-H "Content-Type: application/json" \
-d '{
"filename": ".github/workflows/ci.yml",
"base_content": null,
"head_content": "name: ci\non: [push]\njobs:\n build:\n runs-on: ubuntu-latest\n steps:\n - uses: some-org/deploy-action@main"
}'
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
filename | string | required | The workflow file path, e.g. .github/workflows/ci.yml |
head_content | string | required | The workflow file content after the change (max 512KB) |
base_content | string or null | optional | The workflow content before the change. null (or omitted) means the file is newly added |
trusted_owners | string[] | optional | Extra trusted action owners. Merged with the built-in list (actions, github) — never replaces it |
Response
{
"decision": "WARN",
"safe_for_agent": true,
"risk_score": 0,
"breaking_changes": 0,
"requires_migration": false,
"patterns": ["UNPINNED_ACTION_REF", "NEW_THIRD_PARTY_ACTION"],
"evidence_quality": "low",
"coderifts_version": "1.0",
"analysis_type": "actionguard_workflow_diff",
"actionguard": {
"version": "actionguard-v1",
"findings": [
{
"id": "UNPINNED_ACTION_REF",
"severity": "high",
"action": "some-org/deploy-action",
"file": ".github/workflows/ci.yml",
"line": 8,
"details": "some-org/deploy-action@main is not pinned to a full 40-char commit SHA"
}
],
"parse_errors": []
},
"timestamp": "2026-07-14T00:00:00.000Z"
}
Endpoint: POST /v1/agent-abort-demo
Simulates an agent workflow against an API spec change and returns whether the agent should abort. Supports common frameworks (langgraph, autogen, crewai, openai-functions, anthropic-tools, generic). Execution-linkage for agent runtimes.
Request
curl -X POST \
https://app.coderifts.com/api/v1/agent-abort-demo \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"framework": "langgraph",
"workflow": { "steps": [ { "tool": "get_user", "endpoint": "GET /users/{id}" } ] },
"old_spec": "...",
"head_spec": "..."
}'
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
workflow | object | required | Object with a steps array describing the agent tool calls |
framework | string | optional | Agent framework name |
old_spec / new_spec | string | optional | The API spec before / after the change |
Response
{ "decision": "ALLOW", "risk_score": 0, "safe_for_agent": true, "correlation_id": "..." }
Endpoint: POST /v1/instability-scan
AIDE V2 statistical instability detection. Accepts batched API traffic samples and returns MAD-based Z-score, coefficient of variation, trend slope, and named instability patterns (e.g. LATENCY_REGRESSION, PAYLOAD_FLAKINESS, ERROR_RATE_SPIKE, TOKEN_COST_DRIFT).
Request
curl -X POST \
https://app.coderifts.com/api/v1/instability-scan \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"endpoint": "GET /users",
"samples": { "latency_ms": [120, 135, 410, 980], "error_rate": [0.0, 0.0, 0.2, 0.5] }
}'
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
endpoint | string | required | The API endpoint being analyzed |
samples | object | required | Batched numeric traffic samples (latency, error rate, payload size, token usage) |
Endpoint: GET /v1/metrics
Operational telemetry for your account: request volume, cache efficiency, and pending/error counts over a rolling window.
Request
curl https://app.coderifts.com/api/v1/metrics \
-H "Authorization: Bearer YOUR_API_KEY"
Response
{
"period": "last_24h",
"preflight": { "total_requests": 292, "cache_hits": 0, "cache_hit_ratio": 0, "pending_count": 292, "error_count": 0 }
}
Endpoint: GET /v1/dashboard
Per-repository activity view for API owners: how many agents checked the spec and the distribution of BLOCK / WARN / ALLOW decisions.
Request
curl "https://app.coderifts.com/api/v1/dashboard?repo=owner/repo" \
-H "Authorization: Bearer YOUR_API_KEY"
Query Parameters
| Param | Type | Required | Description |
|---|---|---|---|
repo | string | required | Repository in owner/name form |
A companion GET /v1/cache/stats?repo=owner/name endpoint returns cache hit-ratio for the same repository.
Endpoint: GET /v1/crawler-stats
Tracks which AI crawlers and agents are calling the public preflight endpoint (by IP / User-Agent) over the last 7 days. The adoption signal for AI-agent traffic. No API key required.
Request
curl https://app.coderifts.com/api/v1/crawler-stats
Response
{
"period": "last_7d",
"total_preflight_calls": 2001,
"ai_crawler_calls": 0,
"ai_crawler_rate": 0,
"crawlers": []
}
Endpoint: GET /v1/attestation/public-key
Channel-chain attestation lets an external party cryptographically verify that a verdict response came from CodeRifts and follows an unbroken chain from the previous verdict. Every verdict response can carry an Ed25519-signed chain_receipt; this endpoint publishes the public key used to verify it. No API key required.
Request
curl https://app.coderifts.com/api/v1/attestation/public-key
Response
{
"kid": "k1",
"alg": "Ed25519",
"public_key_pem": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----\n"
}
When no signing key is configured on the server, this endpoint returns 503 with { "error": "attestation_not_configured" }, and verdict responses simply omit the chain fields.
chain_receipt on verdict responses
When attestation is configured, POST /v1/diff, POST /v1/action-verdict and POST /v1/verdict add a top-level chain_receipt string. It is a compact token of two base64url segments joined by a dot: base64url(body).base64url(signature). The receipt lives entirely in the response and never affects the deterministic verdict or its fingerprint. The latest format is v4: the bundle preflight (POST /v1/preflight) and MCP single-spec tools issue v4 receipts that add two signed fields — expires_at (the decision expiry, so a verifier can judge freshness from the token) and bh (decision_body_hash, binding the whole decision envelope). /v1/diff issues v3 (adds the Change-IR ir alongside reg); /v1/action-verdict and /v1/verdict issue v2 (add reg, no ir); older v1 receipts remain verifiable forever.
{
"decision": "ALLOW",
"chain_status": "absent",
"chain_receipt": "eyJ2IjozLCJraWQiOiIyMDI2LTA3LWsxIiwiZnAiOiJzaGEyNTY6NzA2OTkzNDE1NDk3ODQwNDIyNGIyMGZkMWM1ZDllM2EwZGRjYzY5Y2FjYTU4NjdmOGU3Mzc2NmUyODZjY2ZlYyIsInByZXYiOiJudWxsIiwiY2FsbGVyIjoid2ViaG9vayIsInRzIjoiMjAyNi0wNy0yNFQwMDowMDowMC4wMDBaIiwicmVnIjoiNGY1M2NkYTE4YzJiYWEwYzAzNTRiYjVmOWEzZWNiZTVlZDEyYWI0ZDhlMTFiYTg3M2MyZjExMTYxMjAyYjk0NSIsImlyIjoic2hhMjU2OmYzZGU3NzIxZGY2ZWU1MjJiYzkyMjAzY2Y2YjcxZWIzYWM3YjFkMGVlYTEwNmJhMDFkMmJmODE5MzhhM2EzMzQifQ.f3Zk...signature"
}
The decoded body is self-describing so anyone can verify it with the published key:
| Field | Meaning |
|---|---|
v | Receipt format version - 4 (latest, from bundle preflight + MCP; adds expires_at + bh), 3 (/v1/diff), 2 (action-verdict/verdict), or 1 (legacy); all remain verifiable |
kid | Key id of the signing key (matches the public-key endpoint) |
fp | The verdict_fingerprint this receipt attests |
prev | sha256 of the previous receipt, or the string null for the first link |
caller | Caller label (no secret material) |
ts | ISO-8601 timestamp of issuance |
reg | v2 and v3 - hash of the evidence-trust-registry state in force at issuance. Signed but informational: verifiers do NOT compare it to the live registry, so a registry change never invalidates a prior receipt. |
ir | v3 only - the Change-IR hash (sha256:<hex>) the verdict was computed over. Signed but informational, like reg: verifiers do not recompute it. |
The signed bytes are a pipe-joined string, signed with Ed25519. For v4 it is crchain.v1|kid|fp|prev|caller|ts|reg|ir|expires_at|bh; for v3 it drops the trailing |expires_at|bh (crchain.v1|kid|fp|prev|caller|ts|reg|ir); for v2 it also drops |ir; for v1 it drops |reg too (crchain.v1|kid|fp|prev|caller|ts). The prefix tag stays crchain.v1 across all versions. All remain verifiable. To verify: base64url-decode both segments, rebuild the string for the receipt's own version (checking the body v), and check the signature against the published public key.
prev_receipt and chain_status
To link a call to the previous one, pass the previous response's chain_receipt back as an opt-in top-level prev_receipt field in the request body. It is additive: omitting it changes nothing.
{
"action_type": "tool_call",
"provenance": { "channel": "ci_manifest", "issuer_trust": "trusted" },
"tool": { "name": "get_customer", "capabilities": ["read"] },
"memory": { "op": "read", "namespace": "working" },
"prev_receipt": "eyJ2IjoxLCJraWQiOiJrMSJ9.f3Zk...signature"
}
Every verdict response reports a chain_status:
| chain_status | Meaning |
|---|---|
intact | A valid prev_receipt was supplied and verified against the current key |
absent | No prev_receipt was supplied (or attestation is off) - annotation only |
broken | A prev_receipt was supplied but failed verification |
A broken chain combined with a destructive-capability action escalates the decision to REQUIRE_APPROVAL (fail-closed). A broken chain on a read-only action, and any absent chain, are annotation only and never change the decision.
Key rotation
The current model is a single active signing key: one private key (CHAIN_SIGNING_KEY) and its key id (CHAIN_SIGNING_KID, default k1). To rotate:
- Generate a new Ed25519 keypair.
- Set
CHAIN_SIGNING_KEYto the new private key andCHAIN_SIGNING_KIDto a new id. - Redeploy. New receipts are signed under the new kid;
GET /v1/attestation/public-keyserves the new public key.
After rotation, a prev_receipt that was signed under the OLD kid no longer matches the active key and verifies as unknown_kid, so its chain_status becomes broken (fail-closed). Per the rules above, a broken chain is annotation only unless the action is destructive, in which case it escalates to REQUIRE_APPROVAL - it never silently passes. Because the chain lives entirely in the request/response and never in server state, rotation needs no migration.
Multi-key verification - accepting one or more PREVIOUS public keys during a rotation window so in-flight chains stay intact across a rotation - is on the roadmap and is not supported today: exactly one kid is active at a time.
Independent verification
The receipt format above is public and frozen — the canonical spec is RECEIPT_FORMAT.md in the verifier repo. An open-source verifier - Node (zero-dependency) and Python - is available at github.com/coderifts/receipt-verifier, so anyone can verify a chain_receipt without trusting CodeRifts. With no --key it fetches the public key from this endpoint by default; pass --envelope <file> to bind a v4 receipt's bh to a decision envelope.
git clone https://github.com/coderifts/receipt-verifier
cd receipt-verifier
node verify.js "<chain_receipt>"
Endpoint: POST /v1/verify-receipt
Public (no API key). Checks a signed chain_receipt / receipt token you already hold. Returns signature/lifecycle fields (valid, status) and, when you supply intended context, an authorization verdict (currently_authorized, authz_reason).
A valid signature is not authorization; currently_authorized requires the full intended context (operation + environment + target_id / artifact_digest + fingerprint, typically with the decision envelope you are binding). Token-only verify answers authenticity — not “may I act”.
Full-context call → currently_authorized: true
Supply the operation, target, environment, fingerprint, and decision_result that match the receipt you hold:
curl -sS -X POST https://app.coderifts.com/api/v1/verify-receipt \
-H "Content-Type: application/json" \
-d '{
"token": "'"$CHAIN_RECEIPT"'",
"operation": "merge",
"environment": "production",
"target_id": "repo:acme/api@base:abc@head:def",
"fingerprint": "sha256:…",
"decision_result": { /* envelope from the authorize preflight that issued this receipt */ }
}'
{
"valid": true,
"status": "VERIFIED_CURRENT",
"currently_authorized": true,
"authz_reason": "ok",
"correlation_id": "…"
}
Partial call → currently_authorized: false (target mismatch)
Same authentic token, but a wrong or incomplete target — signature can still verify while authorization fails:
curl -sS -X POST https://app.coderifts.com/api/v1/verify-receipt \
-H "Content-Type: application/json" \
-d '{
"token": "'"$CHAIN_RECEIPT"'",
"operation": "merge",
"environment": "production",
"target_id": "repo:other/api@base:xxx@head:yyy",
"fingerprint": "sha256:…",
"decision_result": { /* same envelope */ }
}'
{
"valid": true,
"status": "VERIFIED_CURRENT",
"currently_authorized": false,
"authz_reason": "target_mismatch",
"correlation_id": "…"
}
Token-only ({"token":"…"}) keeps valid/status for signature checks and returns currently_authorized: null with an explicit note that authorization was not evaluated — do not treat that as a grant.
Signed Evidence at Origin
Signed evidence is a third-party Ed25519-signed authorization carried in the request. Unlike chain attestation (which verifies our own signature), evidence verifies a THIRD-PARTY signature against a curated trust registry of registered public keys. Each evidence SCOPE relaxes exactly its own one gate, landing that gate at REQUIRE_APPROVAL instead of BLOCK (never lower); it can never suppress any other gate:
user_authorization- tool/op binding; suppressesissuer_independence_untrusted_destructive(an out-of-band user authorization, K1).source_attestation- content_hash binding to the request data; suppressesdata_laundering_untrusted_data_destructive(a registry-trusted origin vouching for the data being acted upon).
Scopes are isolated: a key trusted for one scope can never satisfy the other (both the registry-granted scope and the payload scope are checked). The mass_pii_exfiltration gate is never relaxable by any scope.
Request field
Pass a top-level evidence ARRAY of compact evidence tokens. It is additive and opt-in: omitting it (or sending an empty array) changes nothing.
{
"action_type": "tool_call",
"provenance": { "channel": "user_message", "issuer_trust": "untrusted" },
"tool": { "name": "delete_records", "capabilities": ["destructive"] },
"memory": { "op": "read", "namespace": "working" },
"evidence": ["trusted-authz-1.eyJzY29wZSI6...payload.f3Zk...signature"]
}
Token format
Each token is three base64url segments joined by dots: kid.base64url(payloadJSON).base64url(sig). The signed bytes are the pipe-joined string crev.v1|kid|sha256hex(payloadB64) where payloadB64 is the exact transmitted middle segment (hashing the transmitted bytes avoids re-canonicalization ambiguity), signed with Ed25519.
The decoded payload is a JSON object. The scope selects which binding fields are required:
| Field | Meaning |
|---|---|
scope | user_authorization or source_attestation |
tool | user_authorization only: the tool name this authorization is for, or * for any tool |
op | user_authorization only: the capability it authorizes (e.g. destructive); must be among the request tool's capabilities |
content_hash | source_attestation only: sha256 hex of the request's memory.data (see below) |
origin | source_attestation only: optional origin label (informational in v1; no origin allow-list yet) |
{ "scope": "user_authorization", "tool": "delete_records", "op": "destructive" }
{ "scope": "source_attestation", "origin": "acme-etl", "content_hash": "sha256hex(memory.data)" }
source_attestation: content binding
A source attestation binds to the actual data being acted upon. Pass that data as an opt-in top-level memory.data string; the origin signs content_hash = sha256hex(memory.data). The hash is over the RAW transmitted string exactly as received (no re-canonicalization - the same transmitted-bytes principle as the token signing input). The gate is suppressed only when a registry-trusted source_attestation key signs a content_hash equal to sha256hex of the request's memory.data. Omitting memory.data (or sending a non-string / empty value) means no content binding, so source_attestation can never fire - behavior is byte-identical to before.
Effect and fail-closed semantics
The gate is suppressed only on the full happy path: the token verifies against a registry-trusted kid AND its payload.scope is user_authorization AND payload.tool matches the request tool (or is *) AND payload.op is among the request capabilities. Every other case - absent, malformed, unknown kid, bad signature, wrong scope, tool or op mismatch - behaves EXACTLY as if no evidence were supplied (fail-closed). A suppressed A1 gate does not disable the other gates: an input that also triggers a data-laundering gate still lands on BLOCK.
Replay semantics: evidence is request-bound, not single-use. The payload embeds the tool and op it authorizes and is checked against the request; there is no nonce and no expiry comparison (any time field in the payload is opaque signed bytes, never compared to a clock). The same evidence deterministically re-authorizes the same action.
Response annotation
Under extended.evidence_detail (not part of the hashed verdict):
"evidence_detail": {
"status": "valid",
"kid": "trusted-authz-1",
"gate_suppressed": "issuer_independence_untrusted_destructive"
}
status is valid (a token authorized this request), invalid (evidence present but nothing authorized it), or absent (no evidence). gate_suppressed is the suppressed gate name, or null.
The trust registry is curated: public keys are added only after review. Contact us to register a key.
Blast Radius Signals
Blast-radius signals are the INVERSE of signed evidence. Where each evidence scope RELAXES exactly one gate (BLOCK down to REQUIRE_APPROVAL), each blast-radius signal TIGHTENS: it lets a caller declare what a destructive or financial action targets, and the verdict escalates deterministically. These signals can only RAISE severity, never lower it, and their absence changes nothing (the same absent-is-no-signal principle as chain attestation and evidence).
They are caller-declared enums on the tool section. The deterministic core does NO config lookup - the vocabulary aligns with the domains[].sensitivity terms but is supplied per-request, not resolved from any config.
Request fields
Two opt-in strings on tool. Omitting them (or sending a non-string) changes nothing - the verdict and its fingerprint are byte-identical to before.
| Field | Values | Meaning |
|---|---|---|
tool.target_sensitivity | low / normal / high / critical | How sensitive the target namespace/resource is. |
tool.scope | single / bulk | Whether the action hits a single record or a bulk/wildcard set. This is a literal caller-declared flag; the core does not parse paths or interpret wildcards. |
{
"action_type": "tool_call",
"provenance": { "channel": "ci_manifest", "issuer_trust": "trusted" },
"tool": { "name": "purge", "capabilities": ["destructive"], "target_sensitivity": "critical", "scope": "bulk" },
"memory": { "op": "delete", "namespace": "working" }
}
Escalation matrix
The signals only ever escalate a HIGH-IMPACT action (a tool whose capabilities include destructive or financial - both carry the same risk weight). Any other action is never escalated by these fields. Escalations are pushed as decision hints and joined by max-severity, so the WORST outcome across all signals and gates wins. (The blast_gate names below keep the _destructive suffix for both classes.)
| Condition (all require destructive or financial) | Escalates to | blast_gate |
|---|---|---|
target_sensitivity is high or critical | REQUIRE_APPROVAL | blast_high_sensitivity_destructive |
scope is bulk | REQUIRE_APPROVAL | blast_bulk_destructive |
target_sensitivity is critical AND scope is bulk | BLOCK (the only BLOCK path) | blast_critical_bulk_destructive |
When more than one rule fires, the most severe wins the blast_gate name, but every hint is still joined into the decision.
Absent and unknown values
Absent (or non-string) fields are no signal - never an escalation and never a relaxation. An UNRECOGNIZED enum value (a typo like critical!! or HUGE) is also treated as no signal - a caller mistake must not escalate - but it is annotated with blast_note: "unrecognized value ignored". Because these signals are tightening-only, declaring low or normal sensitivity never relaxes a verdict that another gate already raised (for example a mass_pii_exfiltration BLOCK stays BLOCK).
Response annotation
Under extended.blast_radius (not part of the hashed verdict; the signal reaches the fingerprint only through the escalated decision, exactly like data_trust):
"blast_radius": {
"gate": "blast_critical_bulk_destructive",
"note": null
}
gate is the fired rule name (or null when nothing escalated); note carries the unknown-value annotation (or null).
Reversibility (deferred)
A third axis - reversibility of the action - is deliberately deferred to a future version. It needs caller-proven semantics before it can tighten a verdict, so v1 ships the two target axes only and does not read any memory.reversible field.
Endpoint: POST /v1/keys GET DELETE
Self-service API key lifecycle.
| Method | Path | Description |
|---|---|---|
POST | /v1/keys | Create a new API key |
GET | /v1/keys/info | Inspect the calling key (tier, limits, usage). Authenticate with Authorization: Bearer; a key in the query string is refused with 401. |
GET | /v1/keys | List your keys (authenticated) |
DELETE | /v1/keys/:prefix | Revoke a key. Prefix must match exactly one record; duplicate prefixes return 409 ambiguous_prefix with match_count. Pass the full key (DELETE /v1/keys/<full-key>, verified by hash) to disambiguate. Never first-match. |
Request
curl -H "Authorization: Bearer cr_live_YOUR_KEY" \
https://app.coderifts.com/api/v1/keys/info
Endpoint: POST /v1/bindings/challenge POST /v1/bindings/prove DELETE /v1/bindings
One-time challenge-response proof that the calling tenant controls a named GitHub repository. API keys stay tenant-scoped; bindings survive key rotation within the tenant. A proven binding upgrades caller-attested completeness from ATTESTED_UNVERIFIED to BOUND_ATTESTED. SERVER_DERIVED is a separate opt-in: derivation: "server" on generic preflight (below) — a proven binding alone never earns that label.
| Method | Path | Description |
|---|---|---|
POST | /v1/bindings/challenge | Issue a short-TTL, single-use challenge for { repository } |
POST | /v1/bindings/prove | Server fetches .coderifts-bind via the GitHub App and records the binding on match |
DELETE | /v1/bindings | Revoke the proven binding for { repository } (this tenant only) |
1. Challenge
curl -sS -X POST https://app.coderifts.com/api/v1/bindings/challenge \
-H "Authorization: Bearer $CODERIFTS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"repository":"acme/api"}'
{
"challenge": "crbind_<64-hex>",
"expires_at": "2026-08-23T12:15:00.000Z",
"repository": "acme/api",
"bind_file": ".coderifts-bind"
}
2. Commit the bind file
Commit the challenge string as the entire contents of .coderifts-bind at the repository root (same place as .coderifts.yml) and push. The CodeRifts GitHub App must be installed on that repository — the server reuses the existing installation token; it does not mint a new one.
3. Prove
curl -sS -X POST https://app.coderifts.com/api/v1/bindings/prove \
-H "Authorization: Bearer $CODERIFTS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"repository":"acme/api"}'
{
"proven": true,
"proven_at": "2026-08-23T12:04:12.000Z",
"repository": "acme/api",
"bind_file": ".coderifts-bind"
}
Failure modes
| Status | error | When |
|---|---|---|
422 | binding_unproven | File missing (reason: file_missing) or contents do not match (reason: challenge_mismatch) |
410 | challenge_expired | Challenge TTL elapsed (15 minutes, single-use) |
403 | repo_inaccessible | GitHub App is not installed on the repository (install it first) |
404 | challenge_not_found | No outstanding challenge for this tenant×repo |
Effect on generic preflight
When context.repository is present and a proven binding exists for the calling tenant, a caller-attested change-set (artifacts[] supplied) carries completeness_mode: "BOUND_ATTESTED" (only if the claim was otherwise self-consistent) and binding: { "proven": true, "proven_at": "..." }. Binding state is context, not verdict input — it does not enter the verdict_fingerprint preimage. Without a proven binding the generic path stays ATTESTED_UNVERIFIED.
Server-derived change-set (derivation: "server")
Opt-in on POST /api/v1/preflight. Default (flag absent) is byte-identical to today. When derivation: "server" is set, the server lists the contract-class files between context.base and context.head via the GitHub App installation (Compare API + Contents), using the same path classifier as the merge gate. Completeness becomes SERVER_DERIVED (this is the first producer of that enum). Caller-supplied artifacts[] with this flag is 400 — one source of truth per request.
Allowed only for an authenticated tenant with a proven binding for context.repository and both base and head present. Violations: 403 binding_not_proven, 400 derivation_requires_base_head, 403 repo_inaccessible (App not installed). Size caps name themselves on 413 BUNDLE_TOO_LARGE (max_artifacts=20, max_file_bytes=524288, max_compare_files=300). A GitHub error mid-fetch is 502 DERIVATION_FETCH_FAILED (retryable) — never a silent partial set.
The signed envelope may carry additive derivation: { "source": "github_compare", "base_sha", "head_sha" } (resolved SHAs). That object is covered by decision_body_hash and is absent on the default path. It does not enter the fingerprint preimage. Caller-supplied base/head on the envelope stay the named refs.
preflight_mode is required on this endpoint. Send analyze or authorize; a request without it is rejected with HTTP 400 — measured against the running service on 2026-09-15. That is specific to /v1/preflight: /v1/diff takes no mode and answers 200 without one, which is why the response examples on this page differ in shape.
curl -sS -X POST https://app.coderifts.com/api/v1/preflight \
-H "Authorization: Bearer $CODERIFTS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"preflight_mode": "authorize",
"derivation": "server",
"context": {
"operation": "merge",
"repository": "acme/api",
"base": "main",
"head": "feature"
}
}'
Receipt authority (ID963)
The signed envelope also carries an additive authority object: { audience, tenant_scope: "bound" | "unbound", binding_proven_at? }. tenant_scope is "bound" only when a proven binding existed for (tenant, context.repository) at decision time. This is covered by decision_body_hash and projected onto receipt_view.binds_to.authority. It is informational — it proves the receipt was minted under a tenant with proven repo control (when bound). It is not an ACL and verifyReceipt does not gain a new required input. audience still identifies the API key (v: + sha256(key)[:12]), not the tenant.
What the receipt binds as the target
The receipt's target is the change set's artifact_digest (control_envelope.receipt_view.binds_to.target_id). A context.target_id you send is not signed into the decision envelope and does not replace that digest; it names the target of an execution grant (cr.exec.v1) when one is issued.
Bound-context enrichment (ID903)
When the binding is proven and context.repository is present, the generic path may attach repository-contextual ID27 blast counts (consumers_observed / graph_source) from persisted usage observations for that trusted repo. Unproven calls stay byte-identical to today. Fields that need the actual PR tree (declared-consumer coverage from coderifts-deps.yml, docs/SDK coverage, history) stay on the webhook path.
Revoke
curl -sS -X DELETE https://app.coderifts.com/api/v1/bindings \
-H "Authorization: Bearer $CODERIFTS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"repository":"acme/api"}'
Response Headers & Debug Mode
Every decision response carries machine-readable headers so agents and proxies can read the verdict without parsing the body. Every response carries a correlation id for tracing.
| Header | Example | Description |
|---|---|---|
X-CodeRifts-Decision | WARN | The verdict: ALLOW / WARN / REQUIRE_APPROVAL / BLOCK |
X-CodeRifts-Risk-Score | 25 | risk_score, 0–100 (information, not a decision) |
X-CodeRifts-Safe-For-Agent | false | Whether the change is safe for an agent to proceed |
x-correlation-id | 8c9ca61d-... | Unique request id (also in the correlation_id body field) |
Debug Mode
Append ?debug=true to a request to add a debug block explaining the decision: cache lookup, score components (S_contract, P_break, S_blast, S_agent), and a human-readable decision path.
curl -D - "https://app.coderifts.com/api/v1/graphql/diff" \
-H "Content-Type: application/json" \
-d '{"base_schema":"type Query { a: String }","head_schema":"type Query { b: Int }"}'
# X-CodeRifts-Decision: WARN
# X-CodeRifts-Risk-Score: 25
# x-correlation-id: 8c9ca61d-...
Response
200 OK — Success
{
"risk_score": 42,
"risk_level": "moderate",
"risk_dimensions": {
"revenue_impact": 6,
"blast_radius": 4,
"app_compatibility": 3,
"security": 0
},
"semver_suggestion": "major",
"breaking_changes": [
{
"type": "path.remove",
"path": "/users",
"method": "GET",
"field": "",
"severity": "high",
"description": "Endpoint removed"
}
],
"non_breaking_changes": [],
"security_findings": [],
"changelog": {
"breaking": ["**Removed** endpoint `GET /users`"],
"added": [],
"changed": [],
"deprecated": []
},
"policy_violations": [],
"should_block": true,
"stats": {
"total_changes": 1,
"breaking_count": 1,
"non_breaking_count": 0,
"security_count": 0
}
}
Response Fields
| Field | Type | Description |
|---|---|---|
risk_score | number | Numeric risk signal, 0 to 100. Information, not a decision — branch on execution_action |
risk_level | string | Human-readable level: minimal, low, moderate, high, critical |
risk_dimensions | object | Breakdown: revenue_impact, blast_radius, app_compatibility, security |
semver_suggestion | string | Suggested version bump: major, minor, or patch |
breaking_changes | array | List of breaking change objects (see below) |
non_breaking_changes | array | List of non-breaking change objects |
security_findings | array | Security-related findings (auth removal, scope changes) |
changelog | object | Auto-generated changelog: breaking, added, changed, deprecated |
policy_violations | array | Policy rule violations based on config |
should_block | boolean | true if risk exceeds threshold or blocked change types found |
stats | object | Summary counts: total_changes, breaking_count, non_breaking_count, security_count |
Breaking Change Object
| Field | Type | Description |
|---|---|---|
type | string | Change type (e.g., path.remove, request.property.removed, response.property.type-changed) |
path | string | API path affected (e.g., /users/{id}) |
method | string | HTTP method (GET, POST, etc.) |
field | string | Specific field name if applicable |
severity | string | low, medium, or high |
description | string | Human-readable description of the change |
economic_impact
Present only when the repository configures it in .coderifts.yml. CodeRifts publishes no default figures.
Run it with curl
No key needed: an analyze preflight returns what changed and never grants anything. Replace analyze with authorize, add "context": {"operation": "merge", "target_id": "…"} and an Authorization: Bearer header to get a decision and a signed receipt.
curl -sS -X POST https://app.coderifts.com/api/v1/preflight \
-H 'Content-Type: application/json' \
-d '{
"preflight_mode": "analyze",
"artifacts": [{
"id": "openapi.yaml", "type": "openapi",
"before": "openapi: 3.0.0\ninfo: {title: Pets, version: \"1\"}\npaths:\n /pets: {get: {responses: {\"200\": {description: ok}}}}",
"after": "openapi: 3.0.0\ninfo: {title: Pets, version: \"1\"}\npaths: {}"
}]
}'
# analysis_outcome: BREAKS_DETECTED · authorization_effect: NONE · may_execute: false
Error Codes
The API returns standard HTTP status codes with a JSON error body:
{
"error": "missing_spec",
"message": "Both old_spec and new_spec are required"
}
| Error Code | HTTP Status | Description |
|---|---|---|
missing_spec | 400 | Both old_spec and new_spec are required |
invalid_spec | 400 | One or both specs are not valid OpenAPI (YAML/JSON parse error) |
spec_too_large | 413 | Request body exceeds 5MB limit |
invalid_api_key | 401 | Missing, invalid, or expired API key |
rate_limited | 429 | Monthly or per-minute rate limit exceeded |
server_error | 500 | Internal server error — contact [email protected] |
SDKs & Integration Examples
SDKs: npm install @coderifts/sdk · pip install coderifts-sdk. CLI: npx coderifts.
cURL
curl -sS -X POST https://app.coderifts.com/api/v1/preflight \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $CODERIFTS_API_KEY" \
-d @change-set.json
# change-set.json: {"preflight_mode": "authorize",
# "context": {"operation": "merge", "target_id": "acme/pets#42"},
# "artifacts": [{"id": "openapi.yaml", "type": "openapi", "before": "…", "after": "…"}]}
Python
import os, requests
body = {
"preflight_mode": "authorize",
"context": {"operation": "merge", "target_id": "acme/pets#42"},
"artifacts": [{"id": "openapi.yaml", "type": "openapi",
"before": open("old-api.yaml").read(), "after": open("new-api.yaml").read()}],
}
r = requests.post("https://app.coderifts.com/api/v1/preflight", json=body,
headers={"Authorization": f"Bearer {os.environ['CODERIFTS_API_KEY']}"})
action = r.json().get("execution_action")
# Branch on execution_action only; anything else is not permission.
if action not in ("CONTINUE", "CONTINUE_WITH_MONITORING"):
raise SystemExit(f"not granted: {action}")
JavaScript (Node.js / fetch)
const res = await fetch('https://app.coderifts.com/api/v1/preflight', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.CODERIFTS_API_KEY}`,
},
body: JSON.stringify({
preflight_mode: 'authorize',
context: { operation: 'merge', target_id: 'acme/pets#42' },
artifacts: [{ id: 'openapi.yaml', type: 'openapi', before: oldSpec, after: newSpec }],
}),
});
const { execution_action } = await res.json();
// Branch on execution_action only; anything else is not permission.
if (!['CONTINUE', 'CONTINUE_WITH_MONITORING'].includes(execution_action)) process.exit(1);
GitHub Actions
# .github/workflows/contract-gate.yml — the merge gate. Require the job named
# "CodeRifts / contract-gate" and the App check "CodeRifts / issuer".
# Full file: https://github.com/coderifts/contract-gate/blob/main/examples/contract-gate.yml
- uses: coderifts/contract-gate@v0
with:
api-key: ${{ secrets.CODERIFTS_API_KEY }}
post-check-run: 'false'
GitLab CI, Jenkins
Call POST /api/v1/preflight from a job, as in the cURL above. The merge gate is GitHub-only today: GitLab has API derivation and no merge gate.
Configuration Templates
Industry-specific .coderifts.yml templates for your repo. Choose your industry, copy the config, and drop it in your repository root.
Drop this file as .coderifts.yml in your repository root. CodeRifts will automatically detect and apply it.
Risk band thresholds
Under risk_scoring you can tune where risk_score (0-100) crosses into the HIGH and CRITICAL bands. These control the risk band label shown in the PR comment only - they do not change whether a check passes or fails. Pass/fail is governed separately by policy.freeze_on_risk_score.
| Key | Default | Range | Effect |
|---|---|---|---|
risk_scoring.high_risk_threshold | 70 | 0-100 | Score at/above this is labeled HIGH in the PR comment. |
risk_scoring.critical_risk_threshold | 85 | 0-100 | Score at/above this is labeled CRITICAL in the PR comment. |
Out-of-range or non-numeric values fall back to the defaults. Example:
risk_scoring:
high_risk_threshold: 70 # 0-100; HIGH band label in the PR comment
critical_risk_threshold: 85 # 0-100; CRITICAL band label
policy:
freeze_on_risk_score: 85 # this is what actually fails the check