@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.
- package/LICENSE +202 -0
- package/NOTICE +2 -0
- package/README.md +14 -1
- package/dist/agent-id.d.ts +13 -0
- package/dist/agent-id.js +29 -0
- package/dist/agent-name.d.ts +9 -0
- package/dist/agent-name.js +58 -0
- package/dist/api.d.ts +1212 -0
- package/dist/api.js +633 -0
- package/dist/base64url.d.ts +4 -0
- package/dist/base64url.js +43 -0
- package/dist/credential.d.ts +392 -0
- package/dist/credential.js +287 -0
- package/dist/db/agent-renames.d.ts +126 -0
- package/dist/db/agent-renames.js +20 -0
- package/dist/db/agents.d.ts +145 -0
- package/dist/db/agents.js +40 -0
- package/dist/db/client.d.ts +1896 -0
- package/dist/db/client.js +36 -0
- package/dist/db/credentials.d.ts +143 -0
- package/dist/db/credentials.js +16 -0
- package/dist/db/deleted-operators.d.ts +75 -0
- package/dist/db/deleted-operators.js +13 -0
- package/dist/db/events.d.ts +160 -0
- package/dist/db/events.js +34 -0
- package/dist/db/feed-items.d.ts +109 -0
- package/dist/db/feed-items.js +16 -0
- package/dist/db/index.d.ts +56 -0
- package/dist/db/index.js +23 -0
- package/dist/db/migrate.d.ts +1 -0
- package/dist/db/migrate.js +30 -0
- package/dist/db/migrator.d.ts +2 -0
- package/dist/db/migrator.js +20 -0
- package/dist/db/operators.d.ts +109 -0
- package/dist/db/operators.js +11 -0
- package/dist/db/quota-counters.d.ts +109 -0
- package/dist/db/quota-counters.js +17 -0
- package/dist/db/ratings.d.ts +160 -0
- package/dist/db/ratings.js +27 -0
- package/dist/db/scores.d.ts +160 -0
- package/dist/db/scores.js +15 -0
- package/dist/db/standing.d.ts +228 -0
- package/dist/db/standing.js +29 -0
- package/dist/db/task-outcomes.d.ts +126 -0
- package/dist/db/task-outcomes.js +18 -0
- package/dist/db/tasks.d.ts +251 -0
- package/dist/db/tasks.js +49 -0
- package/dist/db/timestamps.d.ts +4 -0
- package/dist/db/timestamps.js +24 -0
- package/dist/db/url.d.ts +12 -0
- package/dist/db/url.js +25 -0
- package/dist/dimensions.d.ts +21 -0
- package/dist/dimensions.js +15 -0
- package/dist/envelope.d.ts +16 -0
- package/dist/envelope.js +70 -0
- package/dist/events.d.ts +153 -0
- package/dist/events.js +98 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +12 -0
- package/dist/seal-conformance.d.ts +14 -0
- package/dist/seal-conformance.js +176 -0
- package/dist/standing.d.ts +46 -0
- package/dist/standing.js +79 -0
- package/dist/tasks.d.ts +49 -0
- package/dist/tasks.js +132 -0
- package/dist/top-dimensions.d.ts +9 -0
- package/dist/top-dimensions.js +13 -0
- 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);
|