provenance-protocol 0.1.4 → 0.2.2
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/README.md +58 -8
- package/SPEC.md +483 -0
- package/package.json +19 -4
- package/schema/provenance-0.1.json +229 -0
- package/src/cli.js +249 -151
- package/src/index.d.ts +3 -0
- package/src/index.js +54 -6
- package/src/verify.d.ts +85 -0
- package/src/verify.js +277 -0
- package/test-vectors/README.md +52 -0
- package/test-vectors/signatures-0.1.json +80 -0
package/src/cli.js
CHANGED
|
@@ -1,196 +1,294 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* provenance CLI
|
|
3
|
+
* provenance — Provenance Protocol identity CLI (the `provenance` bin of provenance-protocol)
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* provenance keygen
|
|
7
|
+
* provenance register --id <id> --url <url> [options]
|
|
8
|
+
* provenance status <id>
|
|
9
|
+
* provenance validate [file]
|
|
10
|
+
* provenance revoke --id <id> [--private-key <key>]
|
|
9
11
|
*/
|
|
10
12
|
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
13
|
+
import { createPrivateKey, createPublicKey, generateKeyPairSync, sign as nodeSign } from 'crypto';
|
|
14
|
+
import { readFileSync, existsSync } from 'fs';
|
|
15
|
+
import { resolve } from 'path';
|
|
16
|
+
import { createRequire } from 'module';
|
|
14
17
|
|
|
15
|
-
const API
|
|
16
|
-
const
|
|
18
|
+
const API = process.env.PROVENANCE_API_URL || 'https://getprovenance.dev';
|
|
19
|
+
const VERSION = createRequire(import.meta.url)('../package.json').version;
|
|
17
20
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
21
|
+
// ── Colours ───────────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
const c = {
|
|
24
|
+
reset: '\x1b[0m', dim: '\x1b[2m', bold: '\x1b[1m',
|
|
25
|
+
green: '\x1b[32m', amber: '\x1b[33m', red: '\x1b[31m', white: '\x1b[97m',
|
|
26
|
+
};
|
|
27
|
+
const ok = s => `${c.green}✓${c.reset} ${s}`;
|
|
28
|
+
const err = s => `${c.red}✗${c.reset} ${s}`;
|
|
29
|
+
const dim = s => `${c.dim}${s}${c.reset}`;
|
|
30
|
+
const hi = s => `${c.white}${c.bold}${s}${c.reset}`;
|
|
31
|
+
const amb = s => `${c.amber}${s}${c.reset}`;
|
|
32
|
+
|
|
33
|
+
// ── Arg parsing ───────────────────────────────────────────────────────────────
|
|
24
34
|
|
|
25
35
|
function parseArgs(argv) {
|
|
26
|
-
const args = {};
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
36
|
+
const args = { _: [] };
|
|
37
|
+
let i = 0;
|
|
38
|
+
while (i < argv.length) {
|
|
39
|
+
const a = argv[i];
|
|
40
|
+
if (a.startsWith('--')) {
|
|
41
|
+
const key = a.slice(2);
|
|
42
|
+
const next = argv[i + 1];
|
|
43
|
+
if (next && !next.startsWith('--')) { args[key] = next; i += 2; }
|
|
44
|
+
else { args[key] = true; i++; }
|
|
45
|
+
} else { args._.push(a); i++; }
|
|
34
46
|
}
|
|
35
47
|
return args;
|
|
36
48
|
}
|
|
37
49
|
|
|
38
|
-
|
|
39
|
-
const args = parseArgs(rest);
|
|
50
|
+
// ── Crypto helpers ────────────────────────────────────────────────────────────
|
|
40
51
|
|
|
41
|
-
|
|
52
|
+
function generateKeyPair() {
|
|
53
|
+
const { publicKey, privateKey } = generateKeyPairSync('ed25519', {
|
|
54
|
+
publicKeyEncoding: { type: 'spki', format: 'der' },
|
|
55
|
+
privateKeyEncoding: { type: 'pkcs8', format: 'der' },
|
|
56
|
+
});
|
|
57
|
+
return {
|
|
58
|
+
publicKey: Buffer.from(publicKey).toString('base64'),
|
|
59
|
+
privateKey: Buffer.from(privateKey).toString('base64'),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
42
62
|
|
|
43
|
-
|
|
44
|
-
const {
|
|
63
|
+
function signMessage(privateKeyBase64, message) {
|
|
64
|
+
const key = createPrivateKey({ key: Buffer.from(privateKeyBase64, 'base64'), format: 'der', type: 'pkcs8' });
|
|
65
|
+
return nodeSign(null, Buffer.from(message, 'utf8'), key).toString('base64');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function derivePublicKey(privateKeyBase64) {
|
|
69
|
+
const priv = createPrivateKey({ key: Buffer.from(privateKeyBase64, 'base64'), format: 'der', type: 'pkcs8' });
|
|
70
|
+
return Buffer.from(createPublicKey(priv).export({ type: 'spki', format: 'der' })).toString('base64');
|
|
71
|
+
}
|
|
45
72
|
|
|
46
|
-
|
|
47
|
-
try { execSync(`grep -qxF "${KEY_FILE}" .gitignore 2>/dev/null || echo "${KEY_FILE}" >> .gitignore`); } catch {}
|
|
73
|
+
// ── Commands ──────────────────────────────────────────────────────────────────
|
|
48
74
|
|
|
49
|
-
|
|
50
|
-
console.log(
|
|
51
|
-
|
|
75
|
+
async function cmdKeygen() {
|
|
76
|
+
console.log(`\n${amb('Generating Ed25519 keypair...')}\n`);
|
|
77
|
+
const { publicKey, privateKey } = generateKeyPair();
|
|
78
|
+
|
|
79
|
+
console.log(`${hi('Public key')} ${dim('(add to PROVENANCE.yml identity.public_key)')}`);
|
|
80
|
+
console.log(`${c.green}${publicKey}${c.reset}\n`);
|
|
81
|
+
console.log(`${hi('Private key')} ${dim('(store as PROVENANCE_PRIVATE_KEY — never commit)')}`);
|
|
82
|
+
console.log(`${c.amber}${privateKey}${c.reset}\n`);
|
|
83
|
+
console.log(dim('─'.repeat(60)));
|
|
84
|
+
console.log(dim('Add to your environment:'));
|
|
85
|
+
console.log(` PROVENANCE_PRIVATE_KEY=${privateKey}\n`);
|
|
86
|
+
console.log(dim('Add to PROVENANCE.yml:'));
|
|
52
87
|
console.log(` identity:`);
|
|
53
88
|
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);
|
|
89
|
+
console.log(` algorithm: ed25519\n`);
|
|
57
90
|
}
|
|
58
91
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
}
|
|
92
|
+
async function cmdRegister(args) {
|
|
93
|
+
const id = args.id;
|
|
94
|
+
const url = args.url;
|
|
95
|
+
const name = args.name;
|
|
96
|
+
const description = args.description || args.desc;
|
|
97
|
+
const caps = args.capabilities ? args.capabilities.split(',').map(s => s.trim()) : [];
|
|
98
|
+
const cons = args.constraints ? args.constraints.split(',').map(s => s.trim()) : [];
|
|
99
|
+
const model = args.model;
|
|
100
|
+
const modelId = args['model-id'];
|
|
101
|
+
const ajpEndpoint = args['ajp-endpoint'];
|
|
102
|
+
const privateKey = args['private-key'] || process.env.PROVENANCE_PRIVATE_KEY;
|
|
103
|
+
|
|
104
|
+
if (!id) { console.error(err('--id required')); process.exit(1); }
|
|
105
|
+
if (!url) { console.error(err('--url required')); process.exit(1); }
|
|
86
106
|
|
|
87
|
-
|
|
107
|
+
console.log(`\n${amb('Registering')} ${hi(id)}...\n`);
|
|
108
|
+
|
|
109
|
+
let pubKey, signedChallenge;
|
|
110
|
+
if (privateKey) {
|
|
111
|
+
pubKey = args['public-key'] || process.env.PROVENANCE_PUBLIC_KEY || derivePublicKey(privateKey);
|
|
112
|
+
signedChallenge = signMessage(privateKey, `${id}:REGISTER`);
|
|
113
|
+
console.log(ok('Signing with private key'));
|
|
114
|
+
}
|
|
88
115
|
|
|
89
116
|
const body = {
|
|
90
|
-
provenance_id: id,
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
...(
|
|
94
|
-
...(
|
|
95
|
-
...(
|
|
96
|
-
...(
|
|
97
|
-
...(
|
|
98
|
-
...(
|
|
99
|
-
...(
|
|
117
|
+
provenance_id: id, url,
|
|
118
|
+
...(name && { name }),
|
|
119
|
+
...(description && { description }),
|
|
120
|
+
...(caps.length && { capabilities: caps }),
|
|
121
|
+
...(cons.length && { constraints: cons }),
|
|
122
|
+
...(model && { model_provider: model }),
|
|
123
|
+
...(modelId && { model_id: modelId }),
|
|
124
|
+
...(ajpEndpoint && { ajp_endpoint: ajpEndpoint }),
|
|
125
|
+
...(pubKey && { public_key: pubKey }),
|
|
126
|
+
...(signedChallenge && { signed_challenge: signedChallenge }),
|
|
100
127
|
};
|
|
101
128
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
console.log(
|
|
116
|
-
|
|
117
|
-
} catch (e) {
|
|
118
|
-
console.error('Network error:', e.message);
|
|
119
|
-
process.exit(1);
|
|
120
|
-
}
|
|
121
|
-
process.exit(0);
|
|
129
|
+
const res = await fetch(`${API}/api/agents/register`, {
|
|
130
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body),
|
|
131
|
+
});
|
|
132
|
+
const data = await res.json();
|
|
133
|
+
|
|
134
|
+
if (!res.ok) { console.error(err(data.error || `HTTP ${res.status}`)); process.exit(1); }
|
|
135
|
+
|
|
136
|
+
const agent = data.agent || data;
|
|
137
|
+
console.log(ok(data.created ? 'Agent registered' : 'Agent updated'));
|
|
138
|
+
console.log(` ${dim('confidence:')} ${c.green}${agent.confidence}${c.reset}`);
|
|
139
|
+
console.log(` ${dim('identity_verified:')} ${agent.identity_verified ? c.green + 'true' : c.amber + 'false'}${c.reset}`);
|
|
140
|
+
if (ajpEndpoint) console.log(` ${dim('ajp_endpoint:')} ${ajpEndpoint}`);
|
|
141
|
+
if (!agent.identity_verified)
|
|
142
|
+
console.log(`\n${c.amber}Tip:${c.reset} Run with ${hi('--private-key')} to get identity_verified status`);
|
|
143
|
+
console.log();
|
|
122
144
|
}
|
|
123
145
|
|
|
124
|
-
|
|
146
|
+
async function cmdStatus(args) {
|
|
147
|
+
const id = args._[1];
|
|
148
|
+
if (!id) { console.error(err('Usage: provenance status <provenance_id>')); process.exit(1); }
|
|
125
149
|
|
|
126
|
-
|
|
127
|
-
const id = args._?.[0] || args.id;
|
|
128
|
-
if (!id) { console.error('Usage: npx provenance status <provenance_id>'); process.exit(1); }
|
|
150
|
+
console.log(`\n${amb('Checking')} ${hi(id)}...\n`);
|
|
129
151
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
}
|
|
152
|
+
const res = await fetch(`${API}/api/agent/${id.replace('provenance:', '').replace(':', '/')}`);
|
|
153
|
+
const data = await res.json();
|
|
154
|
+
|
|
155
|
+
if (!res.ok || data.error) { console.error(err(data.error || 'Not found')); process.exit(1); }
|
|
146
156
|
|
|
147
|
-
|
|
157
|
+
const trust = Math.round((data.confidence || 0) * 100);
|
|
158
|
+
const trustColor = trust >= 80 ? c.green : trust >= 50 ? c.amber : c.red;
|
|
148
159
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
160
|
+
console.log(`${hi(data.name || id)}`);
|
|
161
|
+
console.log(`${dim(data.provenance_id)}\n`);
|
|
162
|
+
console.log(`${dim('Trust score:')} ${trustColor}${trust}/100${c.reset}`);
|
|
163
|
+
console.log(`${dim('Declared:')} ${data.declared ? c.green + 'yes' : c.amber + 'no'}${c.reset}`);
|
|
164
|
+
console.log(`${dim('Identity verified:')} ${data.identity_verified ? c.green + 'yes' : c.amber + 'no'}${c.reset}`);
|
|
165
|
+
console.log(`${dim('AJP endpoint:')} ${data.ajp?.endpoint ? c.green + data.ajp.endpoint : c.dim + 'not set'}${c.reset}`);
|
|
166
|
+
console.log(`${dim('Incidents:')} ${(data.incident_count || 0) === 0 ? c.green + '0' : c.red + data.incident_count}${c.reset}`);
|
|
167
|
+
if (data.capabilities?.length) console.log(`${dim('Capabilities:')} ${data.capabilities.join(', ')}`);
|
|
168
|
+
if (data.constraints?.length) console.log(`${dim('Constraints:')} ${data.constraints.join(', ')}`);
|
|
152
169
|
|
|
153
|
-
|
|
154
|
-
const
|
|
170
|
+
console.log();
|
|
171
|
+
for (const [pass, label] of [
|
|
172
|
+
[data.declared, 'PROVENANCE.yml declared'],
|
|
173
|
+
[data.identity_verified, 'Identity verified (Ed25519)'],
|
|
174
|
+
[!!data.ajp?.endpoint, 'AJP endpoint configured'],
|
|
175
|
+
[(data.incident_count||0)===0, 'No open incidents'],
|
|
176
|
+
]) console.log(` ${pass ? ok(label) : dim('○ ' + label)}`);
|
|
177
|
+
console.log();
|
|
178
|
+
}
|
|
155
179
|
|
|
180
|
+
async function cmdValidate(args) {
|
|
181
|
+
const file = args._[1] || 'PROVENANCE.yml';
|
|
182
|
+
const path = resolve(process.cwd(), file);
|
|
183
|
+
if (!existsSync(path)) { console.error(err(`File not found: ${path}`)); process.exit(1); }
|
|
184
|
+
|
|
185
|
+
console.log(`\n${amb('Validating')} ${hi(file)}...\n`);
|
|
186
|
+
const content = readFileSync(path, 'utf8');
|
|
187
|
+
|
|
188
|
+
// "Could not check" and "checked, and it is invalid" must not look alike:
|
|
189
|
+
// this runs in CI, where a validator that cannot reach the service and
|
|
190
|
+
// reports failure would be indistinguishable from a broken declaration.
|
|
191
|
+
// Transport trouble exits 2; a genuinely invalid file exits 1.
|
|
192
|
+
let data;
|
|
156
193
|
try {
|
|
157
|
-
const res = await fetch(`${API}/
|
|
158
|
-
method: 'POST',
|
|
159
|
-
|
|
160
|
-
|
|
194
|
+
const res = await fetch(`${API}/api/mcp`, {
|
|
195
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
196
|
+
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/call',
|
|
197
|
+
params: { name: 'validate_provenance_yml', arguments: { content } } }),
|
|
161
198
|
});
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
199
|
+
if (!res.ok) throw new Error(`validation service returned HTTP ${res.status}`);
|
|
200
|
+
const rpc = await res.json();
|
|
201
|
+
const text = rpc.result?.content?.[0]?.text;
|
|
202
|
+
if (!text) throw new Error('validation service returned no result');
|
|
203
|
+
data = JSON.parse(text);
|
|
204
|
+
if (typeof data.valid !== 'boolean') throw new Error('validation service returned no verdict');
|
|
166
205
|
} catch (e) {
|
|
167
|
-
console.error(
|
|
168
|
-
|
|
206
|
+
console.error(err(`Could not validate: ${e.message}`));
|
|
207
|
+
console.error(dim(`The file was not checked. This is not a validation failure.`));
|
|
208
|
+
console.log();
|
|
209
|
+
process.exit(2);
|
|
169
210
|
}
|
|
170
|
-
|
|
211
|
+
|
|
212
|
+
if (data.valid) console.log(ok('Valid PROVENANCE.yml'));
|
|
213
|
+
else { console.log(err('Validation failed')); for (const e of data.errors || []) console.log(` ${c.red}✗${c.reset} ${e}`); }
|
|
214
|
+
for (const w of data.warnings || []) console.log(` ${c.amber}⚠${c.reset} ${w}`);
|
|
215
|
+
console.log();
|
|
216
|
+
|
|
217
|
+
// Exit non-zero so `provenance validate` can gate a pipeline. It previously
|
|
218
|
+
// exited 0 on an invalid file, which made every CI check that used it pass.
|
|
219
|
+
if (!data.valid) process.exit(1);
|
|
171
220
|
}
|
|
172
221
|
|
|
173
|
-
|
|
222
|
+
async function cmdRevoke(args) {
|
|
223
|
+
const id = args.id;
|
|
224
|
+
const privateKey = args['private-key'] || process.env.PROVENANCE_PRIVATE_KEY;
|
|
225
|
+
if (!id) { console.error(err('--id required')); process.exit(1); }
|
|
226
|
+
if (!privateKey) { console.error(err('--private-key or PROVENANCE_PRIVATE_KEY required')); process.exit(1); }
|
|
174
227
|
|
|
175
|
-
console.log(`
|
|
176
|
-
provenance <command>
|
|
228
|
+
console.log(`\n${c.red}Revoking identity for${c.reset} ${hi(id)}...\n`);
|
|
177
229
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
230
|
+
const res = await fetch(`${API}/api/agents/revoke`, {
|
|
231
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
232
|
+
body: JSON.stringify({ provenance_id: id, signed_challenge: signMessage(privateKey, `${id}:REVOKE`) }),
|
|
233
|
+
});
|
|
234
|
+
const data = await res.json();
|
|
235
|
+
if (!res.ok || !data.success) { console.error(err(data.error || `HTTP ${res.status}`)); process.exit(1); }
|
|
182
236
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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)
|
|
237
|
+
console.log(ok('Identity revoked'));
|
|
238
|
+
console.log(dim('Run `provenance register` with a new keypair to re-establish.\n'));
|
|
239
|
+
}
|
|
193
240
|
|
|
194
|
-
|
|
241
|
+
function cmdHelp() {
|
|
242
|
+
console.log(`
|
|
243
|
+
${hi('provenance')} ${dim(`v${VERSION}`)} — Provenance Protocol identity CLI
|
|
244
|
+
|
|
245
|
+
${amb('Commands:')}
|
|
246
|
+
${hi('keygen')} Generate an Ed25519 keypair
|
|
247
|
+
${hi('register')} --id <id> --url <url> Register or update your agent
|
|
248
|
+
[--name <name>]
|
|
249
|
+
[--description <text>]
|
|
250
|
+
[--capabilities read:web,write:code]
|
|
251
|
+
[--constraints no:pii,no:financial:transact]
|
|
252
|
+
[--model anthropic] [--model-id claude-sonnet-4-6]
|
|
253
|
+
[--ajp-endpoint <url>]
|
|
254
|
+
[--private-key <key>]
|
|
255
|
+
${hi('status')} <provenance_id> Check trust score and checklist
|
|
256
|
+
${hi('validate')} [file] Validate PROVENANCE.yml (default: ./PROVENANCE.yml)
|
|
257
|
+
${hi('revoke')} --id <id> Revoke cryptographic identity
|
|
258
|
+
[--private-key <key>]
|
|
259
|
+
|
|
260
|
+
${amb('Environment variables:')}
|
|
261
|
+
PROVENANCE_ID Your agent's Provenance ID
|
|
262
|
+
PROVENANCE_PRIVATE_KEY Your Ed25519 private key (base64 PKCS8 DER)
|
|
263
|
+
PROVENANCE_API_URL Override API base (default: https://getprovenance.dev)
|
|
264
|
+
|
|
265
|
+
${amb('For AJP job delegation:')}
|
|
266
|
+
${dim('npm install -g ajp-cli')}
|
|
267
|
+
${dim('npx ajp hire <id> --instruction "..."')}
|
|
268
|
+
|
|
269
|
+
${amb('Examples:')}
|
|
270
|
+
provenance keygen
|
|
271
|
+
provenance register --id provenance:github:alice/my-agent --url https://github.com/alice/my-agent
|
|
272
|
+
provenance status provenance:github:alice/my-agent
|
|
273
|
+
provenance validate
|
|
195
274
|
`);
|
|
196
|
-
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
278
|
+
|
|
279
|
+
const argv = process.argv.slice(2);
|
|
280
|
+
const args = parseArgs(argv);
|
|
281
|
+
const cmd = args._[0];
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
if (!cmd || cmd === 'help' || args.help) cmdHelp();
|
|
285
|
+
else if (cmd === 'keygen') await cmdKeygen();
|
|
286
|
+
else if (cmd === 'register') await cmdRegister(args);
|
|
287
|
+
else if (cmd === 'status') await cmdStatus(args);
|
|
288
|
+
else if (cmd === 'validate') await cmdValidate(args);
|
|
289
|
+
else if (cmd === 'revoke') await cmdRevoke(args);
|
|
290
|
+
else { console.error(err(`Unknown command: ${cmd}\nRun \`provenance help\` for usage.`)); process.exit(1); }
|
|
291
|
+
} catch (e) {
|
|
292
|
+
console.error(err(e.message));
|
|
293
|
+
process.exit(1);
|
|
294
|
+
}
|
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:
|
|
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 (
|
|
325
|
-
return { allowed: false, reason:
|
|
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
|
|
496
|
-
|
|
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 };
|