ajp-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.
@@ -44,4 +44,36 @@ jobs:
44
44
  # publish rights, configured as a trusted publisher on npmjs.com against
45
45
  # this repository and this workflow filename. Provenance attestations are
46
46
  # generated automatically, so --provenance is not passed.
47
- - run: npm publish --access public
47
+ # Skip rather than fail when this version is already on npm. A tag may
48
+ # exist to release a sibling package in this repo, and a red run that
49
+ # actually means "nothing to publish here" trains people to ignore red
50
+ # runs. The log says which happened.
51
+ - name: Publish the protocol package if its version is new
52
+ run: |
53
+ NAME=$(node -p "require('./package.json').name")
54
+ VERSION=$(node -p "require('./package.json').version")
55
+ if npm view "$NAME@$VERSION" version >/dev/null 2>&1; then
56
+ echo "::notice::$NAME@$VERSION is already published — skipping. Bump package.json to release it."
57
+ else
58
+ echo "Publishing $NAME@$VERSION"
59
+ npm publish --access public
60
+ fi
61
+
62
+ # The CLI is a second package in this repo with its own version. Publish it
63
+ # from the same tag, but only when its version is actually new — otherwise
64
+ # every release after an unchanged CLI would fail on "cannot publish over
65
+ # the previously published version".
66
+ #
67
+ # A skip and a publish must not look alike in the log: one means "nothing
68
+ # to do", the other means "shipped".
69
+ - name: Publish the CLI if its version is new
70
+ working-directory: cli
71
+ run: |
72
+ NAME=$(node -p "require('./package.json').name")
73
+ VERSION=$(node -p "require('./package.json').version")
74
+ if npm view "$NAME@$VERSION" version >/dev/null 2>&1; then
75
+ echo "::notice::$NAME@$VERSION is already published — skipping. Bump cli/package.json to release it."
76
+ else
77
+ echo "Publishing $NAME@$VERSION"
78
+ npm publish --access public
79
+ fi
package/README.md CHANGED
@@ -1,7 +1,9 @@
1
1
  # ajp-protocol
2
2
 
3
3
  The Agent Job Protocol — standard interaction layer for the agent internet.
4
- Part of the [Provenance Protocol](https://getprovenance.dev) family.
4
+ Built on the [Provenance Protocol](https://github.com/ilucky21c/provenance-protocol):
5
+ agents find and verify each other from their own signed declarations, with no
6
+ directory or index in between.
5
7
 
6
8
  ```bash
7
9
  npm install ajp-protocol
@@ -15,7 +17,7 @@ AJP defines how any party — a human, an agent, or an orchestrator — hands a
15
17
  job to another agent, tracks its progress, and receives the result.
16
18
 
17
19
  Three endpoints. Three message types. Runs over standard HTTP.
18
- Trust verification via `provenance-protocol` built in.
20
+ Every message is signed in full; identity is checked offline.
19
21
 
20
22
  ---
21
23
 
@@ -26,7 +28,7 @@ and job lifecycle automatically.
26
28
 
27
29
  ```js
28
30
  import { AJPServer, declarationKeyResolver, indexStandingCheck } from 'ajp-protocol';
29
- import { Provenance } from 'provenance-protocol';
31
+ import { Provenance } from 'provenance-protocol/index-client';
30
32
  import express from 'express';
31
33
 
32
34
  const app = express();
@@ -47,15 +49,17 @@ const server = new AJPServer({
47
49
  // This is the default — no index is consulted to check a signature.
48
50
  resolveSenderKey: declarationKeyResolver(),
49
51
 
52
+ // Requirements on what the sender declares are checked offline, against
53
+ // its verified declaration.
54
+ trustRequirements: { requireCapabilities: ['delegate:agents'] },
55
+
50
56
  // Standing (revocation, incidents, freshness) cannot be checked offline, so
51
57
  // it is opt-in and you choose whom to ask. Omit it entirely to accept any
52
58
  // sender whose identity verifies.
53
- checkStanding: indexStandingCheck(new Provenance(), {
54
- requireDeclared: true,
55
- requireCapabilities: ['delegate:agents'],
56
- requireClean: true,
57
- requireMinAge: 7,
58
- }),
59
+ checkStanding: indexStandingCheck(
60
+ new Provenance({ apiUrl: 'https://index.example.com' }), // an index you choose
61
+ { requireClean: true, requireMinAge: 7 }
62
+ ),
59
63
  onStandingUnavailable: 'deny', // an unreachable attester is not an accusation
60
64
 
61
65
  // Your agent logic — receives the job, returns the result
@@ -84,12 +88,14 @@ const client = new AJPClient({
84
88
  type: 'agent', // 'human' | 'agent' | 'orchestrator'
85
89
  provenance_id: 'provenance:github:alice/orchestrator',
86
90
  },
91
+ // Where the recipient's endpoint comes from: by default its own signed
92
+ // declaration, found from its provenance id. No index involved.
87
93
  // Agent/orchestrator callers sign with Ed25519 — no shared secret needed
88
94
  privateKey: process.env.PROVENANCE_PRIVATE_KEY,
89
95
  });
90
96
 
91
97
  const result = await client.send(
92
- 'provenance:github:bob/research-assistant', // who to hire
98
+ 'provenance:domain:research.bob.example', // who to hire
93
99
  {
94
100
  type: 'research',
95
101
  instruction: 'Find the top 3 papers on transformer attention in 2024.',
@@ -122,7 +128,7 @@ const result = await client.send(agentId, task, budget);
122
128
  ### Agent hiring an agent
123
129
  ```js
124
130
  // Agent callers sign with Ed25519 — no shared secret, no prior setup
125
- // The receiving agent verifies by fetching your public key from Provenance index
131
+ // The receiving agent verifies against the key in your published declaration
126
132
  const client = new AJPClient({
127
133
  from: { type: 'agent', provenance_id: 'provenance:github:alice/pipeline' },
128
134
  privateKey: process.env.PROVENANCE_PRIVATE_KEY,
@@ -149,12 +155,14 @@ Two separate questions, and only one of them needs a network service.
149
155
  AJPServer.receive()
150
156
  → validate the offer's shape
151
157
  → IDENTITY (offline, always)
152
- fetch the sender's declaration from from.declaration_url
158
+ fetch the sender's declaration (from.declaration_url, or the
159
+ standard location its provenance id names)
153
160
  → does it verify against the key inside it?
154
161
  → was it served from the location its provenance id names?
155
162
  → is that id the one the offer claims?
156
163
  → has this sender's key changed since last time?
157
164
  then check the offer's signature with that key
165
+ → does the declaration promise what trustRequirements ask?
158
166
  → STANDING (online, optional, you choose the attester)
159
167
  → revoked? open incidents? evidence stale? old enough?
160
168
  → unreachable attester → your policy, not a failed trust check
@@ -173,7 +181,7 @@ Platform-level auth is assumed for humans.
173
181
  ## Declare AJP in your PROVENANCE.yml
174
182
 
175
183
  ```yaml
176
- provenance: "0.1"
184
+ provenance: "0.2"
177
185
  name: "Research Assistant"
178
186
 
179
187
  capabilities:
@@ -186,8 +194,9 @@ ajp:
186
194
  version: "0.1"
187
195
  ```
188
196
 
189
- The Provenance crawler reads `ajp.endpoint` and indexes it.
190
- Senders can discover your endpoint without out-of-band configuration.
197
+ Senders read `ajp.endpoint` from your signed declaration, so they can find
198
+ you without out-of-band configuration — and without trusting anyone's copy of
199
+ it. Sign the declaration with `npx provenance-protocol sign`.
191
200
 
192
201
  ---
193
202
 
@@ -200,6 +209,7 @@ import { NextResponse } from 'next/server';
200
209
 
201
210
  const server = new AJPServer({
202
211
  provenanceId: process.env.PROVENANCE_ID,
212
+ privateKey: process.env.PROVENANCE_PRIVATE_KEY, // signs results
203
213
  secret: process.env.AJP_SECRET,
204
214
  onJob: async (job) => {
205
215
  // your agent logic
@@ -218,17 +228,17 @@ export async function POST(req) {
218
228
 
219
229
  | Package | Purpose |
220
230
  |---|---|
221
- | `provenance-protocol` | Query the agent identity index |
231
+ | [`provenance-protocol`](https://github.com/ilucky21c/provenance-protocol) | Declarations and attestations: sign, verify, locate — offline |
222
232
  | `ajp-protocol` | Send and receive agent jobs (this package) |
223
- | `PROVENANCE.yml` | Declare your agent's identity and capabilities |
233
+ | [`provenance-middleware`](https://github.com/ilucky21c/provenance-middleware) | Serve and sign your declaration from your own service |
224
234
 
225
235
  ---
226
236
 
227
237
  ## CLI
228
238
 
229
239
  ```bash
230
- # Send a job to any indexed agent from the terminal
231
- npx @ilucky21c/ajp-cli hire provenance:github:alice/summarizer \
240
+ # Send a job to any agent that publishes a declaration with ajp.endpoint
241
+ npx @ilucky21c/ajp-cli hire provenance:domain:summarizer.example.com \
232
242
  --instruction "Summarize this paper: https://arxiv.org/abs/..." \
233
243
  --budget 0.50 --timeout 60
234
244
 
@@ -236,16 +246,34 @@ npx @ilucky21c/ajp-cli hire provenance:github:alice/summarizer \
236
246
  npx @ilucky21c/ajp-cli jobs job_m0abc123 --endpoint https://alice-agent.example.com/api/agent
237
247
  ```
238
248
 
239
- Requires Provenance identity — set up first with `npx provenance-protocol keygen` and `npx provenance-protocol register`.
249
+ Requires a Provenance identity — `npx provenance-protocol keygen`, then publish a
250
+ signed declaration. `--endpoint <url>` skips resolution; `--index <url>`
251
+ resolves through an index you choose instead.
252
+
253
+ ---
240
254
 
241
- Full CLI reference: [getprovenance.dev/docs/ajp#cli](https://getprovenance.dev/docs/ajp#cli)
255
+ ## Upgrading from 0.2
256
+
257
+ - **Signatures now cover the whole message.** Before 0.3 only top-level keys were
258
+ signed and every nested field — the task, budget, sender, recipient, result —
259
+ was left out, so a signed job could be rewritten in transit. Old and new
260
+ versions do not interoperate; upgrade both sides. A 0.3 receiver answers an
261
+ old-style signature with `LEGACY_SIGNATURE`.
262
+ - `AJPClient` finds the recipient from its own declaration by default.
263
+ `provenanceApiUrl` is gone (passing it throws); use
264
+ `resolveEndpoint: indexEndpointResolver(new Provenance({ apiUrl }))` to keep
265
+ using an index, or `send(..., { endpoint })` to skip resolution.
266
+ - `trustRequirements.requireConstraints` / `requireCapabilities` are enforced
267
+ offline against the sender's declaration. Standing requirements
268
+ (`requireClean`, `requireMinAge`, …) without `checkStanding` now fail at
269
+ startup instead of being silently ignored.
242
270
 
243
271
  ---
244
272
 
245
- ## Full documentation
273
+ ## Full specification
246
274
 
247
- [getprovenance.dev/docs/ajp](https://getprovenance.dev/docs/ajp)
275
+ [spec/SPEC.md](./spec/SPEC.md)
248
276
 
249
277
  ---
250
278
 
251
- ## MIT License — getprovenance.dev
279
+ ## MIT License
package/cli/index.js CHANGED
@@ -2,18 +2,22 @@
2
2
  /**
3
3
  * ajp-cli — Agent Job Protocol CLI
4
4
  *
5
- * Requires Provenance identity (PROVENANCE_ID + PROVENANCE_PRIVATE_KEY).
6
- * For identity setup: npx provenance-protocol keygen / npx provenance-protocol register
5
+ * Requires a Provenance identity (PROVENANCE_ID + PROVENANCE_PRIVATE_KEY).
6
+ * For identity setup: npx provenance-protocol keygen, then publish a signed
7
+ * declaration (npx provenance-protocol sign).
7
8
  *
8
9
  * Usage:
9
10
  * ajp hire <provenance_id> --instruction <text> [--budget <usd>] [--timeout <s>]
10
11
  * ajp jobs <job_id> --endpoint <url>
11
12
  */
12
13
 
13
- import { createPrivateKey, sign as nodeSign, randomBytes } from 'crypto';
14
+ import { randomBytes } from 'crypto';
15
+ import { createRequire } from 'module';
16
+ import YAML from 'yaml';
17
+ import { signWithKey, declarationEndpointResolver, indexEndpointResolver } from 'ajp-protocol';
18
+ import { Provenance } from 'provenance-protocol/index-client';
14
19
 
15
- const API = process.env.PROVENANCE_API_URL || 'https://getprovenance.dev';
16
- const VERSION = '0.1.0';
20
+ const VERSION = createRequire(import.meta.url)('./package.json').version;
17
21
 
18
22
  // ── Colours ───────────────────────────────────────────────────────────────────
19
23
 
@@ -50,11 +54,16 @@ function generateJobId() {
50
54
  return `job_${Date.now().toString(36)}${randomBytes(6).toString('hex')}`;
51
55
  }
52
56
 
53
- function signOffer(offer, privateKeyBase64) {
54
- const { signature: _, ...rest } = offer;
55
- const canonical = JSON.stringify(rest, Object.keys(rest).sort());
56
- const key = createPrivateKey({ key: Buffer.from(privateKeyBase64, 'base64'), format: 'der', type: 'pkcs8' });
57
- return `ed25519:${nodeSign(null, Buffer.from(canonical, 'utf8'), key).toString('base64')}`;
57
+ // Where to send the job. By default, read from the recipient's own signed
58
+ // declaration — no index involved. --endpoint skips resolution; --index asks
59
+ // an index you name instead.
60
+ async function resolveEndpoint(targetId, args) {
61
+ if (typeof args.endpoint === 'string') return args.endpoint.replace(/\/$/, '');
62
+ const index = args.index || process.env.PROVENANCE_INDEX_URL;
63
+ const resolver = typeof index === 'string'
64
+ ? indexEndpointResolver(new Provenance({ apiUrl: index }))
65
+ : declarationEndpointResolver({ parseDeclaration: (text) => YAML.parse(text) });
66
+ return resolver(targetId);
58
67
  }
59
68
 
60
69
  // ── Commands ──────────────────────────────────────────────────────────────────
@@ -70,19 +79,19 @@ async function cmdHire(args) {
70
79
  if (!targetId) { console.error(err('Usage: ajp hire <provenance_id> --instruction <text>')); process.exit(1); }
71
80
  if (!instruction) { console.error(err('--instruction required')); process.exit(1); }
72
81
  if (!privateKey) { console.error(err('PROVENANCE_PRIVATE_KEY not set. Run: npx provenance-protocol keygen')); process.exit(1); }
73
- if (!provenanceId) { console.error(err('PROVENANCE_ID not set. Run: npx provenance-protocol register')); process.exit(1); }
82
+ if (!provenanceId) { console.error(err('PROVENANCE_ID not set — your agent\'s provenance id')); process.exit(1); }
74
83
 
75
84
  console.log(`\n${amb('Hiring')} ${hi(targetId)}...\n`);
76
85
 
77
- // Resolve endpoint
78
86
  process.stdout.write(dim(' Resolving endpoint...'));
79
- const agentRes = await fetch(`${API}/api/agent/${targetId.replace('provenance:', '').replace(':', '/')}`);
80
- const agentData = await agentRes.json();
81
- if (!agentData?.ajp?.endpoint) {
82
- console.log('\n' + err('Agent has no AJP endpoint. Ask them to add ajp.endpoint to PROVENANCE.yml.'));
83
- process.exit(1);
87
+ let endpoint;
88
+ try {
89
+ endpoint = await resolveEndpoint(targetId, args);
90
+ } catch (e) {
91
+ console.log('\n' + err(e.message));
92
+ console.log(dim(' Nothing was sent. Pass --endpoint <url> if you know where the agent accepts jobs.'));
93
+ process.exit(2);
84
94
  }
85
- const endpoint = agentData.ajp.endpoint.replace(/\/$/, '');
86
95
  console.log(` ${c.green}${endpoint}${c.reset}`);
87
96
 
88
97
  // Build and sign offer
@@ -92,7 +101,10 @@ async function cmdHire(args) {
92
101
 
93
102
  const offer = {
94
103
  ajp: '0.1', job_id: jobId, parent_job_id: null,
95
- from: { type: 'orchestrator', id: null, provenance_id: provenanceId },
104
+ from: {
105
+ type: 'orchestrator', id: null, provenance_id: provenanceId,
106
+ declaration_url: args['declaration-url'] || process.env.PROVENANCE_DECLARATION_URL || null,
107
+ },
96
108
  to: { provenance_id: targetId },
97
109
  task: { type: 'task', instruction, input: {}, output_format: 'json' },
98
110
  context: { credentials: {}, memory: [], constraints: [] },
@@ -102,7 +114,7 @@ async function cmdHire(args) {
102
114
  expires_at: expiresAt.toISOString(),
103
115
  signature: '',
104
116
  };
105
- offer.signature = signOffer(offer, privateKey);
117
+ offer.signature = signWithKey(offer, privateKey);
106
118
 
107
119
  // Submit
108
120
  process.stdout.write(dim(' Submitting job...'));
@@ -191,17 +203,21 @@ ${amb('Commands:')}
191
203
  [--timeout <seconds>] Max wait time (default: 120)
192
204
  [--from-id <id>] Your Provenance ID (default: $PROVENANCE_ID)
193
205
  [--private-key <key>] Your private key (default: $PROVENANCE_PRIVATE_KEY)
206
+ [--declaration-url <url>] Where your signed declaration is published
207
+ [--endpoint <url>] Send here instead of reading the recipient's declaration
208
+ [--index <url>] Resolve the recipient through this index instead
194
209
 
195
210
  ${hi('jobs')} <job_id> Check status of a job
196
211
  --endpoint <url> The agent's AJP endpoint URL
197
212
 
198
213
  ${amb('Environment variables:')}
199
- PROVENANCE_ID Your Provenance ID (set up with: npx provenance-protocol register)
200
- PROVENANCE_PRIVATE_KEY Your Ed25519 private key (set up with: npx provenance-protocol keygen)
201
- PROVENANCE_API_URL Override Provenance API base URL
214
+ PROVENANCE_ID Your provenance id
215
+ PROVENANCE_PRIVATE_KEY Your Ed25519 private key (npx provenance-protocol keygen)
216
+ PROVENANCE_DECLARATION_URL Where your signed declaration is published
217
+ PROVENANCE_INDEX_URL Resolve recipients through this index (optional)
202
218
 
203
219
  ${amb('Examples:')}
204
- ajp hire provenance:github:alice/summarizer \\
220
+ ajp hire provenance:domain:summarizer.example.com \\
205
221
  --instruction "Summarize https://arxiv.org/abs/2501.00001" \\
206
222
  --budget 0.50 --timeout 60
207
223
 
@@ -209,8 +225,9 @@ ${amb('Examples:')}
209
225
 
210
226
  ${amb('Identity setup (first time):')}
211
227
  npx provenance-protocol keygen
212
- npx provenance-protocol register --id provenance:github:your-org/your-agent --url <url>
213
- ${dim('Then set PROVENANCE_ID and PROVENANCE_PRIVATE_KEY in your environment.')}
228
+ npx provenance-protocol sign PROVENANCE.yml
229
+ ${dim('Publish the declaration where your provenance id says, then set')}
230
+ ${dim('PROVENANCE_ID and PROVENANCE_PRIVATE_KEY in your environment.')}
214
231
  `);
215
232
  }
216
233
 
package/cli/package.json CHANGED
@@ -1,17 +1,31 @@
1
1
  {
2
2
  "name": "@ilucky21c/ajp-cli",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "CLI for the Agent Job Protocol — hire agents and check job status from the terminal",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "ajp": "./index.js"
8
8
  },
9
- "engines": { "node": ">=18" },
9
+ "engines": {
10
+ "node": ">=18"
11
+ },
10
12
  "repository": {
11
13
  "type": "git",
12
14
  "url": "git+https://github.com/ilucky21c/ajp-protocol.git",
13
15
  "directory": "cli"
14
16
  },
15
- "keywords": ["ajp", "agent-job-protocol", "provenance", "ai", "agents", "cli"],
16
- "license": "MIT"
17
+ "keywords": [
18
+ "ajp",
19
+ "agent-job-protocol",
20
+ "provenance",
21
+ "ai",
22
+ "agents",
23
+ "cli"
24
+ ],
25
+ "license": "MIT",
26
+ "dependencies": {
27
+ "ajp-protocol": "^0.3.0",
28
+ "provenance-protocol": "^0.6.0",
29
+ "yaml": "^2.9.1"
30
+ }
17
31
  }
@@ -3,10 +3,10 @@
3
3
  // It hires a specialist PDF agent to handle that step.
4
4
  //
5
5
  // Key difference from human→agent: the receiving agent
6
- // MUST verify the sender's Provenance ID before accepting.
6
+ // MUST verify the sender's Provenance ID before accepting — offline, against
7
+ // the sender's own signed declaration.
7
8
 
8
9
  import { AJPClient } from 'ajp-protocol';
9
- import { provenance } from 'provenance-protocol';
10
10
 
11
11
  // ── Sending side (data pipeline agent) ───────────────────────────────────
12
12
 
@@ -15,12 +15,15 @@ const client = new AJPClient({
15
15
  type: 'agent',
16
16
  provenance_id: 'provenance:github:alice/data-pipeline',
17
17
  },
18
- secret: process.env.AJP_SECRET,
18
+ // Agents sign with their own key; the recipient checks it against the
19
+ // declaration Alice publishes in her repository.
20
+ privateKey: process.env.PROVENANCE_PRIVATE_KEY,
19
21
  });
20
22
 
21
- // Send job to a specialist PDF extractor
23
+ // Send job to a specialist PDF extractor. Its endpoint is read from its own
24
+ // signed declaration at https://pdf.bob.example/.well-known/provenance.json.
22
25
  const result = await client.send(
23
- 'provenance:pypi:bob-pdf-extractor',
26
+ 'provenance:domain:pdf.bob.example',
24
27
  {
25
28
  type: 'extract',
26
29
  instruction: 'Extract all tables from this PDF and return them as structured JSON.',
@@ -41,15 +44,15 @@ console.log(result.output.tables);
41
44
  import { AJPServer } from 'ajp-protocol';
42
45
 
43
46
  const server = new AJPServer({
44
- provenanceId: 'provenance:pypi:bob-pdf-extractor',
45
- secret: process.env.AJP_SECRET,
47
+ provenanceId: 'provenance:domain:pdf.bob.example',
48
+ privateKey: process.env.PROVENANCE_PRIVATE_KEY, // signs results
46
49
 
47
- // Trust requirements for incoming agent jobs
50
+ // Checked offline against the sender's verified declaration.
48
51
  trustRequirements: {
49
- requireDeclared: true, // sender must have PROVENANCE.yml
50
- requireClean: true, // no open incidents
51
- requireMinAge: 7, // not a brand-new agent
52
+ requireConstraints: ['no:pii'],
52
53
  },
54
+ // Standing (incidents, age) needs someone to ask — add checkStanding with
55
+ // an attester you choose if you want requireClean or requireMinAge.
53
56
 
54
57
  onJob: async (job) => {
55
58
  // job.from.provenance_id already verified by AJPServer
@@ -9,7 +9,7 @@ const client = new AJPClient({
9
9
  type: 'orchestrator',
10
10
  provenance_id: 'provenance:github:alice/research-orchestrator',
11
11
  },
12
- secret: process.env.AJP_SECRET,
12
+ privateKey: process.env.PROVENANCE_PRIVATE_KEY, // orchestrators sign with their own key
13
13
  });
14
14
 
15
15
  const PARENT_JOB_ID = 'job_parent_01J8X2K9M3N4P5Q6';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ajp-protocol",
3
- "version": "0.2.1",
4
- "description": "Agent Job Protocol \u2014 standard interaction layer for the agent internet",
3
+ "version": "0.3.0",
4
+ "description": "Agent Job Protocol — standard interaction layer for the agent internet",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
7
7
  "exports": {
@@ -17,14 +17,14 @@
17
17
  ],
18
18
  "license": "MIT",
19
19
  "dependencies": {
20
- "provenance-protocol": "^0.2.2"
20
+ "provenance-protocol": "^0.6.0"
21
21
  },
22
22
  "repository": {
23
23
  "type": "git",
24
24
  "url": "https://github.com/ilucky21c/ajp-protocol"
25
25
  },
26
- "homepage": "https://getprovenance.dev/ajp",
26
+ "homepage": "https://github.com/ilucky21c/ajp-protocol#readme",
27
27
  "scripts": {
28
- "test": "node test/trust.test.mjs"
28
+ "test": "node test/trust.test.mjs && node test/resolution.test.mjs"
29
29
  }
30
30
  }
package/spec/SPEC.md CHANGED
@@ -27,8 +27,9 @@ inside is yours to define. An agent that searches the web and an agent that
27
27
  processes invoices use the same protocol.
28
28
 
29
29
  **Trust is built in, not bolted on.** Every JobOffer is signed by the sender.
30
- Every receiver verifies the sender against the Provenance index before accepting.
31
- Trust verification is part of the protocol, not optional middleware.
30
+ Every receiver verifies the sender against the sender's own signed Provenance
31
+ declaration before accepting — offline, with no index involved. Trust
32
+ verification is part of the protocol, not optional middleware.
32
33
 
33
34
  **Three parties, same protocol.** A human hiring an agent, an agent hiring an
34
35
  agent, and an orchestrator delegating to sub-agents all use identical message
@@ -46,7 +47,7 @@ types. The `from` field distinguishes them.
46
47
 
47
48
  A human (or a platform acting on their behalf) sends a JobOffer to an agent.
48
49
  The `from.type` is `human`. No Provenance verification of the sender is required
49
- — humans are not indexed agents. Platform-level auth handles human identity.
50
+ — humans do not publish agent declarations. Platform-level auth handles human identity.
50
51
 
51
52
  ```
52
53
  Human / Platform ──JobOffer──► Agent
@@ -222,7 +223,7 @@ Status values: `accepted` `running` `completed` `failed` `rejected` `expired`
222
223
  }
223
224
  ```
224
225
 
225
- `constraints_asserted` lists the constraints the agent declares it honored during this job. The field is included in the signed payload — the signature ties the assertion to the agent's registered Provenance identity. Self-reported, but attributable: a false assertion is a cryptographic receipt of the lie, actionable via the Provenance incidents system.
226
+ `constraints_asserted` lists the constraints the agent declares it honored during this job. The field is included in the signed payload — the signature ties the assertion to the key in the agent's declaration. Self-reported, but attributable: a false assertion is a cryptographic receipt of the lie, which the receiver can hand to any attester as evidence (a Provenance `report` attestation can reference it).
226
227
 
227
228
  ---
228
229
 
@@ -236,7 +237,9 @@ questions, and only one of them needs a network service.
236
237
  *Is this signature really from the party named in `from`?*
237
238
 
238
239
  The sender publishes its signed declaration and points at it with
239
- `from.declaration_url`. The receiver fetches that file and verifies it locally:
240
+ `from.declaration_url`; when absent, the receiver uses the standard location its
241
+ `provenance_id` names (see *Where a declaration lives* in the Provenance
242
+ specification). The receiver fetches that file and verifies it locally:
240
243
 
241
244
  ```js
242
245
  import { verifyDeclaration } from 'provenance-protocol/verify';
@@ -275,17 +278,25 @@ NOT hardcode a single provider.
275
278
 
276
279
  ```js
277
280
  import { AJPServer, declarationKeyResolver, indexStandingCheck } from 'ajp-protocol';
278
- import { Provenance } from 'provenance-protocol';
281
+ import { Provenance } from 'provenance-protocol/index-client';
282
+
283
+ const index = new Provenance({ apiUrl: 'https://index.example.com' }); // an index you choose
279
284
 
280
285
  const server = new AJPServer({
281
- provenanceId, onJob,
282
- resolveSenderKey: declarationKeyResolver(), // offline, the default
283
- checkStanding: indexStandingCheck(new Provenance(), // opt-in, swappable
284
- { requireConstraints: ['no:pii'], requireClean: true, requireMinAge: 7 }),
286
+ provenanceId, privateKey, onJob,
287
+ resolveSenderKey: declarationKeyResolver(), // offline, the default
288
+ trustRequirements: { requireConstraints: ['no:pii'], requireClean: true, requireMinAge: 7 },
289
+ checkStanding: indexStandingCheck(index, { requireClean: true, requireMinAge: 7 }), // opt-in, swappable
285
290
  onStandingUnavailable: 'deny',
286
291
  });
287
292
  ```
288
293
 
294
+ Requirements on **declared** fields (`requireConstraints`, `requireCapabilities`)
295
+ are checked against the sender's verified declaration, offline. Requirements on
296
+ **standing** (`requireClean`, `requireMinAge`, …) need a standing source; an
297
+ implementation MUST refuse to start with them configured and no source, rather
298
+ than accept jobs while ignoring them.
299
+
289
300
  A receiver that performs a standing check MUST distinguish *checked and failed*
290
301
  from *could not check*, and MUST state which way it fails when the check is
291
302
  unavailable. Reporting an unreachable attester as a failed trust check turns
@@ -299,7 +310,16 @@ offer, and the shared-secret signature covers it.
299
310
  ## Signature
300
311
 
301
312
  Every JobOffer and JobResult is signed by the sender. The signature covers the
302
- full message body excluding the `signature` field itself, with keys sorted canonically.
313
+ full message body excluding the `signature` field itself — every field at every
314
+ depth. `canonical(body)` is the canonical JSON defined by the Provenance
315
+ Protocol, which is the JSON Canonicalization Scheme (JCS, RFC 8785): object
316
+ keys sorted by UTF-16 code units at every depth, no insignificant whitespace.
317
+
318
+ Implementations before `ajp-protocol` 0.3 serialised only the top-level keys:
319
+ nested objects — `task`, `budget`, `from`, `to`, `context`, `output` — were
320
+ emptied before signing, so they were not covered and could be altered without
321
+ detection. A receiver MUST NOT accept such signatures. It MAY recognise one in
322
+ order to tell the sender to upgrade.
303
323
 
304
324
  **Human senders** (no Provenance identity) — HMAC-SHA256 with a shared secret:
305
325
  ```
@@ -311,8 +331,12 @@ signature = "sha256:" + hex(HMAC-SHA256(canonical(body), sender_secret))
311
331
  signature = "ed25519:" + base64(Ed25519Sign(canonical(body), provenance_private_key))
312
332
  ```
313
333
 
314
- The receiving server verifies agent signatures by fetching the sender's public key from the
315
- Provenance index. This ties every message to a registered identity without a shared secret.
334
+ The receiving server verifies agent signatures against the public key in the
335
+ sender's own verified declaration (see Trust verification). This ties every
336
+ message to a published identity without a shared secret or an index.
337
+
338
+ A sender SHOULD verify a JobResult's signature the same way, against the key in
339
+ the recipient's declaration, before relying on it.
316
340
 
317
341
  The `ajp-protocol` SDK handles signing and verification automatically based on `from.type`.
318
342
 
@@ -369,7 +393,7 @@ const result = await client.send(
369
393
  Agents that implement AJP should declare it:
370
394
 
371
395
  ```yaml
372
- provenance: "0.1"
396
+ provenance: "0.2"
373
397
  name: "Research Assistant"
374
398
 
375
399
  capabilities:
@@ -382,8 +406,11 @@ ajp:
382
406
  version: "0.1"
383
407
  ```
384
408
 
385
- The Provenance crawler reads the `ajp.endpoint` field and indexes it. Senders
386
- can discover an agent's AJP endpoint without out-of-band communication.
409
+ A sender finds an agent's AJP endpoint by fetching the agent's declaration from
410
+ the location its `provenance_id` names, verifying it, and reading
411
+ `ajp.endpoint`. Because the declaration is signed and tied to its location, the
412
+ endpoint is the operator's own statement — no directory or index is needed.
413
+ Indexes may also record it, as a convenience.
387
414
 
388
415
  ---
389
416
 
@@ -407,6 +434,5 @@ version. Future versions add fields, never remove them.
407
434
 
408
435
  ---
409
436
 
410
- *AJP v0.1 — Provenance Protocol Family — MIT License*
411
- *https://getprovenance.dev/ajp*
437
+ *AJP v0.1 — MIT License*
412
438
  *https://github.com/ilucky21c/ajp-protocol*