@@ -65,6 +65,9 @@ import {
6565 confirmAudienceBindingSuggestion,
6666 dismissAudienceBindingSuggestion,
6767 readAcceptedEveryoneSets,
68+ readAcceptedDefaultPermissionSetsOption,
69+ refuseAcceptedDefaultsWithoutWall,
70+ type DeploymentAcceptance,
6871 type SuggestionDeps,
6972 type SuggestionListFilter,
7073} from './suggested-audience-bindings.js';
@@ -907,6 +910,34 @@ export interface SecurityPluginOptions {
907910 * @default 'member_default'
908911 */
909912 fallbackPermissionSet?: string | null;
913+ /**
914+ * [ADR-0131 D5] The package defaults this deployment accepts for the
915+ * `everyone` anchor, declared as host configuration — read ONLY under a
916+ * walled posture (`group` / `isolated`, `postureEnforcesWall`).
917+ *
918+ * Under a wall, platform standing is configuration, and no reserved
919+ * organization owns deployment-level rows (ADR-0131 §1.5). So a walled host
920+ * names here the `isDefault: true` permission sets every signed-in member
921+ * holds beside the baseline. Declared, this list IS the deployment-level
922+ * acceptance: the `everyone` definition derives from the baseline plus these
923+ * sets, the `sys_audience_binding_suggestion` record is neither read nor
924+ * written, and its list / accept / dismiss doors answer 409
925+ * `SUGGESTION_STATE` naming this option. `[]` declares that nothing beyond
926+ * the baseline is accepted.
927+ *
928+ * Each name passes the same checks an accepted suggestion does, on every
929+ * derivation: the security catalog serves it under some package, it declares
930+ * `isDefault: true`, and it carries no bit the `everyone` anchor forbids
931+ * (ADR-0090 D7). A name failing one is not granted and is reported — at
932+ * `error` for a forbidden bit.
933+ *
934+ * Omitted (the default), the acceptance is the record, owned by the Default
935+ * Organization, as before. Declared under the `single` posture it is refused
936+ * at `start()`: there the record is the acceptance (ADR-0131 D5's
937+ * Default-Organization row), and a declaration nothing reads would only
938+ * look enforced.
939+ */
940+ acceptedDefaultPermissionSets?: readonly string[];
910941}
911942
912943/**
@@ -1166,6 +1197,8 @@ export class SecurityPlugin implements Plugin {
11661197 private fieldMasker = new FieldMasker();
11671198 private readonly bootstrapPermissionSets: PermissionSet[];
11681199 private readonly fallbackPermissionSet: string | null;
1200+ /** [ADR-0131 D5] {@link SecurityPluginOptions.acceptedDefaultPermissionSets}, frozen. */
1201+ private readonly acceptedDefaultPermissionSets: readonly string[] | undefined;
11691202 /**
11701203 * [#7555, ADR-0090 D5] The HUMAN baseline, as the list of set names it is:
11711204 * {@link fallbackPermissionSet} COMPOSED WITH the platform baseline
@@ -1479,6 +1512,15 @@ export class SecurityPlugin implements Plugin {
14791512 // [#7555] …and the baseline that actually resolves is that name COMPOSED
14801513 // with the platform's own, never one displacing the other (ADR-0090 D5).
14811514 this.baselinePermissionSets = composeHumanBaselinePermissionSets(this.fallbackPermissionSet);
1515+ this.acceptedDefaultPermissionSets = readAcceptedDefaultPermissionSetsOption(options.acceptedDefaultPermissionSets);
1516+ }
1517+
1518+ /**
1519+ * [ADR-0131 D5] Where the deployment-level acceptance lives: the posture this
1520+ * plugin resolved, and the host's declaration (read only under a wall).
1521+ */
1522+ private deploymentAcceptance(): DeploymentAcceptance {
1523+ return { posture: this.tenancyPosture, acceptedDefaultPermissionSets: this.acceptedDefaultPermissionSets };
14821524 }
14831525
14841526 async init(ctx: PluginContext): Promise<void> {
@@ -1895,6 +1937,10 @@ export class SecurityPlugin implements Plugin {
18951937 } catch {
18961938 /* engine absent — nothing to widen */
18971939 }
1940+ // [ADR-0131 D5] The host's accepted package defaults are read only under a
1941+ // wall; declared under `single` they are refused here, the first moment the
1942+ // posture is known, rather than left looking enforced.
1943+ refuseAcceptedDefaultsWithoutWall(this.tenancyPosture, this.acceptedDefaultPermissionSets);
18981944 // [ADR-0105 D11] Optional app/plugin membership resolver for the §7.3.1
18991945 // `IN (current_user.<key>)` sets. Absent is the norm — probe once.
19001946 try {
@@ -2012,6 +2058,7 @@ export class SecurityPlugin implements Plugin {
20122058 metadata,
20132059 posture: () => this.tenancyPosture,
20142060 rederiveEveryone: () => this.declareEveryone(ctx.logger),
2061+ acceptedDefaultPermissionSets: this.acceptedDefaultPermissionSets,
20152062 logger: ctx.logger,
20162063 };
20172064 // Typed against the published contract so the registered surface cannot
@@ -4375,7 +4422,7 @@ export class SecurityPlugin implements Plugin {
43754422 if (evt?.type !== 'permission' || evt.state === 'draft') return;
43764423 void (async () => {
43774424 try {
4378- await reconcileAudienceBindingSuggestions(ql, ctx.logger);
4425+ await reconcileAudienceBindingSuggestions(ql, ctx.logger, this.deploymentAcceptance() );
43794426 } catch (e) {
43804427 ctx.logger.warn('[security] audience-binding suggestion sync failed (non-fatal)', { error: (e as Error).message });
43814428 }
@@ -4471,7 +4518,7 @@ export class SecurityPlugin implements Plugin {
44714518 // `manage_metadata` holder's decision — never auto-bound. ONE
44724519 // deployment-level pass, owned by the Default Organization (#22801 → A).
44734520 try {
4474- await reconcileAudienceBindingSuggestions(ql, ctx.logger);
4521+ await reconcileAudienceBindingSuggestions(ql, ctx.logger, this.deploymentAcceptance() );
44754522 } catch (e) {
44764523 ctx.logger.warn('[security] audience-binding suggestion sync failed (non-fatal)', { error: (e as Error).message });
44774524 }
@@ -4670,13 +4717,16 @@ export class SecurityPlugin implements Plugin {
46704717 logger.warn('[security] the everyone anchor does not name a baseline set — high-privilege bits', { set, offending });
46714718 });
46724719 try {
4673- const derived = await readAcceptedEveryoneSets(ql, this.metadata);
4720+ const derived = await readAcceptedEveryoneSets(ql, this.metadata, this.deploymentAcceptance() );
46744721 for (const name of derived.accepted) if (!sets.includes(name)) sets.push(name);
46754722 for (const w of derived.withheld) {
4676- const line =
4677- `[security] the everyone anchor does not carry the accepted set '${w.set}' (package '${w.packageId}'): ` +
4678- `${w.reason}. Its acceptance stays recorded and grants nothing until the set is served again as ` +
4679- 'accepted; a manage_metadata holder can revoke it in Setup.';
4723+ const line = w.source === 'host-configuration'
4724+ ? `[security] the everyone anchor does not carry the set '${w.set}' the security plugin's ` +
4725+ `\`acceptedDefaultPermissionSets\` option declares: ${w.reason}. It grants nothing while that holds; ` +
4726+ 'remove the name from the option, or serve the set again as an isDefault set some package declares.'
4727+ : `[security] the everyone anchor does not carry the accepted set '${w.set}' (package '${w.packageId}'): ` +
4728+ `${w.reason}. Its acceptance stays recorded and grants nothing until the set is served again as ` +
4729+ 'accepted; a manage_metadata holder can revoke it in Setup.';
46804730 if (w.forbidden) logger.error(line, undefined, { set: w.set, package: w.packageId });
46814731 else logger.warn(line, { set: w.set, package: w.packageId });
46824732 }
0 commit comments