provenance-protocol 0.1.3 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,10 +1,13 @@
1
1
  {
2
2
  "name": "provenance-protocol",
3
- "version": "0.1.3",
4
- "description": "SDK for querying the Provenance agent identity index",
3
+ "version": "0.2.1",
4
+ "description": "The Provenance Protocol \u2014 open standard for AI agent identity \u2014 and its reference SDK",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
7
7
  "types": "./src/index.d.ts",
8
+ "bin": {
9
+ "provenance": "./src/cli.js"
10
+ },
8
11
  "exports": {
9
12
  ".": {
10
13
  "types": "./src/index.d.ts",
@@ -13,6 +16,10 @@
13
16
  "./keygen": {
14
17
  "types": "./src/keygen.d.ts",
15
18
  "import": "./src/keygen.js"
19
+ },
20
+ "./verify": {
21
+ "types": "./src/verify.d.ts",
22
+ "import": "./src/verify.js"
16
23
  }
17
24
  },
18
25
  "files": [
@@ -20,7 +27,13 @@
20
27
  "src/index.d.ts",
21
28
  "src/keygen.js",
22
29
  "src/keygen.d.ts",
23
- "README.md"
30
+ "src/verify.d.ts",
31
+ "src/verify.js",
32
+ "src/cli.js",
33
+ "README.md",
34
+ "SPEC.md",
35
+ "schema/provenance-0.1.json",
36
+ "test-vectors/"
24
37
  ],
25
38
  "keywords": [
26
39
  "ai-agent",
@@ -28,7 +41,10 @@
28
41
  "provenance",
29
42
  "trust",
30
43
  "llm",
31
- "verification"
44
+ "verification",
45
+ "ed25519",
46
+ "offline-verification",
47
+ "standard"
32
48
  ],
33
49
  "license": "MIT",
34
50
  "repository": {
@@ -38,5 +54,8 @@
38
54
  "homepage": "https://getprovenance.dev",
39
55
  "engines": {
40
56
  "node": ">=14.0.0"
57
+ },
58
+ "scripts": {
59
+ "test": "node test/verify.test.mjs"
41
60
  }
42
61
  }
@@ -0,0 +1,229 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://getprovenance.dev/schema/0.1.json",
4
+ "title": "PROVENANCE.yml",
5
+ "description": "Provenance Protocol v0.1 schema for AI agent identity files",
6
+ "type": "object",
7
+ "required": [
8
+ "provenance",
9
+ "name",
10
+ "description"
11
+ ],
12
+ "properties": {
13
+ "provenance": {
14
+ "type": "string",
15
+ "description": "Spec version",
16
+ "enum": [
17
+ "0.1"
18
+ ]
19
+ },
20
+ "name": {
21
+ "type": "string",
22
+ "description": "Human-readable name for this agent",
23
+ "minLength": 1,
24
+ "maxLength": 100
25
+ },
26
+ "description": {
27
+ "type": "string",
28
+ "description": "What this agent does, in plain language",
29
+ "minLength": 1,
30
+ "maxLength": 1000
31
+ },
32
+ "version": {
33
+ "type": "string",
34
+ "description": "Semantic version of the agent",
35
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+.*$"
36
+ },
37
+ "model": {
38
+ "type": "object",
39
+ "description": "The LLM powering this agent",
40
+ "properties": {
41
+ "provider": {
42
+ "type": "string",
43
+ "enum": [
44
+ "anthropic",
45
+ "openai",
46
+ "google",
47
+ "mistral",
48
+ "meta",
49
+ "local",
50
+ "other"
51
+ ],
52
+ "description": "LLM provider"
53
+ },
54
+ "model_id": {
55
+ "type": "string",
56
+ "description": "Specific model ID e.g. claude-sonnet-4-5"
57
+ }
58
+ },
59
+ "additionalProperties": false
60
+ },
61
+ "capabilities": {
62
+ "type": "array",
63
+ "description": "What this agent can do \u2014 standard vocabulary or domain:custom",
64
+ "items": {
65
+ "type": "string",
66
+ "pattern": "^[a-z][a-z0-9_-]*(:[a-z0-9_:-]+)?$"
67
+ },
68
+ "uniqueItems": true
69
+ },
70
+ "constraints": {
71
+ "type": "array",
72
+ "description": "Public commitments \u2014 what this agent will NEVER do",
73
+ "items": {
74
+ "type": "string",
75
+ "pattern": "^no:[a-z0-9_:-]+$"
76
+ },
77
+ "uniqueItems": true
78
+ },
79
+ "delegates": {
80
+ "type": "array",
81
+ "description": "Sub-agents this orchestrator spawns \u2014 required if delegate:agents capability declared",
82
+ "items": {
83
+ "type": "object",
84
+ "required": [
85
+ "provenance_id"
86
+ ],
87
+ "properties": {
88
+ "provenance_id": {
89
+ "type": "string",
90
+ "pattern": "^provenance:(github|npm|pypi|huggingface|clawmarket):.+$"
91
+ },
92
+ "purpose": {
93
+ "type": "string",
94
+ "maxLength": 200
95
+ }
96
+ },
97
+ "additionalProperties": false
98
+ }
99
+ },
100
+ "skills": {
101
+ "type": "array",
102
+ "description": "External skills this agent depends on",
103
+ "items": {
104
+ "type": "object",
105
+ "required": [
106
+ "id",
107
+ "source"
108
+ ],
109
+ "properties": {
110
+ "id": {
111
+ "type": "string"
112
+ },
113
+ "source": {
114
+ "type": "string",
115
+ "enum": [
116
+ "skillsmp",
117
+ "github",
118
+ "npm",
119
+ "custom"
120
+ ]
121
+ },
122
+ "url": {
123
+ "type": "string",
124
+ "format": "uri"
125
+ }
126
+ },
127
+ "additionalProperties": false
128
+ }
129
+ },
130
+ "runtime": {
131
+ "type": "object",
132
+ "description": "How this agent runs",
133
+ "properties": {
134
+ "type": {
135
+ "type": "string",
136
+ "enum": [
137
+ "task",
138
+ "persistent",
139
+ "scheduled"
140
+ ],
141
+ "description": "task: runs to completion | persistent: always on | scheduled: cron-like"
142
+ },
143
+ "trigger": {
144
+ "type": "string",
145
+ "enum": [
146
+ "api",
147
+ "webhook",
148
+ "schedule",
149
+ "event"
150
+ ],
151
+ "description": "What starts this agent"
152
+ }
153
+ },
154
+ "additionalProperties": false
155
+ },
156
+ "contact": {
157
+ "type": "object",
158
+ "description": "Who is responsible for this agent",
159
+ "properties": {
160
+ "name": {
161
+ "type": "string",
162
+ "minLength": 1
163
+ },
164
+ "url": {
165
+ "type": "string",
166
+ "format": "uri"
167
+ },
168
+ "email": {
169
+ "type": "string",
170
+ "format": "email"
171
+ }
172
+ },
173
+ "additionalProperties": false
174
+ },
175
+ "provenance_id": {
176
+ "type": "string",
177
+ "description": "Your Provenance identifier \u2014 links this file to your index entry",
178
+ "pattern": "^provenance:(github|npm|pypi|huggingface|clawmarket):.+$"
179
+ },
180
+ "identity": {
181
+ "type": "object",
182
+ "description": "Cryptographic identity. public_key alone advertises the key an agent will use to prove control live. Adding signature makes the declaration itself tamper-evident.",
183
+ "properties": {
184
+ "public_key": {
185
+ "type": "string",
186
+ "description": "Base64-encoded Ed25519 SPKI DER public key. Generate with: generateProvenanceKeyPair() from provenance-protocol/keygen"
187
+ },
188
+ "signature": {
189
+ "type": "string",
190
+ "description": "Base64 Ed25519 signature of '<provenance_id>:<public_key>' \u2014 proves you control the private key and that the declaration is unaltered. Optional: omit it when the key is published only for live challenge-response. Requires provenance_id."
191
+ },
192
+ "algorithm": {
193
+ "type": "string",
194
+ "enum": [
195
+ "ed25519"
196
+ ],
197
+ "description": "Signing algorithm. Always ed25519."
198
+ }
199
+ },
200
+ "required": [
201
+ "public_key"
202
+ ],
203
+ "additionalProperties": false
204
+ },
205
+ "ajp": {
206
+ "type": "object",
207
+ "description": "Agent Job Protocol endpoint \u2014 enables agent-to-agent job delegation",
208
+ "properties": {
209
+ "endpoint": {
210
+ "type": "string",
211
+ "format": "uri",
212
+ "description": "Base URL for your AJP implementation. Must accept POST /jobs, GET /jobs/:id, POST /jobs/:id/ack"
213
+ },
214
+ "version": {
215
+ "type": "string",
216
+ "enum": [
217
+ "0.1"
218
+ ],
219
+ "description": "AJP spec version"
220
+ }
221
+ },
222
+ "required": [
223
+ "endpoint"
224
+ ],
225
+ "additionalProperties": false
226
+ }
227
+ },
228
+ "additionalProperties": false
229
+ }
package/src/cli.js ADDED
@@ -0,0 +1,196 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * provenance CLI
4
+ *
5
+ * npx provenance keygen
6
+ * npx provenance register --id provenance:github:org/agent --url https://...
7
+ * npx provenance status provenance:github:org/agent
8
+ * npx provenance revoke --id provenance:github:org/agent
9
+ */
10
+
11
+ import { writeFileSync, readFileSync, existsSync } from 'fs';
12
+ import { execSync } from 'child_process';
13
+ import { generateProvenanceKeyPair, signChallenge, signForProvenance, signRevocation } from './keygen.js';
14
+
15
+ const API = 'https://getprovenance.dev/api/agents';
16
+ const KEY_FILE = '.provenance-key';
17
+
18
+ function readPrivateKey() {
19
+ if (process.env.PROVENANCE_PRIVATE_KEY) return process.env.PROVENANCE_PRIVATE_KEY.trim();
20
+ if (existsSync(KEY_FILE)) return readFileSync(KEY_FILE, 'utf8').trim();
21
+ console.error(`No private key found. Set PROVENANCE_PRIVATE_KEY or run: npx provenance keygen`);
22
+ process.exit(1);
23
+ }
24
+
25
+ function parseArgs(argv) {
26
+ const args = {};
27
+ for (let i = 0; i < argv.length; i++) {
28
+ if (argv[i].startsWith('--')) {
29
+ args[argv[i].slice(2)] = argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[++i] : true;
30
+ } else {
31
+ args._ = args._ || [];
32
+ args._.push(argv[i]);
33
+ }
34
+ }
35
+ return args;
36
+ }
37
+
38
+ const [,, command, ...rest] = process.argv;
39
+ const args = parseArgs(rest);
40
+
41
+ // ── keygen ──────────────────────────────────────────────────────────────────
42
+
43
+ if (command === 'keygen') {
44
+ const { publicKey, privateKey } = generateProvenanceKeyPair();
45
+
46
+ writeFileSync(KEY_FILE, privateKey, { mode: 0o600 });
47
+ try { execSync(`grep -qxF "${KEY_FILE}" .gitignore 2>/dev/null || echo "${KEY_FILE}" >> .gitignore`); } catch {}
48
+
49
+ console.log(`\nKeypair generated.\n`);
50
+ console.log(`Private key → ${KEY_FILE} (chmod 600, added to .gitignore)`);
51
+ console.log(`\nPublic key (add to PROVENANCE.yml):\n`);
52
+ console.log(` identity:`);
53
+ console.log(` public_key: "${publicKey}"`);
54
+ console.log(` algorithm: ed25519`);
55
+ console.log(`\nNext: npx provenance register --id provenance:<platform>:<org>/<name>\n`);
56
+ process.exit(0);
57
+ }
58
+
59
+ // ── register ─────────────────────────────────────────────────────────────────
60
+
61
+ if (command === 'register') {
62
+ const id = args.id || args._?.[0];
63
+ if (!id) { console.error('Usage: npx provenance register --id provenance:<platform>:<org>/<name> [options]'); process.exit(1); }
64
+
65
+ const privateKey = readPrivateKey();
66
+ const { publicKey } = (() => {
67
+ // derive public key from private key for display — we just use the stored one
68
+ // We can't derive public from private easily here, so read from PROVENANCE.yml or require --public-key
69
+ return { publicKey: args['public-key'] || null };
70
+ })();
71
+
72
+ // If no public key arg, re-generate won't work — need the stored public key
73
+ // Best path: require keygen was run, read public key from PROVENANCE.yml if present
74
+ let pubKey = args['public-key'];
75
+ if (!pubKey) {
76
+ if (existsSync('PROVENANCE.yml')) {
77
+ const yml = readFileSync('PROVENANCE.yml', 'utf8');
78
+ const match = yml.match(/public_key:\s*["']?([A-Za-z0-9+/=]+)["']?/);
79
+ if (match) pubKey = match[1];
80
+ }
81
+ }
82
+ if (!pubKey) {
83
+ console.error('Public key required. Pass --public-key or add identity.public_key to PROVENANCE.yml first.');
84
+ process.exit(1);
85
+ }
86
+
87
+ const signed_challenge = signChallenge(privateKey, id, 'REGISTER');
88
+
89
+ const body = {
90
+ provenance_id: id,
91
+ public_key: pubKey,
92
+ signed_challenge,
93
+ ...(args.url ? { url: args.url } : {}),
94
+ ...(args.name ? { name: args.name } : {}),
95
+ ...(args.description ? { description: args.description } : {}),
96
+ ...(args.capabilities ? { capabilities: args.capabilities.split(',').map(s => s.trim()) } : {}),
97
+ ...(args.constraints ? { constraints: args.constraints.split(',').map(s => s.trim()) } : {}),
98
+ ...(args.model ? { model_provider: args.model } : {}),
99
+ ...(args['model-id'] ? { model_id: args['model-id'] } : {}),
100
+ };
101
+
102
+ try {
103
+ const res = await fetch(`${API}/register`, {
104
+ method: 'POST',
105
+ headers: { 'Content-Type': 'application/json' },
106
+ body: JSON.stringify(body),
107
+ });
108
+ const data = await res.json();
109
+ if (!res.ok) {
110
+ console.error(`\nRegistration failed: ${data.error}`);
111
+ if (data.hint) console.error(`Hint: ${data.hint}`);
112
+ process.exit(1);
113
+ }
114
+ console.log(`\n${data.created ? 'Registered' : 'Updated'}: ${id}`);
115
+ console.log(`identity: ${data.agent?.identity}`);
116
+ console.log(`profile: https://getprovenance.dev/agent/${id.replace('provenance:', '').replace(':', '/')}\n`);
117
+ } catch (e) {
118
+ console.error('Network error:', e.message);
119
+ process.exit(1);
120
+ }
121
+ process.exit(0);
122
+ }
123
+
124
+ // ── status ───────────────────────────────────────────────────────────────────
125
+
126
+ if (command === 'status') {
127
+ const id = args._?.[0] || args.id;
128
+ if (!id) { console.error('Usage: npx provenance status <provenance_id>'); process.exit(1); }
129
+
130
+ try {
131
+ const res = await fetch(`${API}/register?provenance_id=${encodeURIComponent(id)}`);
132
+ const data = await res.json();
133
+ if (!data.registered) { console.log(`\nNot registered: ${id}\n`); process.exit(0); }
134
+ const a = data.agent;
135
+ console.log(`\n${id}`);
136
+ console.log(`name: ${a.name || '—'}`);
137
+ console.log(`identity: ${a.identity}`);
138
+ console.log(`status: ${a.status}`);
139
+ console.log(`profile: https://getprovenance.dev/agent/${id.replace('provenance:', '').replace(':', '/')}\n`);
140
+ } catch (e) {
141
+ console.error('Network error:', e.message);
142
+ process.exit(1);
143
+ }
144
+ process.exit(0);
145
+ }
146
+
147
+ // ── revoke ───────────────────────────────────────────────────────────────────
148
+
149
+ if (command === 'revoke') {
150
+ const id = args.id || args._?.[0];
151
+ if (!id) { console.error('Usage: npx provenance revoke --id <provenance_id>'); process.exit(1); }
152
+
153
+ const privateKey = readPrivateKey();
154
+ const signed_challenge = signRevocation(privateKey, id);
155
+
156
+ try {
157
+ const res = await fetch(`${API}/revoke`, {
158
+ method: 'POST',
159
+ headers: { 'Content-Type': 'application/json' },
160
+ body: JSON.stringify({ provenance_id: id, signed_challenge }),
161
+ });
162
+ const data = await res.json();
163
+ if (!res.ok) { console.error(`Revocation failed: ${data.error}`); process.exit(1); }
164
+ console.log(`\nKey revoked for ${id}.`);
165
+ console.log(`Run npx provenance keygen && npx provenance register --id ${id} to re-register with a new key.\n`);
166
+ } catch (e) {
167
+ console.error('Network error:', e.message);
168
+ process.exit(1);
169
+ }
170
+ process.exit(0);
171
+ }
172
+
173
+ // ── help ─────────────────────────────────────────────────────────────────────
174
+
175
+ console.log(`
176
+ provenance <command>
177
+
178
+ keygen Generate an Ed25519 keypair
179
+ register --id <id> [options] Register or update your agent
180
+ status <id> Check registration status
181
+ revoke --id <id> Revoke your registered key
182
+
183
+ register options:
184
+ --id provenance:<platform>:<org>/<name> (required)
185
+ --url URL to your PROVENANCE.yml (for independent verification)
186
+ --name Agent display name
187
+ --description One-sentence description
188
+ --capabilities read:web,write:summaries
189
+ --constraints no:pii,no:financial:transact
190
+ --model anthropic / openai / etc.
191
+ --model-id claude-sonnet-4-6 / gpt-4o / etc.
192
+ --public-key Base64 public key (auto-read from PROVENANCE.yml if present)
193
+
194
+ Full docs: https://getprovenance.dev/docs
195
+ `);
196
+ process.exit(0);
package/src/index.d.ts CHANGED
@@ -8,6 +8,7 @@ export interface TrustProfile {
8
8
  platform?: string;
9
9
  name?: string;
10
10
  declared?: boolean;
11
+ identity_verified?: boolean;
11
12
  confidence?: number;
12
13
  age_days?: number | null;
13
14
  capabilities?: string[];
@@ -45,6 +46,8 @@ export interface VerifyResult {
45
46
 
46
47
  export interface GateOptions {
47
48
  requireDeclared?: boolean;
49
+ /** Require identity_verified: true — agent must have cryptographic proof of key ownership. */
50
+ requireVerified?: boolean;
48
51
  requireConstraints?: string[];
49
52
  requireCapabilities?: string[];
50
53
  requireClean?: boolean;
package/src/index.js CHANGED
@@ -45,6 +45,35 @@ async function _verifyEd25519(publicKeyBase64, signatureBase64, message) {
45
45
  }
46
46
 
47
47
  const DEFAULT_API = 'https://getprovenance.dev';
48
+
49
+ const SEVERITY_ORDER = ['low', 'medium', 'high', 'critical'];
50
+
51
+ // Returns a failure reason string if clean check fails, null if passes.
52
+ // requireClean: true | false | { minSeverity: 'low'|'medium'|'high'|'critical' }
53
+ function _evaluateClean(requireClean, trust) {
54
+ if (!requireClean) return null;
55
+ if (trust.incidents === 0) return null;
56
+
57
+ // true = block on any open incident
58
+ if (requireClean === true) {
59
+ return `Agent has ${trust.incidents} open incident(s)`;
60
+ }
61
+
62
+ // { minSeverity } = block only if any open incident meets or exceeds that severity
63
+ if (requireClean.minSeverity) {
64
+ const threshold = SEVERITY_ORDER.indexOf(requireClean.minSeverity);
65
+ const incidents = trust.incidents_detail || [];
66
+ const blocking = incidents.filter(inc =>
67
+ SEVERITY_ORDER.indexOf(inc.severity || 'medium') >= threshold
68
+ );
69
+ if (blocking.length > 0) {
70
+ return `Agent has ${blocking.length} open incident(s) at or above severity '${requireClean.minSeverity}'`;
71
+ }
72
+ return null;
73
+ }
74
+
75
+ return null;
76
+ }
48
77
  const DEFAULT_CACHE_TTL = 300; // 5 minutes
49
78
 
50
79
  // Simple LRU cache
@@ -106,7 +135,7 @@ export class Provenance {
106
135
  * // confidence: 0.9, — internal signal, use declared/identity_verified for trust decisions
107
136
  * // capabilities: ['read:web', 'write:summaries'],
108
137
  * // constraints: ['no:financial:transact', 'no:pii'],
109
- * // incidents: 0,
138
+ * // incidents: 0, — open/investigating only; resolved don't block gate()
110
139
  * // model: { provider: 'anthropic', model_id: 'claude-sonnet-4-5' },
111
140
  * // status: 'active',
112
141
  * // }
@@ -141,6 +170,8 @@ export class Provenance {
141
170
  capabilities: data.capabilities || [],
142
171
  constraints: data.constraints || [],
143
172
  incidents: data.incident_count || 0,
173
+ incidents_detail: data.incidents || [],
174
+ resolved_incidents: data.resolved_incidents || [],
144
175
  model: data.model || null,
145
176
  status: data.status || 'unknown',
146
177
  first_seen: data.timestamps?.first_seen || null,
@@ -260,11 +291,18 @@ export class Provenance {
260
291
  * Run all your trust requirements in one call.
261
292
  * Returns { allowed, reason, trust }.
262
293
  *
294
+ * requireClean accepts:
295
+ * - true block on any open incident (default)
296
+ * - false ignore incidents entirely
297
+ * - { minSeverity } block only if any open incident meets or exceeds severity
298
+ * severity order: low < medium < high < critical
299
+ * e.g. { minSeverity: 'high' } allows low/medium incidents
300
+ *
263
301
  * Example:
264
302
  * const result = await provenance.gate('provenance:github:alice/agent', {
265
303
  * requireDeclared: true,
266
304
  * requireConstraints: ['no:financial:transact', 'no:pii'],
267
- * requireClean: true,
305
+ * requireClean: { minSeverity: 'high' },
268
306
  * requireMinAge: 30,
269
307
  * requireMinConfidence: 0.7,
270
308
  * });
@@ -275,6 +313,7 @@ export class Provenance {
275
313
  */
276
314
  async gate(provenanceId, {
277
315
  requireDeclared = false,
316
+ requireVerified = false,
278
317
  requireConstraints = [],
279
318
  requireCapabilities = [],
280
319
  requireClean = true,
@@ -321,8 +360,12 @@ export class Provenance {
321
360
  if (requireDeclared && !trust.declared) {
322
361
  return { allowed: false, reason: 'Agent has not declared a PROVENANCE.yml file', trust };
323
362
  }
324
- if (requireClean && trust.incidents > 0) {
325
- return { allowed: false, reason: `Agent has ${trust.incidents} open incident(s)`, trust };
363
+ if (requireVerified && !trust.identity_verified) {
364
+ return { allowed: false, reason: 'Agent identity is not cryptographically verified', trust };
365
+ }
366
+ if (requireClean) {
367
+ const blocked = _evaluateClean(requireClean, trust);
368
+ if (blocked) return { allowed: false, reason: blocked, trust };
326
369
  }
327
370
  if (requireMinConfidence && trust.confidence < requireMinConfidence) {
328
371
  return { allowed: false, reason: `Agent confidence ${trust.confidence} below required ${requireMinConfidence}`, trust };
@@ -466,6 +509,7 @@ export class Provenance {
466
509
  _evaluateGate(trust, options) {
467
510
  const {
468
511
  requireDeclared = false,
512
+ requireVerified = false,
469
513
  requireConstraints = [],
470
514
  requireCapabilities = [],
471
515
  requireClean = true,
@@ -482,6 +526,9 @@ export class Provenance {
482
526
  if (requireDeclared && !trust.declared) {
483
527
  return { allowed: false, reason: 'Agent has not declared a PROVENANCE.yml file', trust };
484
528
  }
529
+ if (requireVerified && !trust.identity_verified) {
530
+ return { allowed: false, reason: 'Agent identity is not cryptographically verified', trust };
531
+ }
485
532
  for (const c of requireConstraints) {
486
533
  if (!trust.constraints?.includes(c)) {
487
534
  return { allowed: false, reason: `Agent has not committed to constraint: ${c}`, trust };
@@ -492,8 +539,9 @@ export class Provenance {
492
539
  return { allowed: false, reason: `Agent does not have capability: ${c}`, trust };
493
540
  }
494
541
  }
495
- if (requireClean && trust.incidents > 0) {
496
- return { allowed: false, reason: `Agent has ${trust.incidents} open incident(s)`, trust };
542
+ if (requireClean) {
543
+ const blocked = _evaluateClean(requireClean, trust);
544
+ if (blocked) return { allowed: false, reason: blocked, trust };
497
545
  }
498
546
  if (requireMinAge > 0 && (trust.age_days || 0) < requireMinAge) {
499
547
  return { allowed: false, reason: `Agent is only ${trust.age_days || 0} days old (minimum: ${requireMinAge})`, trust };