synomem 0.3.0 → 0.4.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.
Files changed (84) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +4 -4
  3. package/dist/backend.d.ts.map +1 -1
  4. package/dist/backend.js +8 -2
  5. package/dist/backend.js.map +1 -1
  6. package/dist/cli.d.ts +3 -0
  7. package/dist/cli.d.ts.map +1 -1
  8. package/dist/cli.js +280 -16
  9. package/dist/cli.js.map +1 -1
  10. package/dist/client.d.ts +32 -1
  11. package/dist/client.d.ts.map +1 -1
  12. package/dist/client.js +125 -10
  13. package/dist/client.js.map +1 -1
  14. package/dist/cloud.d.ts +16 -0
  15. package/dist/cloud.d.ts.map +1 -0
  16. package/dist/cloud.js +19 -0
  17. package/dist/cloud.js.map +1 -0
  18. package/dist/config.d.ts.map +1 -1
  19. package/dist/config.js +8 -1
  20. package/dist/config.js.map +1 -1
  21. package/dist/configure.d.ts +55 -0
  22. package/dist/configure.d.ts.map +1 -0
  23. package/dist/configure.js +177 -0
  24. package/dist/configure.js.map +1 -0
  25. package/dist/credentials.d.ts +15 -2
  26. package/dist/credentials.d.ts.map +1 -1
  27. package/dist/credentials.js.map +1 -1
  28. package/dist/import.d.ts +58 -43
  29. package/dist/import.d.ts.map +1 -1
  30. package/dist/index.d.ts +2 -1
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +1 -0
  33. package/dist/index.js.map +1 -1
  34. package/dist/mcp/index.d.ts.map +1 -1
  35. package/dist/mcp/index.js +15 -6
  36. package/dist/mcp/index.js.map +1 -1
  37. package/dist/oauth.d.ts.map +1 -1
  38. package/dist/oauth.js +8 -1
  39. package/dist/oauth.js.map +1 -1
  40. package/dist/projections.js +4 -4
  41. package/dist/projections.js.map +1 -1
  42. package/dist/prompt.d.ts +28 -0
  43. package/dist/prompt.d.ts.map +1 -0
  44. package/dist/prompt.js +72 -0
  45. package/dist/prompt.js.map +1 -0
  46. package/dist/remote.d.ts +4 -0
  47. package/dist/remote.d.ts.map +1 -1
  48. package/dist/remote.js +4 -0
  49. package/dist/remote.js.map +1 -1
  50. package/dist/schemas.d.ts +101 -60
  51. package/dist/schemas.d.ts.map +1 -1
  52. package/dist/schemas.js +57 -5
  53. package/dist/schemas.js.map +1 -1
  54. package/dist/service.d.ts +4 -0
  55. package/dist/service.d.ts.map +1 -1
  56. package/dist/storage.d.ts +14 -1
  57. package/dist/storage.d.ts.map +1 -1
  58. package/dist/storage.js +55 -14
  59. package/dist/storage.js.map +1 -1
  60. package/dist/types.d.ts +24 -2
  61. package/dist/types.d.ts.map +1 -1
  62. package/docs/cli.md +1 -1
  63. package/docs/examples.md +1 -1
  64. package/docs/mcp.md +1 -1
  65. package/docs/skill.md +1 -1
  66. package/docs/storage-format.md +1 -1
  67. package/package.json +8 -8
  68. package/src/backend.ts +8 -2
  69. package/src/cli.ts +377 -19
  70. package/src/client.ts +135 -11
  71. package/src/cloud.ts +19 -0
  72. package/src/config.ts +8 -1
  73. package/src/configure.ts +233 -0
  74. package/src/credentials.ts +17 -2
  75. package/src/index.ts +7 -1
  76. package/src/mcp/index.ts +19 -5
  77. package/src/oauth.ts +8 -1
  78. package/src/projections.ts +4 -4
  79. package/src/prompt.ts +88 -0
  80. package/src/remote.ts +24 -0
  81. package/src/schemas.ts +60 -5
  82. package/src/service.ts +4 -0
  83. package/src/storage.ts +70 -13
  84. package/src/types.ts +24 -2
package/src/prompt.ts ADDED
@@ -0,0 +1,88 @@
1
+ /**
2
+ * A small prompt boundary, injectable so the wizard is testable.
3
+ *
4
+ * Deliberately not a dependency: the wizard needs a select, a line, a masked
5
+ * line and a confirm, and a library for that would be more surface than
6
+ * substance. Everything reads from an injected stream, so tests drive the flow
7
+ * without a terminal.
8
+ */
9
+ import { createInterface } from 'node:readline/promises';
10
+ import type { Readable, Writable } from 'node:stream';
11
+
12
+ export interface PromptIo {
13
+ input: Readable;
14
+ output: Writable;
15
+ /**
16
+ * Whether a person is actually there. A wizard must never wait forever on a
17
+ * pipe, so a non-interactive stream is refused with instructions instead.
18
+ */
19
+ interactive: boolean;
20
+ }
21
+
22
+ export function defaultPromptIo(): PromptIo {
23
+ return {
24
+ input: process.stdin,
25
+ output: process.stdout,
26
+ interactive: Boolean(process.stdin.isTTY && process.stdout.isTTY),
27
+ };
28
+ }
29
+
30
+ async function readLine(io: PromptIo, question: string): Promise<string> {
31
+ const rl = createInterface({ input: io.input, output: io.output, terminal: io.interactive });
32
+ try {
33
+ return (await rl.question(question)).trim();
34
+ } finally {
35
+ rl.close();
36
+ }
37
+ }
38
+
39
+ export async function ask(io: PromptIo, question: string, fallback?: string): Promise<string> {
40
+ const suffix = fallback ? ` [${fallback}]` : '';
41
+ const answer = await readLine(io, `${question}${suffix}: `);
42
+ return answer || fallback || '';
43
+ }
44
+
45
+ export async function confirm(io: PromptIo, question: string): Promise<boolean> {
46
+ const answer = await readLine(io, `${question} [y/N]: `);
47
+ return /^y(es)?$/i.test(answer);
48
+ }
49
+
50
+ /**
51
+ * Reads a secret from a non-interactive stream, or prompts for one.
52
+ *
53
+ * Piping a secret in is the SAFE path and is why this accepts a closed stream
54
+ * rather than refusing it: a token passed as a command-line argument is kept by
55
+ * both the shell history and the process list, so `--access-token-stdin` has to
56
+ * work without a terminal.
57
+ */
58
+ export async function askSecret(io: PromptIo, question: string): Promise<string> {
59
+ if (!io.interactive) {
60
+ const chunks: Buffer[] = [];
61
+ for await (const chunk of io.input) chunks.push(Buffer.from(chunk as Uint8Array));
62
+ return Buffer.concat(chunks).toString('utf8').trim();
63
+ }
64
+ // Readline echoes, so an interactive secret is read the same way and the
65
+ // caller is told not to expect masking rather than being silently exposed.
66
+ io.output.write('The value you type will be visible. Paste it, or pipe it in instead.\n');
67
+ return await readLine(io, `${question}: `);
68
+ }
69
+
70
+ export async function select<T extends string>(
71
+ io: PromptIo,
72
+ question: string,
73
+ choices: Array<{ value: T; label: string; detail?: string }>,
74
+ ): Promise<T> {
75
+ io.output.write(`\n${question}\n\n`);
76
+ choices.forEach((choice, index) => {
77
+ io.output.write(` ${index + 1}) ${choice.label}\n`);
78
+ if (choice.detail) io.output.write(` ${choice.detail}\n`);
79
+ });
80
+ io.output.write('\n');
81
+ for (;;) {
82
+ const answer = await readLine(io, `Choose 1-${choices.length} [1]: `);
83
+ const index = Number(answer || '1');
84
+ const choice = choices[index - 1];
85
+ if (choice) return choice.value;
86
+ io.output.write('Enter one of the listed numbers.\n');
87
+ }
88
+ }
package/src/remote.ts CHANGED
@@ -179,6 +179,30 @@ export class RemoteSynomemService implements SynomemService {
179
179
  'GET',
180
180
  `agents/resolve?query=${encodeURIComponent(query)}`,
181
181
  ),
182
+ archive: (idOrAlias: string) =>
183
+ this.mutation<Awaited<ReturnType<SynomemService['agents']['archive']>>>(
184
+ 'POST',
185
+ `agents/${encodeURIComponent(idOrAlias)}/archive`,
186
+ {},
187
+ ),
188
+ restore: (idOrAlias: string) =>
189
+ this.mutation<Awaited<ReturnType<SynomemService['agents']['restore']>>>(
190
+ 'POST',
191
+ `agents/${encodeURIComponent(idOrAlias)}/restore`,
192
+ {},
193
+ ),
194
+ addAliases: (idOrAlias: string, aliases: string[]) =>
195
+ this.mutation<Awaited<ReturnType<SynomemService['agents']['addAliases']>>>(
196
+ 'POST',
197
+ `agents/${encodeURIComponent(idOrAlias)}/aliases`,
198
+ { aliases },
199
+ ),
200
+ removeAliases: (idOrAlias: string, aliases: string[]) =>
201
+ this.mutation<Awaited<ReturnType<SynomemService['agents']['removeAliases']>>>(
202
+ 'POST',
203
+ `agents/${encodeURIComponent(idOrAlias)}/aliases/remove`,
204
+ { aliases },
205
+ ),
182
206
  directory: () =>
183
207
  this.request<Awaited<ReturnType<SynomemService['agents']['directory']>>>(
184
208
  'GET',
package/src/schemas.ts CHANGED
@@ -17,12 +17,34 @@ const reservedIds = new Set([
17
17
  'lpt1',
18
18
  ]);
19
19
 
20
- export const agentIdSchema = z
20
+ /**
21
+ * A handle: the human-friendly name for an agent, unique within its workspace.
22
+ *
23
+ * Mutable, unlike the canonical ID. People and agents type this, so it stays
24
+ * lowercase kebab and refuses the reserved words that would collide with
25
+ * filesystem or route segments.
26
+ */
27
+ export const agentHandleSchema = z
21
28
  .string()
22
29
  .min(1)
23
30
  .max(63)
24
31
  .regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, 'Use lowercase ASCII letters, digits, and hyphens')
25
- .refine((id) => !reservedIds.has(id), 'Reserved agent ID');
32
+ .refine((handle) => !reservedIds.has(handle), 'Reserved agent handle');
33
+
34
+ /** A canonical opaque agent ID: a ULID, uppercase Crockford base32. */
35
+ export const agentUlidSchema = z.string().regex(/^[0-9A-HJKMNP-TV-Z]{26}$/);
36
+
37
+ /**
38
+ * An actor ID as it appears in an event.
39
+ *
40
+ * Accepts a ULID or a handle-shaped name. New agents are created with an opaque
41
+ * ULID so a handle can be renamed without orphaning the events that reference
42
+ * the actor; agents that predate that, and human and system actors, carry a
43
+ * name-shaped ID. Widening rather than replacing keeps append-only history
44
+ * readable — rewriting the actor ID inside stored events to tidy the format
45
+ * would be exactly the rewrite the event log exists to prevent.
46
+ */
47
+ export const agentIdSchema = z.union([agentUlidSchema, agentHandleSchema]);
26
48
 
27
49
  /**
28
50
  * An alias as written, folded to the canonical lowercase form.
@@ -110,16 +132,42 @@ export const evidenceSchema = z
110
132
  });
111
133
 
112
134
  export const profileSchema = z.object({
135
+ /** Canonical, opaque and immutable. Events reference this, never the handle. */
113
136
  id: agentIdSchema,
137
+ handle: agentHandleSchema,
114
138
  displayName: z.string().trim().min(1).max(200),
115
139
  aliases: z.array(agentAliasSchema).max(50).optional(),
116
140
  description: z.string().trim().max(2000).optional(),
141
+ /** Archived agents keep their history and stop being able to act. */
142
+ status: z.enum(['active', 'archived']).default('active'),
117
143
  createdAt: z.string().datetime({ offset: true }),
118
144
  metadata: metadataSchema.optional(),
119
145
  });
120
146
 
121
- export const createAgentSchema = profileSchema.omit({ createdAt: true });
122
- export const updateAgentSchema = createAgentSchema.omit({ id: true }).partial();
147
+ /**
148
+ * Creating an agent names a handle; the canonical ID is generated, never
149
+ * supplied. A caller that could choose the ID could choose one that collides
150
+ * with an archived agent's history.
151
+ */
152
+ export const createAgentSchema = z
153
+ .object({
154
+ handle: agentHandleSchema,
155
+ displayName: z.string().trim().min(1).max(200),
156
+ aliases: z.array(agentAliasSchema).max(50).optional(),
157
+ description: z.string().trim().max(2000).optional(),
158
+ metadata: metadataSchema.optional(),
159
+ })
160
+ .strict();
161
+
162
+ export const updateAgentSchema = z
163
+ .object({
164
+ handle: agentHandleSchema.optional(),
165
+ displayName: z.string().trim().min(1).max(200).optional(),
166
+ aliases: z.array(agentAliasSchema).max(50).optional(),
167
+ description: z.string().trim().max(2000).optional(),
168
+ metadata: metadataSchema.optional(),
169
+ })
170
+ .strict();
123
171
 
124
172
  /**
125
173
  * A runtime binding is a claim about where an agent runs, so the fields stay
@@ -196,7 +244,14 @@ const agentCreatedSchema = baseEventSchema.extend({
196
244
  const agentUpdatedSchema = baseEventSchema.extend({
197
245
  type: z.literal('agent.updated'),
198
246
  agentId: agentIdSchema,
199
- changes: updateAgentSchema,
247
+ /*
248
+ * Archiving is recorded as an update, so `status` belongs in the event even
249
+ * though callers cannot set it through `agents.update` — it moves through
250
+ * `archive` and `restore`, which keep the transition explicit.
251
+ */
252
+ changes: updateAgentSchema.extend({
253
+ status: z.enum(['active', 'archived']).optional(),
254
+ }),
200
255
  });
201
256
 
202
257
  const memoSentSchema = baseEventSchema.extend({
package/src/service.ts CHANGED
@@ -79,6 +79,10 @@ export interface SynomemDomainService {
79
79
  get(idOrAlias: string): Promise<AgentProfile>;
80
80
  list(): Promise<AgentProfile[]>;
81
81
  resolve(query: string): Promise<AgentResolution>;
82
+ archive(idOrAlias: string): Promise<AgentProfile>;
83
+ restore(idOrAlias: string): Promise<AgentProfile>;
84
+ addAliases(idOrAlias: string, aliases: string[]): Promise<AgentProfile>;
85
+ removeAliases(idOrAlias: string, aliases: string[]): Promise<AgentProfile>;
82
86
  directory(): Promise<AgentDirectoryEntry[]>;
83
87
  bindings(idOrAlias: string): Promise<AgentRuntimeBinding[]>;
84
88
  bindRuntime(input: BindRuntimeInput): Promise<AgentRuntimeBinding>;
package/src/storage.ts CHANGED
@@ -255,6 +255,21 @@ CREATE TABLE post_acknowledgments (
255
255
  CREATE INDEX post_acknowledgments_post ON post_acknowledgments(post_id);
256
256
  `;
257
257
 
258
+ const migrationV7 = `
259
+ -- Opaque canonical IDs, with the handle as a separate mutable name.
260
+ --
261
+ -- Existing agents keep their name-shaped ID and take it as their handle too.
262
+ -- Rewriting the actor ID inside stored events to tidy the format would be
263
+ -- exactly the rewrite an append-only log exists to prevent, so history stays
264
+ -- as written and only NEW agents get a generated opaque ID.
265
+ ALTER TABLE agents ADD COLUMN handle TEXT;
266
+ UPDATE agents SET handle = id WHERE handle IS NULL;
267
+ CREATE UNIQUE INDEX agents_handle ON agents(handle);
268
+
269
+ -- Archived agents keep their records and stop being able to act.
270
+ ALTER TABLE agents ADD COLUMN status TEXT NOT NULL DEFAULT 'active';
271
+ `;
272
+
258
273
  const migrationV3 = `
259
274
  DROP TRIGGER IF EXISTS events_append_only_update;
260
275
  DROP TRIGGER IF EXISTS events_append_only_delete;
@@ -430,7 +445,8 @@ export class SynomemStorage implements SynomemRepository {
430
445
 
431
446
  constructor(options: StorageOptions) {
432
447
  this.home = resolve(options.home);
433
- this.storageDirectory = join(this.home, 'synomem');
448
+ // The home is the storage directory; see configLocation in backend.ts.
449
+ this.storageDirectory = this.home;
434
450
  this.databasePath = join(this.storageDirectory, 'synomem.sqlite3');
435
451
  this.configPath = join(this.storageDirectory, 'config.json');
436
452
  this.readOnly = options.readOnly;
@@ -518,7 +534,7 @@ export class SynomemStorage implements SynomemRepository {
518
534
  const version = Number(
519
535
  (db.prepare('PRAGMA user_version').get() as { user_version: number }).user_version,
520
536
  );
521
- if (version > 6) {
537
+ if (version > 7) {
522
538
  throw new SynomemError(
523
539
  'UNSUPPORTED_SCHEMA',
524
540
  `Database schema version ${version} is newer than this package supports.`,
@@ -598,14 +614,26 @@ export class SynomemStorage implements SynomemRepository {
598
614
  db.exec('PRAGMA user_version = 6');
599
615
  });
600
616
  }
617
+ const afterV6 = Number(
618
+ (db.prepare('PRAGMA user_version').get() as { user_version: number }).user_version,
619
+ );
620
+ if (afterV6 === 6) {
621
+ this.transactionSync(() => {
622
+ db.exec(migrationV7);
623
+ db.prepare(
624
+ 'INSERT OR IGNORE INTO schema_migrations(version, applied_at) VALUES (?, ?)',
625
+ ).run(7, new Date().toISOString());
626
+ db.exec('PRAGMA user_version = 7');
627
+ });
628
+ }
601
629
  }
602
630
 
603
631
  private assertSchemaSupported(): void {
604
632
  const version = Number(
605
633
  (this.db().prepare('PRAGMA user_version').get() as { user_version: number }).user_version,
606
634
  );
607
- if (version !== 6) {
608
- if (version >= 1 && version <= 5) {
635
+ if (version !== 7) {
636
+ if (version >= 1 && version <= 6) {
609
637
  throw new SynomemError(
610
638
  'UNSUPPORTED_SCHEMA',
611
639
  `Database schema version ${version} requires migration. Open this home once with readOnly: false, then retry the read-only client.`,
@@ -1530,12 +1558,23 @@ export class SynomemStorage implements SynomemRepository {
1530
1558
  return { schemaVersion, appliedVersions };
1531
1559
  }
1532
1560
 
1561
+ /**
1562
+ * Aliases that also name an agent directly.
1563
+ *
1564
+ * Matches against the HANDLE as well as the canonical ID. With opaque IDs an
1565
+ * alias can no longer accidentally equal one, but it can easily equal another
1566
+ * agent's handle — which is the collision that actually makes a lookup
1567
+ * ambiguous now.
1568
+ */
1533
1569
  aliasIdentityConflicts(): Array<{ alias: string; agentId: string }> {
1534
1570
  return this.db()
1535
1571
  .prepare(
1536
1572
  `SELECT x.alias, x.agent_id AS agentId
1537
- FROM aliases x JOIN agents a ON a.id = x.alias
1538
- ORDER BY x.alias`,
1573
+ FROM aliases x
1574
+ JOIN agents a ON lower(a.id) = x.normalized_alias
1575
+ OR lower(a.handle) = x.normalized_alias
1576
+ WHERE a.id != x.agent_id
1577
+ ORDER BY x.alias`,
1539
1578
  )
1540
1579
  .all() as unknown as Array<{ alias: string; agentId: string }>;
1541
1580
  }
@@ -1727,10 +1766,13 @@ export class SynomemStorage implements SynomemRepository {
1727
1766
  const parsed = profileSchema.parse(profile);
1728
1767
  this.db()
1729
1768
  .prepare(
1730
- 'INSERT INTO agents(id, display_name, profile_json, created_at, updated_at) VALUES (?, ?, ?, ?, ?)',
1769
+ `INSERT INTO agents(id, handle, status, display_name, profile_json, created_at, updated_at)
1770
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
1731
1771
  )
1732
1772
  .run(
1733
1773
  parsed.id,
1774
+ parsed.handle,
1775
+ parsed.status,
1734
1776
  parsed.displayName,
1735
1777
  JSON.stringify(parsed),
1736
1778
  parsed.createdAt,
@@ -1742,8 +1784,18 @@ export class SynomemStorage implements SynomemRepository {
1742
1784
  updateAgent(profile: AgentProfile, updatedAt: string): void {
1743
1785
  const parsed = profileSchema.parse(profile);
1744
1786
  this.db()
1745
- .prepare('UPDATE agents SET display_name = ?, profile_json = ?, updated_at = ? WHERE id = ?')
1746
- .run(parsed.displayName, JSON.stringify(parsed), updatedAt, parsed.id);
1787
+ .prepare(
1788
+ `UPDATE agents SET handle = ?, status = ?, display_name = ?, profile_json = ?,
1789
+ updated_at = ? WHERE id = ?`,
1790
+ )
1791
+ .run(
1792
+ parsed.handle,
1793
+ parsed.status,
1794
+ parsed.displayName,
1795
+ JSON.stringify(parsed),
1796
+ updatedAt,
1797
+ parsed.id,
1798
+ );
1747
1799
  this.db().prepare('DELETE FROM aliases WHERE agent_id = ?').run(parsed.id);
1748
1800
  this.insertAliases(parsed.id, parsed.aliases ?? []);
1749
1801
  }
@@ -1770,10 +1822,10 @@ export class SynomemStorage implements SynomemRepository {
1770
1822
  `SELECT DISTINCT a.profile_json
1771
1823
  FROM agents a
1772
1824
  LEFT JOIN aliases x ON x.agent_id = a.id
1773
- WHERE lower(a.id) = ? OR x.normalized_alias = ?
1825
+ WHERE lower(a.id) = ? OR lower(a.handle) = ? OR x.normalized_alias = ?
1774
1826
  ORDER BY a.id ASC`,
1775
1827
  )
1776
- .all(normalized, normalized) as unknown as ProfileRow[];
1828
+ .all(normalized, normalized, normalized) as unknown as ProfileRow[];
1777
1829
  const candidates = rows.map((row) => profileSchema.parse(JSON.parse(row.profile_json)));
1778
1830
  return candidates.length === 1 ? { match: candidates[0]!, candidates } : { candidates };
1779
1831
  }
@@ -1964,11 +2016,16 @@ export class SynomemStorage implements SynomemRepository {
1964
2016
  });
1965
2017
  }
1966
2018
 
1967
- replaceAgentProjectionManifest(agentId: string, paths: string[], generatedAt: string): void {
2019
+ /**
2020
+ * @param directory the agent's projection directory name, which is its
2021
+ * handle rather than its canonical ID: these rows are keyed by the path on
2022
+ * disk, and projections are named for people to read.
2023
+ */
2024
+ replaceAgentProjectionManifest(directory: string, paths: string[], generatedAt: string): void {
1968
2025
  this.transactionSync(() => {
1969
2026
  this.db()
1970
2027
  .prepare('DELETE FROM projection_manifest WHERE path LIKE ? OR path LIKE ?')
1971
- .run(`${agentId}/%`, `${agentId}\\%`);
2028
+ .run(`${directory}/%`, `${directory}\\%`);
1972
2029
  const insert = this.db().prepare(
1973
2030
  'INSERT INTO projection_manifest(path, generated_at) VALUES (?, ?)',
1974
2031
  );
package/src/types.ts CHANGED
@@ -28,10 +28,22 @@ export interface EvidenceReference {
28
28
  }
29
29
 
30
30
  export interface AgentProfile {
31
+ /**
32
+ * Canonical, opaque, immutable. Every event references this, so it can never
33
+ * change — which is exactly why the handle exists separately.
34
+ */
31
35
  id: string;
36
+ /** The human-friendly name, unique in the workspace and safe to rename. */
37
+ handle: string;
32
38
  displayName: string;
33
39
  aliases?: string[];
34
40
  description?: string;
41
+ /**
42
+ * Archived agents keep their history and stop being able to act. Events
43
+ * reference the actor permanently, so deletion would leave history pointing
44
+ * at nothing.
45
+ */
46
+ status: 'active' | 'archived';
35
47
  createdAt: string;
36
48
  metadata?: Record<string, JsonValue>;
37
49
  }
@@ -684,13 +696,15 @@ export interface BindRuntimeInput {
684
696
  }
685
697
 
686
698
  export interface CreateAgentInput {
687
- id: string;
699
+ /** The handle. The canonical ID is generated, never supplied. */
700
+ handle: string;
688
701
  displayName: string;
689
702
  aliases?: string[];
690
703
  description?: string;
691
704
  metadata?: Record<string, JsonValue>;
692
705
  }
693
706
  export interface UpdateAgentInput {
707
+ handle?: string;
694
708
  displayName?: string;
695
709
  aliases?: string[];
696
710
  description?: string;
@@ -706,7 +720,15 @@ export interface KudosStats {
706
720
  byTag: Record<string, number>;
707
721
  }
708
722
  export interface Diagnostic {
709
- level: 'ok' | 'warning' | 'error';
723
+ /**
724
+ * `skipped` is not a failure.
725
+ *
726
+ * A check the caller lacks permission to run says so and leaves the overall
727
+ * result healthy. Failing the whole diagnostic because an ordinary agent
728
+ * cannot read workspace administration would make `doctor` useless to the
729
+ * callers who need it most.
730
+ */
731
+ level: 'ok' | 'warning' | 'error' | 'skipped';
710
732
  code: string;
711
733
  message: string;
712
734
  path?: string;