borgmcp-shared 1.0.0 → 1.1.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/CONTRIBUTING.md +4 -1
- package/README.md +28 -5
- package/RELEASES.md +2 -3
- package/dist/conformance/adapter.d.ts +6 -101
- package/dist/conformance/adapter.d.ts.map +1 -1
- package/dist/conformance/adapter.js +528 -267
- package/dist/conformance/adapter.js.map +1 -1
- package/dist/conformance/index.d.ts +2 -39
- package/dist/conformance/index.d.ts.map +1 -1
- package/dist/conformance/index.js +13 -42
- package/dist/conformance/index.js.map +1 -1
- package/dist/protocol/contract.d.ts +2 -2
- package/dist/protocol/contract.d.ts.map +1 -1
- package/dist/protocol/contract.js +3 -3
- package/dist/protocol/contract.js.map +1 -1
- package/dist/protocol/errors.d.ts +0 -14
- package/dist/protocol/errors.d.ts.map +1 -1
- package/dist/protocol/errors.js +0 -8
- package/dist/protocol/errors.js.map +1 -1
- package/dist/protocol/version.d.ts +1 -1
- package/dist/protocol/version.js +1 -1
- package/dist/templates.d.ts +0 -1
- package/dist/templates.d.ts.map +1 -1
- package/dist/templates.js +1 -2
- package/dist/templates.js.map +1 -1
- package/docs/compatibility.md +11 -0
- package/docs/enrollment.md +6 -9
- package/docs/releases/1.0.1.md +5 -0
- package/docs/releases/1.1.0.md +19 -0
- package/docs/releasing.md +13 -23
- package/package.json +1 -2
- package/src/conformance/adapter.ts +779 -614
- package/src/conformance/index.ts +19 -62
- package/src/protocol/contract.ts +3 -3
- package/src/protocol/errors.ts +0 -17
- package/src/protocol/version.ts +2 -2
- package/src/templates.ts +1 -3
- package/docs/release-records.json +0 -247
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ErrorCode, compareLogCursor, createProtocolEnvelope, decodeAssociateRepositoryCubeResponseEnvelope, decodeCreateCubeResponseEnvelope, decodeDeleteCubeResponseEnvelope, decodeAppendLogResultEnvelope, decodeAckStatusResultEnvelope, decodeEntryQueryResultEnvelope, decodeDecisionResultEnvelope, decodeDecisionsResultEnvelope, decodeEnrollmentExchangeResponseEnvelope, decodeInvitationArtifact, decodeAttachResponseEnvelope,
|
|
1
|
+
import { ErrorCode, compareLogCursor, createProtocolEnvelope, decodeAssociateRepositoryCubeResponseEnvelope, decodeCreateCubeResponseEnvelope, decodeDeleteCubeResponseEnvelope, decodeAppendLogResultEnvelope, decodeAckStatusResultEnvelope, decodeEntryQueryResultEnvelope, decodeDecisionResultEnvelope, decodeDecisionsResultEnvelope, decodeEnrollmentExchangeResponseEnvelope, decodeInvitationArtifact, decodeAttachResponseEnvelope, decodeEvictDroneResultEnvelope, decodeDeleteRoleResultEnvelope, decodeRoleRationaleResultEnvelope, decodeProtocolEnvelope, decodeProtocolErrorEnvelope, decodeProtocolTagPreflight, decodeReadLogResultEnvelope, decodeResolveRepositoryCubeResponseEnvelope, decodeReassignDroneResultEnvelope, decodeUpdateDroneRuntimeMetadataResponseEnvelope, decodeGetDocumentResultEnvelope, decodeListDocumentsResultEnvelope, decodePutDocumentResultEnvelope, decodeRemoveDocumentResultEnvelope, decodeSseFrames, PROTOCOL_LIMIT_CEILINGS, PROTOCOL_HTTP_CONTRACT, PROTOCOL_VERSION, ROLE_IN_USE_DELETE_MESSAGE, utf8ByteLength, } from '../protocol/index.js';
|
|
2
2
|
import { APPEND_LOG_IDEMPOTENCY_CONFORMANCE, CREATE_CUBE_ASSOCIATION_CONFORMANCE, CREATE_CUBE_RETRY_CONFORMANCE, INVITATION_ARTIFACT_CONFORMANCE, ENROLLMENT_RETRY_CONFORMANCE, DOCUMENT_CONFORMANCE, } from './index.js';
|
|
3
3
|
export const ADAPTER_CONFORMANCE_FIXTURES = [
|
|
4
4
|
{ id: 'http.unauthenticated-liveness', area: 'http' },
|
|
@@ -37,6 +37,33 @@ export const ADAPTER_CONFORMANCE_FIXTURES = [
|
|
|
37
37
|
{ id: 'security.active-stream-revocation', area: 'security' },
|
|
38
38
|
{ id: 'cubes.delete-terminal-cascade', area: 'cubes' },
|
|
39
39
|
];
|
|
40
|
+
const FIXTURE_PREREQUISITES = {
|
|
41
|
+
'repository.explicit-association': ['shared.enrolled-principals'],
|
|
42
|
+
'documents.lifecycle': ['shared.enrolled-principals'],
|
|
43
|
+
'security.adapter-boundary-injection': ['shared.enrolled-principals'],
|
|
44
|
+
'security.oversize-request': ['shared.enrolled-principals'],
|
|
45
|
+
'security.cross-cube-isolation': ['shared.enrolled-principals'],
|
|
46
|
+
'log.mandatory-addressing': ['shared.enrolled-principals'],
|
|
47
|
+
'log.entry-query': ['shared.enrolled-principals'],
|
|
48
|
+
'log.read-cursor-tuple': ['shared.enrolled-principals'],
|
|
49
|
+
'sse.replay-live-transition': ['shared.enrolled-principals'],
|
|
50
|
+
'cursor.explicit-expiry': ['shared.enrolled-principals'],
|
|
51
|
+
'acks.idempotent': ['shared.enrolled-principals'],
|
|
52
|
+
'acks.status-query': ['shared.enrolled-principals'],
|
|
53
|
+
'claims.durable-noncursor': ['shared.enrolled-principals'],
|
|
54
|
+
'decisions.topic-supersession': ['shared.enrolled-principals'],
|
|
55
|
+
'security.manage-access-matrix': ['shared.enrolled-principals'],
|
|
56
|
+
'drones.reassign-invariants': ['shared.enrolled-principals'],
|
|
57
|
+
'security.cross-cube-drone-management': ['shared.enrolled-principals'],
|
|
58
|
+
'drones.evict-terminal-signal': ['shared.enrolled-principals'],
|
|
59
|
+
'metadata.attach-report': ['shared.enrolled-principals'],
|
|
60
|
+
'metadata.self-heal-patch': ['shared.enrolled-principals', 'shared.metadata-baseline'],
|
|
61
|
+
'security.metadata-invalid-atomic': ['shared.enrolled-principals', 'shared.metadata-baseline'],
|
|
62
|
+
'security.metadata-own-seat': ['shared.enrolled-principals', 'shared.metadata-baseline'],
|
|
63
|
+
'security.metadata-cross-cube-isolation': ['shared.enrolled-principals', 'shared.metadata-baseline'],
|
|
64
|
+
'security.metadata-noninterference': ['shared.enrolled-principals', 'shared.metadata-baseline'],
|
|
65
|
+
'security.metadata-secret-non-echo': ['shared.enrolled-principals', 'shared.metadata-baseline'],
|
|
66
|
+
};
|
|
40
67
|
const DEFAULT_STREAM_DEADLINE_MS = 5_000;
|
|
41
68
|
const DEFAULT_PENDING_PROBE_MS = 25;
|
|
42
69
|
function invariant(condition, message) {
|
|
@@ -160,10 +187,19 @@ function listedDroneIds(response) {
|
|
|
160
187
|
});
|
|
161
188
|
}).payload;
|
|
162
189
|
}
|
|
163
|
-
function
|
|
164
|
-
|
|
165
|
-
invariant(
|
|
166
|
-
|
|
190
|
+
function listedDrone(response, droneId) {
|
|
191
|
+
return decodeProtocolEnvelope(response.body, (payload) => {
|
|
192
|
+
invariant(typeof payload === 'object' && payload !== null, 'Roster payload is not an object.');
|
|
193
|
+
const drones = payload.drones;
|
|
194
|
+
invariant(Array.isArray(drones), 'Roster payload omitted drones.');
|
|
195
|
+
const drone = drones.find((candidate) => typeof candidate === 'object' && candidate !== null &&
|
|
196
|
+
candidate.id === droneId);
|
|
197
|
+
return drone ? { ...drone } : null;
|
|
198
|
+
}).payload;
|
|
199
|
+
}
|
|
200
|
+
function withoutRuntimeMetadata(drone) {
|
|
201
|
+
const { agent_kind: _agentKind, reported_model: _reportedModel, working_repo_name: _workingRepoName, working_repo_origin: _workingRepoOrigin, runtime_metadata_reported: _runtimeMetadataReported, ...rest } = drone;
|
|
202
|
+
return rest;
|
|
167
203
|
}
|
|
168
204
|
function assertEnrollmentErrorIsSecretFree(response, requests, description) {
|
|
169
205
|
const diagnostic = JSON.stringify(response.body);
|
|
@@ -189,16 +225,30 @@ export async function runAdapterConformance(environment, options = {}) {
|
|
|
189
225
|
invariant(streamDeadlineMs > 0, 'streamDeadlineMs must be positive.');
|
|
190
226
|
invariant(pendingProbeMs > 0, 'pendingProbeMs must be positive.');
|
|
191
227
|
const results = [];
|
|
192
|
-
const
|
|
228
|
+
const prerequisiteFailures = new Map();
|
|
229
|
+
const record = async (id, execute, prerequisites = []) => {
|
|
230
|
+
const required = [...(FIXTURE_PREREQUISITES[id] ?? []), ...prerequisites];
|
|
231
|
+
const failedPrerequisite = required.find((name) => prerequisiteFailures.has(name));
|
|
232
|
+
if (failedPrerequisite) {
|
|
233
|
+
results.push({
|
|
234
|
+
id,
|
|
235
|
+
ok: false,
|
|
236
|
+
skipped: true,
|
|
237
|
+
observations: {},
|
|
238
|
+
error: `Skipped because prerequisite ${failedPrerequisite} failed: ${prerequisiteFailures.get(failedPrerequisite)}`,
|
|
239
|
+
});
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
193
242
|
try {
|
|
194
243
|
results.push({ id, ok: true, observations: await execute() });
|
|
195
244
|
}
|
|
196
245
|
catch (error) {
|
|
246
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
197
247
|
results.push({
|
|
198
248
|
id,
|
|
199
249
|
ok: false,
|
|
200
250
|
observations: {},
|
|
201
|
-
error:
|
|
251
|
+
error: message,
|
|
202
252
|
});
|
|
203
253
|
}
|
|
204
254
|
};
|
|
@@ -216,13 +266,11 @@ export async function runAdapterConformance(environment, options = {}) {
|
|
|
216
266
|
return { status: 204, bodyless: true };
|
|
217
267
|
});
|
|
218
268
|
await record('protocol.credential-free-preflight', async () => {
|
|
219
|
-
const before = await environment.admin.observeAuthorityState();
|
|
220
269
|
const response = await environment.operations.protocol(null);
|
|
221
270
|
expectStatus(response, 200, 'Credential-free protocol-tag preflight');
|
|
222
271
|
const preflight = decodeProtocolTagPreflight(response.body);
|
|
223
272
|
invariant(Object.keys(preflight).length === 1 && preflight.protocol_version === PROTOCOL_VERSION, 'Protocol-tag preflight exposed more than the exact tag.');
|
|
224
|
-
|
|
225
|
-
return { authenticated: false, mutation_free: true, protocol_version: preflight.protocol_version };
|
|
273
|
+
return { authenticated: false, protocol_version: preflight.protocol_version };
|
|
226
274
|
});
|
|
227
275
|
await record('enrollment.invitation-artifact', async () => {
|
|
228
276
|
const errors = [];
|
|
@@ -260,19 +308,11 @@ export async function runAdapterConformance(environment, options = {}) {
|
|
|
260
308
|
const invitation = await environment.admin.issueSingleUseInvitation(principal, purpose);
|
|
261
309
|
const initialPayload = { ...vector.initial, invitation };
|
|
262
310
|
const retryPayload = { ...vector.retry, invitation };
|
|
263
|
-
const beforeInitial = await environment.admin.observeAuthorityState();
|
|
264
311
|
const initialResponse = await environment.operations.enroll(createProtocolEnvelope(`retry-${index}-initial`, initialPayload));
|
|
265
312
|
expectStatus(initialResponse, 201, `${vector.name} initial request`);
|
|
266
313
|
const initial = decodeEnrollmentExchangeResponseEnvelope(initialResponse.body).payload;
|
|
267
314
|
invariant(initial.purpose === purpose, `${purpose} ${vector.name} returned the wrong purpose.`);
|
|
268
315
|
invariant(same(initial.server_capabilities, purpose === 'owner' ? ['create_cube'] : []), `${purpose} ${vector.name} returned incorrect server authority.`);
|
|
269
|
-
const afterInitial = await environment.admin.observeAuthorityState();
|
|
270
|
-
assertStateDelta(beforeInitial, afterInitial, {
|
|
271
|
-
enrolled_clients: 1,
|
|
272
|
-
enrollment_claims: 1,
|
|
273
|
-
server_capabilities: purpose === 'owner' ? 1 : 0,
|
|
274
|
-
}, `${purpose} ${vector.name} initial request`);
|
|
275
|
-
const beforeRetry = await environment.admin.observeAuthorityState();
|
|
276
316
|
const retryResponse = await environment.operations.enroll(createProtocolEnvelope(`retry-${index}-retry`, retryPayload));
|
|
277
317
|
if (vector.expected.outcome === 'stable_non_secret_identity') {
|
|
278
318
|
expectStatus(retryResponse, 201, `${purpose} ${vector.name}`);
|
|
@@ -286,12 +326,25 @@ export async function runAdapterConformance(environment, options = {}) {
|
|
|
286
326
|
expectError(retryResponse, vector.expected.status, ErrorCode.AUTH_INVALID, `${purpose} ${vector.name}`);
|
|
287
327
|
assertEnrollmentErrorIsSecretFree(retryResponse, [initialPayload, retryPayload], `${purpose} ${vector.name}`);
|
|
288
328
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
329
|
+
const credentialProbeCube = await environment.admin.createCube(`${purpose}-credential-probe-${index}`);
|
|
330
|
+
await environment.admin.grantCube(principal, credentialProbeCube, 'read');
|
|
331
|
+
expectStatus(await environment.operations.read(initialPayload.client_credential, credentialProbeCube, createProtocolEnvelope(`retry-${index}-credential-probe`, { cursor: null, limit: 1 })), 200, `${purpose} ${vector.name} original credential probe`);
|
|
332
|
+
const capabilityProbe = await environment.operations.createCube(initialPayload.client_credential, createProtocolEnvelope(`retry-${index}-${purpose}-capability-probe`, {
|
|
333
|
+
retry_key: `10000000-0000-4000-8000-${String(index + (purpose === 'owner' ? 700 : 600)).padStart(12, '0')}`,
|
|
334
|
+
name: `${purpose} capability probe ${index}`,
|
|
335
|
+
working_repo_name: `${purpose}-capability-probe-${index}`,
|
|
336
|
+
repository: {
|
|
337
|
+
kind: 'local',
|
|
338
|
+
value: `20000000-0000-4000-8000-${String(index + (purpose === 'owner' ? 700 : 600)).padStart(12, '0')}`,
|
|
339
|
+
},
|
|
340
|
+
template: 'software-dev',
|
|
341
|
+
}));
|
|
342
|
+
if (purpose === 'owner') {
|
|
343
|
+
expectStatus(capabilityProbe, 201, `${purpose} ${vector.name} capability probe`);
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
expectError(capabilityProbe, 403, ErrorCode.ACCESS_DENIED, `${purpose} ${vector.name} capability probe`);
|
|
347
|
+
}
|
|
295
348
|
}
|
|
296
349
|
catch (error) {
|
|
297
350
|
retryVectorErrors.push(`${purpose} ${vector.name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -306,7 +359,6 @@ export async function runAdapterConformance(environment, options = {}) {
|
|
|
306
359
|
const ordinaryInvitation = await environment.admin.issueSingleUseInvitation(ordinaryPrincipal, 'client');
|
|
307
360
|
const ownerCredential = `${'Q'.repeat(42)}U`;
|
|
308
361
|
const ordinaryCredential = `${'Y'.repeat(42)}U`;
|
|
309
|
-
const beforeAuthorityEnrollment = await environment.admin.observeAuthorityState();
|
|
310
362
|
const ownerResponse = await environment.operations.enroll(createProtocolEnvelope('owner-enroll', {
|
|
311
363
|
invitation: ownerInvitation,
|
|
312
364
|
retry_key: '00000000-0000-4000-8000-000000000211',
|
|
@@ -325,7 +377,6 @@ export async function runAdapterConformance(environment, options = {}) {
|
|
|
325
377
|
const ordinary = decodeEnrollmentExchangeResponseEnvelope(ordinaryResponse.body).payload;
|
|
326
378
|
invariant(owner.purpose === 'owner' && same(owner.server_capabilities, ['create_cube']), 'Owner enrollment lacked exact create-cube authority.');
|
|
327
379
|
invariant(ordinary.purpose === 'client' && ordinary.server_capabilities.length === 0, 'Ordinary enrollment gained authority.');
|
|
328
|
-
assertStateDelta(beforeAuthorityEnrollment, await environment.admin.observeAuthorityState(), { enrolled_clients: 2, enrollment_claims: 2, server_capabilities: 1 }, 'Owner and ordinary enrollment');
|
|
329
380
|
const cubeRequest = {
|
|
330
381
|
retry_key: '00000000-0000-4000-8000-000000000213',
|
|
331
382
|
name: 'Repository One',
|
|
@@ -334,64 +385,76 @@ export async function runAdapterConformance(environment, options = {}) {
|
|
|
334
385
|
kind: 'origin',
|
|
335
386
|
value: 'https://github.com/Byte-Ventures/repository-one',
|
|
336
387
|
},
|
|
337
|
-
template: '
|
|
388
|
+
template: 'software-dev',
|
|
338
389
|
};
|
|
339
390
|
const droneCredential = await environment.admin.issueDroneSession(ownerPrincipal);
|
|
340
|
-
const beforeDeniedCreate = await environment.admin.observeAuthorityState();
|
|
341
391
|
expectSecretFreeError(await environment.operations.createCube(null, createProtocolEnvelope('cube-missing-auth', cubeRequest)), 401, ErrorCode.AUTH_MISSING, 'Missing-auth cube create', [cubeRequest.retry_key]);
|
|
342
392
|
expectSecretFreeError(await environment.operations.createCube('invalid-credential', createProtocolEnvelope('cube-invalid-auth', cubeRequest)), 401, ErrorCode.AUTH_INVALID, 'Invalid-auth cube create', [cubeRequest.retry_key]);
|
|
343
393
|
expectSecretFreeError(await environment.operations.createCube(ordinaryCredential, createProtocolEnvelope('cube-denied', cubeRequest)), 403, ErrorCode.ACCESS_DENIED, 'Ordinary cube create', [cubeRequest.retry_key]);
|
|
344
394
|
expectSecretFreeError(await environment.operations.createCube(droneCredential, createProtocolEnvelope('cube-drone-denied', cubeRequest)), 403, ErrorCode.ACCESS_DENIED, 'Drone-session cube create', [cubeRequest.retry_key]);
|
|
345
|
-
assertStateDelta(beforeDeniedCreate, await environment.admin.observeAuthorityState(), {}, 'Denied ordinary cube create');
|
|
346
|
-
const beforeCreate = await environment.admin.observeAuthorityState();
|
|
347
395
|
const createdResponse = await environment.operations.createCube(ownerCredential, createProtocolEnvelope('cube-create', cubeRequest));
|
|
348
396
|
expectStatus(createdResponse, 201, 'Owner cube create');
|
|
349
397
|
const created = decodeCreateCubeResponseEnvelope(createdResponse.body).payload;
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
398
|
+
const createdRoleProbes = new Map();
|
|
399
|
+
for (const [kind, roleId, roleClass, isHumanSeat, marker] of [
|
|
400
|
+
['human-seat', created.human_seat_role_id, 'queen', true, 'H'],
|
|
401
|
+
['default-worker', created.default_worker_role_id, 'worker', false, 'D'],
|
|
402
|
+
]) {
|
|
403
|
+
const sessionCredential = marker.repeat(43);
|
|
404
|
+
const attached = await environment.operations.attach(ownerCredential, createProtocolEnvelope(`cube-create-${kind}-probe`, {
|
|
405
|
+
cube_id: created.cube_id,
|
|
406
|
+
role_id: roleId,
|
|
407
|
+
session_credential: sessionCredential,
|
|
408
|
+
}));
|
|
409
|
+
expectStatus(attached, 200, `Created ${kind} role probe`);
|
|
410
|
+
const attachedPayload = decodeAttachResponseEnvelope(attached.body).payload;
|
|
411
|
+
const attachedRole = attachedPayload.role;
|
|
412
|
+
invariant(attachedRole.id === roleId &&
|
|
413
|
+
attachedRole.role_class === roleClass &&
|
|
414
|
+
attachedRole.is_human_seat === isHumanSeat, `Created ${kind} role identity did not match its protocol role.`);
|
|
415
|
+
createdRoleProbes.set(kind, {
|
|
416
|
+
role: attachedRole,
|
|
417
|
+
droneId: attachedPayload.drone.id,
|
|
418
|
+
sessionCredential,
|
|
419
|
+
});
|
|
420
|
+
}
|
|
367
421
|
const retriedCreateResponse = await environment.operations.createCube(ownerCredential, createProtocolEnvelope('cube-retry', cubeRequest));
|
|
368
422
|
expectStatus(retriedCreateResponse, 201, 'Exact cube-create retry');
|
|
369
423
|
const retriedCreate = decodeCreateCubeResponseEnvelope(retriedCreateResponse.body).payload;
|
|
370
424
|
invariant(retriedCreate.result === 'resolved', 'Exact cube-create retry did not report resolved readback.');
|
|
371
425
|
invariant(same({ ...retriedCreate, result: 'created' }, created), 'Exact cube-create retry returned different authoritative fields.');
|
|
372
|
-
assertStateDelta(beforeCreateRetry, await environment.admin.observeAuthorityState(), {}, 'Exact cube-create retry');
|
|
373
426
|
const displayRetryVector = CREATE_CUBE_RETRY_CONFORMANCE.find((vector) => vector.expected.outcome === 'resolved_response' &&
|
|
374
427
|
vector.retry.working_repo_name !== vector.initial.working_repo_name);
|
|
375
428
|
invariant(displayRetryVector !== undefined, 'Missing repository display readback vector.');
|
|
376
|
-
const beforeDisplayRetry = await environment.admin.observeAuthorityState();
|
|
377
429
|
const displayRetryResponse = await environment.operations.createCube(ownerCredential, createProtocolEnvelope('cube-display-retry', {
|
|
378
430
|
...displayRetryVector.retry,
|
|
379
431
|
retry_key: cubeRequest.retry_key,
|
|
380
432
|
}));
|
|
381
433
|
expectStatus(displayRetryResponse, 201, 'Changed repository display retry');
|
|
382
434
|
invariant(same(decodeCreateCubeResponseEnvelope(displayRetryResponse.body).payload, retriedCreate), 'Changed repository display retry did not return stored authoritative display.');
|
|
383
|
-
assertStateDelta(beforeDisplayRetry, await environment.admin.observeAuthorityState(), {}, 'Changed repository display retry');
|
|
384
435
|
for (const [index, vector] of CREATE_CUBE_RETRY_CONFORMANCE.entries()) {
|
|
385
436
|
if (vector.expected.outcome !== 'retry_tuple_mismatch')
|
|
386
437
|
continue;
|
|
387
438
|
const mismatch = { ...vector.retry, retry_key: cubeRequest.retry_key };
|
|
388
|
-
const beforeCreateMismatch = await environment.admin.observeAuthorityState();
|
|
389
439
|
expectSecretFreeError(await environment.operations.createCube(ownerCredential, createProtocolEnvelope(`cube-mismatch-${index}`, mismatch)), 409, ErrorCode.INVALID_INPUT, `Cube-create ${vector.name}`, [cubeRequest.retry_key]);
|
|
390
|
-
|
|
440
|
+
}
|
|
441
|
+
for (const [kind, roleId] of [
|
|
442
|
+
['human-seat', created.human_seat_role_id],
|
|
443
|
+
['default-worker', created.default_worker_role_id],
|
|
444
|
+
]) {
|
|
445
|
+
const initialProbe = createdRoleProbes.get(kind);
|
|
446
|
+
invariant(initialProbe !== undefined, `Missing initial ${kind} role probe.`);
|
|
447
|
+
const attached = await environment.operations.attach(ownerCredential, createProtocolEnvelope(`cube-create-${kind}-post-refusal-probe`, {
|
|
448
|
+
cube_id: created.cube_id,
|
|
449
|
+
role_id: roleId,
|
|
450
|
+
session_credential: initialProbe.sessionCredential,
|
|
451
|
+
prior_drone_id: initialProbe.droneId,
|
|
452
|
+
}));
|
|
453
|
+
expectStatus(attached, 200, `Post-refusal ${kind} role probe`);
|
|
454
|
+
invariant(same(decodeAttachResponseEnvelope(attached.body).payload.role, initialProbe.role), `Cube-create retry refusal changed the full ${kind} role shape.`);
|
|
391
455
|
}
|
|
392
456
|
const sameAssociationVector = CREATE_CUBE_ASSOCIATION_CONFORMANCE.find((vector) => vector.expected.outcome === 'resolved');
|
|
393
457
|
invariant(sameAssociationVector !== undefined, 'Missing same-repository association vector.');
|
|
394
|
-
const beforeAssociationResolve = await environment.admin.observeAuthorityState();
|
|
395
458
|
const associationResolveResponse = await environment.operations.createCube(ownerCredential, createProtocolEnvelope('cube-association-resolve', {
|
|
396
459
|
...sameAssociationVector.request,
|
|
397
460
|
repository: cubeRequest.repository,
|
|
@@ -400,7 +463,6 @@ export async function runAdapterConformance(environment, options = {}) {
|
|
|
400
463
|
const associationResolved = decodeCreateCubeResponseEnvelope(associationResolveResponse.body).payload;
|
|
401
464
|
invariant(associationResolved.result === 'resolved', 'Associated repository did not resolve.');
|
|
402
465
|
invariant(same({ ...associationResolved, result: 'created' }, created), 'Associated repository did not return stored authoritative fields.');
|
|
403
|
-
assertStateDelta(beforeAssociationResolve, await environment.admin.observeAuthorityState(), sameAssociationVector.expected.authority_state_delta, 'Fresh retry for associated repository');
|
|
404
466
|
await environment.admin.grantCreateCubeCapability(ordinaryPrincipal);
|
|
405
467
|
const crossClientRequest = {
|
|
406
468
|
...cubeRequest,
|
|
@@ -410,35 +472,49 @@ export async function runAdapterConformance(environment, options = {}) {
|
|
|
410
472
|
value: '00000000-0000-4000-8000-000000000216',
|
|
411
473
|
},
|
|
412
474
|
};
|
|
413
|
-
const beforeCrossClientCreate = await environment.admin.observeAuthorityState();
|
|
414
475
|
const crossClientResponse = await environment.operations.createCube(ordinaryCredential, createProtocolEnvelope('cube-cross-client', crossClientRequest));
|
|
415
476
|
expectStatus(crossClientResponse, 201, 'Cross-client cube create with reused retry key');
|
|
416
477
|
const crossClientCreated = decodeCreateCubeResponseEnvelope(crossClientResponse.body).payload;
|
|
417
478
|
invariant(crossClientCreated.cube_id !== created.cube_id, 'Cross-client retry key reused another client\'s cube.');
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
479
|
+
for (const [kind, roleId, originalRoleId, marker] of [
|
|
480
|
+
['human-seat', crossClientCreated.human_seat_role_id, created.human_seat_role_id, 'V'],
|
|
481
|
+
['default-worker', crossClientCreated.default_worker_role_id, created.default_worker_role_id, 'W'],
|
|
482
|
+
]) {
|
|
483
|
+
const originalProbe = createdRoleProbes.get(kind);
|
|
484
|
+
invariant(originalProbe !== undefined, `Missing original ${kind} role probe.`);
|
|
485
|
+
const attached = await environment.operations.attach(ordinaryCredential, createProtocolEnvelope(`cube-cross-client-${kind}-probe`, {
|
|
486
|
+
cube_id: crossClientCreated.cube_id,
|
|
487
|
+
role_id: roleId,
|
|
488
|
+
session_credential: marker.repeat(43),
|
|
489
|
+
}));
|
|
490
|
+
expectStatus(attached, 200, `Cross-client ${kind} role probe`);
|
|
491
|
+
const attachedRole = decodeAttachResponseEnvelope(attached.body).payload.role;
|
|
492
|
+
invariant(same({ ...attachedRole, id: originalRoleId }, originalProbe.role), `Cross-client ${kind} role did not match the full template role shape.`);
|
|
493
|
+
}
|
|
421
494
|
const crossClientRetry = await environment.operations.createCube(ordinaryCredential, createProtocolEnvelope('cube-cross-client-retry', crossClientRequest));
|
|
422
495
|
expectStatus(crossClientRetry, 201, 'Exact cross-client cube-create retry');
|
|
423
496
|
const crossClientResolved = decodeCreateCubeResponseEnvelope(crossClientRetry.body).payload;
|
|
424
497
|
invariant(crossClientResolved.result === 'resolved', 'Exact cross-client cube-create retry did not report resolved readback.');
|
|
425
498
|
invariant(same({ ...crossClientResolved, result: 'created' }, crossClientCreated), 'Exact cross-client cube-create retry returned different authoritative fields.');
|
|
426
|
-
assertStateDelta(beforeCrossClientRetry, await environment.admin.observeAuthorityState(), {}, 'Exact cross-client cube-create retry');
|
|
427
499
|
const differentAssociationVector = CREATE_CUBE_ASSOCIATION_CONFORMANCE.find((vector) => vector.expected.outcome === 'created');
|
|
428
500
|
invariant(differentAssociationVector !== undefined, 'Missing different-repository association vector.');
|
|
429
|
-
const beforeSecondCreate = await environment.admin.observeAuthorityState();
|
|
430
501
|
const secondCreatedResponse = await environment.operations.createCube(ownerCredential, createProtocolEnvelope('cube-create-second', differentAssociationVector.request));
|
|
431
502
|
expectStatus(secondCreatedResponse, 201, 'Second cube create');
|
|
432
503
|
const secondCreated = decodeCreateCubeResponseEnvelope(secondCreatedResponse.body).payload;
|
|
433
504
|
invariant(secondCreated.cube_id !== created.cube_id, 'Fresh cube-create retry key reused an existing cube.');
|
|
434
|
-
assertStateDelta(beforeSecondCreate, await environment.admin.observeAuthorityState(), differentAssociationVector.expected.authority_state_delta, 'Second cube create');
|
|
435
505
|
await environment.admin.revokePrincipal(ownerPrincipal);
|
|
436
|
-
const beforeRevokedCreate = await environment.admin.observeAuthorityState();
|
|
437
506
|
expectSecretFreeError(await environment.operations.createCube(ownerCredential, createProtocolEnvelope('cube-revoked', {
|
|
438
507
|
...cubeRequest,
|
|
439
508
|
retry_key: '00000000-0000-4000-8000-000000000215',
|
|
440
509
|
})), 401, ErrorCode.SESSION_REVOKED, 'Revoked owner cube create', ['00000000-0000-4000-8000-000000000215']);
|
|
441
|
-
|
|
510
|
+
return {
|
|
511
|
+
enrollment_status: 201,
|
|
512
|
+
exact_retry_status: 201,
|
|
513
|
+
mismatched_retry: ErrorCode.AUTH_INVALID,
|
|
514
|
+
response_secret_free: true,
|
|
515
|
+
};
|
|
516
|
+
});
|
|
517
|
+
try {
|
|
442
518
|
await environment.admin.reset();
|
|
443
519
|
principalA = await environment.admin.createPrincipal('principal-a');
|
|
444
520
|
principalB = await environment.admin.createPrincipal('principal-b');
|
|
@@ -450,42 +526,24 @@ export async function runAdapterConformance(environment, options = {}) {
|
|
|
450
526
|
const invitationB = await environment.admin.issueSingleUseInvitation(principalB, 'client');
|
|
451
527
|
credentialA = 'A'.repeat(43);
|
|
452
528
|
credentialB = 'E'.repeat(43);
|
|
453
|
-
const
|
|
529
|
+
const enrolledAResponse = await environment.operations.enroll(createProtocolEnvelope('enroll-a1', {
|
|
454
530
|
invitation: invitationA,
|
|
455
531
|
retry_key: '00000000-0000-4000-8000-000000000201',
|
|
456
532
|
client_credential: credentialA,
|
|
457
533
|
client_name: 'conformance-a',
|
|
458
|
-
});
|
|
459
|
-
const
|
|
534
|
+
}));
|
|
535
|
+
const enrolledBResponse = await environment.operations.enroll(createProtocolEnvelope('enroll-b1', {
|
|
460
536
|
invitation: invitationB,
|
|
461
537
|
retry_key: '00000000-0000-4000-8000-000000000202',
|
|
462
538
|
client_credential: credentialB,
|
|
463
539
|
client_name: 'conformance-b',
|
|
464
|
-
});
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
invariant(enrolledA.purpose === 'client' && enrolledB.purpose === 'client', 'Ordinary enrollment returned owner authority.');
|
|
472
|
-
invariant(enrolledA.server_capabilities.length === 0 && enrolledB.server_capabilities.length === 0, 'Ordinary enrollment returned a server capability.');
|
|
473
|
-
invariant(!('credential' in enrolledA) && !('credential' in enrolledB), 'Enrollment response returned a bearer.');
|
|
474
|
-
const retriedAResponse = await environment.operations.enroll(enrollmentARequest);
|
|
475
|
-
expectStatus(retriedAResponse, 201, 'Exact enrollment retry');
|
|
476
|
-
invariant(JSON.stringify(decodeEnrollmentExchangeResponseEnvelope(retriedAResponse.body).payload) ===
|
|
477
|
-
JSON.stringify(enrolledA), 'Exact enrollment retry returned different identities.');
|
|
478
|
-
expectError(await environment.operations.enroll(createProtocolEnvelope('enroll-a-mismatch', {
|
|
479
|
-
...enrollmentARequest.payload,
|
|
480
|
-
retry_key: '00000000-0000-4000-8000-000000000203',
|
|
481
|
-
})), 401, ErrorCode.AUTH_INVALID, 'Enrollment retry mismatch');
|
|
482
|
-
return {
|
|
483
|
-
enrollment_status: 201,
|
|
484
|
-
exact_retry_status: 201,
|
|
485
|
-
mismatched_retry: ErrorCode.AUTH_INVALID,
|
|
486
|
-
response_secret_free: true,
|
|
487
|
-
};
|
|
488
|
-
});
|
|
540
|
+
}));
|
|
541
|
+
expectStatus(enrolledAResponse, 201, 'Shared principal A enrollment');
|
|
542
|
+
expectStatus(enrolledBResponse, 201, 'Shared principal B enrollment');
|
|
543
|
+
}
|
|
544
|
+
catch (error) {
|
|
545
|
+
prerequisiteFailures.set('shared.enrolled-principals', error instanceof Error ? error.message : String(error));
|
|
546
|
+
}
|
|
489
547
|
await record('repository.explicit-association', async () => {
|
|
490
548
|
const repository = {
|
|
491
549
|
kind: 'origin',
|
|
@@ -499,16 +557,24 @@ export async function runAdapterConformance(environment, options = {}) {
|
|
|
499
557
|
working_repo_name: 'legacy-repository',
|
|
500
558
|
repository,
|
|
501
559
|
};
|
|
502
|
-
const
|
|
560
|
+
const alternateResolveRequest = {
|
|
561
|
+
working_repo_name: 'other-repository',
|
|
562
|
+
repository: {
|
|
563
|
+
kind: 'origin',
|
|
564
|
+
value: 'https://github.com/Byte-Ventures/other-repository',
|
|
565
|
+
},
|
|
566
|
+
};
|
|
567
|
+
const resolve = async (requestId, request) => {
|
|
568
|
+
const response = await environment.operations.resolveRepositoryCube(credentialA, createProtocolEnvelope(requestId, request));
|
|
569
|
+
expectStatus(response, 200, requestId);
|
|
570
|
+
return decodeResolveRepositoryCubeResponseEnvelope(response.body).payload;
|
|
571
|
+
};
|
|
503
572
|
const noneResponse = await environment.operations.resolveRepositoryCube(credentialA, createProtocolEnvelope('repository-resolve-none', resolveRequest));
|
|
504
573
|
expectStatus(noneResponse, 200, 'Unassociated repository resolution');
|
|
505
574
|
invariant(same(decodeResolveRepositoryCubeResponseEnvelope(noneResponse.body).payload, { result: 'none' }), 'Unassociated repository did not return explicit none.');
|
|
506
|
-
|
|
575
|
+
invariant(same(await resolve('repository-resolve-none-readback', resolveRequest), { result: 'none' }), 'Read-only repository resolution created an association.');
|
|
507
576
|
const associationRequest = { cube_id: cubeA.id, ...resolveRequest };
|
|
508
|
-
const beforeDenied = await environment.admin.observeAuthorityState();
|
|
509
577
|
expectSecretFreeError(await environment.operations.associateRepositoryCube(credentialB, createProtocolEnvelope('repository-associate-denied', associationRequest)), 403, ErrorCode.ACCESS_DENIED, 'Inaccessible repository cube association', [associationRequest.cube_id, repository.value]);
|
|
510
|
-
assertStateDelta(beforeDenied, await environment.admin.observeAuthorityState(), {}, 'Denied repository association');
|
|
511
|
-
const beforeAssociation = await environment.admin.observeAuthorityState();
|
|
512
578
|
const associatedResponse = await environment.operations.associateRepositoryCube(credentialA, createProtocolEnvelope('repository-associate', associationRequest));
|
|
513
579
|
expectStatus(associatedResponse, 200, 'Explicit repository cube association');
|
|
514
580
|
const associated = decodeAssociateRepositoryCubeResponseEnvelope(associatedResponse.body).payload;
|
|
@@ -524,52 +590,40 @@ export async function runAdapterConformance(environment, options = {}) {
|
|
|
524
590
|
access: prepared.access,
|
|
525
591
|
};
|
|
526
592
|
invariant(same(associated, authoritative), 'Repository association did not return authoritative stored fields.');
|
|
527
|
-
assertStateDelta(beforeAssociation, await environment.admin.observeAuthorityState(), { repository_associations: 1 }, 'Explicit repository association');
|
|
528
|
-
const beforeResolve = await environment.admin.observeAuthorityState();
|
|
529
593
|
const resolvedResponse = await environment.operations.resolveRepositoryCube(credentialA, createProtocolEnvelope('repository-resolve', {
|
|
530
594
|
...resolveRequest,
|
|
531
595
|
working_repo_name: 'ignored-new-display',
|
|
532
596
|
}));
|
|
533
597
|
expectStatus(resolvedResponse, 200, 'Associated repository resolution');
|
|
534
598
|
invariant(same(decodeResolveRepositoryCubeResponseEnvelope(resolvedResponse.body).payload, authoritative), 'Associated repository did not return stored authoritative display.');
|
|
535
|
-
assertStateDelta(beforeResolve, await environment.admin.observeAuthorityState(), {}, 'Associated repository resolution');
|
|
536
|
-
const beforeRetry = await environment.admin.observeAuthorityState();
|
|
537
599
|
const retryResponse = await environment.operations.associateRepositoryCube(credentialA, createProtocolEnvelope('repository-associate-retry', associationRequest));
|
|
538
600
|
expectStatus(retryResponse, 200, 'Exact repository association retry');
|
|
539
601
|
invariant(same(decodeAssociateRepositoryCubeResponseEnvelope(retryResponse.body).payload, authoritative), 'Exact repository association retry changed authoritative fields.');
|
|
540
|
-
assertStateDelta(beforeRetry, await environment.admin.observeAuthorityState(), {}, 'Repository association retry');
|
|
541
602
|
const cubeC = await environment.admin.createCube('cube-c');
|
|
542
603
|
await environment.admin.grantCube(principalA, cubeC);
|
|
543
604
|
await environment.admin.prepareRepositoryCube(cubeC, {
|
|
544
605
|
name: 'Other Legacy Cube',
|
|
545
606
|
template: 'starter',
|
|
546
607
|
});
|
|
547
|
-
const beforeRepositoryConflict = await environment.admin.observeAuthorityState();
|
|
548
608
|
expectSecretFreeError(await environment.operations.associateRepositoryCube(credentialA, createProtocolEnvelope('repository-associate-repository-conflict', {
|
|
549
609
|
...associationRequest,
|
|
550
610
|
cube_id: cubeC.id,
|
|
551
611
|
})), 409, ErrorCode.REPOSITORY_ALREADY_ASSOCIATED, 'Repository-to-other-cube conflict', [cubeA.id, cubeC.id, repository.value]);
|
|
552
|
-
|
|
553
|
-
|
|
612
|
+
invariant(same(await resolve('repository-resolve-after-repository-conflict', resolveRequest), authoritative), 'Repository conflict changed the authoritative original binding.');
|
|
613
|
+
invariant(same(await resolve('repository-resolve-alternate-after-repository-conflict', alternateResolveRequest), { result: 'none' }), 'Repository conflict created an alternate binding.');
|
|
554
614
|
expectSecretFreeError(await environment.operations.associateRepositoryCube(credentialA, createProtocolEnvelope('repository-associate-cube-conflict', {
|
|
555
615
|
cube_id: cubeA.id,
|
|
556
|
-
|
|
557
|
-
repository: {
|
|
558
|
-
kind: 'origin',
|
|
559
|
-
value: 'https://github.com/Byte-Ventures/other-repository',
|
|
560
|
-
},
|
|
616
|
+
...alternateResolveRequest,
|
|
561
617
|
})), 409, ErrorCode.CUBE_ALREADY_ASSOCIATED, 'Cube-to-other-repository conflict', [cubeA.id, repository.value, 'https://github.com/Byte-Ventures/other-repository']);
|
|
562
|
-
|
|
618
|
+
invariant(same(await resolve('repository-resolve-after-cube-conflict', resolveRequest), authoritative), 'Cube conflict changed the authoritative original binding.');
|
|
619
|
+
invariant(same(await resolve('repository-resolve-alternate-after-cube-conflict', alternateResolveRequest), { result: 'none' }), 'Cube conflict created the refused alternate binding.');
|
|
563
620
|
const preparedB = await environment.admin.prepareRepositoryCube(cubeB, {
|
|
564
621
|
name: 'Other Client Legacy Cube',
|
|
565
622
|
template: 'starter',
|
|
566
623
|
});
|
|
567
|
-
const beforeCrossClientResolve = await environment.admin.observeAuthorityState();
|
|
568
624
|
const crossClientResolve = await environment.operations.resolveRepositoryCube(credentialB, createProtocolEnvelope('repository-resolve-cross-client', resolveRequest));
|
|
569
625
|
expectStatus(crossClientResolve, 200, 'Cross-client repository resolution');
|
|
570
626
|
invariant(same(decodeResolveRepositoryCubeResponseEnvelope(crossClientResolve.body).payload, { result: 'none' }), 'Another client repository binding was exposed by resolution.');
|
|
571
|
-
assertStateDelta(beforeCrossClientResolve, await environment.admin.observeAuthorityState(), {}, 'Cross-client repository resolution');
|
|
572
|
-
const beforeCrossClientAssociation = await environment.admin.observeAuthorityState();
|
|
573
627
|
const crossClientAssociation = await environment.operations.associateRepositoryCube(credentialB, createProtocolEnvelope('repository-associate-cross-client', {
|
|
574
628
|
...associationRequest,
|
|
575
629
|
cube_id: cubeB.id,
|
|
@@ -578,20 +632,20 @@ export async function runAdapterConformance(environment, options = {}) {
|
|
|
578
632
|
const crossClientAssociated = decodeAssociateRepositoryCubeResponseEnvelope(crossClientAssociation.body).payload;
|
|
579
633
|
invariant(crossClientAssociated.cube_id === preparedB.cube_id &&
|
|
580
634
|
crossClientAssociated.repository.value === repository.value, 'Another client binding changed cross-client association identity.');
|
|
581
|
-
assertStateDelta(beforeCrossClientAssociation, await environment.admin.observeAuthorityState(), { repository_associations: 1 }, 'Cross-client repository association');
|
|
582
635
|
await environment.admin.revokeCubeGrant(principalA, cubeA);
|
|
583
|
-
const beforeHiddenResolve = await environment.admin.observeAuthorityState();
|
|
584
636
|
const hiddenResolve = await environment.operations.resolveRepositoryCube(credentialA, createProtocolEnvelope('repository-resolve-inaccessible-binding', resolveRequest));
|
|
585
637
|
expectStatus(hiddenResolve, 200, 'Inaccessible repository binding resolution');
|
|
586
638
|
invariant(same(decodeResolveRepositoryCubeResponseEnvelope(hiddenResolve.body).payload, { result: 'none' }), 'Inaccessible repository binding was exposed by read-only resolution.');
|
|
587
|
-
assertStateDelta(beforeHiddenResolve, await environment.admin.observeAuthorityState(), {}, 'Inaccessible repository binding resolution');
|
|
588
|
-
const beforeHiddenAssociation = await environment.admin.observeAuthorityState();
|
|
589
639
|
expectSecretFreeError(await environment.operations.associateRepositoryCube(credentialA, createProtocolEnvelope('repository-associate-inaccessible-binding', {
|
|
590
640
|
...associationRequest,
|
|
591
641
|
cube_id: cubeC.id,
|
|
592
642
|
})), 403, ErrorCode.ACCESS_DENIED, 'Inaccessible existing repository binding', [cubeA.id, cubeC.id, repository.value]);
|
|
593
|
-
|
|
643
|
+
const hiddenAfterRefusal = await resolve('repository-resolve-after-inaccessible-refusal', resolveRequest);
|
|
644
|
+
const alternateAfterRefusal = await resolve('repository-resolve-alternate-after-inaccessible-refusal', alternateResolveRequest);
|
|
594
645
|
await environment.admin.grantCube(principalA, cubeA);
|
|
646
|
+
invariant(same(hiddenAfterRefusal, { result: 'none' }), 'Inaccessible-binding refusal changed the hidden authoritative binding.');
|
|
647
|
+
invariant(same(alternateAfterRefusal, { result: 'none' }), 'Inaccessible-binding refusal created an alternate binding.');
|
|
648
|
+
invariant(same(await resolve('repository-resolve-restored-after-inaccessible-refusal', resolveRequest), authoritative), 'Inaccessible-binding refusal did not preserve the original binding.');
|
|
595
649
|
return {
|
|
596
650
|
none: true,
|
|
597
651
|
resolved: true,
|
|
@@ -890,12 +944,10 @@ export async function runAdapterConformance(environment, options = {}) {
|
|
|
890
944
|
});
|
|
891
945
|
const recipient = await environment.admin.createDrone(principalA, cube, role);
|
|
892
946
|
const message = 'START NOW exact slice';
|
|
893
|
-
const before = await environment.admin.observeAuthorityState();
|
|
894
947
|
expectError(await environment.operations.append(credentialA, cube, createProtocolEnvelope('mandatory-addressing-omitted', {
|
|
895
948
|
post_id: '30000000-0000-4000-8000-000000000001',
|
|
896
949
|
message,
|
|
897
950
|
})), 400, ErrorCode.INVALID_INPUT, 'Omitted mandatory addressing');
|
|
898
|
-
assertStateDelta(before, await environment.admin.observeAuthorityState(), {}, 'Mandatory-addressing refusal');
|
|
899
951
|
const broadcast = await environment.operations.append(credentialA, cube, createProtocolEnvelope('mandatory-addressing-broadcast', {
|
|
900
952
|
post_id: '30000000-0000-4000-8000-000000000002',
|
|
901
953
|
message,
|
|
@@ -916,6 +968,10 @@ export async function runAdapterConformance(environment, options = {}) {
|
|
|
916
968
|
invariant(directedResult.entry.visibility === 'direct', 'Explicit recipient selectors did not produce direct visibility.');
|
|
917
969
|
invariant(directedResult.entry.recipient_drone_ids?.includes(recipient.id) === true, 'Explicit recipient selectors did not resolve the named recipient.');
|
|
918
970
|
invariant(directedResult.entry.recipient_drone_ids?.length === 1, 'Classification changed or fell open the explicit recipient set.');
|
|
971
|
+
const readback = await environment.operations.read(credentialA, cube, createProtocolEnvelope('mandatory-addressing-readback', { cursor: null, limit: 10 }));
|
|
972
|
+
expectStatus(readback, 200, 'Mandatory-addressing persistence readback');
|
|
973
|
+
const persistedIds = decodeReadLogResultEnvelope(readback.body).payload.entries.map((entry) => entry.id);
|
|
974
|
+
invariant(same(persistedIds, [broadcastResult.entry.id, directedResult.entry.id]), 'Rejected mandatory-addressing append was persisted.');
|
|
919
975
|
return { refused_status: 400, code: ErrorCode.INVALID_INPUT, persisted: false, broadcast: true, union: true, no_fall_open: true };
|
|
920
976
|
});
|
|
921
977
|
await record('log.entry-query', async () => {
|
|
@@ -936,6 +992,7 @@ export async function runAdapterConformance(environment, options = {}) {
|
|
|
936
992
|
'22222222-0000-4000-8000-000000000002',
|
|
937
993
|
];
|
|
938
994
|
const entries = [];
|
|
995
|
+
const idSeeds = [];
|
|
939
996
|
for (const [index, id] of ids.entries()) {
|
|
940
997
|
const response = await environment.operations.append(credentialA, cube, createProtocolEnvelope(`entry-query-append-${index}`, {
|
|
941
998
|
post_id: `40000000-0000-4000-8000-00000000000${index}`,
|
|
@@ -944,12 +1001,12 @@ export async function runAdapterConformance(environment, options = {}) {
|
|
|
944
1001
|
}));
|
|
945
1002
|
expectStatus(response, 201, `Entry-query fixture append ${index}`);
|
|
946
1003
|
const entry = decodeAppendLogResultEnvelope(response.body).payload.entry;
|
|
947
|
-
|
|
1004
|
+
idSeeds.push({ current_id: entry.id, query_id: id });
|
|
948
1005
|
entries.push({ ...entry, id });
|
|
949
1006
|
}
|
|
1007
|
+
await environment.admin.seedEntryQueryIds(cube, idSeeds);
|
|
950
1008
|
expectStatus(await environment.operations.ack(acknowledgedCredential, cube, createProtocolEnvelope('entry-query-seed-ack', { entry_id: ids[0], kind: 'ack' })), 204, 'Entry-query acknowledgement seed');
|
|
951
1009
|
expectStatus(await environment.operations.ack(claimingCredential, cube, createProtocolEnvelope('entry-query-seed-claim', { entry_id: ids[1], kind: 'claim' })), 204, 'Entry-query claim seed');
|
|
952
|
-
const before = await environment.admin.observeAuthorityState();
|
|
953
1010
|
for (const [requestId, selector, expectedId] of [
|
|
954
1011
|
['entry-query-full', ids[1], ids[1]],
|
|
955
1012
|
['entry-query-prefix', '11111111', ids[0]],
|
|
@@ -961,15 +1018,19 @@ export async function runAdapterConformance(environment, options = {}) {
|
|
|
961
1018
|
}
|
|
962
1019
|
expectError(await environment.operations.entryQuery(credentialA, cube, createProtocolEnvelope('entry-query-unknown', { entry_id: 'ffffffff' })), 404, ErrorCode.NOT_FOUND, 'Unknown entry query');
|
|
963
1020
|
expectError(await environment.operations.entryQuery(credentialA, cube, createProtocolEnvelope('entry-query-ambiguous', { entry_id: '22222222' })), 409, ErrorCode.LOG_ENTRY_PREFIX_AMBIGUOUS, 'Ambiguous entry query');
|
|
964
|
-
assertStateDelta(before, await environment.admin.observeAuthorityState(), {}, 'Entry query');
|
|
965
1021
|
const acknowledgementStatus = decodeAckStatusResultEnvelope((await environment.operations.ackStatus(credentialA, cube, createProtocolEnvelope('entry-query-ack-status', { entry_id: ids[0] }))).body).payload;
|
|
966
1022
|
const claimStatus = decodeAckStatusResultEnvelope((await environment.operations.ackStatus(credentialA, cube, createProtocolEnvelope('entry-query-claim-status', { entry_id: ids[1] }))).body).payload;
|
|
967
|
-
|
|
968
|
-
invariant(
|
|
1023
|
+
const acknowledgedRecipients = acknowledgementStatus.recipients.filter((recipient) => recipient.acknowledged_at !== null);
|
|
1024
|
+
invariant(acknowledgedRecipients.length === 1 &&
|
|
1025
|
+
acknowledgedRecipients[0].drone_id === acknowledgedRecipient.id &&
|
|
1026
|
+
acknowledgementStatus.recipients.find((recipient) => recipient.drone_id === claimingRecipient.id)?.acknowledged_at === null, 'Entry query changed the seeded acknowledgement set.');
|
|
1027
|
+
invariant(claimStatus.claims.length === 1 &&
|
|
1028
|
+
claimStatus.claims[0].drone_id === claimingRecipient.id &&
|
|
1029
|
+
claimStatus.recipients.every((recipient) => recipient.acknowledged_at === null), 'Entry query changed the seeded claim or acknowledgement set.');
|
|
969
1030
|
const unreadResponse = await environment.operations.read(credentialA, cube, createProtocolEnvelope('entry-query-unread', { cursor: null, limit: 10 }));
|
|
970
1031
|
expectStatus(unreadResponse, 200, 'Entry-query unread read');
|
|
971
1032
|
const unread = decodeReadLogResultEnvelope(unreadResponse.body).payload;
|
|
972
|
-
invariant(unread.entries
|
|
1033
|
+
invariant(same(unread.entries, entries), 'Entry query mutated, consumed, or advanced unread activity.');
|
|
973
1034
|
return {
|
|
974
1035
|
full_uuid: true,
|
|
975
1036
|
unique_prefix: true,
|
|
@@ -981,9 +1042,8 @@ export async function runAdapterConformance(environment, options = {}) {
|
|
|
981
1042
|
mutation: 'none',
|
|
982
1043
|
};
|
|
983
1044
|
});
|
|
984
|
-
const entries = [];
|
|
985
|
-
let readCursor = null;
|
|
986
1045
|
await record('log.read-cursor-tuple', async () => {
|
|
1046
|
+
const entries = [];
|
|
987
1047
|
for (const [index, message] of ['alpha', 'beta', 'gamma'].entries()) {
|
|
988
1048
|
const response = await environment.operations.append(credentialA, cubeA, createProtocolEnvelope(`append-a${index + 1}`, {
|
|
989
1049
|
post_id: `00000000-0000-4000-8000-${String(305 + index).padStart(12, '0')}`,
|
|
@@ -1001,18 +1061,32 @@ export async function runAdapterConformance(environment, options = {}) {
|
|
|
1001
1061
|
invariant(page.has_more && page.behind_by === 1, 'Read pagination metadata is incorrect.');
|
|
1002
1062
|
invariant(page.cursor !== null, 'Read did not return a cursor.');
|
|
1003
1063
|
invariant(page.cursor.id === entries[1].id && page.cursor.created_at === entries[1].created_at, 'Read cursor does not equal the final delivered entry tuple.');
|
|
1004
|
-
readCursor = page.cursor;
|
|
1005
1064
|
return { messages: ['alpha', 'beta'], has_more: true, behind_by: 1, cursor_matches_last_entry: true };
|
|
1006
1065
|
});
|
|
1007
|
-
let liveCursor = null;
|
|
1008
1066
|
await record('sse.replay-live-transition', async () => {
|
|
1009
|
-
|
|
1067
|
+
const principal = await environment.admin.createPrincipal('stream-transition');
|
|
1068
|
+
const cube = await environment.admin.createCube('stream-transition');
|
|
1069
|
+
await environment.admin.grantCube(principal, cube);
|
|
1070
|
+
const credential = await environment.admin.issueDroneSession(principal);
|
|
1071
|
+
let readCursor;
|
|
1072
|
+
for (const [index, message] of ['alpha', 'beta', 'gamma'].entries()) {
|
|
1073
|
+
const response = await environment.operations.append(credential, cube, createProtocolEnvelope(`stream-transition-${message}`, {
|
|
1074
|
+
post_id: `00000000-0000-4000-8000-${String(316 + index).padStart(12, '0')}`,
|
|
1075
|
+
to: 'broadcast',
|
|
1076
|
+
message,
|
|
1077
|
+
}));
|
|
1078
|
+
expectStatus(response, 201, `Stream transition append ${message}`);
|
|
1079
|
+
if (message === 'beta') {
|
|
1080
|
+
const entry = decodeAppendLogResultEnvelope(response.body).payload.entry;
|
|
1081
|
+
readCursor = { id: entry.id, created_at: entry.created_at };
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1010
1084
|
const barrier = environment.admin.armReplayTransition();
|
|
1011
|
-
const openPromise = environment.operations.openStream(
|
|
1085
|
+
const openPromise = environment.operations.openStream(credential, cube, readCursor);
|
|
1012
1086
|
await within(barrier.reached, 'Replay transition boundary', streamDeadlineMs);
|
|
1013
1087
|
try {
|
|
1014
|
-
const appendDelta = environment.operations.append(
|
|
1015
|
-
post_id: '00000000-0000-4000-8000-
|
|
1088
|
+
const appendDelta = environment.operations.append(credential, cube, createProtocolEnvelope('stream-transition-delta', {
|
|
1089
|
+
post_id: '00000000-0000-4000-8000-000000000319',
|
|
1016
1090
|
to: 'broadcast',
|
|
1017
1091
|
message: 'delta',
|
|
1018
1092
|
}));
|
|
@@ -1036,8 +1110,8 @@ export async function runAdapterConformance(environment, options = {}) {
|
|
|
1036
1110
|
invariant(bookmark.type === 'bookmark' && bookmark.replay_complete, 'Stream omitted its replay-complete bookmark.');
|
|
1037
1111
|
const noDuplicate = reader.next();
|
|
1038
1112
|
await provePending(noDuplicate, 'Live stream after delta', pendingProbeMs);
|
|
1039
|
-
const epsilonResponse = await environment.operations.append(
|
|
1040
|
-
post_id: '00000000-0000-4000-8000-
|
|
1113
|
+
const epsilonResponse = await environment.operations.append(credential, cube, createProtocolEnvelope('stream-transition-epsilon', {
|
|
1114
|
+
post_id: '00000000-0000-4000-8000-000000000320',
|
|
1041
1115
|
to: 'broadcast',
|
|
1042
1116
|
message: 'epsilon',
|
|
1043
1117
|
}));
|
|
@@ -1046,7 +1120,6 @@ export async function runAdapterConformance(environment, options = {}) {
|
|
|
1046
1120
|
invariant(epsilon.entry.message === 'epsilon', 'Live stream duplicated or reordered an event.');
|
|
1047
1121
|
invariant(compareLogCursor(delta.cursor, epsilon.cursor) < 0, 'Live stream cursors are not ordered.');
|
|
1048
1122
|
invariant(new Set([replay.entry.id, delta.entry.id, epsilon.entry.id]).size === 3, 'Stream delivered a duplicate entry.');
|
|
1049
|
-
liveCursor = epsilon.cursor;
|
|
1050
1123
|
return { replay: ['gamma'], transition: 'bookmark', live: ['delta', 'epsilon'], duplicates: 0 };
|
|
1051
1124
|
}
|
|
1052
1125
|
finally {
|
|
@@ -1054,20 +1127,37 @@ export async function runAdapterConformance(environment, options = {}) {
|
|
|
1054
1127
|
}
|
|
1055
1128
|
});
|
|
1056
1129
|
await record('cursor.explicit-expiry', async () => {
|
|
1057
|
-
|
|
1058
|
-
await environment.admin.
|
|
1059
|
-
const
|
|
1130
|
+
const cube = await environment.admin.createCube('cursor-expiry');
|
|
1131
|
+
await environment.admin.grantCube(principalA, cube);
|
|
1132
|
+
const appended = await environment.operations.append(credentialA, cube, createProtocolEnvelope('cursor-expiry-entry', {
|
|
1133
|
+
post_id: '00000000-0000-4000-8000-000000000321',
|
|
1134
|
+
to: 'broadcast',
|
|
1135
|
+
message: 'expire this cursor',
|
|
1136
|
+
}));
|
|
1137
|
+
expectStatus(appended, 201, 'Cursor expiry fixture append');
|
|
1138
|
+
const entry = decodeAppendLogResultEnvelope(appended.body).payload.entry;
|
|
1139
|
+
const cursor = { id: entry.id, created_at: entry.created_at };
|
|
1140
|
+
await environment.admin.expireCursor(cube, cursor);
|
|
1141
|
+
const response = await environment.operations.read(credentialA, cube, createProtocolEnvelope('read-expired', { cursor, limit: 10 }));
|
|
1060
1142
|
expectError(response, 410, ErrorCode.CURSOR_EXPIRED, 'Expired cursor read');
|
|
1061
|
-
const stream = await environment.operations.openStream(credentialA,
|
|
1143
|
+
const stream = await environment.operations.openStream(credentialA, cube, cursor);
|
|
1062
1144
|
expectError(stream, 410, ErrorCode.CURSOR_EXPIRED, 'Expired cursor stream');
|
|
1063
1145
|
invariant(stream.stream === null, 'Expired cursor stream exposed a body stream.');
|
|
1064
1146
|
return { read_status: 410, stream_status: 410, code: ErrorCode.CURSOR_EXPIRED };
|
|
1065
1147
|
});
|
|
1066
1148
|
await record('acks.idempotent', async () => {
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
const
|
|
1070
|
-
|
|
1149
|
+
const cube = await environment.admin.createCube('ack-idempotency');
|
|
1150
|
+
await environment.admin.grantCube(principalA, cube);
|
|
1151
|
+
const appended = await environment.operations.append(credentialA, cube, createProtocolEnvelope('ack-idempotency-entry', {
|
|
1152
|
+
post_id: '00000000-0000-4000-8000-000000000322',
|
|
1153
|
+
to: 'broadcast',
|
|
1154
|
+
message: 'acknowledge this entry',
|
|
1155
|
+
}));
|
|
1156
|
+
expectStatus(appended, 201, 'Acknowledgement fixture append');
|
|
1157
|
+
const entry = decodeAppendLogResultEnvelope(appended.body).payload.entry;
|
|
1158
|
+
const request = createProtocolEnvelope('ack-entry1', { entry_id: entry.id, kind: 'ack' });
|
|
1159
|
+
const first = await environment.operations.ack(credentialA, cube, request);
|
|
1160
|
+
const second = await environment.operations.ack(credentialA, cube, request);
|
|
1071
1161
|
expectStatus(first, 204, 'First acknowledgement');
|
|
1072
1162
|
expectStatus(second, 204, 'Repeated acknowledgement');
|
|
1073
1163
|
invariant((first.body === '' || first.body === undefined) && (second.body === '' || second.body === undefined), 'Acknowledgement responses must be bodyless.');
|
|
@@ -1092,7 +1182,6 @@ export async function runAdapterConformance(environment, options = {}) {
|
|
|
1092
1182
|
}));
|
|
1093
1183
|
expectStatus(append, 201, 'Acknowledgement-status fixture append');
|
|
1094
1184
|
const entry = decodeAppendLogResultEnvelope(append.body).payload.entry;
|
|
1095
|
-
const beforeMissing = await environment.admin.observeAuthorityState();
|
|
1096
1185
|
const missingResponse = await environment.operations.ackStatus(credentialA, cube, createProtocolEnvelope('ack-status-missing', { entry_id: entry.id }));
|
|
1097
1186
|
expectStatus(missingResponse, 200, 'Missing acknowledgement status');
|
|
1098
1187
|
const missing = decodeAckStatusResultEnvelope(missingResponse.body).payload;
|
|
@@ -1100,10 +1189,8 @@ export async function runAdapterConformance(environment, options = {}) {
|
|
|
1100
1189
|
missing.recipients.length === 2 &&
|
|
1101
1190
|
missing.recipients.every((recipient) => recipient.acknowledged_at === null) &&
|
|
1102
1191
|
missing.claims.length === 0, 'Missing acknowledgement was not represented as nullable per-recipient state.');
|
|
1103
|
-
assertStateDelta(beforeMissing, await environment.admin.observeAuthorityState(), {}, 'Missing acknowledgement status query');
|
|
1104
1192
|
expectStatus(await environment.operations.ack(acknowledgedCredential, cube, createProtocolEnvelope('ack-status-ack', { entry_id: entry.id, kind: 'ack' })), 204, 'Acknowledgement-status acknowledgement');
|
|
1105
1193
|
expectStatus(await environment.operations.ack(claimingCredential, cube, createProtocolEnvelope('ack-status-claim', { entry_id: entry.id, kind: 'claim' })), 204, 'Acknowledgement-status claim');
|
|
1106
|
-
const beforeDistinct = await environment.admin.observeAuthorityState();
|
|
1107
1194
|
const distinctResponse = await environment.operations.ackStatus(credentialA, cube, createProtocolEnvelope('ack-status-distinct', { entry_id: entry.id }));
|
|
1108
1195
|
expectStatus(distinctResponse, 200, 'Distinct acknowledgement and claim status');
|
|
1109
1196
|
const distinct = decodeAckStatusResultEnvelope(distinctResponse.body).payload;
|
|
@@ -1113,16 +1200,13 @@ export async function runAdapterConformance(environment, options = {}) {
|
|
|
1113
1200
|
distinct.claims.length === 1 &&
|
|
1114
1201
|
distinct.claims[0].drone_id === claimingRecipient.id &&
|
|
1115
1202
|
distinct.recipients.find((recipient) => recipient.drone_id === claimingRecipient.id)?.acknowledged_at === null, 'Acknowledgement status conflated acknowledgement and claim records.');
|
|
1116
|
-
assertStateDelta(beforeDistinct, await environment.admin.observeAuthorityState(), {}, 'Distinct acknowledgement and claim status query');
|
|
1117
|
-
const unreadResponse = await environment.operations.read(credentialA, cube, createProtocolEnvelope('ack-status-unread', { cursor: null, limit: 10 }));
|
|
1118
|
-
expectStatus(unreadResponse, 200, 'Post-query unread read');
|
|
1119
|
-
const unread = decodeReadLogResultEnvelope(unreadResponse.body).payload;
|
|
1120
|
-
invariant(unread.entries.length === 1 && unread.entries[0].id === entry.id, 'Acknowledgement-status query advanced unread delivery state.');
|
|
1121
|
-
const beforeUnknown = await environment.admin.observeAuthorityState();
|
|
1122
1203
|
expectError(await environment.operations.ackStatus(credentialA, cube, createProtocolEnvelope('ack-status-unknown', {
|
|
1123
1204
|
entry_id: '00000000-0000-4000-8000-000000000311',
|
|
1124
1205
|
})), 404, ErrorCode.NOT_FOUND, 'Unknown acknowledgement-status entry');
|
|
1125
|
-
|
|
1206
|
+
const unreadResponse = await environment.operations.read(credentialA, cube, createProtocolEnvelope('ack-status-unread', { cursor: null, limit: 10 }));
|
|
1207
|
+
expectStatus(unreadResponse, 200, 'Post-query unread read');
|
|
1208
|
+
const unread = decodeReadLogResultEnvelope(unreadResponse.body).payload;
|
|
1209
|
+
invariant(same(unread.entries, [entry]), 'Acknowledgement-status query mutated or advanced unread delivery state.');
|
|
1126
1210
|
return {
|
|
1127
1211
|
missing_acknowledgement: null,
|
|
1128
1212
|
acknowledgements: 1,
|
|
@@ -1133,15 +1217,26 @@ export async function runAdapterConformance(environment, options = {}) {
|
|
|
1133
1217
|
};
|
|
1134
1218
|
});
|
|
1135
1219
|
await record('claims.durable-noncursor', async () => {
|
|
1136
|
-
|
|
1137
|
-
const
|
|
1220
|
+
const principal = await environment.admin.createPrincipal('claim-noncursor');
|
|
1221
|
+
const cube = await environment.admin.createCube('claim-noncursor');
|
|
1222
|
+
await environment.admin.grantCube(principal, cube);
|
|
1223
|
+
const credential = await environment.admin.issueDroneSession(principal);
|
|
1224
|
+
const appended = await environment.operations.append(credential, cube, createProtocolEnvelope('claim-fixture-entry', {
|
|
1225
|
+
post_id: '00000000-0000-4000-8000-000000000314',
|
|
1226
|
+
to: 'broadcast',
|
|
1227
|
+
message: 'claim cursor baseline',
|
|
1228
|
+
}));
|
|
1229
|
+
expectStatus(appended, 201, 'Claim fixture append');
|
|
1230
|
+
const entry = decodeAppendLogResultEnvelope(appended.body).payload.entry;
|
|
1231
|
+
const baselineCursor = { id: entry.id, created_at: entry.created_at };
|
|
1232
|
+
const claim = await environment.operations.ack(credential, cube, createProtocolEnvelope('claim-fixture-claim', { entry_id: entry.id, kind: 'claim' }));
|
|
1138
1233
|
expectStatus(claim, 204, 'Claim');
|
|
1139
|
-
const read = await environment.operations.read(
|
|
1234
|
+
const read = await environment.operations.read(credential, cube, createProtocolEnvelope('claim-fixture-readback', { cursor: baselineCursor, limit: 10 }));
|
|
1140
1235
|
expectStatus(read, 200, 'Claim-state read');
|
|
1141
1236
|
const page = decodeReadLogResultEnvelope(read.body).payload;
|
|
1142
1237
|
invariant(page.entries.length === 0, 'Claim unexpectedly created a log entry.');
|
|
1143
|
-
invariant(page.cursor !== null && compareLogCursor(page.cursor,
|
|
1144
|
-
invariant(page.claims.some((item) => item.log_entry_id ===
|
|
1238
|
+
invariant(page.cursor !== null && compareLogCursor(page.cursor, baselineCursor) === 0, 'Claim advanced the log cursor.');
|
|
1239
|
+
invariant(page.claims.some((item) => item.log_entry_id === entry.id), 'Claim was not durable in a later read.');
|
|
1145
1240
|
return { durable_claims: 1, entries: 0, cursor_advanced: false };
|
|
1146
1241
|
});
|
|
1147
1242
|
await record('decisions.topic-supersession', async () => {
|
|
@@ -1168,6 +1263,7 @@ export async function runAdapterConformance(environment, options = {}) {
|
|
|
1168
1263
|
let metadataDroneB;
|
|
1169
1264
|
let metadataSessionA = '';
|
|
1170
1265
|
await record('security.drone-management-authorization', async () => {
|
|
1266
|
+
const observationCredential = await environment.admin.issueDroneSession(principalA);
|
|
1171
1267
|
workerRoleA = await environment.admin.createRole(cubeA, {
|
|
1172
1268
|
roleClass: 'worker', isHumanSeat: false,
|
|
1173
1269
|
});
|
|
@@ -1193,8 +1289,15 @@ export async function runAdapterConformance(environment, options = {}) {
|
|
|
1193
1289
|
deniedCredentials.push({ access: access, credential });
|
|
1194
1290
|
}
|
|
1195
1291
|
for (const { access, credential } of deniedCredentials) {
|
|
1292
|
+
const rosterBefore = await environment.operations.listDrones(observationCredential, cubeA);
|
|
1293
|
+
expectStatus(rosterBefore, 200, `${access} principal pre-denial roster`);
|
|
1294
|
+
const workerBefore = listedDrone(rosterBefore, managedWorker.id);
|
|
1295
|
+
invariant(workerBefore, `${access} principal pre-denial roster omitted the managed worker.`);
|
|
1196
1296
|
expectError(await environment.operations.reassignDrone(credential, cubeA, managedWorker, createProtocolEnvelope(`${access}-reassign-denied`, { role_id: workerRoleA.id })), 403, ErrorCode.ACCESS_DENIED, `${access} principal reassignment`);
|
|
1197
1297
|
expectError(await environment.operations.evictDrone(credential, cubeA, managedWorker, createProtocolEnvelope(`${access}-evict-denied`, {})), 403, ErrorCode.ACCESS_DENIED, `${access} principal eviction`);
|
|
1298
|
+
const rosterAfter = await environment.operations.listDrones(observationCredential, cubeA);
|
|
1299
|
+
expectStatus(rosterAfter, 200, `${access} principal post-denial roster`);
|
|
1300
|
+
invariant(same(listedDrone(rosterAfter, managedWorker.id), workerBefore), `${access} principal management denial mutated the managed worker.`);
|
|
1198
1301
|
}
|
|
1199
1302
|
return { read_status: 403, write_status: 403, manage_required: true };
|
|
1200
1303
|
});
|
|
@@ -1213,12 +1316,8 @@ export async function runAdapterConformance(environment, options = {}) {
|
|
|
1213
1316
|
roleClass: 'worker', isHumanSeat: false,
|
|
1214
1317
|
});
|
|
1215
1318
|
const evictionTarget = await environment.admin.createDrone(principalA, cubeA, workerRoleA);
|
|
1216
|
-
await environment.admin.issueManagedDroneSession(evictionTarget);
|
|
1319
|
+
const evictionTargetCredential = await environment.admin.issueManagedDroneSession(evictionTarget);
|
|
1217
1320
|
const unknownCube = { id: '00000000-0000-4000-8000-000000000399' };
|
|
1218
|
-
const snapshot = async () => ({
|
|
1219
|
-
cubeA: await environment.admin.inspectCubeManagementState(cubeA),
|
|
1220
|
-
cubeB: await environment.admin.inspectCubeManagementState(cubeB),
|
|
1221
|
-
});
|
|
1222
1321
|
const operations = [
|
|
1223
1322
|
{
|
|
1224
1323
|
name: 'cube-update',
|
|
@@ -1253,15 +1352,42 @@ export async function runAdapterConformance(environment, options = {}) {
|
|
|
1253
1352
|
invoke: (credential, cube) => environment.operations.evictDrone(credential, cube, evictionTarget, createProtocolEnvelope('matrix-drone-evict', {})),
|
|
1254
1353
|
},
|
|
1255
1354
|
];
|
|
1355
|
+
const observableState = async (operationName) => {
|
|
1356
|
+
if (operationName === 'decision-record') {
|
|
1357
|
+
const response = await environment.operations.listDecisions(credentialA, cubeA, createProtocolEnvelope('matrix-observe-decisions', {}));
|
|
1358
|
+
expectStatus(response, 200, 'Matrix decision observation');
|
|
1359
|
+
return decodeDecisionsResultEnvelope(response.body).payload;
|
|
1360
|
+
}
|
|
1361
|
+
if (operationName === 'drone-reassign' || operationName === 'drone-evict') {
|
|
1362
|
+
const response = await environment.operations.listDrones(credentialA, cubeA);
|
|
1363
|
+
expectStatus(response, 200, 'Matrix drone observation');
|
|
1364
|
+
return listedDrone(response, operationName === 'drone-reassign' ? managedWorker.id : evictionTarget.id);
|
|
1365
|
+
}
|
|
1366
|
+
return null;
|
|
1367
|
+
};
|
|
1368
|
+
const assertObservableDenial = async (operationName, before) => {
|
|
1369
|
+
if (operationName === 'role-create') {
|
|
1370
|
+
expectError(await environment.operations.roleRationale(credentialA, cubeA, createProtocolEnvelope('matrix-observe-role', {
|
|
1371
|
+
role: 'Matrix Role',
|
|
1372
|
+
section: 'Workflow rationale',
|
|
1373
|
+
})), 404, ErrorCode.ROLE_NOT_FOUND, 'Denied matrix role observation');
|
|
1374
|
+
}
|
|
1375
|
+
else if (before !== null) {
|
|
1376
|
+
invariant(same(await observableState(operationName), before), `${operationName} changed protocol-visible state after denial.`);
|
|
1377
|
+
}
|
|
1378
|
+
};
|
|
1256
1379
|
for (const operation of operations) {
|
|
1257
1380
|
for (const [kind, credential] of [
|
|
1258
1381
|
['read', readCredential],
|
|
1259
1382
|
['write', writeCredential],
|
|
1260
1383
|
['drone-session', droneCredential],
|
|
1261
1384
|
]) {
|
|
1262
|
-
const before = await
|
|
1385
|
+
const before = await observableState(operation.name);
|
|
1263
1386
|
expectError(await operation.invoke(credential, cubeA), 403, ErrorCode.ACCESS_DENIED, `${operation.name} by ${kind} principal`);
|
|
1264
|
-
|
|
1387
|
+
await assertObservableDenial(operation.name, before);
|
|
1388
|
+
if (operation.name === 'drone-evict') {
|
|
1389
|
+
expectStatus(await environment.operations.read(evictionTargetCredential, cubeA, createProtocolEnvelope(`matrix-${kind}-eviction-target-probe`, { cursor: null, limit: 1 })), 200, `${kind} denied eviction target probe`);
|
|
1390
|
+
}
|
|
1265
1391
|
}
|
|
1266
1392
|
for (const [kind, credential, cube] of [
|
|
1267
1393
|
['no-grant', noGrantCredential, cubeA],
|
|
@@ -1269,14 +1395,15 @@ export async function runAdapterConformance(environment, options = {}) {
|
|
|
1269
1395
|
['foreign-cube', credentialA, cubeB],
|
|
1270
1396
|
['unknown-cube', credentialA, unknownCube],
|
|
1271
1397
|
]) {
|
|
1272
|
-
const before = await
|
|
1398
|
+
const before = await observableState(operation.name);
|
|
1273
1399
|
expectError(await operation.invoke(credential, cube), 404, ErrorCode.NOT_FOUND, `${operation.name} against ${kind}`);
|
|
1274
|
-
|
|
1400
|
+
await assertObservableDenial(operation.name, before);
|
|
1401
|
+
if (operation.name === 'drone-evict') {
|
|
1402
|
+
expectStatus(await environment.operations.read(evictionTargetCredential, cubeA, createProtocolEnvelope(`matrix-${kind}-eviction-target-probe`, { cursor: null, limit: 1 })), 200, `${kind} denied eviction target probe`);
|
|
1403
|
+
}
|
|
1275
1404
|
}
|
|
1276
|
-
const beforeSuccess = await environment.admin.inspectCubeManagementState(cubeA);
|
|
1277
1405
|
const success = await operation.invoke(credentialA, cubeA);
|
|
1278
1406
|
expectStatus(success, operation.successStatus, `${operation.name} by managing principal`);
|
|
1279
|
-
invariant(!same(await environment.admin.inspectCubeManagementState(cubeA), beforeSuccess), `${operation.name} managing success did not mutate its declared state.`);
|
|
1280
1407
|
}
|
|
1281
1408
|
return {
|
|
1282
1409
|
operation_count: operations.length,
|
|
@@ -1284,7 +1411,8 @@ export async function runAdapterConformance(environment, options = {}) {
|
|
|
1284
1411
|
read_write_status: 403,
|
|
1285
1412
|
drone_session_status: 403,
|
|
1286
1413
|
hidden_status: 404,
|
|
1287
|
-
|
|
1414
|
+
observable_non_mutation_operations: 4,
|
|
1415
|
+
write_only_operations: 2,
|
|
1288
1416
|
role_labels_authoritative: false,
|
|
1289
1417
|
};
|
|
1290
1418
|
});
|
|
@@ -1327,22 +1455,33 @@ export async function runAdapterConformance(environment, options = {}) {
|
|
|
1327
1455
|
roleClass: 'worker', isHumanSeat: false,
|
|
1328
1456
|
});
|
|
1329
1457
|
const foreignDrone = await environment.admin.createDrone(principalB, cubeB, foreignRole);
|
|
1458
|
+
const foreignRosterBefore = await environment.operations.listDrones(credentialB, cubeB);
|
|
1459
|
+
const localRosterBefore = await environment.operations.listDrones(credentialA, cubeA);
|
|
1460
|
+
expectStatus(foreignRosterBefore, 200, 'Pre-denial foreign roster');
|
|
1461
|
+
expectStatus(localRosterBefore, 200, 'Pre-denial local roster');
|
|
1462
|
+
const foreignDroneBefore = listedDrone(foreignRosterBefore, foreignDrone.id);
|
|
1463
|
+
const managedWorkerBefore = listedDrone(localRosterBefore, managedWorker.id);
|
|
1464
|
+
invariant(foreignDroneBefore && managedWorkerBefore, 'Pre-denial roster omitted a drone-management target.');
|
|
1330
1465
|
expectError(await environment.operations.reassignDrone(credentialA, cubeB, foreignDrone, createProtocolEnvelope('cross-cube-reassign', { role_id: foreignRole.id })), 404, ErrorCode.NOT_FOUND, 'Cross-cube reassignment');
|
|
1331
1466
|
expectError(await environment.operations.evictDrone(credentialA, cubeB, foreignDrone, createProtocolEnvelope('cross-cube-evict', {})), 404, ErrorCode.NOT_FOUND, 'Cross-cube eviction');
|
|
1332
1467
|
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');
|
|
1333
1468
|
expectError(await environment.operations.evictDrone(credentialA, cubeA, foreignDrone, createProtocolEnvelope('foreign-drone-local-evict', {})), 404, ErrorCode.NOT_FOUND, 'Foreign drone eviction through authorized cube route');
|
|
1334
1469
|
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');
|
|
1335
1470
|
await environment.admin.grantCube(principalA, cubeB, 'read');
|
|
1336
|
-
const foreignDroneBefore = await environment.admin.inspectManagedDrone(foreignDrone);
|
|
1337
1471
|
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');
|
|
1338
|
-
invariant(same(await environment.admin.inspectManagedDrone(foreignDrone), foreignDroneBefore), 'Bound drone cross-cube reassignment mutated the foreign target.');
|
|
1339
1472
|
expectError(await environment.operations.evictDrone(workerSession, cubeB, foreignDrone, createProtocolEnvelope('bound-drone-cross-cube-evict', {})), 404, ErrorCode.NOT_FOUND, 'Bound drone cross-cube eviction');
|
|
1340
|
-
|
|
1473
|
+
const foreignRosterAfter = await environment.operations.listDrones(credentialB, cubeB);
|
|
1474
|
+
const localRosterAfter = await environment.operations.listDrones(credentialA, cubeA);
|
|
1475
|
+
expectStatus(foreignRosterAfter, 200, 'Post-denial foreign roster');
|
|
1476
|
+
expectStatus(localRosterAfter, 200, 'Post-denial local roster');
|
|
1477
|
+
invariant(same(listedDrone(foreignRosterAfter, foreignDrone.id), foreignDroneBefore), 'Denied cross-cube management mutated the foreign drone.');
|
|
1478
|
+
invariant(same(listedDrone(localRosterAfter, managedWorker.id), managedWorkerBefore), 'Denied foreign-role management mutated the local drone.');
|
|
1341
1479
|
return {
|
|
1342
1480
|
unauthorized_cube_status: 404,
|
|
1343
1481
|
foreign_drone_status: 404,
|
|
1344
1482
|
foreign_role_status: 404,
|
|
1345
1483
|
bound_drone_cross_cube_status: 404,
|
|
1484
|
+
targets_unchanged: true,
|
|
1346
1485
|
code: ErrorCode.NOT_FOUND,
|
|
1347
1486
|
};
|
|
1348
1487
|
});
|
|
@@ -1359,11 +1498,6 @@ export async function runAdapterConformance(environment, options = {}) {
|
|
|
1359
1498
|
drone_id: evictedDrone.id,
|
|
1360
1499
|
evicted: true,
|
|
1361
1500
|
}), 'Eviction response did not identify the terminal seat.');
|
|
1362
|
-
invariant(same(await environment.admin.inspectManagedDrone(evictedDrone), {
|
|
1363
|
-
role_id: workerRoleA.id,
|
|
1364
|
-
evicted: true,
|
|
1365
|
-
session_revoked: true,
|
|
1366
|
-
}), 'Eviction did not atomically mark the drone evicted and revoke its session.');
|
|
1367
1501
|
const after = await environment.operations.listDrones(credentialA, cubeA);
|
|
1368
1502
|
expectStatus(after, 200, 'Post-eviction roster');
|
|
1369
1503
|
invariant(!listedDroneIds(after).includes(evictedDrone.id), 'Evicted drone remained in roster.');
|
|
@@ -1405,44 +1539,78 @@ export async function runAdapterConformance(environment, options = {}) {
|
|
|
1405
1539
|
retry_key: '00000000-0000-4000-8000-000000000352',
|
|
1406
1540
|
client_credential: roleContractReadCredential,
|
|
1407
1541
|
})), 201, 'Role-contract reader enrollment');
|
|
1542
|
+
const refusalRationale = 'Workflow rationale:\nRefusal role remains.';
|
|
1408
1543
|
const defaultRole = await environment.admin.createRole(roleContractCube, {
|
|
1409
1544
|
roleClass: 'worker',
|
|
1410
1545
|
isHumanSeat: false,
|
|
1411
1546
|
name: 'Default Worker',
|
|
1547
|
+
detailedDescription: refusalRationale,
|
|
1412
1548
|
isDefault: true,
|
|
1413
1549
|
});
|
|
1414
1550
|
const mandatoryRole = await environment.admin.createRole(roleContractCube, {
|
|
1415
1551
|
roleClass: 'worker',
|
|
1416
1552
|
isHumanSeat: false,
|
|
1417
1553
|
name: 'Mandatory Worker',
|
|
1554
|
+
detailedDescription: refusalRationale,
|
|
1418
1555
|
isMandatory: true,
|
|
1419
1556
|
});
|
|
1420
1557
|
const humanRole = await environment.admin.createRole(roleContractCube, {
|
|
1421
1558
|
roleClass: 'queen',
|
|
1422
1559
|
isHumanSeat: true,
|
|
1423
1560
|
name: 'Human Seat',
|
|
1561
|
+
detailedDescription: refusalRationale,
|
|
1424
1562
|
});
|
|
1425
1563
|
const activeRole = await environment.admin.createRole(roleContractCube, {
|
|
1426
1564
|
roleClass: 'worker',
|
|
1427
1565
|
isHumanSeat: false,
|
|
1428
1566
|
name: 'Active Worker',
|
|
1567
|
+
detailedDescription: refusalRationale,
|
|
1429
1568
|
});
|
|
1430
|
-
await environment.admin.createDrone(roleContractPrincipal, roleContractCube, activeRole);
|
|
1569
|
+
const activeDrone = await environment.admin.createDrone(roleContractPrincipal, roleContractCube, activeRole);
|
|
1570
|
+
const activeRosterBefore = await environment.operations.listDrones(roleContractCredential, roleContractCube);
|
|
1571
|
+
expectStatus(activeRosterBefore, 200, 'Pre-refusal active-role roster');
|
|
1572
|
+
const activeDroneBefore = listedDrone(activeRosterBefore, activeDrone.id);
|
|
1573
|
+
invariant(activeDroneBefore, 'Pre-refusal roster omitted the active-role drone.');
|
|
1431
1574
|
const refusals = [
|
|
1432
|
-
[defaultRole, ErrorCode.DEFAULT_ROLE_REQUIRED, 'default role'],
|
|
1433
|
-
[mandatoryRole, ErrorCode.ROLE_REQUIRED, 'mandatory role'],
|
|
1434
|
-
[humanRole, ErrorCode.ROLE_REQUIRED, 'human-seat role'],
|
|
1435
|
-
[activeRole, ErrorCode.ROLE_IN_USE, 'active-drone role'],
|
|
1575
|
+
[defaultRole, ErrorCode.DEFAULT_ROLE_REQUIRED, 'default role', 'Default Worker', 'worker', false],
|
|
1576
|
+
[mandatoryRole, ErrorCode.ROLE_REQUIRED, 'mandatory role', 'Mandatory Worker', 'worker', false],
|
|
1577
|
+
[humanRole, ErrorCode.ROLE_REQUIRED, 'human-seat role', 'Human Seat', 'queen', true],
|
|
1578
|
+
[activeRole, ErrorCode.ROLE_IN_USE, 'active-drone role', 'Active Worker', 'worker', false],
|
|
1436
1579
|
];
|
|
1437
|
-
for (const [index, [role, code, label]] of refusals.entries()) {
|
|
1438
|
-
const before = await environment.admin.inspectCubeManagementState(roleContractCube);
|
|
1580
|
+
for (const [index, [role, code, label, roleName, roleClass, isHumanSeat]] of refusals.entries()) {
|
|
1439
1581
|
const response = await environment.operations.deleteRole(roleContractCredential, roleContractCube, role, createProtocolEnvelope(`delete-refusal-${index}`, {}));
|
|
1440
1582
|
expectError(response, 409, code, `Delete ${label}`);
|
|
1441
|
-
invariant(same(await environment.admin.inspectCubeManagementState(roleContractCube), before), `Delete ${label} mutated cube state after refusal.`);
|
|
1442
1583
|
if (code === ErrorCode.ROLE_IN_USE) {
|
|
1443
1584
|
const message = protocolErrorMessage(response) ?? '';
|
|
1444
1585
|
invariant(message === ROLE_IN_USE_DELETE_MESSAGE, 'ROLE_IN_USE did not direct the caller to reassign or evict the drones first.');
|
|
1586
|
+
const roster = await environment.operations.listDrones(roleContractCredential, roleContractCube);
|
|
1587
|
+
expectStatus(roster, 200, 'Active-role refusal roster readback');
|
|
1588
|
+
invariant(same(listedDrone(roster, activeDrone.id), activeDroneBefore), 'Active-role deletion refusal changed the active drone.');
|
|
1445
1589
|
}
|
|
1590
|
+
const retained = await environment.operations.roleRationale(roleContractReadCredential, roleContractCube, createProtocolEnvelope(`delete-refusal-${index}-readback`, {
|
|
1591
|
+
role: role.id,
|
|
1592
|
+
section: 'Workflow rationale',
|
|
1593
|
+
}));
|
|
1594
|
+
expectStatus(retained, 200, `Retained ${label} rationale lookup`);
|
|
1595
|
+
const retainedPayload = decodeRoleRationaleResultEnvelope(retained.body).payload;
|
|
1596
|
+
invariant(retainedPayload.role_id === role.id &&
|
|
1597
|
+
retainedPayload.role_name === roleName &&
|
|
1598
|
+
same(retainedPayload.section, {
|
|
1599
|
+
heading: 'Workflow rationale',
|
|
1600
|
+
body: refusalRationale,
|
|
1601
|
+
}), `Retained ${label} rationale lookup returned a different role or section.`);
|
|
1602
|
+
const shapeSession = `${String(index).repeat(42)}S`;
|
|
1603
|
+
const shapeProbe = await environment.operations.attach(roleContractCredential, createProtocolEnvelope(`delete-refusal-${index}-shape-probe`, {
|
|
1604
|
+
cube_id: roleContractCube.id,
|
|
1605
|
+
role_id: role.id,
|
|
1606
|
+
session_credential: shapeSession,
|
|
1607
|
+
}));
|
|
1608
|
+
expectStatus(shapeProbe, 200, `Retained ${label} shape probe`);
|
|
1609
|
+
const shapePayload = decodeAttachResponseEnvelope(shapeProbe.body).payload;
|
|
1610
|
+
invariant(shapePayload.role.id === role.id &&
|
|
1611
|
+
shapePayload.role.role_class === roleClass &&
|
|
1612
|
+
shapePayload.role.is_human_seat === isHumanSeat, `Retained ${label} shape probe returned changed role semantics.`);
|
|
1613
|
+
expectStatus(await environment.operations.evictDrone(roleContractCredential, roleContractCube, { id: shapePayload.drone.id }, createProtocolEnvelope(`delete-refusal-${index}-shape-cleanup`, {})), 200, `Retained ${label} shape cleanup`);
|
|
1446
1614
|
}
|
|
1447
1615
|
const unknownRole = { id: '00000000-0000-4000-8000-000000000398' };
|
|
1448
1616
|
expectError(await environment.operations.deleteRole(roleContractCredential, roleContractCube, unknownRole, createProtocolEnvelope('delete-unknown-role', {})), 404, ErrorCode.NOT_FOUND, 'Delete unknown role');
|
|
@@ -1450,10 +1618,32 @@ export async function runAdapterConformance(environment, options = {}) {
|
|
|
1450
1618
|
roleClass: 'worker',
|
|
1451
1619
|
isHumanSeat: false,
|
|
1452
1620
|
name: 'Disposable Worker',
|
|
1621
|
+
detailedDescription: refusalRationale,
|
|
1453
1622
|
});
|
|
1454
|
-
const beforeReadDenial = await environment.admin.inspectCubeManagementState(roleContractCube);
|
|
1455
1623
|
expectError(await environment.operations.deleteRole(roleContractReadCredential, roleContractCube, deletableRole, createProtocolEnvelope('delete-role-read-denied', {})), 403, ErrorCode.ACCESS_DENIED, 'Delete role with read authority');
|
|
1456
|
-
|
|
1624
|
+
const retainedDeletable = await environment.operations.roleRationale(roleContractReadCredential, roleContractCube, createProtocolEnvelope('delete-role-read-denied-readback', {
|
|
1625
|
+
role: deletableRole.id,
|
|
1626
|
+
section: 'Workflow rationale',
|
|
1627
|
+
}));
|
|
1628
|
+
expectStatus(retainedDeletable, 200, 'Read-denied retained role rationale lookup');
|
|
1629
|
+
const retainedDeletablePayload = decodeRoleRationaleResultEnvelope(retainedDeletable.body).payload;
|
|
1630
|
+
invariant(retainedDeletablePayload.role_id === deletableRole.id &&
|
|
1631
|
+
retainedDeletablePayload.role_name === 'Disposable Worker' &&
|
|
1632
|
+
same(retainedDeletablePayload.section, {
|
|
1633
|
+
heading: 'Workflow rationale',
|
|
1634
|
+
body: refusalRationale,
|
|
1635
|
+
}), 'Read-authority deletion denial changed the target role.');
|
|
1636
|
+
const shapeProbe = await environment.operations.attach(roleContractCredential, createProtocolEnvelope('delete-role-read-denied-shape-probe', {
|
|
1637
|
+
cube_id: roleContractCube.id,
|
|
1638
|
+
role_id: deletableRole.id,
|
|
1639
|
+
session_credential: 'S'.repeat(43),
|
|
1640
|
+
}));
|
|
1641
|
+
expectStatus(shapeProbe, 200, 'Read-denied retained role shape probe');
|
|
1642
|
+
const shapePayload = decodeAttachResponseEnvelope(shapeProbe.body).payload;
|
|
1643
|
+
invariant(shapePayload.role.id === deletableRole.id &&
|
|
1644
|
+
shapePayload.role.role_class === 'worker' &&
|
|
1645
|
+
shapePayload.role.is_human_seat === false, 'Read-authority deletion denial changed the target role shape.');
|
|
1646
|
+
expectStatus(await environment.operations.evictDrone(roleContractCredential, roleContractCube, { id: shapePayload.drone.id }, createProtocolEnvelope('delete-role-read-denied-shape-cleanup', {})), 200, 'Read-denied retained role shape cleanup');
|
|
1457
1647
|
const deleted = await environment.operations.deleteRole(roleContractCredential, roleContractCube, deletableRole, createProtocolEnvelope('delete-empty-role', {}));
|
|
1458
1648
|
expectStatus(deleted, 200, 'Delete unassigned role');
|
|
1459
1649
|
invariant(same(decodeDeleteRoleResultEnvelope(deleted.body).payload, {
|
|
@@ -1476,7 +1666,6 @@ export async function runAdapterConformance(environment, options = {}) {
|
|
|
1476
1666
|
const attributedEntry = decodeAppendLogResultEnvelope(attributed.body).payload.entry;
|
|
1477
1667
|
expectStatus(await environment.operations.evictDrone(roleContractCredential, roleContractCube, evictedDrone, createProtocolEnvelope('role-delete-evict', {})), 200, 'Pre-delete drone eviction');
|
|
1478
1668
|
expectStatus(await environment.operations.deleteRole(roleContractCredential, roleContractCube, evictedRole, createProtocolEnvelope('delete-evicted-role', {})), 200, 'Delete role held only by an evicted drone');
|
|
1479
|
-
invariant((await environment.admin.inspectManagedDrone(evictedDrone)).role_id === defaultRole.id, 'Role deletion did not retarget the evicted drone to the surviving default role.');
|
|
1480
1669
|
const read = await environment.operations.read(roleContractCredential, roleContractCube, createProtocolEnvelope('read-role-delete-attribution', { cursor: null, limit: 500 }));
|
|
1481
1670
|
expectStatus(read, 200, 'Post-delete attributed log read');
|
|
1482
1671
|
invariant(decodeReadLogResultEnvelope(read.body).payload.entries.some((entry) => entry.id === attributedEntry.id && entry.drone_id === evictedDrone.id), 'Deleting the evicted drone role lost the drone attribution of an existing log entry.');
|
|
@@ -1489,7 +1678,7 @@ export async function runAdapterConformance(environment, options = {}) {
|
|
|
1489
1678
|
ErrorCode.ROLE_REQUIRED,
|
|
1490
1679
|
],
|
|
1491
1680
|
in_use_message_actionable: true,
|
|
1492
|
-
|
|
1681
|
+
refusals_non_mutating: true,
|
|
1493
1682
|
activity_log_attribution_preserved: true,
|
|
1494
1683
|
};
|
|
1495
1684
|
});
|
|
@@ -1580,12 +1769,19 @@ export async function runAdapterConformance(environment, options = {}) {
|
|
|
1580
1769
|
working_repo_name: 'Byte-Ventures/borg-mcp',
|
|
1581
1770
|
working_repo_origin: 'https://github.com/Byte-Ventures/borg-mcp',
|
|
1582
1771
|
};
|
|
1772
|
+
try {
|
|
1773
|
+
metadataDroneA = await environment.admin.createDrone(principalA, cubeA, workerRoleA);
|
|
1774
|
+
metadataSessionA = await environment.admin.issueManagedDroneSession(metadataDroneA);
|
|
1775
|
+
}
|
|
1776
|
+
catch (error) {
|
|
1777
|
+
prerequisiteFailures.set('shared.metadata-baseline', error instanceof Error ? error.message : String(error));
|
|
1778
|
+
}
|
|
1583
1779
|
await record('metadata.attach-report', async () => {
|
|
1584
|
-
|
|
1780
|
+
const attachSession = 'M'.repeat(43);
|
|
1585
1781
|
const created = await environment.operations.attach(credentialA, createProtocolEnvelope('metadata-attach-created', {
|
|
1586
1782
|
cube_id: cubeA.id,
|
|
1587
1783
|
role_id: workerRoleA.id,
|
|
1588
|
-
session_credential:
|
|
1784
|
+
session_credential: attachSession,
|
|
1589
1785
|
runtime_metadata: {
|
|
1590
1786
|
...knownMetadata,
|
|
1591
1787
|
working_repo_origin: 'git@github.com:Byte-Ventures/borg-mcp.git',
|
|
@@ -1596,7 +1792,7 @@ export async function runAdapterConformance(environment, options = {}) {
|
|
|
1596
1792
|
invariant(createdPayload.result === 'created', 'First metadata attach did not create a seat.');
|
|
1597
1793
|
invariant(same(createdPayload.drone.runtime_metadata, knownMetadata), 'Attach did not echo canonical metadata.');
|
|
1598
1794
|
invariant(createdPayload.drone.runtime_metadata_reported, 'Present attach report was not marked reported.');
|
|
1599
|
-
|
|
1795
|
+
const attachedDrone = { id: createdPayload.drone.id };
|
|
1600
1796
|
const unavailableSession = 'N'.repeat(43);
|
|
1601
1797
|
const unavailable = await environment.operations.attach(credentialA, createProtocolEnvelope('metadata-attach-unavailable', {
|
|
1602
1798
|
cube_id: cubeA.id,
|
|
@@ -1629,13 +1825,13 @@ export async function runAdapterConformance(environment, options = {}) {
|
|
|
1629
1825
|
const reused = await environment.operations.attach(credentialA, createProtocolEnvelope('metadata-attach-reused', {
|
|
1630
1826
|
cube_id: cubeA.id,
|
|
1631
1827
|
role_id: workerRoleA.id,
|
|
1632
|
-
prior_drone_id:
|
|
1633
|
-
session_credential:
|
|
1828
|
+
prior_drone_id: attachedDrone.id,
|
|
1829
|
+
session_credential: attachSession,
|
|
1634
1830
|
runtime_metadata: knownMetadata,
|
|
1635
1831
|
}));
|
|
1636
1832
|
expectStatus(reused, 200, 'Metadata attach reuse');
|
|
1637
1833
|
invariant(decodeAttachResponseEnvelope(reused.body).payload.result === 'reused', 'Prior seat was not reused.');
|
|
1638
|
-
const cleared = await environment.operations.selfMetadataUpdate(
|
|
1834
|
+
const cleared = await environment.operations.selfMetadataUpdate(attachSession, cubeA, createProtocolEnvelope('metadata-clear-all', {
|
|
1639
1835
|
agent_kind: null,
|
|
1640
1836
|
reported_model: null,
|
|
1641
1837
|
working_repo_name: null,
|
|
@@ -1645,7 +1841,7 @@ export async function runAdapterConformance(environment, options = {}) {
|
|
|
1645
1841
|
const clearedPayload = decodeUpdateDroneRuntimeMetadataResponseEnvelope(cleared.body).payload;
|
|
1646
1842
|
invariant(clearedPayload.runtime_metadata_reported, 'Clear-all patch lost reported state.');
|
|
1647
1843
|
invariant(same(clearedPayload.runtime_metadata, explicitUnknownDrone.runtime_metadata), 'Clear-all patch did not produce explicit unknown metadata.');
|
|
1648
|
-
expectStatus(await environment.operations.selfMetadataUpdate(
|
|
1844
|
+
expectStatus(await environment.operations.selfMetadataUpdate(attachSession, cubeA, createProtocolEnvelope('metadata-restore-known', knownMetadata)), 200, 'Restore known metadata');
|
|
1649
1845
|
return {
|
|
1650
1846
|
created: true,
|
|
1651
1847
|
reused: true,
|
|
@@ -1664,17 +1860,29 @@ export async function runAdapterConformance(environment, options = {}) {
|
|
|
1664
1860
|
{ request_id: 'metadata-null-repeat', patch: { reported_model: null } },
|
|
1665
1861
|
{ request_id: 'metadata-same-value', patch: { agent_kind: 'opencode' } },
|
|
1666
1862
|
];
|
|
1863
|
+
let state;
|
|
1667
1864
|
for (const update of updates) {
|
|
1668
1865
|
const response = await environment.operations.selfMetadataUpdate(metadataSessionA, cubeA, createProtocolEnvelope(update.request_id, update.patch));
|
|
1669
1866
|
expectStatus(response, 200, update.request_id);
|
|
1670
|
-
decodeUpdateDroneRuntimeMetadataResponseEnvelope(response.body);
|
|
1867
|
+
state = decodeUpdateDroneRuntimeMetadataResponseEnvelope(response.body).payload;
|
|
1671
1868
|
}
|
|
1672
|
-
|
|
1673
|
-
invariant(state.
|
|
1674
|
-
invariant(state.metadata.agent_kind === 'opencode', 'Omitted field was not preserved.');
|
|
1869
|
+
invariant(state?.runtime_metadata.reported_model === null, 'Explicit null did not clear model metadata.');
|
|
1870
|
+
invariant(state.runtime_metadata.agent_kind === 'opencode', 'Omitted field was not preserved.');
|
|
1675
1871
|
return { known_replace: true, omitted_unchanged: true, null_clear: true, repeat_safe: true };
|
|
1676
1872
|
});
|
|
1677
1873
|
await record('security.metadata-invalid-atomic', async () => {
|
|
1874
|
+
const baselineResponse = await environment.operations.selfMetadataUpdate(metadataSessionA, cubeA, createProtocolEnvelope('metadata-invalid-baseline', {
|
|
1875
|
+
agent_kind: 'opencode',
|
|
1876
|
+
reported_model: null,
|
|
1877
|
+
working_repo_name: 'Byte-Ventures/borg-mcp-shared',
|
|
1878
|
+
working_repo_origin: 'https://github.com/Byte-Ventures/borg-mcp-shared',
|
|
1879
|
+
}));
|
|
1880
|
+
expectStatus(baselineResponse, 200, 'Invalid metadata baseline');
|
|
1881
|
+
decodeUpdateDroneRuntimeMetadataResponseEnvelope(baselineResponse.body);
|
|
1882
|
+
const baselineRoster = await environment.operations.listDrones(credentialA, cubeA);
|
|
1883
|
+
expectStatus(baselineRoster, 200, 'Invalid metadata baseline roster');
|
|
1884
|
+
const baselineDrone = listedDrone(baselineRoster, metadataDroneA.id);
|
|
1885
|
+
invariant(baselineDrone, 'Invalid metadata baseline roster omitted the metadata seat.');
|
|
1678
1886
|
const invalidPatches = [
|
|
1679
1887
|
{},
|
|
1680
1888
|
{ agent_kind: 'OpenCode' },
|
|
@@ -1692,23 +1900,29 @@ export async function runAdapterConformance(environment, options = {}) {
|
|
|
1692
1900
|
{ last_seen: '2026-07-14T10:00:00.000Z' },
|
|
1693
1901
|
];
|
|
1694
1902
|
for (const [index, patch] of invalidPatches.entries()) {
|
|
1695
|
-
const before = await environment.admin.inspectDroneRuntimeState(metadataDroneA);
|
|
1696
1903
|
const response = await environment.operations.selfMetadataUpdate(metadataSessionA, cubeA, createProtocolEnvelope(`metadata-invalid-${index}`, patch));
|
|
1697
1904
|
expectError(response, 400, ErrorCode.INVALID_INPUT, `Invalid metadata patch ${index}`);
|
|
1698
|
-
|
|
1905
|
+
const readbackResponse = await environment.operations.listDrones(credentialA, cubeA);
|
|
1906
|
+
expectStatus(readbackResponse, 200, `Invalid metadata readback ${index}`);
|
|
1907
|
+
invariant(same(listedDrone(readbackResponse, metadataDroneA.id), baselineDrone), `Invalid metadata patch ${index} partially mutated state.`);
|
|
1699
1908
|
}
|
|
1700
1909
|
return { cases: invalidPatches.length, atomic: true, prior_safe_state_retained: true };
|
|
1701
1910
|
});
|
|
1702
1911
|
await record('security.metadata-own-seat', async () => {
|
|
1703
1912
|
const peer = await environment.admin.createDrone(principalA, cubeA, workerRoleA);
|
|
1704
|
-
const
|
|
1705
|
-
|
|
1706
|
-
const
|
|
1913
|
+
const rosterBefore = await environment.operations.listDrones(credentialA, cubeA);
|
|
1914
|
+
expectStatus(rosterBefore, 200, 'Pre-update metadata roster');
|
|
1915
|
+
const peerBefore = listedDrone(rosterBefore, peer.id);
|
|
1916
|
+
const managedBefore = listedDrone(rosterBefore, managedWorker.id);
|
|
1917
|
+
const ownBefore = listedDrone(rosterBefore, metadataDroneA.id);
|
|
1918
|
+
invariant(peerBefore && managedBefore && ownBefore, 'Pre-update metadata roster omitted a fixture drone.');
|
|
1707
1919
|
const response = await environment.operations.selfMetadataUpdate(metadataSessionA, cubeA, createProtocolEnvelope('metadata-own-seat', { agent_kind: 'claude' }));
|
|
1708
1920
|
expectStatus(response, 200, 'Own-seat metadata update');
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
invariant((
|
|
1921
|
+
const rosterAfter = await environment.operations.listDrones(credentialA, cubeA);
|
|
1922
|
+
expectStatus(rosterAfter, 200, 'Post-update metadata roster');
|
|
1923
|
+
invariant(same(listedDrone(rosterAfter, peer.id), peerBefore), 'Own-seat update mutated a peer seat.');
|
|
1924
|
+
invariant(same(listedDrone(rosterAfter, managedWorker.id), managedBefore), 'Own-seat update mutated another existing seat.');
|
|
1925
|
+
invariant(listedDrone(rosterAfter, metadataDroneA.id)?.role_id === ownBefore.role_id, 'Metadata update changed the current seat role.');
|
|
1712
1926
|
expectError(await environment.operations.selfMetadataUpdate(credentialA, cubeA, createProtocolEnvelope('metadata-manager-denied', { agent_kind: 'codex' })), 403, ErrorCode.ACCESS_DENIED, 'Manager metadata update');
|
|
1713
1927
|
expectError(await environment.operations.selfMetadataUpdate('Z'.repeat(43), cubeA, createProtocolEnvelope('metadata-invalid-session', { agent_kind: null })), 401, ErrorCode.AUTH_INVALID, 'Unknown metadata session');
|
|
1714
1928
|
const rejectedStates = [];
|
|
@@ -1721,9 +1935,20 @@ export async function runAdapterConformance(environment, options = {}) {
|
|
|
1721
1935
|
expectStatus(await environment.operations.evictDrone(credentialA, cubeA, evicted, createProtocolEnvelope('metadata-evict-seat', {})), 200, 'Metadata seat eviction');
|
|
1722
1936
|
rejectedStates.push(['evicted', evicted, evictedSession, 410, ErrorCode.DRONE_EVICTED]);
|
|
1723
1937
|
for (const [label, drone, session, status, code] of rejectedStates) {
|
|
1724
|
-
const
|
|
1938
|
+
const rosterBefore = label === 'revoked'
|
|
1939
|
+
? await environment.operations.listDrones(credentialA, cubeA)
|
|
1940
|
+
: null;
|
|
1941
|
+
if (rosterBefore)
|
|
1942
|
+
expectStatus(rosterBefore, 200, 'Pre-denial revoked metadata roster');
|
|
1943
|
+
const droneBefore = rosterBefore ? listedDrone(rosterBefore, drone.id) : null;
|
|
1944
|
+
if (rosterBefore)
|
|
1945
|
+
invariant(droneBefore, 'Pre-denial roster omitted the revoked metadata seat.');
|
|
1725
1946
|
expectError(await environment.operations.selfMetadataUpdate(session, cubeA, createProtocolEnvelope(`metadata-${label}-denied`, { agent_kind: null })), status, code, `${label} metadata session`);
|
|
1726
|
-
|
|
1947
|
+
if (droneBefore) {
|
|
1948
|
+
const rosterAfter = await environment.operations.listDrones(credentialA, cubeA);
|
|
1949
|
+
expectStatus(rosterAfter, 200, 'Post-denial revoked metadata roster');
|
|
1950
|
+
invariant(same(listedDrone(rosterAfter, drone.id), droneBefore), 'Revoked metadata denial mutated the protocol-visible seat.');
|
|
1951
|
+
}
|
|
1727
1952
|
}
|
|
1728
1953
|
return {
|
|
1729
1954
|
own_seat_only: true,
|
|
@@ -1750,19 +1975,27 @@ export async function runAdapterConformance(environment, options = {}) {
|
|
|
1750
1975
|
}));
|
|
1751
1976
|
expectStatus(attachedB, 200, 'Cube B metadata attach');
|
|
1752
1977
|
metadataDroneB = { id: decodeAttachResponseEnvelope(attachedB.body).payload.drone.id };
|
|
1753
|
-
const
|
|
1978
|
+
const rosterBefore = await environment.operations.listDrones(credentialB, cubeB);
|
|
1979
|
+
expectStatus(rosterBefore, 200, 'Pre-probe cube B roster');
|
|
1980
|
+
const beforeB = listedDrone(rosterBefore, metadataDroneB.id);
|
|
1981
|
+
invariant(beforeB, 'Pre-probe cube B roster omitted its metadata seat.');
|
|
1754
1982
|
const foreign = await environment.operations.selfMetadataUpdate(metadataSessionA, cubeB, createProtocolEnvelope('metadata-cross-cube', { reported_model: 'foreign/probe' }));
|
|
1755
1983
|
expectError(foreign, 404, ErrorCode.NOT_FOUND, 'Cross-cube metadata update');
|
|
1756
1984
|
const unknown = await environment.operations.selfMetadataUpdate(metadataSessionA, { id: '90000000-0000-4000-8000-000000000001' }, createProtocolEnvelope('metadata-unknown-cube', { reported_model: 'foreign/probe' }));
|
|
1757
1985
|
expectError(unknown, 404, ErrorCode.NOT_FOUND, 'Unknown-cube metadata update');
|
|
1758
1986
|
invariant(same(foreign.body, unknown.body), 'Foreign and unknown metadata probes were distinguishable.');
|
|
1759
|
-
|
|
1987
|
+
const rosterAfter = await environment.operations.listDrones(credentialB, cubeB);
|
|
1988
|
+
expectStatus(rosterAfter, 200, 'Post-probe cube B roster');
|
|
1989
|
+
invariant(same(listedDrone(rosterAfter, metadataDroneB.id), beforeB), 'Cross-cube metadata update mutated the foreign seat.');
|
|
1760
1990
|
const listedA = await environment.operations.listDrones(credentialA, cubeA);
|
|
1761
1991
|
invariant(!listedDroneIds(listedA).includes(metadataDroneB.id), 'Cube A roster disclosed cube B metadata seat.');
|
|
1762
1992
|
return { update_status: 404, foreign_unknown_indistinguishable: true, foreign_unchanged: true, roster_isolated: true };
|
|
1763
1993
|
});
|
|
1764
1994
|
await record('security.metadata-noninterference', async () => {
|
|
1765
|
-
const
|
|
1995
|
+
const rosterBefore = await environment.operations.listDrones(credentialA, cubeA);
|
|
1996
|
+
expectStatus(rosterBefore, 200, 'Pre-update noninterference roster');
|
|
1997
|
+
const before = listedDrone(rosterBefore, metadataDroneA.id);
|
|
1998
|
+
invariant(before, 'Pre-update noninterference roster omitted the metadata seat.');
|
|
1766
1999
|
for (const [requestId, patch] of [
|
|
1767
2000
|
['metadata-noninterference-known', { reported_model: 'openai/gpt-5.6-sol' }],
|
|
1768
2001
|
['metadata-noninterference-repeat', { reported_model: 'openai/gpt-5.6-sol' }],
|
|
@@ -1778,28 +2011,28 @@ export async function runAdapterConformance(environment, options = {}) {
|
|
|
1778
2011
|
session_credential: metadataSessionA,
|
|
1779
2012
|
}));
|
|
1780
2013
|
expectStatus(reattached, 200, 'Metadata non-interference reattach');
|
|
1781
|
-
const
|
|
1782
|
-
|
|
1783
|
-
const
|
|
1784
|
-
invariant(
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
role_unchanged: true,
|
|
1788
|
-
liveness_unchanged: true,
|
|
1789
|
-
logs_unchanged: true,
|
|
1790
|
-
model_turns_unchanged: true,
|
|
1791
|
-
};
|
|
2014
|
+
const rosterAfter = await environment.operations.listDrones(credentialA, cubeA);
|
|
2015
|
+
expectStatus(rosterAfter, 200, 'Post-update noninterference roster');
|
|
2016
|
+
const after = listedDrone(rosterAfter, metadataDroneA.id);
|
|
2017
|
+
invariant(after, 'Post-update noninterference roster omitted the metadata seat.');
|
|
2018
|
+
invariant(same(withoutRuntimeMetadata(before), withoutRuntimeMetadata(after)), 'Metadata update changed non-metadata roster state.');
|
|
2019
|
+
return { non_metadata_roster_state_unchanged: true };
|
|
1792
2020
|
});
|
|
1793
2021
|
await record('security.metadata-secret-non-echo', async () => {
|
|
1794
2022
|
const marker = 'SECRET-METADATA-MARKER';
|
|
1795
|
-
const
|
|
2023
|
+
const rosterBefore = await environment.operations.listDrones(credentialA, cubeA);
|
|
2024
|
+
expectStatus(rosterBefore, 200, 'Pre-secret metadata roster');
|
|
2025
|
+
const before = listedDrone(rosterBefore, metadataDroneA.id);
|
|
2026
|
+
invariant(before, 'Pre-secret metadata roster omitted the metadata seat.');
|
|
1796
2027
|
const response = await environment.operations.selfMetadataUpdate(metadataSessionA, cubeA, createProtocolEnvelope('metadata-secret', {
|
|
1797
2028
|
working_repo_name: 'owner/repo',
|
|
1798
2029
|
working_repo_origin: `https://user:${marker}@github.com/owner/repo?token=${marker}`,
|
|
1799
2030
|
}));
|
|
1800
2031
|
expectError(response, 400, ErrorCode.INVALID_INPUT, 'Secret-bearing metadata update');
|
|
1801
2032
|
invariant(!JSON.stringify(response.body).includes(marker), 'Metadata error echoed hostile secret input.');
|
|
1802
|
-
|
|
2033
|
+
const rosterAfter = await environment.operations.listDrones(credentialA, cubeA);
|
|
2034
|
+
expectStatus(rosterAfter, 200, 'Post-secret metadata roster');
|
|
2035
|
+
invariant(same(listedDrone(rosterAfter, metadataDroneA.id), before), 'Secret-bearing metadata input was persisted.');
|
|
1803
2036
|
const hostileKeys = [
|
|
1804
2037
|
'SECRET-METADATA-KEY-MARKER',
|
|
1805
2038
|
`control\u001b[2J`,
|
|
@@ -1825,8 +2058,19 @@ export async function runAdapterConformance(environment, options = {}) {
|
|
|
1825
2058
|
};
|
|
1826
2059
|
});
|
|
1827
2060
|
await record('security.active-stream-revocation', async () => {
|
|
1828
|
-
|
|
1829
|
-
const
|
|
2061
|
+
const principal = await environment.admin.createPrincipal('stream-revocation');
|
|
2062
|
+
const cube = await environment.admin.createCube('stream-revocation');
|
|
2063
|
+
await environment.admin.grantCube(principal, cube);
|
|
2064
|
+
const credential = await environment.admin.issueDroneSession(principal);
|
|
2065
|
+
const appended = await environment.operations.append(credential, cube, createProtocolEnvelope('stream-revocation-entry', {
|
|
2066
|
+
post_id: '00000000-0000-4000-8000-000000000315',
|
|
2067
|
+
to: 'broadcast',
|
|
2068
|
+
message: 'revocation cursor baseline',
|
|
2069
|
+
}));
|
|
2070
|
+
expectStatus(appended, 201, 'Revocation fixture append');
|
|
2071
|
+
const entry = decodeAppendLogResultEnvelope(appended.body).payload.entry;
|
|
2072
|
+
const cursor = { id: entry.id, created_at: entry.created_at };
|
|
2073
|
+
const opened = await environment.operations.openStream(credential, cube, cursor);
|
|
1830
2074
|
expectStatus(opened, 200, 'Revocation stream open');
|
|
1831
2075
|
invariant(opened.stream, 'Successful revocation stream omitted its AsyncIterable.');
|
|
1832
2076
|
const reader = new SseEventReader(opened.stream);
|
|
@@ -1835,7 +2079,7 @@ export async function runAdapterConformance(environment, options = {}) {
|
|
|
1835
2079
|
invariant(bookmark.type === 'bookmark' && bookmark.replay_complete, 'Fresh live stream did not complete replay.');
|
|
1836
2080
|
const pending = reader.next();
|
|
1837
2081
|
await provePending(pending, 'Idle live stream', pendingProbeMs);
|
|
1838
|
-
await environment.admin.revokePrincipal(
|
|
2082
|
+
await environment.admin.revokePrincipal(principal);
|
|
1839
2083
|
let terminated = false;
|
|
1840
2084
|
try {
|
|
1841
2085
|
await within(pending, 'Revoked stream termination', streamDeadlineMs);
|
|
@@ -1850,7 +2094,7 @@ export async function runAdapterConformance(environment, options = {}) {
|
|
|
1850
2094
|
finally {
|
|
1851
2095
|
await reader.close();
|
|
1852
2096
|
}
|
|
1853
|
-
const rejected = await environment.operations.read(
|
|
2097
|
+
const rejected = await environment.operations.read(credential, cube, createProtocolEnvelope('read-revoked', { cursor, limit: 10 }));
|
|
1854
2098
|
expectError(rejected, 401, ErrorCode.SESSION_REVOKED, 'Post-revocation request');
|
|
1855
2099
|
return { stream_terminated: true, subsequent_status: 401, subsequent_code: ErrorCode.SESSION_REVOKED };
|
|
1856
2100
|
});
|
|
@@ -1873,13 +2117,14 @@ export async function runAdapterConformance(environment, options = {}) {
|
|
|
1873
2117
|
const writer = await enrollParent('delete-writer', 'W', '704');
|
|
1874
2118
|
const outsider = await enrollParent('delete-outsider', 'O', '705');
|
|
1875
2119
|
await environment.admin.grantCreateCubeCapability(creator.principal);
|
|
1876
|
-
const
|
|
2120
|
+
const deleteCubeRequest = {
|
|
1877
2121
|
retry_key: '00000000-0000-4000-8000-000000000706',
|
|
1878
2122
|
name: 'Disposable Cube',
|
|
1879
2123
|
working_repo_name: 'disposable-cube',
|
|
1880
2124
|
repository: { kind: 'local', value: '00000000-0000-4000-8000-000000000707' },
|
|
1881
|
-
template: '
|
|
1882
|
-
}
|
|
2125
|
+
template: 'software-dev',
|
|
2126
|
+
};
|
|
2127
|
+
const createResponse = await environment.operations.createCube(creator.credential, createProtocolEnvelope('delete-cube-create', deleteCubeRequest));
|
|
1883
2128
|
expectStatus(createResponse, 201, 'Disposable cube creation');
|
|
1884
2129
|
const created = decodeCreateCubeResponseEnvelope(createResponse.body).payload;
|
|
1885
2130
|
const cube = { id: created.cube_id };
|
|
@@ -1889,6 +2134,14 @@ export async function runAdapterConformance(environment, options = {}) {
|
|
|
1889
2134
|
await environment.admin.grantCube(writer.principal, cube, 'write');
|
|
1890
2135
|
const drone = await environment.admin.createDrone(creator.principal, cube, role);
|
|
1891
2136
|
const droneCredential = await environment.admin.issueManagedDroneSession(drone);
|
|
2137
|
+
const roleProbeSession = 'T'.repeat(43);
|
|
2138
|
+
const initialRoleProbe = await environment.operations.attach(manager.credential, createProtocolEnvelope('delete-role-shape-baseline', {
|
|
2139
|
+
cube_id: cube.id,
|
|
2140
|
+
role_id: role.id,
|
|
2141
|
+
session_credential: roleProbeSession,
|
|
2142
|
+
}));
|
|
2143
|
+
expectStatus(initialRoleProbe, 200, 'Deletion default-role shape baseline');
|
|
2144
|
+
const roleProbeDrone = decodeAttachResponseEnvelope(initialRoleProbe.body).payload.drone;
|
|
1892
2145
|
const append = await environment.operations.append(creator.credential, cube, createProtocolEnvelope('delete-log', {
|
|
1893
2146
|
post_id: '00000000-0000-4000-8000-000000000312',
|
|
1894
2147
|
to: 'broadcast',
|
|
@@ -1898,16 +2151,45 @@ export async function runAdapterConformance(environment, options = {}) {
|
|
|
1898
2151
|
const entry = decodeAppendLogResultEnvelope(append.body).payload.entry;
|
|
1899
2152
|
expectStatus(await environment.operations.ack(creator.credential, cube, createProtocolEnvelope('delete-claim', { entry_id: entry.id, kind: 'claim' })), 204, 'Deletion fixture claim');
|
|
1900
2153
|
expectStatus(await environment.operations.recordDecision(manager.credential, cube, createProtocolEnvelope('delete-decision', { topic: 'cleanup', decision: 'delete' })), 201, 'Deletion fixture decision');
|
|
1901
|
-
const
|
|
2154
|
+
const readDenialState = async (label) => {
|
|
2155
|
+
const decisions = await environment.operations.listDecisions(manager.credential, cube, createProtocolEnvelope(`delete-${label}-decision-readback`, {}));
|
|
2156
|
+
expectStatus(decisions, 200, `${label} deletion decision readback`);
|
|
2157
|
+
const active = decodeDecisionsResultEnvelope(decisions.body).payload.decisions;
|
|
2158
|
+
const log = await environment.operations.read(manager.credential, cube, createProtocolEnvelope(`delete-${label}-log-readback`, { cursor: null, limit: 500 }));
|
|
2159
|
+
expectStatus(log, 200, `${label} deletion log readback`);
|
|
2160
|
+
const logPayload = decodeReadLogResultEnvelope(log.body).payload;
|
|
2161
|
+
const roster = await environment.operations.listDrones(manager.credential, cube);
|
|
2162
|
+
expectStatus(roster, 200, `${label} deletion roster readback`);
|
|
2163
|
+
const roleReadback = await environment.operations.attach(manager.credential, createProtocolEnvelope(`delete-${label}-role-readback`, {
|
|
2164
|
+
cube_id: cube.id,
|
|
2165
|
+
role_id: role.id,
|
|
2166
|
+
prior_drone_id: roleProbeDrone.id,
|
|
2167
|
+
session_credential: roleProbeSession,
|
|
2168
|
+
}));
|
|
2169
|
+
expectStatus(roleReadback, 200, `${label} deletion role readback`);
|
|
2170
|
+
const rolePayload = decodeAttachResponseEnvelope(roleReadback.body).payload.role;
|
|
2171
|
+
return {
|
|
2172
|
+
decisions: active,
|
|
2173
|
+
entries: logPayload.entries,
|
|
2174
|
+
claims: logPayload.claims,
|
|
2175
|
+
drone: listedDrone(roster, drone.id),
|
|
2176
|
+
role: rolePayload,
|
|
2177
|
+
};
|
|
2178
|
+
};
|
|
2179
|
+
const denialBaseline = await readDenialState('baseline');
|
|
2180
|
+
const assertDenialState = async (label) => {
|
|
2181
|
+
invariant(same(await readDenialState(label), denialBaseline), `${label} cube deletion denial mutated protocol-visible cube state.`);
|
|
2182
|
+
};
|
|
1902
2183
|
for (const [kind, credential] of [
|
|
1903
2184
|
['read', reader.credential],
|
|
1904
2185
|
['write', writer.credential],
|
|
1905
2186
|
['drone-session', droneCredential],
|
|
1906
2187
|
]) {
|
|
1907
2188
|
expectError(await environment.operations.deleteCube(credential, cube, createProtocolEnvelope(`delete-${kind}-denied`, {})), 403, ErrorCode.ACCESS_DENIED, `${kind} cube deletion`);
|
|
1908
|
-
|
|
2189
|
+
await assertDenialState(kind);
|
|
1909
2190
|
}
|
|
1910
2191
|
expectError(await environment.operations.deleteCube(outsider.credential, cube, createProtocolEnvelope('delete-outsider-denied', {})), 404, ErrorCode.NOT_FOUND, 'Never-authorized cube deletion');
|
|
2192
|
+
await assertDenialState('outsider');
|
|
1911
2193
|
const parentStream = await environment.operations.openStream(manager.credential, cube, null);
|
|
1912
2194
|
const droneStream = await environment.operations.openStream(droneCredential, cube, null);
|
|
1913
2195
|
expectStatus(parentStream, 200, 'Deletion parent stream');
|
|
@@ -1949,19 +2231,6 @@ export async function runAdapterConformance(environment, options = {}) {
|
|
|
1949
2231
|
invariant(closed, 'Deleted cube stream yielded after its terminal error.');
|
|
1950
2232
|
await streamReader.close();
|
|
1951
2233
|
}
|
|
1952
|
-
invariant(same(await environment.admin.inspectDeletedCube(cube), {
|
|
1953
|
-
cube_exists: false,
|
|
1954
|
-
role_count: 0,
|
|
1955
|
-
drone_count: 0,
|
|
1956
|
-
log_count: 0,
|
|
1957
|
-
claim_count: 0,
|
|
1958
|
-
decision_count: 0,
|
|
1959
|
-
grant_count: 0,
|
|
1960
|
-
cube_create_binding_count: 0,
|
|
1961
|
-
repository_association_count: 0,
|
|
1962
|
-
active_stream_count: 0,
|
|
1963
|
-
terminal_credential_count: 5,
|
|
1964
|
-
}), 'Cube deletion did not atomically remove cube-owned state and preserve only terminal credentials.');
|
|
1965
2234
|
for (const [kind, credential] of [
|
|
1966
2235
|
['creator', creator.credential],
|
|
1967
2236
|
['manager', manager.credential],
|
|
@@ -1974,30 +2243,22 @@ export async function runAdapterConformance(environment, options = {}) {
|
|
|
1974
2243
|
}
|
|
1975
2244
|
expectError(await environment.operations.deleteCube(outsider.credential, cube, createProtocolEnvelope('delete-outsider-hidden-repeat', {})), 404, ErrorCode.NOT_FOUND, 'Never-authorized post-delete DELETE');
|
|
1976
2245
|
expectError(await environment.operations.read(outsider.credential, cube, createProtocolEnvelope('delete-outsider-hidden', { cursor: null, limit: 1 })), 404, ErrorCode.NOT_FOUND, 'Never-authorized post-delete request');
|
|
1977
|
-
await environment.
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
['reader', reader.credential],
|
|
1982
|
-
['writer', writer.credential],
|
|
1983
|
-
['drone', droneCredential],
|
|
1984
|
-
]) {
|
|
1985
|
-
expectError(await environment.operations.read(credential, cube, createProtocolEnvelope(`delete-${kind}-after-restart`, { cursor: null, limit: 1 })), PROTOCOL_HTTP_CONTRACT.cube_deleted_status, ErrorCode.CUBE_DELETED, `${kind} post-restart deleted-cube request`);
|
|
1986
|
-
expectError(await environment.operations.deleteCube(credential, cube, createProtocolEnvelope(`delete-${kind}-repeat-after-restart`, {})), PROTOCOL_HTTP_CONTRACT.cube_deleted_status, ErrorCode.CUBE_DELETED, `${kind} post-restart DELETE`);
|
|
1987
|
-
}
|
|
1988
|
-
expectError(await environment.operations.deleteCube(outsider.credential, cube, createProtocolEnvelope('delete-outsider-hidden-repeat-after-restart', {})), 404, ErrorCode.NOT_FOUND, 'Never-authorized post-restart DELETE');
|
|
1989
|
-
expectError(await environment.operations.read(outsider.credential, cube, createProtocolEnvelope('delete-outsider-hidden-after-restart', { cursor: null, limit: 1 })), 404, ErrorCode.NOT_FOUND, 'Never-authorized post-restart request');
|
|
2246
|
+
const recreatedResponse = await environment.operations.createCube(creator.credential, createProtocolEnvelope('delete-cube-recreate', deleteCubeRequest));
|
|
2247
|
+
expectStatus(recreatedResponse, 201, 'Post-delete cube recreation');
|
|
2248
|
+
const recreated = decodeCreateCubeResponseEnvelope(recreatedResponse.body).payload;
|
|
2249
|
+
invariant(recreated.result === 'created' && recreated.cube_id !== cube.id, 'Post-delete recreation resolved retained cube-owned creation state.');
|
|
1990
2250
|
return {
|
|
1991
2251
|
deletion_status: 200,
|
|
1992
|
-
cascade_complete: true,
|
|
1993
2252
|
terminal_stream_error: ErrorCode.CUBE_DELETED,
|
|
1994
2253
|
terminal_http_status: 410,
|
|
1995
|
-
terminal_state_durable_after_restart: true,
|
|
1996
2254
|
unknown_hidden_status: 404,
|
|
1997
2255
|
non_member_manager: true,
|
|
2256
|
+
recreation_created_new_cube: true,
|
|
1998
2257
|
};
|
|
1999
2258
|
});
|
|
2000
|
-
const normalizedTranscript = results
|
|
2259
|
+
const normalizedTranscript = results
|
|
2260
|
+
.filter((result) => !result.skipped)
|
|
2261
|
+
.map(({ id, observations }) => ({ id, observations }));
|
|
2001
2262
|
return { ok: results.every((result) => result.ok), results, normalizedTranscript };
|
|
2002
2263
|
}
|
|
2003
2264
|
//# sourceMappingURL=adapter.js.map
|