You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/scripts/agent-check.ts

149 lines
5.6 KiB

/**
* `agent-check` — the delegation axis's mechanical guard (D-AG.11).
*
* The signed shape audits SIX things (PLAN-agent.md §3 · D-AG.11):
*
* 1. manifest ↔ real public API (typed drift) [needs the manifest tree]
* 2. declared maturity tier [needs the manifest tree]
* 3. truthfulness of per-effect reversibility [needs the manifest tree]
* 4. model-facing quality (descriptions, args, faults) [needs the manifest tree]
* 5. actor anti-forgery — no canon site builds actor contexts by hand
* 6. no-bypass — every act maps to an emitting door [needs the manifest tree]
*
* The plan says the guard is born WITH `src/uix/agent/components/`, never
* after it. That tree is F4b work, so five audits have no subject yet — they
* are DECLARED here (and reported as pending) so the guard can never lag
* behind the surface it watches: adding a manifest turns them on.
*
* Audit 5 does have a subject today: F1 shipped the actor primitive
* (`$libs/actor` + the engine's private WeakMap registry), and nothing until
* now verified the invariant that keeps it honest — that **only the engine
* mints**. A structurally-forged token resolves to nothing at runtime, but a
* cast makes it type-check, and a hand-built actor context in the canon is
* exactly the impersonation the axis forbids (agent.md §5).
*/
import { readdirSync, readFileSync, statSync } from 'node:fs';
import { join, relative, sep } from 'node:path';
const ROOT = process.cwd();
const CANON_ROOTS = ['src/uix', 'src/arts', 'src/libs', 'src/svrs', 'src/packs', 'web'];
/** The minting authority — the only place allowed to produce a token. */
const AGENT_ART = join('src', 'arts', 'agent');
/** Where the manifest tree will live (F4b). */
const MANIFEST_TREE = join('src', 'uix', 'agent', 'components');
interface Finding {
readonly file: string;
readonly line: number;
readonly rule: string;
readonly detail: string;
}
function walk(dir: string, out: string[] = []): string[] {
let entries: string[];
try {
entries = readdirSync(dir);
} catch {
return out;
}
for (const entry of entries) {
if (entry === 'node_modules' || entry === '.svelte-kit' || entry === 'generated') continue;
const full = join(dir, entry);
const stat = statSync(full);
if (stat.isDirectory()) walk(full, out);
else if (/\.(ts|svelte)$/.test(entry)) out.push(full);
}
return out;
}
/** Source files of the canon, excluding the agent art itself. */
function canonFiles(): string[] {
const files: string[] = [];
for (const root of CANON_ROOTS) walk(join(ROOT, root), files);
return files.filter((f) => !relative(ROOT, f).startsWith(AGENT_ART));
}
const findings: Finding[] = [];
// ── Audit 5 · actor anti-forgery ──────────────────────────────────────────
// The token is a compile-time brand with NO runtime constructor: the only way
// to fabricate one is to cast. Outside the engine, a cast is a forgery — and
// it type-checks, which is precisely why this is a lint and not a type rule
// (D-AG.8 §2: "su defensa anti-forja es el lint de agent-check, no el sistema
// de tipos").
const CAST_RE = /\bas\s+(?:unknown\s+as\s+)?ActorToken\b/;
// A hand-built actor context: an `actor:` property whose value is a literal
// (object / string / number) instead of a token RECEIVED from the engine.
// Forwarding (`actor: opts.actor`, `actor`, `actor: ref`) is the sanctioned
// seam and stays silent.
const HANDBUILT_ACTOR_RE = /\bactor\s*:\s*(\{|'|"|`|\d)/;
for (const file of canonFiles()) {
const rel = relative(ROOT, file).split(sep).join('/');
const isTest = /\.(test|spec)\.(ts|svelte)$/.test(rel);
const source = readFileSync(file, 'utf8');
if (!source.includes('ctor')) continue;
const lines = source.split('\n');
lines.forEach((text, i) => {
if (CAST_RE.test(text)) {
findings.push({
file: rel,
line: i + 1,
rule: 'actor-forgery',
detail: 'casts to ActorToken — only EngineAgent may mint a token that resolves'
});
}
// Tests legitimately fabricate a forged token to PROVE it resolves to
// null; that is the invariant being tested, not a violation.
if (!isTest && HANDBUILT_ACTOR_RE.test(text)) {
findings.push({
file: rel,
line: i + 1,
rule: 'actor-handbuilt',
detail: 'builds an actor context by hand — carry the received token verbatim'
});
}
});
}
// ── Audits 1·2·3·4·6 · pending on the manifest tree ───────────────────────
let manifests = 0;
try {
manifests = walk(join(ROOT, MANIFEST_TREE)).length;
} catch {
manifests = 0;
}
const PENDING = [
'manifest ↔ real public API (typed drift)',
'declared maturity tier',
'per-effect reversibility truthfulness',
'model-facing quality (descriptions · args · fault classes)',
'no-bypass (every act maps to an emitting door)'
];
console.log('agent-check — the delegation axis guard (D-AG.11)');
console.log(
` actor anti-forgery: ${findings.length === 0 ? 'clean' : `${findings.length} finding(s)`}` +
` manifests: ${manifests}`
);
if (findings.length > 0) {
console.log('');
for (const f of findings) {
console.log(` ✗ [${f.rule}] ${f.file}:${f.line}`);
console.log(` ${f.detail}`);
}
}
if (manifests === 0) {
console.log('');
console.log(` · no manifest tree yet (${MANIFEST_TREE.split(sep).join('/')}) — F4b.`);
console.log(' Audits waiting for their subject, ON PURPOSE (the guard must not');
console.log(' lag behind the surface it watches — adding a manifest turns them on):');
for (const p of PENDING) console.log(` - ${p}`);
}
if (findings.length > 0) process.exit(1);

Powered by TurnKey Linux.