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
5 changes: 5 additions & 0 deletions packages/pctr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ npx @blocksifr/pctr init # discover agents and tools, write pctr.json
npx @blocksifr/pctr scan # what consequences can they reach?
```

If the project has no agents, `init` says so rather than inventing any. To see how a
scan reads, `npx @blocksifr/pctr init --example` writes a worked example — and every
view of it is labelled as made-up data, so it can never be mistaken for findings about
your code.

Or install it: `npm i -g @blocksifr/pctr`, then just `pctr init` and `pctr scan`.

No account. No network. Everything stays in `./pctr.json` and `./.pctr`.
Expand Down
4 changes: 3 additions & 1 deletion packages/pctr/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ runs:
set -euo pipefail
PCTR="npx --yes @blocksifr/pctr@${{ inputs.version }}"

# init is additive: an existing pctr.json is kept and only extended.
# init is additive: an existing pctr.json is kept and only extended. It never
# fabricates agents, so a repository with none produces an empty scan, not a
# false report.
$PCTR init > /dev/null
$PCTR scan --share pctr-report.md > /dev/null
$PCTR scan --json > pctr-scan.json
Expand Down
41 changes: 31 additions & 10 deletions packages/pctr/bin/pctr.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -103,27 +103,48 @@ async function main() {
case 'init': {
const existing = store.readManifest();
const { manifest, notes, discovered } = discover(process.cwd(), { declared: existing });
// In CI a fabricated example would produce a false report about someone's repo,
// so --no-example keeps an empty scan empty.
if (!manifest.agents.length && !argv.includes('--no-example')) {
manifest.agents = exampleManifest().agents;
manifest.tools = exampleManifest().tools;
manifest.actions = exampleManifest().actions;
notes.push('No agents detected; wrote a worked example you can edit.');
// Never invent agents in someone's repository. Fabricated findings presented in
// the same format as real ones is the fastest way to lose a reader's trust.
const wantsExample = argv.includes('--example');
if (!manifest.agents.length && wantsExample) {
Object.assign(manifest, {
example: true,
agents: exampleManifest().agents,
tools: exampleManifest().tools,
actions: exampleManifest().actions
});
notes.push('Wrote the bundled example. It describes made-up agents, not this project.');
}
const path = store.writeManifest(manifest);
if (asJson) return out(null, { path, manifest, notes });
console.log(r.heading('pctr initialized'));
console.log(`Wrote ${path}`);
console.log(r.field('Agents', String(discovered.agents || manifest.agents.length)));
console.log(r.field('Tools', String(discovered.tools || manifest.tools.length)));
console.log(r.field('Agents found', String(discovered.agents)));
console.log(r.field('Tools found', String(discovered.tools)));
for (const n of notes) console.log(r.dim(`- ${n}`));

if (!manifest.agents.length) {
console.log(`\n${r.yellow('No agents found in this project.')}`);
console.log('PCTR looks for MCP servers, and for agent and tool declarations in');
console.log('your source — OpenAI, Claude, LangGraph, CrewAI, AutoGen, AGT.');
console.log(`\nDeclare them in ${r.bold('pctr.json')}, or see how it works on a worked example:`);
console.log(` ${r.bold('pctr init --example')} ${r.dim('(made-up agents, clearly labelled)')}`);
return 0;
}
console.log(`\nNext: ${r.bold('pctr scan')}`);
return 0;
}

case 'scan': {
const graph = loadGraph();
if (graph.manifest.example && !asJson) console.log(r.exampleBanner());
if (!graph.manifest.agents?.length) {
return out([r.heading('nothing to scan'),
'pctr.json declares no agents, so there is nothing that can cause a consequence.',
'', `Run ${r.bold('pctr init')} to discover them, or ${r.bold('pctr init --example')} to see`,
'how a scan reads on a worked example.'].join('\n'),
{ summary: summarize(graph), protected: [] });
}
if (flag('share')) {
const report = renderReport(graph);
const file = flag('share') !== true ? String(flag('share')) : 'pctr-report.md';
Expand Down Expand Up @@ -495,7 +516,7 @@ Usage
Options
--json Machine-readable output
--share [file] scan: write the findings as shareable Markdown
--no-example init: don't write a worked example when nothing is found
--example init: write the bundled worked example (made-up agents)
--amount <n> Material parameter: amount
--records <n> Material parameter: records affected
--params '<json>' Any other material parameters
Expand Down
2 changes: 1 addition & 1 deletion packages/pctr/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@blocksifr/pctr",
"version": "0.1.0",
"version": "0.1.1",
"description": "See what your AI agents can cause, route them safely, and prove what happened. Consequence preview, trust routing and signed execution receipts for AI agents.",
"type": "module",
"license": "Apache-2.0",
Expand Down
4 changes: 4 additions & 0 deletions packages/pctr/src/render.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ export const field = (label, value, width = 22) =>
`${label.length >= width ? `${label} ` : label.padEnd(width)}${value}`;
export const chain = (ids) => ids.join(`\n${dim(' |')}\n${dim(' v')}\n`);

// Example data must never be mistaken for findings about the reader's own project.
export const exampleBanner = () =>
`\n${yellow('EXAMPLE DATA')} ${dim('— these agents are made up. They are not in this project.')}\n${dim('Run `pctr init` against a project with real agents to scan it.')}`;

export function renderScan(graph) {
const s = summarize(graph);
const out = [heading('PCTR'), 'Scanning your agents...', ''];
Expand Down
4 changes: 4 additions & 0 deletions packages/pctr/src/report.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ export function renderReport(graph, { title = 'PCTR consequence scan', includeBa
const out = [];

out.push(`## ${summary.bySeverity.CRITICAL ? '🛑' : '🔍'} ${title}`, '');
if (graph.manifest?.example) {
out.push('> **Example data.** These agents are made up and are not part of this project. ' +
'This report is showing how a scan reads, not findings about real software.', '');
}
if (includeBadge) out.push(`![PCTR](${badgeUrl(graph)})`, '');
if (!summary.agents) {
out.push(`PCTR found no agents to scan here. Declare them in \`pctr.json\` — see the [manifest format](${HOMEPAGE}#pctrjson).`, '');
Expand Down
11 changes: 10 additions & 1 deletion packages/pctr/tests/report.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ import assert from 'node:assert/strict';
import { buildGraph } from '../src/graph.mjs';
import { renderReport, badgeUrl } from '../src/report.mjs';

const graph = (overrides = {}) => buildGraph({
const manifestOf = (overrides = {}) => ({
principal: 'user:test',
agents: [{ id: 'support', trust: 0.8, evidenceAgeSeconds: 10, authority: ['customers.read'], tools: ['db'], delegatesTo: ['admin'] },
{ id: 'admin', trust: 0.95, evidenceAgeSeconds: 10, authority: ['customers.*'], tools: ['db'] }],
tools: [{ id: 'db', protocol: 'mcp', actions: ['customers.delete', 'customers.read'] }],
actions: [{ id: 'customers.delete', recordsAffected: 1842, connectedWorkflows: 4 }],
...overrides
});
const graph = (overrides = {}) => buildGraph(manifestOf(overrides));

test('the shared report leads with the consequence, not with agent counts', () => {
const report = renderReport(graph());
Expand Down Expand Up @@ -51,3 +52,11 @@ test('an empty scan says so rather than inventing findings', () => {
test('the badge reflects what was actually found', () => {
assert.match(badgeUrl(graph()), /1%20critical%20consequence-critical/);
});

test('example data is never presented as findings about the reader\'s project', () => {
const example = buildGraph(manifestOf({ example: true }));
const report = renderReport(example);
assert.match(report, /\*\*Example data\.\*\* These agents are made up/);
assert.ok(report.indexOf('Example data') < report.indexOf('Highest priority'),
'the warning must come before anything that reads as a finding');
});
Loading