Files
Michał Pierzchała 2ec4e91b11 refactor(core): move the command descriptor registry into its own workspace package (#2348)
* refactor(core): move the command descriptor registry into its own package

`src/core/command-descriptor/`, `src/command-catalog.ts`, `src/core/wait-positionals.ts`
and `src/core/parse-timeout.ts` move as git renames into a new private package
`@agent-device/command-registry` (deps: contracts, selectors). One subpath per module
points straight at the moved file; no `index.ts`, no re-export at the old path. Every
consumer switches to the owning specifier.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jqfa11D8QsCMuL17SsLvDz

* test(host-kit): pin the command-registry package inside the daemon code graph

The daemon reaches the registry and its catalog only by workspace specifier. A walk
that stopped at the package boundary would report an unchanged signature after a
descriptor edit, and the client would keep reusing a daemon running the superseded
policy. The manifest is asserted beside the sources because its `exports` map is what
chose them. The cache doc comment quoting the old ~800-module graph is corrected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jqfa11D8QsCMuL17SsLvDz

* chore(gates): point the descriptor-registry gates at the package path

R66's `COMMAND_DESCRIPTOR_MODULE`, R16's record-runtime join subject and the Fallow
`AssertTrue` totality-guard key follow the registry to its package. The two descriptor
hubs leave `HUB_ENTRY_FILES` because the package manifest now publishes them, so the
eager-closure gate discovers them as facades and one entry gets one rule; this also
flips `denyPlatformImplementations` from false (hub) to true (package entry) for both,
which is intentional and stricter. `command-registry` joins the ranked spine at rank 1.

No `APPROVED_OVER_CEILING` row: rename detection carries every moved entry's merge-base
baseline, so all twelve fall under the no-growth rule rather than a ceiling.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jqfa11D8QsCMuL17SsLvDz

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-09-06 12:55:14 +02:00

229 lines
8.5 KiB
TypeScript

// Catches: daemon code constructing or narrowing an AdmittedRuntimePlan/BoundDeviceRuntime
// outside runtime-admission.ts — manufacturing a runtime proof instead of receiving one from
// the single admission authority, which "Guarantees erode at path boundaries" (AGENTS.md)
// names directly; a type check alone cannot see this because the forged value still type-
// checks as the real proof type.
// Evidence: 7b48531d3b (#2081) retired ADR-0019 cutover scaffolding this policy outlived;
// 4454aef139 (#2092) removed the retired migration scaffolding alongside it.
// Cost: 362 LOC (216 rule + 146 test).
// Kill criterion: none enforced today; retire only by maintainer decision that runtime-admission.ts
// being the sole producer of AdmittedRuntimePlan/BoundDeviceRuntime no longer matters. The
// token is a type, so an `as` cast or narrowing to it type-checks from any module.
import { parseSync } from 'oxc-parser';
import { memberName, propertyName, visitAst } from './layering-ast.ts';
import type { LayeringViolation } from './model.ts';
type AstNode = Record<string, unknown>;
export const RUNTIME_EXECUTION_INTEGRITY_RULE = 'R66 runtime-execution-integrity';
const COMMAND_DESCRIPTOR_MODULE = 'packages/command-registry/src/registry.ts';
const RUNTIME_ADMISSION_MODULE = 'src/daemon/runtime-admission.ts';
const RUNTIME_PROOF_TYPES = new Set(['AdmittedRuntimePlan', 'BoundDeviceRuntime']);
/** Facts are the only admission authority, and daemon code must consume narrowed runtime proofs. */
export function runtimeExecutionIntegrityViolations(
sources: ReadonlyMap<string, string>,
): LayeringViolation[] {
const violations: LayeringViolation[] = [];
for (const [file, source] of sources) {
const program = parseSync(file, source).program as AstNode;
visitAst(program, (node) => {
if (file === COMMAND_DESCRIPTOR_MODULE && isCapabilityDeclaration(node)) {
violations.push(
at(file, source, node, 'command descriptors may not restore capability-bucket admission'),
);
}
if (isLegacyAdmissionCall(node)) {
violations.push(
at(file, source, node, 'runtime facts are the only device-command admission authority'),
);
}
if (!file.startsWith('src/daemon/')) return;
if (isManufacturedRuntimeProof(node)) {
violations.push(
at(file, source, node, 'daemon code may not manufacture a narrowed runtime proof'),
);
}
if (isNonNullOperationRepair(node)) {
violations.push(
at(file, source, node, 'daemon code may not repair a missing runtime operation with !'),
);
}
if (isLiteralBracketedOperationAccess(node)) {
violations.push(
at(
file,
source,
node,
'daemon code must consume narrowed runtime operations through named properties',
),
);
}
});
}
violations.push(...sharedAdmissionViolations(sources));
return violations;
}
function isCapabilityDeclaration(node: AstNode): boolean {
return (
node['type'] === 'Property' &&
node['computed'] !== true &&
propertyName(node['key']) === 'capability'
);
}
function isLegacyAdmissionCall(node: AstNode): boolean {
if (node['type'] !== 'CallExpression') return false;
const callee = node['callee'] as AstNode | undefined;
return callee?.['type'] === 'Identifier' && callee['name'] === 'requireCommandSupported';
}
function isManufacturedRuntimeProof(node: AstNode): boolean {
if (node['type'] !== 'TSAsExpression' && node['type'] !== 'TSTypeAssertion') return false;
let found = false;
visitAst(node['typeAnnotation'], (candidate) => {
if (candidate['type'] !== 'Identifier') return;
const name = String(candidate['name']);
if (RUNTIME_PROOF_TYPES.has(name) || name.endsWith('RuntimeOperations')) found = true;
});
return found;
}
function isNonNullOperationRepair(node: AstNode): boolean {
if (node['type'] !== 'TSNonNullExpression') return false;
const expression = node['expression'] as AstNode | undefined;
if (expression?.['type'] !== 'MemberExpression') return false;
const object = expression['object'] as AstNode | undefined;
return isOperationsMember(object);
}
function isLiteralBracketedOperationAccess(node: AstNode): boolean {
if (node['type'] !== 'MemberExpression' || node['computed'] !== true) return false;
const object = node['object'] as AstNode | undefined;
return isOperationsMember(object) && staticString(node['property']) !== undefined;
}
function isOperationsMember(node: AstNode | undefined): boolean {
return node?.['type'] === 'MemberExpression' && staticMemberName(node) === 'operations';
}
function staticMemberName(node: AstNode): string | undefined {
return node['computed'] === true ? staticString(node['property']) : memberName(node);
}
function staticString(node: unknown): string | undefined {
if (node === null || typeof node !== 'object') return undefined;
const value = node as AstNode;
if (value['type'] === 'Literal' && typeof value['value'] === 'string') {
return value['value'];
}
if (value['type'] === 'BinaryExpression' && value['operator'] === '+') {
const left = staticString(value['left']);
const right = staticString(value['right']);
return left === undefined || right === undefined ? undefined : left + right;
}
if (value['type'] === 'TemplateLiteral') {
const expressions = value['expressions'];
const quasis = value['quasis'];
if (!Array.isArray(expressions) || expressions.length !== 0 || !Array.isArray(quasis)) {
return undefined;
}
return quasis
.map((quasi) => {
const cooked = (quasi as AstNode)['value'];
return cooked && typeof cooked === 'object'
? String((cooked as AstNode)['cooked'] ?? '')
: '';
})
.join('');
}
return undefined;
}
function sharedAdmissionViolations(sources: ReadonlyMap<string, string>): LayeringViolation[] {
const source = sources.get(RUNTIME_ADMISSION_MODULE);
if (source === undefined) {
return [violation(RUNTIME_ADMISSION_MODULE, 1, 'shared runtime admission module is missing')];
}
const program = parseSync(RUNTIME_ADMISSION_MODULE, source).program as AstNode;
const admit = namedFunction(program, 'admitRuntimeOperations');
if (admit === undefined) {
return [
violation(
RUNTIME_ADMISSION_MODULE,
1,
'shared runtime admission must expose admitRuntimeOperations',
),
];
}
const violations: LayeringViolation[] = [];
for (const [helper, role] of [
['requireFactsInspection', 'facts inspection'],
['requireDeviceBinding', 'binding'],
] as const) {
const calls = countNamedCalls(admit, helper);
if (calls !== 1) {
violations.push(
at(
RUNTIME_ADMISSION_MODULE,
source,
admit,
`shared runtime admission must make one ${role} call (found ${calls})`,
),
);
}
const references = countNamedIdentifiers(admit, helper);
if (references !== calls) {
violations.push(
at(
RUNTIME_ADMISSION_MODULE,
source,
admit,
`shared runtime admission must call ${helper} directly without aliasing it`,
),
);
}
}
return violations;
}
function namedFunction(program: AstNode, expected: string): AstNode | undefined {
let found: AstNode | undefined;
visitAst(program, (node) => {
if (node['type'] !== 'FunctionDeclaration') return;
const id = node['id'] as AstNode | null | undefined;
if (id?.['type'] === 'Identifier' && id['name'] === expected) found = node;
});
return found;
}
function countNamedCalls(functionNode: AstNode, expected: string): number {
let count = 0;
visitAst(functionNode, (node) => {
if (node['type'] !== 'CallExpression') return;
const callee = node['callee'] as AstNode | undefined;
if (callee?.['type'] === 'Identifier' && callee['name'] === expected) count += 1;
});
return count;
}
function countNamedIdentifiers(functionNode: AstNode, expected: string): number {
let count = 0;
visitAst(functionNode, (node) => {
if (node['type'] === 'Identifier' && node['name'] === expected) count += 1;
});
return count;
}
function at(file: string, source: string, node: AstNode, message: string): LayeringViolation {
const offset = typeof node['start'] === 'number' ? node['start'] : 0;
return violation(file, source.slice(0, offset).split('\n').length, message);
}
function violation(file: string, line: number, message: string): LayeringViolation {
return { rule: RUNTIME_EXECUTION_INTEGRITY_RULE, file, line, message };
}