provenance-protocol 0.2.1 → 0.3.0

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 CHANGED
@@ -30,11 +30,23 @@ const result = await verifyDeclaration(YAML.parse(fileContents), {
30
30
  });
31
31
 
32
32
  result.valid // the signature verifies against the key in the file
33
+ result.coverage // 'declaration' (0.2) | 'identity' (0.1)
33
34
  result.location // 'match' | 'mismatch' | 'unchecked'
34
35
  result.trustworthy // valid AND served from the location it claims
35
36
  result.fingerprint // SHA-256 of the key — store it to detect rotation
36
37
  ```
37
38
 
39
+ `coverage` matters. Under spec **0.2** the signature covers the whole
40
+ declaration, so deleting a constraint breaks it. Under **0.1** it covered only
41
+ the identity and key — the declared capabilities and constraints were *not*
42
+ protected, and a valid 0.1 signature says nothing about whether they were
43
+ edited. Sign new declarations with 0.2:
44
+
45
+ ```js
46
+ import { signDeclaration } from 'provenance-protocol/keygen';
47
+ declaration.identity.signature = signDeclaration(privateKey, declaration);
48
+ ```
49
+
38
50
  A valid signature proves the declaration came from the holder of that private
39
51
  key and has not been altered. It does **not** prove who that holder is, that
40
52
  the declared capabilities are accurate, or that the declaration is current.
@@ -264,11 +276,11 @@ await fetch('https://getprovenance.dev/api/agents/revoke', {
264
276
  ## CLI
265
277
 
266
278
  ```bash
267
- npx provenance keygen
268
- npx provenance register --id provenance:github:your-org/your-agent --url https://github.com/...
269
- npx provenance status provenance:github:alice/my-agent
270
- npx provenance validate PROVENANCE.yml
271
- npx provenance revoke --id provenance:github:your-org/your-agent
279
+ npx provenance-protocol keygen
280
+ npx provenance-protocol register --id provenance:github:your-org/your-agent --url https://github.com/...
281
+ npx provenance-protocol status provenance:github:alice/my-agent
282
+ npx provenance-protocol validate PROVENANCE.yml
283
+ npx provenance-protocol revoke --id provenance:github:your-org/your-agent
272
284
  ```
273
285
 
274
286
  Full CLI reference: [getprovenance.dev/docs#cli](https://getprovenance.dev/docs#cli)
package/SPEC.md CHANGED
@@ -1,5 +1,5 @@
1
1
  # Provenance Protocol Specification
2
- **Version 0.1**
2
+ **Versions 0.1 and 0.2**
3
3
 
4
4
  ---
5
5
 
@@ -299,7 +299,7 @@ identity:
299
299
  ```
300
300
 
301
301
  `public_key` is required when `identity` is present. `algorithm` is optional
302
- and defaults to `ed25519`; no other value is valid in v0.1.
302
+ and defaults to `ed25519`; no other value is valid.
303
303
 
304
304
  `signature` is optional, and its presence changes what the block means:
305
305
 
@@ -315,7 +315,35 @@ Prefer the signed form wherever the agent has a public location. A verifier
315
315
  that finds a key with no signature has learned which key to challenge, and
316
316
  nothing about whether the file has been altered.
317
317
 
318
- ### What gets signed
318
+ ### What gets signed — and this differs by version
319
+
320
+ The `provenance` field selects the rule. A verifier MUST use the rule for the
321
+ version the declaration declares, and MUST refuse to guess for a version it does
322
+ not know.
323
+
324
+ #### 0.2 — the whole declaration (use this)
325
+
326
+ The signed message is the UTF-8 encoding of:
327
+
328
+ ```
329
+ provenance-declaration-v1:<canonical JSON of the declaration>
330
+ ```
331
+
332
+ The canonical JSON is produced from the **parsed** declaration, with
333
+ `identity.signature` removed — it cannot cover itself — and with object keys
334
+ sorted by Unicode code point recursively and no insignificant whitespace.
335
+
336
+ Because canonicalisation applies to the parsed value rather than the file's
337
+ bytes, comments, indentation, quoting style and key order do not affect the
338
+ signature. A declaration can be reformatted without re-signing. Only JSON
339
+ representable values may be signed; a parser that yields dates or other
340
+ non-plain objects must be made to yield strings instead.
341
+
342
+ Every field is covered, so deleting a constraint or adding a capability breaks
343
+ the signature. `provenance_id` is not required to verify a 0.2 signature, though
344
+ the location check still needs it.
345
+
346
+ #### 0.1 — the identity only (legacy)
319
347
 
320
348
  The signed message is the UTF-8 encoding of:
321
349
 
@@ -323,12 +351,18 @@ The signed message is the UTF-8 encoding of:
323
351
  <provenance_id>:<public_key>
324
352
  ```
325
353
 
326
- where `<public_key>` is the same base64 string that appears in
327
- `identity.public_key`. Concatenating the two binds the key to the identity,
328
- so a key lifted from one declaration cannot be replayed under a different
329
- `provenance_id`.
354
+ This binds the key to the identity, so a key lifted from one declaration cannot
355
+ be replayed under a different `provenance_id`, and it requires `provenance_id`
356
+ to be present.
330
357
 
331
- Signing a declaration therefore requires `provenance_id` to be present.
358
+ **It does not cover any other field.** A 0.1 declaration's capabilities and
359
+ constraints are not protected by its signature: someone with write access to the
360
+ location can delete a declared constraint and the signature still verifies.
361
+ Location and write access are the only controls there.
362
+
363
+ 0.1 declarations remain readable and verifiable — nothing published stops
364
+ working — but new declarations SHOULD use 0.2, and a verifier SHOULD report
365
+ which coverage it checked so a reader is not misled about what was proven.
332
366
 
333
367
  ### How to verify
334
368
 
@@ -344,10 +378,12 @@ anyone.
344
378
 
345
379
  ### What verification proves — and what it does not
346
380
 
347
- A valid signature proves:
381
+ A valid signature proves the declaration was produced by the holder of that
382
+ private key, and:
348
383
 
349
- - the declaration was produced by the holder of that private key, and
350
- - not one byte of `provenance_id` or `public_key` has changed since.
384
+ - under **0.2**, that no field of the declaration has changed since;
385
+ - under **0.1**, only that `provenance_id` and `public_key` have not changed
386
+ everything else is unprotected.
351
387
 
352
388
  A valid signature does **not** prove:
353
389
 
@@ -405,19 +441,25 @@ they would re-check any other freshness signal.
405
441
 
406
442
  An implementation of this specification is conformant if it:
407
443
 
408
- 1. Accepts every file that validates against `schema/provenance-0.1.json`.
444
+ 1. Accepts every file that validates against the schema for its declared
445
+ version — `schema/provenance-0.1.json` or `schema/provenance-0.2.json`.
409
446
  2. Treats `provenance`, `name` and `description` as required and everything
410
447
  else as optional.
411
448
  3. Ignores unrecognised top-level fields rather than rejecting the file.
412
449
  Future spec versions add fields; a `0.1` reader must not break on them.
413
450
  4. Treats `identity.signature` as optional, and an unsigned `identity`
414
451
  block as advertising a key rather than attesting the file.
415
- 5. Reproduces every signature in `test-vectors/signatures-0.1.json` marked
416
- `valid`, and refuses every one marked `invalid`.
417
- 6. Treats a declaration whose `provenance_id` does not match its retrieval
452
+ 5. Applies the signing rule for the version the declaration declares, refuses
453
+ to guess for an unknown version, and reports which coverage it checked —
454
+ a reader must not be left thinking a 0.1 signature protected the
455
+ declared constraints.
456
+ 6. Reproduces every signature in `test-vectors/signatures-0.1.json` and every
457
+ declaration in `test-vectors/declarations-0.2.json` marked `valid`, and
458
+ refuses every one marked `invalid`.
459
+ 7. Treats a declaration whose `provenance_id` does not match its retrieval
418
460
  location as unverified.
419
461
 
420
- Points 5 and 6 are what make independent implementations agree. An
462
+ Points 6 and 7 are what make independent implementations agree. An
421
463
  implementation that passes the test vectors interoperates with every other
422
464
  one that does, with no reference to any particular service.
423
465
 
@@ -442,10 +484,22 @@ monitors, attesters — are applications of the standard, not part of it.
442
484
 
443
485
  ## Versioning
444
486
 
445
- The `provenance` field records which spec version you are using.
446
- We commit to backwards compatibility a `0.1` file will always be
447
- readable regardless of future spec versions.
448
- New versions add fields. Existing fields are never removed.
487
+ The `provenance` field records which spec version you are using, and it is what
488
+ a reader uses to decide how to interpret the file.
489
+
490
+ We commit to backwards compatibility: a `0.1` file will always be readable and
491
+ verifiable, whatever later versions say. Fields are never removed.
492
+
493
+ Usually a new version only adds fields. Version 0.2 is the exception so far — it
494
+ changed what an existing field *covers*, because `identity.signature` in 0.1
495
+ protected only the identity and left the declared capabilities and constraints
496
+ unprotected, which was weaker than readers assumed. Rather than redefine 0.1
497
+ under declarations already published, 0.2 defines the stronger rule and the
498
+ `provenance` field keeps the two apart. A verifier implements both and applies
499
+ the one the declaration asks for.
500
+
501
+ Where a version changes the meaning of an existing field, it will always do so
502
+ by declaring a new version, never by reinterpreting an old one.
449
503
 
450
504
  ---
451
505
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "provenance-protocol",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
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",
@@ -33,7 +33,9 @@
33
33
  "README.md",
34
34
  "SPEC.md",
35
35
  "schema/provenance-0.1.json",
36
- "test-vectors/"
36
+ "test-vectors/",
37
+ "src/canonical.js",
38
+ "schema/provenance-0.2.json"
37
39
  ],
38
40
  "keywords": [
39
41
  "ai-agent",
@@ -56,6 +58,6 @@
56
58
  "node": ">=14.0.0"
57
59
  },
58
60
  "scripts": {
59
- "test": "node test/verify.test.mjs"
61
+ "test": "node test/verify.test.mjs && node test/declaration-signature.test.mjs"
60
62
  }
61
63
  }
@@ -187,7 +187,7 @@
187
187
  },
188
188
  "signature": {
189
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."
190
+ "description": "Base64 Ed25519 signature of '<provenance_id>:<public_key>' \u2014 proves you control the private key under that identity. NOTE: in 0.1 it does NOT cover the rest of the declaration, so capabilities and constraints are not protected by it; spec 0.2 signs the whole declaration. Optional: omit it when the key is published only for live challenge-response. Requires provenance_id."
191
191
  },
192
192
  "algorithm": {
193
193
  "type": "string",
@@ -0,0 +1,229 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://getprovenance.dev/schema/0.2.json",
4
+ "title": "PROVENANCE.yml",
5
+ "description": "Provenance Protocol v0.2 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. 0.2 signs the whole declaration.",
16
+ "enum": [
17
+ "0.2"
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 entire declaration 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 over 'provenance-declaration-v1:' followed by the canonical JSON of this parsed declaration with identity.signature removed. Covers every field, so editing capabilities or deleting a constraint breaks it. Generate with signDeclaration() from provenance-protocol/keygen. Optional: omit when the key is published only for live challenge-response."
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
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * provenance-protocol — canonical form for signing declarations
3
+ *
4
+ * Spec 0.1 signed only "<provenance_id>:<public_key>", so a signature proved
5
+ * key control but left the rest of the declaration unprotected: a constraint
6
+ * could be deleted and the signature would still verify. Spec 0.2 signs the
7
+ * whole declaration, which needs one canonical serialisation that every
8
+ * implementation agrees on byte for byte.
9
+ *
10
+ * Canonicalisation applies to the PARSED value, not the file's bytes. Comments,
11
+ * indentation, quoting style and key order therefore do not affect the
12
+ * signature — a declaration can be reformatted without re-signing, which is
13
+ * what anyone would expect.
14
+ *
15
+ * Rules:
16
+ * - object keys sorted by Unicode code point, recursively
17
+ * - no insignificant whitespace
18
+ * - `identity.signature` removed before signing (it cannot cover itself)
19
+ * - only JSON-representable values; anything else throws rather than being
20
+ * silently coerced into an ambiguous signature
21
+ */
22
+
23
+ const DOMAIN = 'provenance-declaration-v1';
24
+
25
+ /** Thrown when a declaration contains something that cannot be canonicalised. */
26
+ export class CanonicalError extends Error {
27
+ constructor(message) {
28
+ super(message);
29
+ this.name = 'CanonicalError';
30
+ }
31
+ }
32
+
33
+ function canonicalValue(value, path = '$') {
34
+ if (value === null) return 'null';
35
+
36
+ const type = typeof value;
37
+
38
+ if (type === 'string') return JSON.stringify(value);
39
+ if (type === 'boolean') return value ? 'true' : 'false';
40
+ if (type === 'number') {
41
+ if (!Number.isFinite(value)) {
42
+ throw new CanonicalError(`${path}: non-finite numbers cannot be signed`);
43
+ }
44
+ return JSON.stringify(value);
45
+ }
46
+
47
+ if (Array.isArray(value)) {
48
+ return `[${value.map((item, i) => canonicalValue(item, `${path}[${i}]`)).join(',')}]`;
49
+ }
50
+
51
+ if (type === 'object') {
52
+ // A YAML parser may produce Dates, Maps, Buffers and so on. Signing those
53
+ // would depend on whichever serialisation happened to be used, so refuse.
54
+ if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) {
55
+ throw new CanonicalError(
56
+ `${path}: only plain objects can be signed (got ${value.constructor?.name ?? 'unknown type'}) — quote the value as a string`
57
+ );
58
+ }
59
+ const keys = Object.keys(value).filter((k) => value[k] !== undefined).sort();
60
+ const parts = keys.map(
61
+ (k) => `${JSON.stringify(k)}:${canonicalValue(value[k], `${path}.${k}`)}`
62
+ );
63
+ return `{${parts.join(',')}}`;
64
+ }
65
+
66
+ throw new CanonicalError(`${path}: values of type ${type} cannot be signed`);
67
+ }
68
+
69
+ /**
70
+ * The exact string a spec-0.2 declaration signature is computed over.
71
+ *
72
+ * Domain-separated so a declaration signature can never be replayed as a
73
+ * signature over some other kind of message.
74
+ *
75
+ * @param {object} declaration Parsed declaration (its identity.signature is ignored)
76
+ * @returns {string}
77
+ */
78
+ export function declarationSigningPayload(declaration) {
79
+ if (declaration === null || typeof declaration !== 'object' || Array.isArray(declaration)) {
80
+ throw new CanonicalError('Declaration must be a parsed object');
81
+ }
82
+
83
+ const { identity, ...rest } = declaration;
84
+ const covered = { ...rest };
85
+
86
+ if (identity !== undefined) {
87
+ if (identity === null || typeof identity !== 'object' || Array.isArray(identity)) {
88
+ throw new CanonicalError('$.identity must be an object when present');
89
+ }
90
+ // public_key and algorithm ARE covered; only the signature is excluded.
91
+ const { signature: _excluded, ...identityRest } = identity;
92
+ covered.identity = identityRest;
93
+ }
94
+
95
+ return `${DOMAIN}:${canonicalValue(covered)}`;
96
+ }
97
+
98
+ export { DOMAIN as DECLARATION_SIGNING_DOMAIN };