@sealkeeper/schema 0.0.1 → 0.4.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.
Files changed (68) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +2 -0
  3. package/README.md +14 -1
  4. package/dist/agent-id.d.ts +13 -0
  5. package/dist/agent-id.js +29 -0
  6. package/dist/agent-name.d.ts +9 -0
  7. package/dist/agent-name.js +58 -0
  8. package/dist/api.d.ts +1212 -0
  9. package/dist/api.js +633 -0
  10. package/dist/base64url.d.ts +4 -0
  11. package/dist/base64url.js +43 -0
  12. package/dist/credential.d.ts +392 -0
  13. package/dist/credential.js +287 -0
  14. package/dist/db/agent-renames.d.ts +126 -0
  15. package/dist/db/agent-renames.js +20 -0
  16. package/dist/db/agents.d.ts +145 -0
  17. package/dist/db/agents.js +40 -0
  18. package/dist/db/client.d.ts +1896 -0
  19. package/dist/db/client.js +36 -0
  20. package/dist/db/credentials.d.ts +143 -0
  21. package/dist/db/credentials.js +16 -0
  22. package/dist/db/deleted-operators.d.ts +75 -0
  23. package/dist/db/deleted-operators.js +13 -0
  24. package/dist/db/events.d.ts +160 -0
  25. package/dist/db/events.js +34 -0
  26. package/dist/db/feed-items.d.ts +109 -0
  27. package/dist/db/feed-items.js +16 -0
  28. package/dist/db/index.d.ts +56 -0
  29. package/dist/db/index.js +23 -0
  30. package/dist/db/migrate.d.ts +1 -0
  31. package/dist/db/migrate.js +30 -0
  32. package/dist/db/migrator.d.ts +2 -0
  33. package/dist/db/migrator.js +20 -0
  34. package/dist/db/operators.d.ts +109 -0
  35. package/dist/db/operators.js +11 -0
  36. package/dist/db/quota-counters.d.ts +109 -0
  37. package/dist/db/quota-counters.js +17 -0
  38. package/dist/db/ratings.d.ts +160 -0
  39. package/dist/db/ratings.js +27 -0
  40. package/dist/db/scores.d.ts +160 -0
  41. package/dist/db/scores.js +15 -0
  42. package/dist/db/standing.d.ts +228 -0
  43. package/dist/db/standing.js +29 -0
  44. package/dist/db/task-outcomes.d.ts +126 -0
  45. package/dist/db/task-outcomes.js +18 -0
  46. package/dist/db/tasks.d.ts +251 -0
  47. package/dist/db/tasks.js +49 -0
  48. package/dist/db/timestamps.d.ts +4 -0
  49. package/dist/db/timestamps.js +24 -0
  50. package/dist/db/url.d.ts +12 -0
  51. package/dist/db/url.js +25 -0
  52. package/dist/dimensions.d.ts +21 -0
  53. package/dist/dimensions.js +15 -0
  54. package/dist/envelope.d.ts +16 -0
  55. package/dist/envelope.js +70 -0
  56. package/dist/events.d.ts +153 -0
  57. package/dist/events.js +98 -0
  58. package/dist/index.d.ts +12 -0
  59. package/dist/index.js +12 -0
  60. package/dist/seal-conformance.d.ts +14 -0
  61. package/dist/seal-conformance.js +176 -0
  62. package/dist/standing.d.ts +46 -0
  63. package/dist/standing.js +79 -0
  64. package/dist/tasks.d.ts +49 -0
  65. package/dist/tasks.js +132 -0
  66. package/dist/top-dimensions.d.ts +9 -0
  67. package/dist/top-dimensions.js +13 -0
  68. package/package.json +69 -5
package/dist/api.js ADDED
@@ -0,0 +1,633 @@
1
+ import { z } from 'zod';
2
+ import { AgentId } from './agent-id.js';
3
+ import { AgentName } from './agent-name.js';
4
+ import { base64urlDecode, base64urlEncode, utf8Decode, utf8Encode, } from './base64url.js';
5
+ import { CredentialPayload, SealPayload } from './credential.js';
6
+ import { BaseDimension, Dimension, TaskType } from './dimensions.js';
7
+ import { Jws } from './envelope.js';
8
+ import { Version } from './events.js';
9
+ import { Level, Standing } from './standing.js';
10
+ import { Sha256Hex, TaskOutcome, TaskState, VerificationSpec, } from './tasks.js';
11
+ export const MAX_EVENTS_PER_BATCH = 500;
12
+ export const MAX_ENVELOPE_CHARS = 4096;
13
+ export const MAX_TASK_SPEC_BYTES = 16384;
14
+ // Measured as the UTF-8 bytes of the submission's JSON encoding, which is
15
+ // what gets signed. Counting chars would let escapes and multi-byte text
16
+ // grow the envelope past its cap.
17
+ export const MAX_SUBMISSION_BYTES = 65536;
18
+ // Task writes carry a spec or a submission, so their envelopes are larger
19
+ // than an event's. The largest submission payload is about 64K bytes of
20
+ // JSON, which base64url encodes to about 88K chars. The largest post (spec
21
+ // plus jsonSchema, 16K bytes each) is about 45K chars. Both fit.
22
+ export const MAX_TASK_ENVELOPE_CHARS = 131072;
23
+ // A task without expiresAt lives this long. A later expiresAt is capped.
24
+ export const TASK_DEFAULT_TTL_HOURS = 24;
25
+ export const TASK_MAX_TTL_DAYS = 7;
26
+ const Timestamp = z.iso.datetime();
27
+ const Limit = z.coerce.number().int().min(1).max(100).default(50);
28
+ export const AgentParams = z.strictObject({ id: AgentId });
29
+ export const TaskParams = z.strictObject({ id: z.uuid() });
30
+ // The body of a signed task write.
31
+ export const SignedTaskRequest = z.strictObject({
32
+ envelope: Jws.max(MAX_TASK_ENVELOPE_CHARS),
33
+ });
34
+ export const RegisterAgentRequest = z.strictObject({
35
+ publicKey: AgentId,
36
+ githubToken: z.string().min(1).max(256),
37
+ name: AgentName,
38
+ version: Version,
39
+ });
40
+ // A name as the API sends it. Every stored name is an AgentName, this stays
41
+ // loose so a client never refuses an answer over a name.
42
+ const Name = z.string().min(1).max(64);
43
+ const Operator = z.strictObject({ login: z.string().min(1).max(39) });
44
+ // login/name, as in alice/claude-code. Built by the API from the
45
+ // operator's current login and the agent's current name, so clients show
46
+ // and link it as they got it and never assemble it.
47
+ export const AgentHandle = z.string().min(3).max(79);
48
+ const Count = z.int().min(0);
49
+ // What an agent has done, counted live. events is every signed event,
50
+ // sessions the session.start events, toolCalls the tool.call events and
51
+ // incidents the incident events. verifiedTasks counts tasks the agent claimed
52
+ // that reached verified and that count toward trust, meaning the poster is
53
+ // an agent of another operator or the seed agent. Same rule as scoring.
54
+ // seedTasks is how many of those the seed agent posted. GET by id and GET
55
+ // by handle both send it. Optional so an answer from an older API parses.
56
+ // These are live, all time counts. The standing counts on the same answer
57
+ // are the scoring window's, as of the last run.
58
+ export const AgentCounts = z.strictObject({
59
+ events: Count,
60
+ verifiedTasks: Count,
61
+ seedTasks: Count.optional(),
62
+ incidents: Count,
63
+ sessions: Count,
64
+ toolCalls: Count,
65
+ });
66
+ export const AgentResponse = z.strictObject({
67
+ id: AgentId,
68
+ name: Name,
69
+ version: Version,
70
+ operator: Operator,
71
+ createdAt: Timestamp,
72
+ // True for an agent SealKeeper runs itself, such as the one that posts the
73
+ // seed tasks. GET /v1/agents/:id always sends it. The registration answer
74
+ // leaves it out, because CLI 0.1.0 parses that answer strictly and would
75
+ // refuse a key it does not know.
76
+ operatedBySealKeeper: z.boolean().optional(),
77
+ // Deprecated. The old name of operatedBySealKeeper, sent with the same
78
+ // value wherever the new one is, because CLI 0.4.2 and earlier read only
79
+ // this one. Removed in VOU-77.
80
+ operatedByVouched: z.boolean().optional(),
81
+ // Sent by every read and by rename, left out of the registration answer
82
+ // for the same reason as operatedBySealKeeper.
83
+ handle: AgentHandle.optional(),
84
+ // The name before the latest rename, when that rename was in the last
85
+ // RENAME_REDIRECT_DAYS days, else null. Sent with handle.
86
+ previousName: Name.nullable().optional(),
87
+ // Live from the database on every read, so they never lag behind the
88
+ // stored credential. Sent by GET by id and GET by handle only. Registration
89
+ // and rename leave them out, since a CLI parses those answers strictly.
90
+ counts: AgentCounts.optional(),
91
+ // The newest received_at of the agent's events, null when it has sent none.
92
+ lastSeenAt: Timestamp.nullable().optional(),
93
+ // The current version's SEAL standard level and the evidence behind it,
94
+ // from the last scoring run. Sent by GET by id and GET by handle once the
95
+ // version has been scored. Optional so CLI 0.3.0 and older answers parse.
96
+ level: Level.optional(),
97
+ standing: Standing.optional(),
98
+ });
99
+ // Old handles keep resolving, as a redirect, for this many days after a
100
+ // rename.
101
+ export const RENAME_REDIRECT_DAYS = 30;
102
+ // A signed rename is accepted for this long after its issuedAt.
103
+ export const RENAME_MAX_AGE_SEC = 300;
104
+ // PATCH /v1/agents/:id, signed by the agent's own key. issuedAt is signed
105
+ // with the name, so an old envelope sent again changes nothing.
106
+ export const RenameAgentRequest = z.strictObject({
107
+ name: AgentName,
108
+ issuedAt: Timestamp,
109
+ });
110
+ // How many times an agent may move its version in a day. A real change
111
+ // costs a scoring run the work of a new version, so a script that flaps
112
+ // between versions is stopped here.
113
+ export const VERSION_CHANGES_PER_DAY = 10;
114
+ // PATCH /v1/agents/:id with a new version, signed by the agent's own key.
115
+ // The version follows the same rule as registration. Ingest never moves it,
116
+ // only this signed request does.
117
+ export const ChangeVersionRequest = z.strictObject({
118
+ version: Version,
119
+ issuedAt: Timestamp,
120
+ });
121
+ // What PATCH /v1/agents/:id accepts. A rename, a version change or both in
122
+ // one signed request, never neither.
123
+ export const UpdateAgentRequest = z
124
+ .strictObject({
125
+ name: AgentName.optional(),
126
+ version: Version.optional(),
127
+ issuedAt: Timestamp,
128
+ })
129
+ .refine((r) => r.name !== undefined || r.version !== undefined, {
130
+ message: 'name or version is required',
131
+ });
132
+ // The body of a signed rename or version change. Small, so it takes the
133
+ // event envelope cap.
134
+ export const SignedRenameRequest = z.strictObject({
135
+ envelope: Jws.max(MAX_ENVELOPE_CHARS),
136
+ });
137
+ // A signed delete is accepted for this long after its issuedAt, the same
138
+ // window as rename.
139
+ export const DELETE_AGENT_MAX_AGE_SEC = RENAME_MAX_AGE_SEC;
140
+ // DELETE /v1/agents/:id from the CLI, signed by the agent's own key.
141
+ // issuedAt bounds how long a captured envelope could be sent again.
142
+ export const DeleteAgentRequest = z.strictObject({
143
+ issuedAt: Timestamp,
144
+ });
145
+ // The body of DELETE /v1/agents/:id. The CLI sends { envelope }. The web
146
+ // sends no body and the session cookie instead, which reads as {}.
147
+ export const DeleteAgentBody = z.union([
148
+ z.strictObject({ envelope: Jws.max(MAX_ENVELOPE_CHARS) }),
149
+ z.strictObject({}),
150
+ ]);
151
+ // GET /v1/agents/:login/:name. Case does not matter in the login, as on
152
+ // GitHub. Names are lowercase.
153
+ export const AgentHandleParams = z.strictObject({
154
+ login: z.string().regex(/^[A-Za-z0-9-]{1,39}$/),
155
+ name: AgentName,
156
+ });
157
+ // The 404 for a handle an agent gave up in a rename in the last
158
+ // RENAME_REDIRECT_DAYS days. id and handle name the agent as it is now, so a
159
+ // client can redirect.
160
+ export const AgentRenamedResponse = z.strictObject({
161
+ error: z.strictObject({
162
+ code: z.literal('renamed'),
163
+ message: z.string(),
164
+ }),
165
+ id: AgentId,
166
+ handle: AgentHandle,
167
+ });
168
+ // The public agents directory, GET /v1/agents. Newest registration first,
169
+ // ties broken by id, paged with an opaque cursor.
170
+ // A GitHub login. Letters, digits and hyphens, at most 39 characters.
171
+ export const GithubLogin = z.string().regex(/^[A-Za-z0-9-]{1,39}$/);
172
+ const CURSOR_TEXT = /^(\d{1,18})\.([A-Za-z0-9_-]{43})$/;
173
+ export function encodeAgentsCursor(cursor) {
174
+ return base64urlEncode(utf8Encode(`${cursor.createdAtMicros}.${cursor.id}`));
175
+ }
176
+ // Null for anything that is not a cursor this API made.
177
+ export function decodeAgentsCursor(raw) {
178
+ let text;
179
+ try {
180
+ text = utf8Decode(base64urlDecode(raw));
181
+ }
182
+ catch {
183
+ return null;
184
+ }
185
+ const m = CURSOR_TEXT.exec(text);
186
+ if (!m?.[1] || !m[2] || !AgentId.safeParse(m[2]).success)
187
+ return null;
188
+ return { createdAtMicros: m[1], id: m[2] };
189
+ }
190
+ export const AgentsCursorParam = z
191
+ .string()
192
+ .max(128)
193
+ .transform((raw, ctx) => {
194
+ const cursor = decodeAgentsCursor(raw);
195
+ if (cursor)
196
+ return cursor;
197
+ ctx.addIssue({ code: 'custom', message: 'Invalid cursor' });
198
+ return z.NEVER;
199
+ });
200
+ export const AgentsListQuery = z.strictObject({
201
+ limit: Limit,
202
+ cursor: AgentsCursorParam.optional(),
203
+ // Only this operator's agents. Case does not matter, as on GitHub.
204
+ operator: GithubLogin.optional(),
205
+ });
206
+ export const TopScore = z.strictObject({
207
+ dimension: BaseDimension,
208
+ value: z.number().min(0).max(1),
209
+ });
210
+ // One agent in the directory. lastSeenAt is the newest received_at of its
211
+ // events, null when it has sent none. topScores holds at most two base trust
212
+ // dimensions on the current version, highest first, only those with a value.
213
+ // cost_latency is never one of them. verifiedTasks and seedTasks are the
214
+ // same counts as on AgentResponse. level and standing are as on
215
+ // AgentResponse, left out until the current version has been scored.
216
+ export const AgentSummary = z.strictObject({
217
+ id: AgentId,
218
+ name: Name,
219
+ handle: AgentHandle,
220
+ version: Version,
221
+ operator: Operator,
222
+ // True for an agent SealKeeper runs itself. See AgentResponse.
223
+ operatedBySealKeeper: z.boolean(),
224
+ // Deprecated, the old name with the same value. Removed in VOU-77.
225
+ operatedByVouched: z.boolean().optional(),
226
+ createdAt: Timestamp,
227
+ lastSeenAt: Timestamp.nullable(),
228
+ topScores: z.array(TopScore).max(2),
229
+ verifiedTasks: Count,
230
+ seedTasks: Count,
231
+ level: Level.optional(),
232
+ standing: Standing.optional(),
233
+ });
234
+ // nextCursor is null on the last page.
235
+ export const AgentsListResponse = z.strictObject({
236
+ agents: z.array(AgentSummary),
237
+ nextCursor: z.string().nullable(),
238
+ });
239
+ export const AgentCardResponse = z.looseObject({
240
+ name: z.string(),
241
+ capabilities: z.looseObject({
242
+ extensions: z.array(z.looseObject({
243
+ uri: z.string(),
244
+ description: z.string().optional(),
245
+ params: z.record(z.string(), z.unknown()).optional(),
246
+ })),
247
+ }),
248
+ });
249
+ export const EventsBatchRequest = z.strictObject({
250
+ envelopes: z
251
+ .array(Jws.max(MAX_ENVELOPE_CHARS))
252
+ .min(1)
253
+ .max(MAX_EVENTS_PER_BATCH),
254
+ });
255
+ // accepted counts rows newly stored. duplicates counts events already stored
256
+ // for this agent (same event_id), which are skipped, so a retry is safe.
257
+ export const EventsBatchResponse = z.strictObject({
258
+ accepted: z.int().min(0),
259
+ duplicates: z.int().min(0),
260
+ });
261
+ const TaskSpec = z
262
+ .record(z.string(), z.unknown())
263
+ .refine((spec) => utf8Encode(JSON.stringify(spec)).length <= MAX_TASK_SPEC_BYTES, `spec must be at most ${MAX_TASK_SPEC_BYTES} bytes`);
264
+ // taskId is generated by the client and becomes the task id, so a retried
265
+ // post is a no-op. The same poster gets the existing task back, any other
266
+ // poster gets 409.
267
+ export const PostTaskRequest = z.strictObject({
268
+ taskId: z.uuid(),
269
+ taskType: TaskType,
270
+ spec: TaskSpec,
271
+ verification: VerificationSpec,
272
+ expiresAt: Timestamp.optional(),
273
+ });
274
+ export const TaskResponse = z.strictObject({
275
+ id: z.uuid(),
276
+ posterAgentId: AgentId,
277
+ claimantAgentId: AgentId.nullable(),
278
+ taskType: TaskType,
279
+ spec: z.record(z.string(), z.unknown()),
280
+ verification: VerificationSpec,
281
+ state: TaskState,
282
+ postedAt: Timestamp,
283
+ claimedAt: Timestamp.nullable(),
284
+ submittedAt: Timestamp.nullable(),
285
+ verifiedAt: Timestamp.nullable(),
286
+ expiresAt: Timestamp,
287
+ // Present only in responses to the poster or the claimant.
288
+ submission: z.string().optional(),
289
+ });
290
+ // Claim and submit payloads name the task they are for, and the server checks
291
+ // it against the path. Without it a signed claim for one task could be
292
+ // replayed against any other.
293
+ export const ClaimTaskRequest = z.strictObject({ taskId: z.uuid() });
294
+ export const SubmitTaskRequest = z.strictObject({
295
+ taskId: z.uuid(),
296
+ submission: z
297
+ .string()
298
+ .refine((text) => utf8Encode(JSON.stringify(text)).length <= MAX_SUBMISSION_BYTES, `submission must be at most ${MAX_SUBMISSION_BYTES} bytes as JSON`),
299
+ });
300
+ // Named for its task like claim and submit, checked against the path.
301
+ export const TaskOutcomeRequest = z.strictObject({
302
+ taskId: z.uuid(),
303
+ outcome: TaskOutcome,
304
+ evidenceHash: Sha256Hex.optional(),
305
+ });
306
+ export const ListTasksQuery = z.strictObject({
307
+ state: TaskState.default('open'),
308
+ taskType: TaskType.optional(),
309
+ limit: Limit,
310
+ });
311
+ export const ListTasksResponse = z.strictObject({
312
+ tasks: z.array(TaskResponse),
313
+ });
314
+ // The body of a signed rating. A rating payload is small, so it takes the
315
+ // event envelope cap.
316
+ export const SignedRatingRequest = z.strictObject({
317
+ envelope: Jws.max(MAX_ENVELOPE_CHARS),
318
+ });
319
+ export const RatingValue = z.int().min(1).max(5);
320
+ // One rater rates one ratee on one dimension. A new rating on the same
321
+ // dimension replaces the old one. issuedAt is signed with the rest, so an
322
+ // old envelope sent again can never replace a newer rating.
323
+ export const RatingRequest = z.strictObject({
324
+ rateeAgentId: AgentId,
325
+ dimension: Dimension,
326
+ value: RatingValue,
327
+ issuedAt: Timestamp,
328
+ });
329
+ // The stored rating. raterScoreAtTime is the rater's score when it rated,
330
+ // which is the weight the rating carries in scoring.
331
+ export const RatingResponse = z.strictObject({
332
+ rateeAgentId: AgentId,
333
+ dimension: Dimension,
334
+ value: RatingValue,
335
+ raterScoreAtTime: z.number().min(0).max(1),
336
+ });
337
+ // A base dimension with no row is still listed, with every field after
338
+ // dimension null. Competence entries appear only where a row exists.
339
+ export const ScoreEntry = z.strictObject({
340
+ version: Version,
341
+ dimension: Dimension,
342
+ value: z.number().min(0).max(1).nullable(),
343
+ windowStart: Timestamp.nullable(),
344
+ windowEnd: Timestamp.nullable(),
345
+ computedAt: Timestamp.nullable(),
346
+ });
347
+ // The agent's current version only.
348
+ export const ScoreResponse = z.strictObject({
349
+ agentId: AgentId,
350
+ scores: z.array(ScoreEntry),
351
+ });
352
+ // The signed in operator and the agents they own, for the web's my agents
353
+ // page. lastSeenAt is the newest received_at of the agent's events, null
354
+ // when it has sent none. Agents are ordered by lastSeenAt, newest first,
355
+ // with the never seen last. verifiedTasks and seedTasks are the same counts
356
+ // as on AgentResponse, and so are level and standing.
357
+ export const MeAgent = z.strictObject({
358
+ id: AgentId,
359
+ name: Name,
360
+ handle: AgentHandle,
361
+ operator: Operator,
362
+ version: Version,
363
+ createdAt: Timestamp,
364
+ lastSeenAt: Timestamp.nullable(),
365
+ scores: ScoreResponse,
366
+ verifiedTasks: Count,
367
+ seedTasks: Count,
368
+ level: Level.optional(),
369
+ standing: Standing.optional(),
370
+ });
371
+ // operator.createdAt is when the operator first registered or signed in,
372
+ // shown as operator since on the account page.
373
+ export const MeResponse = z.strictObject({
374
+ operator: z.strictObject({
375
+ login: z.string().min(1).max(39),
376
+ githubId: z.int().positive(),
377
+ createdAt: Timestamp,
378
+ }),
379
+ agents: z.array(MeAgent),
380
+ });
381
+ // DELETE /v1/me. The operator types their GitHub login to confirm, and it
382
+ // must equal the signed in login exactly.
383
+ export const DeleteMeRequest = z.strictObject({
384
+ login: z.string().min(1).max(39),
385
+ });
386
+ // GET /v1/agents/:id/seal and its alias /credential. seal and credential are
387
+ // the same compact JWS. credential is kept for one release (VOU-77).
388
+ export const CredentialResponse = z.strictObject({
389
+ credential: Jws,
390
+ seal: Jws,
391
+ payload: CredentialPayload,
392
+ });
393
+ // A bare /v1/leaderboard is the reliability board.
394
+ export const LeaderboardQuery = z.strictObject({
395
+ dimension: Dimension.default('reliability'),
396
+ limit: Limit,
397
+ });
398
+ // Rows on the current version only, ranked by level first (gold, silver,
399
+ // bronze, then none or not yet scored), then by value, highest first. Ties
400
+ // go to the value computed first.
401
+ export const LeaderboardEntry = z.strictObject({
402
+ rank: z.int().min(1),
403
+ agentId: AgentId,
404
+ name: Name,
405
+ handle: AgentHandle,
406
+ operator: Operator,
407
+ version: Version,
408
+ value: z.number().min(0).max(1),
409
+ // Tasks this agent claimed that reached verified and count toward trust,
410
+ // across all versions. The same rule as AgentCounts.verifiedTasks.
411
+ verifiedTasks: z.int().min(0),
412
+ // How many of verifiedTasks the seed agent posted. The API always sends
413
+ // it. Optional so the web still reads an answer from an older API.
414
+ seedTasks: Count.optional(),
415
+ // True for an agent SealKeeper runs itself. See AgentResponse.
416
+ operatedBySealKeeper: z.boolean(),
417
+ // Deprecated, the old name with the same value. Removed in VOU-77.
418
+ operatedByVouched: z.boolean().optional(),
419
+ // As on AgentResponse. Optional so the web reads an older API's answer.
420
+ level: Level.optional(),
421
+ standing: Standing.optional(),
422
+ });
423
+ export const LeaderboardResponse = z.strictObject({
424
+ dimension: Dimension,
425
+ entries: z.array(LeaderboardEntry),
426
+ });
427
+ // The public feed. Payloads carry public facts only. Never an operator
428
+ // email, an envelope, a spec or a submission.
429
+ export const FeedKind = z.enum([
430
+ 'registration',
431
+ 'task_verified',
432
+ 'score_change',
433
+ 'flag',
434
+ 'version_change',
435
+ ]);
436
+ // The one flag code today. Two sides of a counterparty task disagreed.
437
+ export const FeedFlagCode = z.enum(['outcome_disagreement']);
438
+ // name is the agent's name when the item was written. The item's handle
439
+ // is the agent as it is now.
440
+ const FeedAgent = {
441
+ agentId: AgentId,
442
+ name: Name,
443
+ };
444
+ export const FeedPayloads = {
445
+ registration: z.strictObject({ ...FeedAgent, version: Version }),
446
+ task_verified: z.strictObject({
447
+ ...FeedAgent,
448
+ taskId: z.uuid(),
449
+ taskType: TaskType,
450
+ }),
451
+ score_change: z.strictObject({
452
+ ...FeedAgent,
453
+ version: Version,
454
+ dimension: Dimension,
455
+ // null when the dimension had no value before.
456
+ old: z.number().min(0).max(1).nullable(),
457
+ new: z.number().min(0).max(1),
458
+ }),
459
+ flag: z.strictObject({
460
+ ...FeedAgent,
461
+ code: FeedFlagCode,
462
+ taskId: z.uuid(),
463
+ }),
464
+ // The agent moved its version with a signed request. The old and the new
465
+ // version, beside the agent every item names.
466
+ version_change: z.strictObject({
467
+ ...FeedAgent,
468
+ old: Version,
469
+ new: Version,
470
+ }),
471
+ };
472
+ const feedItemOf = (kind) => z.strictObject({
473
+ // The SSE event id. Ascending in insert order.
474
+ id: z.int().min(1),
475
+ kind: z.literal(kind),
476
+ agentId: AgentId.nullable(),
477
+ // The agent the payload names, as it is now. Looked up when the item is
478
+ // read, so a renamed agent's old items link to where it lives today.
479
+ operator: Operator,
480
+ handle: AgentHandle,
481
+ payload: FeedPayloads[kind],
482
+ createdAt: Timestamp,
483
+ });
484
+ export const FeedItem = z.discriminatedUnion('kind', [
485
+ feedItemOf('registration'),
486
+ feedItemOf('task_verified'),
487
+ feedItemOf('score_change'),
488
+ feedItemOf('flag'),
489
+ feedItemOf('version_change'),
490
+ ]);
491
+ export const FEED_RECENT_MAX = 200;
492
+ export const FeedRecentQuery = z.strictObject({
493
+ limit: z.coerce.number().int().min(1).max(FEED_RECENT_MAX).default(50),
494
+ });
495
+ // Newest first.
496
+ export const FeedRecentResponse = z.strictObject({
497
+ items: z.array(FeedItem),
498
+ });
499
+ // Network totals for the landing page. eventsLast24h counts by the server's
500
+ // received_at, so a client clock cannot move it.
501
+ export const StatsResponse = z.strictObject({
502
+ agents: z.int().min(0),
503
+ verifiedTasks: z.int().min(0),
504
+ eventsLast24h: z.int().min(0),
505
+ });
506
+ // GET /v1/check/:login/:name. One call for a caller that is about to
507
+ // delegate work and wants a yes or no on the agent's track record. Query
508
+ // values arrive as text and are parsed exactly, so an empty or odd value is
509
+ // a 400 and never quietly becomes 0.
510
+ const QueryCount = z
511
+ .string()
512
+ .regex(/^\d{1,7}$/, 'must be a whole number')
513
+ .transform(Number);
514
+ const QueryScore = z
515
+ .string()
516
+ .regex(/^[01](\.\d{1,6})?$/, 'must be a number from 0 to 1')
517
+ .transform(Number)
518
+ .pipe(z.number().min(0).max(1));
519
+ // minVerified, maxIncidents and minLevel always run, with these defaults.
520
+ // minReliability and minSafety run only when given. minLevel defaults to
521
+ // bronze, the first level that recommends. minLevel=none asks for no level,
522
+ // which every agent meets.
523
+ export const CHECK_DEFAULT_MIN_VERIFIED = 1;
524
+ export const CHECK_DEFAULT_MAX_INCIDENTS = 0;
525
+ export const CHECK_DEFAULT_MIN_LEVEL = 'bronze';
526
+ export const CheckQuery = z.strictObject({
527
+ minVerified: QueryCount.default(CHECK_DEFAULT_MIN_VERIFIED),
528
+ maxIncidents: QueryCount.default(CHECK_DEFAULT_MAX_INCIDENTS),
529
+ minReliability: QueryScore.optional(),
530
+ minSafety: QueryScore.optional(),
531
+ minLevel: Level.default(CHECK_DEFAULT_MIN_LEVEL),
532
+ });
533
+ // The checks, named after the query value that sets them, so the name says
534
+ // which way the comparison goes. min is actual >= required, max is
535
+ // actual <= required. For minLevel that is the order of LEVEL_RANK. seal is
536
+ // the one check no query value sets. It is sent only when the SEAL is
537
+ // withheld, required present and actual withheld, and it fails.
538
+ export const CheckName = z.enum([
539
+ 'seal',
540
+ 'minVerified',
541
+ 'maxIncidents',
542
+ 'minReliability',
543
+ 'minSafety',
544
+ 'minLevel',
545
+ ]);
546
+ // Whether the agent holds a current SEAL, for the seal check.
547
+ export const SealPresence = z.enum(['present', 'withheld']);
548
+ // actual is null when the agent has no value yet, as for a trust dimension
549
+ // before any verified task. A null never passes. minLevel carries levels
550
+ // in required and actual, and seal carries SealPresence.
551
+ export const Check = z.strictObject({
552
+ name: CheckName,
553
+ required: z.union([z.number().min(0), Level, SealPresence]),
554
+ actual: z.union([z.number().min(0), Level, SealPresence]).nullable(),
555
+ ok: z.boolean(),
556
+ });
557
+ // ok is true only when every check is. credential is the agent's current
558
+ // SEAL, so the caller can verify the numbers offline with the Vouched public
559
+ // key. This is the shape the CLI's Mastra adapter publishes as its own type,
560
+ // so it stays as it is. What the API sends is SealCheckResponse below.
561
+ export const CheckResponse = z.strictObject({
562
+ ok: z.boolean(),
563
+ id: AgentId,
564
+ handle: AgentHandle,
565
+ checks: z.array(Check).min(1),
566
+ credential: Jws,
567
+ });
568
+ // What GET /v1/check answers. CheckResponse plus seal, the same string as
569
+ // credential. Both are sent for one release, then credential goes (VOU-77).
570
+ // Both are null when the SEAL is withheld after 90 dormant days, and the
571
+ // answer then carries a failing seal check, so ok is false.
572
+ export const SealCheckResponse = CheckResponse.extend({
573
+ credential: Jws.nullable(),
574
+ seal: Jws.nullable(),
575
+ }).refine((r) => r.seal === r.credential &&
576
+ (r.seal !== null ||
577
+ r.checks.some((k) => k.name === 'seal' && k.actual === 'withheld')), 'seal and credential are the same, and null only with a withheld seal check');
578
+ // GET /v1/agents/:id/seal, its alias /credential and the handle route, when
579
+ // the agent's current version has been dormant for 90 days or more and the
580
+ // last scoring run marked it no_seal. No SEAL is issued until the next run
581
+ // after an accepted event. dormant_days is counted at the answer.
582
+ export const SealWithheldResponse = z.strictObject({
583
+ error: z.strictObject({
584
+ code: z.literal('no_seal'),
585
+ message: z.string(),
586
+ }),
587
+ id: AgentId,
588
+ dormant_days: z.int().min(0).nullable(),
589
+ });
590
+ // POST /v1/seal/verify. The route's body cap keeps the SEAL small, so the
591
+ // string itself has no tighter limit here.
592
+ export const SealVerifyRequest = z.strictObject({ seal: z.string() });
593
+ export const SealBrokenReason = z.enum([
594
+ 'malformed',
595
+ 'unknown_kid',
596
+ 'bad_signature',
597
+ 'wrong_issuer',
598
+ 'unsupported_version',
599
+ 'expired',
600
+ 'not_yet_valid',
601
+ ]);
602
+ // Always 200 for a SEAL, good or broken. expiresIn is whole seconds left.
603
+ // payload is version 1, or a legacy payload without ver before
604
+ // LEGACY_UNTIL.
605
+ export const SealVerifyResponse = z.discriminatedUnion('valid', [
606
+ z.strictObject({
607
+ valid: z.literal(true),
608
+ payload: SealPayload,
609
+ expiresIn: z.int().min(1),
610
+ }),
611
+ z.strictObject({ valid: z.literal(false), reason: SealBrokenReason }),
612
+ ]);
613
+ export const ScoreRunResponse = z.strictObject({
614
+ scored: z.int().min(0),
615
+ });
616
+ // POST /internal/seed/run. open is the seed agent's open tasks after the
617
+ // run, added is how many this run posted.
618
+ export const SeedRunResponse = z.strictObject({
619
+ open: z.int().min(0),
620
+ added: z.int().min(0),
621
+ });
622
+ export const ErrorIssue = z.strictObject({
623
+ path: z.array(z.union([z.string(), z.number()])),
624
+ code: z.string(),
625
+ message: z.string(),
626
+ });
627
+ export const ErrorResponse = z.strictObject({
628
+ error: z.strictObject({
629
+ code: z.string(),
630
+ message: z.string(),
631
+ issues: z.array(ErrorIssue).optional(),
632
+ }),
633
+ });
@@ -0,0 +1,4 @@
1
+ export declare function base64urlEncode(bytes: Uint8Array): string;
2
+ export declare function base64urlDecode(text: string): Uint8Array<ArrayBuffer>;
3
+ export declare const utf8Encode: (text: string) => Uint8Array<ArrayBuffer>;
4
+ export declare const utf8Decode: (bytes: Uint8Array) => string;
@@ -0,0 +1,43 @@
1
+ const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
2
+ const LOOKUP = new Map([...ALPHABET].map((char, i) => [char, i]));
3
+ export function base64urlEncode(bytes) {
4
+ let out = '';
5
+ let buffer = 0;
6
+ let bits = 0;
7
+ for (const byte of bytes) {
8
+ buffer = ((buffer << 8) | byte) & 0xffff;
9
+ bits += 8;
10
+ while (bits >= 6) {
11
+ bits -= 6;
12
+ out += ALPHABET[(buffer >> bits) & 63];
13
+ }
14
+ }
15
+ if (bits > 0)
16
+ out += ALPHABET[(buffer << (6 - bits)) & 63];
17
+ return out;
18
+ }
19
+ export function base64urlDecode(text) {
20
+ if (text.length % 4 === 1)
21
+ throw new Error('Invalid base64url length');
22
+ const out = new Uint8Array(Math.floor((text.length * 6) / 8));
23
+ let buffer = 0;
24
+ let bits = 0;
25
+ let j = 0;
26
+ for (const char of text) {
27
+ const value = LOOKUP.get(char);
28
+ if (value === undefined)
29
+ throw new Error('Invalid base64url character');
30
+ buffer = ((buffer << 6) | value) & 0xfff;
31
+ bits += 6;
32
+ if (bits >= 8) {
33
+ bits -= 8;
34
+ out[j++] = (buffer >> bits) & 0xff;
35
+ }
36
+ }
37
+ if ((buffer & ((1 << bits) - 1)) !== 0) {
38
+ throw new Error('Non-canonical base64url');
39
+ }
40
+ return out;
41
+ }
42
+ export const utf8Encode = (text) => new TextEncoder().encode(text);
43
+ export const utf8Decode = (bytes) => new TextDecoder('utf-8', { fatal: true }).decode(bytes);