← Files ECZ-ID Agent TrustARCHIVED FILE
skills/agent-trust-review/scripts/review.mjs
21.2 KB · Oct 2, 2026 · 00:32 UTC
#!/usr/bin/env node
// ECZ-ID Agent Trust: portable evidence review (generated by the ECZ-ID Plugin Foundry; do not edit).
// Reads file NAMES and PATHS under the given root. Opens no file. No network. Writes nothing.
// Same detectors, Review Priority rules and next actions as the ECZ-ID VS Code extension.
import { readdirSync } from "node:fs";
import { join, relative } from "node:path";
export const DEFAULT_IGNORES = new Set(["node_modules", ".git", ".pnpm-store", "dist", "out", "build", ".next", ".turbo", ".venv", "venv", "__pycache__", "target", "coverage"]);
export const DOT_ALLOWLIST = new Set([".github", ".gitlab", ".well-known"]);
/** Workspace-relative file paths, filename and path only. Dot-entries are skipped except the allowlist. */
export function listFiles(root, { maxDepth = 8, maxFiles = 20000, extraDotEntries = [] } = {}) {
const results = [];
const dots = new Set([...DOT_ALLOWLIST, ...extraDotEntries]);
const walk = (dir, depth) => {
if (depth > maxDepth || results.length >= maxFiles) return;
let entries;
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return; }
for (const e of entries) {
if (results.length >= maxFiles) return;
if (e.name.startsWith(".") && !dots.has(e.name)) continue;
const full = join(dir, e.name);
if (e.isDirectory()) { if (DEFAULT_IGNORES.has(e.name)) continue; walk(full, depth + 1); }
else if (e.isFile()) results.push(relative(root, full).split("\\").join("/"));
}
};
walk(root, 0);
return results;
}
const RESOLVER_REF = [/(^|\/)\.well-known\/ecz-[a-z0-9-]*\.json$/i, /(^|\/)ecz-(agent|mcp|id)[a-z0-9-]*\.json$/i, /(^|\/)ecz-[a-z0-9-]+\.json$/i];
const REASON = {
EVIDENCE_OBSERVED: "Evidence observed locally for the items listed.",
EVIDENCE_NOT_OBSERVED: "Some expected evidence was not observed locally. This is neutral. It does not mean a problem exists.",
REVIEW_RECOMMENDED: "Observed evidence may still need human review before reliance.",
NO_PUBLIC_PROOF_REFERENCE: "No public resolver proof reference was found yet. This does not mean unsafe. Local policy decides.",
PARTIAL_PUBLIC_PROOF: "Partial public proof reference detected. Resolver-verifiable proof may make this easier to review.",
RECHECK_BEFORE_RELIANCE: "Re-check before reliance. Results reflect the workspace at scan time.",
LOCAL_POLICY_DECIDES: "Your local policy decides whether the observed evidence is sufficient."
};
function matchAny(patterns, files) {
for (const f of files) for (const re of patterns) if (re.test(f)) return f;
return undefined;
}
/** Same semantics as family/detect.ts detectEvidence. */
export function detectEvidence(spec, files, workspaceName) {
const observed = [], notObserved = [], reviewRequired = [];
for (const d of spec.detectors) {
const hit = matchAny(d.patterns.map((p) => new RegExp(p, "i")), files);
if (hit) {
const item = { id: d.id, label: d.label, status: "observed", detail: d.observedDetail, path: hit };
observed.push(item);
if (d.reviewWhenObserved) reviewRequired.push({ ...item, status: "review-required" });
} else notObserved.push({ id: d.id, label: d.label, status: "not-observed", detail: d.notObservedDetail });
}
const codes = [];
if (observed.length) codes.push("EVIDENCE_OBSERVED");
if (notObserved.length) codes.push("EVIDENCE_NOT_OBSERVED");
if (reviewRequired.length) codes.push("REVIEW_RECOMMENDED");
codes.push(matchAny(RESOLVER_REF, files) ? "PARTIAL_PUBLIC_PROOF" : "NO_PUBLIC_PROOF_REFERENCE");
codes.push("LOCAL_POLICY_DECIDES", "RECHECK_BEFORE_RELIANCE");
return { specialistId: spec.extensionId ?? spec.name, scannedAt: new Date().toISOString(), workspaceName, observed, notObserved, reviewRequired, reasonCodes: codes.map((id) => ({ id, message: REASON[id] })) };
}
export const PRIORITY_DISCLAIMER = "Review Priority is not a safety, approval or compliance determination. It indicates how much attention this evidence review deserves, based only on what was observed locally by filename and path.";
export const PRIORITY_MEANING = {
LOW: "Every evidence class this review looks for was observed. Review the documents themselves before reliance.",
NORMAL: "Observed evidence still needs human review, or supporting evidence was not observed. Worth completing before the next review.",
ELEVATED: "A primary evidence class was not observed. Review before you rely on this workspace as an evidence source.",
HIGH: "Evidence that a regulator, auditor or customer is likely to ask for first was not observed, or several primary classes are missing together."
};
export const ELEVATED_GAP_AGGREGATION_THRESHOLD = 2;
const RANK = { LOW: 0, NORMAL: 1, ELEVATED: 2, HIGH: 3 };
const FROM_WEIGHT = { high: "HIGH", elevated: "ELEVATED", normal: "NORMAL" };
/** Same rules as family/valueLayer.ts computeEvidenceReviewPriority. */
export function computeReviewPriority(spec, result, profile) {
const observed = new Map(result.observed.map((i) => [i.id, i]));
const review = new Set(result.reviewRequired.map((i) => i.id));
const lines = [];
for (const d of spec.detectors) {
const g = profile.guidance.find((x) => x.detectorId === d.id);
if (observed.has(d.id)) {
const needs = review.has(d.id);
lines.push({ detectorId: d.id, label: d.label, status: needs ? "review-required" : "observed", weight: "none", contributes: needs ? "NORMAL" : "LOW", detail: needs ? "Observed by filename and path; the document itself still needs human review." : "Observed by filename and path." });
} else {
const w = g?.weightWhenNotObserved ?? "normal";
lines.push({ detectorId: d.id, label: d.label, status: "not-observed", weight: w, contributes: FROM_WEIGHT[w], detail: `Not observed by filename and path (${w === "normal" ? "supporting" : "primary"} evidence class).` });
}
}
const counts = { LOW: 0, NORMAL: 0, ELEVATED: 0, HIGH: 0 };
for (const l of lines) counts[l.contributes]++;
let priority, rationale;
if (counts.HIGH > 0) { priority = "HIGH"; rationale = `HIGH because ${counts.HIGH} evidence class${counts.HIGH === 1 ? "" : "es"} that ${counts.HIGH === 1 ? "is" : "are"} usually requested first ${counts.HIGH === 1 ? "was" : "were"} not observed.`; }
else if (counts.ELEVATED >= ELEVATED_GAP_AGGREGATION_THRESHOLD) { priority = "HIGH"; rationale = `HIGH because ${counts.ELEVATED} primary evidence classes were not observed together (threshold ${ELEVATED_GAP_AGGREGATION_THRESHOLD}).`; }
else if (counts.ELEVATED > 0) { priority = "ELEVATED"; rationale = "ELEVATED because one primary evidence class was not observed."; }
else if (counts.NORMAL > 0) { priority = "NORMAL"; rationale = `NORMAL because ${counts.NORMAL} item${counts.NORMAL === 1 ? "" : "s"} ${counts.NORMAL === 1 ? "needs" : "need"} human review or supporting evidence was not observed.`; }
else { priority = "LOW"; rationale = "LOW because every evidence class was observed and none is flagged for review."; }
lines.sort((a, b) => RANK[b.contributes] - RANK[a.contributes] || a.detectorId.localeCompare(b.detectorId));
return { priority, meaning: PRIORITY_MEANING[priority], disclaimer: PRIORITY_DISCLAIMER, rationale, reasons: lines, counts };
}
/** Same rules as family/valueLayer.ts selectContextualActions. */
export function selectContextualActions(result, profile) {
const observed = new Set(result.observed.map((i) => i.id));
const notObserved = new Set(result.notObserved.map((i) => i.id));
const max = profile.maxActions ?? 3;
return profile.actions
.map((a, idx) => ({ a, idx }))
.filter(({ a }) => a.always || a.whenNotObserved?.some((id) => notObserved.has(id)) || a.whenObserved?.some((id) => observed.has(id)))
.sort((x, y) => (y.a.rank ?? 0) - (x.a.rank ?? 0) || x.idx - y.idx)
.map(({ a }) => a)
.slice(0, Math.max(0, max));
}
const NEUTRAL = [
"This is an evidence-organising review, not a verdict.",
"It does not assert safety, certification, approval or compliance.",
"Filename and path detection shows that a document exists where you expect it. It does not read the document and cannot judge its quality.",
"Missing evidence is neutral. Your local policy decides what is sufficient.",
"Re-check before reliance; results reflect the workspace at scan time."
];
export function renderReview(spec, result, profile) {
const p = computeReviewPriority(spec, result, profile);
const actions = selectContextualActions(result, profile);
const observed = new Map(result.observed.map((i) => [i.id, i]));
const review = new Set(result.reviewRequired.map((i) => i.id));
const L = [];
L.push(`# ${spec.displayName}: Evidence Review`, "", `**${profile.question}**`, "", profile.hook, "");
if (result.workspaceName) L.push(`Workspace: **${result.workspaceName}** | Scanned: ${result.scannedAt} | Method: filename and path only`, "");
L.push(`## Review Priority: ${p.priority}`, "", p.meaning, "", `Why: ${p.rationale}`, "");
L.push(...p.reasons.map((r) => `- ${r.label}: ${r.status.toUpperCase().replace("-", " ")} (contributes ${r.contributes}). ${r.detail}`), "");
L.push(`_${p.disclaimer}_`, "");
L.push("## What we observed, what we did not, and why it matters", "");
for (const d of spec.detectors) {
const g = profile.guidance.find((x) => x.detectorId === d.id);
const hit = observed.get(d.id);
const status = hit ? (review.has(d.id) ? "OBSERVED, REVIEW REQUIRED" : "OBSERVED") : "NOT OBSERVED";
L.push(`### ${d.label}: ${status}`, "");
if (hit?.path) L.push(`- Where: \`${hit.path}\``);
if (hit?.detail) L.push(`- Observed: ${hit.detail}`);
if (!hit && d.notObservedDetail) L.push(`- Observed: ${d.notObservedDetail}`);
if (g) {
L.push(`- Why it matters: ${g.whyItMatters}`);
L.push(`- Review next: ${hit ? g.reviewWhenObserved : g.reviewWhenNotObserved}`);
if (g.capability) L.push(`- If you want to go further: ${g.capability.label}. ${g.capability.note} ${g.capability.url}`);
}
L.push("");
}
L.push("## What this means", "", ...result.reasonCodes.map((rc) => `- ${rc.message}`), "");
L.push("## What this does not mean", "", ...NEUTRAL.map((s) => `- ${s}`), "");
L.push("## Next actions for this result", "");
if (actions.length) { actions.forEach((a, i) => { L.push(`${i + 1}. **${a.label}**: ${a.note}`); L.push(` ${a.url}`); }); L.push(""); }
else L.push("_No contextual action for this result._", "");
if (profile.discovery) L.push(`${profile.discovery.label}: ${profile.discovery.url}`, "");
L.push("TrustOps handles setup and checkout. This review runs no payment and creates no ECZ-ID truth, entitlement or Resolver proof.", "");
return L.join("\n");
}
/** JSON projection for machine consumers. */
export function projectReview(spec, result, profile) {
const p = computeReviewPriority(spec, result, profile);
return {
schema_version: "1.0.0",
product: spec.name,
display_name: spec.displayName,
generated_at_utc: result.scannedAt,
method: "filename-and-path-only",
workspace: result.workspaceName,
review_priority: { level: p.priority, meaning: p.meaning, rationale: p.rationale, disclaimer: p.disclaimer, reasons: p.reasons },
observations: [...result.observed.map((i) => ({ ...i, status: result.reviewRequired.some((r) => r.id === i.id) ? "review-required" : "observed" })), ...result.notObserved].sort((a, b) => a.id.localeCompare(b.id)),
public_safe_reason_codes: result.reasonCodes,
contextual_next_actions: selectContextualActions(result, profile).map((a) => ({ id: a.id, label: a.label, url: a.url, kind: a.kind, note: a.note })),
discovery: profile.discovery ?? null,
privacy: { local_first: true, source_upload: false, hidden_telemetry: false, network_during_review: "none" },
do_not_infer: ["safety", "approval", "certification", "compliance", "entitlement", "binding", "current_identity_state"]
};
}
const SPEC = {"name":"eczid-agent-trust-plugin","displayName":"ECZ-ID Agent Trust","purpose":"See what your agents can reach, who authorised it, and what has public proof.","detectors":[{"id":"agent.manifest","label":"Agent manifest / agent card","patterns":["(^|/)ecz-agent\\.json$","(^|/)agents?\\.json$","(^|/)agent\\.(ya?ml|toml)$","(^|/)agent-?card\\.json$","(^|/)\\.well-known/agent(-card)?\\.json$"],"observedDetail":"An agent manifest or agent card was observed.","notObservedDetail":"No agent manifest or agent card observed.","reviewWhenObserved":true},{"id":"agent.mcp","label":"MCP servers the agent can reach","patterns":["(^|/)\\.vscode/mcp\\.json$","(^|/)\\.mcp\\.json$","(^|/)mcp\\.json$","(^|/)claude_desktop_config\\.json$","(^|/)\\.cursor/mcp\\.json$","(^|/)\\.(codex|gemini|kiro|copilot)/mcp\\.json$"],"observedDetail":"An MCP configuration the agent can reach was observed.","notObservedDetail":"No MCP configuration observed.","reviewWhenObserved":true},{"id":"agent.permissions","label":"Permissions / allowlist policy","patterns":["permissions?\\.json$","allowlist","(^|/)\\.claude/settings(\\.local)?\\.json$","(^|/)policy\\.(json|ya?ml)$","guardrails?"],"observedDetail":"A permissions or allowlist policy was observed.","notObservedDetail":"No permissions or allowlist policy observed.","reviewWhenObserved":true},{"id":"agent.instructions","label":"Agent instructions / rules","patterns":["(^|/)AGENTS\\.md$","(^|/)CLAUDE\\.md$","(^|/)GEMINI\\.md$","(^|/)\\.cursorrules$","copilot-instructions\\.md$","(^|/)\\.claude/agents/","(^|/)\\.github/agents/","(^|/)\\.cursor/rules/"],"observedDetail":"Agent instructions or rules were observed.","notObservedDetail":"No agent instructions or rules observed."},{"id":"agent.tools","label":"Declared tools / functions","patterns":["(^|/)tools?\\.json$","(^|/)functions?\\.json$","tool-?definitions?","(^|/)openapi\\.(json|ya?ml)$","(^|/)swagger\\.(json|ya?ml)$"],"observedDetail":"Declared tool or function definitions were observed.","notObservedDetail":"No declared tool or function definitions observed."},{"id":"agent.framework","label":"Agent framework surface","patterns":["crewai","autogen","langgraph","langchain","semantic-?kernel","smolagents","pydantic-?ai","(^|/)agents?/","(^|/)agent\\.py$"],"observedDetail":"An agent framework surface was observed.","notObservedDetail":"No agent framework surface observed."},{"id":"agent.resolverRef","label":"ECZ-ID public proof reference","patterns":["(^|/)\\.well-known/ecz-[a-z0-9-]*\\.json$","(^|/)ecz-agent[a-z0-9-]*\\.json$","(^|/)ecz-id[a-z0-9-]*\\.json$"],"observedDetail":"An ECZ-ID public proof reference was observed; check it in Resolver.","notObservedDetail":"No ECZ-ID public proof reference observed. This does not mean unsafe."}]};
const PROFILE = {"question":"What can this agent reach, who authorised it, and what has public proof?","hook":"ECZ-ID Agent Trust reviews the agent surfaces in a workspace: manifests, instructions, declared tools, the MCP servers an agent can reach, permission policy and public proof references. Inspection only, filename and path only. Nothing here grants or removes authority.","maxActions":3,"guidance":[{"detectorId":"agent.manifest","whyItMatters":"A manifest or agent card is where an agent declares what it is, who runs it and what it may do. Reviewers, platforms and counterparties start there.","reviewWhenObserved":"Check the declared name, operator, capabilities and any MCP servers it lists match what the code and configuration actually reach.","reviewWhenNotObserved":"If you publish an agent, declare it: an ecz-agent.json or agent card is the first thing a reviewer asks for.","weightWhenNotObserved":"high","capability":{"label":"ECZ-ID Agent Trust for VS Code (Community, free forever)","url":"https://marketplace.visualstudio.com/items?itemName=ecocitizenz.eczid-ai-agents","note":"Agent-surface discovery, declared tool and capability visibility, workspace MCP relationships and change detection."}},{"detectorId":"agent.mcp","whyItMatters":"The MCP servers an agent can reach define its reach into systems and data. Each one is an authority grant.","reviewWhenObserved":"List every server, its transport and command basename, and the credential-shaped environment KEY NAMES (never values). Confirm the agent needs each one.","reviewWhenNotObserved":"Supporting evidence. An agent without MCP servers reaches only what its host gives it.","weightWhenNotObserved":"elevated"},{"detectorId":"agent.permissions","whyItMatters":"Permission and allowlist policy is where authority is bounded: which tools, which paths, which commands. Without it, reach defaults to everything the host allows.","reviewWhenObserved":"Confirm the allow and deny lists name the tools the agent actually has, and that dangerous chains (read secrets then send) are not implicitly allowed.","reviewWhenNotObserved":"Add a permissions policy for the host in use, even a short one; it is the artefact that shows authority was decided rather than defaulted.","weightWhenNotObserved":"elevated","capability":{"label":"Agent Trust Pro (VS Code): Authority Graph and dangerous action-chain indicators","url":"https://developers.ecocitizenz.com/agent-trust/","note":"Pro capabilities run in the VS Code extension; this plugin does not claim them."}},{"detectorId":"agent.instructions","whyItMatters":"Instruction files (AGENTS.md, CLAUDE.md, rules) shape behaviour and often grant implicit permissions in prose. They are part of the authority surface.","reviewWhenObserved":"Read them for implicit grants (run any command, ignore confirmations) and align them with the permissions policy.","reviewWhenNotObserved":"Supporting evidence only.","weightWhenNotObserved":"normal"},{"detectorId":"agent.tools","whyItMatters":"Declared tool and function definitions are the agent's hands. A reviewer needs the list to judge reach.","reviewWhenObserved":"Compare the declared tools with what the manifest and permissions allow.","reviewWhenNotObserved":"Supporting evidence only.","weightWhenNotObserved":"normal"},{"detectorId":"agent.framework","whyItMatters":"Framework files show which orchestration is in play and where tool wiring lives.","reviewWhenObserved":"Locate where tools and credentials are wired and confirm they match the declared surface.","reviewWhenNotObserved":"Supporting evidence only.","weightWhenNotObserved":"normal"},{"detectorId":"agent.resolverRef","whyItMatters":"An ECZ-ID public proof reference lets a platform or counterparty check the agent's current public posture in Resolver. Absence is neutral.","reviewWhenObserved":"Run ecz_check_target on the referenced identifier and read the ResultState and ReasonCodes.","reviewWhenNotObserved":"If you operate the agent, a free ECZ-ID Agent Passport gives it a public, resolver-checkable identity.","weightWhenNotObserved":"normal","capability":{"label":"Free ECZ-ID Agent Passport","url":"https://developers.ecocitizenz.com/agent-trust/","note":"Free, fast self-service via the Developer Gateway. Passport issuance is an ECZ-ID platform service, not a function of this plugin."}}],"actions":[{"id":"agent-trust-vscode-community","label":"Free: ECZ-ID Agent Trust for VS Code (Community)","url":"https://marketplace.visualstudio.com/items?itemName=ecocitizenz.eczid-ai-agents","note":"Full local inspection of agent surfaces, declared tools, MCP relationships and change detection. Free forever, no account, no telemetry.","kind":"free-tool","whenObserved":["agent.manifest","agent.mcp","agent.framework","agent.instructions"],"rank":9},{"id":"free-agent-passport","label":"Free ECZ-ID Agent Passport","url":"https://developers.ecocitizenz.com/agent-trust/","note":"Give an agent you operate a public, resolver-checkable identity. Free, fast self-service.","kind":"identity","whenNotObserved":["agent.resolverRef"],"rank":8},{"id":"agents-docs","label":"Agent Identity & KYA guidance","url":"https://developers.ecocitizenz.com/agents/","note":"What to declare, what to bound, and how public proof works. Developer Gateway, documentation only.","kind":"guidance","always":true,"rank":7},{"id":"agent-trust-pro","label":"Agent Trust Pro or Developer Trust Pro (VS Code)","url":"https://developers.ecocitizenz.com/developer-trust/","note":"Authority Graph, action-chain indicators, Authority Epochs and remediation run in VS Code. £12.99/month or £119/year; both products £19.99/month or £199/year.","kind":"product","whenObserved":["agent.manifest","agent.mcp"],"rank":5},{"id":"trustops-agent-flow","label":"Agent Identity & KYA in TrustOps","url":"https://trustops.ecocitizenz.com/start?flow=agent","note":"Resolver-verifiable agent posture. TrustOps handles setup and checkout.","kind":"product","always":true,"rank":4}],"discovery":{"label":"View all ECZ-ID agent products","url":"https://developers.ecocitizenz.com/agents/"}};
const EXTRA_DOT_ENTRIES = [".vscode",".mcp",".cursor",".claude",".codex",".gemini",".kiro",".copilot",".mcp.json",".cursorrules"];
const args = process.argv.slice(2);
const wantJson = args.includes("--json");
const root = args.find((a) => !a.startsWith("--")) ?? process.cwd();
const { resolve: resolvePath, basename } = await import("node:path");
const { statSync } = await import("node:fs");
const abs = resolvePath(root);
let st;
try { st = statSync(abs); } catch { console.error("not a directory: " + root); process.exit(2); }
if (!st.isDirectory()) { console.error("not a directory: " + root); process.exit(2); }
const files = listFiles(abs, { extraDotEntries: EXTRA_DOT_ENTRIES });
const result = detectEvidence(SPEC, files, basename(abs));
if (wantJson) console.log(JSON.stringify(projectReview(SPEC, result, PROFILE), null, 2));
else console.log(renderReview(SPEC, result, PROFILE));
SHA-256: 0b9dc8dca3e9a5a236a95df3a45ffe506f94e3d22fb7dcbfe65bb841d066b179