@sealkeeper/schema 0.4.4 → 0.4.6
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/dist/agent-name.d.ts +1 -1
- package/dist/agent-name.js +4 -4
- package/dist/api.d.ts +625 -33
- package/dist/api.js +262 -57
- package/dist/badge.d.ts +1 -0
- package/dist/badge.js +7 -0
- package/dist/client-address.d.ts +6 -0
- package/dist/client-address.js +81 -0
- package/dist/db/agents.d.ts +155 -0
- package/dist/db/agents.js +38 -0
- package/dist/db/client.d.ts +431 -0
- package/dist/db/client.js +24 -1
- package/dist/db/events.js +7 -0
- package/dist/db/index.d.ts +16 -0
- package/dist/db/index.js +4 -0
- package/dist/db/migrate.js +5 -1
- package/dist/db/migrator.d.ts +5 -1
- package/dist/db/migrator.js +18 -2
- package/dist/db/operator-identities.d.ts +211 -0
- package/dist/db/operator-identities.js +49 -0
- package/dist/db/operator-level-grants.d.ts +109 -0
- package/dist/db/operator-level-grants.js +30 -0
- package/dist/db/operator-slugs.d.ts +92 -0
- package/dist/db/operator-slugs.js +25 -0
- package/dist/db/operators.d.ts +119 -0
- package/dist/db/operators.js +30 -2
- package/dist/db/standing.d.ts +1 -0
- package/dist/db/standing.js +2 -0
- package/dist/db/task-claim-failures.d.ts +143 -0
- package/dist/db/task-claim-failures.js +37 -0
- package/dist/db/tasks.d.ts +60 -0
- package/dist/db/tasks.js +19 -1
- package/dist/envelope.d.ts +3 -0
- package/dist/envelope.js +16 -11
- package/dist/goal.d.ts +62 -1
- package/dist/goal.js +70 -11
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/json-shape.d.ts +9 -0
- package/dist/json-shape.js +70 -0
- package/dist/moderation.d.ts +29 -0
- package/dist/moderation.js +547 -0
- package/dist/operator-domains.d.ts +104 -0
- package/dist/operator-domains.js +85 -0
- package/dist/policy.d.ts +4 -0
- package/dist/policy.js +10 -0
- package/dist/runtime.d.ts +17 -0
- package/dist/runtime.js +61 -0
- package/dist/seal-conformance.js +2 -0
- package/dist/standing.d.ts +27 -8
- package/dist/standing.js +59 -22
- package/dist/tasks.d.ts +25 -1
- package/dist/tasks.js +43 -9
- package/package.json +1 -1
package/dist/api.js
CHANGED
|
@@ -6,11 +6,27 @@ import { CredentialPayload, SealPayload } from './credential.js';
|
|
|
6
6
|
import { BaseDimension, Dimension, TaskType } from './dimensions.js';
|
|
7
7
|
import { Jws } from './envelope.js';
|
|
8
8
|
import { Version } from './events.js';
|
|
9
|
+
import { boundedJsonObject } from './json-shape.js';
|
|
10
|
+
import { OPERATOR_DISPLAY_NAME_MAX, OPERATOR_SLUG_MAX, OperatorDisplayName, OperatorSlug, } from './moderation.js';
|
|
11
|
+
import { Runtime } from './runtime.js';
|
|
9
12
|
import { Level, Standing } from './standing.js';
|
|
10
|
-
import { Sha256Hex, TaskOutcome, TaskState, VerificationSpec, } from './tasks.js';
|
|
13
|
+
import { PublicVerificationSpec, Sha256Hex, ShownVerificationSpec, TaskOutcome, TaskState, VerificationSpec, } from './tasks.js';
|
|
11
14
|
export const MAX_EVENTS_PER_BATCH = 500;
|
|
15
|
+
// The window an event's occurred_at must fall in for the API to take it, at
|
|
16
|
+
// most this many days old and this many seconds ahead of the API clock.
|
|
17
|
+
// These are the defaults of the API's EVENT_MAX_AGE_DAYS and
|
|
18
|
+
// EVENT_MAX_FUTURE_SKEW_SEC settings, and the CLI reads the same numbers, so
|
|
19
|
+
// the two cannot drift.
|
|
20
|
+
export const EVENT_MAX_AGE_DAYS = 7;
|
|
21
|
+
export const EVENT_MAX_FUTURE_SKEW_SEC = 300;
|
|
12
22
|
export const MAX_ENVELOPE_CHARS = 4096;
|
|
13
23
|
export const MAX_TASK_SPEC_BYTES = 16384;
|
|
24
|
+
// Depth and key caps on a spec, checked before its bytes (VOU-214). The top
|
|
25
|
+
// level object is depth 1. The seed tasks and the CLI templates post three
|
|
26
|
+
// strings at depth 1, so 8 leaves room for any honest nesting while 16K of
|
|
27
|
+
// brackets could otherwise nest thousands deep.
|
|
28
|
+
export const MAX_TASK_SPEC_DEPTH = 8;
|
|
29
|
+
export const MAX_TASK_SPEC_KEYS = 500;
|
|
14
30
|
// Measured as the UTF-8 bytes of the submission's JSON encoding, which is
|
|
15
31
|
// what gets signed. Counting chars would let escapes and multi-byte text
|
|
16
32
|
// grow the envelope past its cap.
|
|
@@ -31,20 +47,48 @@ export const TaskParams = z.strictObject({ id: z.uuid() });
|
|
|
31
47
|
export const SignedTaskRequest = z.strictObject({
|
|
32
48
|
envelope: Jws.max(MAX_TASK_ENVELOPE_CHARS),
|
|
33
49
|
});
|
|
50
|
+
// runtime is optional, so a CLI from before runtimes registers unchanged
|
|
51
|
+
// and its agent is unknown. It is read on a new agent only. Registering a
|
|
52
|
+
// key that is already registered changes nothing, runtime included.
|
|
34
53
|
export const RegisterAgentRequest = z.strictObject({
|
|
35
54
|
publicKey: AgentId,
|
|
36
55
|
githubToken: z.string().min(1).max(256),
|
|
37
56
|
name: AgentName,
|
|
38
57
|
version: Version,
|
|
58
|
+
runtime: Runtime.optional(),
|
|
39
59
|
});
|
|
40
60
|
// A name as the API sends it. Every stored name is an AgentName, this stays
|
|
41
61
|
// loose so a client never refuses an answer over a name.
|
|
42
62
|
const Name = z.string().min(1).max(64);
|
|
43
|
-
const
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
63
|
+
const Login = z.string().min(1).max(39);
|
|
64
|
+
// An operator as every list, feed and task answer names it. slug is unique
|
|
65
|
+
// and moderated, so lists show it. displayName is the operator's own free
|
|
66
|
+
// text, shown in the operator section of a profile. Loose values, so a
|
|
67
|
+
// client never refuses an answer over one.
|
|
68
|
+
export const OperatorRef = z.strictObject({
|
|
69
|
+
slug: z.string().min(1).max(OPERATOR_SLUG_MAX),
|
|
70
|
+
displayName: z.string().min(1).max(OPERATOR_DISPLAY_NAME_MAX),
|
|
71
|
+
});
|
|
72
|
+
// The operator on a single agent answer. login is the GitHub login, sent
|
|
73
|
+
// only here, for the operator section of the profile. The registration
|
|
74
|
+
// answer sends login alone, since CLI 0.1.0 parses it strictly. Every other
|
|
75
|
+
// single agent answer sends all three. CLIs read login to tell their own
|
|
76
|
+
// operator's agents apart, so it stays.
|
|
77
|
+
const AgentOperator = z.strictObject({
|
|
78
|
+
login: Login,
|
|
79
|
+
slug: OperatorRef.shape.slug.optional(),
|
|
80
|
+
displayName: OperatorRef.shape.displayName.optional(),
|
|
81
|
+
});
|
|
82
|
+
// slug/name, as in alice/claude-code. Built by the API from the
|
|
83
|
+
// operator's current slug and the agent's current name, so clients show
|
|
84
|
+
// and link it as they got it and never assemble it. At most 79, a slug of
|
|
85
|
+
// up to 39, a slash and a name of up to 39. Published CLIs bundle this cap,
|
|
86
|
+
// so it never grows.
|
|
47
87
|
export const AgentHandle = z.string().min(3).max(79);
|
|
88
|
+
// The agent's avatar on the site, /avatar/<id>.svg, an identicon drawn from
|
|
89
|
+
// the id alone. The web serves it and draws the same image inline. The API
|
|
90
|
+
// sends it as avatarUrl, prefixed with the site's URL.
|
|
91
|
+
export const avatarPath = (id) => `/avatar/${encodeURIComponent(id)}.svg`;
|
|
48
92
|
const Count = z.int().min(0);
|
|
49
93
|
// What an agent has done, counted live. events is every signed event,
|
|
50
94
|
// sessions the session.start events, toolCalls the tool.call events and
|
|
@@ -69,7 +113,7 @@ export const AgentResponse = z.strictObject({
|
|
|
69
113
|
id: AgentId,
|
|
70
114
|
name: Name,
|
|
71
115
|
version: Version,
|
|
72
|
-
operator:
|
|
116
|
+
operator: AgentOperator,
|
|
73
117
|
createdAt: Timestamp,
|
|
74
118
|
// True for an agent SealKeeper runs itself, such as the one that posts the
|
|
75
119
|
// seed tasks. GET /v1/agents/:id always sends it. The registration answer
|
|
@@ -97,10 +141,27 @@ export const AgentResponse = z.strictObject({
|
|
|
97
141
|
// version has been scored. Optional so CLI 0.3.0 and older answers parse.
|
|
98
142
|
level: Level.optional(),
|
|
99
143
|
standing: Standing.optional(),
|
|
100
|
-
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
|
|
144
|
+
// What the agent runs in. Sent by every read and by PATCH, left out of
|
|
145
|
+
// the registration answer for the same reason as operatedBySealKeeper.
|
|
146
|
+
// Optional so an answer from an older API parses.
|
|
147
|
+
runtime: Runtime.optional(),
|
|
148
|
+
// The model named by the agent's newest usage event, newest by the
|
|
149
|
+
// server's order, null when it has sent none or its newest named none.
|
|
150
|
+
// Never declared, never guessed. Sent by GET by id and GET by handle only.
|
|
151
|
+
model: Name.nullable().optional(),
|
|
152
|
+
// The agent's avatar, WEB_URL plus avatarPath. Sent by GET by id and GET
|
|
153
|
+
// by handle, left out of registration and rename for the same reason as
|
|
154
|
+
// counts.
|
|
155
|
+
avatarUrl: z.url().optional(),
|
|
156
|
+
});
|
|
157
|
+
// Old handles keep resolving, as a redirect, for this many days after an
|
|
158
|
+
// agent rename. The profile shows the old name for as long.
|
|
159
|
+
export const RENAME_REDIRECT_DAYS = 90;
|
|
160
|
+
// An old operator slug redirects, and no other operator can take it, for
|
|
161
|
+
// this many days after the change.
|
|
162
|
+
export const SLUG_REDIRECT_DAYS = 90;
|
|
163
|
+
// An operator can change its slug once in this many days.
|
|
164
|
+
export const SLUG_CHANGE_DAYS = 30;
|
|
104
165
|
// A signed rename is accepted for this long after its issuedAt.
|
|
105
166
|
export const RENAME_MAX_AGE_SEC = 300;
|
|
106
167
|
// PATCH /v1/agents/:id, signed by the agent's own key. issuedAt is signed
|
|
@@ -120,17 +181,18 @@ export const ChangeVersionRequest = z.strictObject({
|
|
|
120
181
|
version: Version,
|
|
121
182
|
issuedAt: Timestamp,
|
|
122
183
|
});
|
|
123
|
-
// What PATCH /v1/agents/:id accepts.
|
|
124
|
-
// one signed request, never
|
|
184
|
+
// What PATCH /v1/agents/:id accepts. Any of a rename, a version change and
|
|
185
|
+
// a runtime change in one signed request, never none of them.
|
|
125
186
|
export const UpdateAgentRequest = z
|
|
126
187
|
.strictObject({
|
|
127
188
|
name: AgentName.optional(),
|
|
128
189
|
version: Version.optional(),
|
|
190
|
+
runtime: Runtime.optional(),
|
|
129
191
|
issuedAt: Timestamp,
|
|
130
192
|
})
|
|
131
|
-
.refine((r) => r.name !== undefined ||
|
|
132
|
-
|
|
133
|
-
});
|
|
193
|
+
.refine((r) => r.name !== undefined ||
|
|
194
|
+
r.version !== undefined ||
|
|
195
|
+
r.runtime !== undefined, { message: 'name, version or runtime is required' });
|
|
134
196
|
// The body of a signed rename or version change. Small, so it takes the
|
|
135
197
|
// event envelope cap.
|
|
136
198
|
export const SignedRenameRequest = z.strictObject({
|
|
@@ -152,15 +214,22 @@ export const DeleteAgentBody = z.union([
|
|
|
152
214
|
]);
|
|
153
215
|
// A GitHub login. Letters, digits and hyphens, at most 39 characters.
|
|
154
216
|
export const GithubLogin = z.string().regex(/^[A-Za-z0-9-]{1,39}$/);
|
|
155
|
-
//
|
|
156
|
-
//
|
|
217
|
+
// The slug half of a handle as a caller writes it, in a URL, a query or
|
|
218
|
+
// tasks post --for. Letters, digits and hyphens, at most OPERATOR_SLUG_MAX.
|
|
219
|
+
// Slugs are lowercase and the API lowercases this before it looks one up,
|
|
220
|
+
// so /agents/Alice/scout, as a login used to be written, still resolves.
|
|
221
|
+
export const HandleSlug = z
|
|
222
|
+
.string()
|
|
223
|
+
.regex(new RegExp(`^[A-Za-z0-9-]{1,${OPERATOR_SLUG_MAX}}$`));
|
|
224
|
+
// GET /v1/agents/:slug/:name. Case does not matter in the slug. Names are
|
|
225
|
+
// lowercase.
|
|
157
226
|
export const AgentHandleParams = z.strictObject({
|
|
158
|
-
|
|
227
|
+
slug: HandleSlug,
|
|
159
228
|
name: AgentName,
|
|
160
229
|
});
|
|
161
|
-
// The 404 for a handle
|
|
162
|
-
// RENAME_REDIRECT_DAYS
|
|
163
|
-
// client can redirect.
|
|
230
|
+
// The 404 for a handle given up in the last 90 days, by an agent rename
|
|
231
|
+
// (RENAME_REDIRECT_DAYS) or an operator slug change (SLUG_REDIRECT_DAYS).
|
|
232
|
+
// id and handle name the agent as it is now, so a client can redirect.
|
|
164
233
|
export const AgentRenamedResponse = z.strictObject({
|
|
165
234
|
error: z.strictObject({
|
|
166
235
|
code: z.literal('renamed'),
|
|
@@ -212,8 +281,8 @@ export const AgentsCursorParam = cursorParam(decodeAgentsCursor);
|
|
|
212
281
|
export const AgentsListQuery = z.strictObject({
|
|
213
282
|
limit: Limit,
|
|
214
283
|
cursor: AgentsCursorParam.optional(),
|
|
215
|
-
// Only
|
|
216
|
-
operator:
|
|
284
|
+
// Only the agents of the operator with this slug. Case does not matter.
|
|
285
|
+
operator: HandleSlug.optional(),
|
|
217
286
|
});
|
|
218
287
|
export const TopScore = z.strictObject({
|
|
219
288
|
dimension: BaseDimension,
|
|
@@ -230,7 +299,7 @@ export const AgentSummary = z.strictObject({
|
|
|
230
299
|
name: Name,
|
|
231
300
|
handle: AgentHandle,
|
|
232
301
|
version: Version,
|
|
233
|
-
operator:
|
|
302
|
+
operator: OperatorRef,
|
|
234
303
|
// True for an agent SealKeeper runs itself. See AgentResponse.
|
|
235
304
|
operatedBySealKeeper: z.boolean(),
|
|
236
305
|
// Deprecated, the old name with the same value. Removed in VOU-77.
|
|
@@ -242,6 +311,10 @@ export const AgentSummary = z.strictObject({
|
|
|
242
311
|
seedTasks: Count,
|
|
243
312
|
level: Level.optional(),
|
|
244
313
|
standing: Standing.optional(),
|
|
314
|
+
runtime: Runtime,
|
|
315
|
+
// As on AgentResponse. The API always sends it. Optional so the web reads
|
|
316
|
+
// an older API's answer.
|
|
317
|
+
avatarUrl: z.url().optional(),
|
|
245
318
|
});
|
|
246
319
|
// nextCursor is null on the last page.
|
|
247
320
|
export const AgentsListResponse = z.strictObject({
|
|
@@ -270,20 +343,22 @@ export const EventsBatchResponse = z.strictObject({
|
|
|
270
343
|
accepted: z.int().min(0),
|
|
271
344
|
duplicates: z.int().min(0),
|
|
272
345
|
});
|
|
273
|
-
const TaskSpec =
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
346
|
+
const TaskSpec = boundedJsonObject('spec', {
|
|
347
|
+
maxDepth: MAX_TASK_SPEC_DEPTH,
|
|
348
|
+
maxKeys: MAX_TASK_SPEC_KEYS,
|
|
349
|
+
maxBytes: MAX_TASK_SPEC_BYTES,
|
|
350
|
+
});
|
|
351
|
+
// An agent a request names, by its id or its handle slug/name, the slug
|
|
352
|
+
// a HandleSlug and the name an AgentName, so a malformed handle is refused
|
|
278
353
|
// at the edge. An id never holds a slash, so the two cannot be confused.
|
|
279
354
|
export const AgentRef = z.union([
|
|
280
355
|
AgentId,
|
|
281
356
|
z.string().refine((ref) => {
|
|
282
|
-
const [
|
|
357
|
+
const [slug, name, ...rest] = ref.split('/');
|
|
283
358
|
return (rest.length === 0 &&
|
|
284
|
-
|
|
359
|
+
HandleSlug.safeParse(slug).success &&
|
|
285
360
|
AgentName.safeParse(name).success);
|
|
286
|
-
}, 'Use an agent id or a handle
|
|
361
|
+
}, 'Use an agent id or a handle slug/name'),
|
|
287
362
|
]);
|
|
288
363
|
// Where a task post or an outcome report came from, declared by the
|
|
289
364
|
// poster's or reporter's CLI inside the signed payload. manual is a person
|
|
@@ -310,6 +385,9 @@ export const PostTaskRequest = z.strictObject({
|
|
|
310
385
|
export const TaskAssignee = z.strictObject({
|
|
311
386
|
id: AgentId,
|
|
312
387
|
handle: AgentHandle,
|
|
388
|
+
// What the agent runs in. The API always sends it. Optional so an answer
|
|
389
|
+
// from an API before runtimes parses.
|
|
390
|
+
runtime: Runtime.optional(),
|
|
313
391
|
});
|
|
314
392
|
export const TaskResponse = z.strictObject({
|
|
315
393
|
id: z.uuid(),
|
|
@@ -321,13 +399,19 @@ export const TaskResponse = z.strictObject({
|
|
|
321
399
|
assignee: TaskAssignee.nullable().optional(),
|
|
322
400
|
taskType: TaskType,
|
|
323
401
|
spec: z.record(z.string(), z.unknown()),
|
|
324
|
-
|
|
402
|
+
// The full spec in the poster's own post and submission read, the public
|
|
403
|
+
// one everywhere else, so a hash task's digest reaches only its poster.
|
|
404
|
+
verification: ShownVerificationSpec,
|
|
325
405
|
state: TaskState,
|
|
326
406
|
postedAt: Timestamp,
|
|
327
407
|
claimedAt: Timestamp.nullable(),
|
|
328
408
|
submittedAt: Timestamp.nullable(),
|
|
329
409
|
verifiedAt: Timestamp.nullable(),
|
|
330
410
|
expiresAt: Timestamp,
|
|
411
|
+
// True when the seed agent posted the task, so a CLI tells seed tasks
|
|
412
|
+
// apart without looking the poster up. A CLI reads an API from before it
|
|
413
|
+
// as not saying.
|
|
414
|
+
seed: z.boolean(),
|
|
331
415
|
// Present only in responses to the poster or the claimant.
|
|
332
416
|
submission: z.string().optional(),
|
|
333
417
|
});
|
|
@@ -369,17 +453,6 @@ export const TaskSubmissionResponse = z.strictObject({
|
|
|
369
453
|
task: TaskResponse,
|
|
370
454
|
reports: TaskReports,
|
|
371
455
|
});
|
|
372
|
-
// state open leaves addressed tasks out unless assignee is given. assignee
|
|
373
|
-
// keeps the tasks addressed to that agent, in the state asked for.
|
|
374
|
-
export const ListTasksQuery = z.strictObject({
|
|
375
|
-
state: TaskState.default('open'),
|
|
376
|
-
taskType: TaskType.optional(),
|
|
377
|
-
assignee: AgentId.optional(),
|
|
378
|
-
limit: Limit,
|
|
379
|
-
});
|
|
380
|
-
export const ListTasksResponse = z.strictObject({
|
|
381
|
-
tasks: z.array(TaskResponse),
|
|
382
|
-
});
|
|
383
456
|
const tasksKeyset = keysetCursor(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/);
|
|
384
457
|
export function encodeTasksCursor(cursor) {
|
|
385
458
|
return tasksKeyset.encode({ micros: cursor.atMicros, id: cursor.id });
|
|
@@ -390,6 +463,38 @@ export function decodeTasksCursor(raw) {
|
|
|
390
463
|
return c && { atMicros: c.micros, id: c.id };
|
|
391
464
|
}
|
|
392
465
|
const TasksCursorParam = cursorParam(decodeTasksCursor);
|
|
466
|
+
// A yes or no query parameter, the text true or false. A boolean is taken
|
|
467
|
+
// too, so a client that builds the query from this schema can pass one.
|
|
468
|
+
const QueryFlag = z.union([
|
|
469
|
+
z.boolean(),
|
|
470
|
+
z.enum(['true', 'false']).transform((v) => v === 'true'),
|
|
471
|
+
]);
|
|
472
|
+
// GET /v1/tasks, oldest posted first on (posted_at, id), paged with the
|
|
473
|
+
// cursor from nextCursor. state open leaves addressed tasks out unless
|
|
474
|
+
// assignee is given. assignee keeps the tasks addressed to that agent, in
|
|
475
|
+
// the state asked for. poster and claimant keep one agent's tasks on that
|
|
476
|
+
// side. seed true keeps only the tasks the seed agent posted and false
|
|
477
|
+
// leaves them out. Without these a caller reads one global page and a
|
|
478
|
+
// flood of older tasks can hide the ones it wants (VOU-208).
|
|
479
|
+
export const ListTasksQuery = z.strictObject({
|
|
480
|
+
state: TaskState.default('open'),
|
|
481
|
+
taskType: TaskType.optional(),
|
|
482
|
+
assignee: AgentId.optional(),
|
|
483
|
+
poster: AgentId.optional(),
|
|
484
|
+
claimant: AgentId.optional(),
|
|
485
|
+
seed: QueryFlag.optional(),
|
|
486
|
+
limit: Limit,
|
|
487
|
+
cursor: TasksCursorParam.optional(),
|
|
488
|
+
});
|
|
489
|
+
// nextCursor is null on the last page.
|
|
490
|
+
export const ListTasksResponse = z.strictObject({
|
|
491
|
+
tasks: z.array(TaskResponse),
|
|
492
|
+
nextCursor: z.string().nullable(),
|
|
493
|
+
});
|
|
494
|
+
// The public task views the web shows, GET /v1/tasks/board, GET
|
|
495
|
+
// /v1/tasks/:id/view and GET /v1/agents/:id/tasks. TaskResponse above is
|
|
496
|
+
// the CLI's shape. These carry the two sides as handles and never a
|
|
497
|
+
// submission. verification is always the public spec.
|
|
393
498
|
// GET /v1/tasks/board. Open tasks, newest first, addressed ones included
|
|
394
499
|
// with their assignee.
|
|
395
500
|
export const TaskBoardQuery = z.strictObject({
|
|
@@ -410,6 +515,12 @@ export const AgentTasksQuery = z.strictObject({
|
|
|
410
515
|
export const TaskParty = z.strictObject({
|
|
411
516
|
id: AgentId,
|
|
412
517
|
handle: AgentHandle,
|
|
518
|
+
// What the agent runs in. The API always sends it. Optional so the web
|
|
519
|
+
// still reads an answer from an older API.
|
|
520
|
+
runtime: Runtime.optional(),
|
|
521
|
+
// The level of the agent's current version from the last scoring run,
|
|
522
|
+
// for the name line. Left out until the agent has been scored.
|
|
523
|
+
level: Level.optional(),
|
|
413
524
|
// True for an agent SealKeeper runs itself, the seed agent.
|
|
414
525
|
operatedBySealKeeper: z.boolean(),
|
|
415
526
|
});
|
|
@@ -417,8 +528,8 @@ export const TaskView = z.strictObject({
|
|
|
417
528
|
id: z.uuid(),
|
|
418
529
|
taskType: TaskType,
|
|
419
530
|
spec: z.record(z.string(), z.unknown()),
|
|
420
|
-
// The public spec, never the full schema.
|
|
421
|
-
verification:
|
|
531
|
+
// The public spec, never the full schema and never a hash digest.
|
|
532
|
+
verification: PublicVerificationSpec,
|
|
422
533
|
state: TaskState,
|
|
423
534
|
poster: TaskParty,
|
|
424
535
|
// Null while open, and after the claimant agent was deleted.
|
|
@@ -509,7 +620,7 @@ export const MeAgent = z.strictObject({
|
|
|
509
620
|
id: AgentId,
|
|
510
621
|
name: Name,
|
|
511
622
|
handle: AgentHandle,
|
|
512
|
-
operator:
|
|
623
|
+
operator: OperatorRef,
|
|
513
624
|
version: Version,
|
|
514
625
|
createdAt: Timestamp,
|
|
515
626
|
lastSeenAt: Timestamp.nullable(),
|
|
@@ -518,17 +629,39 @@ export const MeAgent = z.strictObject({
|
|
|
518
629
|
seedTasks: Count,
|
|
519
630
|
level: Level.optional(),
|
|
520
631
|
standing: Standing.optional(),
|
|
632
|
+
runtime: Runtime,
|
|
633
|
+
});
|
|
634
|
+
// The signed in operator. createdAt is when the operator first registered
|
|
635
|
+
// or signed in, shown as operator since on the account page.
|
|
636
|
+
// slugChangedAt is when the slug last changed, null until the first change.
|
|
637
|
+
// The next change is allowed SLUG_CHANGE_DAYS after it.
|
|
638
|
+
// slugNoticeDismissedAt is when the operator dismissed the one time slug
|
|
639
|
+
// notice on /me (DELETE /v1/me/slug-notice), null while it still shows.
|
|
640
|
+
export const MeOperator = z.strictObject({
|
|
641
|
+
login: Login,
|
|
642
|
+
slug: OperatorRef.shape.slug,
|
|
643
|
+
displayName: OperatorRef.shape.displayName,
|
|
644
|
+
slugChangedAt: Timestamp.nullable(),
|
|
645
|
+
slugNoticeDismissedAt: Timestamp.nullable(),
|
|
646
|
+
githubId: z.int().positive(),
|
|
647
|
+
createdAt: Timestamp,
|
|
521
648
|
});
|
|
522
|
-
// operator.createdAt is when the operator first registered or signed in,
|
|
523
|
-
// shown as operator since on the account page.
|
|
524
649
|
export const MeResponse = z.strictObject({
|
|
525
|
-
operator:
|
|
526
|
-
login: z.string().min(1).max(39),
|
|
527
|
-
githubId: z.int().positive(),
|
|
528
|
-
createdAt: Timestamp,
|
|
529
|
-
}),
|
|
650
|
+
operator: MeOperator,
|
|
530
651
|
agents: z.array(MeAgent),
|
|
531
652
|
});
|
|
653
|
+
// PATCH /v1/me, from the web's account page with the session cookie. A new
|
|
654
|
+
// slug, a new display name, or both. The shapes are checked here, reserved
|
|
655
|
+
// words, protected names and names in use by the API (409). The answer is
|
|
656
|
+
// MeOperator.
|
|
657
|
+
export const UpdateMeRequest = z
|
|
658
|
+
.strictObject({
|
|
659
|
+
slug: OperatorSlug.optional(),
|
|
660
|
+
displayName: OperatorDisplayName.optional(),
|
|
661
|
+
})
|
|
662
|
+
.refine((r) => r.slug !== undefined || r.displayName !== undefined, {
|
|
663
|
+
message: 'slug or displayName is required',
|
|
664
|
+
});
|
|
532
665
|
// DELETE /v1/me. The operator types their GitHub login to confirm, and it
|
|
533
666
|
// must equal the signed in login exactly.
|
|
534
667
|
export const DeleteMeRequest = z.strictObject({
|
|
@@ -554,8 +687,11 @@ export const LeaderboardEntry = z.strictObject({
|
|
|
554
687
|
agentId: AgentId,
|
|
555
688
|
name: Name,
|
|
556
689
|
handle: AgentHandle,
|
|
557
|
-
operator:
|
|
690
|
+
operator: OperatorRef,
|
|
558
691
|
version: Version,
|
|
692
|
+
// What the agent runs in. The API always sends it. Optional so the web
|
|
693
|
+
// still reads an answer from an older API.
|
|
694
|
+
runtime: Runtime.optional(),
|
|
559
695
|
value: z.number().min(0).max(1),
|
|
560
696
|
// Tasks this agent claimed that reached verified and count toward trust,
|
|
561
697
|
// across all versions. The same rule as AgentCounts.verifiedTasks.
|
|
@@ -655,8 +791,13 @@ const feedItemOf = (kind) => z.strictObject({
|
|
|
655
791
|
agentId: AgentId.nullable(),
|
|
656
792
|
// The agent the payload names, as it is now. Looked up when the item is
|
|
657
793
|
// read, so a renamed agent's old items link to where it lives today.
|
|
658
|
-
operator:
|
|
794
|
+
operator: OperatorRef,
|
|
659
795
|
handle: AgentHandle,
|
|
796
|
+
// What the agent runs in and the level of its current version, as they
|
|
797
|
+
// are now, for the row's name line. Optional so the web reads an older
|
|
798
|
+
// API's answer. level is left out until the agent has been scored.
|
|
799
|
+
runtime: Runtime.optional(),
|
|
800
|
+
level: Level.optional(),
|
|
660
801
|
payload: FeedPayloads[kind],
|
|
661
802
|
createdAt: Timestamp,
|
|
662
803
|
});
|
|
@@ -684,7 +825,7 @@ export const StatsResponse = z.strictObject({
|
|
|
684
825
|
verifiedTasks: z.int().min(0),
|
|
685
826
|
eventsLast24h: z.int().min(0),
|
|
686
827
|
});
|
|
687
|
-
// GET /v1/check/:
|
|
828
|
+
// GET /v1/check/:slug/:name. One call for a caller that is about to
|
|
688
829
|
// delegate work and wants a yes or no on the agent's track record. Query
|
|
689
830
|
// values arrive as text and are parsed exactly, so an empty or odd value is
|
|
690
831
|
// a 400 and never quietly becomes 0.
|
|
@@ -800,6 +941,70 @@ export const SeedRunResponse = z.strictObject({
|
|
|
800
941
|
open: z.int().min(0),
|
|
801
942
|
added: z.int().min(0),
|
|
802
943
|
});
|
|
944
|
+
// GET /internal/metrics (VOU-146). days is the number of UTC days the
|
|
945
|
+
// perDay rows cover, today included.
|
|
946
|
+
export const INTERNAL_METRICS_MAX_DAYS = 90;
|
|
947
|
+
export const InternalMetricsQuery = z.strictObject({
|
|
948
|
+
days: z.coerce
|
|
949
|
+
.number()
|
|
950
|
+
.int()
|
|
951
|
+
.min(1)
|
|
952
|
+
.max(INTERNAL_METRICS_MAX_DAYS)
|
|
953
|
+
.default(30),
|
|
954
|
+
});
|
|
955
|
+
// A count split by operator. sealkeeper is the operator that owns the seed
|
|
956
|
+
// agent (SEED_AGENT_ID), others is everyone else.
|
|
957
|
+
const OperatorSplit = z.strictObject({
|
|
958
|
+
sealkeeper: z.int().min(0),
|
|
959
|
+
others: z.int().min(0),
|
|
960
|
+
});
|
|
961
|
+
const MetricsDay = z.strictObject({
|
|
962
|
+
// YYYY-MM-DD, a UTC day.
|
|
963
|
+
day: z.string(),
|
|
964
|
+
agentsRegistered: OperatorSplit,
|
|
965
|
+
// Tasks verified that day by kind, the scoring kind rules. Excluded
|
|
966
|
+
// tasks are left out.
|
|
967
|
+
verifiedTasks: z.strictObject({
|
|
968
|
+
seed: z.int().min(0),
|
|
969
|
+
serverChecked: z.int().min(0),
|
|
970
|
+
confirmed: z.int().min(0),
|
|
971
|
+
}),
|
|
972
|
+
// Addressed tasks posted that day and addressed tasks verified that day.
|
|
973
|
+
addressedTasks: z.strictObject({
|
|
974
|
+
posted: z.int().min(0),
|
|
975
|
+
verified: z.int().min(0),
|
|
976
|
+
}),
|
|
977
|
+
// Distinct poster and claimant operator pairs behind the day's verified
|
|
978
|
+
// server checked and confirmed tasks.
|
|
979
|
+
operatorPairs: z.int().min(0),
|
|
980
|
+
// Tasks posted that day with origin routine, and outcome reports last
|
|
981
|
+
// written that day with origin routine.
|
|
982
|
+
routine: z.strictObject({
|
|
983
|
+
tasksPosted: z.int().min(0),
|
|
984
|
+
outcomes: z.int().min(0),
|
|
985
|
+
}),
|
|
986
|
+
});
|
|
987
|
+
export const InternalMetricsResponse = z.strictObject({
|
|
988
|
+
days: z.int().min(1),
|
|
989
|
+
// False when SEED_AGENT_ID is unset or not registered, and then every
|
|
990
|
+
// count is in others.
|
|
991
|
+
seedOperatorKnown: z.boolean(),
|
|
992
|
+
snapshot: z.strictObject({
|
|
993
|
+
agents: OperatorSplit,
|
|
994
|
+
// Each agent at the level of its current version's standing row, none
|
|
995
|
+
// when it has no row yet.
|
|
996
|
+
levels: z.strictObject({
|
|
997
|
+
none: OperatorSplit,
|
|
998
|
+
bronze: OperatorSplit,
|
|
999
|
+
silver: OperatorSplit,
|
|
1000
|
+
gold: OperatorSplit,
|
|
1001
|
+
}),
|
|
1002
|
+
}),
|
|
1003
|
+
// Oldest first, one row per UTC day, today last.
|
|
1004
|
+
perDay: z.array(MetricsDay),
|
|
1005
|
+
// Roadmap numbers the current tables cannot count, each with why.
|
|
1006
|
+
notMeasured: z.array(z.strictObject({ metric: z.string(), why: z.string() })),
|
|
1007
|
+
});
|
|
803
1008
|
export const ErrorIssue = z.strictObject({
|
|
804
1009
|
path: z.array(z.union([z.string(), z.number()])),
|
|
805
1010
|
code: z.string(),
|
package/dist/badge.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const BADGE_CSP = "default-src 'none'; style-src 'unsafe-inline'; sandbox";
|
package/dist/badge.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// The Content-Security-Policy on every badge and avatar answer, from the web
|
|
2
|
+
// route, the web's static headers in next.config.ts and the API badge proxy.
|
|
3
|
+
// The SVG carries no script and loads nothing. sandbox stops a script
|
|
4
|
+
// running even if one got in, default-src 'none' stops any fetch, and inline
|
|
5
|
+
// style is the one thing let through, so a badge opened on its own still
|
|
6
|
+
// draws. Defined once here so the three cannot drift.
|
|
7
|
+
export const BADGE_CSP = "default-src 'none'; style-src 'unsafe-inline'; sandbox";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const WEB_VISITOR_HEADER = "X-SealKeeper-Client";
|
|
3
|
+
export declare function ipKey(addr: string): string;
|
|
4
|
+
export declare function lastForwardedFor(xff: string | null | undefined): string | null;
|
|
5
|
+
export declare const ClientAddress: z.ZodString;
|
|
6
|
+
export declare function visitorAddress(xff: string | null | undefined): string | null;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
// How the API and the web name a client for their rate limits (VOU-211).
|
|
3
|
+
// Both run on Cloud Run, which appends the connecting address to
|
|
4
|
+
// X-Forwarded-For. Earlier entries are whatever the client sent, so only
|
|
5
|
+
// the last one counts. Defined once here so the web's limits and the API's
|
|
6
|
+
// quota key a visitor the same way.
|
|
7
|
+
// The header the web's server sends on an API call it makes for a visitor,
|
|
8
|
+
// with that visitor's address as visitorAddress gives it. The API reads it
|
|
9
|
+
// only next to a matching WEB_SHARED_SECRET, so a browser gains nothing by
|
|
10
|
+
// sending it. The header is not in the API's CORS allow list either.
|
|
11
|
+
export const WEB_VISITOR_HEADER = 'X-SealKeeper-Client';
|
|
12
|
+
// An address as a rate limit key. An IPv6 address comes back as its /64
|
|
13
|
+
// prefix, since one host usually holds a whole /64 and could otherwise take
|
|
14
|
+
// a fresh bucket, and a fresh feed stream slot, per address. IPv4 comes back
|
|
15
|
+
// unchanged, and so does anything that is not an IPv6 address.
|
|
16
|
+
export function ipKey(addr) {
|
|
17
|
+
if (!addr.includes(':'))
|
|
18
|
+
return addr;
|
|
19
|
+
const bare = addr.replace(/^\[|\](:\d+)?$/g, '').split('%')[0] ?? '';
|
|
20
|
+
// An IPv4 mapped address such as ::ffff:1.2.3.4 is that IPv4 address.
|
|
21
|
+
const mapped = /^::ffff:(\d+\.\d+\.\d+\.\d+)$/i.exec(bare);
|
|
22
|
+
if (mapped?.[1])
|
|
23
|
+
return mapped[1];
|
|
24
|
+
const groups = expandIpv6(bare);
|
|
25
|
+
if (!groups)
|
|
26
|
+
return addr;
|
|
27
|
+
return `${groups.slice(0, 4).join(':')}::/64`;
|
|
28
|
+
}
|
|
29
|
+
// The eight hextets of an IPv6 address, lower case without leading zeros,
|
|
30
|
+
// or null when it is not one.
|
|
31
|
+
function expandIpv6(addr) {
|
|
32
|
+
const halves = addr.toLowerCase().split('::');
|
|
33
|
+
if (halves.length > 2)
|
|
34
|
+
return null;
|
|
35
|
+
const part = (s) => (s ? s.split(':') : []);
|
|
36
|
+
const head = part(halves[0]);
|
|
37
|
+
const tail = part(halves[1]);
|
|
38
|
+
// A trailing dotted IPv4 part counts as two hextets.
|
|
39
|
+
const last = (halves.length === 2 ? tail : head).at(-1);
|
|
40
|
+
if (last?.includes('.')) {
|
|
41
|
+
const o = last.split('.').map(Number);
|
|
42
|
+
if (o.length !== 4 || o.some((n) => !(n >= 0 && n <= 255)))
|
|
43
|
+
return null;
|
|
44
|
+
const [a = 0, b = 0, d = 0, e = 0] = o;
|
|
45
|
+
const pair = [((a << 8) | b).toString(16), ((d << 8) | e).toString(16)];
|
|
46
|
+
(halves.length === 2 ? tail : head).splice(-1, 1, ...pair);
|
|
47
|
+
}
|
|
48
|
+
const fill = 8 - head.length - tail.length;
|
|
49
|
+
if (halves.length === 1 ? fill !== 0 : fill < 1)
|
|
50
|
+
return null;
|
|
51
|
+
const all = [...head, ...Array(fill).fill('0'), ...tail];
|
|
52
|
+
if (!all.every((h) => /^[0-9a-f]{1,4}$/.test(h)))
|
|
53
|
+
return null;
|
|
54
|
+
return all.map((h) => Number.parseInt(h, 16).toString(16));
|
|
55
|
+
}
|
|
56
|
+
// The last X-Forwarded-For entry, trimmed, or null when there is none.
|
|
57
|
+
export function lastForwardedFor(xff) {
|
|
58
|
+
return xff?.split(',').at(-1)?.trim() || null;
|
|
59
|
+
}
|
|
60
|
+
const OCTET = '(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)';
|
|
61
|
+
const HEXTET = '(?:0|[1-9a-f][0-9a-f]{0,3})';
|
|
62
|
+
const IPV4 = new RegExp(`^${OCTET}(?:\\.${OCTET}){3}$`);
|
|
63
|
+
const IPV6_64 = new RegExp(`^${HEXTET}(?::${HEXTET}){3}::/64$`);
|
|
64
|
+
// A real address in the form ipKey gives it, dotted IPv4 or an IPv6 /64
|
|
65
|
+
// prefix. The API checks WEB_VISITOR_HEADER against it and ignores the
|
|
66
|
+
// header when it does not match.
|
|
67
|
+
export const ClientAddress = z
|
|
68
|
+
.string()
|
|
69
|
+
.max(64)
|
|
70
|
+
.refine((v) => IPV4.test(v) || IPV6_64.test(v), 'Expected an IP address');
|
|
71
|
+
// The visitor's address for a rate limit, from the X-Forwarded-For header
|
|
72
|
+
// of a request that came through Cloud Run. Null when there is no entry or
|
|
73
|
+
// the last one is not an IP address, and the caller then has no visitor to
|
|
74
|
+
// name.
|
|
75
|
+
export function visitorAddress(xff) {
|
|
76
|
+
const last = lastForwardedFor(xff);
|
|
77
|
+
if (!last)
|
|
78
|
+
return null;
|
|
79
|
+
const parsed = ClientAddress.safeParse(ipKey(last));
|
|
80
|
+
return parsed.success ? parsed.data : null;
|
|
81
|
+
}
|