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.
- package/dist/conformance/adapter.d.ts +13 -0
- package/dist/conformance/adapter.d.ts.map +1 -1
- package/dist/conformance/adapter.js +208 -1
- package/dist/conformance/adapter.js.map +1 -1
- package/dist/conformance/index.d.ts +43 -0
- package/dist/conformance/index.d.ts.map +1 -1
- package/dist/conformance/index.js +91 -0
- package/dist/conformance/index.js.map +1 -1
- package/dist/protocol/contract.d.ts +18 -1
- package/dist/protocol/contract.d.ts.map +1 -1
- package/dist/protocol/contract.js +19 -2
- package/dist/protocol/contract.js.map +1 -1
- package/dist/protocol/coordination.d.ts +27 -0
- package/dist/protocol/coordination.d.ts.map +1 -1
- package/dist/protocol/coordination.js +71 -1
- package/dist/protocol/coordination.js.map +1 -1
- package/dist/protocol/errors.d.ts +5 -0
- package/dist/protocol/errors.d.ts.map +1 -1
- package/dist/protocol/errors.js +5 -0
- package/dist/protocol/errors.js.map +1 -1
- package/dist/protocol/version.d.ts +1 -1
- package/dist/protocol/version.js +1 -1
- package/docs/compatibility.md +9 -1
- package/docs/enrollment.md +2 -5
- package/docs/release-records.json +14 -0
- package/docs/releasing.md +5 -9
- package/package.json +1 -1
- package/src/conformance/adapter.ts +373 -0
- package/src/conformance/index.ts +139 -0
- package/src/protocol/contract.ts +20 -2
- package/src/protocol/coordination.ts +121 -0
- package/src/protocol/errors.ts +5 -0
- package/src/protocol/version.ts +2 -2
|
@@ -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"}
|
package/dist/protocol/errors.js
CHANGED
|
@@ -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,
|
|
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"}
|
package/dist/protocol/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const PROTOCOL_VERSION = '
|
|
1
|
+
export const PROTOCOL_VERSION = '8';
|
|
2
2
|
//# sourceMappingURL=version.js.map
|
package/docs/compatibility.md
CHANGED
|
@@ -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
|
-
|
|
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
|
package/docs/enrollment.md
CHANGED
|
@@ -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
|
|
29
|
-
`borgmcp-shared@0.7.1` are published and immutable.
|
|
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
|
|
183
|
-
`borgmcp-shared@0.7.1` are published and immutable.
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
@@ -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);
|