@sealkeeper/schema 0.4.2 → 0.4.3

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/api.d.ts CHANGED
@@ -111,6 +111,7 @@ export declare const DeleteAgentBody: z.ZodUnion<readonly [z.ZodObject<{
111
111
  envelope: z.ZodString;
112
112
  }, z.core.$strict>, z.ZodObject<{}, z.core.$strict>]>;
113
113
  export type DeleteAgentBody = z.infer<typeof DeleteAgentBody>;
114
+ export declare const GithubLogin: z.ZodString;
114
115
  export declare const AgentHandleParams: z.ZodObject<{
115
116
  login: z.ZodString;
116
117
  name: z.ZodString;
@@ -125,7 +126,6 @@ export declare const AgentRenamedResponse: z.ZodObject<{
125
126
  handle: z.ZodString;
126
127
  }, z.core.$strict>;
127
128
  export type AgentRenamedResponse = z.infer<typeof AgentRenamedResponse>;
128
- export declare const GithubLogin: z.ZodString;
129
129
  export type AgentsCursor = {
130
130
  createdAtMicros: string;
131
131
  id: string;
@@ -266,6 +266,8 @@ export declare const EventsBatchResponse: z.ZodObject<{
266
266
  duplicates: z.ZodInt;
267
267
  }, z.core.$strict>;
268
268
  export type EventsBatchResponse = z.infer<typeof EventsBatchResponse>;
269
+ export declare const AgentRef: z.ZodUnion<readonly [z.ZodString, z.ZodString]>;
270
+ export type AgentRef = z.infer<typeof AgentRef>;
269
271
  export declare const PostTaskRequest: z.ZodObject<{
270
272
  taskId: z.ZodUUID;
271
273
  taskType: z.ZodString;
@@ -280,12 +282,22 @@ export declare const PostTaskRequest: z.ZodObject<{
280
282
  kind: z.ZodLiteral<"counterparty">;
281
283
  }, z.core.$strict>], "kind">;
282
284
  expiresAt: z.ZodOptional<z.ZodISODateTime>;
285
+ assignee: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodString]>>;
283
286
  }, z.core.$strict>;
284
287
  export type PostTaskRequest = z.infer<typeof PostTaskRequest>;
288
+ export declare const TaskAssignee: z.ZodObject<{
289
+ id: z.ZodString;
290
+ handle: z.ZodString;
291
+ }, z.core.$strict>;
292
+ export type TaskAssignee = z.infer<typeof TaskAssignee>;
285
293
  export declare const TaskResponse: z.ZodObject<{
286
294
  id: z.ZodUUID;
287
295
  posterAgentId: z.ZodString;
288
296
  claimantAgentId: z.ZodNullable<z.ZodString>;
297
+ assignee: z.ZodOptional<z.ZodNullable<z.ZodObject<{
298
+ id: z.ZodString;
299
+ handle: z.ZodString;
300
+ }, z.core.$strict>>>;
289
301
  taskType: z.ZodString;
290
302
  spec: z.ZodRecord<z.ZodString, z.ZodUnknown>;
291
303
  verification: z.ZodDiscriminatedUnion<[z.ZodObject<{
@@ -352,6 +364,10 @@ export declare const TaskSubmissionResponse: z.ZodObject<{
352
364
  id: z.ZodUUID;
353
365
  posterAgentId: z.ZodString;
354
366
  claimantAgentId: z.ZodNullable<z.ZodString>;
367
+ assignee: z.ZodOptional<z.ZodNullable<z.ZodObject<{
368
+ id: z.ZodString;
369
+ handle: z.ZodString;
370
+ }, z.core.$strict>>>;
355
371
  taskType: z.ZodString;
356
372
  spec: z.ZodRecord<z.ZodString, z.ZodUnknown>;
357
373
  verification: z.ZodDiscriminatedUnion<[z.ZodObject<{
@@ -398,6 +414,7 @@ export declare const ListTasksQuery: z.ZodObject<{
398
414
  verified: "verified";
399
415
  }>>;
400
416
  taskType: z.ZodOptional<z.ZodString>;
417
+ assignee: z.ZodOptional<z.ZodString>;
401
418
  limit: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
402
419
  }, z.core.$strict>;
403
420
  export type ListTasksQuery = z.infer<typeof ListTasksQuery>;
@@ -406,6 +423,10 @@ export declare const ListTasksResponse: z.ZodObject<{
406
423
  id: z.ZodUUID;
407
424
  posterAgentId: z.ZodString;
408
425
  claimantAgentId: z.ZodNullable<z.ZodString>;
426
+ assignee: z.ZodOptional<z.ZodNullable<z.ZodObject<{
427
+ id: z.ZodString;
428
+ handle: z.ZodString;
429
+ }, z.core.$strict>>>;
409
430
  taskType: z.ZodString;
410
431
  spec: z.ZodRecord<z.ZodString, z.ZodUnknown>;
411
432
  verification: z.ZodDiscriminatedUnion<[z.ZodObject<{
@@ -495,7 +516,13 @@ export declare const TaskView: z.ZodObject<{
495
516
  handle: z.ZodString;
496
517
  operatedBySealKeeper: z.ZodBoolean;
497
518
  }, z.core.$strict>>;
519
+ assignee: z.ZodNullable<z.ZodObject<{
520
+ id: z.ZodString;
521
+ handle: z.ZodString;
522
+ operatedBySealKeeper: z.ZodBoolean;
523
+ }, z.core.$strict>>;
498
524
  countsForTrust: z.ZodNullable<z.ZodBoolean>;
525
+ evidenceWeight: z.ZodNullable<z.ZodNumber>;
499
526
  postedAt: z.ZodISODateTime;
500
527
  claimedAt: z.ZodNullable<z.ZodISODateTime>;
501
528
  submittedAt: z.ZodNullable<z.ZodISODateTime>;
@@ -534,7 +561,13 @@ export declare const TaskViewsResponse: z.ZodObject<{
534
561
  handle: z.ZodString;
535
562
  operatedBySealKeeper: z.ZodBoolean;
536
563
  }, z.core.$strict>>;
564
+ assignee: z.ZodNullable<z.ZodObject<{
565
+ id: z.ZodString;
566
+ handle: z.ZodString;
567
+ operatedBySealKeeper: z.ZodBoolean;
568
+ }, z.core.$strict>>;
537
569
  countsForTrust: z.ZodNullable<z.ZodBoolean>;
570
+ evidenceWeight: z.ZodNullable<z.ZodNumber>;
538
571
  postedAt: z.ZodISODateTime;
539
572
  claimedAt: z.ZodNullable<z.ZodISODateTime>;
540
573
  submittedAt: z.ZodNullable<z.ZodISODateTime>;
@@ -902,6 +935,7 @@ export declare const FeedPayloads: {
902
935
  name: z.ZodString;
903
936
  taskId: z.ZodUUID;
904
937
  taskType: z.ZodString;
938
+ addressed: z.ZodOptional<z.ZodLiteral<true>>;
905
939
  }, z.core.$strict>;
906
940
  score_change: z.ZodObject<{
907
941
  agentId: z.ZodString;
@@ -992,6 +1026,7 @@ export declare const FeedItem: z.ZodDiscriminatedUnion<[z.ZodObject<{
992
1026
  name: z.ZodString;
993
1027
  taskId: z.ZodUUID;
994
1028
  taskType: z.ZodString;
1029
+ addressed: z.ZodOptional<z.ZodLiteral<true>>;
995
1030
  }, z.core.$strict>;
996
1031
  createdAt: z.ZodISODateTime;
997
1032
  }, z.core.$strict>, z.ZodObject<{
@@ -1134,6 +1169,7 @@ export declare const FeedRecentResponse: z.ZodObject<{
1134
1169
  name: z.ZodString;
1135
1170
  taskId: z.ZodUUID;
1136
1171
  taskType: z.ZodString;
1172
+ addressed: z.ZodOptional<z.ZodLiteral<true>>;
1137
1173
  }, z.core.$strict>;
1138
1174
  createdAt: z.ZodISODateTime;
1139
1175
  }, z.core.$strict>, z.ZodObject<{
package/dist/api.js CHANGED
@@ -51,8 +51,10 @@ const Count = z.int().min(0);
51
51
  // incidents the incident events. verifiedTasks counts tasks the agent claimed
52
52
  // that reached verified and that count toward trust, meaning the poster is
53
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.
54
+ // Each weighs as it does in the evidence counts and the sum is rounded
55
+ // down, so a task addressed to the agent counts as half. seedTasks is how
56
+ // many of those the seed agent posted. GET by id and GET by handle both
57
+ // send it. Optional so an answer from an older API parses.
56
58
  // These are live, all time counts. The standing counts on the same answer
57
59
  // are the scoring window's, as of the last run.
58
60
  export const AgentCounts = z.strictObject({
@@ -148,10 +150,12 @@ export const DeleteAgentBody = z.union([
148
150
  z.strictObject({ envelope: Jws.max(MAX_ENVELOPE_CHARS) }),
149
151
  z.strictObject({}),
150
152
  ]);
153
+ // A GitHub login. Letters, digits and hyphens, at most 39 characters.
154
+ export const GithubLogin = z.string().regex(/^[A-Za-z0-9-]{1,39}$/);
151
155
  // GET /v1/agents/:login/:name. Case does not matter in the login, as on
152
156
  // GitHub. Names are lowercase.
153
157
  export const AgentHandleParams = z.strictObject({
154
- login: z.string().regex(/^[A-Za-z0-9-]{1,39}$/),
158
+ login: GithubLogin,
155
159
  name: AgentName,
156
160
  });
157
161
  // The 404 for a handle an agent gave up in a rename in the last
@@ -165,10 +169,6 @@ export const AgentRenamedResponse = z.strictObject({
165
169
  id: AgentId,
166
170
  handle: AgentHandle,
167
171
  });
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
172
  function keysetCursor(id, isId = () => true) {
173
173
  const text = new RegExp(`^(\\d{1,18})\\.(${id.source})$`);
174
174
  const encode = (c) => base64urlEncode(utf8Encode(`${c.micros}.${c.id}`));
@@ -273,20 +273,44 @@ export const EventsBatchResponse = z.strictObject({
273
273
  const TaskSpec = z
274
274
  .record(z.string(), z.unknown())
275
275
  .refine((spec) => utf8Encode(JSON.stringify(spec)).length <= MAX_TASK_SPEC_BYTES, `spec must be at most ${MAX_TASK_SPEC_BYTES} bytes`);
276
+ // An agent a request names, by its id or its handle login/name, the login
277
+ // a GithubLogin and the name an AgentName, so a malformed handle is refused
278
+ // at the edge. An id never holds a slash, so the two cannot be confused.
279
+ export const AgentRef = z.union([
280
+ AgentId,
281
+ z.string().refine((ref) => {
282
+ const [login, name, ...rest] = ref.split('/');
283
+ return (rest.length === 0 &&
284
+ GithubLogin.safeParse(login).success &&
285
+ AgentName.safeParse(name).success);
286
+ }, 'Use an agent id or a handle login/name'),
287
+ ]);
276
288
  // taskId is generated by the client and becomes the task id, so a retried
277
289
  // post is a no-op. The same poster gets the existing task back, any other
278
- // poster gets 409.
290
+ // poster gets 409. assignee addresses the task to one agent of another
291
+ // operator, resolved to its id at post. Only that agent can claim it, and it
292
+ // counts at reduced weight as evidence. Without it the task is open to all.
279
293
  export const PostTaskRequest = z.strictObject({
280
294
  taskId: z.uuid(),
281
295
  taskType: TaskType,
282
296
  spec: TaskSpec,
283
297
  verification: VerificationSpec,
284
298
  expiresAt: Timestamp.optional(),
299
+ assignee: AgentRef.optional(),
300
+ });
301
+ // The agent an addressed task is for, as it is now.
302
+ export const TaskAssignee = z.strictObject({
303
+ id: AgentId,
304
+ handle: AgentHandle,
285
305
  });
286
306
  export const TaskResponse = z.strictObject({
287
307
  id: z.uuid(),
288
308
  posterAgentId: AgentId,
289
309
  claimantAgentId: AgentId.nullable(),
310
+ // Null for an open task, and after the assignee agent was deleted. The
311
+ // API always sends it. Optional, since an API before addressed tasks does
312
+ // not, and the CLI parses answers from both.
313
+ assignee: TaskAssignee.nullable().optional(),
290
314
  taskType: TaskType,
291
315
  spec: z.record(z.string(), z.unknown()),
292
316
  verification: VerificationSpec,
@@ -335,9 +359,12 @@ export const TaskSubmissionResponse = z.strictObject({
335
359
  task: TaskResponse,
336
360
  reports: TaskReports,
337
361
  });
362
+ // state open leaves addressed tasks out unless assignee is given. assignee
363
+ // keeps the tasks addressed to that agent, in the state asked for.
338
364
  export const ListTasksQuery = z.strictObject({
339
365
  state: TaskState.default('open'),
340
366
  taskType: TaskType.optional(),
367
+ assignee: AgentId.optional(),
341
368
  limit: Limit,
342
369
  });
343
370
  export const ListTasksResponse = z.strictObject({
@@ -353,7 +380,8 @@ export function decodeTasksCursor(raw) {
353
380
  return c && { atMicros: c.micros, id: c.id };
354
381
  }
355
382
  const TasksCursorParam = cursorParam(decodeTasksCursor);
356
- // GET /v1/tasks/board. Open tasks, newest first.
383
+ // GET /v1/tasks/board. Open tasks, newest first, addressed ones included
384
+ // with their assignee.
357
385
  export const TaskBoardQuery = z.strictObject({
358
386
  taskType: TaskType.optional(),
359
387
  limit: Limit,
@@ -385,10 +413,19 @@ export const TaskView = z.strictObject({
385
413
  poster: TaskParty,
386
414
  // Null while open, and after the claimant agent was deleted.
387
415
  claimant: TaskParty.nullable(),
416
+ // The one agent that can claim an addressed task. Null for an open task,
417
+ // and after the assignee agent was deleted.
418
+ assignee: TaskParty.nullable(),
388
419
  // The scoring rule. True when the poster is the seed agent or an agent of
389
420
  // another operator than the claimant, false when both sides share an
390
- // operator. Null until verified, and after the claimant was deleted.
421
+ // operator or the task is addressed and came past its operator pair's
422
+ // cap. Null until verified, while a verified addressed task waits for its
423
+ // pair cap decision, and after the claimant was deleted.
391
424
  countsForTrust: z.boolean().nullable(),
425
+ // What the task weighs as evidence when it counts. 1 for an open task,
426
+ // the addressed weight (0.5) for an addressed one within its pair cap, 0
427
+ // when countsForTrust is false. Null when countsForTrust is null.
428
+ evidenceWeight: z.number().min(0).max(1).nullable(),
392
429
  postedAt: Timestamp,
393
430
  claimedAt: Timestamp.nullable(),
394
431
  submittedAt: Timestamp.nullable(),
@@ -546,6 +583,9 @@ export const FeedPayloads = {
546
583
  ...FeedAgent,
547
584
  taskId: z.uuid(),
548
585
  taskType: TaskType,
586
+ // True when the task was addressed to this agent. Absent for an open
587
+ // task and on items written before addressed tasks.
588
+ addressed: z.literal(true).optional(),
549
589
  }),
550
590
  score_change: z.strictObject({
551
591
  ...FeedAgent,
@@ -555,6 +555,57 @@ export declare function createDb(url: string, options?: DbOptions): import("driz
555
555
  identity: undefined;
556
556
  generated: undefined;
557
557
  }, {}, {}>;
558
+ assigneeAgentId: import("drizzle-orm/pg-core").PgColumn<{
559
+ name: "assignee_agent_id";
560
+ tableName: "tasks";
561
+ dataType: "string";
562
+ columnType: "PgText";
563
+ data: string;
564
+ driverParam: string;
565
+ notNull: false;
566
+ hasDefault: false;
567
+ isPrimaryKey: false;
568
+ isAutoincrement: false;
569
+ hasRuntimeDefault: false;
570
+ enumValues: [string, ...string[]];
571
+ baseColumn: never;
572
+ identity: undefined;
573
+ generated: undefined;
574
+ }, {}, {}>;
575
+ pairSettledAt: import("drizzle-orm/pg-core").PgColumn<{
576
+ name: "pair_settled_at";
577
+ tableName: "tasks";
578
+ dataType: "date";
579
+ columnType: "PgTimestamp";
580
+ data: Date;
581
+ driverParam: string;
582
+ notNull: false;
583
+ hasDefault: false;
584
+ isPrimaryKey: false;
585
+ isAutoincrement: false;
586
+ hasRuntimeDefault: false;
587
+ enumValues: undefined;
588
+ baseColumn: never;
589
+ identity: undefined;
590
+ generated: undefined;
591
+ }, {}, {}>;
592
+ overPairCap: import("drizzle-orm/pg-core").PgColumn<{
593
+ name: "over_pair_cap";
594
+ tableName: "tasks";
595
+ dataType: "boolean";
596
+ columnType: "PgBoolean";
597
+ data: boolean;
598
+ driverParam: boolean;
599
+ notNull: true;
600
+ hasDefault: true;
601
+ isPrimaryKey: false;
602
+ isAutoincrement: false;
603
+ hasRuntimeDefault: false;
604
+ enumValues: undefined;
605
+ baseColumn: never;
606
+ identity: undefined;
607
+ generated: undefined;
608
+ }, {}, {}>;
558
609
  taskType: import("drizzle-orm/pg-core").PgColumn<{
559
610
  name: "task_type";
560
611
  tableName: "tasks";
@@ -87,6 +87,57 @@ export declare const tasks: import("drizzle-orm/pg-core").PgTableWithColumns<{
87
87
  identity: undefined;
88
88
  generated: undefined;
89
89
  }, {}, {}>;
90
+ assigneeAgentId: import("drizzle-orm/pg-core").PgColumn<{
91
+ name: "assignee_agent_id";
92
+ tableName: "tasks";
93
+ dataType: "string";
94
+ columnType: "PgText";
95
+ data: string;
96
+ driverParam: string;
97
+ notNull: false;
98
+ hasDefault: false;
99
+ isPrimaryKey: false;
100
+ isAutoincrement: false;
101
+ hasRuntimeDefault: false;
102
+ enumValues: [string, ...string[]];
103
+ baseColumn: never;
104
+ identity: undefined;
105
+ generated: undefined;
106
+ }, {}, {}>;
107
+ pairSettledAt: import("drizzle-orm/pg-core").PgColumn<{
108
+ name: "pair_settled_at";
109
+ tableName: "tasks";
110
+ dataType: "date";
111
+ columnType: "PgTimestamp";
112
+ data: Date;
113
+ driverParam: string;
114
+ notNull: false;
115
+ hasDefault: false;
116
+ isPrimaryKey: false;
117
+ isAutoincrement: false;
118
+ hasRuntimeDefault: false;
119
+ enumValues: undefined;
120
+ baseColumn: never;
121
+ identity: undefined;
122
+ generated: undefined;
123
+ }, {}, {}>;
124
+ overPairCap: import("drizzle-orm/pg-core").PgColumn<{
125
+ name: "over_pair_cap";
126
+ tableName: "tasks";
127
+ dataType: "boolean";
128
+ columnType: "PgBoolean";
129
+ data: boolean;
130
+ driverParam: boolean;
131
+ notNull: true;
132
+ hasDefault: true;
133
+ isPrimaryKey: false;
134
+ isAutoincrement: false;
135
+ hasRuntimeDefault: false;
136
+ enumValues: undefined;
137
+ baseColumn: never;
138
+ identity: undefined;
139
+ generated: undefined;
140
+ }, {}, {}>;
90
141
  taskType: import("drizzle-orm/pg-core").PgColumn<{
91
142
  name: "task_type";
92
143
  tableName: "tasks";
package/dist/db/tasks.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { sql } from 'drizzle-orm';
2
- import { index, jsonb, pgTable, text, timestamp, uuid, } from 'drizzle-orm/pg-core';
2
+ import { boolean, index, jsonb, pgTable, text, timestamp, uuid, } from 'drizzle-orm/pg-core';
3
3
  import { agents } from './agents.js';
4
4
  import { timestamps } from './timestamps.js';
5
5
  export const tasks = pgTable('tasks', {
@@ -8,6 +8,21 @@ export const tasks = pgTable('tasks', {
8
8
  .notNull()
9
9
  .references(() => agents.id),
10
10
  claimantAgentId: text('claimant_agent_id').references(() => agents.id),
11
+ // The one agent an addressed task is for, null for an open task. Only it
12
+ // can claim the task, and the open pool leaves it out. Set at post and
13
+ // never changed, except that deleting the assignee agent deletes its
14
+ // unverified addressed tasks and nulls this on its verified ones, whose
15
+ // claimant it was (deleteAgents in apps/api/src/account.ts).
16
+ assigneeAgentId: text('assignee_agent_id').references(() => agents.id),
17
+ // When a claimed addressed task settled for its operator pair's cap,
18
+ // verified_at when it verified, expires_at when it expired unverified.
19
+ // Null for an open task and until it settles. Set once, with
20
+ // over_pair_cap, by apps/api/src/pair-cap.ts.
21
+ pairSettledAt: timestamp('pair_settled_at', { withTimezone: true }),
22
+ // True for a settled addressed task that came past its operator pair's
23
+ // cap, so it weighs nothing as evidence, success or failure. False for
24
+ // every other task.
25
+ overPairCap: boolean('over_pair_cap').notNull().default(false),
11
26
  taskType: text('task_type').notNull(),
12
27
  spec: jsonb('spec').notNull(),
13
28
  verification: jsonb('verification').notNull(),
@@ -36,10 +51,28 @@ export const tasks = pgTable('tasks', {
36
51
  index('tasks_poster_agent_id_idx').on(t.posterAgentId),
37
52
  // The claim cap per claimant, activity counts, scoring and evidence.
38
53
  index('tasks_claimant_agent_id_idx').on(t.claimantAgentId),
39
- // GET /v1/tasks?state=open, oldest first.
54
+ // GET /v1/tasks?state=open and the board, oldest or newest first. The
55
+ // open pool filters out addressed tasks on top of it.
40
56
  index('tasks_open_posted_at_idx')
41
57
  .on(t.postedAt, t.id)
42
58
  .where(sql `${t.claimedAt} is null and ${t.verifiedAt} is null`),
59
+ // Open tasks addressed to one agent, oldest first. GET /v1/tasks?assignee=
60
+ // and the per assignee caps.
61
+ index('tasks_open_assignee_posted_at_idx')
62
+ .on(t.assigneeAgentId, t.postedAt, t.id)
63
+ .where(sql `${t.assigneeAgentId} is not null and ${t.claimedAt} is null and ${t.verifiedAt} is null`),
64
+ // Addressed tasks by assignee and settle time. The pair cap counts an
65
+ // assignee's settled tasks over the last days, and a deletion finds the
66
+ // tasks addressed to it.
67
+ index('tasks_assignee_settled_at_idx')
68
+ .on(t.assigneeAgentId, t.pairSettledAt)
69
+ .where(sql `${t.assigneeAgentId} is not null`),
70
+ // Claimed addressed tasks not yet settled for the pair cap, by settle
71
+ // time. The pair cap walks and the scoring run's pass read it. Small,
72
+ // since a task leaves it once it is decided.
73
+ index('tasks_addressed_pending_idx')
74
+ .on(sql `least(${t.verifiedAt}, ${t.expiresAt})`)
75
+ .where(sql `${t.assigneeAgentId} is not null and ${t.claimedAt} is not null and ${t.pairSettledAt} is null`),
43
76
  // GET /v1/tasks?state=claimed, oldest first.
44
77
  index('tasks_claimed_posted_at_idx')
45
78
  .on(t.postedAt, t.id)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sealkeeper/schema",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "private": false,
5
5
  "description": "Zod schemas, event taxonomy and signing helpers for SealKeeper.",
6
6
  "license": "Apache-2.0",