ajp-protocol 0.1.0 → 0.2.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/spec/SPEC.md CHANGED
@@ -121,7 +121,8 @@ Confirm result received. Triggers payment settlement if applicable.
121
121
  "from": {
122
122
  "type": "human",
123
123
  "id": "user_abc123",
124
- "provenance_id": null
124
+ "provenance_id": null,
125
+ "declaration_url": null
125
126
  },
126
127
 
127
128
  "to": {
@@ -159,6 +160,13 @@ Confirm result received. Triggers payment settlement if applicable.
159
160
  }
160
161
  ```
161
162
 
163
+ `from.declaration_url` is where the sender's signed declaration is published.
164
+ Agent and orchestrator senders SHOULD provide it: it is what lets a receiver
165
+ establish the sender's key offline rather than trusting a third-party index.
166
+ `null` for human senders, whose identity is carried by the platform's signature.
167
+
168
+ ---
169
+
162
170
  ### JobStatus
163
171
 
164
172
  ```json
@@ -195,6 +203,8 @@ Status values: `accepted` `running` `completed` `failed` `rejected` `expired`
195
203
  ]
196
204
  },
197
205
 
206
+ "constraints_asserted": ["no:pii", "no:persist:data"],
207
+
198
208
  "usage": {
199
209
  "llm_tokens": 4821,
200
210
  "duration_seconds": 18,
@@ -208,48 +218,103 @@ Status values: `accepted` `running` `completed` `failed` `rejected` `expired`
208
218
  },
209
219
 
210
220
  "completed_at": "2026-03-05T10:00:19Z",
211
- "signature": "sha256:e5f6g7h8..."
221
+ "signature": "ed25519:base64..."
212
222
  }
213
223
  ```
214
224
 
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
+
215
227
  ---
216
228
 
217
229
  ## Trust verification
218
230
 
219
- When `from.type` is `agent`, the receiving agent MUST run a trust check before
220
- accepting the job. Using the `provenance-protocol` SDK:
231
+ Establishing whether to accept a job from another agent splits into two
232
+ questions, and only one of them needs a network service.
233
+
234
+ ### 1. Identity — MUST, and offline
235
+
236
+ *Is this signature really from the party named in `from`?*
237
+
238
+ The sender publishes its signed declaration and points at it with
239
+ `from.declaration_url`. The receiver fetches that file and verifies it locally:
221
240
 
222
241
  ```js
223
- import { provenance } from 'provenance-protocol';
242
+ import { verifyDeclaration } from 'provenance-protocol/verify';
224
243
 
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
- });
244
+ const result = await verifyDeclaration(declaration, { retrievedFrom: declarationUrl });
245
+ // result.valid — signature checks against the key inside the file
246
+ // result.location — the file was served from the location its id names
247
+ // result.fingerprint — the key's fingerprint, for rotation detection
248
+ ```
231
249
 
232
- if (!result.allowed) {
233
- return res.status(403).json({ error: 'Trust check failed', reason: result.reason });
234
- }
250
+ A receiver MUST reject the offer unless the declaration verifies, the retrieval
251
+ location matches the `provenance_id` it claims, and that id equals
252
+ `from.provenance_id`. The sender's public key is then taken from the declaration
253
+ and used to check the offer's signature.
254
+
255
+ No index is consulted. Re-hosting a genuine declaration elsewhere fails the
256
+ location check, and forging one requires the genuine private key.
257
+
258
+ Receivers SHOULD remember the key fingerprint they saw for a `provenance_id`. A
259
+ later offer signed by a different key is a key rotation and MUST be treated as
260
+ a material change rather than a routine update.
261
+
262
+ An earlier version of this specification obtained the sender's public key from a
263
+ single index, which made signature verification depend on one service being
264
+ reachable. That is no longer permitted for identity.
265
+
266
+ ### 2. Standing — SHOULD, online, and the receiver's choice
267
+
268
+ *Is that party currently in good order?* Revoked, open incidents, stale
269
+ evidence. This cannot be answered offline: the absence of news cannot be
270
+ carried in a document, so somebody has to be asked.
271
+
272
+ **Which attester to ask is the receiver's decision, not this protocol's.** One
273
+ index, several, a private attester, or none. A conformant implementation MUST
274
+ NOT hardcode a single provider.
275
+
276
+ ```js
277
+ import { AJPServer, declarationKeyResolver, indexStandingCheck } from 'ajp-protocol';
278
+ import { Provenance } from 'provenance-protocol';
279
+
280
+ 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 }),
285
+ onStandingUnavailable: 'deny',
286
+ });
235
287
  ```
236
288
 
237
- For `from.type === 'human'`, trust verification is handled by the platform
238
- (ClawMarket, SkillsMP, etc.) before the JobOffer is issued.
289
+ A receiver that performs a standing check MUST distinguish *checked and failed*
290
+ from *could not check*, and MUST state which way it fails when the check is
291
+ unavailable. Reporting an unreachable attester as a failed trust check turns
292
+ someone else's downtime into an accusation against the sender.
293
+
294
+ For `from.type === 'human'`, identity is established by the platform issuing the
295
+ offer, and the shared-secret signature covers it.
239
296
 
240
297
  ---
241
298
 
242
299
  ## Signature
243
300
 
244
301
  Every JobOffer and JobResult is signed by the sender. The signature covers the
245
- full message body excluding the `signature` field itself.
302
+ full message body excluding the `signature` field itself, with keys sorted canonically.
303
+
304
+ **Human senders** (no Provenance identity) — HMAC-SHA256 with a shared secret:
305
+ ```
306
+ signature = "sha256:" + hex(HMAC-SHA256(canonical(body), sender_secret))
307
+ ```
246
308
 
309
+ **Agent and orchestrator senders** — Ed25519 with the sender's registered Provenance private key:
247
310
  ```
248
- signature = "sha256:" + hex(HMAC-SHA256(JSON.stringify(body_without_signature), sender_secret))
311
+ signature = "ed25519:" + base64(Ed25519Sign(canonical(body), provenance_private_key))
249
312
  ```
250
313
 
251
- Receiving agents verify signatures before processing. The `ajp-protocol` SDK
252
- handles signing and verification automatically.
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.
316
+
317
+ The `ajp-protocol` SDK handles signing and verification automatically based on `from.type`.
253
318
 
254
319
  ---
255
320
 
@@ -262,7 +327,8 @@ import { AJPServer } from 'ajp-protocol';
262
327
 
263
328
  const server = new AJPServer({
264
329
  provenanceId: 'provenance:github:alice/research-assistant',
265
- secret: process.env.AJP_SECRET,
330
+ privateKey: process.env.PROVENANCE_PRIVATE_KEY,
331
+ constraints: ['no:pii', 'no:persist:data'], // asserted in every signed result
266
332
  onJob: async (job) => {
267
333
  // your agent logic here
268
334
  return { papers: [...] };
@@ -321,6 +387,19 @@ can discover an agent's AJP endpoint without out-of-band communication.
321
387
 
322
388
  ---
323
389
 
390
+ ## Implementing this specification
391
+
392
+ This specification and its JSON Schema are published under the MIT Licence.
393
+ You may implement them in any language, for any purpose, commercial or
394
+ otherwise, without permission, notification or fee.
395
+
396
+ AJP describes messages exchanged directly between agents. Nothing in it
397
+ requires contacting any particular service. Trust verification is a separate
398
+ concern, defined by the [Provenance Protocol](https://github.com/ilucky21c/provenance-protocol);
399
+ an AJP implementation may use any verifier, or none.
400
+
401
+ ---
402
+
324
403
  ## Versioning
325
404
 
326
405
  The `ajp` field in every message declares the spec version. `0.1` is the current
@@ -329,5 +408,5 @@ version. Future versions add fields, never remove them.
329
408
  ---
330
409
 
331
410
  *AJP v0.1 — Provenance Protocol Family — MIT License*
332
- *https://provenance.dev/ajp*
333
- *https://github.com/provenance-protocol/ajp*
411
+ *https://getprovenance.dev/ajp*
412
+ *https://github.com/ilucky21c/ajp-protocol*
package/src/client.js CHANGED
@@ -5,29 +5,41 @@
5
5
  * Consistent with provenance-protocol SDK class structure.
6
6
  */
7
7
 
8
- import { sign, generateJobId, validateOffer } from './utils.js';
8
+ import { sign, signWithKey, generateJobId, validateOffer } from './utils.js';
9
9
  import { Provenance } from 'provenance-protocol';
10
10
 
11
11
  export class AJPClient {
12
12
 
13
13
  /**
14
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)
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
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)
19
+ * @param {string} [opts.from.declaration_url] — where this sender's signed
20
+ * declaration is published. Lets receivers establish this sender's key
21
+ * offline instead of looking it up in an index. Strongly recommended:
22
+ * without it a receiver must fall back to trusting a third party.
23
+ * @param {string} [opts.privateKey] — Base64 PKCS8 Ed25519 private key (PROVENANCE_PRIVATE_KEY).
24
+ * Required for agent/orchestrator senders. Allows any
25
+ * indexed agent to call any other without a shared secret.
26
+ * @param {string} [opts.secret] — HMAC secret. Required for human senders only.
27
+ * @param {string} [opts.provenanceApiUrl] — override Provenance API URL
28
+ * @param {number} [opts.defaultTimeoutMs] — default job timeout in ms (30s)
22
29
  */
23
- constructor({ from, secret, provenanceApiUrl, defaultTimeoutMs = 30000 }) {
30
+ constructor({ from, privateKey, secret, provenanceApiUrl, defaultTimeoutMs = 30000 }) {
24
31
  this.from = from;
25
- this.secret = secret;
32
+ this.privateKey = privateKey || null;
33
+ this.secret = secret || null;
26
34
  this.defaultTimeoutMs = defaultTimeoutMs;
27
35
  this.provenance = new Provenance({ apiUrl: provenanceApiUrl });
28
36
 
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');
37
+ if (from.type === 'agent' || from.type === 'orchestrator') {
38
+ if (!from.provenance_id) throw new Error('from.provenance_id required when type is agent or orchestrator');
39
+ if (!privateKey) throw new Error('privateKey (PROVENANCE_PRIVATE_KEY) required for agent/orchestrator senders');
40
+ }
41
+ if (from.type === 'human' && !secret) {
42
+ throw new Error('secret required for human senders');
31
43
  }
32
44
  }
33
45
 
@@ -64,6 +76,7 @@ export class AJPClient {
64
76
  type: this.from.type,
65
77
  id: this.from.id || null,
66
78
  provenance_id: this.from.provenance_id || null,
79
+ declaration_url: this.from.declaration_url || null,
67
80
  },
68
81
 
69
82
  to: { provenance_id: toProvenanceId },
@@ -93,8 +106,10 @@ export class AJPClient {
93
106
  signature: '',
94
107
  };
95
108
 
96
- // Sign it
97
- offer.signature = sign(offer, this.secret);
109
+ // Sign with Ed25519 (agents/orchestrators) or HMAC (humans)
110
+ offer.signature = (this.from.type === 'agent' || this.from.type === 'orchestrator')
111
+ ? signWithKey(offer, this.privateKey)
112
+ : sign(offer, this.secret);
98
113
 
99
114
  // Validate before sending
100
115
  const { valid, errors } = validateOffer(offer);
@@ -155,7 +170,9 @@ export class AJPClient {
155
170
  method: 'POST',
156
171
  headers: { 'Content-Type': 'application/json' },
157
172
  body: JSON.stringify({ received: true }),
158
- }).catch(() => {});
173
+ }).catch((e) => {
174
+ console.warn(`[AJP] Ack failed for ${jobId} — payment settlement may not trigger:`, e.message);
175
+ });
159
176
  return status;
160
177
  }
161
178
 
@@ -182,8 +199,11 @@ export class AJPClient {
182
199
  const endpoint = profile.provenance_yml?.ajp?.endpoint;
183
200
  if (endpoint) return endpoint.replace(/\/$/, '');
184
201
 
185
- // Fallback: derive from agent URL
186
- if (profile.url) return `${profile.url.replace(/\/$/, '')}/api/agent`;
202
+ // Fallback: derive from agent URL — unreliable, agent should declare ajp.endpoint in PROVENANCE.yml
203
+ if (profile.url) {
204
+ console.warn(`[AJP] No ajp.endpoint declared for ${provenanceId} — falling back to ${profile.url}/api/agent. Add ajp.endpoint to PROVENANCE.yml for reliability.`);
205
+ return `${profile.url.replace(/\/$/, '')}/api/agent`;
206
+ }
187
207
 
188
208
  throw new Error(`No AJP endpoint found for ${provenanceId}`);
189
209
  } catch (e) {
package/src/index.js CHANGED
@@ -22,3 +22,10 @@
22
22
  export { AJPClient } from './client.js';
23
23
  export { AJPServer } from './server.js';
24
24
  export { sign, verify, generateJobId, validateOffer, JOB_STATUS, FROM_TYPE } from './utils.js';
25
+ export {
26
+ declarationKeyResolver,
27
+ indexKeyResolver,
28
+ firstResolver,
29
+ indexStandingCheck,
30
+ SenderIdentityError,
31
+ } from './trust.js';
package/src/server.js CHANGED
@@ -6,36 +6,64 @@
6
6
  * Express, Next.js API routes, Fastify, or any Node HTTP framework.
7
7
  */
8
8
 
9
- import { verify, validateOffer, JOB_STATUS, FROM_TYPE } from './utils.js';
9
+ import { verify, verifyWithKey, sign, signWithKey, validateOffer, JOB_STATUS, FROM_TYPE } from './utils.js';
10
10
  import { Provenance } from 'provenance-protocol';
11
+ import { declarationKeyResolver, SenderIdentityError } from './trust.js';
11
12
 
12
13
  export class AJPServer {
13
14
 
14
15
  /**
15
16
  * @param {object} opts
16
- * @param {string} opts.provenanceId — this agent's Provenance ID
17
- * @param {string} opts.secret — HMAC verification secret (must match client)
18
- * @param {Function} opts.onJob — async (job) => result — your agent logic
17
+ * @param {string} opts.provenanceId — this agent's Provenance ID
18
+ * @param {string} opts.privateKey — Base64 PKCS8 Ed25519 private key (PROVENANCE_PRIVATE_KEY).
19
+ * Used to sign job results so callers can verify authenticity.
20
+ * @param {string} [opts.secret] — HMAC secret for verifying human callers. Optional if you
21
+ * only accept agent/orchestrator callers.
22
+ * @param {Function} opts.onJob — async (job) => result — your agent logic
23
+ * @param {function} [opts.resolveSenderKey] — async (provenanceId, from) => { publicKey, fingerprint }
24
+ * How the sender's key is established. Defaults to resolving it from the
25
+ * sender's own signed declaration, offline, with no index involved.
26
+ * @param {function} [opts.checkStanding] — async (provenanceId) => { allowed, reason }
27
+ * Optional current-standing check (revocation, incidents, freshness).
28
+ * Off by default: standing is the receiver's policy, not a protocol rule.
29
+ * @param {'deny'|'allow'} [opts.onStandingUnavailable] — what to do when the
30
+ * standing check itself fails to answer. Defaults to 'deny'.
19
31
  * @param {object} [opts.trustRequirements] — applied to all agent/orchestrator senders
20
32
  * @param {boolean} [opts.trustRequirements.requireDeclared]
21
33
  * @param {string[]} [opts.trustRequirements.requireConstraints]
34
+ * @param {string[]} [opts.trustRequirements.requireCapabilities] — e.g. ['delegate:agents'] to only accept jobs from declared orchestrators
22
35
  * @param {boolean} [opts.trustRequirements.requireClean]
23
36
  * @param {number} [opts.trustRequirements.requireMinAge]
24
- * @param {number} [opts.trustRequirements.requireMinConfidence]
37
+ * @param {string[]} [opts.constraints] — constraints this agent honors (from PROVENANCE.yml).
38
+ * Included as `constraints_asserted` in every signed JobResult,
39
+ * creating a cryptographic receipt tied to the registered identity.
25
40
  * @param {string} [opts.provenanceApiUrl] — override Provenance API URL
26
41
  */
27
42
  constructor({
28
43
  provenanceId,
44
+ privateKey,
29
45
  secret,
30
46
  onJob,
47
+ constraints = [],
31
48
  trustRequirements = {},
32
49
  provenanceApiUrl,
50
+ resolveSenderKey,
51
+ checkStanding,
52
+ onStandingUnavailable = 'deny',
33
53
  }) {
54
+ if (!privateKey) throw new Error('privateKey (PROVENANCE_PRIVATE_KEY) required — used to sign job results');
34
55
  this.provenanceId = provenanceId;
35
- this.secret = secret;
56
+ this.privateKey = privateKey;
57
+ this.secret = secret || null;
36
58
  this.onJob = onJob;
59
+ this.constraints = constraints;
37
60
  this.trustRequirements = trustRequirements;
38
61
  this.provenance = new Provenance({ apiUrl: provenanceApiUrl });
62
+ // Identity resolution never touches an index by default: a signature check
63
+ // that depends on someone's web service being up is not a signature check.
64
+ this.resolveSenderKey = resolveSenderKey ?? declarationKeyResolver();
65
+ this.checkStanding = checkStanding ?? null;
66
+ this.onStandingUnavailable = onStandingUnavailable;
39
67
 
40
68
  // In-memory job store — replace with DB for production
41
69
  this.jobs = new Map();
@@ -55,28 +83,59 @@ export class AJPServer {
55
83
  }
56
84
 
57
85
  // 2. Verify signature
58
- if (!verify(offer, this.secret)) {
59
- return this._json(res, 401, { error: 'Invalid signature' });
86
+ if (offer.from.type === FROM_TYPE.HUMAN) {
87
+ // Human callers: HMAC-SHA256 with shared secret
88
+ if (!this.secret) {
89
+ return this._json(res, 403, { error: 'This agent does not accept human callers' });
90
+ }
91
+ if (!verify(offer, this.secret)) {
92
+ return this._json(res, 401, { error: 'Invalid signature' });
93
+ }
94
+ } else {
95
+ // Agent/orchestrator callers: Ed25519. The key comes from whatever the
96
+ // receiver configured — by default the sender's own signed declaration,
97
+ // verified offline. No index is consulted to check a signature.
98
+ let sender;
99
+ try {
100
+ sender = await this.resolveSenderKey(offer.from.provenance_id, offer.from);
101
+ } catch (e) {
102
+ const code = e instanceof SenderIdentityError ? e.code : 'SENDER_IDENTITY_FAILED';
103
+ return this._json(res, 403, { error: 'Sender identity could not be established', reason: e.message, code });
104
+ }
105
+ if (!sender?.publicKey) {
106
+ return this._json(res, 403, { error: 'Sender identity could not be established', reason: 'No public key resolved' });
107
+ }
108
+ if (!verifyWithKey(offer, sender.publicKey)) {
109
+ return this._json(res, 401, { error: 'Invalid signature' });
110
+ }
60
111
  }
61
112
 
62
- // 3. Trust check — required for agent/orchestrator senders
63
- if (offer.from.type === FROM_TYPE.AGENT || offer.from.type === FROM_TYPE.ORCHESTRATOR) {
64
- const trustResult = await this.provenance.gate(
65
- offer.from.provenance_id,
66
- {
67
- requireDeclared: this.trustRequirements.requireDeclared ?? false,
68
- requireConstraints: this.trustRequirements.requireConstraints ?? [],
69
- requireClean: this.trustRequirements.requireClean ?? true,
70
- requireMinAge: this.trustRequirements.requireMinAge ?? 0,
71
- requireMinConfidence: this.trustRequirements.requireMinConfidence ?? 0,
113
+ // 3. Standing check — optional, and the receiver's policy rather than a
114
+ // protocol requirement. Identity is settled above without a network
115
+ // service; standing is the part that genuinely needs asking someone,
116
+ // so it is configured, degradable, and absent by default.
117
+ if (
118
+ this.checkStanding &&
119
+ (offer.from.type === FROM_TYPE.AGENT || offer.from.type === FROM_TYPE.ORCHESTRATOR)
120
+ ) {
121
+ let standing;
122
+ try {
123
+ standing = await this.checkStanding(offer.from.provenance_id, this.trustRequirements);
124
+ } catch (e) {
125
+ // "Could not check" must not be reported as "checked and failed".
126
+ if (this.onStandingUnavailable === 'allow') {
127
+ standing = { allowed: true, unavailable: true, reason: e.message };
128
+ } else {
129
+ return this._json(res, 403, {
130
+ error: 'Standing could not be checked',
131
+ reason: e.message,
132
+ code: 'STANDING_UNAVAILABLE',
133
+ });
72
134
  }
73
- );
135
+ }
74
136
 
75
- if (!trustResult.allowed) {
76
- return this._json(res, 403, {
77
- error: 'Trust check failed',
78
- reason: trustResult.reason,
79
- });
137
+ if (standing && standing.allowed === false) {
138
+ return this._json(res, 403, { error: 'Trust check failed', reason: standing.reason ?? null });
80
139
  }
81
140
  }
82
141
 
@@ -128,11 +187,11 @@ export class AJPServer {
128
187
 
129
188
  if (job.status === JOB_STATUS.COMPLETED) {
130
189
  response.output = job.output;
190
+ response.constraints_asserted = job.constraints_asserted;
131
191
  response.usage = job.usage;
132
- response.agent = {
133
- provenance_id: this.provenanceId,
134
- };
192
+ response.agent = { provenance_id: this.provenanceId };
135
193
  response.completed_at = job.completed_at;
194
+ response.signature = job.signature;
136
195
  }
137
196
 
138
197
  if (job.status === JOB_STATUS.FAILED) {
@@ -184,14 +243,33 @@ export class AJPServer {
184
243
  this.jobs.set(jobId, job);
185
244
 
186
245
  try {
187
- const output = await this.onJob(job);
246
+ const maxSeconds = job.budget?.max_seconds ?? 120;
247
+ const output = await Promise.race([
248
+ this.onJob(job),
249
+ new Promise((_, reject) =>
250
+ setTimeout(() => reject(new Error(`Job exceeded max_seconds (${maxSeconds}s)`)), maxSeconds * 1000)
251
+ ),
252
+ ]);
188
253
 
189
254
  const durationSeconds = (Date.now() - startTime) / 1000;
190
255
  job.status = JOB_STATUS.COMPLETED;
191
256
  job.output = output;
257
+ job.constraints_asserted = this.constraints;
192
258
  job.completed_at = new Date().toISOString();
193
259
  job.updated_at = job.completed_at;
194
260
  job.usage.duration_seconds = durationSeconds;
261
+
262
+ // Sign the result with Ed25519 — callers verify using this agent's public key from Provenance index.
263
+ // constraints_asserted is included in the signed payload — a cryptographic receipt of declared behavior.
264
+ job.signature = signWithKey({
265
+ job_id: job.job_id,
266
+ status: job.status,
267
+ output: job.output,
268
+ constraints_asserted: job.constraints_asserted,
269
+ completed_at: job.completed_at,
270
+ agent: { provenance_id: this.provenanceId },
271
+ }, this.privateKey);
272
+
195
273
  this.jobs.set(jobId, job);
196
274
 
197
275
  // Deliver result via callback if set
@@ -200,7 +278,7 @@ export class AJPServer {
200
278
  }
201
279
 
202
280
  } catch (e) {
203
- job.status = JOB_STATUS.FAILED;
281
+ job.status = e.message.includes('max_seconds') ? JOB_STATUS.EXPIRED : JOB_STATUS.FAILED;
204
282
  job.error = e.message;
205
283
  job.updated_at = new Date().toISOString();
206
284
  this.jobs.set(jobId, job);
@@ -220,9 +298,11 @@ export class AJPServer {
220
298
  job_id: job.job_id,
221
299
  status: job.status,
222
300
  output: job.output,
301
+ constraints_asserted: job.constraints_asserted,
223
302
  usage: job.usage,
224
303
  agent: { provenance_id: this.provenanceId },
225
304
  completed_at: job.completed_at,
305
+ signature: job.signature,
226
306
  }),
227
307
  });
228
308
  } catch (e) {