Skip to content

Commit 1804e29

Browse files
committed
feat(plugin-security)!: a Setup create or rename into a position name a package or a built-in holds answers the metadata door's refusal (C2 stage S10)
Claude-Session: https://claude.ai/code/session_013j5gkUCpqQiti4GgPqqmnt Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6a3f82e commit 1804e29

3 files changed

Lines changed: 190 additions & 21 deletions

File tree

‎packages/plugins/plugin-security/src/position-write-through.test.ts‎

Lines changed: 104 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,13 @@
1919
* - a create, or a rename, into a name the metadata door refuses answers the
2020
* door's own refusal and keeps nothing (seat re-rule Q1 = A); an edit of a
2121
* row that already carries such a name still lands as a row write;
22-
* - a name a package or a built-in holds stands the write-through down: the
23-
* write is passed on and nothing reaches metadata (Q2 = A), pinned at the
24-
* write-through's own level rather than at the data door's answer;
22+
* - an edit that keeps a name a package or a built-in holds, and a delete of
23+
* such a row, stand the write-through down: the write is passed on and
24+
* nothing reaches metadata (Q2 = A), pinned at the write-through's own level
25+
* rather than at the data door's answer;
26+
* - a create, or a rename into, a name a package or a built-in holds answers
27+
* the metadata door's own locked-base refusal and keeps nothing (C2 stage
28+
* S10, ADR-0048 addendum N.2/N.3); the engine's own refusals keep theirs;
2529
* - a package registering a name a Setup position holds in the environment
2630
* ledger is refused `NAMESPACE_CONFLICT` (Q3 = A, within Q4 = A);
2731
* - controls: a walled posture, a system write and a kernel without a metadata
@@ -393,17 +397,21 @@ describe('[ADR-0131 D3] Q1 = A — a name the metadata door refuses is refused w
393397
});
394398
});
395399

396-
describe('[ADR-0131 D3] Q2 = A — a name a package or a built-in holds stands the write-through down', () => {
400+
describe('[ADR-0131 D3] Q2 = A — an edit that keeps, or a delete of, a name a package or a built-in holds stands the write-through down', () => {
397401
// Pinned at the write-through's own level, never at the data door's answer:
398402
// whether the data door admits an edit of a package-declared row is the
399403
// system-row gate's call, decided on the row's provenance stamp, and that
400404
// stamp is another change's. The write-through must stand down whatever the
401405
// stamp says, because it reads the name's holder from the engine registry.
402-
it('the write-through passes a write on a package-held or built-in name to the next step, and writes nothing to metadata', async () => {
406+
// A CREATE under such a name is no stand-down since stage S10 (below).
407+
it('the write-through passes an edit or a delete on a package-held name to the next step, and writes nothing to metadata', async () => {
403408
const b = await boot();
404409
const door = {
405410
saveMetaItem: vi.fn(async (_request: Record<string, unknown>) => undefined),
406411
deleteMetaItem: vi.fn(async (_request: Record<string, unknown>) => undefined),
412+
// The real door's verdict, so the S10 refusal is in force for this double too.
413+
packagedBaseRefusal: vi.fn((request: { type: string; name: string; operation: 'save' | 'delete' }) =>
414+
b.protocol.packagedBaseRefusal(request) as Error | null),
407415
};
408416
const writeThrough = createPositionWriteThrough({
409417
ql: b.engine, getProtocol: () => door, getPosture: () => 'single', logger: b.logger,
@@ -418,9 +426,6 @@ describe('[ADR-0131 D3] Q2 = A — a name a package or a built-in holds stands t
418426
const edit = { object: 'sys_position', operation: 'update', data: { id: 'pos_pkg', label: 'Edited', delegatable: true }, context: b.admin };
419427
expect(await passOn(edit, () => b.engine.update('sys_position', { id: 'pos_pkg', label: 'Edited', delegatable: true }, { context: SYS } as any)))
420428
.toBe(1);
421-
// A create under a built-in name (the plugin registers the six under its own package).
422-
const builtIn = { object: 'sys_position', operation: 'insert', data: { name: 'everyone', label: 'Everyone' }, context: b.admin };
423-
expect(await passOn(builtIn, async () => ({ id: 'pos_builtin', name: 'everyone' }))).toBe(1);
424429
// A delete of the package-declared row.
425430
const del = { object: 'sys_position', operation: 'delete', options: { where: { id: 'pos_pkg' } }, context: b.admin };
426431
expect(await passOn(del, () => b.engine.delete('sys_position', { where: { id: 'pos_pkg' }, context: SYS } as any))).toBe(1);
@@ -446,6 +451,97 @@ describe('[ADR-0131 D3] Q2 = A — a name a package or a built-in holds stands t
446451
});
447452
});
448453

454+
describe('[C2 stage S10, ADR-0048 addendum N.2/N.3] one namespace — a create or a rename into a held name answers the door\'s refusal', () => {
455+
/** The metadata door's own envelope for an environment save over a name a package or a built-in holds. */
456+
const DOOR_REFUSAL = { code: 'NOT_OVERRIDABLE', status: 403 };
457+
458+
it.each([
459+
['a package declares', PKG_POSITION],
460+
['the platform declares as an audience anchor', 'everyone'],
461+
['the platform declares as an audience anchor (guest)', 'guest'],
462+
])('a create under a name %s is refused with the door\'s envelope, and keeps no row and no definition', async (_holder, name) => {
463+
const b = await boot();
464+
const before = (await b.rows(name)).map((r) => r.id);
465+
const refusal = await refusalOf(create(b, { name, label: 'Mine' }));
466+
expect(envelope(refusal)).toEqual(DOOR_REFUSAL);
467+
expect((await b.rows(name)).map((r) => r.id), 'the created row was undone').toEqual(before);
468+
expect(await b.envRows(name)).toEqual([]);
469+
});
470+
471+
it('the refusal is the door\'s own, relayed as the door built it — the same verdict a metadata save gets', async () => {
472+
const b = await boot();
473+
const refusal: any = await refusalOf(create(b, { name: PKG_POSITION, label: 'Mine' }));
474+
const door: any = await refusalOf(
475+
b.protocol.saveMetaItem({ type: 'position', name: PKG_POSITION, item: { name: PKG_POSITION, label: 'Mine' } }),
476+
);
477+
expect({ code: refusal?.code, status: refusal?.status, message: refusal?.message })
478+
.toEqual({ code: door?.code, status: door?.status, message: door?.message });
479+
});
480+
481+
it('a bulk create carrying one held name is refused whole: no row of the write is kept', async () => {
482+
const b = await boot();
483+
const refusal = await refusalOf(b.engine.insert('sys_position', [
484+
{ name: 'bulk_lead', label: 'Bulk lead' },
485+
{ name: PKG_POSITION, label: 'Mine' },
486+
], { context: b.admin } as any));
487+
expect(envelope(refusal)).toEqual(DOOR_REFUSAL);
488+
expect(await b.rows('bulk_lead')).toEqual([]);
489+
expect(await b.envRows('bulk_lead')).toEqual([]);
490+
expect((await b.rows(PKG_POSITION)).map((r) => r.id)).toEqual(['pos_pkg']);
491+
});
492+
493+
it.each([
494+
['a package declares', PKG_POSITION],
495+
['the platform declares', 'guest'],
496+
])('a rename into a name %s is refused; the row and its definition keep the old name', async (_holder, held) => {
497+
const b = await boot();
498+
const created: any = await create(b, { name: 'route_lead', label: 'Route lead' });
499+
const refusal = await refusalOf(patch(b, created.id, { name: held, label: 'Renamed' }));
500+
expect(envelope(refusal)).toEqual(DOOR_REFUSAL);
501+
const [row] = await b.rows('route_lead');
502+
expect({ id: row?.id, label: row?.label }).toEqual({ id: created.id, label: 'Route lead' });
503+
expect((await b.rows(held)).map((r) => r.id)).not.toContain(created.id);
504+
// Before S10 the rename stood down for the new name and then deleted the old name's definition.
505+
expect((await b.envRows('route_lead')).map((r) => r.body)).toEqual([
506+
{ name: 'route_lead', label: 'Route lead', delegatable: false },
507+
]);
508+
expect(await b.envRows(held)).toEqual([]);
509+
});
510+
511+
it('control: the engine\'s own refusals keep their answer — a reserved identity name stays 400 VALIDATION_FAILED', async () => {
512+
const b = await boot();
513+
const refusal = await refusalOf(create(b, { name: 'org_admin', label: 'Mine' }));
514+
expect(envelope(refusal)).toEqual({ code: 'VALIDATION_FAILED', status: 400 });
515+
});
516+
517+
it('control: an edit that keeps a held name stays a row write, with no definition and no refusal', async () => {
518+
const b = await boot();
519+
// A row that predates this stage: an organization's own row under the package's name.
520+
await b.engine.insert('sys_position', {
521+
id: 'pos_twin', name: PKG_POSITION, label: 'Twin', organization_id: ORG,
522+
}, { context: SYS } as any);
523+
await patch(b, 'pos_twin', { label: 'Twin (relabelled)' });
524+
expect((await b.rows(PKG_POSITION)).find((r) => r.id === 'pos_twin')?.label).toBe('Twin (relabelled)');
525+
expect(await b.envRows(PKG_POSITION)).toEqual([]);
526+
});
527+
528+
it('control: a door that brings no locked-base verdict keeps the stand-down', async () => {
529+
const b = await boot();
530+
const door = {
531+
saveMetaItem: vi.fn(async (_request: Record<string, unknown>) => undefined),
532+
deleteMetaItem: vi.fn(async (_request: Record<string, unknown>) => undefined),
533+
};
534+
const writeThrough = createPositionWriteThrough({
535+
ql: b.engine, getProtocol: () => door, getPosture: () => 'single', logger: b.logger,
536+
});
537+
const opCtx: any = { object: 'sys_position', operation: 'insert', data: { name: 'everyone', label: 'Everyone' }, context: b.admin };
538+
const next = vi.fn(async () => { opCtx.result = { id: 'pos_builtin', name: 'everyone' }; });
539+
await writeThrough(opCtx, next);
540+
expect(next).toHaveBeenCalledTimes(1);
541+
expect(door.saveMetaItem).not.toHaveBeenCalled();
542+
});
543+
});
544+
449545
describe('[ADR-0131 D3] Q3 = A — a package cannot register a name a Setup position holds in the environment ledger', () => {
450546
it('the registry item seam refuses it 422 NAMESPACE_CONFLICT, naming both holders', async () => {
451547
const b = await boot();

‎packages/plugins/plugin-security/src/position-write-through.ts‎

Lines changed: 81 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,34 @@
3939
* for every definition the catalog lists at the next boot, so the deleted
4040
* position would come back.
4141
*
42+
* ## One namespace — a name a package or a built-in holds is not taken (C2 stage S10)
43+
*
44+
* Positions hold one name per deployment (ADR-0048 addendum, N.2), and a write
45+
* with no package provenance over a package-held name stays under ADR-0005
46+
* overlay precedence (N.3): `position` takes no environment overlay
47+
* (`allowOrgOverride: false`), so the metadata door refuses that save with its
48+
* locked-base refusal (`403 NOT_OVERRIDABLE`). A Setup **create**, or a
49+
* **rename into** such a name, is now answered with that same refusal, and the
50+
* row write is undone — before this stage the write-through stood down and the
51+
* row landed beside the package's (or the platform's) position under the same
52+
* name, while a rename also deleted the renamed position's own definition.
53+
*
54+
* - **asked of its owner, never re-derived**: the verdict is the metadata
55+
* door's `packagedBaseRefusal` (the predicate and the emitter `saveMetaItem`
56+
* uses — the engine registry's artifact provenance,
57+
* `SchemaRegistry.getArtifactItem`), relayed as the door built it, code,
58+
* status and sentence; this module stamps no code of its own. It is asked
59+
* for the verdict alone, so nothing is written to environment metadata for
60+
* such a name, whatever the answer;
61+
* - **after the engine's own checks**: the row first, as for every other write
62+
* here, so a refusal the engine already makes keeps its own answer (the
63+
* reserved built-in identity names `400 VALIDATION_FAILED`, one name per
64+
* organization `409 UNIQUE_VIOLATION`); only a row the engine accepted is
65+
* asked about, and its write is undone when the door refuses the name;
66+
* - **a door without that verdict** (a protocol that does not bring
67+
* `packagedBaseRefusal`) keeps the stand-down below, as the `/automation`
68+
* doors keep theirs.
69+
*
4270
* ## Where it stands down — the write proceeds exactly as before
4371
*
4472
* - **a system write** (`isSystem`): the seeders and the package door write
@@ -48,12 +76,14 @@
4876
* (the walled half is a later stage's);
4977
* - **a kernel whose metadata protocol cannot save and delete**: the legacy
5078
* direct write, as the permission-set write-through does;
51-
* - **a name a package or a built-in holds**: its definition is the package's
52-
* (or the platform's), and the metadata door refuses an environment save over
53-
* it (`403 NOT_OVERRIDABLE`, `allowOrgOverride: false`). The question is read
54-
* from the engine registry's artifact provenance
79+
* - **an edit that keeps a name a package or a built-in holds, and a delete
80+
* of such a row**: its definition is the package's (or the platform's), and
81+
* the metadata door refuses an environment save over it. The question is
82+
* read from the engine registry's artifact provenance
5583
* (`SchemaRegistry.getArtifactItem`), the same source the door decides
56-
* `NOT_OVERRIDABLE` from (seat re-rule, Q2 = A);
84+
* `NOT_OVERRIDABLE` from (seat re-rule, Q2 = A). Whether the data door admits
85+
* such an edit at all is the system-row gate's call, on the row's provenance
86+
* stamp;
5787
* - **an edit of a row whose name the metadata door does not accept**, with the
5888
* name unchanged: such a row predates this stage, has no definition and can
5989
* have none, so the edit stays a row write (Q1 = A's control). The predicate
@@ -103,6 +133,13 @@ export interface PositionWriteThroughLogger {
103133
export interface PositionMetadataDoor {
104134
saveMetaItem(request: { type: string; name: string; item: Record<string, unknown>; actor?: string }): Promise<unknown>;
105135
deleteMetaItem(request: { type: string; name: string; actor?: string }): Promise<unknown>;
136+
/**
137+
* The door's locked-base verdict without a write: the refusal `saveMetaItem`
138+
* would raise for `(type, name)` on that ground, or `null`
139+
* (`ObjectStackProtocolImplementation.packagedBaseRefusal`). Optional: a door
140+
* without it keeps the stand-down for a package-held name (module note).
141+
*/
142+
packagedBaseRefusal?(request: { type: string; name: string; operation: 'save' | 'delete' }): Error | null;
106143
}
107144

108145
export interface PositionWriteThroughDeps {
@@ -152,6 +189,17 @@ export function packageHoldsPosition(ql: any, name: string): boolean {
152189
return false;
153190
}
154191

192+
/**
193+
* [C2 stage S10, ADR-0048 addendum N.2/N.3] The metadata door's refusal of a
194+
* position taking `name` — its locked-base verdict, asked and never re-derived
195+
* (module note, "One namespace"). `null` when the door would not refuse that
196+
* save on the locked-base ground, or brings no such verdict.
197+
*/
198+
function heldPositionNameRefusal(door: PositionMetadataDoor, name: unknown): Error | null {
199+
if (typeof name !== 'string' || name === '' || typeof door.packagedBaseRefusal !== 'function') return null;
200+
return door.packagedBaseRefusal({ type: POSITION_METADATA_TYPE, name, operation: 'save' });
201+
}
202+
155203
let cachedPositionNameSchema: { safeParse(v: unknown): { success: boolean } } | null = null;
156204

157205
/**
@@ -225,8 +273,10 @@ function logError(logger: PositionWriteThroughLogger | undefined, message: strin
225273

226274
/**
227275
* Engine middleware: under `single`, write every non-system data-door create,
228-
* edit, rename and delete of a position through to its environment definition.
229-
* See the module note for the order, the undo and where it stands down.
276+
* edit, rename and delete of a position through to its environment definition,
277+
* and refuse a create or a rename into a name a package or a built-in holds.
278+
* See the module note for the order, the undo, the refusal and where it stands
279+
* down.
230280
*/
231281
export function createPositionWriteThrough(
232282
deps: PositionWriteThroughDeps,
@@ -281,7 +331,7 @@ export function createPositionWriteThrough(
281331
`[security] a ${POSITION_OBJECT} create was refused by the metadata door, and undoing its row FAILED: the ` +
282332
`position '${String(row.name)}' exists as a row with no environment definition while the caller was told ` +
283333
'the create was refused. Fix: delete the row by hand, then create the position again under a name the ' +
284-
'metadata door accepts (lowercase snake_case).',
334+
'metadata door accepts (lowercase snake_case, and not a name a package or a built-in already holds).',
285335
e,
286336
{ id: row.id, name: row.name },
287337
);
@@ -326,9 +376,19 @@ export function createPositionWriteThrough(
326376
const result = opCtx.result;
327377
const written = (Array.isArray(result) ? result : [result])
328378
.filter((r: unknown): r is Record<string, unknown> => !!r && typeof r === 'object' && scalarId((r as any).id));
379+
const createdRows: Record<string, unknown>[] = [];
380+
for (const created of written) createdRows.push((await readRowById(ql, created.id)) ?? created);
381+
// [S10] A name a package or a built-in holds is not taken: the door's
382+
// refusal is the answer, asked before any definition is saved.
383+
for (const row of createdRows) {
384+
const refusal = heldPositionNameRefusal(door, row.name);
385+
if (refusal) {
386+
await undoInsert(written);
387+
throw refusal;
388+
}
389+
}
329390
const saved: Record<string, unknown>[] = [];
330-
for (const created of written) {
331-
const row = (await readRowById(ql, created.id)) ?? created;
391+
for (const row of createdRows) {
332392
if (!environmentOwns(row.name)) continue;
333393
try {
334394
await saveDefinition(door, row, actor);
@@ -364,6 +424,17 @@ export function createPositionWriteThrough(
364424
if (post) posts.push({ pre, post });
365425
}
366426

427+
// [S10] A rename into a name a package or a built-in holds is not taken:
428+
// the door's refusal is the answer, and the row gets its old values back.
429+
// An edit that keeps such a name stands down below.
430+
for (const { pre, post } of posts) {
431+
const refusal = pre.name !== post.name ? heldPositionNameRefusal(door, post.name) : null;
432+
if (refusal) {
433+
await undoUpdate(targets, patch);
434+
throw refusal;
435+
}
436+
}
437+
367438
// Every new definition first; the old names' definitions go only once all landed.
368439
const saved: Array<{ pre: Record<string, unknown>; post: Record<string, unknown> }> = [];
369440
for (const pair of posts) {

‎packages/plugins/plugin-security/src/security-plugin.ts‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4537,9 +4537,11 @@ export class SecurityPlugin implements Plugin {
45374537
// rename or delete of a position also writes its DEFINITION through the
45384538
// metadata door at environment scope — the row first, then the
45394539
// definition; a refused definition undoes the row. Registered AFTER the
4540-
// security middleware, so it runs INSIDE it. Stands down for a walled
4541-
// posture, a name a package or built-in holds, and a kernel without a
4542-
// capable metadata protocol. See `position-write-through.ts`.
4540+
// security middleware, so it runs INSIDE it. A create or a rename into a
4541+
// name a package or built-in holds answers the metadata door's own
4542+
// refusal (C2 stage S10). Stands down for a walled posture, an edit that
4543+
// keeps or a delete of such a name, and a kernel without a capable
4544+
// metadata protocol. See `position-write-through.ts`.
45434545
ql.registerMiddleware(
45444546
createPositionWriteThrough({
45454547
ql,

0 commit comments

Comments
 (0)