/** * `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);