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
31 changes: 31 additions & 0 deletions .github/workflows/truecopy-gate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Dogfood: the stack pins its own MCP tool surface. mcp-manifest.json is the
# server's real tools/list response (support/dump-tools.mjs), pinned in
# truecopy.lock. The chain: code -> manifest (--check) -> lock (verify).
# Changing a tool name, description, or schema — or a dep bump that changes
# the generated schemas — fails this gate until consciously re-pinned.
name: truecopy gate

on:
push:
branches: [master]
pull_request:
branches: [master]

permissions: read-all

jobs:
verify:
name: verify pinned tool surface
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 20
- run: npm ci --no-audit --no-fund
- name: manifest matches the live tool surface
run: node support/dump-tools.mjs --check
- name: truecopy verify
uses: askalf/truecopy-action@5c6fcf00edb83ff4846206392722292d45fee227 # v1.0.1
98 changes: 98 additions & 0 deletions mcp-manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
{
"name": "own-your-stack",
"tools": [
{
"name": "canon_scan",
"title": "Scan an MCP/skill manifest for supply-chain poisoning",
"description": "Paste an MCP server or skill manifest (JSON) and truecopy scans its tool names/descriptions for hidden instructions, exfiltration lures, and other poisoned-skill / tool-poisoning attacks. Returns a verdict (clean / flagged) and the findings. Vet a third-party tool BEFORE you trust it.",
"inputSchema": {
"type": "object",
"properties": {
"manifest": {
"type": "string",
"description": "the MCP/skill manifest as JSON text"
}
},
"required": [
"manifest"
],
"$schema": "http://json-schema.org/draft-07/schema#"
},
"annotations": {
"readOnlyHint": true
},
"execution": {
"taskSupport": "forbidden"
}
},
{
"name": "keeper_lease",
"title": "Lease a credential — you get an opaque handle, never the secret",
"description": "Request a short-lived, scoped lease for a credential held in the strongroom vault. You receive a lease handle (id + scope + ttl); the secret itself is materialized only at the egress point when the lease is redeemed, and never enters your context. The named secret must already be in the vault.",
"inputSchema": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "the vault secret name to lease"
},
"host": {
"description": "restrict the lease to this destination host",
"type": "string"
},
"ttlS": {
"description": "lease lifetime in seconds (default 300)",
"type": "integer",
"exclusiveMinimum": 0,
"maximum": 9007199254740991
},
"uses": {
"description": "max redemptions (default 1)",
"type": "integer",
"exclusiveMinimum": 0,
"maximum": 9007199254740991
}
},
"required": [
"name"
],
"$schema": "http://json-schema.org/draft-07/schema#"
},
"execution": {
"taskSupport": "forbidden"
}
},
{
"name": "warden_check",
"title": "Is this action safe to run? (action firewall)",
"description": "Submit a tool action — { tool, input } — and get redstamp's verdict: decision (allow / approve / block), risk tier, and the reasons. Catches shell/exec, SSRF + cloud-metadata, secret exfiltration, dangerous writes/deletes, and prompt-injection in the arguments. Call this BEFORE executing any consequential tool call.",
"inputSchema": {
"type": "object",
"properties": {
"tool": {
"type": "string",
"description": "the tool/action name, e.g. fetch, shell, write, delete, read"
},
"input": {
"description": "the arguments the tool would run with (url, command, path, content, …)",
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
}
},
"required": [
"tool"
],
"$schema": "http://json-schema.org/draft-07/schema#"
},
"annotations": {
"readOnlyHint": true
},
"execution": {
"taskSupport": "forbidden"
}
}
]
}
48 changes: 48 additions & 0 deletions support/dump-tools.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* Dump the composed server's live tool surface — the exact `tools/list`
* wire response, via a real MCP client over an in-memory transport — to
* mcp-manifest.json, where truecopy pins it (see truecopy.lock).
*
* node support/dump-tools.mjs regenerate mcp-manifest.json
* node support/dump-tools.mjs --check exit 1 if the committed manifest
* no longer matches the code
*
* The chain this enables in CI: code -> manifest (--check) -> lock
* (truecopy verify). Changing the tool surface — or bumping a dep that
* changes the generated schemas — fails the gate until the manifest is
* regenerated and consciously re-pinned.
*/
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
import { createOysServer } from '../mcp.mjs';

const MANIFEST = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'mcp-manifest.json');

const { server } = createOysServer();
const [clientT, serverT] = InMemoryTransport.createLinkedPair();
const client = new Client({ name: 'oys-dump-tools', version: '0' });
await Promise.all([server.connect(serverT), client.connect(clientT)]);

const { tools } = await client.listTools();
const manifest = {
name: 'own-your-stack',
tools: tools.slice().sort((a, b) => a.name.localeCompare(b.name)),
};
const rendered = JSON.stringify(manifest, null, 2) + '\n';
await client.close();

if (process.argv.includes('--check')) {
const committed = fs.existsSync(MANIFEST) ? fs.readFileSync(MANIFEST, 'utf8').replace(/\r\n/g, '\n') : '';
if (committed !== rendered) {
console.error('mcp-manifest.json is stale: the server\'s tool surface changed.');
console.error('Regenerate with `node support/dump-tools.mjs`, review the diff, and re-pin (`truecopy add mcp-manifest.json`).');
process.exit(1);
}
console.log('mcp-manifest.json matches the live tool surface');
} else {
fs.writeFileSync(MANIFEST, rendered);
console.log(`wrote ${MANIFEST} (${manifest.tools.length} tools)`);
}
23 changes: 23 additions & 0 deletions truecopy.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"version": 1,
"skills": {
"own-your-stack": {
"source": "mcp-manifest.json",
"kind": "mcp",
"hash": "82ee2bdcb462d9ddac4ed2a488a5b6c10a2308fbe6071ef5da3a12c47e14b44c",
"scannedAt": "2026-07-16T17:30:10.521Z",
"verdict": "clean",
"findings": 0,
"detection": {
"engine": "redstamp",
"version": "0.4.1"
},
"parts": {
"canon_scan": "b0465b249ae555a3872488db17c1fb2965951ec7b9bdfe6c4590901e6cbbd15a",
"keeper_lease": "7a62c4055c035c4a6602a098264f8ce150026e8d1ac7951a4e3cad2f3d645c16",
"warden_check": "21e1989a475f3483a81a2fb85e9f214bb0d453cc634f32d438e125c1852797f1",
"(manifest)": "d0e70ff8f270eabfc57551ef012b3becc28668e593fdefa6185405c51984fa6b"
}
}
}
}