ajp-protocol 0.1.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/spec/SPEC.md ADDED
@@ -0,0 +1,333 @@
1
+ # Agent Job Protocol (AJP)
2
+ **Version 0.1 — Provenance Protocol Family**
3
+
4
+ ---
5
+
6
+ ## What is AJP?
7
+
8
+ AJP is the standard interaction layer for the agent internet.
9
+
10
+ It defines how any party — a human, an agent, or an orchestrator — hands a job
11
+ to another agent, tracks its progress, and receives the result. The envelope is
12
+ always the same. The task inside varies by domain.
13
+
14
+ Think of it as HTTP for agent work. HTTP defines how messages travel across the
15
+ web without caring what the content is. AJP defines how jobs travel between
16
+ agents without caring what the job is.
17
+
18
+ It is intentionally minimal. Three endpoints. Three message types. JSON
19
+ throughout. Runs over standard HTTP. No new infrastructure required.
20
+
21
+ ---
22
+
23
+ ## Design principles
24
+
25
+ **One envelope, any task.** The JobOffer wrapper is universal. The `task` field
26
+ inside is yours to define. An agent that searches the web and an agent that
27
+ processes invoices use the same protocol.
28
+
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.
32
+
33
+ **Three parties, same protocol.** A human hiring an agent, an agent hiring an
34
+ agent, and an orchestrator delegating to sub-agents all use identical message
35
+ types. The `from` field distinguishes them.
36
+
37
+ **Async by default.** Jobs are accepted and executed asynchronously. The
38
+ `callback` field tells the receiver where to send the result. Polling via
39
+ `GET /jobs/:id` is also supported for simpler implementations.
40
+
41
+ ---
42
+
43
+ ## The three use cases
44
+
45
+ ### 1. Human hiring an agent
46
+
47
+ A human (or a platform acting on their behalf) sends a JobOffer to an agent.
48
+ 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
+
51
+ ```
52
+ Human / Platform ──JobOffer──► Agent
53
+ ◄──JobResult──
54
+ ```
55
+
56
+ ### 2. Agent hiring an agent
57
+
58
+ An agent sends a JobOffer to another agent. The `from.type` is `agent`. The
59
+ receiving agent MUST verify the sender's Provenance ID before accepting.
60
+ The sender must have `delegate:agents` in its declared capabilities.
61
+
62
+ ```
63
+ Agent A ──JobOffer──► Agent B
64
+ ◄──JobResult──
65
+ ```
66
+
67
+ ### 3. Orchestrator delegating to sub-agents
68
+
69
+ An orchestrator (itself an agent with a Provenance ID) breaks a task into
70
+ subtasks and delegates each to a specialist agent. The full chain is auditable —
71
+ every job references its `parent_job_id`, allowing reconstruction of the
72
+ complete execution tree.
73
+
74
+ ```
75
+ Orchestrator ──JobOffer──► Sub-agent A
76
+ ──JobOffer──► Sub-agent B
77
+ ◄──JobResult── Sub-agent A
78
+ ◄──JobResult── Sub-agent B
79
+ ```
80
+
81
+ ---
82
+
83
+ ## Endpoints
84
+
85
+ Every AJP-compliant agent exposes three endpoints:
86
+
87
+ ### POST /jobs
88
+ Receive a new job offer.
89
+
90
+ **Request body:** `JobOffer`
91
+ **Response 202:** `{ job_id, status: "accepted" }`
92
+ **Response 400:** `{ error, reason }` — malformed offer
93
+ **Response 403:** `{ error, reason }` — trust check failed
94
+ **Response 402:** `{ error, reason }` — budget insufficient
95
+ **Response 429:** `{ error, retry_after }` — agent busy
96
+
97
+ ### GET /jobs/:job_id
98
+ Check the status of a job.
99
+
100
+ **Response 200:** `JobStatus`
101
+ **Response 404:** job not found
102
+
103
+ ### POST /jobs/:job_id/ack
104
+ Confirm result received. Triggers payment settlement if applicable.
105
+
106
+ **Request body:** `{ received: true, feedback?: string }`
107
+ **Response 200:** `{ settled: true }`
108
+
109
+ ---
110
+
111
+ ## Message types
112
+
113
+ ### JobOffer
114
+
115
+ ```json
116
+ {
117
+ "ajp": "0.1",
118
+ "job_id": "job_01J8X2K9M3N4P5Q6R7S8T9U0V1",
119
+ "parent_job_id": null,
120
+
121
+ "from": {
122
+ "type": "human",
123
+ "id": "user_abc123",
124
+ "provenance_id": null
125
+ },
126
+
127
+ "to": {
128
+ "provenance_id": "provenance:github:alice/research-assistant"
129
+ },
130
+
131
+ "task": {
132
+ "type": "research",
133
+ "instruction": "Find the three most cited papers on transformer attention mechanisms published in 2024. Return titles, authors, citation counts, and a 2-sentence summary of each.",
134
+ "input": {},
135
+ "output_format": "json"
136
+ },
137
+
138
+ "context": {
139
+ "credentials": {},
140
+ "memory": [],
141
+ "constraints": []
142
+ },
143
+
144
+ "budget": {
145
+ "max_usd": 0.50,
146
+ "max_seconds": 120,
147
+ "max_llm_tokens": 10000
148
+ },
149
+
150
+ "callback": {
151
+ "url": "https://clawmarket.com/api/jobs/job_01J8X2K9M3N4P5Q6R7S8T9U0V1/result",
152
+ "headers": { "Authorization": "Bearer token_xyz" }
153
+ },
154
+
155
+ "issued_at": "2026-03-05T10:00:00Z",
156
+ "expires_at": "2026-03-05T10:02:00Z",
157
+
158
+ "signature": "sha256:a1b2c3d4..."
159
+ }
160
+ ```
161
+
162
+ ### JobStatus
163
+
164
+ ```json
165
+ {
166
+ "ajp": "0.1",
167
+ "job_id": "job_01J8X2K9M3N4P5Q6R7S8T9U0V1",
168
+ "status": "running",
169
+ "progress": 0.4,
170
+ "message": "Found 2 of 3 papers, searching for third",
171
+ "started_at": "2026-03-05T10:00:01Z",
172
+ "updated_at": "2026-03-05T10:00:08Z",
173
+ "estimated_completion": "2026-03-05T10:00:20Z"
174
+ }
175
+ ```
176
+
177
+ Status values: `accepted` `running` `completed` `failed` `rejected` `expired`
178
+
179
+ ### JobResult
180
+
181
+ ```json
182
+ {
183
+ "ajp": "0.1",
184
+ "job_id": "job_01J8X2K9M3N4P5Q6R7S8T9U0V1",
185
+ "status": "completed",
186
+
187
+ "output": {
188
+ "papers": [
189
+ {
190
+ "title": "Flash Attention 3",
191
+ "authors": ["Tri Dao", "Daniel Y. Fu"],
192
+ "citations": 412,
193
+ "summary": "..."
194
+ }
195
+ ]
196
+ },
197
+
198
+ "usage": {
199
+ "llm_tokens": 4821,
200
+ "duration_seconds": 18,
201
+ "cost_usd": 0.12
202
+ },
203
+
204
+ "agent": {
205
+ "provenance_id": "provenance:github:alice/research-assistant",
206
+ "version": "1.2.0",
207
+ "model": { "provider": "anthropic", "model_id": "claude-sonnet-4-5" }
208
+ },
209
+
210
+ "completed_at": "2026-03-05T10:00:19Z",
211
+ "signature": "sha256:e5f6g7h8..."
212
+ }
213
+ ```
214
+
215
+ ---
216
+
217
+ ## Trust verification
218
+
219
+ When `from.type` is `agent`, the receiving agent MUST run a trust check before
220
+ accepting the job. Using the `provenance-protocol` SDK:
221
+
222
+ ```js
223
+ import { provenance } from 'provenance-protocol';
224
+
225
+ const result = await provenance.gate(offer.from.provenance_id, {
226
+ requireDeclared: true,
227
+ requireConstraints: [], // add what your agent requires
228
+ requireClean: true,
229
+ requireMinAge: 7, // don't accept jobs from brand-new agents
230
+ });
231
+
232
+ if (!result.allowed) {
233
+ return res.status(403).json({ error: 'Trust check failed', reason: result.reason });
234
+ }
235
+ ```
236
+
237
+ For `from.type === 'human'`, trust verification is handled by the platform
238
+ (ClawMarket, SkillsMP, etc.) before the JobOffer is issued.
239
+
240
+ ---
241
+
242
+ ## Signature
243
+
244
+ Every JobOffer and JobResult is signed by the sender. The signature covers the
245
+ full message body excluding the `signature` field itself.
246
+
247
+ ```
248
+ signature = "sha256:" + hex(HMAC-SHA256(JSON.stringify(body_without_signature), sender_secret))
249
+ ```
250
+
251
+ Receiving agents verify signatures before processing. The `ajp-protocol` SDK
252
+ handles signing and verification automatically.
253
+
254
+ ---
255
+
256
+ ## Adding AJP to your agent
257
+
258
+ ### Expose the three endpoints
259
+
260
+ ```js
261
+ import { AJPServer } from 'ajp-protocol';
262
+
263
+ const server = new AJPServer({
264
+ provenanceId: 'provenance:github:alice/research-assistant',
265
+ secret: process.env.AJP_SECRET,
266
+ onJob: async (job) => {
267
+ // your agent logic here
268
+ return { papers: [...] };
269
+ },
270
+ });
271
+
272
+ // Express / Next.js / any HTTP framework
273
+ app.post('/jobs', server.receive());
274
+ app.get('/jobs/:id', server.status());
275
+ app.post('/jobs/:id/ack', server.ack());
276
+ ```
277
+
278
+ ### Send a job to another agent
279
+
280
+ ```js
281
+ import { AJPClient } from 'ajp-protocol';
282
+
283
+ const client = new AJPClient({
284
+ from: { type: 'agent', provenance_id: 'provenance:github:alice/orchestrator' },
285
+ secret: process.env.AJP_SECRET,
286
+ });
287
+
288
+ const result = await client.send(
289
+ 'provenance:github:bob/pdf-extractor',
290
+ {
291
+ type: 'extract',
292
+ instruction: 'Extract all tables from this PDF',
293
+ input: { url: 'https://example.com/report.pdf' },
294
+ },
295
+ { max_usd: 0.25, max_seconds: 60 }
296
+ );
297
+ ```
298
+
299
+ ---
300
+
301
+ ## PROVENANCE.yml integration
302
+
303
+ Agents that implement AJP should declare it:
304
+
305
+ ```yaml
306
+ provenance: "0.1"
307
+ name: "Research Assistant"
308
+
309
+ capabilities:
310
+ - read:web
311
+ - ajp:receiver # this agent accepts AJP jobs
312
+ - ajp:sender # this agent can send AJP jobs to others
313
+
314
+ ajp:
315
+ endpoint: "https://alice.dev/api/agent/jobs"
316
+ version: "0.1"
317
+ ```
318
+
319
+ The Provenance crawler reads the `ajp.endpoint` field and indexes it. Senders
320
+ can discover an agent's AJP endpoint without out-of-band communication.
321
+
322
+ ---
323
+
324
+ ## Versioning
325
+
326
+ The `ajp` field in every message declares the spec version. `0.1` is the current
327
+ version. Future versions add fields, never remove them.
328
+
329
+ ---
330
+
331
+ *AJP v0.1 — Provenance Protocol Family — MIT License*
332
+ *https://provenance.dev/ajp*
333
+ *https://github.com/provenance-protocol/ajp*
package/src/client.js ADDED
@@ -0,0 +1,196 @@
1
+ /**
2
+ * AJPClient — Send jobs to other agents.
3
+ *
4
+ * Used by: humans (via platforms), agents, orchestrators.
5
+ * Consistent with provenance-protocol SDK class structure.
6
+ */
7
+
8
+ import { sign, generateJobId, validateOffer } from './utils.js';
9
+ import { Provenance } from 'provenance-protocol';
10
+
11
+ export class AJPClient {
12
+
13
+ /**
14
+ * @param {object} opts
15
+ * @param {object} opts.from — sender identity
16
+ * @param {string} opts.from.type — 'human' | 'agent' | 'orchestrator'
17
+ * @param {string} [opts.from.id] — platform user ID (human only)
18
+ * @param {string} [opts.from.provenance_id] — required for agent/orchestrator
19
+ * @param {string} opts.secret — HMAC signing secret
20
+ * @param {string} [opts.provenanceApiUrl] — override Provenance API URL
21
+ * @param {number} [opts.defaultTimeoutMs] — default job timeout in ms (30s)
22
+ */
23
+ constructor({ from, secret, provenanceApiUrl, defaultTimeoutMs = 30000 }) {
24
+ this.from = from;
25
+ this.secret = secret;
26
+ this.defaultTimeoutMs = defaultTimeoutMs;
27
+ this.provenance = new Provenance({ apiUrl: provenanceApiUrl });
28
+
29
+ if ((from.type === 'agent' || from.type === 'orchestrator') && !from.provenance_id) {
30
+ throw new Error('from.provenance_id required when type is agent or orchestrator');
31
+ }
32
+ }
33
+
34
+ // ── Main send method ──────────────────────────────────────────────────
35
+
36
+ /**
37
+ * Send a job to an agent and wait for the result.
38
+ *
39
+ * @param {string} toProvenanceId — receiving agent's Provenance ID
40
+ * @param {object} task — { type, instruction, input?, output_format? }
41
+ * @param {object} [budget] — { max_usd, max_seconds?, max_llm_tokens? }
42
+ * @param {object} [opts]
43
+ * @param {string} [opts.parentJobId] — set for sub-tasks in orchestration
44
+ * @param {object} [opts.context] — { credentials?, memory?, constraints? }
45
+ * @param {object} [opts.callback] — { url, headers? } for async delivery
46
+ * @param {number} [opts.pollIntervalMs] — how often to poll for result (2000)
47
+ * @returns {Promise<JobResult>}
48
+ */
49
+ async send(toProvenanceId, task, budget = {}, opts = {}) {
50
+ // Resolve the agent's AJP endpoint from Provenance
51
+ const endpoint = await this._resolveEndpoint(toProvenanceId);
52
+
53
+ // Build the job offer
54
+ const jobId = generateJobId();
55
+ const now = new Date();
56
+ const expiresAt = new Date(now.getTime() + (budget.max_seconds || 120) * 1000);
57
+
58
+ const offer = {
59
+ ajp: '0.1',
60
+ job_id: jobId,
61
+ parent_job_id: opts.parentJobId || null,
62
+
63
+ from: {
64
+ type: this.from.type,
65
+ id: this.from.id || null,
66
+ provenance_id: this.from.provenance_id || null,
67
+ },
68
+
69
+ to: { provenance_id: toProvenanceId },
70
+
71
+ task: {
72
+ type: task.type,
73
+ instruction: task.instruction,
74
+ input: task.input || {},
75
+ output_format: task.output_format || 'json',
76
+ },
77
+
78
+ context: {
79
+ credentials: opts.context?.credentials || {},
80
+ memory: opts.context?.memory || [],
81
+ constraints: opts.context?.constraints || [],
82
+ },
83
+
84
+ budget: {
85
+ max_usd: budget.max_usd ?? 1.0,
86
+ max_seconds: budget.max_seconds ?? 120,
87
+ max_llm_tokens: budget.max_llm_tokens ?? 10000,
88
+ },
89
+
90
+ callback: opts.callback || null,
91
+ issued_at: now.toISOString(),
92
+ expires_at: expiresAt.toISOString(),
93
+ signature: '',
94
+ };
95
+
96
+ // Sign it
97
+ offer.signature = sign(offer, this.secret);
98
+
99
+ // Validate before sending
100
+ const { valid, errors } = validateOffer(offer);
101
+ if (!valid) throw new Error(`Invalid JobOffer: ${errors.join(', ')}`);
102
+
103
+ // Send
104
+ const res = await fetch(`${endpoint}/jobs`, {
105
+ method: 'POST',
106
+ headers: { 'Content-Type': 'application/json' },
107
+ body: JSON.stringify(offer),
108
+ });
109
+
110
+ if (res.status === 403) {
111
+ const body = await res.json();
112
+ throw new Error(`Agent rejected job (trust check failed): ${body.reason}`);
113
+ }
114
+ if (res.status === 402) {
115
+ const body = await res.json();
116
+ throw new Error(`Agent rejected job (budget insufficient): ${body.reason}`);
117
+ }
118
+ if (res.status === 429) {
119
+ const body = await res.json();
120
+ throw new Error(`Agent busy. Retry after ${body.retry_after}s`);
121
+ }
122
+ if (!res.ok) {
123
+ const body = await res.json().catch(() => ({}));
124
+ throw new Error(`Agent returned ${res.status}: ${body.error || 'unknown error'}`);
125
+ }
126
+
127
+ const accepted = await res.json();
128
+
129
+ // If async (callback provided), return the acceptance immediately
130
+ if (opts.callback) return { job_id: accepted.job_id, status: 'accepted' };
131
+
132
+ // Otherwise poll for result
133
+ return this._poll(endpoint, accepted.job_id, {
134
+ pollIntervalMs: opts.pollIntervalMs || 2000,
135
+ timeoutMs: this.defaultTimeoutMs,
136
+ });
137
+ }
138
+
139
+ // ── Polling ──────────────────────────────────────────────────────────
140
+
141
+ async _poll(endpoint, jobId, { pollIntervalMs, timeoutMs }) {
142
+ const deadline = Date.now() + timeoutMs;
143
+
144
+ while (Date.now() < deadline) {
145
+ await this._sleep(pollIntervalMs);
146
+
147
+ const res = await fetch(`${endpoint}/jobs/${jobId}`);
148
+ if (!res.ok) throw new Error(`Status check failed: ${res.status}`);
149
+
150
+ const status = await res.json();
151
+
152
+ if (status.status === 'completed') {
153
+ // Acknowledge receipt
154
+ await fetch(`${endpoint}/jobs/${jobId}/ack`, {
155
+ method: 'POST',
156
+ headers: { 'Content-Type': 'application/json' },
157
+ body: JSON.stringify({ received: true }),
158
+ }).catch(() => {});
159
+ return status;
160
+ }
161
+
162
+ if (status.status === 'failed') {
163
+ throw new Error(`Job failed: ${status.message || 'unknown reason'}`);
164
+ }
165
+
166
+ if (status.status === 'rejected') {
167
+ throw new Error(`Job rejected: ${status.message || 'unknown reason'}`);
168
+ }
169
+ }
170
+
171
+ throw new Error(`Job timed out after ${timeoutMs}ms`);
172
+ }
173
+
174
+ // ── Endpoint resolution ───────────────────────────────────────────────
175
+
176
+ async _resolveEndpoint(provenanceId) {
177
+ try {
178
+ const profile = await this.provenance.check(provenanceId);
179
+ if (!profile.found) throw new Error(`Agent not found in Provenance index: ${provenanceId}`);
180
+
181
+ // AJP endpoint is stored in the agent's PROVENANCE.yml
182
+ const endpoint = profile.provenance_yml?.ajp?.endpoint;
183
+ if (endpoint) return endpoint.replace(/\/$/, '');
184
+
185
+ // Fallback: derive from agent URL
186
+ if (profile.url) return `${profile.url.replace(/\/$/, '')}/api/agent`;
187
+
188
+ throw new Error(`No AJP endpoint found for ${provenanceId}`);
189
+ } catch (e) {
190
+ if (e.message.includes('No AJP endpoint')) throw e;
191
+ throw new Error(`Could not resolve endpoint for ${provenanceId}: ${e.message}`);
192
+ }
193
+ }
194
+
195
+ _sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
196
+ }
package/src/index.js ADDED
@@ -0,0 +1,24 @@
1
+ /**
2
+ * ajp-protocol
3
+ *
4
+ * The Agent Job Protocol — standard interaction layer for the agent internet.
5
+ * Part of the Provenance Protocol family.
6
+ *
7
+ * npm install ajp-protocol
8
+ *
9
+ * Usage (receiving agent):
10
+ * import { AJPServer } from 'ajp-protocol';
11
+ * const server = new AJPServer({ provenanceId, secret, onJob });
12
+ * app.post('/jobs', server.receive());
13
+ * app.get('/jobs/:id', server.status());
14
+ * app.post('/jobs/:id/ack', server.ack());
15
+ *
16
+ * Usage (sending agent or platform):
17
+ * import { AJPClient } from 'ajp-protocol';
18
+ * const client = new AJPClient({ from, secret });
19
+ * const result = await client.send(toProvenanceId, task, budget);
20
+ */
21
+
22
+ export { AJPClient } from './client.js';
23
+ export { AJPServer } from './server.js';
24
+ export { sign, verify, generateJobId, validateOffer, JOB_STATUS, FROM_TYPE } from './utils.js';