borgmcp-shared 0.9.0 → 0.10.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.
@@ -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,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;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"}
@@ -16,6 +16,11 @@ export var ErrorCode;
16
16
  ErrorCode["REPOSITORY_ALREADY_ASSOCIATED"] = "REPOSITORY_ALREADY_ASSOCIATED";
17
17
  ErrorCode["CUBE_ALREADY_ASSOCIATED"] = "CUBE_ALREADY_ASSOCIATED";
18
18
  ErrorCode["ROLE_IN_USE"] = "ROLE_IN_USE";
19
+ ErrorCode["DEFAULT_ROLE_REQUIRED"] = "DEFAULT_ROLE_REQUIRED";
20
+ ErrorCode["ROLE_REQUIRED"] = "ROLE_REQUIRED";
21
+ ErrorCode["ROLE_REFERENCED"] = "ROLE_REFERENCED";
22
+ ErrorCode["ROLE_NOT_FOUND"] = "ROLE_NOT_FOUND";
23
+ ErrorCode["ROLE_SECTION_NOT_FOUND"] = "ROLE_SECTION_NOT_FOUND";
19
24
  ErrorCode["ROLE_HAS_FROZEN_DRONES"] = "ROLE_HAS_FROZEN_DRONES";
20
25
  ErrorCode["CUBE_DELETED"] = "CUBE_DELETED";
21
26
  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,SAsCX;AAtCD,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,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;IAQnC,kDAAqC,CAAA;AACvC,CAAC,EAtCW,SAAS,KAAT,SAAS,QAsCpB"}
@@ -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
+ The `borgmcp-shared@0.10.0` release candidate 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,19 @@
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=="
15
29
  }
16
30
  ]
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.0",
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,
@@ -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,
@@ -322,6 +330,17 @@ export interface ConformanceOperations {
322
330
  drone: ConformanceDrone,
323
331
  request: unknown,
324
332
  ): Promise<ConformanceHttpResponse>;
333
+ deleteRole(
334
+ credential: string,
335
+ cube: ConformanceCube,
336
+ role: ConformanceRole,
337
+ request: unknown,
338
+ ): Promise<ConformanceHttpResponse>;
339
+ roleRationale(
340
+ credential: string,
341
+ cube: ConformanceCube,
342
+ request: unknown,
343
+ ): Promise<ConformanceHttpResponse>;
325
344
  openStream(
326
345
  credential: string,
327
346
  cube: ConformanceCube,
@@ -354,6 +373,8 @@ export const ADAPTER_CONFORMANCE_FIXTURES = [
354
373
  { id: 'drones.reassign-invariants', area: 'drones' },
355
374
  { id: 'security.cross-cube-drone-management', area: 'security' },
356
375
  { id: 'drones.evict-terminal-signal', area: 'drones' },
376
+ { id: 'roles.delete-contract', area: 'roles' },
377
+ { id: 'roles.rationale-contract', area: 'roles' },
357
378
  { id: 'security.drone-session-rejection-causes', area: 'security' },
358
379
  { id: 'metadata.attach-report', area: 'metadata' },
359
380
  { id: 'metadata.self-heal-patch', area: 'metadata' },
@@ -408,6 +429,14 @@ function protocolError(response: ConformanceHttpResponse): ErrorCode | null {
408
429
  }
409
430
  }
410
431
 
432
+ function protocolErrorMessage(response: ConformanceHttpResponse): string | null {
433
+ try {
434
+ return decodeProtocolErrorEnvelope(response.body).error.message;
435
+ } catch {
436
+ return null;
437
+ }
438
+ }
439
+
411
440
  function delay(milliseconds: number): Promise<void> {
412
441
  const setTimeoutValue = (globalThis as unknown as {
413
442
  setTimeout?: (callback: () => void, delay: number) => unknown;
@@ -2014,6 +2043,350 @@ export async function runAdapterConformance(
2014
2043
  };
2015
2044
  });
2016
2045
 
2046
+ let roleContractCube!: ConformanceCube;
2047
+ let roleContractPrincipal!: ConformancePrincipal;
2048
+ let roleContractCredential = '';
2049
+ let roleContractReadCredential = '';
2050
+ await record('roles.delete-contract', async () => {
2051
+ roleContractPrincipal = await environment.admin.createPrincipal('role-contract-manager');
2052
+ const roleContractReader = await environment.admin.createPrincipal('role-contract-reader');
2053
+ roleContractCube = await environment.admin.createCube('role-contract-cube');
2054
+ await environment.admin.grantCube(roleContractPrincipal, roleContractCube, 'manage');
2055
+ await environment.admin.grantCube(roleContractReader, roleContractCube, 'read');
2056
+ const managerInvitation = await environment.admin.issueSingleUseInvitation(
2057
+ roleContractPrincipal,
2058
+ 'client',
2059
+ );
2060
+ const readerInvitation = await environment.admin.issueSingleUseInvitation(
2061
+ roleContractReader,
2062
+ 'client',
2063
+ );
2064
+ roleContractCredential = `${'D'.repeat(42)}Q`;
2065
+ roleContractReadCredential = `${'L'.repeat(42)}Q`;
2066
+ expectStatus(await environment.operations.enroll(createProtocolEnvelope(
2067
+ 'role-contract-manager-enroll',
2068
+ {
2069
+ invitation: managerInvitation,
2070
+ retry_key: '00000000-0000-4000-8000-000000000351',
2071
+ client_credential: roleContractCredential,
2072
+ },
2073
+ )), 201, 'Role-contract manager enrollment');
2074
+ expectStatus(await environment.operations.enroll(createProtocolEnvelope(
2075
+ 'role-contract-reader-enroll',
2076
+ {
2077
+ invitation: readerInvitation,
2078
+ retry_key: '00000000-0000-4000-8000-000000000352',
2079
+ client_credential: roleContractReadCredential,
2080
+ },
2081
+ )), 201, 'Role-contract reader enrollment');
2082
+
2083
+ const defaultRole = await environment.admin.createRole(roleContractCube, {
2084
+ roleClass: 'worker',
2085
+ isHumanSeat: false,
2086
+ name: 'Default Worker',
2087
+ isDefault: true,
2088
+ });
2089
+ const mandatoryRole = await environment.admin.createRole(roleContractCube, {
2090
+ roleClass: 'worker',
2091
+ isHumanSeat: false,
2092
+ name: 'Mandatory Worker',
2093
+ isMandatory: true,
2094
+ });
2095
+ const humanRole = await environment.admin.createRole(roleContractCube, {
2096
+ roleClass: 'queen',
2097
+ isHumanSeat: true,
2098
+ name: 'Human Seat',
2099
+ });
2100
+ const referencedRole = await environment.admin.createRole(roleContractCube, {
2101
+ roleClass: 'worker',
2102
+ isHumanSeat: false,
2103
+ name: 'Routed Reviewer',
2104
+ });
2105
+ await environment.admin.referenceRoleFromTaxonomy(roleContractCube, referencedRole);
2106
+ const activeRole = await environment.admin.createRole(roleContractCube, {
2107
+ roleClass: 'worker',
2108
+ isHumanSeat: false,
2109
+ name: 'Active Worker',
2110
+ });
2111
+ await environment.admin.createDrone(roleContractPrincipal, roleContractCube, activeRole);
2112
+
2113
+ const refusals = [
2114
+ [defaultRole, ErrorCode.DEFAULT_ROLE_REQUIRED, 'default role'],
2115
+ [mandatoryRole, ErrorCode.ROLE_REQUIRED, 'mandatory role'],
2116
+ [humanRole, ErrorCode.ROLE_REQUIRED, 'human-seat role'],
2117
+ [referencedRole, ErrorCode.ROLE_REFERENCED, 'taxonomy-referenced role'],
2118
+ [activeRole, ErrorCode.ROLE_IN_USE, 'active-drone role'],
2119
+ ] as const;
2120
+ for (const [index, [role, code, label]] of refusals.entries()) {
2121
+ const before = await environment.admin.inspectCubeManagementState(roleContractCube);
2122
+ const response = await environment.operations.deleteRole(
2123
+ roleContractCredential,
2124
+ roleContractCube,
2125
+ role,
2126
+ createProtocolEnvelope(`delete-refusal-${index}`, {}),
2127
+ );
2128
+ expectError(response, 409, code, `Delete ${label}`);
2129
+ invariant(
2130
+ same(await environment.admin.inspectCubeManagementState(roleContractCube), before),
2131
+ `Delete ${label} mutated cube state after refusal.`,
2132
+ );
2133
+ if (code === ErrorCode.ROLE_IN_USE) {
2134
+ const message = protocolErrorMessage(response) ?? '';
2135
+ invariant(
2136
+ message === ROLE_IN_USE_DELETE_MESSAGE,
2137
+ 'ROLE_IN_USE did not direct the caller to reassign or evict the drones first.',
2138
+ );
2139
+ }
2140
+ }
2141
+
2142
+ const unknownRole = { id: '00000000-0000-4000-8000-000000000398' };
2143
+ expectError(
2144
+ await environment.operations.deleteRole(
2145
+ roleContractCredential,
2146
+ roleContractCube,
2147
+ unknownRole,
2148
+ createProtocolEnvelope('delete-unknown-role', {}),
2149
+ ),
2150
+ 404,
2151
+ ErrorCode.NOT_FOUND,
2152
+ 'Delete unknown role',
2153
+ );
2154
+
2155
+ const deletableRole = await environment.admin.createRole(roleContractCube, {
2156
+ roleClass: 'worker',
2157
+ isHumanSeat: false,
2158
+ name: 'Disposable Worker',
2159
+ });
2160
+ const beforeReadDenial = await environment.admin.inspectCubeManagementState(roleContractCube);
2161
+ expectError(
2162
+ await environment.operations.deleteRole(
2163
+ roleContractReadCredential,
2164
+ roleContractCube,
2165
+ deletableRole,
2166
+ createProtocolEnvelope('delete-role-read-denied', {}),
2167
+ ),
2168
+ 403,
2169
+ ErrorCode.ACCESS_DENIED,
2170
+ 'Delete role with read authority',
2171
+ );
2172
+ invariant(
2173
+ same(
2174
+ await environment.admin.inspectCubeManagementState(roleContractCube),
2175
+ beforeReadDenial,
2176
+ ),
2177
+ 'Read-authority role deletion mutated cube state.',
2178
+ );
2179
+ const deleted = await environment.operations.deleteRole(
2180
+ roleContractCredential,
2181
+ roleContractCube,
2182
+ deletableRole,
2183
+ createProtocolEnvelope('delete-empty-role', {}),
2184
+ );
2185
+ expectStatus(deleted, 200, 'Delete unassigned role');
2186
+ invariant(
2187
+ same(decodeDeleteRoleResultEnvelope(deleted.body).payload, {
2188
+ role_id: deletableRole.id,
2189
+ deleted: true,
2190
+ }),
2191
+ 'Role deletion response did not identify the deleted role.',
2192
+ );
2193
+
2194
+ const evictedRole = await environment.admin.createRole(roleContractCube, {
2195
+ roleClass: 'worker',
2196
+ isHumanSeat: false,
2197
+ name: 'Former Worker',
2198
+ });
2199
+ const evictedDrone = await environment.admin.createDrone(
2200
+ roleContractPrincipal,
2201
+ roleContractCube,
2202
+ evictedRole,
2203
+ );
2204
+ const evictedSession = await environment.admin.issueManagedDroneSession(evictedDrone);
2205
+ const attributed = await environment.operations.append(
2206
+ evictedSession,
2207
+ roleContractCube,
2208
+ createProtocolEnvelope('role-delete-attribution', { message: 'attribution survives' }),
2209
+ );
2210
+ expectStatus(attributed, 201, 'Pre-delete attributed log');
2211
+ const attributedEntry = decodeAppendLogResultEnvelope(attributed.body).payload.entry;
2212
+ expectStatus(await environment.operations.evictDrone(
2213
+ roleContractCredential,
2214
+ roleContractCube,
2215
+ evictedDrone,
2216
+ createProtocolEnvelope('role-delete-evict', {}),
2217
+ ), 200, 'Pre-delete drone eviction');
2218
+ expectStatus(await environment.operations.deleteRole(
2219
+ roleContractCredential,
2220
+ roleContractCube,
2221
+ evictedRole,
2222
+ createProtocolEnvelope('delete-evicted-role', {}),
2223
+ ), 200, 'Delete role held only by an evicted drone');
2224
+ invariant(
2225
+ (await environment.admin.inspectManagedDrone(evictedDrone)).role_id === defaultRole.id,
2226
+ 'Role deletion did not retarget the evicted drone to the surviving default role.',
2227
+ );
2228
+ const read = await environment.operations.read(
2229
+ roleContractCredential,
2230
+ roleContractCube,
2231
+ createProtocolEnvelope('read-role-delete-attribution', { cursor: null, limit: 500 }),
2232
+ );
2233
+ expectStatus(read, 200, 'Post-delete attributed log read');
2234
+ invariant(
2235
+ decodeReadLogResultEnvelope(read.body).payload.entries.some(
2236
+ (entry) => entry.id === attributedEntry.id && entry.drone_id === evictedDrone.id,
2237
+ ),
2238
+ 'Deleting the evicted drone role lost the drone attribution of an existing log entry.',
2239
+ );
2240
+
2241
+ return {
2242
+ success_status: 200,
2243
+ hidden_status: 404,
2244
+ refusal_codes: [
2245
+ ErrorCode.ROLE_IN_USE,
2246
+ ErrorCode.DEFAULT_ROLE_REQUIRED,
2247
+ ErrorCode.ROLE_REQUIRED,
2248
+ ErrorCode.ROLE_REFERENCED,
2249
+ ],
2250
+ in_use_message_actionable: true,
2251
+ evicted_drone_retargeted: true,
2252
+ activity_log_attribution_preserved: true,
2253
+ };
2254
+ });
2255
+
2256
+ await record('roles.rationale-contract', async () => {
2257
+ const detailedDescription = [
2258
+ 'Implements assigned work.',
2259
+ '',
2260
+ 'Workflow rationale:',
2261
+ 'This exact stored section body is returned.',
2262
+ '',
2263
+ 'Boundaries:',
2264
+ 'Do not expand scope.',
2265
+ ].join('\n');
2266
+ const role = await environment.admin.createRole(roleContractCube, {
2267
+ roleClass: 'worker',
2268
+ isHumanSeat: false,
2269
+ name: 'Rationale Builder',
2270
+ detailedDescription,
2271
+ });
2272
+ const expectedSection = {
2273
+ heading: 'Workflow rationale',
2274
+ body: 'Workflow rationale:\nThis exact stored section body is returned.\n\n',
2275
+ };
2276
+ for (const [index, [selector, section]] of [
2277
+ [role.id, 'Workflow rationale'],
2278
+ ['rAtIoNaLe BuIlDeR', 'wOrKfLoW rAtIoNaLe'],
2279
+ ].entries()) {
2280
+ const response = await environment.operations.roleRationale(
2281
+ roleContractReadCredential,
2282
+ roleContractCube,
2283
+ createProtocolEnvelope(`rationale-lookup-${index}`, { role: selector, section }),
2284
+ );
2285
+ expectStatus(response, 200, 'Role rationale lookup');
2286
+ const result = decodeRoleRationaleResultEnvelope(response.body).payload;
2287
+ invariant(
2288
+ result.role_id === role.id &&
2289
+ result.role_name === 'Rationale Builder' &&
2290
+ same(result.section, expectedSection),
2291
+ 'Role rationale lookup did not return the canonical heading and exact stored section body.',
2292
+ );
2293
+ }
2294
+ expectError(
2295
+ await environment.operations.roleRationale(
2296
+ roleContractReadCredential,
2297
+ roleContractCube,
2298
+ createProtocolEnvelope('rationale-unknown-role', {
2299
+ role: 'Unknown Role',
2300
+ section: 'Workflow rationale',
2301
+ }),
2302
+ ),
2303
+ 404,
2304
+ ErrorCode.ROLE_NOT_FOUND,
2305
+ 'Unknown role rationale lookup',
2306
+ );
2307
+ const inaccessibleRole = await environment.admin.createRole(cubeB, {
2308
+ roleClass: 'worker',
2309
+ isHumanSeat: false,
2310
+ name: 'Foreign Rationale Role',
2311
+ detailedDescription,
2312
+ });
2313
+ expectError(
2314
+ await environment.operations.roleRationale(
2315
+ roleContractReadCredential,
2316
+ roleContractCube,
2317
+ createProtocolEnvelope('rationale-inaccessible-role', {
2318
+ role: inaccessibleRole.id,
2319
+ section: 'Workflow rationale',
2320
+ }),
2321
+ ),
2322
+ 404,
2323
+ ErrorCode.ROLE_NOT_FOUND,
2324
+ 'Inaccessible role rationale lookup',
2325
+ );
2326
+ expectError(
2327
+ await environment.operations.roleRationale(
2328
+ roleContractReadCredential,
2329
+ roleContractCube,
2330
+ createProtocolEnvelope('rationale-unknown-section', {
2331
+ role: role.id,
2332
+ section: 'Missing rationale',
2333
+ }),
2334
+ ),
2335
+ 404,
2336
+ ErrorCode.ROLE_SECTION_NOT_FOUND,
2337
+ 'Unknown role section lookup',
2338
+ );
2339
+ await environment.admin.createRole(roleContractCube, {
2340
+ roleClass: 'worker',
2341
+ isHumanSeat: false,
2342
+ name: 'Ambiguous Role',
2343
+ detailedDescription,
2344
+ });
2345
+ await environment.admin.createRole(roleContractCube, {
2346
+ roleClass: 'worker',
2347
+ isHumanSeat: false,
2348
+ name: 'AMBIGUOUS ROLE',
2349
+ detailedDescription,
2350
+ });
2351
+ expectError(
2352
+ await environment.operations.roleRationale(
2353
+ roleContractReadCredential,
2354
+ roleContractCube,
2355
+ createProtocolEnvelope('rationale-ambiguous-role', {
2356
+ role: 'ambiguous role',
2357
+ section: 'Workflow rationale',
2358
+ }),
2359
+ ),
2360
+ 400,
2361
+ ErrorCode.INVALID_INPUT,
2362
+ 'Ambiguous role rationale lookup',
2363
+ );
2364
+ expectError(
2365
+ await environment.operations.roleRationale(
2366
+ roleContractReadCredential,
2367
+ roleContractCube,
2368
+ createProtocolEnvelope('rationale-invalid-selector', {
2369
+ role: ' Rationale Builder',
2370
+ section: 'Workflow rationale',
2371
+ }),
2372
+ ),
2373
+ 400,
2374
+ ErrorCode.INVALID_INPUT,
2375
+ 'Invalid role rationale selector',
2376
+ );
2377
+ return {
2378
+ uuid_lookup: true,
2379
+ case_insensitive_name_lookup: true,
2380
+ canonical_heading: true,
2381
+ exact_body: true,
2382
+ unknown_role_code: ErrorCode.ROLE_NOT_FOUND,
2383
+ inaccessible_role_code: ErrorCode.ROLE_NOT_FOUND,
2384
+ unknown_section_code: ErrorCode.ROLE_SECTION_NOT_FOUND,
2385
+ ambiguous_role_code: ErrorCode.INVALID_INPUT,
2386
+ invalid_selector_code: ErrorCode.INVALID_INPUT,
2387
+ };
2388
+ });
2389
+
2017
2390
  await record('security.drone-session-rejection-causes', async () => {
2018
2391
  const revokedDrone = await environment.admin.createDrone(principalA, cubeA, workerRoleA);
2019
2392
  const expiredDrone = await environment.admin.createDrone(principalA, cubeA, workerRoleA);