Skip to content
Merged
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
14 changes: 14 additions & 0 deletions .cursor/rules/agent-trust.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
description: Terminal49/API paved path and verification gate
alwaysApply: true
---

# Agent trust gate

- Extend OpenAPI endpoints, npm packages, and MCP tools through
`skills/agent-trust/references/paved-path.md`.
- Keep `skills/agent-trust/feature-map.json` synchronized with MCP tool
registrations, SDK package exports, and non-generated SDK/MCP docs routes.
- Run `npm run agent-verify` before handing off code changes. Do not claim a
passing result from `agent-verify:quick`.
- Keep generated files generated and credentials out of this public repository.
14 changes: 14 additions & 0 deletions .cursor/rules/pstack-onboarding.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
description: Repository-local P-Stack routing
alwaysApply: true
---

# P-Stack

For non-trivial Terminal49/API work, use the installed P-Stack plugin's
`/poteto-mode` workflow when available. Use `/how` for unfamiliar subsystems
and `/reflect` to turn durable lessons into executable guards. The agent-trust
rule owns the repository verification gate.

Model and reasoning-budget choices belong in the user-local file created by
`/setup-pstack`; never commit personal model slugs.
43 changes: 43 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
* @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
/bin/check-openapi-types.test.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
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ jobs:
cache-dependency-path: package-lock.json
- name: Install workspace dependencies
run: npm ci
- name: Check agent trust guards and generated OpenAPI artifacts
run: npm run check:agent-trust
- name: Check root and every workspace
run: npx vp run check
- name: Test every workspace
Expand Down
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.'
8 changes: 8 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,14 @@ For substantive documentation writing, use the repo-local skill at `skills/termi
- CI (`.github/workflows/ci.yml`) runs build + test + lint for both packages.
- Running the MCP server locally + testing tool calls with Claude Desktop (stdio and gateway paths): [packages/mcp/LOCAL_DEV.md](packages/mcp/LOCAL_DEV.md). Gateway env template: `.env.local.example`.

### Agent paved path

- Read [`skills/agent-trust/SKILL.md`](skills/agent-trust/SKILL.md) before adding or changing an OpenAPI endpoint, npm package, MCP tool, SDK entrypoint, or SDK/MCP docs route.
- Extend the repository through [`skills/agent-trust/references/paved-path.md`](skills/agent-trust/references/paved-path.md); do not create alternate registration, generation, or verification flows.
- Keep [`skills/agent-trust/feature-map.json`](skills/agent-trust/feature-map.json) synchronized in the same commit. CI rejects MCP tool, SDK export, and docs-route drift.
- Run `npm run agent-verify` before handoff. `npm run agent-verify:quick` is for iteration and is not the completion gate.
- P-Stack is optional user tooling, not a repository dependency. Install it with `/add-plugin pstack`, then run `/setup-pstack` to choose a user-local model budget. Repository wiring is in [`skills/agent-trust/references/pstack.md`](skills/agent-trust/references/pstack.md).

---

## MCP Auth Gateway notes
Expand Down
Loading
Loading