Skip to content

Commit cb9252d

Browse files
committed
fix(cli): under a wall, the rewrite gate's remedy for a converted binding names the act the wall allows
Under group/isolated no ceremony step carries binding rows, so the gate's pointer at authored-conversion named a step that reports done. The walled remedy now names the act by where the position's definition lives: declare an undeclared position with the bound sets through one door (the deployment's code, or the metadata door as the platform administrator); add the missing sets to an editable declared one through the metadata door; for a definition the door reports not editable (sealed, ADR-0131 D6), state the fact and name no act. The single-posture pointer and the uncomposed-plugin remedy are unchanged. Claude-Session: https://claude.ai/code/session_01Q9PZ5Pzqcus4woVvTZwWNU Co-authored-by: Claude <noreply@anthropic.com>
1 parent 086fa46 commit cb9252d

1 file changed

Lines changed: 62 additions & 1 deletion

File tree

‎packages/cli/src/utils/organization-ownership-apply.ts‎

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1970,6 +1970,64 @@ async function unnamedForComposition(run: Run, ids: readonly string[]): Promise<
19701970
return out;
19711971
}
19721972

1973+
/**
1974+
* [#23542] The remedy for a `converted` binding — rows that bind position
1975+
* `name` to sets its catalog definition does not name; `sets` is the
1976+
* conversion's answer, the definition's sets followed by the ones the rows add
1977+
* — when the gate refuses it under a wall. There no step carries binding rows:
1978+
* `authored-conversion` reports and returns before it reads one (ADR-0131 D3,
1979+
* the module doc), so pointing at that step points at nothing.
1980+
*
1981+
* The one act that clears the gate is a definition naming every bound set,
1982+
* which the operator makes, through the door the position's definition lives
1983+
* behind:
1984+
*
1985+
* - undeclared: declare it with `sets`, in the deployment's code or through
1986+
* the metadata door as the platform administrator — one of the two only, as
1987+
* a cold boot refuses a name a package declares and the environment catalog
1988+
* also holds;
1989+
* - declared, and the metadata door reports it editable: add the missing sets
1990+
* to its `permissionSets` through that door;
1991+
* - declared, and not editable (ADR-0131 D6: a definition a package ships is
1992+
* sealed; or an ADR-0010 lock), or its state unreadable: the fact, and no
1993+
* act, because no door adds the sets.
1994+
*
1995+
* Whether it is editable is the metadata door's own answer
1996+
* ({@link MetadataLockPort}), never inferred from the body. The gate still
1997+
* refuses; nothing here writes.
1998+
*/
1999+
async function walledBindingRemedy(run: Run, name: string, sets: readonly string[]): Promise<string> {
2000+
const why = `Under the ${run.opts.posture} posture no ceremony step carries binding rows (ADR-0131 D3).`;
2001+
const door = `the metadata door as the platform administrator (PUT /api/v1/meta/position/${name})`;
2002+
const entry = await resolveName(run, 'position', name);
2003+
if (!entry) {
2004+
return (
2005+
`${why} Declare the position ${name} in the environment catalog with permissionSets naming ${sets.join(', ')}, ` +
2006+
`through one door only: the deployment's code, or ${door}. A cold boot refuses a position name a package declares ` +
2007+
'and the environment catalog also holds. Then re-run.'
2008+
);
2009+
}
2010+
let verdict: Awaited<ReturnType<MetadataLockPort['verdict']>>;
2011+
try {
2012+
verdict = await run.opts.lock.verdict('position', name, null);
2013+
} catch (error) {
2014+
return (
2015+
`${why} Whether the metadata door can edit the definition of ${name} could not be read ` +
2016+
`(${error instanceof Error ? error.message : String(error)}), so no act is named here.`
2017+
);
2018+
}
2019+
if (!verdict.editable) {
2020+
return (
2021+
`${why} The metadata door reports the definition of ${name} not editable (lock ${verdict.lock ?? 'unknown'}` +
2022+
`${verdict.lockReason ? `: ${verdict.lockReason}` : ''}; a definition a package ships is sealed, ADR-0131 D6), ` +
2023+
'so no door on this database adds those sets.'
2024+
);
2025+
}
2026+
const declared = Array.isArray(entry.definition.permissionSets) ? entry.definition.permissionSets.map(String) : [];
2027+
const missing = sets.filter((set) => !declared.includes(set));
2028+
return `${why} Add ${missing.join(', ')} to the permissionSets of the position ${name} through ${door}, then re-run.`;
2029+
}
2030+
19732031
/** Read-only: does every reference resolve by name? The gate of fate 2. */
19742032
async function verifyReferences(run: Run): Promise<Verification> {
19752033
const out: Verification = { blockers: [], notes: [], grants: 0, verified: 0 };
@@ -2052,7 +2110,10 @@ async function verifyReferences(run: Run): Promise<Verification> {
20522110
position.fate === 'conflicting'
20532111
? `Position ${position.name}: organizations bind different permission sets to it; the bindings cannot be carried into one definition.`
20542112
: `Binding rows give position ${position.name} sets its catalog definition does not name (${position.permissionSets.join(', ')}); ` +
2055-
`deleting them would lose a grant. ${(await uncomposedSecurityRemedy(run, 'position', position.name)) ?? 'The authored-conversion step carries them; resolve its blocker, then re-run.'}`,
2113+
`deleting them would lose a grant. ${(await uncomposedSecurityRemedy(run, 'position', position.name)) ??
2114+
(run.opts.posture === 'single'
2115+
? 'The authored-conversion step carries them; resolve its blocker, then re-run.'
2116+
: await walledBindingRemedy(run, position.name, position.permissionSets))}`,
20562117
);
20572118
}
20582119
}

0 commit comments

Comments
 (0)