@opengeni/contracts 2.2.0-canary.0 → 2.5.0-canary.0

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.
@@ -4,12 +4,40 @@ import { Permission } from "./permissions";
4
4
  export const OrganizationMembershipRole = z.enum(["owner", "admin", "member"]);
5
5
  export type OrganizationMembershipRole = z.infer<typeof OrganizationMembershipRole>;
6
6
 
7
+ export const WorkspaceMemberRole = z.enum(["viewer", "member", "admin", "custom"]);
8
+ export type WorkspaceMemberRole = z.infer<typeof WorkspaceMemberRole>;
9
+
10
+ export const AssignableWorkspaceMemberRole = z.enum(["viewer", "member", "admin"]);
11
+ export type AssignableWorkspaceMemberRole = z.infer<typeof AssignableWorkspaceMemberRole>;
12
+
7
13
  export const OrganizationInvitationStatus = z.enum(["pending", "accepted", "revoked", "expired"]);
8
14
  export type OrganizationInvitationStatus = z.infer<typeof OrganizationInvitationStatus>;
9
15
 
16
+ export const OrganizationUserSetupDeliveryState = z.enum([
17
+ "pending",
18
+ "sent",
19
+ "failed",
20
+ "outcome_unknown",
21
+ "revoked",
22
+ ]);
23
+ export type OrganizationUserSetupDeliveryState = z.infer<typeof OrganizationUserSetupDeliveryState>;
24
+
25
+ export const OrganizationUserSetupDelivery = z.object({
26
+ id: z.string().uuid(),
27
+ state: OrganizationUserSetupDeliveryState,
28
+ attemptCount: z.number().int().nonnegative(),
29
+ revision: z.number().int().positive(),
30
+ errorClass: z.string().min(1).max(64).nullable(),
31
+ retryState: z.enum(["available", "reconciliation_required", "unavailable"]),
32
+ sentAt: z.string().datetime({ offset: true }).nullable(),
33
+ updatedAt: z.string().datetime({ offset: true }),
34
+ });
35
+ export type OrganizationUserSetupDelivery = z.infer<typeof OrganizationUserSetupDelivery>;
36
+
10
37
  export const OrganizationInvitation = z.object({
11
38
  id: z.string().uuid(),
12
39
  organizationId: z.string().uuid(),
40
+ organizationName: z.string().trim().min(1).max(120).nullable().default(null),
13
41
  targetEmail: z.string().email(),
14
42
  targetName: z.string().trim().min(1).max(120).nullable().default(null),
15
43
  initialWorkspaceIds: z.array(z.string().uuid()).max(100).default([]),
@@ -20,6 +48,7 @@ export const OrganizationInvitation = z.object({
20
48
  acceptedMembershipId: z.string().uuid().nullable(),
21
49
  createdAt: z.string().datetime({ offset: true }),
22
50
  updatedAt: z.string().datetime({ offset: true }),
51
+ delivery: OrganizationUserSetupDelivery.nullable().default(null),
23
52
  });
24
53
  export type OrganizationInvitation = z.infer<typeof OrganizationInvitation>;
25
54
 
@@ -40,6 +69,34 @@ export const OrganizationMember = z.object({
40
69
  });
41
70
  export type OrganizationMember = z.infer<typeof OrganizationMember>;
42
71
 
72
+ export const OrganizationAdministrationMemberWorkspaceAccess = z.object({
73
+ workspaceId: z.string().uuid(),
74
+ workspaceName: z.string().min(1).max(255),
75
+ membershipId: z.string().uuid(),
76
+ role: WorkspaceMemberRole,
77
+ updatedAt: z.string().datetime({ offset: true }),
78
+ });
79
+ export type OrganizationAdministrationMemberWorkspaceAccess = z.infer<
80
+ typeof OrganizationAdministrationMemberWorkspaceAccess
81
+ >;
82
+
83
+ /** Safe admin projection. Personal-workspace and personal-resource metadata are absent. */
84
+ export const OrganizationAdministrationMember = z.object({
85
+ id: z.string().uuid(),
86
+ organizationId: z.string().uuid(),
87
+ subjectId: z.string().min(1).max(1024),
88
+ name: z.string().min(1).max(1024).nullable(),
89
+ email: z.string().email().max(320).nullable(),
90
+ role: OrganizationMembershipRole,
91
+ status: z.enum(["provisioning", "active", "suspended", "revoked"]),
92
+ authorizationRevision: z.number().int().positive(),
93
+ sharedWorkspaceAccess: z.array(OrganizationAdministrationMemberWorkspaceAccess).max(500),
94
+ revokedAt: z.string().datetime({ offset: true }).nullable(),
95
+ createdAt: z.string().datetime({ offset: true }),
96
+ updatedAt: z.string().datetime({ offset: true }),
97
+ });
98
+ export type OrganizationAdministrationMember = z.infer<typeof OrganizationAdministrationMember>;
99
+
43
100
  export const OrganizationSummary = z.object({
44
101
  id: z.string().uuid(),
45
102
  name: z.string().trim().min(1).max(120),
@@ -71,12 +128,17 @@ export type UpdateOrganizationPrivateSessionSettingsRequest = z.infer<
71
128
 
72
129
  export const OrganizationWorkspaceAccessMember = z.object({
73
130
  membershipId: z.string().uuid(),
131
+ organizationMembershipId: z.string().uuid().nullable(),
74
132
  subjectId: z.string().min(1).max(1024),
133
+ name: z.string().min(1).max(1024).nullable(),
134
+ email: z.string().email().max(320).nullable(),
75
135
  subjectLabel: z.string().min(1).max(1024).nullable(),
76
136
  principalKind: z.enum(["human", "service"]),
77
- role: z.string().min(1).max(64),
137
+ organizationRole: OrganizationMembershipRole.nullable(),
138
+ role: WorkspaceMemberRole,
78
139
  permissions: z.array(z.string().min(1).max(128)),
79
140
  createdAt: z.string().datetime({ offset: true }),
141
+ updatedAt: z.string().datetime({ offset: true }),
80
142
  });
81
143
  export type OrganizationWorkspaceAccessMember = z.infer<typeof OrganizationWorkspaceAccessMember>;
82
144
 
@@ -90,32 +152,80 @@ export const OrganizationWorkspaceAccess = z.object({
90
152
  });
91
153
  export type OrganizationWorkspaceAccess = z.infer<typeof OrganizationWorkspaceAccess>;
92
154
 
155
+ export const OrganizationWorkspaceRoleDefinition = z.object({
156
+ role: AssignableWorkspaceMemberRole,
157
+ label: z.string().min(1).max(64),
158
+ description: z.string().min(1).max(512),
159
+ permissions: z.array(Permission).min(1),
160
+ });
161
+ export type OrganizationWorkspaceRoleDefinition = z.infer<
162
+ typeof OrganizationWorkspaceRoleDefinition
163
+ >;
164
+
93
165
  export const OrganizationAdministrationOverview = z.object({
94
166
  organization: OrganizationSummary,
167
+ roles: z.array(OrganizationWorkspaceRoleDefinition).length(3),
95
168
  workspaces: z.array(OrganizationWorkspaceAccess).max(500),
96
169
  });
97
170
  export type OrganizationAdministrationOverview = z.infer<typeof OrganizationAdministrationOverview>;
98
171
 
99
- // Organization-control-plane assignment of an existing active organization
100
- // member to a shared workspace. The target is a membership rather than an
101
- // email address: joining the organization and receiving workspace access stay
102
- // two explicit lifecycle steps, and this surface cannot become a user lookup.
103
- export const AddOrganizationWorkspaceMemberRequest = z.object({
104
- organizationMembershipId: z.string().uuid(),
105
- role: z.string().trim().min(1).max(64).optional(),
106
- permissions: z.array(Permission).max(128),
107
- });
108
- export type AddOrganizationWorkspaceMemberRequest = z.infer<
109
- typeof AddOrganizationWorkspaceMemberRequest
172
+ export const CreateOrganizationWorkspaceRequest = z
173
+ .object({
174
+ name: z.string().trim().min(1).max(120),
175
+ operationId: z.string().uuid(),
176
+ })
177
+ .strict();
178
+ export type CreateOrganizationWorkspaceRequest = z.infer<typeof CreateOrganizationWorkspaceRequest>;
179
+
180
+ export const UpdateOrganizationWorkspaceRequest = z
181
+ .object({
182
+ name: z.string().trim().min(1).max(120),
183
+ expectedUpdatedAt: z.string().datetime({ offset: true }),
184
+ operationId: z.string().uuid(),
185
+ })
186
+ .strict();
187
+ export type UpdateOrganizationWorkspaceRequest = z.infer<typeof UpdateOrganizationWorkspaceRequest>;
188
+
189
+ export const PutOrganizationWorkspaceMemberRequest = z.discriminatedUnion("role", [
190
+ z
191
+ .object({
192
+ role: AssignableWorkspaceMemberRole,
193
+ expectedUpdatedAt: z.string().datetime({ offset: true }).nullable(),
194
+ operationId: z.string().uuid(),
195
+ })
196
+ .strict(),
197
+ z
198
+ .object({
199
+ role: z.literal("custom"),
200
+ permissions: z.array(Permission).max(128),
201
+ expectedUpdatedAt: z.string().datetime({ offset: true }).nullable(),
202
+ operationId: z.string().uuid(),
203
+ })
204
+ .strict(),
205
+ ]);
206
+ export type PutOrganizationWorkspaceMemberRequest = z.infer<
207
+ typeof PutOrganizationWorkspaceMemberRequest
110
208
  >;
111
209
 
112
- export const CreateOrganizationWorkspaceRequest = z.object({
113
- name: z.string().trim().min(1).max(120),
114
- slug: z.string().trim().min(1).max(120).nullable().optional(),
115
- agentInstructions: z.string().nullable().optional(),
116
- operationId: z.string().uuid(),
117
- });
118
- export type CreateOrganizationWorkspaceRequest = z.infer<typeof CreateOrganizationWorkspaceRequest>;
210
+ export const RevokeOrganizationWorkspaceMemberRequest = z
211
+ .object({
212
+ expectedUpdatedAt: z.string().datetime({ offset: true }),
213
+ operationId: z.string().uuid(),
214
+ })
215
+ .strict();
216
+ export type RevokeOrganizationWorkspaceMemberRequest = z.infer<
217
+ typeof RevokeOrganizationWorkspaceMemberRequest
218
+ >;
219
+
220
+ export const RevokeOrganizationWorkspaceMemberResponse = z
221
+ .object({
222
+ removed: z.boolean(),
223
+ replay: z.boolean(),
224
+ })
225
+ .strict();
226
+ export type RevokeOrganizationWorkspaceMemberResponse = z.infer<
227
+ typeof RevokeOrganizationWorkspaceMemberResponse
228
+ >;
119
229
 
120
230
  export const CreateOrganizationRequest = z.object({
121
231
  name: z.string().trim().min(1).max(120),
@@ -238,6 +348,96 @@ export type CreateOrganizationInvitationRequest = z.infer<
238
348
  typeof CreateOrganizationInvitationRequest
239
349
  >;
240
350
 
351
+ /**
352
+ * Public, signed-out completion contract for an invitation-bound account setup.
353
+ * The bearer is delivered out of band to the invited email address. It is
354
+ * never persisted in plaintext; `operationId` makes an outcome-unknown retry
355
+ * converge on the first committed result.
356
+ */
357
+ export const CompleteOrganizationUserSetupRequest = z.object({
358
+ token: z.string().min(32).max(2048),
359
+ name: z.string().trim().min(1).max(120),
360
+ password: z.string().min(8).max(128),
361
+ operationId: z.string().uuid(),
362
+ });
363
+ export type CompleteOrganizationUserSetupRequest = z.infer<
364
+ typeof CompleteOrganizationUserSetupRequest
365
+ >;
366
+
367
+ export const CompleteOrganizationUserSetupResponse = z.object({
368
+ status: z.literal("complete"),
369
+ });
370
+ export type CompleteOrganizationUserSetupResponse = z.infer<
371
+ typeof CompleteOrganizationUserSetupResponse
372
+ >;
373
+
374
+ export const PreviewOrganizationUserSetupRequest = z
375
+ .object({ token: z.string().min(32).max(2048) })
376
+ .strict();
377
+ export type PreviewOrganizationUserSetupRequest = z.infer<
378
+ typeof PreviewOrganizationUserSetupRequest
379
+ >;
380
+
381
+ export const OrganizationUserSetupPreviewWorkspace = z.object({
382
+ workspaceId: z.string().uuid(),
383
+ workspaceName: z.string().min(1).max(255),
384
+ role: AssignableWorkspaceMemberRole,
385
+ });
386
+ export type OrganizationUserSetupPreviewWorkspace = z.infer<
387
+ typeof OrganizationUserSetupPreviewWorkspace
388
+ >;
389
+
390
+ const OrganizationUserSetupPreviewUnavailable = z.object({
391
+ state: z.enum(["unavailable", "expired", "revoked", "completed"]),
392
+ });
393
+ const OrganizationUserSetupPreviewPending = z.object({
394
+ state: z.literal("pending"),
395
+ organizationId: z.string().uuid(),
396
+ organizationName: z.string().trim().min(1).max(120),
397
+ targetEmail: z.string().email().max(320),
398
+ targetName: z.string().trim().min(1).max(120).nullable(),
399
+ organizationRole: OrganizationMembershipRole,
400
+ sharedWorkspaceAccess: z.array(OrganizationUserSetupPreviewWorkspace).max(100),
401
+ expiresAt: z.string().datetime({ offset: true }),
402
+ });
403
+ export const OrganizationUserSetupPreview = z.discriminatedUnion("state", [
404
+ OrganizationUserSetupPreviewUnavailable,
405
+ OrganizationUserSetupPreviewPending,
406
+ ]);
407
+ export type OrganizationUserSetupPreview = z.infer<typeof OrganizationUserSetupPreview>;
408
+
409
+ export const RetryOrganizationUserSetupDeliveryRequest = z
410
+ .object({ operationId: z.string().uuid() })
411
+ .strict();
412
+ export type RetryOrganizationUserSetupDeliveryRequest = z.infer<
413
+ typeof RetryOrganizationUserSetupDeliveryRequest
414
+ >;
415
+
416
+ /** Authenticated organization-name-only setup after ordinary account creation. */
417
+ export const CompleteSelfServiceOrganizationSetupRequest = z.object({
418
+ organizationName: z.string().trim().min(1).max(120),
419
+ operationId: z.string().uuid(),
420
+ });
421
+ export type CompleteSelfServiceOrganizationSetupRequest = z.infer<
422
+ typeof CompleteSelfServiceOrganizationSetupRequest
423
+ >;
424
+
425
+ export const CompleteSelfServiceOrganizationSetupResponse = z.object({
426
+ status: z.literal("complete"),
427
+ organizationId: z.string().uuid(),
428
+ personalWorkspaceId: z.string().uuid(),
429
+ });
430
+ export type CompleteSelfServiceOrganizationSetupResponse = z.infer<
431
+ typeof CompleteSelfServiceOrganizationSetupResponse
432
+ >;
433
+
434
+ export const SelfServiceOrganizationOnboardingStatus = z.object({
435
+ state: z.enum(["required", "invitation_pending", "unavailable", "complete"]),
436
+ });
437
+ export type SelfServiceOrganizationOnboardingStatus = z.infer<
438
+ typeof SelfServiceOrganizationOnboardingStatus
439
+ >;
440
+
241
441
  export const AcceptOrganizationInvitationRequest = z.object({
242
442
  expectedRevision: z.number().int().positive(),
243
443
  operationId: z.string().uuid(),
@@ -325,6 +525,9 @@ export type AcceptOrganizationInvitationResponse = z.infer<
325
525
  export const ListOrganizationMembersResponse = z.object({
326
526
  members: z.array(OrganizationMember),
327
527
  });
528
+ export const ListOrganizationAdministrationMembersResponse = z.object({
529
+ members: z.array(OrganizationAdministrationMember).max(1000),
530
+ });
328
531
  export const ListSelfOrganizationMembershipsResponse = z.object({
329
532
  memberships: z.array(OrganizationMember),
330
533
  });
package/src/remember.ts CHANGED
@@ -21,7 +21,7 @@ import { WorkspaceInstructionPolicyTarget } from "./workspace-instruction-polici
21
21
  /**
22
22
  * Explicit user-directed durable write ("remember X for this workspace").
23
23
  *
24
- * The lane is the Company Brain area the content belongs to:
24
+ * The lane is the Agent Knowledge area the content belongs to:
25
25
  * - `preference`: a Ways-of-working preference (how agents should act);
26
26
  * - `instruction_policy`: a mandatory workspace rule ("always"/"never");
27
27
  * - `knowledge`: a company/product/people fact.
@@ -84,7 +84,7 @@ export const RememberRequest = z.discriminatedUnion("lane", [
84
84
  lane: z.literal("instruction_policy"),
85
85
  // A mandatory rule is composed verbatim into the prompt of every session
86
86
  // it applies to, for as long as it stays active, so this is the tightest
87
- // agent budget in the Company Brain.
87
+ // agent budget in Agent Knowledge.
88
88
  content: z
89
89
  .string()
90
90
  .trim()
@@ -145,11 +145,13 @@ export const RememberActivationSummary = z.discriminatedUnion("destination", [
145
145
  destination: z.literal("knowledge"),
146
146
  receiptId: z.string().uuid(),
147
147
  claimId: z.string().uuid(),
148
+ /** The retrievable Memory record materialized from the approved claim. */
149
+ memoryId: z.string().uuid(),
148
150
  approvalReviewId: z.string().uuid(),
149
151
  effectiveAt: z.string().datetime().nullable(),
150
152
  authorityKind: z.literal("human_confirmed"),
151
- /** Knowledge approvals are undone through the Knowledge review lifecycle (`knowledge_correct` or a human revocation). */
152
- undo: z.literal("knowledge_review"),
153
+ /** Correct or archive the materialized record through the Memory lifecycle. */
154
+ undo: z.literal("memory_management"),
153
155
  }),
154
156
  ]);
155
157
  export type RememberActivationSummary = z.infer<typeof RememberActivationSummary>;
@@ -0,0 +1,324 @@
1
+ /** Human-authored session titles keep the existing public API ceiling. */
2
+ export const SESSION_TITLE_MAX_CHARACTERS = 200;
3
+
4
+ /**
5
+ * Automatic titles are intentionally much shorter than the manual rename
6
+ * ceiling. This is measured in grapheme clusters so emoji and combining text
7
+ * are never split into malformed display strings.
8
+ */
9
+ export const AUTOMATIC_SESSION_TITLE_MAX_GRAPHEMES = 80;
10
+
11
+ /**
12
+ * Safe durable title used until semantic generation succeeds. It contains no
13
+ * user prompt bytes, so provider failure/offline operation cannot leak a
14
+ * credential or leave a raw prompt prefix in navigation surfaces.
15
+ */
16
+ export const AUTOMATIC_SESSION_TITLE_FALLBACK = "New conversation";
17
+
18
+ let titleSegmenter: Intl.Segmenter | null | undefined;
19
+
20
+ const KNOWN_SENSITIVE_VALUE_PATTERNS = [
21
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----/iu,
22
+ /\bBearer\s+[A-Za-z0-9._~+/=-]{8,}/iu,
23
+ /\b(?:sk-(?:proj-)?|gh[oprsu]_|github_pat_|glpat-|xox[baprs]-)[A-Za-z0-9_-]{8,}/iu,
24
+ /\bAKIA[0-9A-Z]{16}\b/u,
25
+ /\bAIza[0-9A-Za-z_-]{20,}\b/u,
26
+ /\beyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/u,
27
+ /[?&](?:access_token|api_key|apikey|password|secret|token)=[^\s&#]+/iu,
28
+ ] as const;
29
+
30
+ const URI_SCHEME_CANDIDATE_PATTERN = /\b[a-z][a-z0-9+.-]*:\S+/giu;
31
+ const WINDOWS_DRIVE_PATH_PATTERN = /^[a-z]:[\\/](?![\\/])[^:]*$/iu;
32
+
33
+ const SCHEMELESS_HOST_CANDIDATE_PATTERN =
34
+ /\b(?:www\.)?(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z](?:[a-z0-9-]{0,61}[a-z0-9])?(?::\d{1,5})?(?:[/?#][^\s]*)?/giu;
35
+
36
+ const SCHEMELESS_LOCAL_NETWORK_PATTERNS = [
37
+ /\blocalhost(?:(?::\d{1,5})(?:[/?#][^\s]*)?|[/?#][^\s]*)/iu,
38
+ /\b(?:(?:25[0-5]|2[0-4]\d|1?\d?\d)\.){3}(?:25[0-5]|2[0-4]\d|1?\d?\d)(?:(?::\d{1,5})(?:[/?#][^\s]*)?|[/?#][^\s]*)/u,
39
+ /\[(?=[0-9a-f:.]*:[0-9a-f:.]*\])[0-9a-f:.]+\](?:(?::\d{1,5})(?:[/?#][^\s]*)?|[/?#][^\s]*)/iu,
40
+ ] as const;
41
+
42
+ const FILE_LIKE_HOST_SUFFIXES = new Set([
43
+ "css",
44
+ "html",
45
+ "js",
46
+ "json",
47
+ "jsx",
48
+ "lock",
49
+ "md",
50
+ "sql",
51
+ "toml",
52
+ "ts",
53
+ "tsx",
54
+ "xml",
55
+ "yaml",
56
+ "yml",
57
+ ]);
58
+
59
+ // These authorities are established framework/namespace notation whose exact
60
+ // casing is meaningful. Keep this exception deliberately narrow: generic
61
+ // PascalCase or uppercase authorities remain host-like because an uppercase
62
+ // real domain (for example MICROSOFT.COM/Admin) must not bypass the URL gate.
63
+ const DOTTED_TECHNOLOGY_PATH_AUTHORITIES = new Set([
64
+ "ASP.NET",
65
+ "AWS.SDK",
66
+ "Microsoft.Extensions",
67
+ "System.IO",
68
+ ]);
69
+
70
+ const SECRET_ASSIGNMENT_CANDIDATE_PATTERN =
71
+ /(?:^|[^A-Za-z0-9])(?:(['"])([A-Za-z][A-Za-z0-9_. -]*)\1|([A-Za-z][A-Za-z0-9_.-]*))\s*[=:]\s*[^\s,;]+/gu;
72
+
73
+ const SECRET_LABEL_ASSIGNMENT_PATTERN =
74
+ /\b(?:api[ _-]?key|access[ _-]?token|auth[ _-]?token|credential|credentials|password|passwd|private[ _-]?key|secret|token)\b\s*[=:]\s*[^\s,;]+/iu;
75
+
76
+ const SENSITIVE_ASSIGNMENT_KEY_SUFFIXES = new Set([
77
+ "credential",
78
+ "credentials",
79
+ "password",
80
+ "passwd",
81
+ "secret",
82
+ "token",
83
+ ]);
84
+
85
+ const COMPACT_SENSITIVE_ASSIGNMENT_KEY_SUFFIXES = [
86
+ "apikey",
87
+ "accesskey",
88
+ "accesskeyid",
89
+ "accesstoken",
90
+ "authtoken",
91
+ "credential",
92
+ "credentials",
93
+ "password",
94
+ "passwd",
95
+ "privatekey",
96
+ "secret",
97
+ "secretkey",
98
+ "token",
99
+ ] as const;
100
+
101
+ const SENSITIVE_ASSIGNMENT_KEY_WORD_SUFFIXES = [
102
+ ["api", "key"],
103
+ ["access", "key"],
104
+ ["access", "key", "id"],
105
+ ["auth", "key"],
106
+ ["private", "key"],
107
+ ["secret", "key"],
108
+ ] as const;
109
+
110
+ function hasWordSuffix(words: readonly string[], suffix: readonly string[]): boolean {
111
+ if (words.length < suffix.length) return false;
112
+ const offset = words.length - suffix.length;
113
+ return suffix.every((word, index) => words[offset + index] === word);
114
+ }
115
+
116
+ function containsSensitiveAssignment(value: string): boolean {
117
+ for (const match of value.matchAll(SECRET_ASSIGNMENT_CANDIDATE_PATTERN)) {
118
+ const key = match[2] ?? match[3];
119
+ if (!key) continue;
120
+ const words = key
121
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
122
+ .replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2")
123
+ .toLowerCase()
124
+ .split(/[^a-z0-9]+/)
125
+ .filter(Boolean);
126
+ const last = words.at(-1);
127
+ if (last && SENSITIVE_ASSIGNMENT_KEY_SUFFIXES.has(last)) return true;
128
+ if (last && COMPACT_SENSITIVE_ASSIGNMENT_KEY_SUFFIXES.some((suffix) => last.endsWith(suffix))) {
129
+ return true;
130
+ }
131
+ if (SENSITIVE_ASSIGNMENT_KEY_WORD_SUFFIXES.some((suffix) => hasWordSuffix(words, suffix))) {
132
+ return true;
133
+ }
134
+ }
135
+ return false;
136
+ }
137
+
138
+ function containsUriScheme(value: string): boolean {
139
+ for (const match of value.matchAll(URI_SCHEME_CANDIDATE_PATTERN)) {
140
+ // A Windows drive path is the one scheme-shaped token accepted here. Keep
141
+ // the exception exact: one separator after the drive letter and no later
142
+ // colon, so x://host and a nested scheme remain rejected.
143
+ if (!WINDOWS_DRIVE_PATH_PATTERN.test(match[0])) return true;
144
+ }
145
+ return false;
146
+ }
147
+
148
+ function isDottedTechnologyPath(candidate: string): boolean {
149
+ if (/[?:#]/u.test(candidate)) return false;
150
+ const [authority, ...pathSegments] = candidate.split("/");
151
+ if (!authority || pathSegments.length === 0 || pathSegments.some((segment) => !segment)) {
152
+ return false;
153
+ }
154
+ if (
155
+ !DOTTED_TECHNOLOGY_PATH_AUTHORITIES.has(authority) ||
156
+ pathSegments.some((segment) => !/^[A-Z][A-Za-z0-9_-]*$/u.test(segment))
157
+ ) {
158
+ return false;
159
+ }
160
+ return true;
161
+ }
162
+
163
+ function containsSchemelessUrl(value: string): boolean {
164
+ if (SCHEMELESS_LOCAL_NETWORK_PATTERNS.some((pattern) => pattern.test(value))) return true;
165
+
166
+ for (const match of value.matchAll(SCHEMELESS_HOST_CANDIDATE_PATTERN)) {
167
+ const candidate = match[0];
168
+ if (candidate.toLowerCase().startsWith("www.")) return true;
169
+ if (!/[/?#]/u.test(candidate)) continue;
170
+
171
+ const authority = candidate.split(/[/?#]/u, 1)[0] ?? "";
172
+ const hostname = authority.replace(/:\d{1,5}$/u, "");
173
+ const suffix = hostname.split(".").at(-1)?.toLowerCase();
174
+ if (suffix && FILE_LIKE_HOST_SUFFIXES.has(suffix)) continue;
175
+ if (isDottedTechnologyPath(candidate)) continue;
176
+ return true;
177
+ }
178
+ return false;
179
+ }
180
+
181
+ const OPAQUE_IDENTIFIER_PATTERN =
182
+ /\b(?=[A-Za-z0-9_-]{32,}\b)(?=[A-Za-z0-9_-]*[A-Za-z])(?=[A-Za-z0-9_-]*\d)[A-Za-z0-9_-]+\b/u;
183
+
184
+ const DEFAULT_IGNORABLE_CODE_POINTS = /\p{Default_Ignorable_Code_Point}+/gu;
185
+ const ESCAPED_QUOTE_DELIMITERS = /\\+(?=["'])/gu;
186
+
187
+ const TITLE_LABEL_PATTERN =
188
+ /^(?:(?:suggested|generated|concise)\s+)?(?:(?:session|chat|conversation|task)\s+)?title\s*[:\-–—]\s*/iu;
189
+
190
+ const LEADING_BOILERPLATE_PATTERNS = [
191
+ /^(?:hi|hello|hey)(?:\s+there)?\s*[,!:.\-–—]*\s*/iu,
192
+ /^(?:i\s+(?:would\s+like|want|need)\s+you\s+to|i['’]d\s+like\s+you\s+to)\s+/iu,
193
+ /^(?:please\s+)?(?:can|could|would|will)\s+you\s+/iu,
194
+ /^(?:your|the)\s+(?:task|job)\s+is\s+to\s+/iu,
195
+ /^(?:please\s+)?help\s+me\s+(?:to\s+)?/iu,
196
+ /^please(?:\s*[,!:.\-–—]+\s*|\s+|$)/iu,
197
+ ] as const;
198
+
199
+ function automaticTitleDetectionValue(value: string): string {
200
+ // Detection uses a compatibility-normalized shadow value so fullwidth
201
+ // punctuation/letters, invisible token splits, and serialized quote
202
+ // delimiters cannot evade the policy.
203
+ // The accepted title itself stays byte-for-byte in the user's language and
204
+ // emoji form; this shadow is never returned or persisted.
205
+ return value
206
+ .normalize("NFKC")
207
+ .replace(DEFAULT_IGNORABLE_CODE_POINTS, "")
208
+ .replace(ESCAPED_QUOTE_DELIMITERS, "");
209
+ }
210
+
211
+ function hasVisibleAutomaticTitleContent(value: string): boolean {
212
+ return automaticTitleDetectionValue(value).trim().length > 0;
213
+ }
214
+
215
+ function containsSensitiveAutomaticTitleValue(value: string): boolean {
216
+ const detectionValue = automaticTitleDetectionValue(value);
217
+
218
+ if (KNOWN_SENSITIVE_VALUE_PATTERNS.some((pattern) => pattern.test(detectionValue))) return true;
219
+ if (containsUriScheme(detectionValue)) return true;
220
+ if (containsSchemelessUrl(detectionValue)) return true;
221
+ if (SECRET_LABEL_ASSIGNMENT_PATTERN.test(detectionValue)) return true;
222
+ if (containsSensitiveAssignment(detectionValue)) return true;
223
+ if (OPAQUE_IDENTIFIER_PATTERN.test(detectionValue)) return true;
224
+ return false;
225
+ }
226
+
227
+ function stripAutomaticTitleBoilerplate(value: string): string {
228
+ let title = value;
229
+ for (let pass = 0; pass < 4; pass += 1) {
230
+ const before = title;
231
+ title = title
232
+ .replace(/^(?:```[^\n]*|[\s#>*_`"'“”‘’\-–—]+)+/u, "")
233
+ .replace(TITLE_LABEL_PATTERN, "");
234
+ for (const pattern of LEADING_BOILERPLATE_PATTERNS) {
235
+ title = title.replace(pattern, "");
236
+ }
237
+ title = title.trim();
238
+ if (title === before) break;
239
+ }
240
+ return title;
241
+ }
242
+
243
+ function automaticTitleGraphemes(value: string): string[] {
244
+ if (titleSegmenter === undefined) {
245
+ titleSegmenter =
246
+ typeof Intl.Segmenter === "function"
247
+ ? new Intl.Segmenter(undefined, { granularity: "grapheme" })
248
+ : null;
249
+ }
250
+ if (titleSegmenter) {
251
+ return Array.from(titleSegmenter.segment(value), (part) => part.segment);
252
+ }
253
+
254
+ // Older embedded runtimes may not ship Intl.Segmenter. Preserve surrogate
255
+ // pairs plus common combining/emoji sequences instead of failing module load
256
+ // or slicing UTF-16 code units.
257
+ const graphemes: string[] = [];
258
+ for (const point of value) {
259
+ const prior = graphemes.at(-1);
260
+ if (
261
+ prior &&
262
+ (/^[\p{Mark}\u{FE0E}\u{FE0F}\p{Emoji_Modifier}]$/u.test(point) ||
263
+ point === "\u200d" ||
264
+ prior.endsWith("\u200d"))
265
+ ) {
266
+ graphemes[graphemes.length - 1] = `${prior}${point}`;
267
+ } else {
268
+ graphemes.push(point);
269
+ }
270
+ }
271
+ return graphemes;
272
+ }
273
+
274
+ function boundAutomaticTitle(value: string): string {
275
+ const words = value.split(/\s+/u);
276
+ const wordBounded = words.length > 10 ? words.slice(0, 10).join(" ") : value;
277
+ const graphemes = automaticTitleGraphemes(wordBounded);
278
+ if (graphemes.length <= AUTOMATIC_SESSION_TITLE_MAX_GRAPHEMES) return wordBounded;
279
+
280
+ const prefix = graphemes.slice(0, AUTOMATIC_SESSION_TITLE_MAX_GRAPHEMES).join("");
281
+ const lastWhitespace = prefix.search(/\s+\S*$/u);
282
+ return (lastWhitespace >= 16 ? prefix.slice(0, lastWhitespace) : prefix).trimEnd();
283
+ }
284
+
285
+ /**
286
+ * Normalize a model/system-authored title before it reaches durable session
287
+ * metadata. Human renames intentionally do not use this path.
288
+ *
289
+ * Returns null when the candidate is empty, only boilerplate, or appears to
290
+ * contain a credential/opaque prompt value. Callers should retain the current
291
+ * title (normally {@link AUTOMATIC_SESSION_TITLE_FALLBACK}) in that case.
292
+ */
293
+ export function normalizeAutomaticSessionTitle(value: string): string | null {
294
+ const firstLine = value
295
+ .replace(/[\u0000-\u001f\u007f-\u009f]+/gu, "\n")
296
+ .split(/\n+/u)
297
+ .map((line) => line.trim())
298
+ .find(Boolean);
299
+ if (
300
+ !firstLine ||
301
+ !hasVisibleAutomaticTitleContent(firstLine) ||
302
+ containsSensitiveAutomaticTitleValue(firstLine)
303
+ ) {
304
+ return null;
305
+ }
306
+
307
+ let title = stripAutomaticTitleBoilerplate(firstLine)
308
+ .replace(/\s+/gu, " ")
309
+ .replace(/[\s.!?,;:\-–—]+$/u, "")
310
+ .trim();
311
+ if (
312
+ !title ||
313
+ !hasVisibleAutomaticTitleContent(title) ||
314
+ containsSensitiveAutomaticTitleValue(title)
315
+ ) {
316
+ return null;
317
+ }
318
+
319
+ title = boundAutomaticTitle(title)
320
+ .replace(/[\s.!?,;:\-–—]+$/u, "")
321
+ .trim();
322
+ if (!title || !hasVisibleAutomaticTitleContent(title)) return null;
323
+ return title;
324
+ }