From 472d760ad0359946096a5d72bbd91402d70764c4 Mon Sep 17 00:00:00 2001 From: Giovanni Giovanni Date: Fri, 17 Jul 2026 05:48:17 -0400 Subject: [PATCH] chore: remove the standalone afi-skills catalog surface The Gateway authenticates and routes; it does not own or serve a manually curated agent skills catalog. Removes the file-backed reader of the deleted afi-skills repository and everything that existed only to expose it. Removed: - src/services/skillsService.ts (read ../afi-skills/manifest.json; the AFI_SKILLS_MANIFEST_PATH / AFI_SKILL_PLUGIN_MAP env vars lived here). - the /api/v1/skills, /api/v1/skills/:id, /api/v1/skills/capabilities routes and their imports in src/http/app.ts. - tests/skills.test.ts (proved only the removed behavior). - stale afi-skills references in src/index.ts comments and two docs. Added tests/guardrails/no-skills-surface.test.ts to make reintroduction (sibling reader, env var, service module, route surface, CI checkout) structurally impossible. Gateway's real boundary behavior is untouched: authentication, tenant/provider resolution, provider-to-strategy authorization, routing, honest errors, routes-not-writes, and the evidence-v2 boundary. build + typecheck + full test suite (56/56) green. Co-Authored-By: Claude Opus 4.8 (1M context) --- TSSD_VAULT_READINESS_REPORT.md | 35 +----- docs/AFI_AGENT_PLAYBOOK.v0.1.md | 4 +- src/http/app.ts | 45 -------- src/index.ts | 3 +- src/services/skillsService.ts | 120 --------------------- tests/guardrails/no-skills-surface.test.ts | 93 ++++++++++++++++ tests/skills.test.ts | 51 --------- 7 files changed, 96 insertions(+), 255 deletions(-) delete mode 100644 src/services/skillsService.ts create mode 100644 tests/guardrails/no-skills-surface.test.ts delete mode 100644 tests/skills.test.ts diff --git a/TSSD_VAULT_READINESS_REPORT.md b/TSSD_VAULT_READINESS_REPORT.md index 1e3c2f9..195938d 100644 --- a/TSSD_VAULT_READINESS_REPORT.md +++ b/TSSD_VAULT_READINESS_REPORT.md @@ -233,35 +233,6 @@ afi-config provides **governance** for vault configuration but no **implementati --- -### 2.6 afi-skills - -**What Exists:** - -- **Vault Replay Skill:** - - `skills/provenance/vault-replay-determinism.md` (183 lines) - **Comprehensive skill definition:** - - Purpose: Verify deterministic replay of historical epochs - - Inputs: epoch_id, original_outputs, replay_config - - Outputs: determinism_result, diff_report - - Allowed tools: `codex:replay`, `tssd:read` - - Risk level: medium, determinism_required: true - - Golden cases: `evals/provenance/vault-replay-determinism/golden_cases.json` - -- **Codex:** - - `.afi-codex.json` (32 lines) - Skill library - - Provides: agent-skills, skill-library, skill-schemas - - Consumers: afi-core, afi-reactor, afi-factory - -**What's Missing:** - -- No implementation of vault-replay-determinism skill (only spec) -- No other vault-related skills (e.g., vault-query, vault-audit, vault-export) - -**Implications:** - -afi-skills defines **provenance and replay requirements** for the vault but has **no implementations**. The skill spec assumes `tssd:read` tool exists (it doesn't yet). - ---- - ### 2.7 afi-token **What Exists:** @@ -544,7 +515,7 @@ afi-factory is **not involved** in vault design or implementation. This is expec - **Possible interpretations**: - **Re-execute pipeline**: Re-run RAW → ENRICHED → ANALYZED → SCORED with same inputs - **Outcome evaluation**: Compare predicted vs. actual outcomes (REPLAYED stage) - - **Determinism check**: Verify bit-for-bit identical results (afi-skills vault-replay-determinism) + - **Determinism check**: Verify bit-for-bit identical results - **Recommendation**: Support **all three** with different replay modes: - `replay --mode=re-execute` - Re-run pipeline - `replay --mode=evaluate` - Compare outcomes @@ -722,10 +693,6 @@ afi-factory is **not involved** in vault design or implementation. This is expec - `.afi-codex.json` - `schemas/vault.schema.json` -**afi-skills** (2 files): -- `.afi-codex.json` -- `skills/provenance/vault-replay-determinism.md` - **afi-token** (3 files): - `.afi-codex.json` - `src/AFIMintCoordinator.sol`, `src/TestCoreToken.sol` diff --git a/docs/AFI_AGENT_PLAYBOOK.v0.1.md b/docs/AFI_AGENT_PLAYBOOK.v0.1.md index c273167..1f2774c 100644 --- a/docs/AFI_AGENT_PLAYBOOK.v0.1.md +++ b/docs/AFI_AGENT_PLAYBOOK.v0.1.md @@ -89,9 +89,7 @@ Capabilities implemented as **Eliza actions, providers, evaluators, or services* - May orchestrate non-AFI data sources (news, markets, social sentiment). - MUST NOT reimplement AFI scoring, DAG logic, or tokenomics in runtime code. -**Contrast with AFI Skills (repo land)**: -- **AFI Skills** (in `afi-skills` repo): Canonical, testable capabilities encoded as config/templates/code modules. -- **Runtime Skills** (agent side): Behaviors exposed via Eliza plugins that call AFI services. +**Runtime Skills** (agent side): behaviors exposed via Eliza plugins that call AFI services (distinct from pipeline-authoring artifacts, which are not loaded or executed in the agent runtime). --- diff --git a/src/http/app.ts b/src/http/app.ts index 3dbfb72..e5ba5f6 100644 --- a/src/http/app.ts +++ b/src/http/app.ts @@ -8,11 +8,6 @@ import { projectSubmission, } from "../services/reactorSubmitter.js"; import { apiKeyAuthMiddleware, createRateLimiter, type AuthedRequest } from "../middleware/apiKeyAuth.js"; -import { - listSkills, - getSkillById, - summarizeCapabilities, -} from "../services/skillsService.js"; export interface AppDeps { apiKeyStore?: ApiKeyStore; @@ -142,46 +137,6 @@ export function buildApp(deps: AppDeps = {}) { return res.status(projected.status).json(projected.body); }); - // Skills discovery (public) - app.get("/api/v1/skills", async (req: Request, res: Response) => { - try { - const { q, domain, tag } = req.query; - const skills = await listSkills( - { - q: typeof q === "string" ? q : undefined, - domain: typeof domain === "string" ? domain : undefined, - tag: typeof tag === "string" ? tag : undefined, - }, - process.env.AFI_SKILLS_MANIFEST_PATH - ); - return res.status(200).json({ items: skills }); - } catch (err) { - elizaLogger.error("[skills:list] error", err); - return res.status(500).json({ error: "skills_list_failed" }); - } - }); - - app.get("/api/v1/skills/:id", async (req: Request, res: Response) => { - try { - const skill = await getSkillById(req.params.id, process.env.AFI_SKILLS_MANIFEST_PATH); - if (!skill) return res.status(404).json({ error: "not_found" }); - return res.status(200).json(skill); - } catch (err) { - elizaLogger.error("[skills:get] error", err); - return res.status(500).json({ error: "skills_get_failed" }); - } - }); - - app.get("/api/v1/skills/capabilities", async (_req: Request, res: Response) => { - try { - const summary = await summarizeCapabilities(process.env.AFI_SKILLS_MANIFEST_PATH); - return res.status(200).json(summary); - } catch (err) { - elizaLogger.error("[skills:capabilities] error", err); - return res.status(500).json({ error: "skills_capabilities_failed" }); - } - }); - // 404 app.use((req: Request, res: Response) => { res.status(404).json({ error: "not_found", path: req.path }); diff --git a/src/index.ts b/src/index.ts index 211124a..59ad410 100644 --- a/src/index.ts +++ b/src/index.ts @@ -49,7 +49,7 @@ import * as readline from "readline"; * - DISCORD_APPLICATION_ID: Discord application ID (if using Discord client) * - DISCORD_API_TOKEN: Discord bot token (if using Discord client) * - AFI_REACTOR_URL: URL for AFI Reactor API (future, for AFI telemetry plugin) - * - AFI_CORE_URL: URL for AFI Core API (future, for AFI skills plugin) + * - AFI_CORE_URL: URL for AFI Core API (future) */ async function main() { try { @@ -84,7 +84,6 @@ async function main() { // TODO: Register additional plugins when available: // - @elizaos/plugin-node (Node.js services: browser, PDF, speech, etc.) - // - @afi/plugin-afi-skills (skill invocation via AFI APIs) elizaLogger.success("✅ AgentRuntime framework initialized successfully"); elizaLogger.info("🤖 Framework ready for custom characters"); diff --git a/src/services/skillsService.ts b/src/services/skillsService.ts deleted file mode 100644 index e75ccd5..0000000 --- a/src/services/skillsService.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { promises as fs } from "fs"; -import path from "path"; - -export interface SkillRecord { - id: string; - name: string; - version?: string; - domain?: string; - description?: string; - risk_level?: string; - determinism_required?: boolean; - allowed_tools?: string[]; - owners?: string[]; - last_updated?: string; - tags?: string[]; - file_path?: string; - pluginId?: string; -} - -export interface SkillsManifest { - version: string; - generated_at?: string; - skill_count?: number; - skills: SkillRecord[]; -} - -export interface SkillSearchQuery { - q?: string; - domain?: string; - tag?: string; -} - -export interface CapabilitySummary { - totalSkills: number; - domains: Record; - tags: Record; -} - -const defaultManifestPath = path.resolve(process.cwd(), "../afi-skills/manifest.json"); - -let cachedManifest: SkillsManifest | null = null; -let cachedPath: string | null = null; - -function loadPluginMap(): Record { - const raw = process.env.AFI_SKILL_PLUGIN_MAP; - if (!raw) return {}; - try { - return JSON.parse(raw); - } catch (err) { - console.warn("[skillsService] Failed to parse AFI_SKILL_PLUGIN_MAP, ignoring", err); - return {}; - } -} - -export async function loadSkillsManifest(manifestPath?: string): Promise { - const resolved = manifestPath ? path.resolve(manifestPath) : defaultManifestPath; - - if (cachedManifest && cachedPath === resolved) { - return cachedManifest; - } - - const data = await fs.readFile(resolved, "utf8"); - const parsed: SkillsManifest = JSON.parse(data); - - const pluginMap = loadPluginMap(); - parsed.skills = (parsed.skills || []).map((s) => ({ - ...s, - pluginId: pluginMap[s.id], - })); - - cachedManifest = parsed; - cachedPath = resolved; - return parsed; -} - -export async function listSkills(query: SkillSearchQuery = {}, manifestPath?: string): Promise { - const manifest = await loadSkillsManifest(manifestPath); - const q = query.q?.toLowerCase(); - const tag = query.tag?.toLowerCase(); - const domain = query.domain?.toLowerCase(); - - return (manifest.skills || []).filter((skill) => { - if (domain && skill.domain?.toLowerCase() !== domain) return false; - if (tag && !skill.tags?.some((t) => t.toLowerCase() === tag)) return false; - if (q) { - const haystack = [skill.id, skill.name, skill.description, ...(skill.tags || [])] - .filter(Boolean) - .join(" ") - .toLowerCase(); - if (!haystack.includes(q)) return false; - } - return true; - }); -} - -export async function getSkillById(id: string, manifestPath?: string): Promise { - const manifest = await loadSkillsManifest(manifestPath); - return manifest.skills.find((s) => s.id === id); -} - -export async function summarizeCapabilities(manifestPath?: string): Promise { - const manifest = await loadSkillsManifest(manifestPath); - const domains: Record = {}; - const tags: Record = {}; - - for (const skill of manifest.skills) { - if (skill.domain) { - domains[skill.domain] = (domains[skill.domain] || 0) + 1; - } - for (const t of skill.tags || []) { - tags[t] = (tags[t] || 0) + 1; - } - } - - return { - totalSkills: manifest.skills.length, - domains, - tags, - }; -} diff --git a/tests/guardrails/no-skills-surface.test.ts b/tests/guardrails/no-skills-surface.test.ts new file mode 100644 index 0000000..0171d71 --- /dev/null +++ b/tests/guardrails/no-skills-surface.test.ts @@ -0,0 +1,93 @@ +/** + * GUARDRAIL — the Gateway serves no standalone skills catalog. + * + * The Gateway authenticates and routes; it does not own or serve a manually + * curated agent skills catalog. The removed `afi-skills` repository was read by + * a filesystem manifest reader and exposed through `/api/v1/skills*` routes; + * this guard makes those reintroduction paths structurally impossible: + * + * - a filesystem reader of the removed repo's manifest (`../afi-skills`), + * - an environment variable locating that repo/manifest, + * - a skills-manifest service module or its route surface, + * - a CI step that checks out the removed sibling repository. + * + * The token list below exists solely for enforcement; it is not documentation. + * + * Scope: package manifests, lockfile, source (src/plugins/scripts), and the + * workflow files under .github/workflows. + */ + +import { describe, it, expect } from "vitest"; +import { readFileSync, existsSync, readdirSync, statSync } from "node:fs"; +import { join, resolve } from "node:path"; + +const REPO_ROOT = resolve(__dirname, "..", ".."); + +/** The removed repository, by name and as a sibling path. */ +const FORBIDDEN_REPO = "afi-skills"; +const FORBIDDEN_SIBLING = "../afi-skills"; + +/** Removed environment variables that located the manifest / plugin map. */ +const FORBIDDEN_ENV = ["AFI_SKILLS_MANIFEST_PATH", "AFI_SKILL_PLUGIN_MAP"]; + +/** Removed source surfaces. */ +const FORBIDDEN_TOKENS = ["skillsService", "/api/v1/skills"]; + +const SCANNED_DIRS = ["src", "plugins", "scripts"]; + +function collect(dir: string, exts: string[]): string[] { + const abs = join(REPO_ROOT, dir); + if (!existsSync(abs)) return []; + const out: string[] = []; + const walk = (current: string) => { + for (const entry of readdirSync(current)) { + if (entry === "node_modules" || entry === "dist" || entry.startsWith(".")) continue; + const full = join(current, entry); + if (statSync(full).isDirectory()) walk(full); + else if (exts.some((e) => entry.endsWith(e))) out.push(full); + } + }; + walk(abs); + return out; +} + +const rel = (f: string) => f.slice(REPO_ROOT.length + 1); +const SOURCE_FILES = SCANNED_DIRS.flatMap((d) => collect(d, [".ts", ".mts"])); +const WORKFLOW_FILES = collect(".github/workflows", [".yml", ".yaml"]); + +describe("GUARDRAIL: gateway serves no standalone skills catalog", () => { + it("scans a non-empty set of source files", () => { + expect(SOURCE_FILES.length).toBeGreaterThan(5); + }); + + it("no source file reintroduces the removed skills surface", () => { + const banned = [FORBIDDEN_REPO, FORBIDDEN_SIBLING, ...FORBIDDEN_ENV, ...FORBIDDEN_TOKENS]; + const offenders: string[] = []; + for (const file of SOURCE_FILES) { + const content = readFileSync(file, "utf8"); + if (banned.some((t) => content.includes(t))) offenders.push(rel(file)); + } + expect(offenders).toEqual([]); + }); + + it("declares no skills-repo dependency and keeps the lockfile free of its sibling path", () => { + const pkg = JSON.parse(readFileSync(join(REPO_ROOT, "package.json"), "utf8")); + const all = { ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}), ...(pkg.optionalDependencies ?? {}) }; + expect(Object.keys(all).some((d) => d.includes(FORBIDDEN_REPO))).toBe(false); + const lockPath = join(REPO_ROOT, "package-lock.json"); + if (existsSync(lockPath)) { + expect(readFileSync(lockPath, "utf8").includes(FORBIDDEN_SIBLING)).toBe(false); + } + }); + + it("checks out no external skills sibling repository in CI", () => { + const offenders: string[] = []; + for (const file of WORKFLOW_FILES) { + const content = readFileSync(file, "utf8"); + if (content.includes(FORBIDDEN_SIBLING) || /repository:\s*\S*afi-skills/.test(content)) { + offenders.push(rel(file)); + } + } + expect(offenders).toEqual([]); + }); +}); diff --git a/tests/skills.test.ts b/tests/skills.test.ts deleted file mode 100644 index 249ae6c..0000000 --- a/tests/skills.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { describe, it, expect, beforeAll, afterAll } from "vitest"; -import { writeFileSync, rmSync } from "fs"; -import path from "path"; -import os from "os"; -import { listSkills, getSkillById, summarizeCapabilities } from "../src/services/skillsService.js"; - -const tmpManifestPath = path.join(os.tmpdir(), "afi-skills-manifest-test.json"); - -beforeAll(() => { - const manifest = { - version: "0.1.0", - skills: [ - { id: "alpha", name: "Alpha Skill", domain: "test", tags: ["foo"], description: "Alpha" }, - { id: "beta", name: "Beta Skill", domain: "demo", tags: ["bar"] }, - ], - }; - writeFileSync(tmpManifestPath, JSON.stringify(manifest), "utf8"); - process.env.AFI_SKILLS_MANIFEST_PATH = tmpManifestPath; -}); - -afterAll(() => { - try { - rmSync(tmpManifestPath, { force: true }); - } catch { - /* ignore */ - } -}); - -describe("skills service", () => { - it("lists skills", async () => { - const skills = await listSkills({}, tmpManifestPath); - expect(skills).toHaveLength(2); - }); - - it("filters by query", async () => { - const skills = await listSkills({ q: "beta" }, tmpManifestPath); - expect(skills).toHaveLength(1); - expect(skills[0].id).toBe("beta"); - }); - - it("returns detail", async () => { - const skill = await getSkillById("alpha", tmpManifestPath); - expect(skill?.id).toBe("alpha"); - }); - - it("summarizes capabilities", async () => { - const summary = await summarizeCapabilities(tmpManifestPath); - expect(summary.totalSkills).toBe(2); - expect(summary.domains.test).toBe(1); - }); -});