provenance-protocol 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ilucky21c
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,134 @@
1
+ # provenance-protocol
2
+
3
+ SDK for querying the [Provenance](https://provenance.dev) agent identity index.
4
+
5
+ Drop this into any receiving system — marketplace, API, agent orchestrator —
6
+ to verify an AI agent's identity and trust profile before allowing it in.
7
+
8
+ ```bash
9
+ npm install provenance-protocol
10
+ ```
11
+
12
+ ---
13
+
14
+ ## Quick start
15
+
16
+ ```js
17
+ import { provenance } from 'provenance-protocol';
18
+
19
+ // Check a single agent
20
+ const trust = await provenance.check('provenance:github:alice/research-assistant');
21
+ console.log(trust);
22
+ // {
23
+ // found: true,
24
+ // declared: true,
25
+ // age_days: 142,
26
+ // confidence: 0.9,
27
+ // capabilities: ['read:web', 'write:summaries'],
28
+ // constraints: ['no:financial:transact', 'no:pii'],
29
+ // incidents: 0,
30
+ // status: 'active'
31
+ // }
32
+ ```
33
+
34
+ ---
35
+
36
+ ## gate() — all checks in one call
37
+
38
+ The most useful method for receiving systems.
39
+
40
+ ```js
41
+ const result = await provenance.gate('provenance:github:alice/agent', {
42
+ requireDeclared: true, // must have PROVENANCE.yml
43
+ requireConstraints: ['no:financial:transact', 'no:pii'], // must have committed to these
44
+ requireClean: true, // no open incidents
45
+ requireMinAge: 30, // must be at least 30 days old
46
+ requireMinConfidence: 0.7, // classification confidence
47
+ });
48
+
49
+ if (!result.allowed) {
50
+ return res.status(403).json({ error: result.reason });
51
+ // e.g. "Agent has not committed to constraint: no:financial:transact"
52
+ }
53
+
54
+ // result.trust has the full profile if you need it
55
+ ```
56
+
57
+ ---
58
+
59
+ ## Individual methods
60
+
61
+ ```js
62
+ // Boolean checks
63
+ await provenance.hasConstraint(id, 'no:financial:transact'); // → true/false
64
+ await provenance.hasCapability(id, 'read:web'); // → true/false
65
+ await provenance.isClean(id); // → true/false
66
+ await provenance.isOldEnough(id, 90); // → true/false (90+ days)
67
+
68
+ // Search for agents
69
+ const results = await provenance.search({
70
+ capabilities: ['read:web'],
71
+ constraints: ['no:financial:transact'],
72
+ declared: true,
73
+ limit: 10,
74
+ });
75
+ ```
76
+
77
+ ---
78
+
79
+ ## Configuration
80
+
81
+ ```js
82
+ import { Provenance } from 'provenance-protocol';
83
+
84
+ const provenance = new Provenance({
85
+ apiUrl: 'https://your-own-provenance-instance.com',
86
+ cacheTTL: 300, // Cache results for 5 minutes (default)
87
+ onApiError: 'deny' // 'throw' | 'allow' | 'deny' (default: 'throw')
88
+ });
89
+ ```
90
+
91
+ ### Caching
92
+
93
+ All `check()` calls are automatically cached with a configurable TTL (default 5 minutes). This dramatically reduces latency and load when used in middleware or hot paths.
94
+
95
+ ### Fail-Safe Behavior
96
+
97
+ When the Provenance API is unreachable, you can configure how `gate()` responds:
98
+
99
+ - `'throw'` (default): Throws an error, letting your app handle it
100
+ - `'deny'`: Returns `{ allowed: false }` — fail closed
101
+ - `'allow'`: Returns `{ allowed: true }` — fail open
102
+
103
+ ```js
104
+ // Fail closed if API is down
105
+ const result = await provenance.gate(id, {
106
+ requireDeclared: true,
107
+ onApiError: 'deny'
108
+ });
109
+
110
+ if (result.fallback) {
111
+ console.warn('Verification skipped due to API unavailability');
112
+ }
113
+ ```
114
+
115
+ ---
116
+
117
+ ## What the trust object contains
118
+
119
+ | Field | Type | Description |
120
+ |---|---|---|
121
+ | `found` | boolean | Agent exists in Provenance index |
122
+ | `declared` | boolean | Has a PROVENANCE.yml file |
123
+ | `age_days` | number | Days since first indexed |
124
+ | `confidence` | number | 0–1 classification confidence |
125
+ | `capabilities` | string[] | What the agent declares it can do |
126
+ | `constraints` | string[] | What the agent has publicly committed never to do |
127
+ | `incidents` | number | Number of open incidents |
128
+ | `status` | string | active / suspended / removed |
129
+ | `model` | object | `{ provider, model_id }` if declared |
130
+ | `first_seen` | string | ISO date of first public appearance |
131
+
132
+ ---
133
+
134
+ ## MIT License — provenance.dev
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "provenance-protocol",
3
+ "version": "0.1.1",
4
+ "description": "SDK for querying the Provenance agent identity index",
5
+ "type": "module",
6
+ "main": "./src/index.js",
7
+ "types": "./src/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./src/index.d.ts",
11
+ "import": "./src/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "src/index.js",
16
+ "src/index.d.ts",
17
+ "README.md"
18
+ ],
19
+ "keywords": [
20
+ "ai-agent",
21
+ "agent-identity",
22
+ "provenance",
23
+ "trust",
24
+ "llm",
25
+ "verification"
26
+ ],
27
+ "license": "MIT",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/ilucky21c/provenance-protocol"
31
+ },
32
+ "homepage": "https://provenance.dev",
33
+ "engines": {
34
+ "node": ">=14.0.0"
35
+ }
36
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,180 @@
1
+ /**
2
+ * provenance-protocol TypeScript definitions
3
+ */
4
+
5
+ export interface TrustProfile {
6
+ found: boolean;
7
+ provenance_id: string;
8
+ platform?: string;
9
+ name?: string;
10
+ declared?: boolean;
11
+ confidence?: number;
12
+ age_days?: number | null;
13
+ capabilities?: string[];
14
+ constraints?: string[];
15
+ incidents?: number;
16
+ model?: {
17
+ provider: string;
18
+ model_id: string;
19
+ } | null;
20
+ status?: string;
21
+ first_seen?: string | null;
22
+ url?: string;
23
+ /** Base64-encoded Ed25519 SPKI public key, if the agent registered one. */
24
+ public_key?: string | null;
25
+ }
26
+
27
+ export interface GateResult {
28
+ allowed: boolean;
29
+ reason: string | null;
30
+ trust: TrustProfile | null;
31
+ fallback?: boolean;
32
+ }
33
+
34
+ export interface SignedProof {
35
+ /** The nonce you sent to the agent. */
36
+ nonce: string;
37
+ /** The base64 signature the agent returned. */
38
+ signature: string;
39
+ }
40
+
41
+ export interface VerifyResult {
42
+ verified: boolean;
43
+ reason: string | null;
44
+ }
45
+
46
+ export interface GateOptions {
47
+ requireDeclared?: boolean;
48
+ requireConstraints?: string[];
49
+ requireCapabilities?: string[];
50
+ requireClean?: boolean;
51
+ requireMinAge?: number;
52
+ requireMinConfidence?: number;
53
+ /**
54
+ * Cryptographically verify the agent controls its declared private key.
55
+ * The agent must have signed `${provenanceId}:${nonce}` with its private key.
56
+ */
57
+ requireSignedProof?: SignedProof;
58
+ onApiError?: 'throw' | 'allow' | 'deny';
59
+ }
60
+
61
+ export interface SearchParams {
62
+ q?: string;
63
+ platform?: string;
64
+ capabilities?: string[];
65
+ constraints?: string[];
66
+ declared?: boolean;
67
+ minConfidence?: number;
68
+ limit?: number;
69
+ offset?: number;
70
+ }
71
+
72
+ export interface SearchResult {
73
+ agents: TrustProfile[];
74
+ total: number;
75
+ limit: number;
76
+ offset: number;
77
+ }
78
+
79
+ export interface ProvenanceOptions {
80
+ apiUrl?: string;
81
+ cacheTTL?: number;
82
+ onApiError?: 'throw' | 'allow' | 'deny';
83
+ }
84
+
85
+ export class Provenance {
86
+ constructor(options?: ProvenanceOptions);
87
+
88
+ /**
89
+ * Check an agent's trust profile.
90
+ * Results are cached for the configured TTL (default 5 minutes).
91
+ */
92
+ check(provenanceId: string): Promise<TrustProfile>;
93
+
94
+ /**
95
+ * Returns true if the agent has publicly committed to a constraint.
96
+ */
97
+ hasConstraint(provenanceId: string, constraint: string): Promise<boolean>;
98
+
99
+ /**
100
+ * Returns true if the agent has declared a capability.
101
+ */
102
+ hasCapability(provenanceId: string, capability: string): Promise<boolean>;
103
+
104
+ /**
105
+ * Returns true if the agent has no open incidents.
106
+ */
107
+ isClean(provenanceId: string): Promise<boolean>;
108
+
109
+ /**
110
+ * Returns true if the agent has existed for at least minDays.
111
+ */
112
+ isOldEnough(provenanceId: string, minDays: number): Promise<boolean>;
113
+
114
+ /**
115
+ * Run all your trust requirements in one call.
116
+ * Supports fail-safe behavior via onApiError option.
117
+ * Pass requireSignedProof to also verify cryptographic identity.
118
+ */
119
+ gate(provenanceId: string, options?: GateOptions): Promise<GateResult>;
120
+
121
+ /**
122
+ * Verify that a running agent cryptographically owns the identity it claims.
123
+ *
124
+ * Protocol: send the agent a nonce, ask it to sign `${provenanceId}:${nonce}`,
125
+ * then call this to verify the returned signature against the public key in the index.
126
+ *
127
+ * @param provenanceId The agent's Provenance ID
128
+ * @param nonce The nonce you sent to the agent
129
+ * @param signature The base64 signature the agent returned
130
+ */
131
+ verifySignature(provenanceId: string, nonce: string, signature: string): Promise<VerifyResult>;
132
+
133
+ /**
134
+ * Search for agents by capabilities, constraints, platform etc.
135
+ */
136
+ search(params?: SearchParams): Promise<SearchResult>;
137
+
138
+ /**
139
+ * Check multiple agents in a single request (max 50).
140
+ * More efficient than calling check() multiple times.
141
+ */
142
+ checkBatch(provenanceIds: string[]): Promise<Record<string, TrustProfile>>;
143
+
144
+ /**
145
+ * Gate multiple agents in a single request.
146
+ */
147
+ gateBatch(provenanceIds: string[], options?: GateOptions): Promise<Record<string, GateResult>>;
148
+
149
+ /**
150
+ * Register or update this agent in the Provenance index.
151
+ * Call once at agent startup — idempotent, safe to call on every boot.
152
+ */
153
+ register(profile: RegisterProfile): Promise<RegisterResult>;
154
+ }
155
+
156
+ export interface RegisterProfile {
157
+ id: string;
158
+ url: string;
159
+ name?: string;
160
+ description?: string;
161
+ capabilities?: string[];
162
+ constraints?: string[];
163
+ model_provider?: string;
164
+ model_id?: string;
165
+ contact_url?: string;
166
+ ajp_endpoint?: string;
167
+ public_key?: string;
168
+ version?: string;
169
+ }
170
+
171
+ export interface RegisterResult {
172
+ created?: boolean;
173
+ updated?: boolean;
174
+ agent: object;
175
+ }
176
+
177
+ /**
178
+ * Default instance pointing at provenance.dev
179
+ */
180
+ export const provenance: Provenance;
package/src/index.js ADDED
@@ -0,0 +1,570 @@
1
+ /**
2
+ * provenance-protocol SDK
3
+ *
4
+ * Drop this into any receiving system — marketplace, API, agent orchestrator.
5
+ * Query the Provenance index before trusting an agent.
6
+ *
7
+ * npm install provenance-protocol
8
+ *
9
+ * Usage:
10
+ * import { Provenance } from 'provenance-protocol';
11
+ * const trust = await Provenance.check('provenance:github:alice/research-assistant');
12
+ */
13
+
14
+ // ── Cryptographic helpers ──────────────────────────────────────────────────
15
+ // Uses the Web Crypto API (crypto.subtle) which is available in:
16
+ // - All modern browsers
17
+ // - Node.js 18+ (globalThis.crypto.subtle)
18
+ // No external dependencies needed.
19
+
20
+ /**
21
+ * Verify an Ed25519 signature against a public key stored in the index.
22
+ *
23
+ * @param {string} publicKeyBase64 Base64-encoded SPKI DER public key
24
+ * @param {string} signatureBase64 Base64-encoded raw signature (64 bytes)
25
+ * @param {string} message The message that was signed (UTF-8 string)
26
+ * @returns {Promise<boolean>}
27
+ */
28
+ async function _verifyEd25519(publicKeyBase64, signatureBase64, message) {
29
+ const subtle = globalThis.crypto?.subtle;
30
+ if (!subtle) throw new Error('Web Crypto API (crypto.subtle) not available');
31
+
32
+ const keyBuffer = Uint8Array.from(Buffer.from(publicKeyBase64, 'base64'));
33
+ const sigBuffer = Uint8Array.from(Buffer.from(signatureBase64, 'base64'));
34
+ const msgBuffer = new TextEncoder().encode(message);
35
+
36
+ const cryptoKey = await subtle.importKey(
37
+ 'spki',
38
+ keyBuffer,
39
+ { name: 'Ed25519' },
40
+ false,
41
+ ['verify']
42
+ );
43
+
44
+ return subtle.verify('Ed25519', cryptoKey, sigBuffer, msgBuffer);
45
+ }
46
+
47
+ const DEFAULT_API = 'https://provenance.dev';
48
+ const DEFAULT_CACHE_TTL = 300; // 5 minutes
49
+
50
+ // Simple LRU cache
51
+ class Cache {
52
+ constructor(ttlSeconds = DEFAULT_CACHE_TTL) {
53
+ this.cache = new Map();
54
+ this.ttl = ttlSeconds * 1000;
55
+ }
56
+
57
+ get(key) {
58
+ const item = this.cache.get(key);
59
+ if (!item) return null;
60
+ if (Date.now() > item.expiry) {
61
+ this.cache.delete(key);
62
+ return null;
63
+ }
64
+ return item.value;
65
+ }
66
+
67
+ set(key, value) {
68
+ this.cache.set(key, {
69
+ value,
70
+ expiry: Date.now() + this.ttl
71
+ });
72
+ }
73
+
74
+ clear() {
75
+ this.cache.clear();
76
+ }
77
+ }
78
+
79
+ export class Provenance {
80
+
81
+ constructor({
82
+ apiUrl = DEFAULT_API,
83
+ cacheTTL = DEFAULT_CACHE_TTL,
84
+ onApiError = 'throw' // 'throw' | 'allow' | 'deny'
85
+ } = {}) {
86
+ this.apiUrl = apiUrl.replace(/\/$/, '');
87
+ this.cache = new Cache(cacheTTL);
88
+ this.onApiError = onApiError;
89
+ }
90
+
91
+ // ── Main method — the one most receiving systems need ────────────────────
92
+
93
+ /**
94
+ * Check an agent's trust profile.
95
+ *
96
+ * @param {string} provenanceId e.g. "provenance:github:alice/research-assistant"
97
+ * @returns {object} trust summary
98
+ *
99
+ * Example:
100
+ * const trust = await provenance.check('provenance:github:alice/research-assistant');
101
+ * // {
102
+ * // found: true,
103
+ * // declared: true, — has PROVENANCE.yml
104
+ * // age_days: 142, — how long this agent has existed publicly
105
+ * // confidence: 0.9,
106
+ * // capabilities: ['read:web', 'write:summaries'],
107
+ * // constraints: ['no:financial:transact', 'no:pii'],
108
+ * // incidents: 0,
109
+ * // model: { provider: 'anthropic', model_id: 'claude-sonnet-4-5' },
110
+ * // status: 'active',
111
+ * // }
112
+ */
113
+ async check(provenanceId) {
114
+ // Check cache first
115
+ const cached = this.cache.get(provenanceId);
116
+ if (cached) return cached;
117
+
118
+ const path = this._idToPath(provenanceId);
119
+ try {
120
+ const res = await fetch(`${this.apiUrl}/api/agent/${path}`);
121
+ if (res.status === 404) {
122
+ const notFound = { found: false, provenance_id: provenanceId };
123
+ this.cache.set(provenanceId, notFound);
124
+ return notFound;
125
+ }
126
+ if (!res.ok) throw new Error(`Provenance API error: ${res.status}`);
127
+ const data = await res.json();
128
+
129
+ const result = {
130
+ found: true,
131
+ provenance_id: data.provenance_id,
132
+ platform: data.platform,
133
+ name: data.name,
134
+ declared: data.declared,
135
+ confidence: data.confidence,
136
+ age_days: data.timestamps?.first_seen
137
+ ? Math.floor((Date.now() - new Date(data.timestamps.first_seen)) / 86400000)
138
+ : null,
139
+ capabilities: data.capabilities || [],
140
+ constraints: data.constraints || [],
141
+ incidents: data.incident_count || 0,
142
+ model: data.model || null,
143
+ status: data.status || 'unknown',
144
+ first_seen: data.timestamps?.first_seen || null,
145
+ url: data.url,
146
+ public_key: data.public_key || null,
147
+ };
148
+
149
+ // Cache the result
150
+ this.cache.set(provenanceId, result);
151
+ return result;
152
+ } catch (e) {
153
+ throw new Error(`Provenance.check failed: ${e.message}`);
154
+ }
155
+ }
156
+
157
+ // ── Convenience guard methods — boolean checks ───────────────────────────
158
+
159
+ /**
160
+ * Returns true if the agent has publicly committed to a constraint.
161
+ *
162
+ * Example:
163
+ * if (!await provenance.hasConstraint(id, 'no:financial:transact')) {
164
+ * throw new Error('Agent not cleared for financial operations');
165
+ * }
166
+ */
167
+ async hasConstraint(provenanceId, constraint) {
168
+ const trust = await this.check(provenanceId);
169
+ return trust.found && trust.constraints.includes(constraint);
170
+ }
171
+
172
+ /**
173
+ * Returns true if the agent has declared a capability.
174
+ */
175
+ async hasCapability(provenanceId, capability) {
176
+ const trust = await this.check(provenanceId);
177
+ return trust.found && trust.capabilities.includes(capability);
178
+ }
179
+
180
+ /**
181
+ * Returns true if the agent has no open incidents.
182
+ */
183
+ async isClean(provenanceId) {
184
+ const trust = await this.check(provenanceId);
185
+ return trust.found && trust.incidents === 0 && trust.status === 'active';
186
+ }
187
+
188
+ /**
189
+ * Returns true if the agent has existed for at least minDays.
190
+ * Age is a proxy for reliability — a 6-month-old agent with no incidents
191
+ * is more trustworthy than a brand-new one.
192
+ */
193
+ async isOldEnough(provenanceId, minDays) {
194
+ const trust = await this.check(provenanceId);
195
+ return trust.found && trust.age_days !== null && trust.age_days >= minDays;
196
+ }
197
+
198
+ // ── Cryptographic identity verification ──────────────────────────────────
199
+
200
+ /**
201
+ * Verify that a running agent cryptographically owns the identity it claims.
202
+ *
203
+ * This closes the gap between "a repo declares this identity" and "the agent
204
+ * talking to you actually controls that repo's private key."
205
+ *
206
+ * Protocol (challenge-response):
207
+ * 1. Receiving system generates a nonce: const nonce = crypto.randomUUID()
208
+ * 2. Receiving system sends nonce to agent
209
+ * 3. Agent signs: signChallenge(privateKey, provenanceId, nonce) → signature
210
+ * 4. Receiving system verifies: await provenance.verifySignature(id, nonce, signature)
211
+ *
212
+ * @param {string} provenanceId e.g. "provenance:github:alice/research-assistant"
213
+ * @param {string} nonce The nonce you sent to the agent (UUID or random string)
214
+ * @param {string} signatureBase64 Base64 signature returned by the agent
215
+ * @returns {{ verified: boolean, reason: string | null }}
216
+ *
217
+ * Example:
218
+ * const nonce = crypto.randomUUID();
219
+ * // ... send nonce to agent, receive signature back ...
220
+ * const result = await provenance.verifySignature(
221
+ * 'provenance:github:alice/research-assistant',
222
+ * nonce,
223
+ * agentSignature
224
+ * );
225
+ * if (!result.verified) throw new Error(`Identity check failed: ${result.reason}`);
226
+ */
227
+ async verifySignature(provenanceId, nonce, signatureBase64) {
228
+ let trust;
229
+ try {
230
+ trust = await this.check(provenanceId);
231
+ } catch (e) {
232
+ return { verified: false, reason: `Could not fetch agent profile: ${e.message}` };
233
+ }
234
+
235
+ if (!trust.found) {
236
+ return { verified: false, reason: 'Agent not found in Provenance index' };
237
+ }
238
+ if (!trust.public_key) {
239
+ return { verified: false, reason: 'Agent has no public key registered in PROVENANCE.yml' };
240
+ }
241
+
242
+ // The signed message is always: "<provenanceId>:<nonce>"
243
+ // This binds the signature to both the agent's identity and the specific challenge,
244
+ // preventing replay attacks and cross-agent signature reuse.
245
+ const message = `${provenanceId}:${nonce}`;
246
+
247
+ try {
248
+ const verified = await _verifyEd25519(trust.public_key, signatureBase64, message);
249
+ return { verified, reason: verified ? null : 'Signature is invalid' };
250
+ } catch (e) {
251
+ return { verified: false, reason: `Signature verification error: ${e.message}` };
252
+ }
253
+ }
254
+
255
+ // ── Gate method — combine all checks in one call ─────────────────────────
256
+
257
+ /**
258
+ * Run all your trust requirements in one call.
259
+ * Returns { allowed, reason, trust }.
260
+ *
261
+ * Example:
262
+ * const result = await provenance.gate('provenance:github:alice/agent', {
263
+ * requireDeclared: true,
264
+ * requireConstraints: ['no:financial:transact', 'no:pii'],
265
+ * requireClean: true,
266
+ * requireMinAge: 30,
267
+ * requireMinConfidence: 0.7,
268
+ * });
269
+ *
270
+ * if (!result.allowed) {
271
+ * return res.status(403).json({ error: result.reason });
272
+ * }
273
+ */
274
+ async gate(provenanceId, {
275
+ requireDeclared = false,
276
+ requireConstraints = [],
277
+ requireCapabilities = [],
278
+ requireClean = true,
279
+ requireMinAge = 0,
280
+ requireMinConfidence = 0,
281
+ requireSignedProof = null,
282
+ // { nonce: string, signature: string }
283
+ // When provided, cryptographically verifies the agent controls its declared
284
+ // private key. The agent must have signed `${provenanceId}:${nonce}`.
285
+ onApiError, // Override instance default if provided
286
+ } = {}) {
287
+ const errorPolicy = onApiError || this.onApiError;
288
+ let trust;
289
+ try {
290
+ trust = await this.check(provenanceId);
291
+ } catch (e) {
292
+ // Handle API failures based on policy
293
+ if (errorPolicy === 'allow') {
294
+ return {
295
+ allowed: true,
296
+ reason: 'Verification skipped (API unavailable)',
297
+ trust: null,
298
+ fallback: true
299
+ };
300
+ }
301
+ if (errorPolicy === 'deny') {
302
+ return {
303
+ allowed: false,
304
+ reason: `Verification failed (API unavailable): ${e.message}`,
305
+ trust: null,
306
+ fallback: true
307
+ };
308
+ }
309
+ // errorPolicy === 'throw'
310
+ throw e;
311
+ }
312
+
313
+ if (!trust.found) {
314
+ return { allowed: false, reason: 'Agent not found in Provenance index', trust };
315
+ }
316
+ if (trust.status !== 'active') {
317
+ return { allowed: false, reason: `Agent status is ${trust.status}`, trust };
318
+ }
319
+ if (requireDeclared && !trust.declared) {
320
+ return { allowed: false, reason: 'Agent has not declared a PROVENANCE.yml file', trust };
321
+ }
322
+ if (requireClean && trust.incidents > 0) {
323
+ return { allowed: false, reason: `Agent has ${trust.incidents} open incident(s)`, trust };
324
+ }
325
+ if (requireMinConfidence && trust.confidence < requireMinConfidence) {
326
+ return { allowed: false, reason: `Agent confidence ${trust.confidence} below required ${requireMinConfidence}`, trust };
327
+ }
328
+ if (requireMinAge && (trust.age_days === null || trust.age_days < requireMinAge)) {
329
+ return { allowed: false, reason: `Agent is ${trust.age_days ?? 0} days old, minimum is ${requireMinAge}`, trust };
330
+ }
331
+ for (const constraint of requireConstraints) {
332
+ if (!trust.constraints.includes(constraint)) {
333
+ return { allowed: false, reason: `Agent has not committed to constraint: ${constraint}`, trust };
334
+ }
335
+ }
336
+ for (const capability of requireCapabilities) {
337
+ if (!trust.capabilities.includes(capability)) {
338
+ return { allowed: false, reason: `Agent does not declare capability: ${capability}`, trust };
339
+ }
340
+ }
341
+ if (requireSignedProof) {
342
+ const { nonce, signature } = requireSignedProof;
343
+ const result = await this.verifySignature(provenanceId, nonce, signature);
344
+ if (!result.verified) {
345
+ return { allowed: false, reason: `Cryptographic identity verification failed: ${result.reason}`, trust };
346
+ }
347
+ }
348
+
349
+ return { allowed: true, reason: null, trust };
350
+ }
351
+
352
+ // ── Search ────────────────────────────────────────────────────────────────
353
+
354
+ /**
355
+ * Search for agents by capabilities, constraints, platform etc.
356
+ *
357
+ * Example:
358
+ * const agents = await provenance.search({
359
+ * capabilities: ['read:web'],
360
+ * constraints: ['no:financial:transact'],
361
+ * declared: true,
362
+ * });
363
+ */
364
+ async search(params = {}) {
365
+ const qs = new URLSearchParams();
366
+ if (params.q) qs.set('q', params.q);
367
+ if (params.platform) qs.set('platform', params.platform);
368
+ if (params.capabilities?.length) qs.set('capabilities', params.capabilities.join(','));
369
+ if (params.constraints?.length) qs.set('constraints', params.constraints.join(','));
370
+ if (params.declared !== undefined) qs.set('declared', String(params.declared));
371
+ if (params.minConfidence) qs.set('min_confidence', String(params.minConfidence));
372
+ if (params.limit) qs.set('limit', String(params.limit));
373
+ if (params.offset) qs.set('offset', String(params.offset));
374
+
375
+ try {
376
+ const res = await fetch(`${this.apiUrl}/api/search?${qs}`);
377
+ if (!res.ok) throw new Error(`Provenance API error: ${res.status}`);
378
+ return res.json();
379
+ } catch (e) {
380
+ throw new Error(`Provenance.search failed: ${e.message}`);
381
+ }
382
+ }
383
+
384
+ // ── Batch operations ────────────────────────────────────────────────────
385
+
386
+ /**
387
+ * Check multiple agents in a single request.
388
+ * More efficient than calling check() multiple times.
389
+ *
390
+ * @param {string[]} provenanceIds - Array of provenance IDs (max 50)
391
+ * @returns {object} Map of provenance_id → trust profile
392
+ */
393
+ async checkBatch(provenanceIds) {
394
+ if (!Array.isArray(provenanceIds) || provenanceIds.length === 0) {
395
+ throw new Error('provenanceIds must be a non-empty array');
396
+ }
397
+ if (provenanceIds.length > 50) {
398
+ throw new Error('Maximum 50 IDs per batch request');
399
+ }
400
+
401
+ // Check cache first, collect uncached IDs
402
+ const results = {};
403
+ const uncached = [];
404
+
405
+ for (const id of provenanceIds) {
406
+ const cached = this.cache.get(id);
407
+ if (cached) {
408
+ results[id] = cached;
409
+ } else {
410
+ uncached.push(id);
411
+ }
412
+ }
413
+
414
+ // If all cached, return immediately
415
+ if (uncached.length === 0) {
416
+ return results;
417
+ }
418
+
419
+ // Fetch uncached from API
420
+ try {
421
+ const res = await fetch(`${this.apiUrl}/api/agents/batch`, {
422
+ method: 'POST',
423
+ headers: { 'Content-Type': 'application/json' },
424
+ body: JSON.stringify({ ids: uncached }),
425
+ });
426
+
427
+ if (!res.ok) throw new Error(`Provenance API error: ${res.status}`);
428
+
429
+ const data = await res.json();
430
+
431
+ // Cache and merge results
432
+ for (const id of uncached) {
433
+ const profile = data.results?.[id] || { found: false, provenance_id: id };
434
+ this.cache.set(id, profile);
435
+ results[id] = profile;
436
+ }
437
+
438
+ return results;
439
+ } catch (e) {
440
+ throw new Error(`Provenance.checkBatch failed: ${e.message}`);
441
+ }
442
+ }
443
+
444
+ /**
445
+ * Gate multiple agents in a single request.
446
+ *
447
+ * @param {string[]} provenanceIds - Array of provenance IDs
448
+ * @param {object} options - Same options as gate()
449
+ * @returns {object} Map of provenance_id → gate result
450
+ */
451
+ async gateBatch(provenanceIds, options = {}) {
452
+ const profiles = await this.checkBatch(provenanceIds);
453
+ const results = {};
454
+
455
+ for (const id of provenanceIds) {
456
+ const trust = profiles[id];
457
+ results[id] = this._evaluateGate(trust, options);
458
+ }
459
+
460
+ return results;
461
+ }
462
+
463
+ // Internal: evaluate gate rules against a trust profile
464
+ _evaluateGate(trust, options) {
465
+ const {
466
+ requireDeclared = false,
467
+ requireConstraints = [],
468
+ requireCapabilities = [],
469
+ requireClean = true,
470
+ requireMinAge = 0,
471
+ requireMinConfidence = 0,
472
+ } = options;
473
+
474
+ if (!trust || !trust.found) {
475
+ return { allowed: false, reason: 'Agent not found in Provenance index', trust };
476
+ }
477
+ if (trust.status !== 'active') {
478
+ return { allowed: false, reason: `Agent status is ${trust.status}`, trust };
479
+ }
480
+ if (requireDeclared && !trust.declared) {
481
+ return { allowed: false, reason: 'Agent has not declared a PROVENANCE.yml file', trust };
482
+ }
483
+ for (const c of requireConstraints) {
484
+ if (!trust.constraints?.includes(c)) {
485
+ return { allowed: false, reason: `Agent has not committed to constraint: ${c}`, trust };
486
+ }
487
+ }
488
+ for (const c of requireCapabilities) {
489
+ if (!trust.capabilities?.includes(c)) {
490
+ return { allowed: false, reason: `Agent does not have capability: ${c}`, trust };
491
+ }
492
+ }
493
+ if (requireClean && trust.incidents > 0) {
494
+ return { allowed: false, reason: `Agent has ${trust.incidents} open incident(s)`, trust };
495
+ }
496
+ if (requireMinAge > 0 && (trust.age_days || 0) < requireMinAge) {
497
+ return { allowed: false, reason: `Agent is only ${trust.age_days || 0} days old (minimum: ${requireMinAge})`, trust };
498
+ }
499
+ if (requireMinConfidence > 0 && (trust.confidence || 0) < requireMinConfidence) {
500
+ return { allowed: false, reason: `Agent confidence ${trust.confidence} below minimum ${requireMinConfidence}`, trust };
501
+ }
502
+
503
+ return { allowed: true, reason: null, trust };
504
+ }
505
+
506
+ // ── Internal ──────────────────────────────────────────────────────────────
507
+
508
+ _idToPath(provenanceId) {
509
+ // provenance:github:alice/research-assistant
510
+ // → github/alice/research-assistant
511
+ return provenanceId.replace('provenance:', '').replace(':', '/');
512
+ }
513
+ }
514
+
515
+ // ── Self-registration ─────────────────────────────────────────────────────
516
+
517
+ /**
518
+ * Register or update this agent in the Provenance index.
519
+ * Call once at agent startup — idempotent, safe to call on every boot.
520
+ *
521
+ * Example:
522
+ * import { provenance } from 'provenance-protocol';
523
+ *
524
+ * await provenance.register({
525
+ * id: 'provenance:github:your-org/your-agent',
526
+ * url: 'https://github.com/your-org/your-agent',
527
+ * name: 'Your Agent',
528
+ * description: 'What it does',
529
+ * capabilities: ['read:web', 'write:summaries'],
530
+ * constraints: ['no:pii', 'no:financial:transact'],
531
+ * });
532
+ *
533
+ * @param {object} profile
534
+ * @param {string} profile.id provenance:<platform>:<owner>/<name>
535
+ * @param {string} profile.url Canonical URL (GitHub repo, package page, etc.)
536
+ * @param {string} [profile.name] Display name
537
+ * @param {string} [profile.description] One-sentence description
538
+ * @param {string[]} [profile.capabilities]
539
+ * @param {string[]} [profile.constraints]
540
+ * @param {string} [profile.model_provider]
541
+ * @param {string} [profile.model_id]
542
+ * @param {string} [profile.contact_url]
543
+ * @param {string} [profile.ajp_endpoint]
544
+ * @param {string} [profile.public_key] Ed25519 public key: "ed25519:<base64>"
545
+ * @param {string} [profile.version]
546
+ * @returns {{ created: boolean, updated: boolean, agent: object }}
547
+ */
548
+ async register(profile = {}) {
549
+ const { id, ...rest } = profile;
550
+ if (!id) throw new Error('profile.id is required');
551
+
552
+ try {
553
+ const res = await fetch(`${this.apiUrl}/api/agents/register`, {
554
+ method: 'POST',
555
+ headers: { 'Content-Type': 'application/json' },
556
+ body: JSON.stringify({ provenance_id: id, ...rest }),
557
+ });
558
+ if (!res.ok) {
559
+ const err = await res.json().catch(() => ({}));
560
+ throw new Error(err.error || `Registration failed: ${res.status}`);
561
+ }
562
+ return res.json();
563
+ } catch (e) {
564
+ throw new Error(`Provenance.register failed: ${e.message}`);
565
+ }
566
+ }
567
+ }
568
+
569
+ // Default instance pointing at provenance.dev
570
+ export const provenance = new Provenance();