borgmcp 2.7.2 → 2.8.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 (63) hide show
  1. package/README.md +96 -230
  2. package/dist/assimilate-cmd.d.ts.map +1 -1
  3. package/dist/assimilate-cmd.js +11 -2
  4. package/dist/assimilate-cmd.js.map +1 -1
  5. package/dist/claude.js +6 -16
  6. package/dist/claude.js.map +1 -1
  7. package/dist/cli-help.d.ts +5 -0
  8. package/dist/cli-help.d.ts.map +1 -1
  9. package/dist/cli-help.js +49 -1
  10. package/dist/cli-help.js.map +1 -1
  11. package/dist/drone-lifecycle.d.ts +8 -0
  12. package/dist/drone-lifecycle.d.ts.map +1 -1
  13. package/dist/drone-lifecycle.js +19 -0
  14. package/dist/drone-lifecycle.js.map +1 -1
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +14 -2
  17. package/dist/index.js.map +1 -1
  18. package/dist/log-stream.d.ts +4 -0
  19. package/dist/log-stream.d.ts.map +1 -1
  20. package/dist/log-stream.js +28 -4
  21. package/dist/log-stream.js.map +1 -1
  22. package/dist/remote-client.d.ts +1 -1
  23. package/dist/remote-client.d.ts.map +1 -1
  24. package/dist/remote-client.js +42 -7
  25. package/dist/remote-client.js.map +1 -1
  26. package/dist/server-errors.d.ts +8 -0
  27. package/dist/server-errors.d.ts.map +1 -1
  28. package/dist/server-errors.js +19 -0
  29. package/dist/server-errors.js.map +1 -1
  30. package/dist/server-facade.d.ts +4 -0
  31. package/dist/server-facade.d.ts.map +1 -1
  32. package/dist/server-facade.js +11 -0
  33. package/dist/server-facade.js.map +1 -1
  34. package/dist/server-handshake.d.ts +1 -1
  35. package/dist/server-trust.d.ts.map +1 -1
  36. package/dist/server-trust.js +10 -0
  37. package/dist/server-trust.js.map +1 -1
  38. package/dist/setup.js +2 -4
  39. package/dist/setup.js.map +1 -1
  40. package/dist/tool-manifest.d.ts.map +1 -1
  41. package/dist/tool-manifest.js +14 -13
  42. package/dist/tool-manifest.js.map +1 -1
  43. package/dist/update-cmd.d.ts.map +1 -1
  44. package/dist/update-cmd.js +122 -16
  45. package/dist/update-cmd.js.map +1 -1
  46. package/docs/EXTRACTION_PROVENANCE.md +7 -7
  47. package/docs/LOCAL_SERVER.md +3 -3
  48. package/docs/RELEASING.md +16 -6
  49. package/package.json +2 -2
  50. package/src/assimilate-cmd.ts +13 -2
  51. package/src/claude.ts +11 -16
  52. package/src/cli-help.ts +65 -1
  53. package/src/drone-lifecycle.ts +25 -0
  54. package/src/index.ts +15 -1
  55. package/src/log-stream.ts +26 -5
  56. package/src/remote-client.ts +56 -5
  57. package/src/server-errors.ts +24 -0
  58. package/src/server-facade.ts +15 -0
  59. package/src/server-handshake.ts +1 -1
  60. package/src/server-trust.ts +11 -0
  61. package/src/setup.ts +2 -4
  62. package/src/tool-manifest.ts +14 -13
  63. package/src/update-cmd.ts +145 -16
@@ -16,6 +16,7 @@ import {
16
16
  import { randomUUID } from 'node:crypto';
17
17
  import {
18
18
  createProtocolEnvelope,
19
+ decodeDeleteCubeResponse,
19
20
  decodeDroneRuntimeMetadataState,
20
21
  decodeEvictDroneResult,
21
22
  decodeProtocolEnvelope,
@@ -23,6 +24,7 @@ import {
23
24
  decodeReassignDroneResult,
24
25
  decodeUpdateDroneRuntimeMetadataResponse,
25
26
  ErrorCode,
27
+ ProtocolContractError,
26
28
  type AgentKind,
27
29
  type EvictDroneResult,
28
30
  type ReassignDroneResult,
@@ -30,7 +32,12 @@ import {
30
32
  import { consolePrefix } from './console-prefix.js';
31
33
  import { debugLog } from './debug.js';
32
34
  import { assertUuidShape } from './evict-drone.js';
33
- import { DroneEvictedError, DRONE_EVICTED_CODE } from './drone-lifecycle.js';
35
+ import {
36
+ CubeDeletedError,
37
+ CUBE_DELETED_CODE,
38
+ DroneEvictedError,
39
+ DRONE_EVICTED_CODE,
40
+ } from './drone-lifecycle.js';
34
41
  import type { MessageTaxonomy, MessageTaxonomyClass } from 'borgmcp-shared/templates';
35
42
  import { getTemplate, type Template, type TemplateRole } from 'borgmcp-shared/templates';
36
43
  import { parseRoleSections } from 'borgmcp-shared/role-section';
@@ -41,8 +48,10 @@ import { loadBorgServerTrust, type ServerFetch } from './server-trust.js';
41
48
  import {
42
49
  BorgServerError,
43
50
  BorgServerHttpError,
51
+ BorgProtocolMismatchError,
44
52
  BorgServerTrustError,
45
53
  BorgServerUnreachableError,
54
+ CubeDeletionConfirmationError,
46
55
  LocalManageCredentialUnavailableError,
47
56
  LocalManageRequiredError,
48
57
  } from './server-errors.js';
@@ -226,6 +235,12 @@ async function decodeLocalProtocolResponse<T>(
226
235
  // CR5: a TYPED transport-timeout verdict (message kept for call-site parity).
227
236
  throw new BorgServerUnreachableError('Local Borg server request timed out');
228
237
  }
238
+ if (
239
+ error instanceof ProtocolContractError &&
240
+ error.code === ErrorCode.UNSUPPORTED_PROTOCOL_VERSION
241
+ ) {
242
+ throw new BorgProtocolMismatchError();
243
+ }
229
244
  throw error;
230
245
  } finally {
231
246
  clearTimeout(timeout);
@@ -329,6 +344,9 @@ async function localManageRequest<T>(
329
344
  }),
330
345
  }), true, decodePayload);
331
346
  } catch (error) {
347
+ if (error instanceof CubeDeletedError) {
348
+ throw new CubeDeletedError(operation.cubeName);
349
+ }
332
350
  if (
333
351
  error instanceof BorgServerHttpError &&
334
352
  error.status === 403 &&
@@ -704,6 +722,7 @@ async function authedFetch(
704
722
  // terminal controls. Decode only the bounded protocol error code for typed
705
723
  // branching; never surface the server-provided message or details.
706
724
  let code: ErrorCode | undefined;
725
+ let protocolMismatch = false;
707
726
  try {
708
727
  const body = await readBoundedResponseBody(
709
728
  response,
@@ -713,7 +732,13 @@ async function authedFetch(
713
732
  const parsed = JSON.parse(body);
714
733
  try {
715
734
  code = decodeProtocolErrorEnvelope(parsed).error.code;
716
- } catch {
735
+ } catch (error) {
736
+ if (
737
+ error instanceof ProtocolContractError &&
738
+ error.code === ErrorCode.UNSUPPORTED_PROTOCOL_VERSION
739
+ ) {
740
+ protocolMismatch = true;
741
+ }
717
742
  if (
718
743
  parsed !== null && typeof parsed === 'object' &&
719
744
  parsed.error !== null && typeof parsed.error === 'object' &&
@@ -738,10 +763,15 @@ async function authedFetch(
738
763
  code = undefined;
739
764
  }
740
765
  debugLog(`✗ ${response.status} ${method} ${path}`);
766
+ if (protocolMismatch) throw new BorgProtocolMismatchError();
741
767
  if (droneSession !== undefined && response.status === 410 && code === DRONE_EVICTED_CODE) {
742
768
  if (localSessionCredentialRef !== undefined) markSeatRejected(localSessionCredentialRef);
743
769
  throw new DroneEvictedError();
744
770
  }
771
+ if (response.status === 410 && code === CUBE_DELETED_CODE) {
772
+ if (localSessionCredentialRef !== undefined) markSeatRejected(localSessionCredentialRef);
773
+ throw new CubeDeletedError();
774
+ }
745
775
  throw new BorgServerHttpError(
746
776
  response.status,
747
777
  `Borg server request failed (HTTP ${response.status})`,
@@ -1316,9 +1346,30 @@ export async function patchTaxonomyClass(
1316
1346
  * Delete a cube. Cascade-deletes all roles, drones, and log entries.
1317
1347
  * Requires a live cube-manage grant on the selected local client.
1318
1348
  */
1319
- export async function deleteCube(cubeId: string): Promise<void> {
1320
- void cubeId;
1321
- localUnsupported('cube deletion');
1349
+ export async function deleteCube(cubeId: string, confirmCubeId: string): Promise<void> {
1350
+ if (confirmCubeId !== cubeId) {
1351
+ throw new CubeDeletionConfirmationError(cubeId, confirmCubeId);
1352
+ }
1353
+ assertUuidShape(cubeId, 'cube_id');
1354
+ const active = await getActiveCube();
1355
+ if (!active?.serverTrustIdentity) throw new Error('Selected Borg server authority state is missing or unreadable');
1356
+ const cubeName = cubeId === active.cubeId ? active.name : cubeId;
1357
+ const result = await localManageRequest(
1358
+ active,
1359
+ `/api/cubes/${cubeId}`,
1360
+ 'DELETE',
1361
+ {
1362
+ operation: `delete cube ${manageCopyValue(cubeName)}`,
1363
+ cubeName,
1364
+ noMutation: 'The cube was not deleted.',
1365
+ },
1366
+ {},
1367
+ decodeDeleteCubeResponse,
1368
+ );
1369
+ if (!result) throw new Error('Local Borg server returned an empty cube deletion response');
1370
+ if (result.cube_id !== cubeId) {
1371
+ throw new Error('Local Borg server returned a deletion response for an unexpected cube');
1372
+ }
1322
1373
  }
1323
1374
 
1324
1375
  /**
@@ -35,6 +35,16 @@ export class BorgServerHttpError extends Error {
35
35
  }
36
36
  }
37
37
 
38
+ export class BorgProtocolMismatchError extends Error {
39
+ constructor() {
40
+ super(
41
+ 'This client and the selected Borg server use different protocol versions. ' +
42
+ 'Update `borgmcp-server` and `borgmcp` to matching releases, server first and then client.',
43
+ );
44
+ this.name = 'BorgProtocolMismatchError';
45
+ }
46
+ }
47
+
38
48
  export class LocalManageRequiredError extends Error {
39
49
  constructor(
40
50
  public readonly operation: string,
@@ -106,6 +116,20 @@ export class CubeCreationConfirmationError extends Error {
106
116
  }
107
117
  }
108
118
 
119
+ export class CubeDeletionConfirmationError extends Error {
120
+ constructor(
121
+ public readonly cubeId: string,
122
+ public readonly confirmCubeId: string | undefined,
123
+ ) {
124
+ const supplied = confirmCubeId === undefined ? '(missing)' : `"${confirmCubeId}"`;
125
+ super(
126
+ `Cube deletion is irreversible. The confirmation cube ID ${supplied} must exactly match ` +
127
+ `the requested cube ID "${cubeId}". No cube was deleted.`,
128
+ );
129
+ this.name = 'CubeDeletionConfirmationError';
130
+ }
131
+ }
132
+
109
133
  export type RepositoryAssociationFailure =
110
134
  | 'repository-already-associated'
111
135
  | 'cube-already-associated'
@@ -12,6 +12,7 @@ export type ParsedServerFacadeArgs =
12
12
  | { kind: 'help' }
13
13
  | { kind: 'cube-init-help' }
14
14
  | { kind: 'cube-init'; args: string[] }
15
+ | { kind: 'command-help'; command: ServerLifecycleCommand }
15
16
  | { kind: 'command'; command: ServerLifecycleCommand; args: string[] }
16
17
  | { kind: 'error'; reason: 'unknown-command'; command: string };
17
18
 
@@ -33,6 +34,9 @@ export function parseServerFacadeArgs(args: readonly string[]): ParsedServerFaca
33
34
  if (!(SERVER_LIFECYCLE_COMMANDS as readonly string[]).includes(command)) {
34
35
  return { kind: 'error', reason: 'unknown-command', command };
35
36
  }
37
+ if (rest.some(isHelpFlag)) {
38
+ return { kind: 'command-help', command: command as ServerLifecycleCommand };
39
+ }
36
40
  return {
37
41
  kind: 'command',
38
42
  command: command as ServerLifecycleCommand,
@@ -163,6 +167,13 @@ export function unknownServerCommandText(command: string): string {
163
167
  );
164
168
  }
165
169
 
170
+ export function serverLifecycleHelpText(command: ServerLifecycleCommand): string {
171
+ return (
172
+ `Usage: borg server ${command} [arguments]\n\n` +
173
+ `Command arguments are server-owned and pass to the verified borg-mcp-server executable when run.\n`
174
+ );
175
+ }
176
+
166
177
  export function missingServerExecutableText(command: ServerLifecycleCommand): string {
167
178
  return (
168
179
  `Local server command is unavailable: borg-mcp-server was not found.\n` +
@@ -256,6 +267,10 @@ export async function runEarlyServerFacade(
256
267
  output.writeStdout(cubeInitHelpText(getPackageVersion()));
257
268
  return 0;
258
269
  }
270
+ if (parsed.kind === 'command-help') {
271
+ output.writeStdout(serverLifecycleHelpText(parsed.command));
272
+ return 0;
273
+ }
259
274
  if (parsed.kind === 'error') {
260
275
  output.writeStderr(unknownServerCommandText(parsed.command));
261
276
  return 1;
@@ -196,7 +196,7 @@ export interface ServerAttachResult {
196
196
  }
197
197
 
198
198
  /**
199
- * Attach an enrolled client principal to one granted cube/role over protocol v6.
199
+ * Attach an enrolled client principal to one granted cube/role over protocol v7.
200
200
  * The client CSPRNG-generates the session bearer and persists it PENDING in the
201
201
  * OS keychain (keyed by the stable per-seat identity) BEFORE this request, so an
202
202
  * interrupted/lost response is recovered by re-sending the exact same bearer —
@@ -1,3 +1,4 @@
1
+ import { Buffer } from 'node:buffer';
1
2
  import { createHash, timingSafeEqual, X509Certificate } from 'node:crypto';
2
3
  import { constants } from 'node:fs';
3
4
  import { open } from 'node:fs/promises';
@@ -157,6 +158,16 @@ export function createPinnedServerFetch(origin: string, caCertificate: string):
157
158
 
158
159
  const body = requestBody(init.body);
159
160
  const headers = new Headers(init.headers);
161
+ if (body !== undefined) {
162
+ const contentLength = String(
163
+ typeof body === 'string' ? Buffer.byteLength(body, 'utf8') : body.byteLength,
164
+ );
165
+ const declaredLength = headers.get('Content-Length');
166
+ if (declaredLength !== null && declaredLength !== contentLength) {
167
+ throw new Error('Borg server transport refused an inconsistent Content-Length');
168
+ }
169
+ if (declaredLength === null) headers.set('Content-Length', contentLength);
170
+ }
160
171
  return await new Promise<Response>((resolvePromise, rejectPromise) => {
161
172
  const request = httpsRequest({
162
173
  protocol: 'https:',
package/src/setup.ts CHANGED
@@ -38,6 +38,7 @@ import { handleVersionFlag } from './version.js';
38
38
  import { initDebugFromArgv } from './debug.js';
39
39
  import { defaultApprovalIo, setupApprovalWarnings } from './cli-tool-approval.js';
40
40
  import { offerFirstRunServerInstall } from './first-run-server.js';
41
+ import { setupNextStepsText } from './cli-help.js';
41
42
 
42
43
  /**
43
44
  * Main setup wizard
@@ -184,10 +185,7 @@ async function main() {
184
185
  // Success message
185
186
  console.log(chalk.green.bold('\nSetup complete!\n'));
186
187
  console.log(chalk.yellow('🔄 Restart Claude Code / Codex / OpenCode (or open a new session) for the changes to take effect.\n'));
187
- console.log(chalk.gray('◼ Next steps:'));
188
- console.log(chalk.gray('1. cd into your project, then run "borg assimilate --host <host>" to join a cube'));
189
- console.log(chalk.gray(' (this connects to your local server and launches your agent)'));
190
- console.log(chalk.gray('2. Use `borg assimilate --host <host> --enroll` from the operator terminal to enroll a new client\n'));
188
+ console.log(chalk.gray(setupNextStepsText()));
191
189
  }
192
190
 
193
191
  // Run wizard
@@ -58,10 +58,10 @@ export const TOOL_MANIFEST: ToolManifestEntry[] = [
58
58
  {
59
59
  name: 'borg_assimilate',
60
60
  description:
61
- "RE-ATTACH this session to the drone seat already saved for this worktree (gh#780: " +
62
- "this tool never creates seats). Provide the cube's name; on a match it returns the " +
63
- "cube directive, your role's instructions, and recent activity for the EXISTING seat. " +
64
- 'To create a seat or switch cubes, run `borg assimilate` in a terminal instead.',
61
+ "Reconnect this session to the existing drone saved for this worktree. This tool " +
62
+ "never creates drones. Provide the cube's name; on a match it returns the cube " +
63
+ "directive, your role's instructions, and recent activity for that drone. To create " +
64
+ 'a drone or switch cubes, run `borg assimilate` in a terminal instead.',
65
65
  inputSchema: {
66
66
  type: 'object',
67
67
  properties: {
@@ -116,7 +116,7 @@ export const TOOL_MANIFEST: ToolManifestEntry[] = [
116
116
  {
117
117
  name: 'borg_playbook',
118
118
  description:
119
- 'Load the full operating-playbook chapter — the detailed disciplines, rationale, and examples behind the rule-spine in your regen (verification discipline v1/v2/v3, concrete source-of-truth surfaces, four-surface propagation). This detail is kept OUT of the regen bootstrap to keep it light; fetch it ONCE per session when doing review/verify-class work. Static text — do NOT re-fetch on every wake.',
119
+ 'Load the full operating-playbook chapter — the detailed disciplines, rationale, and examples behind the abbreviated session instructions (verification discipline v1/v2/v3, concrete source-of-truth surfaces, four-surface propagation). This detail is omitted from the initial context to keep it light; fetch it ONCE per session when doing review/verify-class work. Static text — do NOT re-fetch on every wake.',
120
120
  inputSchema: {
121
121
  type: 'object',
122
122
  properties: {},
@@ -153,7 +153,7 @@ export const TOOL_MANIFEST: ToolManifestEntry[] = [
153
153
  name: 'borg_role-rationale',
154
154
  description:
155
155
  "Fetch an on-demand rationale/case-study section for a role playbook. " +
156
- "Pass a role name/id and a plain-label section key to read the rationale without expanding every regen.",
156
+ "Pass a role name/id and a plain-label section key to read the rationale without expanding every context refresh.",
157
157
  inputSchema: {
158
158
  type: 'object',
159
159
  properties: {
@@ -249,7 +249,7 @@ export const TOOL_MANIFEST: ToolManifestEntry[] = [
249
249
  {
250
250
  name: 'borg_decide',
251
251
  description:
252
- 'Record a RATIFIED cube decision in the durable decision registry (gh#740) so drones cite it by topic instead of restating from memory. Coordinator/Queen are workflow-eligible to ratify, but their labels grant no server permission; the selected local client must have a live cube-manage grant. Recording IS the ratification act; a decision is not ratified until it is in the registry. Topic-keyed: recording a new decision on an existing topic supersedes the prior (one active per topic). Surfaces in borg_regen + borg_decisions.',
252
+ 'Record a RATIFIED cube decision in the durable decision registry so drones cite it by topic instead of restating from memory. Coordinator and Queen roles are workflow-eligible to ratify, but role labels grant no server permission; the selected local client must have a live cube-manage grant. Recording IS the ratification act; a decision is not ratified until it is in the registry. Topic-keyed: recording a new decision on an existing topic supersedes the prior (one active per topic). The decision appears in borg_regen and borg_decisions.',
253
253
  inputSchema: {
254
254
  type: 'object',
255
255
  required: ['topic', 'decision'],
@@ -272,7 +272,7 @@ export const TOOL_MANIFEST: ToolManifestEntry[] = [
272
272
  {
273
273
  name: 'borg_decisions',
274
274
  description:
275
- 'List the active ratified decisions for the cube (gh#740) — the source of truth to CITE instead of restating a decision from memory. Any member may read. Pass `topic` to fetch one topic\'s active decision; omit for all active decisions.',
275
+ 'List the active ratified decisions for the cube — the source of truth to CITE instead of restating a decision from memory. Any member may read. Pass `topic` to fetch one topic\'s active decision; omit for all active decisions.',
276
276
  inputSchema: {
277
277
  type: 'object',
278
278
  properties: {
@@ -350,7 +350,7 @@ export const TOOL_MANIFEST: ToolManifestEntry[] = [
350
350
  pattern: '^[a-z0-9-]+$',
351
351
  maxLength: 64,
352
352
  },
353
- cube_directive: { type: 'string', description: 'Markdown text every drone in this cube will see in regen. Anything project-specific.' },
353
+ cube_directive: { type: 'string', description: 'Project-specific Markdown shown to every drone when it refreshes cube context.' },
354
354
  template: {
355
355
  type: 'string',
356
356
  description: 'Optional template name to apply after cube creation (e.g. "software-dev"). Roles are merged by name; the default Drone role gets overwritten by the template if a same-named role is in the template.',
@@ -419,13 +419,14 @@ export const TOOL_MANIFEST: ToolManifestEntry[] = [
419
419
  },
420
420
  {
421
421
  name: 'borg_delete-cube',
422
- description: 'Delete a cube and all its roles, drones, and log entries. Irreversible confirm with the user before invoking unless the cube is clearly disposable.',
422
+ description: 'Delete a cube and all its roles, drones, and log entries. Irreversible; requires the exact cube UUID again after explicit user confirmation.',
423
423
  inputSchema: {
424
424
  type: 'object',
425
425
  properties: {
426
426
  cube_id: { type: 'string', description: 'UUID of the cube to delete.' },
427
+ confirm_cube_id: { type: 'string', description: 'Explicit user confirmation: repeat the exact cube UUID to confirm this irreversible deletion.' },
427
428
  },
428
- required: ['cube_id'],
429
+ required: ['cube_id', 'confirm_cube_id'],
429
430
  },
430
431
  },
431
432
  {
@@ -439,7 +440,7 @@ export const TOOL_MANIFEST: ToolManifestEntry[] = [
439
440
  short_description: { type: 'string', description: 'One-line summary, shown to every drone in the cube.' },
440
441
  detailed_description: { type: 'string', description: 'Full playbook for drones in this role — workflow, conventions, log signals to post.' },
441
442
  is_default: { type: 'boolean', description: 'If true, new drones assimilating into this cube are assigned this role. Demotes the previous default.' },
442
- is_mandatory: { type: 'boolean', description: 'If true, role-less assimilate fills this unoccupied non-queen role before ordinary worker roles. A mandatory human-seat role is therefore selected first until occupied.' },
443
+ is_mandatory: { type: 'boolean', description: 'If true, role-less assimilation prioritizes this unoccupied role before ordinary worker roles. Platform-wide management roles are never auto-assigned; a mandatory human-operator role is selected first until occupied.' },
443
444
  is_human_seat: { type: 'boolean', description: 'If true, this role represents the cube\'s human-occupied seat (where the human Queen sits directly). The class-hierarchy guard in reassign-drone allows promotion FROM a human-seat role TO the platform Queen role; promotion from non-human-seat roles is rejected.' },
444
445
  can_broadcast: { type: 'boolean', description: 'If true, drones in this role may post broadcast log entries when strict broadcast gating is enabled.' },
445
446
  receives_all_direct: { type: 'boolean', description: 'If true, drones in this role can see direct log entries as observer/audit recipients.' },
@@ -458,7 +459,7 @@ export const TOOL_MANIFEST: ToolManifestEntry[] = [
458
459
  short_description: { type: 'string', description: 'New short description (optional).' },
459
460
  detailed_description: { type: 'string', description: 'New detailed playbook (optional).' },
460
461
  is_default: { type: 'boolean', description: 'Set true to make this the cube\'s default role (optional).' },
461
- is_mandatory: { type: 'boolean', description: 'Set true/false to prioritize this unoccupied non-queen role during role-less assimilate.' },
462
+ is_mandatory: { type: 'boolean', description: 'Set true/false to prioritize this unoccupied role during role-less assimilation. Platform-wide management roles are never auto-assigned.' },
462
463
  is_human_seat: { type: 'boolean', description: 'Set true/false to mark/unmark this as the cube\'s human-occupied seat (the elevation source for the platform Queen role).' },
463
464
  can_broadcast: { type: 'boolean', description: 'Set true/false to allow or deny broadcast log entries when strict broadcast gating is enabled.' },
464
465
  receives_all_direct: { type: 'boolean', description: 'Set true/false to grant or remove observer visibility into direct log entries.' },