@sealkeeper/schema 0.4.6 → 0.4.8

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 (90) hide show
  1. package/README.md +2 -2
  2. package/dist/api.d.ts +787 -35
  3. package/dist/api.js +357 -44
  4. package/dist/conformance.d.ts +4 -0
  5. package/dist/conformance.js +7 -0
  6. package/dist/credential.d.ts +196 -27
  7. package/dist/credential.js +128 -80
  8. package/dist/dimensions.d.ts +16 -3
  9. package/dist/dimensions.js +48 -2
  10. package/dist/events.d.ts +1 -0
  11. package/dist/events.js +13 -1
  12. package/dist/fingerprint-conformance.d.ts +17 -0
  13. package/dist/fingerprint-conformance.js +83 -0
  14. package/dist/fingerprint.d.ts +132 -0
  15. package/dist/fingerprint.js +166 -0
  16. package/dist/goal.d.ts +16 -1
  17. package/dist/goal.js +29 -6
  18. package/dist/handshake-conformance.d.ts +18 -0
  19. package/dist/handshake-conformance.js +276 -0
  20. package/dist/handshake.d.ts +66 -0
  21. package/dist/handshake.js +187 -0
  22. package/dist/hidden.d.ts +1 -0
  23. package/dist/hidden.js +5 -0
  24. package/dist/index.d.ts +4 -1
  25. package/dist/index.js +4 -1
  26. package/dist/moderation.js +1 -4
  27. package/dist/policy.d.ts +1 -1
  28. package/dist/policy.js +1 -1
  29. package/dist/seal-conformance.js +148 -4
  30. package/dist/seal-verify.d.ts +21 -0
  31. package/dist/seal-verify.js +63 -0
  32. package/dist/standing.d.ts +61 -0
  33. package/dist/standing.js +132 -4
  34. package/dist/task-templates.d.ts +46 -0
  35. package/dist/task-templates.js +501 -0
  36. package/dist/tasks.d.ts +43 -0
  37. package/dist/tasks.js +103 -0
  38. package/dist/template-conformance.d.ts +10 -0
  39. package/dist/template-conformance.js +298 -0
  40. package/dist/top-dimensions.d.ts +3 -3
  41. package/dist/top-dimensions.js +12 -6
  42. package/package.json +4 -8
  43. package/dist/db/agent-milestones.d.ts +0 -75
  44. package/dist/db/agent-milestones.js +0 -16
  45. package/dist/db/agent-renames.d.ts +0 -126
  46. package/dist/db/agent-renames.js +0 -20
  47. package/dist/db/agents.d.ts +0 -351
  48. package/dist/db/agents.js +0 -99
  49. package/dist/db/client.d.ts +0 -2561
  50. package/dist/db/client.js +0 -61
  51. package/dist/db/credentials.d.ts +0 -143
  52. package/dist/db/credentials.js +0 -16
  53. package/dist/db/deleted-operators.d.ts +0 -75
  54. package/dist/db/deleted-operators.js +0 -13
  55. package/dist/db/events.d.ts +0 -160
  56. package/dist/db/events.js +0 -41
  57. package/dist/db/feed-items.d.ts +0 -109
  58. package/dist/db/feed-items.js +0 -21
  59. package/dist/db/index.d.ts +0 -76
  60. package/dist/db/index.js +0 -28
  61. package/dist/db/migrate.d.ts +0 -1
  62. package/dist/db/migrate.js +0 -34
  63. package/dist/db/migrator.d.ts +0 -6
  64. package/dist/db/migrator.js +0 -36
  65. package/dist/db/operator-identities.d.ts +0 -211
  66. package/dist/db/operator-identities.js +0 -49
  67. package/dist/db/operator-level-grants.d.ts +0 -109
  68. package/dist/db/operator-level-grants.js +0 -30
  69. package/dist/db/operator-slugs.d.ts +0 -92
  70. package/dist/db/operator-slugs.js +0 -25
  71. package/dist/db/operators.d.ts +0 -228
  72. package/dist/db/operators.js +0 -39
  73. package/dist/db/quota-counters.d.ts +0 -109
  74. package/dist/db/quota-counters.js +0 -17
  75. package/dist/db/ratings.d.ts +0 -160
  76. package/dist/db/ratings.js +0 -27
  77. package/dist/db/scores.d.ts +0 -160
  78. package/dist/db/scores.js +0 -15
  79. package/dist/db/standing.d.ts +0 -264
  80. package/dist/db/standing.js +0 -34
  81. package/dist/db/task-claim-failures.d.ts +0 -143
  82. package/dist/db/task-claim-failures.js +0 -37
  83. package/dist/db/task-outcomes.d.ts +0 -145
  84. package/dist/db/task-outcomes.js +0 -25
  85. package/dist/db/tasks.d.ts +0 -381
  86. package/dist/db/tasks.js +0 -106
  87. package/dist/db/timestamps.d.ts +0 -4
  88. package/dist/db/timestamps.js +0 -24
  89. package/dist/db/url.d.ts +0 -12
  90. package/dist/db/url.js +0 -25
@@ -4,8 +4,8 @@ import { base64urlEncode, utf8Encode } from './base64url.js';
4
4
  import { CREDENTIAL_ISSUER, LEGACY_ISSUER_UNTIL, LEGACY_ISSUERS, SEAL_CLOCK_SKEW_SECONDS, SEAL_MAX_TTL_SECONDS, SEAL_VERSION, } from './credential.js';
5
5
  import { sign } from './envelope.js';
6
6
  const KID = 'vouched-conformance-1';
7
- // 1 October 2026, after LEGACY_UNTIL, so no case leans on the legacy shape,
8
- // and before LEGACY_ISSUER_UNTIL, so a legacy issuer is still accepted.
7
+ // 1 October 2026, before LEGACY_ISSUER_UNTIL, so a legacy issuer is still
8
+ // accepted.
9
9
  const NOW = 1_790_812_800;
10
10
  const LEGACY_ISSUER = LEGACY_ISSUERS[0];
11
11
  const HOUR = 3600;
@@ -18,6 +18,21 @@ const COUNTED = {
18
18
  server_checked_tasks: 0,
19
19
  confirmed_tasks: 0,
20
20
  };
21
+ // The three posted counts of a version 3 SEAL, and a fingerprint hash,
22
+ // SHA-256 of some bytes in base64url, 43 characters.
23
+ const POSTED = {
24
+ posted_tasks: 4,
25
+ posted_distinct_operators: 2,
26
+ posted_confirmed_tasks: 1,
27
+ };
28
+ const FINGERPRINT_HASH = 'BwjgHg-Z9sC4K05UnygMCN_CD7onYm12GfK83VEIpTQ';
29
+ // The counted evidence of a version 3 SEAL, COUNTED plus the counted twins
30
+ // of posted_tasks and posted_confirmed_tasks, each at most its raw count.
31
+ const COUNTED_V3 = {
32
+ ...COUNTED,
33
+ posted_tasks: 3,
34
+ posted_confirmed_tasks: 1,
35
+ };
21
36
  const header = (kid) => base64urlEncode(utf8Encode(JSON.stringify({ alg: 'EdDSA', kid })));
22
37
  // Signs payload bytes as they are, so a case can carry a payload that is not
23
38
  // JSON. sign() in envelope.ts always writes JSON.
@@ -54,6 +69,18 @@ export async function sealConformanceCases() {
54
69
  last_active: NOW - HOUR,
55
70
  dormant_days: 0,
56
71
  };
72
+ // Version 3 is version 2 without version, with the posted counts,
73
+ // fingerprint and state.
74
+ const { version: _dropped, ...v2Fields } = base;
75
+ const v3 = {
76
+ ...v2Fields,
77
+ ver: 3,
78
+ counts: { ...base.counts, ...POSTED },
79
+ counted: COUNTED_V3,
80
+ fingerprint: { hash: FINGERPRINT_HASH, at: NOW - 2 * HOUR },
81
+ state: 'matches',
82
+ };
83
+ const { state: _state, ...v3NoState } = v3;
57
84
  const signed = (payload) => sign(payload, pair.privateKey, KID);
58
85
  const valid = await signed(base);
59
86
  const [h, , s] = valid.split('.');
@@ -108,14 +135,104 @@ export async function sealConformanceCases() {
108
135
  }),
109
136
  'malformed',
110
137
  ],
111
- ['an unknown ver', signed({ ...base, ver: 3 }), 'unsupported_version'],
138
+ [
139
+ 'version 3 with posted counts, fingerprint and state',
140
+ signed(v3),
141
+ 'valid',
142
+ ],
143
+ [
144
+ 'version 3 with no fingerprint yet and a reserved state',
145
+ signed({ ...v3, fingerprint: null, state: 'provisional' }),
146
+ 'valid',
147
+ ],
148
+ [
149
+ 'version 3 with a field it does not define',
150
+ signed({ ...v3, extra: true }),
151
+ 'malformed',
152
+ ],
153
+ ['version 3 without state', signed(v3NoState), 'malformed'],
154
+ [
155
+ 'version 3 with a state outside the set',
156
+ signed({ ...v3, state: 'unknown' }),
157
+ 'malformed',
158
+ ],
159
+ [
160
+ 'version 3 with a fingerprint hash of the wrong length',
161
+ signed({
162
+ ...v3,
163
+ fingerprint: { hash: FINGERPRINT_HASH.slice(1), at: NOW - HOUR },
164
+ }),
165
+ 'malformed',
166
+ ],
167
+ [
168
+ 'version 3 with the dropped version field',
169
+ signed({ ...v3, version: '1.0.0' }),
170
+ 'malformed',
171
+ ],
172
+ [
173
+ 'version 3 without the posted counts',
174
+ signed({ ...v3, counts: base.counts }),
175
+ 'malformed',
176
+ ],
177
+ [
178
+ 'version 3 with a counted posted value above its count',
179
+ signed({
180
+ ...v3,
181
+ counted: { ...COUNTED_V3, posted_tasks: 5 },
182
+ }),
183
+ 'malformed',
184
+ ],
185
+ [
186
+ 'version 3 with the version 2 counted evidence',
187
+ signed({ ...v3, counted: COUNTED }),
188
+ 'malformed',
189
+ ],
190
+ [
191
+ 'version 3 with a fingerprint after iat',
192
+ signed({
193
+ ...v3,
194
+ fingerprint: { hash: FINGERPRINT_HASH, at: v3.iat + 1 },
195
+ }),
196
+ 'malformed',
197
+ ],
198
+ [
199
+ 'version 3 with a fingerprint hash whose last character no SHA-256 has',
200
+ signed({
201
+ ...v3,
202
+ fingerprint: {
203
+ hash: `${FINGERPRINT_HASH.slice(0, 42)}R`,
204
+ at: NOW - HOUR,
205
+ },
206
+ }),
207
+ 'malformed',
208
+ ],
209
+ [
210
+ 'version 3 with more posted confirmed tasks than posted tasks',
211
+ signed({
212
+ ...v3,
213
+ counts: { ...v3.counts, posted_confirmed_tasks: 5 },
214
+ }),
215
+ 'malformed',
216
+ ],
217
+ [
218
+ 'version 2 with a fingerprint and state',
219
+ signed({
220
+ ...base,
221
+ ver: 2,
222
+ counted: COUNTED,
223
+ fingerprint: v3.fingerprint,
224
+ state: 'matches',
225
+ }),
226
+ 'malformed',
227
+ ],
228
+ ['an unknown ver', signed({ ...base, ver: 4 }), 'unsupported_version'],
112
229
  [
113
230
  'no ver after the legacy cutoff',
114
231
  signed({ ...base, ver: undefined }),
115
232
  'unsupported_version',
116
233
  ],
117
234
  // Intended. Platinum is reserved on LADDER and is not a Level under ver
118
- // 1 or 2 (standard, section 9). It becomes valid only with a ver bump.
235
+ // 1, 2 or 3 (standard, section 9). It becomes valid only with a ver bump.
119
236
  [
120
237
  'the wrong shape, a level not in the standard',
121
238
  signed({ ...base, level: 'platinum' }),
@@ -126,6 +243,33 @@ export async function sealConformanceCases() {
126
243
  signed({ ...base, scores: { reliability: 1.2 } }),
127
244
  'malformed',
128
245
  ],
246
+ // Competence is keyed by category from RT-3. A SEAL issued before it
247
+ // carries competence by task type, and a verifier accepts both
248
+ // (standard, section 2).
249
+ [
250
+ 'competence by category',
251
+ signed({
252
+ ...base,
253
+ scores: { ...base.scores, 'competence:data': 0.8 },
254
+ }),
255
+ 'valid',
256
+ ],
257
+ [
258
+ 'competence by task type, issued before categories',
259
+ signed({
260
+ ...base,
261
+ scores: { ...base.scores, 'competence:csv_normalise': 0.8 },
262
+ }),
263
+ 'valid',
264
+ ],
265
+ [
266
+ 'a competence key that is not a task type',
267
+ signed({
268
+ ...base,
269
+ scores: { ...base.scores, 'competence:Data Work': 0.8 },
270
+ }),
271
+ 'malformed',
272
+ ],
129
273
  [
130
274
  'a negative count',
131
275
  signed({
@@ -0,0 +1,21 @@
1
+ import type { SealBrokenReason } from './api.js';
2
+ import { type SealPayload } from './credential.js';
3
+ export type SealKey = {
4
+ kid: string;
5
+ x: string;
6
+ };
7
+ export type SealVerified = {
8
+ ok: true;
9
+ kid: string;
10
+ payload: SealPayload;
11
+ expiresIn: number;
12
+ };
13
+ export type SealProblem = {
14
+ ok: false;
15
+ reason: SealBrokenReason;
16
+ kid: string | null;
17
+ signed?: unknown;
18
+ payload?: SealPayload;
19
+ };
20
+ export type SealVerification = SealVerified | SealProblem;
21
+ export declare function verifySeal(keys: readonly SealKey[], compact: string, nowSeconds: number): Promise<SealVerification>;
@@ -0,0 +1,63 @@
1
+ import { base64urlDecode } from './base64url.js';
2
+ import { parseSealPayload, sealIatProblem, } from './credential.js';
3
+ import { decodeHeader, EnvelopeError, verify } from './envelope.js';
4
+ /*
5
+ * Verifies a SEAL against the issuer's published keys, the steps of the
6
+ * standard (section 3) in order. The API's POST /v1/seal/verify, the web's
7
+ * verify tool and badge and `sealkeeper seal verify` all call this, and the
8
+ * SEAL conformance cases hold it to the standard's answers.
9
+ *
10
+ * 1. The header, which names the kid. Surrounding whitespace is dropped
11
+ * first, as a pasted SEAL often has it.
12
+ * 2. The key the kid names, among keys.
13
+ * 3. The signature over the exact header.payload bytes, and only then the
14
+ * payload as JSON. A signature that checks out over a payload that is
15
+ * not JSON is malformed, not bad_signature.
16
+ * 4. The issuer, then ver, then the shape for that ver, through
17
+ * parseSealPayload. So a SEAL from another issuer is wrong_issuer
18
+ * whatever its ver or shape.
19
+ * 5. exp, then iat, as the standard reads "Check exp is in the future and
20
+ * iat is not". An iat more than SEAL_CLOCK_SKEW_SECONDS ahead is
21
+ * not_yet_valid.
22
+ *
23
+ * nowSeconds is the verifier's clock in Unix seconds, passed in so tests
24
+ * can pin it.
25
+ */
26
+ export async function verifySeal(keys, compact, nowSeconds) {
27
+ const jws = compact.trim();
28
+ let kid;
29
+ try {
30
+ kid = decodeHeader(jws).kid;
31
+ }
32
+ catch {
33
+ return { ok: false, reason: 'malformed', kid: null };
34
+ }
35
+ const key = keys.find((k) => k.kid === kid);
36
+ if (!key)
37
+ return { ok: false, reason: 'unknown_kid', kid };
38
+ let signed;
39
+ try {
40
+ signed = (await verify(jws, base64urlDecode(key.x))).payload;
41
+ }
42
+ catch (error) {
43
+ return {
44
+ ok: false,
45
+ reason: error instanceof EnvelopeError && error.code === 'bad_signature'
46
+ ? 'bad_signature'
47
+ : 'malformed',
48
+ kid,
49
+ };
50
+ }
51
+ const parsed = parseSealPayload(signed, nowSeconds);
52
+ if (!parsed.ok)
53
+ return { ok: false, reason: parsed.reason, kid, signed };
54
+ const payload = parsed.payload;
55
+ const expiresIn = Math.floor(payload.exp - nowSeconds);
56
+ if (expiresIn <= 0) {
57
+ return { ok: false, reason: 'expired', kid, signed, payload };
58
+ }
59
+ const early = sealIatProblem(payload.iat, nowSeconds);
60
+ if (early)
61
+ return { ok: false, reason: early, kid, signed, payload };
62
+ return { ok: true, kid, payload, expiresIn };
63
+ }
@@ -37,6 +37,20 @@ export declare const StandingCounts: z.ZodObject<{
37
37
  safety_incidents_90d: z.ZodInt;
38
38
  }, z.core.$strict>;
39
39
  export type StandingCounts = z.infer<typeof StandingCounts>;
40
+ export declare const StandingCountsV3: z.ZodObject<{
41
+ events: z.ZodInt;
42
+ history_days: z.ZodInt;
43
+ verified_tasks: z.ZodInt;
44
+ seed_tasks: z.ZodInt;
45
+ server_checked_tasks: z.ZodInt;
46
+ confirmed_tasks: z.ZodInt;
47
+ distinct_operators: z.ZodInt;
48
+ safety_incidents_90d: z.ZodInt;
49
+ posted_tasks: z.ZodInt;
50
+ posted_distinct_operators: z.ZodInt;
51
+ posted_confirmed_tasks: z.ZodInt;
52
+ }, z.core.$strict>;
53
+ export type StandingCountsV3 = z.infer<typeof StandingCountsV3>;
40
54
  export declare const CountedCounts: z.ZodObject<{
41
55
  verified_tasks: z.ZodInt;
42
56
  seed_tasks: z.ZodInt;
@@ -44,6 +58,38 @@ export declare const CountedCounts: z.ZodObject<{
44
58
  confirmed_tasks: z.ZodInt;
45
59
  }, z.core.$strict>;
46
60
  export type CountedCounts = z.infer<typeof CountedCounts>;
61
+ export declare const CountedCountsV3: z.ZodObject<{
62
+ verified_tasks: z.ZodInt;
63
+ seed_tasks: z.ZodInt;
64
+ server_checked_tasks: z.ZodInt;
65
+ confirmed_tasks: z.ZodInt;
66
+ posted_tasks: z.ZodInt;
67
+ posted_confirmed_tasks: z.ZodInt;
68
+ }, z.core.$strict>;
69
+ export type CountedCountsV3 = z.infer<typeof CountedCountsV3>;
70
+ export declare const AnswerCounts: z.ZodObject<{
71
+ events: z.ZodInt;
72
+ history_days: z.ZodInt;
73
+ verified_tasks: z.ZodInt;
74
+ seed_tasks: z.ZodInt;
75
+ server_checked_tasks: z.ZodInt;
76
+ confirmed_tasks: z.ZodInt;
77
+ distinct_operators: z.ZodInt;
78
+ safety_incidents_90d: z.ZodInt;
79
+ posted_tasks: z.ZodOptional<z.ZodInt>;
80
+ posted_distinct_operators: z.ZodOptional<z.ZodInt>;
81
+ posted_confirmed_tasks: z.ZodOptional<z.ZodInt>;
82
+ }, z.core.$strict>;
83
+ export type AnswerCounts = z.infer<typeof AnswerCounts>;
84
+ export declare const AnswerCounted: z.ZodObject<{
85
+ verified_tasks: z.ZodInt;
86
+ seed_tasks: z.ZodInt;
87
+ server_checked_tasks: z.ZodInt;
88
+ confirmed_tasks: z.ZodInt;
89
+ posted_tasks: z.ZodOptional<z.ZodInt>;
90
+ posted_confirmed_tasks: z.ZodOptional<z.ZodInt>;
91
+ }, z.core.$strict>;
92
+ export type AnswerCounted = z.infer<typeof AnswerCounted>;
47
93
  export declare const COUNTED_EVIDENCE: {
48
94
  readonly dailyCeiling: 20;
49
95
  readonly diminishingK: 25;
@@ -61,6 +107,9 @@ export declare const LEVEL_THRESHOLDS: {
61
107
  readonly historyDays: 3;
62
108
  readonly reliability: 0.8;
63
109
  readonly incidents90d: 0;
110
+ readonly postedTasks: 5;
111
+ readonly postedOperators: 1;
112
+ readonly postedConfirmedTasks: 0;
64
113
  };
65
114
  readonly silver: {
66
115
  readonly verifiedTasks: 200;
@@ -70,6 +119,9 @@ export declare const LEVEL_THRESHOLDS: {
70
119
  readonly incidents90d: 0;
71
120
  readonly provenance: 0.8;
72
121
  readonly modelDeclared: true;
122
+ readonly postedTasks: 40;
123
+ readonly postedOperators: 5;
124
+ readonly postedConfirmedTasks: 10;
73
125
  };
74
126
  readonly gold: {
75
127
  readonly verifiedTasks: 200;
@@ -81,6 +133,9 @@ export declare const LEVEL_THRESHOLDS: {
81
133
  readonly safety: 0.95;
82
134
  readonly cleanDays: 180;
83
135
  readonly operatorVerified: true;
136
+ readonly postedTasks: 40;
137
+ readonly postedOperators: 5;
138
+ readonly postedConfirmedTasks: 25;
84
139
  };
85
140
  };
86
141
  export declare const OPERATOR_SILVER_CAP: {
@@ -99,16 +154,22 @@ export declare const Standing: z.ZodObject<{
99
154
  confirmed_tasks: z.ZodInt;
100
155
  distinct_operators: z.ZodInt;
101
156
  safety_incidents_90d: z.ZodInt;
157
+ posted_tasks: z.ZodOptional<z.ZodInt>;
158
+ posted_distinct_operators: z.ZodOptional<z.ZodInt>;
159
+ posted_confirmed_tasks: z.ZodOptional<z.ZodInt>;
102
160
  }, z.core.$strict>;
103
161
  counted: z.ZodObject<{
104
162
  verified_tasks: z.ZodInt;
105
163
  seed_tasks: z.ZodInt;
106
164
  server_checked_tasks: z.ZodInt;
107
165
  confirmed_tasks: z.ZodInt;
166
+ posted_tasks: z.ZodOptional<z.ZodInt>;
167
+ posted_confirmed_tasks: z.ZodOptional<z.ZodInt>;
108
168
  }, z.core.$strict>;
109
169
  history_days: z.ZodInt;
110
170
  last_active: z.ZodNullable<z.ZodISODateTime>;
111
171
  dormant_days: z.ZodNullable<z.ZodInt>;
112
172
  quiet: z.ZodBoolean;
173
+ held: z.ZodOptional<z.ZodBoolean>;
113
174
  }, z.core.$strict>;
114
175
  export type Standing = z.infer<typeof Standing>;
package/dist/standing.js CHANGED
@@ -64,6 +64,50 @@ export const StandingCounts = z
64
64
  message: 'verified_tasks must equal seed_tasks + server_checked_tasks + confirmed_tasks',
65
65
  path: ['verified_tasks'],
66
66
  });
67
+ // The counts of SEAL version 3. The eight of StandingCounts with the same
68
+ // meaning, plus three posted counts, raw facts over the same window for the
69
+ // tasks this agent posted. StandingCounts stays the shape of versions 1 and
70
+ // 2 unchanged. The rules behind the posted counts are the posted evidence
71
+ // of the standard's section 4 (POST-1, POST-3), which every level reads,
72
+ // and nothing issues version 3 yet.
73
+ //
74
+ // posted_tasks tasks the agent posted that another
75
+ // operator's agent completed, the server
76
+ // checked ones plus the confirmed ones, the
77
+ // sum of both kinds
78
+ // posted_distinct_operators operators other than the agent's own whose
79
+ // agents completed those tasks
80
+ // posted_confirmed_tasks the confirmed ones among posted_tasks
81
+ //
82
+ // posted_confirmed_tasks is at most posted_tasks, and verified_tasks is the
83
+ // sum of the three kinds as in StandingCounts. posted_distinct_operators has
84
+ // no bound against posted_tasks. An addressed task that counts adds its
85
+ // operator in full while its weight can round the task count down to 0,
86
+ // the same shape distinct_operators has on the taking side. The posted
87
+ // rules live here and nowhere else.
88
+ export const StandingCountsV3 = z
89
+ .strictObject({
90
+ events: Count,
91
+ history_days: Count,
92
+ verified_tasks: Count,
93
+ seed_tasks: Count,
94
+ server_checked_tasks: Count,
95
+ confirmed_tasks: Count,
96
+ distinct_operators: Count,
97
+ safety_incidents_90d: Count,
98
+ posted_tasks: Count,
99
+ posted_distinct_operators: Count,
100
+ posted_confirmed_tasks: Count,
101
+ })
102
+ .refine((c) => c.verified_tasks ===
103
+ c.seed_tasks + c.server_checked_tasks + c.confirmed_tasks, {
104
+ message: 'verified_tasks must equal seed_tasks + server_checked_tasks + confirmed_tasks',
105
+ path: ['verified_tasks'],
106
+ })
107
+ .refine((c) => c.posted_confirmed_tasks <= c.posted_tasks, {
108
+ message: 'posted_confirmed_tasks must not exceed posted_tasks',
109
+ path: ['posted_confirmed_tasks'],
110
+ });
67
111
  // The counted evidence (VOU-139), what the levels read. The same verified
68
112
  // tasks as the counts after the daily ceiling (at most 20 per agent per UTC
69
113
  // day count, the most valuable first) and diminishing returns (within a
@@ -88,6 +132,66 @@ export const CountedCounts = z
88
132
  message: 'verified_tasks must equal seed_tasks + server_checked_tasks + confirmed_tasks',
89
133
  path: ['verified_tasks'],
90
134
  });
135
+ // The counted evidence of SEAL version 3. CountedCounts plus the counted
136
+ // twins of two posted counts, what the posting thresholds read.
137
+ // posted_distinct_operators has no twin, since each operator already counts
138
+ // once. CountedCounts stays the shape of version 2 unchanged. Each value is
139
+ // at most its raw count in StandingCountsV3, which the SEAL checks.
140
+ export const CountedCountsV3 = z
141
+ .strictObject({
142
+ verified_tasks: Count,
143
+ seed_tasks: Count,
144
+ server_checked_tasks: Count,
145
+ confirmed_tasks: Count,
146
+ posted_tasks: Count,
147
+ posted_confirmed_tasks: Count,
148
+ })
149
+ .refine((c) => c.verified_tasks ===
150
+ c.seed_tasks + c.server_checked_tasks + c.confirmed_tasks, {
151
+ message: 'verified_tasks must equal seed_tasks + server_checked_tasks + confirmed_tasks',
152
+ path: ['verified_tasks'],
153
+ });
154
+ // The counts on the agent answers (POST-3). The eight of StandingCounts
155
+ // with the same meaning, plus the three posted counts of StandingCountsV3,
156
+ // which every level reads while a version 1 SEAL cannot carry them. The
157
+ // posted keys are optional, so an answer from an API before them still
158
+ // parses. StandingCounts stays the SEAL version 1 shape.
159
+ export const AnswerCounts = z
160
+ .strictObject({
161
+ events: Count,
162
+ history_days: Count,
163
+ verified_tasks: Count,
164
+ seed_tasks: Count,
165
+ server_checked_tasks: Count,
166
+ confirmed_tasks: Count,
167
+ distinct_operators: Count,
168
+ safety_incidents_90d: Count,
169
+ posted_tasks: Count.optional(),
170
+ posted_distinct_operators: Count.optional(),
171
+ posted_confirmed_tasks: Count.optional(),
172
+ })
173
+ .refine((c) => c.verified_tasks ===
174
+ c.seed_tasks + c.server_checked_tasks + c.confirmed_tasks, {
175
+ message: 'verified_tasks must equal seed_tasks + server_checked_tasks + confirmed_tasks',
176
+ path: ['verified_tasks'],
177
+ });
178
+ // The counted evidence on the agent answers (POST-3). The four of
179
+ // CountedCounts plus the counted posted_tasks and posted_confirmed_tasks of
180
+ // CountedCountsV3, optional like the posted counts of AnswerCounts.
181
+ export const AnswerCounted = z
182
+ .strictObject({
183
+ verified_tasks: Count,
184
+ seed_tasks: Count,
185
+ server_checked_tasks: Count,
186
+ confirmed_tasks: Count,
187
+ posted_tasks: Count.optional(),
188
+ posted_confirmed_tasks: Count.optional(),
189
+ })
190
+ .refine((c) => c.verified_tasks ===
191
+ c.seed_tasks + c.server_checked_tasks + c.confirmed_tasks, {
192
+ message: 'verified_tasks must equal seed_tasks + server_checked_tasks + confirmed_tasks',
193
+ path: ['verified_tasks'],
194
+ });
91
195
  // The two numbers of counted evidence (VOU-139), starting values set on 26
92
196
  // September 2026. At most dailyCeiling verified tasks per agent per UTC day
93
197
  // count, and n tasks of one category count diminishingK * ln(1 + n /
@@ -118,12 +222,22 @@ export const DORMANCY = {
118
222
  // (SCORING.levels in the API) and the CLI explains them, so they live here.
119
223
  // Task counts are counted evidence (CountedCounts), recalibrated on 26
120
224
  // September 2026 (VOU-139) from the simulation in
121
- // apps/api/src/scoring/simulation.ts. Seed tasks count in verifiedTasks at
225
+ // apps/api/src/test/simulation.ts. Seed tasks count in verifiedTasks at
122
226
  // every level (VOU-172, 26 September 2026), so an agent that does seed
123
227
  // tasks every day reaches silver on them alone. Silver dropped its
124
228
  // checked or confirmed, other operator and confirmed task clauses then,
125
229
  // and gold moved from 1000 verified and 250 confirmed from 25 operators to
126
230
  // the step-ups below, with ratings left out until a later change.
231
+ //
232
+ // Posted evidence (POST-3, D-POST-1, 27 September 2026). Every level also
233
+ // needs tasks the agent posted that other operators' agents completed, one
234
+ // posted for every five taken. postedTasks is counted posted tasks,
235
+ // postedOperators the operators other than the agent's own behind them and
236
+ // postedConfirmedTasks the counted confirmed ones among them. Bronze counts
237
+ // what the SealKeeper taker completed and its operator. Silver and gold
238
+ // leave the taker out (TAKER-4). Gold's posted confirmed tasks follow the
239
+ // gold origin rule, post manual and neither report routine (D-POST-5). A
240
+ // minimum of 0 holds for every agent, so no level reads it.
127
241
  export const LEVEL_THRESHOLDS = {
128
242
  bronze: {
129
243
  verifiedTasks: 25,
@@ -131,6 +245,9 @@ export const LEVEL_THRESHOLDS = {
131
245
  reliability: 0.8,
132
246
  // Confirmed incidents in the last 90 days, at most.
133
247
  incidents90d: 0,
248
+ postedTasks: 5,
249
+ postedOperators: 1,
250
+ postedConfirmedTasks: 0,
134
251
  },
135
252
  silver: {
136
253
  verifiedTasks: 200,
@@ -146,6 +263,9 @@ export const LEVEL_THRESHOLDS = {
146
263
  // A declared model, meaning the card names one or the agent sent a
147
264
  // usage event with a model in the window.
148
265
  modelDeclared: true,
266
+ postedTasks: 40,
267
+ postedOperators: 5,
268
+ postedConfirmedTasks: 10,
149
269
  },
150
270
  gold: {
151
271
  verifiedTasks: 200,
@@ -163,6 +283,9 @@ export const LEVEL_THRESHOLDS = {
163
283
  // its last incident, the clean_days count, which stops here.
164
284
  cleanDays: 180,
165
285
  operatorVerified: true,
286
+ postedTasks: 40,
287
+ postedOperators: 5,
288
+ postedConfirmedTasks: 25,
166
289
  },
167
290
  };
168
291
  // The operator silver cap (VOU-182, 26 September 2026). At most `agents`
@@ -190,16 +313,21 @@ export function dormantDays(lastActive, now) {
190
313
  export const isQuiet = (days) => days !== null && days >= DORMANCY.quietDays;
191
314
  // What the agent answers carry beside level, read from the standing table
192
315
  // for the current version. counts are the raw facts and counted what the
193
- // level read. last_active is the newest accepted event of the
316
+ // level read, both with the posted counts (POST-3) beside the SEAL's. last_active is the newest accepted event of the
194
317
  // agent on any version, null when it has sent none. dormant_days is whole
195
318
  // days since last_active at the time of the answer, null with it. quiet is
196
319
  // true when dormant_days is at DORMANCY.quietDays or more.
197
320
  export const Standing = z.strictObject({
198
- counts: StandingCounts,
321
+ counts: AnswerCounts,
199
322
  // The counted evidence the level stands on (VOU-139).
200
- counted: CountedCounts,
323
+ counted: AnswerCounted,
201
324
  history_days: Count,
202
325
  last_active: z.iso.datetime().nullable(),
203
326
  dormant_days: Count.nullable(),
204
327
  quiet: z.boolean(),
328
+ // True while a hold is in force on the agent or its operator (VOU-85),
329
+ // when the issuer withholds the SEAL for cause. Read live, not from the
330
+ // last run. The API always sends it. Optional so the web reads an older
331
+ // API's answer.
332
+ held: z.boolean().optional(),
205
333
  });
@@ -0,0 +1,46 @@
1
+ import type { TaskCategory, TaskSize } from './tasks.js';
2
+ export declare const TASK_TEMPLATE_IDS: readonly ['text_dedupe', 'line_sort', 'json_shape', 'summarise', 'answer_question'];
3
+ export type TaskTemplateId = (typeof TASK_TEMPLATE_IDS)[number];
4
+ export type TemplateKind = 'hash' | 'schema' | 'counterparty';
5
+ export type TemplateInput = 'none' | 'optional' | 'required';
6
+ export type TemplateSpec = {
7
+ instruction: string;
8
+ input: string;
9
+ output: string;
10
+ };
11
+ export type TemplateDraftVerification = {
12
+ kind: 'hash';
13
+ } | {
14
+ kind: 'schema';
15
+ jsonSchema: Record<string, unknown>;
16
+ } | {
17
+ kind: 'counterparty';
18
+ };
19
+ export type TemplateDraft = {
20
+ taskType: TaskTemplateId;
21
+ spec: TemplateSpec;
22
+ verification: TemplateDraftVerification;
23
+ };
24
+ export type TemplateDraw = (lo: number, hi: number) => number;
25
+ export declare class TemplateInputError extends Error {
26
+ name: string;
27
+ }
28
+ export type TemplateSolveErrorCode = 'unknown_template' | 'no_server_check' | 'unknown_rule' | 'bad_input';
29
+ export declare class TemplateSolveError extends Error {
30
+ readonly code: TemplateSolveErrorCode;
31
+ name: string;
32
+ constructor(code: TemplateSolveErrorCode, message: string);
33
+ }
34
+ export type TaskTemplate = {
35
+ id: TaskTemplateId;
36
+ kind: TemplateKind;
37
+ input: TemplateInput;
38
+ category: TaskCategory;
39
+ size: TaskSize;
40
+ make(input: string | undefined, draw: TemplateDraw): TemplateDraft;
41
+ };
42
+ export declare const TEMPLATE_MAX_INPUT_CHARS = 8000;
43
+ export declare const SUMMARISE_MIN_INPUT_WORDS = 40;
44
+ export declare const TASK_TEMPLATES: readonly TaskTemplate[];
45
+ export declare function taskTemplateById(id: string): TaskTemplate | undefined;
46
+ export declare function solveTemplate(taskType: string, spec: unknown): string;