mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-27 21:19:18 +00:00
* feat(skill-catalog): host-repo skill catalog core + mcp config keys New src/core/skill-catalog.ts resolves the agent repo's skills dir, builds a flat catalog, fetches one skill's prose, and gates publishing. Path confinement (manifest-vetted lookup + realpath + SKILL.md file-type check), 256KB response cap, frontmatter allowlist, and D7 tool cross-reference live here. config.ts gains the mcp.publish_skills / mcp.skills_dir keys (+ KNOWN_CONFIG entries). * feat(mcp): list_skills + get_skill read ops for thin-client skill discovery Two read-scope, non-localOnly ops (dynamic-import skill-catalog to avoid the cycle) let Codex/Claude Code/Perplexity discover and follow the agent's skills over gbrain serve. Descriptions + the instructional envelope constants are pinned in operations-descriptions.ts. * feat(mcp): default new installs to publish skills; consent prompt on upgrade gbrain init writes mcp.publish_skills:true (file plane) so new installs publish by default. gbrain upgrade prompts existing installs once (DB plane), strongly recommending opt-in, showing the resolved skills dir + that SKILL.md contents become readable by authorized remote MCP callers. * test(skill-catalog): catalog, security-confinement, transport-gate, description pins 40 cases: buildSkillCatalog/getSkillDetail/D7 split (skill-catalog.test.ts), path traversal + symlink + poisoned-manifest + oversize + non-SKILL.md + gate (skill-catalog-security.test.ts), and real dispatchToolCall gate+scope+plane coverage (skill-catalog-transports.test.ts). Plus list_skills/get_skill description + envelope pins. * chore: bump version and changelog (v0.41.36.0) MCP skill catalog (list_skills / get_skill) — thin clients can discover and follow the agent repo's skills over gbrain serve. PR2 (tarball/install) filed in TODOS. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: document skill-catalog (list_skills/get_skill + mcp config keys) for v0.41.36.0 Add the src/core/skill-catalog.ts Key-files annotation to CLAUDE.md covering the two new read-scope MCP ops, the mcp.publish_skills / mcp.skills_dir config keys, the full trust-boundary mitigation stack, and the init/upgrade wiring. Regenerate llms-full.txt to match (CI build-llms drift gate). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
187 lines
6.6 KiB
TypeScript
187 lines
6.6 KiB
TypeScript
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
|
|
import { mkdirSync, writeFileSync, symlinkSync, rmSync, mkdtempSync } from 'fs';
|
|
import { join } from 'path';
|
|
import { tmpdir } from 'os';
|
|
import { withEnv } from './helpers/with-env.ts';
|
|
import type { OperationContext } from '../src/core/operations.ts';
|
|
import { OperationError } from '../src/core/operations.ts';
|
|
import {
|
|
resolveSkillMdPath,
|
|
getSkillDetail,
|
|
resolveSkillsDir,
|
|
assertPublishEnabled,
|
|
} from '../src/core/skill-catalog.ts';
|
|
|
|
function ctx(remote: boolean, scopes?: string[]): OperationContext {
|
|
return {
|
|
remote,
|
|
auth: scopes ? { token: 't', clientId: 'c', scopes } : undefined,
|
|
config: {} as OperationContext['config'],
|
|
engine: null as unknown as OperationContext['engine'],
|
|
logger: { info() {}, warn() {}, error() {} },
|
|
dryRun: false,
|
|
sourceId: 'default',
|
|
} as OperationContext;
|
|
}
|
|
|
|
let root: string;
|
|
let skillsDir: string;
|
|
|
|
beforeAll(() => {
|
|
root = mkdtempSync(join(tmpdir(), 'skill-sec-'));
|
|
skillsDir = join(root, 'skills');
|
|
mkdirSync(join(skillsDir, 'good'), { recursive: true });
|
|
writeFileSync(join(skillsDir, 'good', 'SKILL.md'), '---\nname: good\n---\n\nbody\n');
|
|
// An oversize skill (> 256KB default cap).
|
|
mkdirSync(join(skillsDir, 'huge'), { recursive: true });
|
|
writeFileSync(join(skillsDir, 'huge', 'SKILL.md'), '---\nname: huge\n---\n' + 'x'.repeat(300 * 1024));
|
|
// A non-SKILL.md target reached via a poisoned manifest.json.
|
|
writeFileSync(join(root, 'secret.txt'), 'TOP SECRET OUTSIDE SKILLS');
|
|
mkdirSync(join(skillsDir, 'notskill'), { recursive: true });
|
|
writeFileSync(join(skillsDir, 'notskill', 'data.md'), 'not a skill file');
|
|
});
|
|
|
|
afterAll(() => {
|
|
try { rmSync(root, { recursive: true, force: true }); } catch { /* best-effort */ }
|
|
});
|
|
|
|
describe('resolveSkillMdPath — name shape rejection (pre-FS)', () => {
|
|
for (const bad of ['../etc/passwd', 'a/b', 'a\\b', 'x |