borgmcp-shared 0.4.0 → 0.4.3
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/README.md +29 -16
- package/SECURITY.md +6 -5
- package/dist/conformance/adapter.d.ts +58 -8
- package/dist/conformance/adapter.d.ts.map +1 -1
- package/dist/conformance/adapter.js +260 -14
- package/dist/conformance/adapter.js.map +1 -1
- package/dist/protocol/contract.d.ts +17 -1
- package/dist/protocol/contract.d.ts.map +1 -1
- package/dist/protocol/contract.js +17 -1
- package/dist/protocol/contract.js.map +1 -1
- package/dist/protocol/coordination.d.ts +25 -0
- package/dist/protocol/coordination.d.ts.map +1 -1
- package/dist/protocol/coordination.js +44 -0
- package/dist/protocol/coordination.js.map +1 -1
- package/dist/protocol/errors.d.ts.map +1 -1
- package/dist/protocol/errors.js.map +1 -1
- package/dist/templates.d.ts +3 -2
- package/dist/templates.d.ts.map +1 -1
- package/dist/templates.js +45 -40
- package/dist/templates.js.map +1 -1
- package/docs/enrollment.md +19 -15
- package/docs/releasing.md +119 -284
- package/package.json +2 -2
- package/src/conformance/adapter.ts +589 -26
- package/src/protocol/contract.ts +17 -1
- package/src/protocol/coordination.ts +82 -0
- package/src/protocol/errors.ts +3 -2
- package/src/templates.ts +51 -45
package/README.md
CHANGED
|
@@ -88,8 +88,14 @@ Every JSON coordination request and successful JSON response is carried inside
|
|
|
88
88
|
exceptions are the `204` liveness and acknowledgement responses. Payload codecs
|
|
89
89
|
are exported separately so adapters can validate the envelope first and then
|
|
90
90
|
validate the operation-specific payload without accepting ambiguous fields.
|
|
91
|
+
Cube managers reassign a seat with `PATCH /api/cubes/:cubeId/drones/:droneId`
|
|
92
|
+
and evict one with `DELETE` on the same path. Both operations use strict
|
|
93
|
+
versioned request and success envelopes. An evicted seat's former bearer receives
|
|
94
|
+
the terminal `410 DRONE_EVICTED` signal; revoked sessions return
|
|
95
|
+
`401 SESSION_REVOKED`, while expired sessions return the only recoverable
|
|
96
|
+
authentication outcome, `401 AUTH_EXPIRED`.
|
|
91
97
|
See [docs/enrollment.md](docs/enrollment.md) for purpose-bound owner enrollment,
|
|
92
|
-
ordinary ungranted enrollment, cube creation, pending
|
|
98
|
+
ordinary ungranted enrollment, cube creation, pending enrollment, and retry contracts.
|
|
93
99
|
|
|
94
100
|
## Conformance
|
|
95
101
|
|
|
@@ -99,14 +105,19 @@ readonly data rather than Vitest-specific helpers, so they work with any test
|
|
|
99
105
|
runner and in any JavaScript runtime. Cases cover HTTP and canonical errors,
|
|
100
106
|
credential misuse, isolation and revocation, SSE framing/replay/cursor ordering,
|
|
101
107
|
executable enrollment authority/retry/mismatch/redaction and cube-create
|
|
102
|
-
idempotency, acks, claims,
|
|
108
|
+
idempotency, acks, claims, decisions, cube-scoped drone reassignment, role-class
|
|
109
|
+
and single-seat invariants, eviction exclusion, and terminal bearer signaling.
|
|
110
|
+
Manage-scoped cube, role, taxonomy, decision, and drone operations also share an
|
|
111
|
+
authority matrix: managing parents may mutate; known same-cube read/write
|
|
112
|
+
parents receive `403 ACCESS_DENIED`; drone sessions remain non-managing; and
|
|
113
|
+
no-grant, foreign, or unknown cubes remain hidden behind `404 NOT_FOUND`.
|
|
103
114
|
|
|
104
115
|
Implement `AdapterConformanceDriver` with raw responses from the target adapter,
|
|
105
116
|
then call `runAdapterConformance`. The runner creates and decodes envelopes,
|
|
106
117
|
drives state transitions, and decides pass/fail; adapters do not submit expected
|
|
107
|
-
results.
|
|
108
|
-
|
|
109
|
-
|
|
118
|
+
results. Each server or client adapter runs this single portable suite against
|
|
119
|
+
its local/self-hosted implementation. The package does not define a second
|
|
120
|
+
authority, migration target, or fallback implementation.
|
|
110
121
|
|
|
111
122
|
The package's own suite covers built-in templates, role-section patching,
|
|
112
123
|
broadcast high-water-mark ordering, drone-address formatting, and current public
|
|
@@ -126,17 +137,19 @@ the coordinated rollout order for introducing protocol changes.
|
|
|
126
137
|
|
|
127
138
|
## Distribution
|
|
128
139
|
|
|
129
|
-
`borgmcp-shared@0.
|
|
130
|
-
|
|
131
|
-
`
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
`
|
|
138
|
-
|
|
139
|
-
|
|
140
|
+
`borgmcp-shared@0.4.0` is the published clean-slate v2 baseline and is
|
|
141
|
+
immutable, but its artifact predates the local/self-hosted package cleanup. The
|
|
142
|
+
immutable `v0.4.1` verification artifact exposed a platform-dependent SBOM audit
|
|
143
|
+
and must never be published, moved, reused, rerun, or substituted.
|
|
144
|
+
`borgmcp-shared@0.4.2` is published and immutable. This source now identifies
|
|
145
|
+
the unpublished `0.4.3` session-lifecycle contract release: expired credentials
|
|
146
|
+
return `AUTH_EXPIRED`, while explicitly revoked credentials return `SESSION_REVOKED`.
|
|
147
|
+
This reviewed version bump grants no tag or publish authority; creating the
|
|
148
|
+
annotated `v0.4.3` tag and publishing its exact reviewed artifact remain separate,
|
|
149
|
+
independently gated steps. After that release, consumers pin exact
|
|
150
|
+
`borgmcp-shared@0.4.3` and commit registry lockfiles. No registry token belongs in
|
|
151
|
+
this repository, package metadata, lockfiles, or a committed `.npmrc`; publishing
|
|
152
|
+
uses protected external credentials and provenance.
|
|
140
153
|
|
|
141
154
|
The first client and server releases must consume the reviewed registry release,
|
|
142
155
|
not a Git, tag, or local-path dependency. Public release requires the
|
package/SECURITY.md
CHANGED
|
@@ -6,10 +6,10 @@ Please report suspected vulnerabilities privately through GitHub Security
|
|
|
6
6
|
Advisories for this repository. Open the repository's **Security** tab, select
|
|
7
7
|
**Advisories**, and choose **Report a vulnerability**.
|
|
8
8
|
|
|
9
|
-
If GitHub's private reporting interface is unavailable,
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
If GitHub's private reporting interface is unavailable, do not open a public
|
|
10
|
+
issue containing vulnerability details. Wait until private repository reporting
|
|
11
|
+
is available. Do not include credentials, access tokens, user data, or other
|
|
12
|
+
secrets unless the maintainers explicitly request them through a secure channel.
|
|
13
13
|
|
|
14
14
|
Do not disclose the issue publicly until the maintainers confirm that a fix is
|
|
15
15
|
available and coordinated disclosure is appropriate.
|
|
@@ -18,7 +18,8 @@ available and coordinated disclosure is appropriate.
|
|
|
18
18
|
|
|
19
19
|
Security reports may concern this package's published artifacts, protocol
|
|
20
20
|
contracts, conformance data, dependency chain, or documentation. Vulnerabilities
|
|
21
|
-
in
|
|
21
|
+
in the client or self-hosted server belong in those repositories' private
|
|
22
|
+
security-reporting channels.
|
|
22
23
|
|
|
23
24
|
Only supported package versions receive security fixes. The latest published
|
|
24
25
|
version is always supported; older versions may receive fixes when a safe
|
|
@@ -9,6 +9,25 @@ export interface ConformancePrincipal {
|
|
|
9
9
|
export interface ConformanceCube {
|
|
10
10
|
readonly id: string;
|
|
11
11
|
}
|
|
12
|
+
export interface ConformanceRole {
|
|
13
|
+
readonly id: string;
|
|
14
|
+
}
|
|
15
|
+
export interface ConformanceDrone {
|
|
16
|
+
readonly id: string;
|
|
17
|
+
}
|
|
18
|
+
export type ConformanceCubeAccess = 'read' | 'write' | 'manage';
|
|
19
|
+
export interface ConformanceCubeManagementState {
|
|
20
|
+
readonly directive: string;
|
|
21
|
+
readonly taxonomy_marker: string | null;
|
|
22
|
+
readonly role_ids: readonly string[];
|
|
23
|
+
readonly active_decision_ids: readonly string[];
|
|
24
|
+
readonly drones: ReadonlyArray<{
|
|
25
|
+
readonly id: string;
|
|
26
|
+
readonly role_id: string;
|
|
27
|
+
readonly evicted: boolean;
|
|
28
|
+
readonly session_revoked: boolean;
|
|
29
|
+
}>;
|
|
30
|
+
}
|
|
12
31
|
export interface ConformanceAuthorityState {
|
|
13
32
|
enrolled_clients: number;
|
|
14
33
|
enrollment_claims: number;
|
|
@@ -42,7 +61,21 @@ export interface ConformanceAdmin {
|
|
|
42
61
|
reset(): Promise<void>;
|
|
43
62
|
createPrincipal(name: string): Promise<ConformancePrincipal>;
|
|
44
63
|
createCube(name: string): Promise<ConformanceCube>;
|
|
45
|
-
grantCube(principal: ConformancePrincipal, cube: ConformanceCube): Promise<void>;
|
|
64
|
+
grantCube(principal: ConformancePrincipal, cube: ConformanceCube, access?: ConformanceCubeAccess): Promise<void>;
|
|
65
|
+
createRole(cube: ConformanceCube, input: {
|
|
66
|
+
readonly roleClass: 'queen' | 'worker';
|
|
67
|
+
readonly isHumanSeat: boolean;
|
|
68
|
+
}): Promise<ConformanceRole>;
|
|
69
|
+
createDrone(principal: ConformancePrincipal, cube: ConformanceCube, role: ConformanceRole): Promise<ConformanceDrone>;
|
|
70
|
+
issueManagedDroneSession(drone: ConformanceDrone): Promise<string>;
|
|
71
|
+
revokeManagedDroneSession(drone: ConformanceDrone): Promise<void>;
|
|
72
|
+
expireManagedDroneSession(drone: ConformanceDrone): Promise<void>;
|
|
73
|
+
inspectManagedDrone(drone: ConformanceDrone): Promise<{
|
|
74
|
+
readonly role_id: string;
|
|
75
|
+
readonly evicted: boolean;
|
|
76
|
+
readonly session_revoked: boolean;
|
|
77
|
+
}>;
|
|
78
|
+
inspectCubeManagementState(cube: ConformanceCube): Promise<ConformanceCubeManagementState>;
|
|
46
79
|
grantCreateCubeCapability(principal: ConformancePrincipal): Promise<void>;
|
|
47
80
|
issueDroneSession(principal: ConformancePrincipal): Promise<string>;
|
|
48
81
|
issueSingleUseInvitation(principal: ConformancePrincipal, purpose: 'owner' | 'client'): Promise<string>;
|
|
@@ -62,8 +95,14 @@ export interface ConformanceOperations {
|
|
|
62
95
|
appendRaw(credential: string, cube: ConformanceCube, body: string): Promise<ConformanceHttpResponse>;
|
|
63
96
|
read(credential: string, cube: ConformanceCube, request: unknown): Promise<ConformanceHttpResponse>;
|
|
64
97
|
ack(credential: string, cube: ConformanceCube, request: unknown): Promise<ConformanceHttpResponse>;
|
|
98
|
+
updateCube(credential: string, cube: ConformanceCube, request: unknown): Promise<ConformanceHttpResponse>;
|
|
99
|
+
createRole(credential: string, cube: ConformanceCube, request: unknown): Promise<ConformanceHttpResponse>;
|
|
100
|
+
patchTaxonomy(credential: string, cube: ConformanceCube, request: unknown): Promise<ConformanceHttpResponse>;
|
|
65
101
|
recordDecision(credential: string, cube: ConformanceCube, request: unknown): Promise<ConformanceHttpResponse>;
|
|
66
102
|
listDecisions(credential: string, cube: ConformanceCube, request: unknown): Promise<ConformanceHttpResponse>;
|
|
103
|
+
listDrones(credential: string, cube: ConformanceCube): Promise<ConformanceHttpResponse>;
|
|
104
|
+
reassignDrone(credential: string, cube: ConformanceCube, drone: ConformanceDrone, request: unknown): Promise<ConformanceHttpResponse>;
|
|
105
|
+
evictDrone(credential: string, cube: ConformanceCube, drone: ConformanceDrone, request: unknown): Promise<ConformanceHttpResponse>;
|
|
67
106
|
openStream(credential: string, cube: ConformanceCube, cursor: LogCursor | null): Promise<ConformanceStreamResponse>;
|
|
68
107
|
}
|
|
69
108
|
export interface ConformanceEnvironment {
|
|
@@ -106,6 +145,24 @@ export declare const ADAPTER_CONFORMANCE_FIXTURES: readonly [{
|
|
|
106
145
|
}, {
|
|
107
146
|
readonly id: "decisions.topic-supersession";
|
|
108
147
|
readonly area: "decisions";
|
|
148
|
+
}, {
|
|
149
|
+
readonly id: "security.drone-management-authorization";
|
|
150
|
+
readonly area: "security";
|
|
151
|
+
}, {
|
|
152
|
+
readonly id: "security.manage-access-matrix";
|
|
153
|
+
readonly area: "security";
|
|
154
|
+
}, {
|
|
155
|
+
readonly id: "drones.reassign-invariants";
|
|
156
|
+
readonly area: "drones";
|
|
157
|
+
}, {
|
|
158
|
+
readonly id: "security.cross-cube-drone-management";
|
|
159
|
+
readonly area: "security";
|
|
160
|
+
}, {
|
|
161
|
+
readonly id: "drones.evict-terminal-signal";
|
|
162
|
+
readonly area: "drones";
|
|
163
|
+
}, {
|
|
164
|
+
readonly id: "security.drone-session-rejection-causes";
|
|
165
|
+
readonly area: "security";
|
|
109
166
|
}, {
|
|
110
167
|
readonly id: "security.active-stream-revocation";
|
|
111
168
|
readonly area: "security";
|
|
@@ -125,16 +182,9 @@ export interface AdapterConformanceReport {
|
|
|
125
182
|
observations: Record<string, unknown>;
|
|
126
183
|
}>;
|
|
127
184
|
}
|
|
128
|
-
export interface EquivalentAdapterConformanceReport {
|
|
129
|
-
ok: boolean;
|
|
130
|
-
cloud: AdapterConformanceReport;
|
|
131
|
-
local: AdapterConformanceReport;
|
|
132
|
-
equivalent: boolean;
|
|
133
|
-
}
|
|
134
185
|
export interface AdapterConformanceOptions {
|
|
135
186
|
streamDeadlineMs?: number;
|
|
136
187
|
pendingProbeMs?: number;
|
|
137
188
|
}
|
|
138
189
|
export declare function runAdapterConformance(environment: ConformanceEnvironment, options?: AdapterConformanceOptions): Promise<AdapterConformanceReport>;
|
|
139
|
-
export declare function runEquivalentAdapterConformance(cloud: ConformanceEnvironment, local: ConformanceEnvironment, options?: AdapterConformanceOptions): Promise<EquivalentAdapterConformanceReport>;
|
|
140
190
|
//# sourceMappingURL=adapter.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../src/conformance/adapter.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../src/conformance/adapter.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ErrorCode, compareLogCursor, createProtocolEnvelope, decodeCreateCubeResponseEnvelope, decodeAppendLogResultEnvelope, decodeDecisionResultEnvelope, decodeDecisionsResultEnvelope, decodeEnrollmentExchangeResponseEnvelope, decodeProtocolErrorEnvelope, decodeProtocolTagPreflight, decodeReadLogResultEnvelope, decodeSseFrames, PROTOCOL_LIMIT_CEILINGS, PROTOCOL_VERSION, utf8ByteLength, } from '../protocol/index.js';
|
|
1
|
+
import { ErrorCode, compareLogCursor, createProtocolEnvelope, decodeCreateCubeResponseEnvelope, decodeAppendLogResultEnvelope, decodeDecisionResultEnvelope, decodeDecisionsResultEnvelope, decodeEnrollmentExchangeResponseEnvelope, decodeEvictDroneResultEnvelope, decodeProtocolEnvelope, decodeProtocolErrorEnvelope, decodeProtocolTagPreflight, decodeReadLogResultEnvelope, decodeReassignDroneResultEnvelope, decodeSseFrames, PROTOCOL_LIMIT_CEILINGS, PROTOCOL_HTTP_CONTRACT, PROTOCOL_VERSION, utf8ByteLength, } from '../protocol/index.js';
|
|
2
2
|
import { ENROLLMENT_RETRY_CONFORMANCE } from './index.js';
|
|
3
3
|
export const ADAPTER_CONFORMANCE_FIXTURES = [
|
|
4
4
|
{ id: 'http.unauthenticated-liveness', area: 'http' },
|
|
@@ -13,6 +13,12 @@ export const ADAPTER_CONFORMANCE_FIXTURES = [
|
|
|
13
13
|
{ id: 'acks.idempotent', area: 'acks' },
|
|
14
14
|
{ id: 'claims.durable-noncursor', area: 'claims' },
|
|
15
15
|
{ id: 'decisions.topic-supersession', area: 'decisions' },
|
|
16
|
+
{ id: 'security.drone-management-authorization', area: 'security' },
|
|
17
|
+
{ id: 'security.manage-access-matrix', area: 'security' },
|
|
18
|
+
{ id: 'drones.reassign-invariants', area: 'drones' },
|
|
19
|
+
{ id: 'security.cross-cube-drone-management', area: 'security' },
|
|
20
|
+
{ id: 'drones.evict-terminal-signal', area: 'drones' },
|
|
21
|
+
{ id: 'security.drone-session-rejection-causes', area: 'security' },
|
|
16
22
|
{ id: 'security.active-stream-revocation', area: 'security' },
|
|
17
23
|
];
|
|
18
24
|
const DEFAULT_STREAM_DEADLINE_MS = 5_000;
|
|
@@ -117,6 +123,19 @@ function logEvent(event, description) {
|
|
|
117
123
|
function same(left, right) {
|
|
118
124
|
return JSON.stringify(left) === JSON.stringify(right);
|
|
119
125
|
}
|
|
126
|
+
function listedDroneIds(response) {
|
|
127
|
+
return decodeProtocolEnvelope(response.body, (payload) => {
|
|
128
|
+
invariant(typeof payload === 'object' && payload !== null, 'Roster payload is not an object.');
|
|
129
|
+
const drones = payload.drones;
|
|
130
|
+
invariant(Array.isArray(drones), 'Roster payload omitted drones.');
|
|
131
|
+
return drones.map((drone) => {
|
|
132
|
+
invariant(typeof drone === 'object' && drone !== null, 'Roster contains an invalid drone.');
|
|
133
|
+
const id = drone.id;
|
|
134
|
+
invariant(typeof id === 'string', 'Roster drone omitted its id.');
|
|
135
|
+
return id;
|
|
136
|
+
});
|
|
137
|
+
}).payload;
|
|
138
|
+
}
|
|
120
139
|
function assertStateDelta(before, after, expected, description) {
|
|
121
140
|
for (const key of Object.keys(before)) {
|
|
122
141
|
invariant(after[key] - before[key] === (expected[key] ?? 0), `${description} changed ${key} by ${after[key] - before[key]}; expected ${expected[key] ?? 0}.`);
|
|
@@ -529,6 +548,246 @@ export async function runAdapterConformance(environment, options = {}) {
|
|
|
529
548
|
invariant(second.supersedes === first.id, 'Superseding decision did not reference its predecessor.');
|
|
530
549
|
return { active_count: 1, active_decision: 'second', supersedes_first: true };
|
|
531
550
|
});
|
|
551
|
+
let workerRoleA;
|
|
552
|
+
let workerRoleB;
|
|
553
|
+
let managedWorker;
|
|
554
|
+
let readCredential = '';
|
|
555
|
+
let writeCredential = '';
|
|
556
|
+
let workerSession = '';
|
|
557
|
+
await record('security.drone-management-authorization', async () => {
|
|
558
|
+
workerRoleA = await environment.admin.createRole(cubeA, {
|
|
559
|
+
roleClass: 'worker', isHumanSeat: false,
|
|
560
|
+
});
|
|
561
|
+
managedWorker = await environment.admin.createDrone(principalA, cubeA, workerRoleA);
|
|
562
|
+
const deniedCredentials = [];
|
|
563
|
+
for (const [index, access] of ['read', 'write'].entries()) {
|
|
564
|
+
const principalName = access === 'read' ? 'Coordinator' : 'write-principal';
|
|
565
|
+
const principal = await environment.admin.createPrincipal(principalName);
|
|
566
|
+
await environment.admin.grantCube(principal, cubeA, access);
|
|
567
|
+
const invitation = await environment.admin.issueSingleUseInvitation(principal, 'client');
|
|
568
|
+
const credential = `${(access === 'read' ? 'R' : 'W').repeat(42)}Q`;
|
|
569
|
+
if (access === 'read')
|
|
570
|
+
readCredential = credential;
|
|
571
|
+
else
|
|
572
|
+
writeCredential = credential;
|
|
573
|
+
const enrollment = await environment.operations.enroll(createProtocolEnvelope(`${access}-principal-enroll`, {
|
|
574
|
+
invitation,
|
|
575
|
+
retry_key: `00000000-0000-4000-8000-${String(301 + index).padStart(12, '0')}`,
|
|
576
|
+
client_credential: credential,
|
|
577
|
+
client_name: principalName,
|
|
578
|
+
}));
|
|
579
|
+
expectStatus(enrollment, 201, `${access} principal enrollment`);
|
|
580
|
+
deniedCredentials.push({ access: access, credential });
|
|
581
|
+
}
|
|
582
|
+
for (const { access, credential } of deniedCredentials) {
|
|
583
|
+
expectError(await environment.operations.reassignDrone(credential, cubeA, managedWorker, createProtocolEnvelope(`${access}-reassign-denied`, { role_id: workerRoleA.id })), 403, ErrorCode.ACCESS_DENIED, `${access} principal reassignment`);
|
|
584
|
+
expectError(await environment.operations.evictDrone(credential, cubeA, managedWorker, createProtocolEnvelope(`${access}-evict-denied`, {})), 403, ErrorCode.ACCESS_DENIED, `${access} principal eviction`);
|
|
585
|
+
}
|
|
586
|
+
return { read_status: 403, write_status: 403, manage_required: true };
|
|
587
|
+
});
|
|
588
|
+
await record('security.manage-access-matrix', async () => {
|
|
589
|
+
const noGrantPrincipal = await environment.admin.createPrincipal('Coordinator role without cube grant');
|
|
590
|
+
const noGrantInvitation = await environment.admin.issueSingleUseInvitation(noGrantPrincipal, 'client');
|
|
591
|
+
const noGrantCredential = `${'N'.repeat(42)}Q`;
|
|
592
|
+
expectStatus(await environment.operations.enroll(createProtocolEnvelope('no-grant-principal-enroll', {
|
|
593
|
+
invitation: noGrantInvitation,
|
|
594
|
+
retry_key: '00000000-0000-4000-8000-000000000303',
|
|
595
|
+
client_credential: noGrantCredential,
|
|
596
|
+
client_name: 'Coordinator',
|
|
597
|
+
})), 201, 'No-grant principal enrollment');
|
|
598
|
+
const droneCredential = await environment.admin.issueManagedDroneSession(managedWorker);
|
|
599
|
+
const matrixTargetRole = await environment.admin.createRole(cubeA, {
|
|
600
|
+
roleClass: 'worker', isHumanSeat: false,
|
|
601
|
+
});
|
|
602
|
+
const evictionTarget = await environment.admin.createDrone(principalA, cubeA, workerRoleA);
|
|
603
|
+
await environment.admin.issueManagedDroneSession(evictionTarget);
|
|
604
|
+
const unknownCube = { id: '00000000-0000-4000-8000-000000000399' };
|
|
605
|
+
const snapshot = async () => ({
|
|
606
|
+
cubeA: await environment.admin.inspectCubeManagementState(cubeA),
|
|
607
|
+
cubeB: await environment.admin.inspectCubeManagementState(cubeB),
|
|
608
|
+
});
|
|
609
|
+
const operations = [
|
|
610
|
+
{
|
|
611
|
+
name: 'cube-update',
|
|
612
|
+
successStatus: 200,
|
|
613
|
+
invoke: (credential, cube) => environment.operations.updateCube(credential, cube, createProtocolEnvelope('matrix-cube-update', { cube_directive: 'matrix-directive' })),
|
|
614
|
+
},
|
|
615
|
+
{
|
|
616
|
+
name: 'role-create',
|
|
617
|
+
successStatus: 201,
|
|
618
|
+
invoke: (credential, cube) => environment.operations.createRole(credential, cube, createProtocolEnvelope('matrix-role-create', { name: 'Matrix Role' })),
|
|
619
|
+
},
|
|
620
|
+
{
|
|
621
|
+
name: 'taxonomy-patch',
|
|
622
|
+
successStatus: 200,
|
|
623
|
+
invoke: (credential, cube) => environment.operations.patchTaxonomy(credential, cube, createProtocolEnvelope('matrix-taxonomy-patch', { marker: 'matrix-taxonomy' })),
|
|
624
|
+
},
|
|
625
|
+
{
|
|
626
|
+
name: 'decision-record',
|
|
627
|
+
successStatus: 201,
|
|
628
|
+
invoke: (credential, cube) => environment.operations.recordDecision(credential, cube, createProtocolEnvelope('matrix-decision-record', {
|
|
629
|
+
topic: 'matrix-authority', decision: 'manage only',
|
|
630
|
+
})),
|
|
631
|
+
},
|
|
632
|
+
{
|
|
633
|
+
name: 'drone-reassign',
|
|
634
|
+
successStatus: 200,
|
|
635
|
+
invoke: (credential, cube) => environment.operations.reassignDrone(credential, cube, managedWorker, createProtocolEnvelope('matrix-drone-reassign', { role_id: matrixTargetRole.id })),
|
|
636
|
+
},
|
|
637
|
+
{
|
|
638
|
+
name: 'drone-evict',
|
|
639
|
+
successStatus: 200,
|
|
640
|
+
invoke: (credential, cube) => environment.operations.evictDrone(credential, cube, evictionTarget, createProtocolEnvelope('matrix-drone-evict', {})),
|
|
641
|
+
},
|
|
642
|
+
];
|
|
643
|
+
for (const operation of operations) {
|
|
644
|
+
for (const [kind, credential] of [
|
|
645
|
+
['read', readCredential],
|
|
646
|
+
['write', writeCredential],
|
|
647
|
+
['drone-session', droneCredential],
|
|
648
|
+
]) {
|
|
649
|
+
const before = await snapshot();
|
|
650
|
+
expectError(await operation.invoke(credential, cubeA), 403, ErrorCode.ACCESS_DENIED, `${operation.name} by ${kind} principal`);
|
|
651
|
+
invariant(same(await snapshot(), before), `${operation.name} mutated state after ${kind} denial.`);
|
|
652
|
+
}
|
|
653
|
+
for (const [kind, credential, cube] of [
|
|
654
|
+
['no-grant', noGrantCredential, cubeA],
|
|
655
|
+
['foreign-principal', credentialB, cubeA],
|
|
656
|
+
['foreign-cube', credentialA, cubeB],
|
|
657
|
+
['unknown-cube', credentialA, unknownCube],
|
|
658
|
+
]) {
|
|
659
|
+
const before = await snapshot();
|
|
660
|
+
expectError(await operation.invoke(credential, cube), 404, ErrorCode.NOT_FOUND, `${operation.name} against ${kind}`);
|
|
661
|
+
invariant(same(await snapshot(), before), `${operation.name} mutated state after ${kind} denial.`);
|
|
662
|
+
}
|
|
663
|
+
const beforeSuccess = await environment.admin.inspectCubeManagementState(cubeA);
|
|
664
|
+
const success = await operation.invoke(credentialA, cubeA);
|
|
665
|
+
expectStatus(success, operation.successStatus, `${operation.name} by managing principal`);
|
|
666
|
+
invariant(!same(await environment.admin.inspectCubeManagementState(cubeA), beforeSuccess), `${operation.name} managing success did not mutate its declared state.`);
|
|
667
|
+
}
|
|
668
|
+
return {
|
|
669
|
+
operation_count: operations.length,
|
|
670
|
+
manage_success: true,
|
|
671
|
+
read_write_status: 403,
|
|
672
|
+
drone_session_status: 403,
|
|
673
|
+
hidden_status: 404,
|
|
674
|
+
denied_mutations: 0,
|
|
675
|
+
role_labels_authoritative: false,
|
|
676
|
+
};
|
|
677
|
+
});
|
|
678
|
+
await record('drones.reassign-invariants', async () => {
|
|
679
|
+
workerRoleB = await environment.admin.createRole(cubeA, {
|
|
680
|
+
roleClass: 'worker', isHumanSeat: false,
|
|
681
|
+
});
|
|
682
|
+
const humanSourceRole = await environment.admin.createRole(cubeA, {
|
|
683
|
+
roleClass: 'worker', isHumanSeat: true,
|
|
684
|
+
});
|
|
685
|
+
const queenRole = await environment.admin.createRole(cubeA, {
|
|
686
|
+
roleClass: 'queen', isHumanSeat: false,
|
|
687
|
+
});
|
|
688
|
+
const occupiedHumanRole = await environment.admin.createRole(cubeA, {
|
|
689
|
+
roleClass: 'worker', isHumanSeat: true,
|
|
690
|
+
});
|
|
691
|
+
const humanSource = await environment.admin.createDrone(principalA, cubeA, humanSourceRole);
|
|
692
|
+
await environment.admin.createDrone(principalA, cubeA, occupiedHumanRole);
|
|
693
|
+
const contender = await environment.admin.createDrone(principalA, cubeA, workerRoleA);
|
|
694
|
+
workerSession = await environment.admin.issueManagedDroneSession(managedWorker);
|
|
695
|
+
const reassigned = await environment.operations.reassignDrone(credentialA, cubeA, managedWorker, createProtocolEnvelope('reassign-worker', { role_id: workerRoleB.id }));
|
|
696
|
+
expectStatus(reassigned, 200, 'Worker reassignment');
|
|
697
|
+
const reassignedDrone = decodeReassignDroneResultEnvelope(reassigned.body).payload.drone;
|
|
698
|
+
invariant(reassignedDrone.id === managedWorker.id && reassignedDrone.role_id === workerRoleB.id, 'Reassignment response did not identify the persisted target role.');
|
|
699
|
+
expectError(await environment.operations.reassignDrone(workerSession, cubeA, managedWorker, createProtocolEnvelope('reassign-drone-session-denied', { role_id: workerRoleA.id })), 403, ErrorCode.ACCESS_DENIED, 'Drone-session reassignment');
|
|
700
|
+
expectError(await environment.operations.reassignDrone(credentialA, cubeA, managedWorker, createProtocolEnvelope('reassign-queen-denied', { role_id: queenRole.id })), 403, ErrorCode.ACCESS_DENIED, 'Worker-to-queen reassignment');
|
|
701
|
+
const promoted = await environment.operations.reassignDrone(credentialA, cubeA, humanSource, createProtocolEnvelope('reassign-queen-allowed', { role_id: queenRole.id }));
|
|
702
|
+
expectStatus(promoted, 200, 'Human-seat-to-queen reassignment');
|
|
703
|
+
invariant(decodeReassignDroneResultEnvelope(promoted.body).payload.drone.role_id === queenRole.id, 'Human-seat-to-queen reassignment did not persist.');
|
|
704
|
+
expectError(await environment.operations.reassignDrone(credentialA, cubeA, contender, createProtocolEnvelope('reassign-occupied-denied', { role_id: occupiedHumanRole.id })), 409, ErrorCode.ROLE_IN_USE, 'Occupied human-seat reassignment');
|
|
705
|
+
return {
|
|
706
|
+
worker_reassigned: true,
|
|
707
|
+
drone_session_denied: true,
|
|
708
|
+
queen_requires_human_seat: true,
|
|
709
|
+
occupied_human_seat_denied: true,
|
|
710
|
+
};
|
|
711
|
+
});
|
|
712
|
+
await record('security.cross-cube-drone-management', async () => {
|
|
713
|
+
const foreignRole = await environment.admin.createRole(cubeB, {
|
|
714
|
+
roleClass: 'worker', isHumanSeat: false,
|
|
715
|
+
});
|
|
716
|
+
const foreignDrone = await environment.admin.createDrone(principalB, cubeB, foreignRole);
|
|
717
|
+
expectError(await environment.operations.reassignDrone(credentialA, cubeB, foreignDrone, createProtocolEnvelope('cross-cube-reassign', { role_id: foreignRole.id })), 404, ErrorCode.NOT_FOUND, 'Cross-cube reassignment');
|
|
718
|
+
expectError(await environment.operations.evictDrone(credentialA, cubeB, foreignDrone, createProtocolEnvelope('cross-cube-evict', {})), 404, ErrorCode.NOT_FOUND, 'Cross-cube eviction');
|
|
719
|
+
expectError(await environment.operations.reassignDrone(credentialA, cubeA, foreignDrone, createProtocolEnvelope('foreign-drone-local-route', { role_id: workerRoleA.id })), 404, ErrorCode.NOT_FOUND, 'Foreign drone reassignment through authorized cube route');
|
|
720
|
+
expectError(await environment.operations.evictDrone(credentialA, cubeA, foreignDrone, createProtocolEnvelope('foreign-drone-local-evict', {})), 404, ErrorCode.NOT_FOUND, 'Foreign drone eviction through authorized cube route');
|
|
721
|
+
expectError(await environment.operations.reassignDrone(credentialA, cubeA, managedWorker, createProtocolEnvelope('foreign-role-local-drone', { role_id: foreignRole.id })), 404, ErrorCode.NOT_FOUND, 'Foreign role reassignment through authorized cube route');
|
|
722
|
+
await environment.admin.grantCube(principalA, cubeB, 'read');
|
|
723
|
+
const foreignDroneBefore = await environment.admin.inspectManagedDrone(foreignDrone);
|
|
724
|
+
expectError(await environment.operations.reassignDrone(workerSession, cubeB, foreignDrone, createProtocolEnvelope('bound-drone-cross-cube-reassign', { role_id: foreignRole.id })), 404, ErrorCode.NOT_FOUND, 'Bound drone cross-cube reassignment');
|
|
725
|
+
invariant(same(await environment.admin.inspectManagedDrone(foreignDrone), foreignDroneBefore), 'Bound drone cross-cube reassignment mutated the foreign target.');
|
|
726
|
+
expectError(await environment.operations.evictDrone(workerSession, cubeB, foreignDrone, createProtocolEnvelope('bound-drone-cross-cube-evict', {})), 404, ErrorCode.NOT_FOUND, 'Bound drone cross-cube eviction');
|
|
727
|
+
invariant(same(await environment.admin.inspectManagedDrone(foreignDrone), foreignDroneBefore), 'Bound drone cross-cube eviction mutated the foreign target.');
|
|
728
|
+
return {
|
|
729
|
+
unauthorized_cube_status: 404,
|
|
730
|
+
foreign_drone_status: 404,
|
|
731
|
+
foreign_role_status: 404,
|
|
732
|
+
bound_drone_cross_cube_status: 404,
|
|
733
|
+
code: ErrorCode.NOT_FOUND,
|
|
734
|
+
};
|
|
735
|
+
});
|
|
736
|
+
await record('drones.evict-terminal-signal', async () => {
|
|
737
|
+
const evictedDrone = await environment.admin.createDrone(principalA, cubeA, workerRoleA);
|
|
738
|
+
const evictedCredential = await environment.admin.issueManagedDroneSession(evictedDrone);
|
|
739
|
+
expectStatus(await environment.operations.read(evictedCredential, cubeA, createProtocolEnvelope('evict-probe-before', { cursor: null, limit: 1 })), 200, 'Pre-eviction seat probe');
|
|
740
|
+
const before = await environment.operations.listDrones(credentialA, cubeA);
|
|
741
|
+
expectStatus(before, 200, 'Pre-eviction roster');
|
|
742
|
+
invariant(listedDroneIds(before).includes(evictedDrone.id), 'Active drone was absent from roster.');
|
|
743
|
+
const evicted = await environment.operations.evictDrone(credentialA, cubeA, evictedDrone, createProtocolEnvelope('evict-managed-drone', {}));
|
|
744
|
+
expectStatus(evicted, 200, 'Drone eviction');
|
|
745
|
+
invariant(same(decodeEvictDroneResultEnvelope(evicted.body).payload, {
|
|
746
|
+
drone_id: evictedDrone.id,
|
|
747
|
+
evicted: true,
|
|
748
|
+
}), 'Eviction response did not identify the terminal seat.');
|
|
749
|
+
invariant(same(await environment.admin.inspectManagedDrone(evictedDrone), {
|
|
750
|
+
role_id: workerRoleA.id,
|
|
751
|
+
evicted: true,
|
|
752
|
+
session_revoked: true,
|
|
753
|
+
}), 'Eviction did not atomically mark the drone evicted and revoke its session.');
|
|
754
|
+
const after = await environment.operations.listDrones(credentialA, cubeA);
|
|
755
|
+
expectStatus(after, 200, 'Post-eviction roster');
|
|
756
|
+
invariant(!listedDroneIds(after).includes(evictedDrone.id), 'Evicted drone remained in roster.');
|
|
757
|
+
expectError(await environment.operations.append(credentialA, cubeA, createProtocolEnvelope('evict-direct-target', {
|
|
758
|
+
message: 'must-not-fan-out',
|
|
759
|
+
visibility: 'direct',
|
|
760
|
+
recipientDroneIds: [evictedDrone.id],
|
|
761
|
+
})), 404, ErrorCode.NOT_FOUND, 'Evicted direct recipient');
|
|
762
|
+
expectError(await environment.operations.read(evictedCredential, cubeA, createProtocolEnvelope('evict-probe-after', { cursor: null, limit: 1 })), PROTOCOL_HTTP_CONTRACT.drone_evicted_status, ErrorCode.DRONE_EVICTED, 'Evicted seat probe');
|
|
763
|
+
return {
|
|
764
|
+
eviction_status: 200,
|
|
765
|
+
roster_visible: false,
|
|
766
|
+
fanout_reachable: false,
|
|
767
|
+
old_bearer_status: 410,
|
|
768
|
+
old_bearer_code: ErrorCode.DRONE_EVICTED,
|
|
769
|
+
};
|
|
770
|
+
});
|
|
771
|
+
await record('security.drone-session-rejection-causes', async () => {
|
|
772
|
+
const revokedDrone = await environment.admin.createDrone(principalA, cubeA, workerRoleA);
|
|
773
|
+
const expiredDrone = await environment.admin.createDrone(principalA, cubeA, workerRoleA);
|
|
774
|
+
const revokedCredential = await environment.admin.issueManagedDroneSession(revokedDrone);
|
|
775
|
+
const expiredCredential = await environment.admin.issueManagedDroneSession(expiredDrone);
|
|
776
|
+
await environment.admin.revokeManagedDroneSession(revokedDrone);
|
|
777
|
+
await environment.admin.expireManagedDroneSession(expiredDrone);
|
|
778
|
+
for (const [label, credential, code] of [
|
|
779
|
+
['revoked', revokedCredential, ErrorCode.SESSION_REVOKED],
|
|
780
|
+
['expired', expiredCredential, ErrorCode.AUTH_EXPIRED],
|
|
781
|
+
]) {
|
|
782
|
+
expectError(await environment.operations.read(credential, cubeA, createProtocolEnvelope(`${label}-seat-probe`, { cursor: null, limit: 1 })), 401, code, `${label} seat probe`);
|
|
783
|
+
}
|
|
784
|
+
return {
|
|
785
|
+
revoked_status: 401,
|
|
786
|
+
expired_status: 401,
|
|
787
|
+
revoked_code: ErrorCode.SESSION_REVOKED,
|
|
788
|
+
expired_code: ErrorCode.AUTH_EXPIRED,
|
|
789
|
+
};
|
|
790
|
+
});
|
|
532
791
|
await record('security.active-stream-revocation', async () => {
|
|
533
792
|
invariant(liveCursor, 'Stream fixture did not produce a live cursor.');
|
|
534
793
|
const opened = await environment.operations.openStream(credentialA, cubeA, liveCursor);
|
|
@@ -562,17 +821,4 @@ export async function runAdapterConformance(environment, options = {}) {
|
|
|
562
821
|
const normalizedTranscript = results.map(({ id, observations }) => ({ id, observations }));
|
|
563
822
|
return { ok: results.every((result) => result.ok), results, normalizedTranscript };
|
|
564
823
|
}
|
|
565
|
-
export async function runEquivalentAdapterConformance(cloud, local, options = {}) {
|
|
566
|
-
const [cloudReport, localReport] = await Promise.all([
|
|
567
|
-
runAdapterConformance(cloud, options),
|
|
568
|
-
runAdapterConformance(local, options),
|
|
569
|
-
]);
|
|
570
|
-
const equivalent = same(cloudReport.normalizedTranscript, localReport.normalizedTranscript);
|
|
571
|
-
return {
|
|
572
|
-
ok: cloudReport.ok && localReport.ok && equivalent,
|
|
573
|
-
cloud: cloudReport,
|
|
574
|
-
local: localReport,
|
|
575
|
-
equivalent,
|
|
576
|
-
};
|
|
577
|
-
}
|
|
578
824
|
//# sourceMappingURL=adapter.js.map
|