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.
149 lines
5.6 KiB
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);
|