mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
2ec4e91b11
* 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>
229 lines
8.5 KiB
TypeScript
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 };
|
|
}
|