borgmcp-shared 0.9.0 → 0.10.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.
@@ -1,7 +1,6 @@
1
1
  export declare enum ErrorCode {
2
2
  AUTH_MISSING = "AUTH_MISSING",
3
3
  AUTH_INVALID = "AUTH_INVALID",
4
- AUTH_EXPIRED = "AUTH_EXPIRED",
5
4
  SUBSCRIPTION_REQUIRED = "SUBSCRIPTION_REQUIRED",
6
5
  ACCESS_DENIED = "ACCESS_DENIED",
7
6
  INVALID_INPUT = "INVALID_INPUT",
@@ -15,6 +14,11 @@ export declare enum ErrorCode {
15
14
  REPOSITORY_ALREADY_ASSOCIATED = "REPOSITORY_ALREADY_ASSOCIATED",
16
15
  CUBE_ALREADY_ASSOCIATED = "CUBE_ALREADY_ASSOCIATED",
17
16
  ROLE_IN_USE = "ROLE_IN_USE",
17
+ DEFAULT_ROLE_REQUIRED = "DEFAULT_ROLE_REQUIRED",
18
+ ROLE_REQUIRED = "ROLE_REQUIRED",
19
+ ROLE_REFERENCED = "ROLE_REFERENCED",
20
+ ROLE_NOT_FOUND = "ROLE_NOT_FOUND",
21
+ ROLE_SECTION_NOT_FOUND = "ROLE_SECTION_NOT_FOUND",
18
22
  ROLE_HAS_FROZEN_DRONES = "ROLE_HAS_FROZEN_DRONES",
19
23
  CUBE_DELETED = "CUBE_DELETED",
20
24
  DRONE_EVICTED = "DRONE_EVICTED",
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/protocol/errors.ts"],"names":[],"mappings":"AACA,oBAAY,SAAS;IACnB,YAAY,iBAAiB;IAC7B,YAAY,iBAAiB;IAC7B,YAAY,iBAAiB;IAC7B,qBAAqB,0BAA0B;IAC/C,aAAa,kBAAkB;IAC/B,aAAa,kBAAkB;IAC/B,iBAAiB,sBAAsB;IACvC,iBAAiB,sBAAsB;IACvC,mBAAmB,wBAAwB;IAC3C,cAAc,mBAAmB;IACjC,cAAc,mBAAmB;IACjC,sBAAsB,2BAA2B;IACjD,SAAS,cAAc;IACvB,6BAA6B,kCAAkC;IAC/D,uBAAuB,4BAA4B;IACnD,WAAW,gBAAgB;IAC3B,sBAAsB,2BAA2B;IACjD,YAAY,iBAAiB;IAC7B,aAAa,kBAAkB;IAC/B,YAAY,iBAAiB;IAC7B,4BAA4B,iCAAiC;IAC7D,cAAc,mBAAmB;IACjC,cAAc,mBAAmB;IACjC,eAAe,oBAAoB;IAQnC,gBAAgB,qBAAqB;CACtC;AAGD,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,SAAS,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/protocol/errors.ts"],"names":[],"mappings":"AACA,oBAAY,SAAS;IACnB,YAAY,iBAAiB;IAC7B,YAAY,iBAAiB;IAC7B,qBAAqB,0BAA0B;IAC/C,aAAa,kBAAkB;IAC/B,aAAa,kBAAkB;IAC/B,iBAAiB,sBAAsB;IACvC,iBAAiB,sBAAsB;IACvC,mBAAmB,wBAAwB;IAC3C,cAAc,mBAAmB;IACjC,cAAc,mBAAmB;IACjC,sBAAsB,2BAA2B;IACjD,SAAS,cAAc;IACvB,6BAA6B,kCAAkC;IAC/D,uBAAuB,4BAA4B;IACnD,WAAW,gBAAgB;IAC3B,qBAAqB,0BAA0B;IAC/C,aAAa,kBAAkB;IAC/B,eAAe,oBAAoB;IACnC,cAAc,mBAAmB;IACjC,sBAAsB,2BAA2B;IACjD,sBAAsB,2BAA2B;IACjD,YAAY,iBAAiB;IAC7B,aAAa,kBAAkB;IAC/B,YAAY,iBAAiB;IAC7B,4BAA4B,iCAAiC;IAC7D,cAAc,mBAAmB;IACjC,cAAc,mBAAmB;IACjC,eAAe,oBAAoB;IAOnC,gBAAgB,qBAAqB;CACtC;AAGD,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,SAAS,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB"}
@@ -2,7 +2,6 @@ export var ErrorCode;
2
2
  (function (ErrorCode) {
3
3
  ErrorCode["AUTH_MISSING"] = "AUTH_MISSING";
4
4
  ErrorCode["AUTH_INVALID"] = "AUTH_INVALID";
5
- ErrorCode["AUTH_EXPIRED"] = "AUTH_EXPIRED";
6
5
  ErrorCode["SUBSCRIPTION_REQUIRED"] = "SUBSCRIPTION_REQUIRED";
7
6
  ErrorCode["ACCESS_DENIED"] = "ACCESS_DENIED";
8
7
  ErrorCode["INVALID_INPUT"] = "INVALID_INPUT";
@@ -16,6 +15,11 @@ export var ErrorCode;
16
15
  ErrorCode["REPOSITORY_ALREADY_ASSOCIATED"] = "REPOSITORY_ALREADY_ASSOCIATED";
17
16
  ErrorCode["CUBE_ALREADY_ASSOCIATED"] = "CUBE_ALREADY_ASSOCIATED";
18
17
  ErrorCode["ROLE_IN_USE"] = "ROLE_IN_USE";
18
+ ErrorCode["DEFAULT_ROLE_REQUIRED"] = "DEFAULT_ROLE_REQUIRED";
19
+ ErrorCode["ROLE_REQUIRED"] = "ROLE_REQUIRED";
20
+ ErrorCode["ROLE_REFERENCED"] = "ROLE_REFERENCED";
21
+ ErrorCode["ROLE_NOT_FOUND"] = "ROLE_NOT_FOUND";
22
+ ErrorCode["ROLE_SECTION_NOT_FOUND"] = "ROLE_SECTION_NOT_FOUND";
19
23
  ErrorCode["ROLE_HAS_FROZEN_DRONES"] = "ROLE_HAS_FROZEN_DRONES";
20
24
  ErrorCode["CUBE_DELETED"] = "CUBE_DELETED";
21
25
  ErrorCode["DRONE_EVICTED"] = "DRONE_EVICTED";
@@ -1 +1 @@
1
- {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/protocol/errors.ts"],"names":[],"mappings":"AACA,MAAM,CAAN,IAAY,SAiCX;AAjCD,WAAY,SAAS;IACnB,0CAA6B,CAAA;IAC7B,0CAA6B,CAAA;IAC7B,0CAA6B,CAAA;IAC7B,4DAA+C,CAAA;IAC/C,4CAA+B,CAAA;IAC/B,4CAA+B,CAAA;IAC/B,oDAAuC,CAAA;IACvC,oDAAuC,CAAA;IACvC,wDAA2C,CAAA;IAC3C,8CAAiC,CAAA;IACjC,8CAAiC,CAAA;IACjC,8DAAiD,CAAA;IACjD,oCAAuB,CAAA;IACvB,4EAA+D,CAAA;IAC/D,gEAAmD,CAAA;IACnD,wCAA2B,CAAA;IAC3B,8DAAiD,CAAA;IACjD,0CAA6B,CAAA;IAC7B,4CAA+B,CAAA;IAC/B,0CAA6B,CAAA;IAC7B,0EAA6D,CAAA;IAC7D,8CAAiC,CAAA;IACjC,8CAAiC,CAAA;IACjC,gDAAmC,CAAA;IAQnC,kDAAqC,CAAA;AACvC,CAAC,EAjCW,SAAS,KAAT,SAAS,QAiCpB"}
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/protocol/errors.ts"],"names":[],"mappings":"AACA,MAAM,CAAN,IAAY,SAoCX;AApCD,WAAY,SAAS;IACnB,0CAA6B,CAAA;IAC7B,0CAA6B,CAAA;IAC7B,4DAA+C,CAAA;IAC/C,4CAA+B,CAAA;IAC/B,4CAA+B,CAAA;IAC/B,oDAAuC,CAAA;IACvC,oDAAuC,CAAA;IACvC,wDAA2C,CAAA;IAC3C,8CAAiC,CAAA;IACjC,8CAAiC,CAAA;IACjC,8DAAiD,CAAA;IACjD,oCAAuB,CAAA;IACvB,4EAA+D,CAAA;IAC/D,gEAAmD,CAAA;IACnD,wCAA2B,CAAA;IAC3B,4DAA+C,CAAA;IAC/C,4CAA+B,CAAA;IAC/B,gDAAmC,CAAA;IACnC,8CAAiC,CAAA;IACjC,8DAAiD,CAAA;IACjD,8DAAiD,CAAA;IACjD,0CAA6B,CAAA;IAC7B,4CAA+B,CAAA;IAC/B,0CAA6B,CAAA;IAC7B,0EAA6D,CAAA;IAC7D,8CAAiC,CAAA;IACjC,8CAAiC,CAAA;IACjC,gDAAmC,CAAA;IAOnC,kDAAqC,CAAA;AACvC,CAAC,EApCW,SAAS,KAAT,SAAS,QAoCpB"}
@@ -1,3 +1,3 @@
1
- export declare const PROTOCOL_VERSION: "7";
1
+ export declare const PROTOCOL_VERSION: "8";
2
2
  export type ProtocolVersion = typeof PROTOCOL_VERSION;
3
3
  //# sourceMappingURL=version.d.ts.map
@@ -1,2 +1,2 @@
1
- export const PROTOCOL_VERSION = '7';
1
+ export const PROTOCOL_VERSION = '8';
2
2
  //# sourceMappingURL=version.js.map
@@ -66,7 +66,7 @@ close. Former authorized parent and drone credentials receive
66
66
  `410 CUBE_DELETED` for that cube, including after authority restart; callers
67
67
  that were never authorized still receive non-enumerating `404 NOT_FOUND`.
68
68
 
69
- The `borgmcp-shared@0.9.0` release candidate retains protocol v7 and carries
69
+ Published `borgmcp-shared@0.9.0` retains protocol v7 and carries
70
70
  the v2 invitation artifact: a canonical opaque token carrying the HTTPS
71
71
  endpoint, CA SPKI SHA-256 pin, enrollment authority, secret, and integrity
72
72
  binding. Decision write requests cap each `decision` and optional `rationale`
@@ -74,6 +74,14 @@ field at 512 UTF-8 bytes independently; response decoders retain longer
74
74
  historical values. The strict codec rejects legacy or non-canonical invitation
75
75
  forms before enrollment dispatch.
76
76
 
77
+ `borgmcp-shared@0.10.0` is published and immutable and carries protocol v8. It adds
78
+ manage-gated `DELETE /api/cubes/:cubeId/roles/:roleId` with integrity refusals
79
+ for active, default, mandatory, human-seat, and taxonomy-referenced roles. It
80
+ also adds the read-authorized `POST /api/cubes/:cubeId/role-rationale` lookup for
81
+ one named, plain-label section of a role description. A v7 peer and a v8 peer
82
+ reject each other during credential-free preflight before either new route is
83
+ dispatched.
84
+
77
85
  Removing or reinterpreting an existing field is a protocol-breaking change even
78
86
  when TypeScript permits it. Implementations must not infer compatibility from a
79
87
  successful build alone — they must run the shared conformance suite against the
@@ -25,11 +25,8 @@ mutation and must never be rerun or moved. `borgmcp-shared@0.6.1`,
25
25
  `fa8a2dc072d4ffe2a16d5f02576fead822a2f72e`; successful attempt-1 workflow run
26
26
  `30169732628` published registry integrity
27
27
  `sha512-Wm4b0uoOAw9JCz5OTHD0Q2uXKkeWYdkVksdeZvRG8l62XGMY+G8GkNEsZT9L533LbVbQ29GhgF0htjDenQThDg==`.
28
- Never rerun or move that tag. `borgmcp-shared@0.7.0` and
29
- `borgmcp-shared@0.7.1` are published and immutable. This source now identifies
30
- the unpublished `0.9.0` protocol-v7 decision-limit release candidate. The
31
- version bump grants no tag or publication authority: creating `v0.9.0` and
32
- publishing the reviewed artifact remain separate, independently gated steps.
28
+ Never rerun or move that tag. `borgmcp-shared@0.7.0`,
29
+ `borgmcp-shared@0.7.1`, and `borgmcp-shared@0.9.0` are published and immutable.
33
30
  There is no compatibility path that returns a bearer from the server.
34
31
 
35
32
  A client verifies the credential-free `GET /api/protocol` tag preflight before
@@ -12,5 +12,33 @@
12
12
  "verify_job_id": null,
13
13
  "publish_job_id": null,
14
14
  "artifact_integrity": "sha512-d4GE7ezGyoFN0uwA62gvRPUMfMdJ1kxhTunqU8dW3Qhy1n2SlNXZWr1Nthm6Goo0bAgvax8KEywyReodA6FyQA=="
15
+ },
16
+ {
17
+ "outcome": "published",
18
+ "version": "0.9.0",
19
+ "tag": "v0.9.0",
20
+ "tag_object": "4e9a1d2d62494efc0e4d9e3f3d7e1f142dac12c7",
21
+ "commit": "f57c9d9fffb8875830f65f12104c661b1e66fba6",
22
+ "tree": "b8e1825accee5bb619fca00e60a080f4ca973990",
23
+ "workflow_run_id": 30924764414,
24
+ "workflow_run_attempt": 1,
25
+ "workflow_conclusion": "success",
26
+ "verify_job_id": null,
27
+ "publish_job_id": null,
28
+ "artifact_integrity": "sha512-bfZPP9JGgBQrCFoZetabqKHc8HLaUqHVR3GJLb/1F1oon7z/B4el4aeBHVvXlxN9+2G7kU/ymPZ/K25nVQapmQ=="
29
+ },
30
+ {
31
+ "outcome": "published",
32
+ "version": "0.10.0",
33
+ "tag": "v0.10.0",
34
+ "tag_object": "1a7639f5c26082ecbe562a4e48d2a10a7aa632b1",
35
+ "commit": "8e5bd088b8069951a687156c35cf68d44985ddc1",
36
+ "tree": "1eda6cf7f378c2fcdf50510ec3b64acd968fff6e",
37
+ "workflow_run_id": 31255563527,
38
+ "workflow_run_attempt": 1,
39
+ "workflow_conclusion": "success",
40
+ "verify_job_id": null,
41
+ "publish_job_id": null,
42
+ "artifact_integrity": "sha512-NYZJi6z0g/Txb6ge+5NgRPNRszVEi0eNmICxkoZq5bGkJWm5qEvNSt3ws90Xz8IOKuWcyHC9i6sAK6cbZxwYDw=="
15
43
  }
16
44
  ]
package/docs/releasing.md CHANGED
@@ -179,12 +179,8 @@ mutation. Never rerun or move that tag. `borgmcp-shared@0.6.1`,
179
179
  `fa8a2dc072d4ffe2a16d5f02576fead822a2f72e`; successful attempt-1 workflow run
180
180
  `30169732628` published registry integrity
181
181
  `sha512-Wm4b0uoOAw9JCz5OTHD0Q2uXKkeWYdkVksdeZvRG8l62XGMY+G8GkNEsZT9L533LbVbQ29GhgF0htjDenQThDg==`.
182
- Never rerun or move that tag. `borgmcp-shared@0.7.0` and
183
- `borgmcp-shared@0.7.1` are published and immutable. This source identifies the
184
- unpublished `0.9.0` protocol-v7 decision-limit release candidate. Its release requires
185
- reviewed source and explicit publication authorization through one protected
186
- publish run whose terminal boundary is successful `npm publish`. The workflow
187
- performs no post-publication registry readback. Consumers must update the
188
- reviewed shared artifact before adopting the matching server and client
189
- releases; incompatible protocol v2, v3, v4, v5, v6, and v7 peers fail closed at
190
- credential-free preflight.
182
+ Never rerun or move that tag. `borgmcp-shared@0.7.0`,
183
+ `borgmcp-shared@0.7.1`, and `borgmcp-shared@0.9.0` are published and immutable.
184
+ Consumers must update the reviewed shared artifact before adopting the matching
185
+ server and client releases; incompatible protocol v2, v3, v4, v5, v6, and v7
186
+ peers fail closed at credential-free preflight.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "borgmcp-shared",
3
- "version": "0.9.0",
3
+ "version": "0.10.1",
4
4
  "description": "Shared protocol, domain, templates, and conformance contracts for Borg MCP implementations.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -13,6 +13,8 @@ import {
13
13
  decodeAttachResponseEnvelope,
14
14
  decodeDroneRuntimeMetadataPatch,
15
15
  decodeEvictDroneResultEnvelope,
16
+ decodeDeleteRoleResultEnvelope,
17
+ decodeRoleRationaleResultEnvelope,
16
18
  decodeProtocolEnvelope,
17
19
  decodeProtocolErrorEnvelope,
18
20
  decodeProtocolTagPreflight,
@@ -24,6 +26,7 @@ import {
24
26
  PROTOCOL_LIMIT_CEILINGS,
25
27
  PROTOCOL_HTTP_CONTRACT,
26
28
  PROTOCOL_VERSION,
29
+ ROLE_IN_USE_DELETE_MESSAGE,
27
30
  utf8ByteLength,
28
31
  type CreateCubeResponse,
29
32
  type CreateCubeRepository,
@@ -67,7 +70,7 @@ export interface ConformanceDroneRuntimeState {
67
70
  readonly metadata_revision: number;
68
71
  readonly cube_id: string;
69
72
  readonly role_id: string;
70
- readonly session_state: 'active' | 'revoked' | 'expired';
73
+ readonly session_state: 'active' | 'revoked';
71
74
  readonly evicted: boolean;
72
75
  readonly last_seen: string;
73
76
  readonly heartbeat_count: number;
@@ -199,7 +202,12 @@ export interface ConformanceAdmin {
199
202
  createRole(cube: ConformanceCube, input: {
200
203
  readonly roleClass: 'queen' | 'worker';
201
204
  readonly isHumanSeat: boolean;
205
+ readonly name?: string;
206
+ readonly detailedDescription?: string;
207
+ readonly isDefault?: boolean;
208
+ readonly isMandatory?: boolean;
202
209
  }): Promise<ConformanceRole>;
210
+ referenceRoleFromTaxonomy(cube: ConformanceCube, role: ConformanceRole): Promise<void>;
203
211
  createDrone(
204
212
  principal: ConformancePrincipal,
205
213
  cube: ConformanceCube,
@@ -207,7 +215,6 @@ export interface ConformanceAdmin {
207
215
  ): Promise<ConformanceDrone>;
208
216
  issueManagedDroneSession(drone: ConformanceDrone): Promise<string>;
209
217
  revokeManagedDroneSession(drone: ConformanceDrone): Promise<void>;
210
- expireManagedDroneSession(drone: ConformanceDrone): Promise<void>;
211
218
  inspectManagedDrone(drone: ConformanceDrone): Promise<{
212
219
  readonly role_id: string;
213
220
  readonly evicted: boolean;
@@ -322,6 +329,17 @@ export interface ConformanceOperations {
322
329
  drone: ConformanceDrone,
323
330
  request: unknown,
324
331
  ): Promise<ConformanceHttpResponse>;
332
+ deleteRole(
333
+ credential: string,
334
+ cube: ConformanceCube,
335
+ role: ConformanceRole,
336
+ request: unknown,
337
+ ): Promise<ConformanceHttpResponse>;
338
+ roleRationale(
339
+ credential: string,
340
+ cube: ConformanceCube,
341
+ request: unknown,
342
+ ): Promise<ConformanceHttpResponse>;
325
343
  openStream(
326
344
  credential: string,
327
345
  cube: ConformanceCube,
@@ -354,7 +372,8 @@ export const ADAPTER_CONFORMANCE_FIXTURES = [
354
372
  { id: 'drones.reassign-invariants', area: 'drones' },
355
373
  { id: 'security.cross-cube-drone-management', area: 'security' },
356
374
  { id: 'drones.evict-terminal-signal', area: 'drones' },
357
- { id: 'security.drone-session-rejection-causes', area: 'security' },
375
+ { id: 'roles.delete-contract', area: 'roles' },
376
+ { id: 'roles.rationale-contract', area: 'roles' },
358
377
  { id: 'metadata.attach-report', area: 'metadata' },
359
378
  { id: 'metadata.self-heal-patch', area: 'metadata' },
360
379
  { id: 'security.metadata-invalid-atomic', area: 'security' },
@@ -408,6 +427,14 @@ function protocolError(response: ConformanceHttpResponse): ErrorCode | null {
408
427
  }
409
428
  }
410
429
 
430
+ function protocolErrorMessage(response: ConformanceHttpResponse): string | null {
431
+ try {
432
+ return decodeProtocolErrorEnvelope(response.body).error.message;
433
+ } catch {
434
+ return null;
435
+ }
436
+ }
437
+
411
438
  function delay(milliseconds: number): Promise<void> {
412
439
  const setTimeoutValue = (globalThis as unknown as {
413
440
  setTimeout?: (callback: () => void, delay: number) => unknown;
@@ -2014,33 +2041,347 @@ export async function runAdapterConformance(
2014
2041
  };
2015
2042
  });
2016
2043
 
2017
- await record('security.drone-session-rejection-causes', async () => {
2018
- const revokedDrone = await environment.admin.createDrone(principalA, cubeA, workerRoleA);
2019
- const expiredDrone = await environment.admin.createDrone(principalA, cubeA, workerRoleA);
2020
- const revokedCredential = await environment.admin.issueManagedDroneSession(revokedDrone);
2021
- const expiredCredential = await environment.admin.issueManagedDroneSession(expiredDrone);
2022
- await environment.admin.revokeManagedDroneSession(revokedDrone);
2023
- await environment.admin.expireManagedDroneSession(expiredDrone);
2024
- for (const [label, credential, code] of [
2025
- ['revoked', revokedCredential, ErrorCode.SESSION_REVOKED],
2026
- ['expired', expiredCredential, ErrorCode.AUTH_EXPIRED],
2027
- ] as const) {
2028
- expectError(
2029
- await environment.operations.read(
2030
- credential,
2031
- cubeA,
2032
- createProtocolEnvelope(`${label}-seat-probe`, { cursor: null, limit: 1 }),
2033
- ),
2034
- 401,
2035
- code,
2036
- `${label} seat probe`,
2044
+ let roleContractCube!: ConformanceCube;
2045
+ let roleContractPrincipal!: ConformancePrincipal;
2046
+ let roleContractCredential = '';
2047
+ let roleContractReadCredential = '';
2048
+ await record('roles.delete-contract', async () => {
2049
+ roleContractPrincipal = await environment.admin.createPrincipal('role-contract-manager');
2050
+ const roleContractReader = await environment.admin.createPrincipal('role-contract-reader');
2051
+ roleContractCube = await environment.admin.createCube('role-contract-cube');
2052
+ await environment.admin.grantCube(roleContractPrincipal, roleContractCube, 'manage');
2053
+ await environment.admin.grantCube(roleContractReader, roleContractCube, 'read');
2054
+ const managerInvitation = await environment.admin.issueSingleUseInvitation(
2055
+ roleContractPrincipal,
2056
+ 'client',
2057
+ );
2058
+ const readerInvitation = await environment.admin.issueSingleUseInvitation(
2059
+ roleContractReader,
2060
+ 'client',
2061
+ );
2062
+ roleContractCredential = `${'D'.repeat(42)}Q`;
2063
+ roleContractReadCredential = `${'L'.repeat(42)}Q`;
2064
+ expectStatus(await environment.operations.enroll(createProtocolEnvelope(
2065
+ 'role-contract-manager-enroll',
2066
+ {
2067
+ invitation: managerInvitation,
2068
+ retry_key: '00000000-0000-4000-8000-000000000351',
2069
+ client_credential: roleContractCredential,
2070
+ },
2071
+ )), 201, 'Role-contract manager enrollment');
2072
+ expectStatus(await environment.operations.enroll(createProtocolEnvelope(
2073
+ 'role-contract-reader-enroll',
2074
+ {
2075
+ invitation: readerInvitation,
2076
+ retry_key: '00000000-0000-4000-8000-000000000352',
2077
+ client_credential: roleContractReadCredential,
2078
+ },
2079
+ )), 201, 'Role-contract reader enrollment');
2080
+
2081
+ const defaultRole = await environment.admin.createRole(roleContractCube, {
2082
+ roleClass: 'worker',
2083
+ isHumanSeat: false,
2084
+ name: 'Default Worker',
2085
+ isDefault: true,
2086
+ });
2087
+ const mandatoryRole = await environment.admin.createRole(roleContractCube, {
2088
+ roleClass: 'worker',
2089
+ isHumanSeat: false,
2090
+ name: 'Mandatory Worker',
2091
+ isMandatory: true,
2092
+ });
2093
+ const humanRole = await environment.admin.createRole(roleContractCube, {
2094
+ roleClass: 'queen',
2095
+ isHumanSeat: true,
2096
+ name: 'Human Seat',
2097
+ });
2098
+ const referencedRole = await environment.admin.createRole(roleContractCube, {
2099
+ roleClass: 'worker',
2100
+ isHumanSeat: false,
2101
+ name: 'Routed Reviewer',
2102
+ });
2103
+ await environment.admin.referenceRoleFromTaxonomy(roleContractCube, referencedRole);
2104
+ const activeRole = await environment.admin.createRole(roleContractCube, {
2105
+ roleClass: 'worker',
2106
+ isHumanSeat: false,
2107
+ name: 'Active Worker',
2108
+ });
2109
+ await environment.admin.createDrone(roleContractPrincipal, roleContractCube, activeRole);
2110
+
2111
+ const refusals = [
2112
+ [defaultRole, ErrorCode.DEFAULT_ROLE_REQUIRED, 'default role'],
2113
+ [mandatoryRole, ErrorCode.ROLE_REQUIRED, 'mandatory role'],
2114
+ [humanRole, ErrorCode.ROLE_REQUIRED, 'human-seat role'],
2115
+ [referencedRole, ErrorCode.ROLE_REFERENCED, 'taxonomy-referenced role'],
2116
+ [activeRole, ErrorCode.ROLE_IN_USE, 'active-drone role'],
2117
+ ] as const;
2118
+ for (const [index, [role, code, label]] of refusals.entries()) {
2119
+ const before = await environment.admin.inspectCubeManagementState(roleContractCube);
2120
+ const response = await environment.operations.deleteRole(
2121
+ roleContractCredential,
2122
+ roleContractCube,
2123
+ role,
2124
+ createProtocolEnvelope(`delete-refusal-${index}`, {}),
2125
+ );
2126
+ expectError(response, 409, code, `Delete ${label}`);
2127
+ invariant(
2128
+ same(await environment.admin.inspectCubeManagementState(roleContractCube), before),
2129
+ `Delete ${label} mutated cube state after refusal.`,
2037
2130
  );
2131
+ if (code === ErrorCode.ROLE_IN_USE) {
2132
+ const message = protocolErrorMessage(response) ?? '';
2133
+ invariant(
2134
+ message === ROLE_IN_USE_DELETE_MESSAGE,
2135
+ 'ROLE_IN_USE did not direct the caller to reassign or evict the drones first.',
2136
+ );
2137
+ }
2038
2138
  }
2139
+
2140
+ const unknownRole = { id: '00000000-0000-4000-8000-000000000398' };
2141
+ expectError(
2142
+ await environment.operations.deleteRole(
2143
+ roleContractCredential,
2144
+ roleContractCube,
2145
+ unknownRole,
2146
+ createProtocolEnvelope('delete-unknown-role', {}),
2147
+ ),
2148
+ 404,
2149
+ ErrorCode.NOT_FOUND,
2150
+ 'Delete unknown role',
2151
+ );
2152
+
2153
+ const deletableRole = await environment.admin.createRole(roleContractCube, {
2154
+ roleClass: 'worker',
2155
+ isHumanSeat: false,
2156
+ name: 'Disposable Worker',
2157
+ });
2158
+ const beforeReadDenial = await environment.admin.inspectCubeManagementState(roleContractCube);
2159
+ expectError(
2160
+ await environment.operations.deleteRole(
2161
+ roleContractReadCredential,
2162
+ roleContractCube,
2163
+ deletableRole,
2164
+ createProtocolEnvelope('delete-role-read-denied', {}),
2165
+ ),
2166
+ 403,
2167
+ ErrorCode.ACCESS_DENIED,
2168
+ 'Delete role with read authority',
2169
+ );
2170
+ invariant(
2171
+ same(
2172
+ await environment.admin.inspectCubeManagementState(roleContractCube),
2173
+ beforeReadDenial,
2174
+ ),
2175
+ 'Read-authority role deletion mutated cube state.',
2176
+ );
2177
+ const deleted = await environment.operations.deleteRole(
2178
+ roleContractCredential,
2179
+ roleContractCube,
2180
+ deletableRole,
2181
+ createProtocolEnvelope('delete-empty-role', {}),
2182
+ );
2183
+ expectStatus(deleted, 200, 'Delete unassigned role');
2184
+ invariant(
2185
+ same(decodeDeleteRoleResultEnvelope(deleted.body).payload, {
2186
+ role_id: deletableRole.id,
2187
+ deleted: true,
2188
+ }),
2189
+ 'Role deletion response did not identify the deleted role.',
2190
+ );
2191
+
2192
+ const evictedRole = await environment.admin.createRole(roleContractCube, {
2193
+ roleClass: 'worker',
2194
+ isHumanSeat: false,
2195
+ name: 'Former Worker',
2196
+ });
2197
+ const evictedDrone = await environment.admin.createDrone(
2198
+ roleContractPrincipal,
2199
+ roleContractCube,
2200
+ evictedRole,
2201
+ );
2202
+ const evictedSession = await environment.admin.issueManagedDroneSession(evictedDrone);
2203
+ const attributed = await environment.operations.append(
2204
+ evictedSession,
2205
+ roleContractCube,
2206
+ createProtocolEnvelope('role-delete-attribution', { message: 'attribution survives' }),
2207
+ );
2208
+ expectStatus(attributed, 201, 'Pre-delete attributed log');
2209
+ const attributedEntry = decodeAppendLogResultEnvelope(attributed.body).payload.entry;
2210
+ expectStatus(await environment.operations.evictDrone(
2211
+ roleContractCredential,
2212
+ roleContractCube,
2213
+ evictedDrone,
2214
+ createProtocolEnvelope('role-delete-evict', {}),
2215
+ ), 200, 'Pre-delete drone eviction');
2216
+ expectStatus(await environment.operations.deleteRole(
2217
+ roleContractCredential,
2218
+ roleContractCube,
2219
+ evictedRole,
2220
+ createProtocolEnvelope('delete-evicted-role', {}),
2221
+ ), 200, 'Delete role held only by an evicted drone');
2222
+ invariant(
2223
+ (await environment.admin.inspectManagedDrone(evictedDrone)).role_id === defaultRole.id,
2224
+ 'Role deletion did not retarget the evicted drone to the surviving default role.',
2225
+ );
2226
+ const read = await environment.operations.read(
2227
+ roleContractCredential,
2228
+ roleContractCube,
2229
+ createProtocolEnvelope('read-role-delete-attribution', { cursor: null, limit: 500 }),
2230
+ );
2231
+ expectStatus(read, 200, 'Post-delete attributed log read');
2232
+ invariant(
2233
+ decodeReadLogResultEnvelope(read.body).payload.entries.some(
2234
+ (entry) => entry.id === attributedEntry.id && entry.drone_id === evictedDrone.id,
2235
+ ),
2236
+ 'Deleting the evicted drone role lost the drone attribution of an existing log entry.',
2237
+ );
2238
+
2239
+ return {
2240
+ success_status: 200,
2241
+ hidden_status: 404,
2242
+ refusal_codes: [
2243
+ ErrorCode.ROLE_IN_USE,
2244
+ ErrorCode.DEFAULT_ROLE_REQUIRED,
2245
+ ErrorCode.ROLE_REQUIRED,
2246
+ ErrorCode.ROLE_REFERENCED,
2247
+ ],
2248
+ in_use_message_actionable: true,
2249
+ evicted_drone_retargeted: true,
2250
+ activity_log_attribution_preserved: true,
2251
+ };
2252
+ });
2253
+
2254
+ await record('roles.rationale-contract', async () => {
2255
+ const detailedDescription = [
2256
+ 'Implements assigned work.',
2257
+ '',
2258
+ 'Workflow rationale:',
2259
+ 'This exact stored section body is returned.',
2260
+ '',
2261
+ 'Boundaries:',
2262
+ 'Do not expand scope.',
2263
+ ].join('\n');
2264
+ const role = await environment.admin.createRole(roleContractCube, {
2265
+ roleClass: 'worker',
2266
+ isHumanSeat: false,
2267
+ name: 'Rationale Builder',
2268
+ detailedDescription,
2269
+ });
2270
+ const expectedSection = {
2271
+ heading: 'Workflow rationale',
2272
+ body: 'Workflow rationale:\nThis exact stored section body is returned.\n\n',
2273
+ };
2274
+ for (const [index, [selector, section]] of [
2275
+ [role.id, 'Workflow rationale'],
2276
+ ['rAtIoNaLe BuIlDeR', 'wOrKfLoW rAtIoNaLe'],
2277
+ ].entries()) {
2278
+ const response = await environment.operations.roleRationale(
2279
+ roleContractReadCredential,
2280
+ roleContractCube,
2281
+ createProtocolEnvelope(`rationale-lookup-${index}`, { role: selector, section }),
2282
+ );
2283
+ expectStatus(response, 200, 'Role rationale lookup');
2284
+ const result = decodeRoleRationaleResultEnvelope(response.body).payload;
2285
+ invariant(
2286
+ result.role_id === role.id &&
2287
+ result.role_name === 'Rationale Builder' &&
2288
+ same(result.section, expectedSection),
2289
+ 'Role rationale lookup did not return the canonical heading and exact stored section body.',
2290
+ );
2291
+ }
2292
+ expectError(
2293
+ await environment.operations.roleRationale(
2294
+ roleContractReadCredential,
2295
+ roleContractCube,
2296
+ createProtocolEnvelope('rationale-unknown-role', {
2297
+ role: 'Unknown Role',
2298
+ section: 'Workflow rationale',
2299
+ }),
2300
+ ),
2301
+ 404,
2302
+ ErrorCode.ROLE_NOT_FOUND,
2303
+ 'Unknown role rationale lookup',
2304
+ );
2305
+ const inaccessibleRole = await environment.admin.createRole(cubeB, {
2306
+ roleClass: 'worker',
2307
+ isHumanSeat: false,
2308
+ name: 'Foreign Rationale Role',
2309
+ detailedDescription,
2310
+ });
2311
+ expectError(
2312
+ await environment.operations.roleRationale(
2313
+ roleContractReadCredential,
2314
+ roleContractCube,
2315
+ createProtocolEnvelope('rationale-inaccessible-role', {
2316
+ role: inaccessibleRole.id,
2317
+ section: 'Workflow rationale',
2318
+ }),
2319
+ ),
2320
+ 404,
2321
+ ErrorCode.ROLE_NOT_FOUND,
2322
+ 'Inaccessible role rationale lookup',
2323
+ );
2324
+ expectError(
2325
+ await environment.operations.roleRationale(
2326
+ roleContractReadCredential,
2327
+ roleContractCube,
2328
+ createProtocolEnvelope('rationale-unknown-section', {
2329
+ role: role.id,
2330
+ section: 'Missing rationale',
2331
+ }),
2332
+ ),
2333
+ 404,
2334
+ ErrorCode.ROLE_SECTION_NOT_FOUND,
2335
+ 'Unknown role section lookup',
2336
+ );
2337
+ await environment.admin.createRole(roleContractCube, {
2338
+ roleClass: 'worker',
2339
+ isHumanSeat: false,
2340
+ name: 'Ambiguous Role',
2341
+ detailedDescription,
2342
+ });
2343
+ await environment.admin.createRole(roleContractCube, {
2344
+ roleClass: 'worker',
2345
+ isHumanSeat: false,
2346
+ name: 'AMBIGUOUS ROLE',
2347
+ detailedDescription,
2348
+ });
2349
+ expectError(
2350
+ await environment.operations.roleRationale(
2351
+ roleContractReadCredential,
2352
+ roleContractCube,
2353
+ createProtocolEnvelope('rationale-ambiguous-role', {
2354
+ role: 'ambiguous role',
2355
+ section: 'Workflow rationale',
2356
+ }),
2357
+ ),
2358
+ 400,
2359
+ ErrorCode.INVALID_INPUT,
2360
+ 'Ambiguous role rationale lookup',
2361
+ );
2362
+ expectError(
2363
+ await environment.operations.roleRationale(
2364
+ roleContractReadCredential,
2365
+ roleContractCube,
2366
+ createProtocolEnvelope('rationale-invalid-selector', {
2367
+ role: ' Rationale Builder',
2368
+ section: 'Workflow rationale',
2369
+ }),
2370
+ ),
2371
+ 400,
2372
+ ErrorCode.INVALID_INPUT,
2373
+ 'Invalid role rationale selector',
2374
+ );
2039
2375
  return {
2040
- revoked_status: 401,
2041
- expired_status: 401,
2042
- revoked_code: ErrorCode.SESSION_REVOKED,
2043
- expired_code: ErrorCode.AUTH_EXPIRED,
2376
+ uuid_lookup: true,
2377
+ case_insensitive_name_lookup: true,
2378
+ canonical_heading: true,
2379
+ exact_body: true,
2380
+ unknown_role_code: ErrorCode.ROLE_NOT_FOUND,
2381
+ inaccessible_role_code: ErrorCode.ROLE_NOT_FOUND,
2382
+ unknown_section_code: ErrorCode.ROLE_SECTION_NOT_FOUND,
2383
+ ambiguous_role_code: ErrorCode.INVALID_INPUT,
2384
+ invalid_selector_code: ErrorCode.INVALID_INPUT,
2044
2385
  };
2045
2386
  });
2046
2387
 
@@ -2264,10 +2605,6 @@ export async function runAdapterConformance(
2264
2605
  const revokedSession = await environment.admin.issueManagedDroneSession(revoked);
2265
2606
  await environment.admin.revokeManagedDroneSession(revoked);
2266
2607
  rejectedStates.push(['revoked', revoked, revokedSession, 401, ErrorCode.SESSION_REVOKED]);
2267
- const expired = await environment.admin.createDrone(principalA, cubeA, workerRoleA);
2268
- const expiredSession = await environment.admin.issueManagedDroneSession(expired);
2269
- await environment.admin.expireManagedDroneSession(expired);
2270
- rejectedStates.push(['expired', expired, expiredSession, 401, ErrorCode.AUTH_EXPIRED]);
2271
2608
  const evicted = await environment.admin.createDrone(principalA, cubeA, workerRoleA);
2272
2609
  const evictedSession = await environment.admin.issueManagedDroneSession(evicted);
2273
2610
  expectStatus(
@@ -2302,7 +2639,7 @@ export async function runAdapterConformance(
2302
2639
  own_seat_only: true,
2303
2640
  manager_denied: true,
2304
2641
  unknown_session_denied: true,
2305
- revoked_expired_evicted_denied: true,
2642
+ revoked_evicted_denied: true,
2306
2643
  };
2307
2644
  });
2308
2645