Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
e91743f
feat: add pull request risk policy
cursoragent Sep 27, 2026
bc32fb4
fix: cover exported SDK surfaces in risk policy
cursoragent Sep 27, 2026
a2021f6
chore: validate generated cleanup
cursoragent Sep 27, 2026
0f1f39b
fix(ci): reconcile advisory trust policy from current evidence
dodeja Oct 6, 2026
dc7b118
docs: add trust policy installation and GitHub verification trial
dodeja Oct 6, 2026
3b86212
fix(ci): reject ambiguous attempt and bypass evidence
dodeja Oct 6, 2026
e618df7
chore(ci): verify generated collection after trust stack update
dodeja Oct 6, 2026
95196fb
chore(ci): verify restored policy artifacts
dodeja Oct 6, 2026
11a7949
fix(ci): cover SDK errors and ref-qualified workflow paths
dodeja Oct 6, 2026
994a846
docs: add required frontmatter to trust policy guides
dodeja Oct 6, 2026
0846b1b
fix(ci): reconcile shadow labels from current issue state
dodeja Oct 6, 2026
499f06e
fix(ci): inspect every effective branch-rule page
dodeja Oct 6, 2026
b2ac048
docs: keep maintainer policy guides with the agent skill
dodeja Oct 6, 2026
469a626
fix(ci): order trust evidence by current attempt start
dodeja Oct 6, 2026
c1852b6
docs: describe exact-attempt CI evidence ordering
dodeja Oct 6, 2026
0ab43b5
style: format updated policy fixtures
dodeja Oct 6, 2026
b20682e
fix(ci): continue advisory reconciliation after an individual failure
dodeja Oct 14, 2026
9c24edd
chore: Auto-generate Postman collection from openapi.json [skip ci]
github-actions[bot] Oct 14, 2026
9581b2e
test(ci): retain failure reporting for targeted reconciliation
dodeja Oct 14, 2026
c7641d9
fix(ci): aggregate layered native review protections
dodeja Oct 14, 2026
629d135
fix(ci): keep the shadow comparison outside merge authority
dodeja Oct 14, 2026
2a196f4
fix(ci): require coverage drift checks in trusted run evidence
dodeja Oct 14, 2026
6b5f819
fix(ci): refresh advisory shadow scores on PR metadata edits
dodeja Oct 14, 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
44 changes: 44 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
* @dodeja @SylvainGravejat

# High-risk control planes and release inputs
/.github/ @dodeja @SylvainGravejat
/package.json @dodeja @SylvainGravejat
/package-lock.json @dodeja @SylvainGravejat
/npm-shrinkwrap.json @dodeja @SylvainGravejat
/tsconfig.json @dodeja @SylvainGravejat
/vercel.json @dodeja @SylvainGravejat
/vite.config.ts @dodeja @SylvainGravejat

# Public API contract, MCP runtime, and OAuth gateway
/docs/openapi.json @dodeja @SylvainGravejat
/api/ @dodeja @SylvainGravejat
/packages/mcp/ @dodeja @SylvainGravejat

# TypeScript SDK package and public export surface
/sdks/typescript-sdk/package.json @dodeja @SylvainGravejat
/sdks/typescript-sdk/src/client.ts @dodeja @SylvainGravejat
/sdks/typescript-sdk/src/index.ts @dodeja @SylvainGravejat
/sdks/typescript-sdk/src/client/jsonapi.ts @dodeja @SylvainGravejat
/sdks/typescript-sdk/src/client/errors.ts @dodeja @SylvainGravejat
/sdks/typescript-sdk/src/client/managers/ @dodeja @SylvainGravejat
/sdks/typescript-sdk/src/client/mappers.ts @dodeja @SylvainGravejat
/sdks/typescript-sdk/src/client/transport.ts @dodeja @SylvainGravejat
/sdks/typescript-sdk/src/generated/ @dodeja @SylvainGravejat
/sdks/typescript-sdk/src/types/ @dodeja @SylvainGravejat

# Risk policy and agent-trust verification
/scripts/ci/ @dodeja @SylvainGravejat
/skills/agent-trust/ @dodeja @SylvainGravejat
/.cursor/rules/agent-trust.mdc @dodeja @SylvainGravejat
/bin/agent-verify.mjs @dodeja @SylvainGravejat
/bin/check-agent-trust.mjs @dodeja @SylvainGravejat
/bin/check-agent-trust.test.mjs @dodeja @SylvainGravejat
/bin/check-openapi-types.mjs @dodeja @SylvainGravejat

# Capability guides require human review
/docs/mcp/ @dodeja @SylvainGravejat
/docs/sdk/ @dodeja @SylvainGravejat
/docs/api-docs/in-depth-guides/mcp.mdx @dodeja @SylvainGravejat
/docs/api-docs/getting-started/sdk-quickstart.mdx @dodeja @SylvainGravejat
/docs/risk-gates.md @dodeja @SylvainGravejat
/docs/risk-gates-trial.md @dodeja @SylvainGravejat
209 changes: 209 additions & 0 deletions .github/workflows/shadow-risk.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
name: Shadow Risk

on:
pull_request_target:
types: [opened, reopened, synchronize, edited, labeled, unlabeled, ready_for_review]

permissions:
contents: read
issues: write
pull-requests: read

concurrency:
group: shadow-risk-${{ github.event.pull_request.number }}
cancel-in-progress: false

jobs:
shadow:
if: github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
name: Jev shadow comparison (non-blocking)
runs-on: ${{ (startsWith(vars.CI_RUNNER_SMALL, 'blacksmith-') && vars.CI_RUNNER_SMALL) || 'blacksmith-2vcpu-ubuntu-2404' }}
steps:
- name: Resolve trusted default-branch commit
id: trusted
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd
with:
script: |
const { data } = await github.rest.git.getRef({
...context.repo,
ref: `heads/${context.payload.repository.default_branch}`,
});
core.setOutput('sha', data.object.sha);
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
ref: ${{ steps.trusted.outputs.sha }}
persist-credentials: false
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38
with:
node-version: 24

- name: Collect pull request context
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd
with:
script: |
const fs = require('node:fs');
const files = await github.paginate(github.rest.pulls.listFiles, {
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
per_page: 100,
});
const { data: pull } = await github.rest.pulls.get({
...context.repo,
pull_number: context.issue.number,
});
const { pathToFileURL } = require('node:url');
const { classifyFiles } = await import(pathToFileURL(
`${process.env.GITHUB_WORKSPACE}/scripts/ci/classify-pr-risk.mjs`,
).href);
classifyFiles(files, pull.changed_files);
fs.writeFileSync(
'/tmp/shadow-input.json',
JSON.stringify({
title: pull.title,
body: pull.body,
changedFiles: pull.changed_files,
files: files.map((file) => ({
filename: file.filename,
previous_filename: file.previous_filename,
status: file.status,
additions: file.additions,
deletions: file.deletions,
patch: file.patch,
})),
}),
);

- name: Score shadow risk
id: score
continue-on-error: true
env:
JEV_API_KEY: ${{ secrets.JEV_API_KEY }}
run: node scripts/ci/score-shadow-risk.mjs --json /tmp/shadow-input.json >> "$GITHUB_OUTPUT"

- name: Apply shadow label and sticky comparison
continue-on-error: true
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd
env:
SHADOW_RISK: ${{ steps.score.outputs.shadow-risk }}
SHADOW_SOURCE: ${{ steps.score.outputs.source }}
PATH_RISK: ${{ steps.score.outputs.path-risk }}
with:
script: |
const validRisks = new Set(['low', 'medium', 'high']);
const shadow = validRisks.has(process.env.SHADOW_RISK)
? process.env.SHADOW_RISK
: 'medium';
const path = validRisks.has(process.env.PATH_RISK)
? process.env.PATH_RISK
: 'medium';
const source =
process.env.SHADOW_SOURCE || 'workflow heuristic fallback';
const definitions = {
low: {
color: 'A7F3D0',
description: 'Shadow-only risk signal: low',
},
medium: {
color: 'FDE68A',
description: 'Shadow-only risk signal: medium',
},
high: {
color: 'FCA5A5',
description: 'Shadow-only risk signal: high',
},
};

for (const [risk, definition] of Object.entries(definitions)) {
const name = `risk-shadow:${risk}`;
try {
await github.rest.issues.getLabel({
owner: context.repo.owner,
repo: context.repo.repo,
name,
});
} catch (error) {
if (error.status !== 404) throw error;
await github.rest.issues.createLabel({
owner: context.repo.owner,
repo: context.repo.repo,
name,
...definition,
});
}
}

const pull = context.payload.pull_request;
const labels = (await github.paginate(
github.rest.issues.listLabelsOnIssue,
{
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pull.number,
per_page: 100,
},
)).map((label) => label.name);
for (const risk of Object.keys(definitions)) {
const label = `risk-shadow:${risk}`;
if (risk === shadow || !labels.includes(label)) continue;
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pull.number,
name: label,
});
}
if (!labels.includes(`risk-shadow:${shadow}`)) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pull.number,
labels: [`risk-shadow:${shadow}`],
});
}

const marker = '<!-- terminal49-shadow-risk -->';
const comparison =
path === shadow ? 'agreement' : `disagreement (${path} vs ${shadow})`;
const body = `${marker}
### Shadow risk calibration

| Signal | Result | Authority |
| --- | --- | --- |
| Path classifier | \`risk:${path}\` | Deterministic advisory policy |
| ${source} | \`risk-shadow:${shadow}\` | Shadow only |

Calibration result: **${comparison}**.

Shadow risk never blocks or enables merge. Keep it in observation mode until roughly 50 pull requests have human-reviewed labels, then calibrate before considering any policy use.`;
const comments = await github.paginate(
github.rest.issues.listComments,
{
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pull.number,
per_page: 100,
},
);
const existing = comments.find((comment) =>
comment.user?.login === 'github-actions[bot]' && comment.body?.includes(marker),
);
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pull.number,
body,
});
}

- name: Keep shadow scoring non-blocking
if: always()
run: echo "Shadow risk is observational and never gates merge."
64 changes: 64 additions & 0 deletions .github/workflows/trust-policy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
name: Trust Policy Advisory

on:
pull_request_target:
types: [opened, reopened, synchronize, labeled, unlabeled, ready_for_review, converted_to_draft, edited]
workflow_run:
workflows: [CI, Trust Review Signal]
types: [completed]
schedule:
- cron: '17 * * * *'
workflow_dispatch:
inputs:
pull_number:
description: 'PR number; leave empty to reconcile all open PRs'
required: false
type: string

permissions:
contents: read
actions: read
checks: read
issues: write
pull-requests: read

concurrency:
group: trust-policy-advisory
cancel-in-progress: false

jobs:
reconcile:
name: Advisory policy (never authorizes or merges)
if: >-
github.ref == format('refs/heads/{0}', github.event.repository.default_branch) &&
(github.event_name != 'pull_request_target' ||
github.event.pull_request.base.ref == github.event.repository.default_branch)
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Resolve trusted default-branch commit
id: trusted
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd
with:
script: |
const { data } = await github.rest.git.getRef({
...context.repo,
ref: `heads/${context.payload.repository.default_branch}`,
});
core.setOutput('sha', data.object.sha);
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
ref: ${{ steps.trusted.outputs.sha }}
persist-credentials: false
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38
with:
node-version: 24
- name: Reconcile fresh PR evidence
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd
with:
script: |
const { pathToFileURL } = require('node:url');
const { reconcileEvent } = await import(pathToFileURL(
`${process.env.GITHUB_WORKSPACE}/scripts/ci/reconcile-trust.mjs`,
).href);
await reconcileEvent({ github, context, core });
14 changes: 14 additions & 0 deletions .github/workflows/trust-review-signal.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
name: Trust Review Signal

on:
pull_request_review:
types: [submitted, edited, dismissed]

permissions: {}

jobs:
signal:
name: Advisory review refresh signal
runs-on: ubuntu-latest
steps:
- run: echo 'Review changed. The default-branch controller reads current evidence.'
4 changes: 2 additions & 2 deletions bin/agent-verify.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ const steps = [
? []
: [
{
name: 'Workspace tests',
command: ['npx', '--no-install', 'vp', 'run', 'test'],
name: 'Policy and workspace tests',
command: ['npm', 'test'],
},
]),
{
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@
"check": "vp check && npm run build --workspace @terminal49/sdk && npm run type-check:api && vp run -r type-check",
"format": "vp fmt --write api packages sdks",
"lint:api": "vp lint api && vp fmt --check api",
"test": "npm run test:agent-trust && vp run -r test",
"test": "npm run test:agent-trust && npm run test:risk-policy && vp run -r test",
"test:coverage": "node --test tools/generate-coverage.test.mjs tools/generate-rail-terminals.test.mjs && node tools/generate-coverage.mjs --check && node tools/generate-rail-terminals.mjs --check",
"test:agent-trust": "node --test bin/check-agent-trust.test.mjs bin/check-openapi-types.test.mjs",
"test:risk-policy": "node --test scripts/ci/*.test.mjs",
"type-check:api": "tsc --noEmit -p tsconfig.json",
"sdk:docs": "npm run docs --workspace @terminal49/sdk"
},
Expand Down
Loading
Loading