@inixiative/signet 0.5.1 → 0.6.1

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/README.md CHANGED
@@ -57,6 +57,12 @@ const signet = await SignetClient.fromFile(credentialFile);
57
57
  await signet.describe();
58
58
  ```
59
59
 
60
+ ## Declared operations
61
+
62
+ An integration whose operations Kingdom executes generically declares them here, one module per resource kind: `threads.ts` for a Foundry's `threads` resource, `archive.ts` for an Archive's `archiveLibrary`. Each declares its operation enum, display names, the input each operation takes, its lens schema and the lens's narrowing. `declaredOperationSchema` and `operationInputSchemas` join them (operation names are unique across contracts), `operationInputKeys(operation)` gives the `access/execute` keys an operation takes besides `resourceId` and `limit`, and `operationInputFromAccess(operation, input)` picks and parses exactly those. Kingdom's catalog, its input check and its dispatch read these, so a new operation is added here first.
63
+
64
+ - **Archive** (`archiveLibrary`): `documents.read`, `documents.search`, `sessions.write`, `sessions.delete`. The lens is `{ documentIds?, fields? }` (`{}` reaches every document) and `narrowsArchiveLens` narrows it. `archiveSourceOperations` (`sessions.write`, `sessions.delete`) are a paired local Archive acting on its own sessions: Kingdom binds them to its `sourceId`, so their grant takes no lens. `archiveLensRefusal(operation, lens)` says why a grant can't carry a lens (search needs content over every document).
65
+
60
66
  ## Agent conversations
61
67
 
62
68
  A conversation wraps a host Foundry's main thread (Kingdom `docs/AGENT-CONVERSATIONS.md`, Foundry `docs/reference/CONVERSATIONS.md`). A member is a Signet on the host Foundry's one `threads` resource; Kingdom admits each call and relays it over the host's Installation socket, and stores no conversation or message.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inixiative/signet",
3
- "version": "0.5.1",
3
+ "version": "0.6.1",
4
4
  "description": "Present Kingdom Signets: the wire contracts, DPoP proofs, device pairing and a renewing client shared by Kingdom, Archive, Foundry and Oracle.",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -35,15 +35,15 @@
35
35
  "devDependencies": {
36
36
  "@asteasolutions/zod-to-openapi": "^9.1.0",
37
37
  "@biomejs/biome": "2.5.2",
38
- "@inixiative/config": "^0.5.116",
39
- "@types/bun": "1.4.2",
38
+ "@inixiative/config": "^0.5.157",
39
+ "@types/bun": "1.4.4",
40
40
  "lefthook": "2.1.9",
41
41
  "typescript": "6.0.3",
42
42
  "zod": "^4.3.6"
43
43
  },
44
- "packageManager": "bun@1.4.2",
44
+ "packageManager": "bun@1.4.4",
45
45
  "engines": {
46
- "bun": ">=1.4.2"
46
+ "bun": ">=1.4.4"
47
47
  },
48
48
  "repository": {
49
49
  "type": "git",
package/src/archive.ts ADDED
@@ -0,0 +1,103 @@
1
+ import { z } from 'zod';
2
+
3
+ /**
4
+ * Archive libraries (Kingdom `docs/kingdom.md`, Archive `docs/KINGDOM.md`). A hosted Archive is an
5
+ * integration with one `archiveLibrary` resource. Kingdom admits each call through a Signet and runs
6
+ * it against that Archive with the integration's credential. The Archive server never sees the lens,
7
+ * so Kingdom applies it.
8
+ */
9
+ export const archiveLibraryResourceKind = 'archiveLibrary';
10
+
11
+ /** An Archive session id: `archiveKey(snapshot)`, the digest of its source and session. */
12
+ export const archiveDocumentIdSchema = z.string().regex(/^[a-f0-9]{64}$/);
13
+ export const archiveFieldSchema = z.enum(['id', 'title', 'content', 'tags', 'createdAt']);
14
+ export type ArchiveField = z.infer<typeof archiveFieldSchema>;
15
+
16
+ /** Which documents and fields of an `archiveLibrary` a grant reaches; `{}` reaches all of them. */
17
+ export const archiveLensSchema = z.strictObject({
18
+ documentIds: z.array(archiveDocumentIdSchema).max(1000).optional(),
19
+ fields: z.array(archiveFieldSchema).max(6).optional(),
20
+ });
21
+ export type ArchiveLens = z.infer<typeof archiveLensSchema>;
22
+
23
+ const subset = <T>(granted: T[] | undefined, requested: T[] | undefined) =>
24
+ !requested || (granted !== undefined && granted.every((item) => requested.includes(item)));
25
+
26
+ /** `granted` reaches no document or field `requested` doesn't. */
27
+ export const narrowsArchiveLens = (granted: ArchiveLens, requested: ArchiveLens): boolean =>
28
+ subset(granted.documentIds, requested.documentIds) && subset(granted.fields, requested.fields);
29
+
30
+ /** The operations an `archiveLibrary` resource carries. */
31
+ export const archiveOperationSchema = z.enum([
32
+ 'documents.read',
33
+ 'documents.search',
34
+ 'sessions.write',
35
+ 'sessions.delete',
36
+ ]);
37
+ export type ArchiveOperation = z.infer<typeof archiveOperationSchema>;
38
+
39
+ export const archiveOperationNames: Record<ArchiveOperation, string> = {
40
+ 'documents.read': 'Read permitted archive documents',
41
+ 'documents.search': 'Search permitted archive documents',
42
+ 'sessions.write': 'Write sessions from a paired local Archive',
43
+ 'sessions.delete': 'Delete sessions a paired local Archive wrote',
44
+ };
45
+
46
+ /**
47
+ * Operations a paired local Archive runs on its own sessions: Kingdom binds each one to the
48
+ * Archive's `sourceId`, so a grant of them takes no lens.
49
+ */
50
+ export const archiveSourceOperations = [
51
+ 'sessions.write',
52
+ 'sessions.delete',
53
+ ] as const satisfies readonly ArchiveOperation[];
54
+ export const isArchiveSourceOperation = (operation: ArchiveOperation) =>
55
+ (archiveSourceOperations as readonly ArchiveOperation[]).includes(operation);
56
+
57
+ const fieldsSchema = z.array(archiveFieldSchema).min(1).max(5);
58
+
59
+ /**
60
+ * The input each Archive operation takes. `access/execute` carries the same fields beside
61
+ * `resourceId` and `limit` (`accessInputSchema`); Kingdom allows exactly these keys per operation.
62
+ */
63
+ export const archiveInputSchemas = {
64
+ /** Without `documentId`, the newest documents the lens reaches, up to `limit`. */
65
+ 'documents.read': z.strictObject({
66
+ documentId: archiveDocumentIdSchema.optional(),
67
+ fields: fieldsSchema.optional(),
68
+ }),
69
+ 'documents.search': z.strictObject({
70
+ query: z.string().max(1000).optional(),
71
+ budget: z.number().int().min(16).max(32768).optional(),
72
+ fields: fieldsSchema.optional(),
73
+ projectId: z.string().min(1).max(256).optional(),
74
+ tag: z.string().min(1).max(120).optional(),
75
+ }),
76
+ /**
77
+ * An Archive snapshot (`@inixiative/archive`), parsed by Kingdom before it is stored. `restore`
78
+ * brings back a session deleted at the hosted Archive on purpose (a routed part sent back), which
79
+ * its tombstone otherwise refuses.
80
+ */
81
+ 'sessions.write': z.strictObject({
82
+ snapshot: z.record(z.string(), z.unknown()),
83
+ previousDigest: z
84
+ .string()
85
+ .regex(/^[a-f0-9]{64}$/)
86
+ .nullable()
87
+ .optional(),
88
+ restore: z.boolean().optional(),
89
+ }),
90
+ 'sessions.delete': z.strictObject({ documentId: archiveDocumentIdSchema }),
91
+ } as const satisfies Record<ArchiveOperation, z.ZodObject>;
92
+
93
+ /** Why a grant of `operation` can't carry `lens`, or null when it can. */
94
+ export const archiveLensRefusal = (operation: ArchiveOperation, lens: ArchiveLens) => {
95
+ if (isArchiveSourceOperation(operation) && Object.keys(lens).length > 0)
96
+ return 'Writing or deleting sessions takes no lens';
97
+ if (
98
+ operation === 'documents.search' &&
99
+ (lens.documentIds || (lens.fields && !lens.fields.includes('content')))
100
+ )
101
+ return 'Search needs a lens that shows content and is not limited to chosen documents';
102
+ return null;
103
+ };
package/src/contracts.ts CHANGED
@@ -1,16 +1,26 @@
1
1
  import { z } from 'zod';
2
+ import {
3
+ archiveDocumentIdSchema,
4
+ archiveFieldSchema,
5
+ archiveInputSchemas,
6
+ archiveLensSchema,
7
+ archiveLibraryResourceKind,
8
+ archiveOperationSchema,
9
+ narrowsArchiveLens,
10
+ } from './archive';
2
11
  import {
3
12
  foundryIdSchema,
4
13
  narrowsThreadsLens,
5
- type ThreadOperation,
6
14
  threadFloorActionSchema,
7
15
  threadIdSchema,
8
16
  threadInputSchemas,
17
+ threadOperationSchema,
9
18
  threadsLensSchema,
10
19
  threadsResourceKind,
11
20
  threadTextLimit,
12
21
  } from './threads';
13
22
 
23
+ export * from './archive';
14
24
  export * from './threads';
15
25
 
16
26
  const token43 = /^[A-Za-z0-9_-]{43}$/;
@@ -19,10 +29,6 @@ export const accessTokenPattern = /^kingdom_[A-Za-z0-9_-]{43}$/;
19
29
  export const renewalCredentialPattern = /^signet_renew_[A-Za-z0-9_-]{43}$/;
20
30
  export const nonceSchema = z.string().regex(token43);
21
31
 
22
- export const archiveDocumentIdSchema = z.string().regex(/^[a-f0-9]{64}$/);
23
- export const archiveFieldSchema = z.enum(['id', 'title', 'content', 'tags', 'createdAt']);
24
- export type ArchiveField = z.infer<typeof archiveFieldSchema>;
25
-
26
32
  export const ownerModelSchema = z.enum([
27
33
  'User',
28
34
  'OrganizationUser',
@@ -50,14 +56,6 @@ export const signetKeySchema = publicClientKeySchema.extend({
50
56
  export type PublicClientKey = z.infer<typeof publicClientKeySchema>;
51
57
  export type SignetKey = z.infer<typeof signetKeySchema>;
52
58
 
53
- /** The resource kind of a hosted Archive library: one per Archive integration. */
54
- export const archiveLibraryResourceKind = 'archiveLibrary';
55
- /** Which documents and fields of an `archiveLibrary` a grant reaches; `{}` reaches all of them. */
56
- export const archiveLensSchema = z.strictObject({
57
- documentIds: z.array(archiveDocumentIdSchema).max(1000).optional(),
58
- fields: z.array(archiveFieldSchema).max(6).optional(),
59
- });
60
- export type ArchiveLens = z.infer<typeof archiveLensSchema>;
61
59
  /** Every other resource kind takes no lens. */
62
60
  export const emptyLensSchema = z.strictObject({});
63
61
 
@@ -78,9 +76,6 @@ export const resourceLensSchema = (kind: string) =>
78
76
  ? threadsLensSchema
79
77
  : emptyLensSchema;
80
78
 
81
- const subset = <T>(granted: T[] | undefined, requested: T[] | undefined) =>
82
- !requested || (granted !== undefined && granted.every((item) => requested.includes(item)));
83
-
84
79
  /**
85
80
  * `granted` reaches nothing `requested` doesn't, for lenses of the same resource. Approving someone
86
81
  * else's request may only narrow; an owner revising their own grant may widen (review U3). A lens of
@@ -89,9 +84,7 @@ const subset = <T>(granted: T[] | undefined, requested: T[] | undefined) =>
89
84
  export const narrowsSignetLens = (granted: SignetLens, requested: SignetLens): boolean => {
90
85
  if ('threads' in granted || 'threads' in requested)
91
86
  return 'threads' in granted && 'threads' in requested && narrowsThreadsLens(granted, requested);
92
- return (
93
- subset(granted.documentIds, requested.documentIds) && subset(granted.fields, requested.fields)
94
- );
87
+ return narrowsArchiveLens(granted, requested);
95
88
  };
96
89
 
97
90
  export const signetResourceSchema = z.strictObject({
@@ -406,6 +399,7 @@ export const accessInputSchema = z.strictObject({
406
399
  .regex(/^[a-f0-9]{64}$/)
407
400
  .nullable()
408
401
  .optional(),
402
+ restore: z.boolean().optional(),
409
403
  threadId: threadIdSchema.optional(),
410
404
  postId: z.uuid().optional(),
411
405
  text: z.string().min(1).max(threadTextLimit).optional(),
@@ -428,20 +422,43 @@ export type AccessRequest = z.infer<typeof accessRequestSchema>;
428
422
  export const accessResultSchema = z.object({ executionId: z.uuid(), result: z.unknown() });
429
423
 
430
424
  /**
431
- * The host-side input of a thread operation, taken from an `access/execute` input: exactly the keys
432
- * that operation takes (no `resourceId`, no `limit`). Kingdom builds `thread.request` frames with it.
425
+ * Every operation a contract in this package declares (`threads.ts`, `archive.ts`), and the input
426
+ * each takes. Kingdom's `access/execute` check, its catalog and its dispatch read these, so a new
427
+ * operation is added here and nowhere else. Operation names are unique across contracts.
428
+ */
429
+ export const declaredOperationSchema = z.enum([
430
+ ...threadOperationSchema.options,
431
+ ...archiveOperationSchema.options,
432
+ ]);
433
+ export type DeclaredOperation = z.infer<typeof declaredOperationSchema>;
434
+ export const operationInputSchemas = {
435
+ ...threadInputSchemas,
436
+ ...archiveInputSchemas,
437
+ } as const satisfies Record<DeclaredOperation, z.ZodObject>;
438
+ export type OperationInput<O extends DeclaredOperation> = z.infer<
439
+ (typeof operationInputSchemas)[O]
440
+ >;
441
+
442
+ /** The `access/execute` input keys a declared operation takes, besides `resourceId` and `limit`. */
443
+ export const operationInputKeys = <O extends DeclaredOperation>(operation: O) =>
444
+ Object.keys(operationInputSchemas[operation].shape) as (keyof OperationInput<O>)[];
445
+
446
+ /**
447
+ * A declared operation's input, taken from an `access/execute` input: exactly the keys that
448
+ * operation takes (no `resourceId`, no `limit`). Kingdom builds `thread.request` frames and Archive
449
+ * calls with it.
433
450
  */
434
- export const threadInputFromAccess = <O extends ThreadOperation>(
451
+ export const operationInputFromAccess = <O extends DeclaredOperation>(
435
452
  operation: O,
436
453
  input: AccessRequest['input'],
437
454
  ) => {
438
- const schema = threadInputSchemas[operation];
455
+ const schema = operationInputSchemas[operation];
439
456
  const picked = Object.fromEntries(
440
457
  Object.keys(schema.shape).flatMap((key) =>
441
458
  key in input ? [[key, input[key as keyof typeof input]]] : [],
442
459
  ),
443
460
  );
444
- return schema.safeParse(picked) as z.ZodSafeParseResult<z.infer<(typeof threadInputSchemas)[O]>>;
461
+ return schema.safeParse(picked) as z.ZodSafeParseResult<OperationInput<O>>;
445
462
  };
446
463
 
447
464
  /** What Kingdom pushes over the Installation socket, and what a poll of both reads assembles. */
package/src/threads.ts CHANGED
@@ -106,13 +106,22 @@ export const threadOperationSchema = z.enum([
106
106
  ]);
107
107
  export type ThreadOperation = z.infer<typeof threadOperationSchema>;
108
108
 
109
+ export const threadOperationNames: Record<ThreadOperation, string> = {
110
+ 'thread.read': "Read a conversation's thread through this member's view",
111
+ 'thread.prompt': 'Post into a conversation',
112
+ 'thread.floor': "Request, release or pass a conversation's floor",
113
+ 'prompt.resolve': "Answer the host's held prompts (owner only)",
114
+ };
115
+
109
116
  /**
110
117
  * A party in a conversation, as Foundry names it (not "participant": that is a Warden contributor).
118
+ * `local` is a member on the host's own machine (its own Codex, a local Herald), joined in-process
119
+ * with no Signet; `signet` is one that reaches the host through Kingdom.
111
120
  * Read paths (entries, views) use plain objects, so a host that adds a field never costs an older
112
121
  * member its entries; lenses and request frames stay strict.
113
122
  */
114
123
  export const conversationMemberSchema = z.object({
115
- kind: z.enum(['owner', 'local-herald', 'signet']),
124
+ kind: z.enum(['owner', 'local', 'signet']),
116
125
  id: z.string().min(1).max(128),
117
126
  });
118
127
  export type ConversationMember = z.infer<typeof conversationMemberSchema>;
@@ -154,10 +163,6 @@ export const threadInputSchemas = {
154
163
  }),
155
164
  }),
156
165
  } as const satisfies Record<ThreadOperation, z.ZodObject>;
157
- /** The `access/execute` input keys each thread operation takes, besides `resourceId`. */
158
- export const threadInputKeys = <O extends ThreadOperation>(operation: O) =>
159
- Object.keys(threadInputSchemas[operation].shape) as (keyof ThreadInput<O>)[];
160
- export type ThreadInput<O extends ThreadOperation> = z.infer<(typeof threadInputSchemas)[O]>;
161
166
 
162
167
  /**
163
168
  * One entry of the main thread's journal as a member sees it: an owner message, a main-agent