@sealkeeper/schema 0.4.4 → 0.4.5
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 +604 -27
- package/dist/api.js +207 -43
- package/dist/badge.d.ts +1 -0
- package/dist/badge.js +7 -0
- package/dist/db/agents.d.ts +70 -0
- package/dist/db/agents.js +18 -0
- package/dist/db/client.d.ts +341 -0
- package/dist/db/client.js +2 -0
- 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 +3 -1
- package/dist/db/migrator.js +8 -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 +13 -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 +5 -0
- package/dist/index.js +5 -0
- package/dist/moderation.d.ts +28 -0
- package/dist/moderation.js +450 -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 +23 -1
- package/dist/tasks.js +29 -5
- package/package.json +1 -1
package/dist/goal.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { AgentId } from './agent-id.js';
|
|
3
3
|
import { Version } from './events.js';
|
|
4
|
-
import { Level } from './standing.js';
|
|
4
|
+
import { LADDER, Level } from './standing.js';
|
|
5
5
|
// GET /v1/agents/:id/goal. What the agent's current version needs for its
|
|
6
6
|
// next level, threshold by threshold, and what to do next. The API sends
|
|
7
7
|
// these strictly. A client parses them loosely (packages/cli/src/responses.ts),
|
|
@@ -15,6 +15,11 @@ export const GOAL_ACTION_CODES = [
|
|
|
15
15
|
// The agent has sent nothing accepted for a while and its level drops.
|
|
16
16
|
// count is the dormant days.
|
|
17
17
|
'dormant',
|
|
18
|
+
// The operator's verified domain was missing at its last check and the
|
|
19
|
+
// grace is running (VOU-185). operator_verified, which gold needs, goes
|
|
20
|
+
// false when it ends. count is the whole days left and until the moment
|
|
21
|
+
// it ends. Sent at any level.
|
|
22
|
+
'operator_verification_lapsing',
|
|
18
23
|
// Counterparty tasks this agent claimed wait for its outcome report. A
|
|
19
24
|
// confirmed task counts for its claimant, so this counts toward the
|
|
20
25
|
// agent's own level. count is how many.
|
|
@@ -22,10 +27,12 @@ export const GOAL_ACTION_CODES = [
|
|
|
22
27
|
// Tasks addressed to this agent are waiting to be claimed. count is how
|
|
23
28
|
// many.
|
|
24
29
|
'addressed_waiting',
|
|
25
|
-
// Verified
|
|
30
|
+
// Verified tasks still needed. Seed tasks count toward it at every level
|
|
31
|
+
// (VOU-172). count is how many.
|
|
26
32
|
'claim_seed_tasks',
|
|
27
|
-
// Verified tasks from other operators' agents still needed.
|
|
28
|
-
//
|
|
33
|
+
// Verified tasks from other operators' agents still needed. No threshold
|
|
34
|
+
// sends it since VOU-172, where seed tasks count at every level. Kept so
|
|
35
|
+
// a client that knows it keeps working.
|
|
29
36
|
'claim_tasks',
|
|
30
37
|
// Confirmed counterparty tasks with other operators still needed.
|
|
31
38
|
'counterparty_tasks',
|
|
@@ -38,11 +45,15 @@ export const GOAL_ACTION_CODES = [
|
|
|
38
45
|
'safety_below',
|
|
39
46
|
// An incident in the window. count is the incidents counted.
|
|
40
47
|
'safety_incident_window',
|
|
48
|
+
// Gold's clean record, days since the later of the first accepted event
|
|
49
|
+
// and the last incident, is short. count is the days still to go.
|
|
50
|
+
'clean_days',
|
|
41
51
|
// The version's share of the agent's events is under the threshold.
|
|
42
52
|
'provenance_below',
|
|
43
53
|
// No model declared on the card or in a usage event.
|
|
44
54
|
'declare_model',
|
|
45
|
-
// Operator identity not verified beyond a GitHub login.
|
|
55
|
+
// Operator identity not verified beyond a GitHub login. The operator
|
|
56
|
+
// verifies a domain on the account page (VOU-185).
|
|
46
57
|
'operator_unverified',
|
|
47
58
|
// Ratings from silver or gold agents still needed.
|
|
48
59
|
'need_ratings',
|
|
@@ -50,6 +61,10 @@ export const GOAL_ACTION_CODES = [
|
|
|
50
61
|
// version, and the new version's level is capped one below that
|
|
51
62
|
// version's until its own record earns it.
|
|
52
63
|
'version_cap',
|
|
64
|
+
// Every silver threshold holds, and the operator's silver slots are all
|
|
65
|
+
// taken (OPERATOR_SILVER_CAP), so the agent is held at bronze. count is
|
|
66
|
+
// the whole days until the next slot frees and until the moment it does.
|
|
67
|
+
'operator_silver_cap',
|
|
53
68
|
// Counterparty tasks this agent posted wait for its outcome report. It
|
|
54
69
|
// helps the claimant, the other agent, and adds nothing to this agent's
|
|
55
70
|
// level, so it comes after every step that does. count is how many.
|
|
@@ -71,17 +86,59 @@ export const GoalThreshold = z.strictObject({
|
|
|
71
86
|
raw: z.number().nullable(),
|
|
72
87
|
});
|
|
73
88
|
// One next step. code is machine readable, count what is left where a
|
|
74
|
-
// number says it, else null.
|
|
89
|
+
// number says it, else null. until is when the step clears by itself, sent
|
|
90
|
+
// only with operator_silver_cap. Clients parse actions loosely, so one
|
|
91
|
+
// that does not know until ignores it.
|
|
75
92
|
export const GoalAction = z.strictObject({
|
|
76
93
|
code: z.string().min(1).max(64),
|
|
77
94
|
count: Count.nullable(),
|
|
95
|
+
until: z.iso.datetime().optional(),
|
|
96
|
+
});
|
|
97
|
+
// Where one level of LADDER stands for the agent. reached is the stored
|
|
98
|
+
// level and every level below it. next is the issued level above it, the
|
|
99
|
+
// same as nextLevel. locked is an issued level above next. reserved is a
|
|
100
|
+
// level the standard names and does not issue yet (platinum), whatever the
|
|
101
|
+
// agent has, so a client shows it as coming later.
|
|
102
|
+
export const GOAL_LADDER_STATES = [
|
|
103
|
+
'reached',
|
|
104
|
+
'next',
|
|
105
|
+
'locked',
|
|
106
|
+
'reserved',
|
|
107
|
+
];
|
|
108
|
+
export const GoalLadderState = z.enum(GOAL_LADDER_STATES);
|
|
109
|
+
// One entry per LADDER level, lowest first.
|
|
110
|
+
export const GoalLadderStep = z.strictObject({
|
|
111
|
+
level: z.enum(LADDER.map((s) => s.level)),
|
|
112
|
+
state: GoalLadderState,
|
|
113
|
+
});
|
|
114
|
+
// One item of gold's checklist. code is the name of the gold threshold it
|
|
115
|
+
// stands for (GoalThreshold.name), done whether it holds. progress is
|
|
116
|
+
// current of required where the rule has a number, null for a flag such as
|
|
117
|
+
// operator_verified. For clean_days that reads as "Safety record, 72 of 180
|
|
118
|
+
// days", for history_days as "Active on 41 of 60 days".
|
|
119
|
+
export const GoalStep = z.strictObject({
|
|
120
|
+
code: z.string().min(1).max(64),
|
|
121
|
+
done: z.boolean(),
|
|
122
|
+
progress: z
|
|
123
|
+
.strictObject({
|
|
124
|
+
current: z.number(),
|
|
125
|
+
required: z.number(),
|
|
126
|
+
})
|
|
127
|
+
.nullable(),
|
|
78
128
|
});
|
|
79
129
|
// level and the thresholds come from the standing row of the current
|
|
80
|
-
// version, so they match the SEAL. nextLevel is
|
|
81
|
-
//
|
|
82
|
-
// the
|
|
83
|
-
//
|
|
84
|
-
//
|
|
130
|
+
// version, so they match the SEAL. nextLevel is the issued level above
|
|
131
|
+
// level. null means no issued level is above it, not that level is the top
|
|
132
|
+
// of the ladder, since a reserved level (platinum) can sit above. A client
|
|
133
|
+
// from before the ladder (CLI 0.4.x) reads null as nothing left to do, so
|
|
134
|
+
// null stays at gold. thresholds are every rule of nextLevel, none when it
|
|
135
|
+
// is null. ladder is every LADDER level with its state. steps is gold's
|
|
136
|
+
// checklist, sent when nextLevel is gold and empty otherwise, the step-ups
|
|
137
|
+
// first. pending is live (GoalPending). asOf is the scoring run the numbers
|
|
138
|
+
// come from, null before the first. today is live too, the agent's verified
|
|
139
|
+
// tasks of the current UTC day that count toward its level, at most
|
|
140
|
+
// ceiling, and what is left. A task past the ceiling still verifies and
|
|
141
|
+
// shows, and counts for nothing.
|
|
85
142
|
export const GoalToday = z.strictObject({
|
|
86
143
|
// The UTC day, YYYY-MM-DD, so a cached answer from another day is known.
|
|
87
144
|
day: z.iso.date(),
|
|
@@ -104,7 +161,9 @@ export const GoalResponse = z.strictObject({
|
|
|
104
161
|
version: Version,
|
|
105
162
|
level: Level,
|
|
106
163
|
nextLevel: Level.nullable(),
|
|
164
|
+
ladder: z.array(GoalLadderStep),
|
|
107
165
|
thresholds: z.array(GoalThreshold),
|
|
166
|
+
steps: z.array(GoalStep),
|
|
108
167
|
actions: z.array(GoalAction),
|
|
109
168
|
pending: GoalPending,
|
|
110
169
|
today: GoalToday,
|
package/dist/index.d.ts
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
export * from './agent-id.js';
|
|
2
2
|
export * from './agent-name.js';
|
|
3
3
|
export * from './api.js';
|
|
4
|
+
export * from './badge.js';
|
|
4
5
|
export * from './base64url.js';
|
|
5
6
|
export * from './credential.js';
|
|
6
7
|
export * from './dimensions.js';
|
|
7
8
|
export * from './envelope.js';
|
|
8
9
|
export * from './events.js';
|
|
9
10
|
export * from './goal.js';
|
|
11
|
+
export * from './moderation.js';
|
|
12
|
+
export * from './operator-domains.js';
|
|
13
|
+
export * from './policy.js';
|
|
14
|
+
export * from './runtime.js';
|
|
10
15
|
export * from './standing.js';
|
|
11
16
|
export * from './tasks.js';
|
|
12
17
|
export * from './top-dimensions.js';
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
export * from './agent-id.js';
|
|
2
2
|
export * from './agent-name.js';
|
|
3
3
|
export * from './api.js';
|
|
4
|
+
export * from './badge.js';
|
|
4
5
|
export * from './base64url.js';
|
|
5
6
|
export * from './credential.js';
|
|
6
7
|
export * from './dimensions.js';
|
|
7
8
|
export * from './envelope.js';
|
|
8
9
|
export * from './events.js';
|
|
9
10
|
export * from './goal.js';
|
|
11
|
+
export * from './moderation.js';
|
|
12
|
+
export * from './operator-domains.js';
|
|
13
|
+
export * from './policy.js';
|
|
14
|
+
export * from './runtime.js';
|
|
10
15
|
export * from './standing.js';
|
|
11
16
|
export * from './tasks.js';
|
|
12
17
|
export * from './top-dimensions.js';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const OPERATOR_SLUG_MIN = 1;
|
|
3
|
+
export declare const OPERATOR_SLUG_MAX = 39;
|
|
4
|
+
export declare const OPERATOR_DISPLAY_NAME_MIN = 1;
|
|
5
|
+
export declare const OPERATOR_DISPLAY_NAME_MAX = 64;
|
|
6
|
+
export declare const OperatorSlug: z.ZodString;
|
|
7
|
+
export type OperatorSlug = z.infer<typeof OperatorSlug>;
|
|
8
|
+
export declare const OperatorDisplayName: z.ZodString;
|
|
9
|
+
export type OperatorDisplayName = z.infer<typeof OperatorDisplayName>;
|
|
10
|
+
export declare function normaliseName(text: string): string;
|
|
11
|
+
export declare const displayNameKey: typeof normaliseName;
|
|
12
|
+
export declare const PROTECTED_NAMES: readonly string[];
|
|
13
|
+
export declare const PROTECTED_SUFFIXES: readonly string[];
|
|
14
|
+
export declare const PROTECTED_PREFIXES: readonly string[];
|
|
15
|
+
export declare function protectedNameOf(text: string): string | null;
|
|
16
|
+
export declare const RESERVED_WORDS: readonly string[];
|
|
17
|
+
export declare function isReservedWord(text: string): boolean;
|
|
18
|
+
export type NameRefusalCode = 'name_protected' | 'name_reserved';
|
|
19
|
+
export type NameRefusal = {
|
|
20
|
+
code: NameRefusalCode;
|
|
21
|
+
message: string;
|
|
22
|
+
};
|
|
23
|
+
export declare function moderateSlug(slug: string): NameRefusal | null;
|
|
24
|
+
export declare function moderateDisplayName(name: string): NameRefusal | null;
|
|
25
|
+
export declare function slugBase(login: string): string;
|
|
26
|
+
export declare const suffixedSlug: (base: string, n: number) => string;
|
|
27
|
+
export type SlugSuffixReason = 'reserved' | 'protected' | 'taken';
|
|
28
|
+
export declare function slugSuffixReason(login: string, slug: string): SlugSuffixReason | null;
|
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
// Moderation of the names an operator chooses, the operator slug and the
|
|
3
|
+
// operator display name. The API runs these checks on every write and is
|
|
4
|
+
// the only enforcement. The CLI and the web run the same checks for early
|
|
5
|
+
// feedback. A name another operator already holds is the API's call, since
|
|
6
|
+
// only the database knows (409 name_taken).
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Shapes
|
|
9
|
+
export const OPERATOR_SLUG_MIN = 1;
|
|
10
|
+
// 39, like a GitHub login. A 39 character slug, a slash and a 39 character
|
|
11
|
+
// agent name make a 79 character handle, the AgentHandle cap that published
|
|
12
|
+
// CLIs bundle.
|
|
13
|
+
export const OPERATOR_SLUG_MAX = 39;
|
|
14
|
+
export const OPERATOR_DISPLAY_NAME_MIN = 1;
|
|
15
|
+
export const OPERATOR_DISPLAY_NAME_MAX = 64;
|
|
16
|
+
const SLUG = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
17
|
+
// The operator's slug, the first half of every handle, as in alice/claude-code.
|
|
18
|
+
// Lowercase letters, digits and single hyphens, starting and ending with a
|
|
19
|
+
// letter or digit. Unique across SealKeeper. Reserved words and protected
|
|
20
|
+
// names are refused by moderateSlug, not by this shape.
|
|
21
|
+
export const OperatorSlug = z
|
|
22
|
+
.string()
|
|
23
|
+
.min(OPERATOR_SLUG_MIN)
|
|
24
|
+
.max(OPERATOR_SLUG_MAX)
|
|
25
|
+
.regex(SLUG, 'Use lowercase letters, digits and hyphens, starting and ending with a letter or digit')
|
|
26
|
+
.refine((slug) => !slug.includes('--'), 'No double hyphen');
|
|
27
|
+
// Control, format, private use, unassigned and lone surrogate characters
|
|
28
|
+
// (bidi overrides, zero width joiners and the like), and the line and
|
|
29
|
+
// paragraph separators, which can hide, reorder or break what a reader sees.
|
|
30
|
+
const HIDDEN = /[\p{C}\p{Zl}\p{Zp}]/u;
|
|
31
|
+
const LETTER_OR_DIGIT = /[\p{L}\p{N}]/u;
|
|
32
|
+
// The operator's display name. Free text, no control or format characters,
|
|
33
|
+
// no space at either end, and at least one letter or digit, so its
|
|
34
|
+
// normalised key is never empty. Protected names are refused by
|
|
35
|
+
// moderateDisplayName.
|
|
36
|
+
export const OperatorDisplayName = z
|
|
37
|
+
.string()
|
|
38
|
+
.min(OPERATOR_DISPLAY_NAME_MIN)
|
|
39
|
+
.max(OPERATOR_DISPLAY_NAME_MAX)
|
|
40
|
+
.refine((name) => !HIDDEN.test(name), 'No control characters')
|
|
41
|
+
.refine((name) => name === name.trim(), 'No space at the start or end')
|
|
42
|
+
.refine((name) => LETTER_OR_DIGIT.test(name), 'Use at least one letter or digit');
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Normalisation
|
|
45
|
+
// Letters from other scripts that read as a Latin letter, and a few Latin
|
|
46
|
+
// letters NFKD does not split into a base and a mark.
|
|
47
|
+
const HOMOGLYPHS = {
|
|
48
|
+
// Cyrillic
|
|
49
|
+
а: 'a',
|
|
50
|
+
в: 'b',
|
|
51
|
+
с: 'c',
|
|
52
|
+
ԁ: 'd',
|
|
53
|
+
е: 'e',
|
|
54
|
+
ё: 'e',
|
|
55
|
+
һ: 'h',
|
|
56
|
+
н: 'h',
|
|
57
|
+
і: 'i',
|
|
58
|
+
ї: 'i',
|
|
59
|
+
ј: 'j',
|
|
60
|
+
к: 'k',
|
|
61
|
+
ӏ: 'l',
|
|
62
|
+
м: 'm',
|
|
63
|
+
п: 'n',
|
|
64
|
+
о: 'o',
|
|
65
|
+
р: 'p',
|
|
66
|
+
ԛ: 'q',
|
|
67
|
+
г: 'r',
|
|
68
|
+
ѕ: 's',
|
|
69
|
+
т: 't',
|
|
70
|
+
у: 'y',
|
|
71
|
+
ш: 'w',
|
|
72
|
+
ԝ: 'w',
|
|
73
|
+
х: 'x',
|
|
74
|
+
// Greek
|
|
75
|
+
α: 'a',
|
|
76
|
+
β: 'b',
|
|
77
|
+
ϲ: 'c',
|
|
78
|
+
ε: 'e',
|
|
79
|
+
η: 'n',
|
|
80
|
+
ι: 'i',
|
|
81
|
+
κ: 'k',
|
|
82
|
+
μ: 'u',
|
|
83
|
+
ν: 'v',
|
|
84
|
+
ο: 'o',
|
|
85
|
+
ρ: 'p',
|
|
86
|
+
τ: 't',
|
|
87
|
+
υ: 'u',
|
|
88
|
+
χ: 'x',
|
|
89
|
+
ω: 'w',
|
|
90
|
+
// Latin letters without a decomposition
|
|
91
|
+
ı: 'i',
|
|
92
|
+
ȷ: 'j',
|
|
93
|
+
ł: 'l',
|
|
94
|
+
ø: 'o',
|
|
95
|
+
đ: 'd',
|
|
96
|
+
ħ: 'h',
|
|
97
|
+
ŧ: 't',
|
|
98
|
+
ß: 'ss',
|
|
99
|
+
æ: 'ae',
|
|
100
|
+
œ: 'oe',
|
|
101
|
+
ɡ: 'g',
|
|
102
|
+
ɑ: 'a',
|
|
103
|
+
// Digits and symbols used as letters
|
|
104
|
+
'0': 'o',
|
|
105
|
+
'1': 'l',
|
|
106
|
+
'3': 'e',
|
|
107
|
+
'4': 'a',
|
|
108
|
+
'5': 's',
|
|
109
|
+
'7': 't',
|
|
110
|
+
'8': 'b',
|
|
111
|
+
'@': 'a',
|
|
112
|
+
$: 's',
|
|
113
|
+
'|': 'l',
|
|
114
|
+
'!': 'l',
|
|
115
|
+
};
|
|
116
|
+
// i and l look alike in many fonts (GoogIe with a capital I), so both fold
|
|
117
|
+
// to l. Run after HOMOGLYPHS, so a Cyrillic і, a Greek ι or a dotless ı,
|
|
118
|
+
// which fold to i there, fold on to l as well.
|
|
119
|
+
const I_AS_L = /i/g;
|
|
120
|
+
const MARKS = /\p{M}/gu;
|
|
121
|
+
const NOT_LETTER_OR_DIGIT = /[^\p{L}\p{N}]/gu;
|
|
122
|
+
// The form two names are compared in. NFKC, lowercase, accents dropped,
|
|
123
|
+
// homoglyphs and digit swaps folded, everything but letters and digits
|
|
124
|
+
// dropped, then letter pairs that read as one letter (rn as m, vv as w)
|
|
125
|
+
// folded. Open-AI, OPENAI and 0penai all give the same key. Letters of
|
|
126
|
+
// scripts with no Latin lookalike are kept, so a display name in another
|
|
127
|
+
// script still has a key of its own.
|
|
128
|
+
export function normaliseName(text) {
|
|
129
|
+
const folded = Array.from(text.normalize('NFKC').toLowerCase().normalize('NFKD').replace(MARKS, ''), (ch) => HOMOGLYPHS[ch] ?? ch)
|
|
130
|
+
.join('')
|
|
131
|
+
.replace(I_AS_L, 'l');
|
|
132
|
+
return folded
|
|
133
|
+
.replace(NOT_LETTER_OR_DIGIT, '')
|
|
134
|
+
.replace(/rn/g, 'm')
|
|
135
|
+
.replace(/vv/g, 'w');
|
|
136
|
+
}
|
|
137
|
+
// The display_name_key column. Unique across operators, so two display
|
|
138
|
+
// names that read the same cannot both be held.
|
|
139
|
+
export const displayNameKey = normaliseName;
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
// Protected names
|
|
142
|
+
// The top AI and technology companies and products. A slug or display name
|
|
143
|
+
// that normalises to one of these, with or without a common suffix, is
|
|
144
|
+
// refused. Written as the name reads. The list protects the distinctive
|
|
145
|
+
// form only, so Stability AI protects stabilityai (and stability-ai-labs)
|
|
146
|
+
// but not the plain word stability. Products that are also common first
|
|
147
|
+
// names (Devin, Alexa, Siri) are left out, since a login such as devin
|
|
148
|
+
// would otherwise lose its slug. Claude is the one exception.
|
|
149
|
+
export const PROTECTED_NAMES = [
|
|
150
|
+
// SealKeeper itself
|
|
151
|
+
'SealKeeper',
|
|
152
|
+
'Vouched',
|
|
153
|
+
// AI labs and model makers
|
|
154
|
+
'Anthropic',
|
|
155
|
+
'Claude',
|
|
156
|
+
'OpenAI',
|
|
157
|
+
'ChatGPT',
|
|
158
|
+
'GPT',
|
|
159
|
+
'Codex',
|
|
160
|
+
'DALL-E',
|
|
161
|
+
'Google',
|
|
162
|
+
'Alphabet',
|
|
163
|
+
'DeepMind',
|
|
164
|
+
'Gemini',
|
|
165
|
+
'Microsoft',
|
|
166
|
+
'Copilot',
|
|
167
|
+
'Bing',
|
|
168
|
+
'Azure',
|
|
169
|
+
'Meta',
|
|
170
|
+
'Facebook',
|
|
171
|
+
'Instagram',
|
|
172
|
+
'WhatsApp',
|
|
173
|
+
'Llama',
|
|
174
|
+
'xAI',
|
|
175
|
+
'Grok',
|
|
176
|
+
'Twitter',
|
|
177
|
+
'Mistral',
|
|
178
|
+
'Le Chat',
|
|
179
|
+
'Cohere',
|
|
180
|
+
'DeepSeek',
|
|
181
|
+
'Qwen',
|
|
182
|
+
'Alibaba',
|
|
183
|
+
'Zhipu',
|
|
184
|
+
'Baidu',
|
|
185
|
+
'ByteDance',
|
|
186
|
+
'TikTok',
|
|
187
|
+
'Tencent',
|
|
188
|
+
'Perplexity',
|
|
189
|
+
'Character AI',
|
|
190
|
+
'Stability AI',
|
|
191
|
+
'Midjourney',
|
|
192
|
+
'ElevenLabs',
|
|
193
|
+
'Hugging Face',
|
|
194
|
+
'Scale AI',
|
|
195
|
+
'AI21',
|
|
196
|
+
'Nous Research',
|
|
197
|
+
'Together AI',
|
|
198
|
+
'Groq',
|
|
199
|
+
'Cerebras',
|
|
200
|
+
'Ollama',
|
|
201
|
+
'LangChain',
|
|
202
|
+
'LlamaIndex',
|
|
203
|
+
'CrewAI',
|
|
204
|
+
'AutoGPT',
|
|
205
|
+
'Mastra',
|
|
206
|
+
'OpenClaw',
|
|
207
|
+
// Coding agents and developer tools
|
|
208
|
+
'Cursor',
|
|
209
|
+
'Anysphere',
|
|
210
|
+
'Windsurf',
|
|
211
|
+
'Codeium',
|
|
212
|
+
'Replit',
|
|
213
|
+
'Lovable',
|
|
214
|
+
'StackBlitz',
|
|
215
|
+
'Vercel',
|
|
216
|
+
'Sourcegraph',
|
|
217
|
+
'Tabnine',
|
|
218
|
+
'JetBrains',
|
|
219
|
+
'GitHub',
|
|
220
|
+
'GitLab',
|
|
221
|
+
'Bitbucket',
|
|
222
|
+
'Atlassian',
|
|
223
|
+
'Notion',
|
|
224
|
+
'Slack',
|
|
225
|
+
'Discord',
|
|
226
|
+
'Figma',
|
|
227
|
+
'Stripe',
|
|
228
|
+
'Supabase',
|
|
229
|
+
'Cloudflare',
|
|
230
|
+
'Netlify',
|
|
231
|
+
'Docker',
|
|
232
|
+
'npm',
|
|
233
|
+
// Platforms and hardware
|
|
234
|
+
'Apple',
|
|
235
|
+
'Amazon',
|
|
236
|
+
'AWS',
|
|
237
|
+
'Nvidia',
|
|
238
|
+
'AMD',
|
|
239
|
+
'Intel',
|
|
240
|
+
'IBM',
|
|
241
|
+
'Oracle',
|
|
242
|
+
'Salesforce',
|
|
243
|
+
'Samsung',
|
|
244
|
+
'Adobe',
|
|
245
|
+
'Netflix',
|
|
246
|
+
'Uber',
|
|
247
|
+
'Palantir',
|
|
248
|
+
'Databricks',
|
|
249
|
+
'Snowflake',
|
|
250
|
+
'Zapier',
|
|
251
|
+
];
|
|
252
|
+
// Suffixes an impersonator adds to a protected name. Stripped one at a time
|
|
253
|
+
// from the end, and the rest compared again.
|
|
254
|
+
export const PROTECTED_SUFFIXES = [
|
|
255
|
+
'inc',
|
|
256
|
+
'labs',
|
|
257
|
+
'ai',
|
|
258
|
+
'hq',
|
|
259
|
+
'official',
|
|
260
|
+
'team',
|
|
261
|
+
];
|
|
262
|
+
// Prefixes an impersonator puts in front, as in the-real-openai. Stripped
|
|
263
|
+
// from the start the same way.
|
|
264
|
+
export const PROTECTED_PREFIXES = [
|
|
265
|
+
'official',
|
|
266
|
+
'real',
|
|
267
|
+
'the',
|
|
268
|
+
'team',
|
|
269
|
+
];
|
|
270
|
+
const PROTECTED_KEYS = new Map(PROTECTED_NAMES.map((name) => [normaliseName(name), name]));
|
|
271
|
+
const SUFFIX_KEYS = PROTECTED_SUFFIXES.map(normaliseName);
|
|
272
|
+
const PREFIX_KEYS = PROTECTED_PREFIXES.map(normaliseName);
|
|
273
|
+
// Every form of a key a protected name could hide in. The key itself, then
|
|
274
|
+
// the key with every combination of prefixes and suffixes stripped. Never
|
|
275
|
+
// empty.
|
|
276
|
+
function keyForms(key) {
|
|
277
|
+
const forms = new Set();
|
|
278
|
+
const queue = [key];
|
|
279
|
+
while (queue.length > 0) {
|
|
280
|
+
const form = queue.pop();
|
|
281
|
+
if (form === '' || forms.has(form))
|
|
282
|
+
continue;
|
|
283
|
+
forms.add(form);
|
|
284
|
+
for (const suffix of SUFFIX_KEYS) {
|
|
285
|
+
if (form.length > suffix.length && form.endsWith(suffix)) {
|
|
286
|
+
queue.push(form.slice(0, -suffix.length));
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
for (const prefix of PREFIX_KEYS) {
|
|
290
|
+
if (form.length > prefix.length && form.startsWith(prefix)) {
|
|
291
|
+
queue.push(form.slice(prefix.length));
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return [...forms];
|
|
296
|
+
}
|
|
297
|
+
// The protected name a text impersonates, as written in PROTECTED_NAMES, or
|
|
298
|
+
// null when it impersonates none.
|
|
299
|
+
export function protectedNameOf(text) {
|
|
300
|
+
for (const form of keyForms(normaliseName(text))) {
|
|
301
|
+
const name = PROTECTED_KEYS.get(form);
|
|
302
|
+
if (name !== undefined)
|
|
303
|
+
return name;
|
|
304
|
+
}
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
// ---------------------------------------------------------------------------
|
|
308
|
+
// Reserved words
|
|
309
|
+
// Route and product words a slug would collide with or read as part of the
|
|
310
|
+
// site. Slugs only, a display name is never part of a URL.
|
|
311
|
+
export const RESERVED_WORDS = [
|
|
312
|
+
'about',
|
|
313
|
+
'account',
|
|
314
|
+
'accounts',
|
|
315
|
+
'admin',
|
|
316
|
+
'administrator',
|
|
317
|
+
'agent',
|
|
318
|
+
'agents',
|
|
319
|
+
'anonymous',
|
|
320
|
+
'api',
|
|
321
|
+
'app',
|
|
322
|
+
'assets',
|
|
323
|
+
'auth',
|
|
324
|
+
'avatar',
|
|
325
|
+
'avatars',
|
|
326
|
+
'badge',
|
|
327
|
+
'badges',
|
|
328
|
+
'blog',
|
|
329
|
+
'callback',
|
|
330
|
+
'check',
|
|
331
|
+
'cli',
|
|
332
|
+
'dashboard',
|
|
333
|
+
'directory',
|
|
334
|
+
'docs',
|
|
335
|
+
'feed',
|
|
336
|
+
'help',
|
|
337
|
+
'home',
|
|
338
|
+
'join',
|
|
339
|
+
'leaderboard',
|
|
340
|
+
'legal',
|
|
341
|
+
'login',
|
|
342
|
+
'logout',
|
|
343
|
+
'me',
|
|
344
|
+
'moderator',
|
|
345
|
+
'new',
|
|
346
|
+
'null',
|
|
347
|
+
'oauth',
|
|
348
|
+
'official',
|
|
349
|
+
'operator',
|
|
350
|
+
'operators',
|
|
351
|
+
'privacy',
|
|
352
|
+
'profile',
|
|
353
|
+
'prove',
|
|
354
|
+
'register',
|
|
355
|
+
'root',
|
|
356
|
+
'search',
|
|
357
|
+
'seal',
|
|
358
|
+
'seals',
|
|
359
|
+
'sealkeeper',
|
|
360
|
+
'security',
|
|
361
|
+
'settings',
|
|
362
|
+
'signin',
|
|
363
|
+
'signout',
|
|
364
|
+
'signup',
|
|
365
|
+
'staff',
|
|
366
|
+
'static',
|
|
367
|
+
'status',
|
|
368
|
+
'support',
|
|
369
|
+
'system',
|
|
370
|
+
'task',
|
|
371
|
+
'tasks',
|
|
372
|
+
'terms',
|
|
373
|
+
'undefined',
|
|
374
|
+
'v1',
|
|
375
|
+
'verify',
|
|
376
|
+
'vouched',
|
|
377
|
+
'well-known',
|
|
378
|
+
'what-is-shared',
|
|
379
|
+
'www',
|
|
380
|
+
];
|
|
381
|
+
// Compared in normalised form, so adm1n and a-p-i are refused as well.
|
|
382
|
+
const RESERVED_KEYS = new Set(RESERVED_WORDS.map(normaliseName));
|
|
383
|
+
export function isReservedWord(text) {
|
|
384
|
+
return RESERVED_KEYS.has(normaliseName(text));
|
|
385
|
+
}
|
|
386
|
+
function protectedRefusal(text) {
|
|
387
|
+
const name = protectedNameOf(text);
|
|
388
|
+
return name === null
|
|
389
|
+
? null
|
|
390
|
+
: {
|
|
391
|
+
code: 'name_protected',
|
|
392
|
+
message: `${text} reads as ${name}, which is a protected name. Pick another.`,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
// The refusal for an operator slug, or null when it may be used. The shape
|
|
396
|
+
// is OperatorSlug's job, this checks reserved words and protected names.
|
|
397
|
+
export function moderateSlug(slug) {
|
|
398
|
+
if (isReservedWord(slug)) {
|
|
399
|
+
return {
|
|
400
|
+
code: 'name_reserved',
|
|
401
|
+
message: `${slug} is reserved by SealKeeper. Pick another.`,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
return protectedRefusal(slug);
|
|
405
|
+
}
|
|
406
|
+
// The refusal for an operator display name, or null when it may be used.
|
|
407
|
+
// The shape is OperatorDisplayName's job. Whether another operator holds
|
|
408
|
+
// the same key is the API's check against display_name_key.
|
|
409
|
+
export function moderateDisplayName(name) {
|
|
410
|
+
return protectedRefusal(name);
|
|
411
|
+
}
|
|
412
|
+
// ---------------------------------------------------------------------------
|
|
413
|
+
// Default slugs
|
|
414
|
+
// A login as a slug. Lowercase, every run of characters other than a to z
|
|
415
|
+
// and 0 to 9 made one hyphen, hyphens trimmed from the ends, since an old
|
|
416
|
+
// GitHub login can end in one. A login is not always letters, digits and
|
|
417
|
+
// hyphens (an Enterprise Managed User login ends in _shortcode), and a
|
|
418
|
+
// slug must be. The same rule as sk_base in migration 0025. The API's
|
|
419
|
+
// default slug for a new operator starts from it.
|
|
420
|
+
export function slugBase(login) {
|
|
421
|
+
const base = login
|
|
422
|
+
.toLowerCase()
|
|
423
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
424
|
+
.replace(/^-|-$/g, '')
|
|
425
|
+
.slice(0, OPERATOR_SLUG_MAX)
|
|
426
|
+
.replace(/-$/, '');
|
|
427
|
+
return base === '' ? 'operator' : base;
|
|
428
|
+
}
|
|
429
|
+
// base with -n on the end, cut so the whole stays within the slug limit.
|
|
430
|
+
export const suffixedSlug = (base, n) => `${base.slice(0, OPERATOR_SLUG_MAX - `-${n}`.length).replace(/-+$/, '')}-${n}`;
|
|
431
|
+
// The reason the slug is the login's slug with a -2, -3 and so on, or null
|
|
432
|
+
// when it is not. A slug the operator chose, or one left behind by a GitHub
|
|
433
|
+
// login change, is not of that form and gives null. The caller also checks
|
|
434
|
+
// that the slug never changed, since an operator can pick a suffixed slug.
|
|
435
|
+
// reserved and protected are read from today's lists, taken is the rest.
|
|
436
|
+
export function slugSuffixReason(login, slug) {
|
|
437
|
+
const base = slugBase(login);
|
|
438
|
+
const n = /-([0-9]+)$/.exec(slug)?.[1];
|
|
439
|
+
if (n === undefined || slug === base)
|
|
440
|
+
return null;
|
|
441
|
+
const k = Number(n);
|
|
442
|
+
if (k < 2 || suffixedSlug(base, k) !== slug)
|
|
443
|
+
return null;
|
|
444
|
+
const refusal = moderateSlug(base);
|
|
445
|
+
if (refusal?.code === 'name_reserved')
|
|
446
|
+
return 'reserved';
|
|
447
|
+
if (refusal?.code === 'name_protected')
|
|
448
|
+
return 'protected';
|
|
449
|
+
return 'taken';
|
|
450
|
+
}
|