open source is social v-it.org
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

vet: add --dangerous-accept and sandboxed sub-agent vetting

Three changes to vit vet:

1. --dangerous-accept (human only): permanent project-wide vet bypass.
Writes .vit/dangerous-accept flag file. Two-step with --confirm.
Writes .vit/.gitignore to prevent committing the flag.

2. Agent error: when an agent runs plain vit vet <ref>, new error
explains the sandboxed sub-agent pattern and suggests --trust --confirm.

3. --trust --confirm: bypasses requireNotAgent() for sandboxed sub-agents.
--confirm alone does nothing. Must be paired with --trust.

Ref argument changed from <ref> to [ref] (not needed for --dangerous-accept).

Tests cover: dangerous-accept with/without confirm, agent blocking,
duplicate gitignore, --trust --confirm agent/human, --confirm without --trust.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+193 -40
+75 -19
src/cmd/vet.js
··· 1 1 // SPDX-License-Identifier: AGPL-3.0-only 2 2 // Copyright (c) 2026 sol pbc 3 3 4 + import { existsSync, readFileSync, writeFileSync, appendFileSync } from 'node:fs'; 5 + import { join } from 'node:path'; 4 6 import { requireDid } from '../lib/config.js'; 5 7 import { CAP_COLLECTION, SKILL_COLLECTION } from '../lib/constants.js'; 6 8 import { restoreAgent } from '../lib/oauth.js'; 7 - import { appendLog, readProjectConfig, readFollowing } from '../lib/vit-dir.js'; 8 - import { requireNotAgent } from '../lib/agent.js'; 9 + import { appendLog, readProjectConfig, readFollowing, vitDir } from '../lib/vit-dir.js'; 10 + import { requireNotAgent, detectCodingAgent } from '../lib/agent.js'; 9 11 import { resolveRef, REF_PATTERN } from '../lib/cap-ref.js'; 10 12 import { isSkillRef, isValidSkillRef, nameFromSkillRef } from '../lib/skill-ref.js'; 11 13 import { mark, brand, name } from '../lib/brand.js'; 12 14 import { resolvePds, listRecordsFromPds } from '../lib/pds.js'; 13 15 16 + function ensureGitignore() { 17 + const gitignorePath = join(vitDir(), '.gitignore'); 18 + const entry = 'dangerous-accept'; 19 + if (existsSync(gitignorePath)) { 20 + const content = readFileSync(gitignorePath, 'utf-8'); 21 + if (content.includes(entry)) return; 22 + } 23 + appendFileSync(gitignorePath, entry + '\n'); 24 + } 25 + 14 26 export default function register(program) { 15 27 program 16 28 .command('vet') 17 - .argument('<ref>', 'Cap or skill reference (e.g. fast-cache-invalidation or skill-agent-test-patterns)') 29 + .argument('[ref]', 'Cap or skill reference (e.g. fast-cache-invalidation or skill-agent-test-patterns)') 18 30 .description('Review a cap or skill before trusting it') 19 31 .option('--did <did>', 'DID to use') 20 32 .option('--trust', 'Mark the item as locally trusted') 33 + .option('--dangerous-accept', 'Permanently disable vet gate for this project (human only)') 34 + .option('--confirm', 'Confirm dangerous-accept, or bypass agent gate with --trust') 21 35 .option('-v, --verbose', 'Show step-by-step details') 22 36 .action(async (ref, opts) => { 23 37 try { 24 - const gate = requireNotAgent(); 25 - if (!gate.ok) { 26 - console.error(`${name} vet must be run by a human. run it in your own terminal.`); 27 - console.error(''); 28 - console.error('vetting requires human review for safety.'); 29 - console.error('ask your user to run this command in their terminal:'); 30 - console.error(''); 31 - console.error(` vit vet ${ref}`); 32 - console.error(''); 33 - console.error('after reviewing, they can trust it with:'); 34 - console.error(''); 35 - console.error(` vit vet ${ref} --trust`); 36 - console.error(''); 37 - console.error('once trusted, ask your user to confirm and you can proceed.'); 38 + // --- dangerous-accept flow --- 39 + if (opts.dangerousAccept) { 40 + const gate = requireNotAgent(); 41 + if (!gate.ok) { 42 + console.error(`${name} vet --dangerous-accept is human-only. agents cannot set this flag.`); 43 + process.exitCode = 1; 44 + return; 45 + } 46 + 47 + if (opts.confirm) { 48 + // Write the flag file 49 + const dir = vitDir(); 50 + const acceptPath = join(dir, 'dangerous-accept'); 51 + writeFileSync(acceptPath, JSON.stringify({ acceptedAt: new Date().toISOString() }) + '\n'); 52 + ensureGitignore(); 53 + console.log('dangerous-accept enabled for this project.'); 54 + console.log(''); 55 + console.log('agents can now remix and learn without vetting.'); 56 + console.log('to revoke: delete .vit/dangerous-accept'); 57 + } else { 58 + console.log(''); 59 + console.log(' WARNING: this permanently disables the vetting safety gate for all'); 60 + console.log(' caps and skills in this project.'); 61 + console.log(''); 62 + console.log(' any agent running in this project can remix caps and learn skills'); 63 + console.log(' without human review. only do this if you trust the agent\'s judgment'); 64 + console.log(' and the network sources you follow.'); 65 + console.log(''); 66 + console.log(' to proceed, confirm: vit vet --dangerous-accept --confirm'); 67 + } 68 + return; 69 + } 70 + 71 + // --- Regular vet flow: ref is required --- 72 + if (!ref) { 73 + console.error('ref argument is required for vetting. usage: vit vet <ref>'); 38 74 process.exitCode = 1; 39 75 return; 40 76 } 41 77 78 + // --- Agent gate --- 79 + const agent = detectCodingAgent(); 80 + if (agent) { 81 + if (opts.trust && opts.confirm) { 82 + // Sandboxed sub-agent pattern — allow it 83 + } else { 84 + console.error('vit vet is for human review. agents should not vet directly.'); 85 + console.error(''); 86 + console.error('if you are a sandboxed sub-agent specifically tasked with vetting,'); 87 + console.error('you can bypass this gate:'); 88 + console.error(''); 89 + console.error(` vit vet ${ref} --trust --confirm`); 90 + console.error(''); 91 + console.error('this will trust the ref without interactive review. only use this'); 92 + console.error('if you are a dedicated vetting agent running in an isolated context.'); 93 + process.exitCode = 1; 94 + return; 95 + } 96 + } 97 + 42 98 const { verbose } = opts; 43 99 const isSkill = isSkillRef(ref); 44 100 ··· 72 128 } 73 129 if (verbose) console.log(`[verbose] beacon: ${beacon}`); 74 130 75 - const { agent } = await restoreAgent(did); 131 + const { agent: oauthAgent } = await restoreAgent(did); 76 132 if (verbose) console.log('[verbose] session restored'); 77 133 78 134 // build DID list from following + self ··· 149 205 // Skill vet — no beacon required 150 206 const skillName = nameFromSkillRef(ref); 151 207 152 - const { agent } = await restoreAgent(did); 208 + const { agent: oauthAgent } = await restoreAgent(did); 153 209 if (verbose) console.log('[verbose] session restored'); 154 210 155 211 const following = readFollowing();
+2 -2
test/vet-skill.test.js
··· 16 16 test('rejects when run inside a coding agent (skill ref)', () => { 17 17 const result = run('vet skill-agent-test', undefined, { CLAUDECODE: '1' }); 18 18 expect(result.exitCode).toBe(1); 19 - expect(result.stderr).toContain('must be run by a human'); 19 + expect(result.stderr).toContain('vit vet is for human review'); 20 20 expect(result.stderr).toContain('vit vet skill-agent-test'); 21 - expect(result.stderr).toContain('--trust'); 21 + expect(result.stderr).toContain('--trust --confirm'); 22 22 }); 23 23 24 24 test('rejects invalid skill ref', () => {
+116 -19
test/vet.test.js
··· 3 3 4 4 import { describe, test, expect } from 'bun:test'; 5 5 import { run } from './helpers.js'; 6 + import { mkdirSync, writeFileSync, readFileSync, existsSync, rmSync } from 'node:fs'; 7 + import { tmpdir } from 'node:os'; 8 + import { join } from 'node:path'; 9 + 10 + const noAgentEnv = { CLAUDECODE: '', GEMINI_CLI: '', CODEX_CI: '' }; 11 + const agentEnv = { CLAUDECODE: '1' }; 6 12 7 13 describe('vit vet', () => { 8 - test('shows help with <ref> argument', () => { 14 + test('shows help with [ref] argument', () => { 9 15 const result = run('vet --help'); 10 - expect(result.stdout).toContain('<ref>'); 16 + expect(result.stdout).toContain('[ref]'); 11 17 }); 12 18 13 - test('rejects when run inside a coding agent', () => { 14 - const result = run('vet fast-cache-invalidation', undefined, { CLAUDECODE: '1' }); 15 - expect(result.exitCode).toBe(1); 16 - expect(result.stderr).toContain('must be run by a human'); 17 - expect(result.stderr).toContain('vit vet fast-cache-invalidation'); 18 - expect(result.stderr).toContain('--trust'); 19 + test('rejects invalid ref format (human)', () => { 20 + const result = run('vet not-valid', undefined, noAgentEnv); 21 + expect(result.exitCode).not.toBe(0); 22 + expect(result.stderr).toContain('invalid ref'); 19 23 }); 20 24 21 - test('rejects when run inside gemini', () => { 22 - const result = run('vet fast-cache-invalidation', undefined, { CLAUDECODE: '', GEMINI_CLI: '1', CODEX_CI: '' }); 23 - expect(result.exitCode).toBe(1); 24 - expect(result.stderr).toContain('must be run by a human'); 25 + test('errors when no ref and no --dangerous-accept', () => { 26 + const result = run('vet', undefined, noAgentEnv); 27 + expect(result.exitCode).not.toBe(0); 28 + expect(result.stderr).toContain('ref argument is required'); 25 29 }); 26 30 27 - test('rejects invalid ref format', () => { 28 - const result = run('vet not-valid', undefined, { CLAUDECODE: '', GEMINI_CLI: '', CODEX_CI: '' }); 29 - expect(result.exitCode).not.toBe(0); 30 - expect(result.stderr).toContain('invalid ref'); 31 + // --- dangerous-accept tests --- 32 + 33 + describe('--dangerous-accept', () => { 34 + test('without --confirm: prints warning, does NOT write flag', () => { 35 + const tmp = join(tmpdir(), '.test-vet-da-' + Math.random().toString(36).slice(2)); 36 + mkdirSync(join(tmp, '.vit'), { recursive: true }); 37 + const result = run('vet --dangerous-accept', tmp, noAgentEnv); 38 + expect(result.exitCode).toBe(0); 39 + expect(result.stdout).toContain('WARNING'); 40 + expect(result.stdout).toContain('--dangerous-accept --confirm'); 41 + expect(existsSync(join(tmp, '.vit', 'dangerous-accept'))).toBe(false); 42 + rmSync(tmp, { recursive: true, force: true }); 43 + }); 44 + 45 + test('with --confirm: writes flag file and .gitignore', () => { 46 + const tmp = join(tmpdir(), '.test-vet-da-confirm-' + Math.random().toString(36).slice(2)); 47 + mkdirSync(join(tmp, '.vit'), { recursive: true }); 48 + const result = run('vet --dangerous-accept --confirm', tmp, noAgentEnv); 49 + expect(result.exitCode).toBe(0); 50 + expect(result.stdout).toContain('dangerous-accept enabled'); 51 + expect(existsSync(join(tmp, '.vit', 'dangerous-accept'))).toBe(true); 52 + const flagContent = JSON.parse(readFileSync(join(tmp, '.vit', 'dangerous-accept'), 'utf-8').trim()); 53 + expect(flagContent.acceptedAt).toBeTruthy(); 54 + const gitignore = readFileSync(join(tmp, '.vit', '.gitignore'), 'utf-8'); 55 + expect(gitignore).toContain('dangerous-accept'); 56 + rmSync(tmp, { recursive: true, force: true }); 57 + }); 58 + 59 + test('blocked when agent detected', () => { 60 + const tmp = join(tmpdir(), '.test-vet-da-agent-' + Math.random().toString(36).slice(2)); 61 + mkdirSync(join(tmp, '.vit'), { recursive: true }); 62 + const result = run('vet --dangerous-accept', tmp, agentEnv); 63 + expect(result.exitCode).toBe(1); 64 + expect(result.stderr).toContain('human-only'); 65 + expect(existsSync(join(tmp, '.vit', 'dangerous-accept'))).toBe(false); 66 + rmSync(tmp, { recursive: true, force: true }); 67 + }); 68 + 69 + test('blocked with --confirm when agent detected', () => { 70 + const tmp = join(tmpdir(), '.test-vet-da-agent-c-' + Math.random().toString(36).slice(2)); 71 + mkdirSync(join(tmp, '.vit'), { recursive: true }); 72 + const result = run('vet --dangerous-accept --confirm', tmp, agentEnv); 73 + expect(result.exitCode).toBe(1); 74 + expect(result.stderr).toContain('human-only'); 75 + expect(existsSync(join(tmp, '.vit', 'dangerous-accept'))).toBe(false); 76 + rmSync(tmp, { recursive: true, force: true }); 77 + }); 78 + 79 + test('running twice does not duplicate .gitignore entry', () => { 80 + const tmp = join(tmpdir(), '.test-vet-da-twice-' + Math.random().toString(36).slice(2)); 81 + mkdirSync(join(tmp, '.vit'), { recursive: true }); 82 + run('vet --dangerous-accept --confirm', tmp, noAgentEnv); 83 + run('vet --dangerous-accept --confirm', tmp, noAgentEnv); 84 + const gitignore = readFileSync(join(tmp, '.vit', '.gitignore'), 'utf-8'); 85 + const matches = gitignore.match(/dangerous-accept/g); 86 + expect(matches.length).toBe(1); 87 + rmSync(tmp, { recursive: true, force: true }); 88 + }); 31 89 }); 32 90 33 - test('fails with no arguments', () => { 34 - const result = run('vet'); 35 - expect(result.exitCode).not.toBe(0); 91 + // --- agent gate tests --- 92 + 93 + describe('agent gate', () => { 94 + test('agent without flags: error with sandboxed sub-agent hint', () => { 95 + const result = run('vet fast-cache-invalidation', undefined, agentEnv); 96 + expect(result.exitCode).toBe(1); 97 + expect(result.stderr).toContain('vit vet is for human review'); 98 + expect(result.stderr).toContain('--trust --confirm'); 99 + expect(result.stderr).toContain('sandboxed sub-agent'); 100 + }); 101 + 102 + test('agent with --trust but no --confirm: error', () => { 103 + const result = run('vet fast-cache-invalidation --trust', undefined, agentEnv); 104 + expect(result.exitCode).toBe(1); 105 + expect(result.stderr).toContain('vit vet is for human review'); 106 + expect(result.stderr).toContain('--trust --confirm'); 107 + }); 108 + 109 + test('agent with --confirm but no --trust: error', () => { 110 + const result = run('vet fast-cache-invalidation --confirm', undefined, agentEnv); 111 + expect(result.exitCode).toBe(1); 112 + expect(result.stderr).toContain('vit vet is for human review'); 113 + }); 114 + 115 + test('agent with --trust --confirm: passes agent gate (fails at DID)', () => { 116 + // Should pass agent gate but fail later at DID check 117 + const configHome = join(tmpdir(), '.test-vet-tc-' + Math.random().toString(36).slice(2)); 118 + mkdirSync(configHome, { recursive: true }); 119 + const result = run('vet fast-cache-invalidation --trust --confirm', undefined, { ...agentEnv, XDG_CONFIG_HOME: configHome }); 120 + // Should NOT contain the agent gate error 121 + expect(result.stderr).not.toContain('vit vet is for human review'); 122 + rmSync(configHome, { recursive: true, force: true }); 123 + }); 124 + 125 + test('human with --trust --confirm: passes (--confirm harmless for humans)', () => { 126 + const configHome = join(tmpdir(), '.test-vet-tc-human-' + Math.random().toString(36).slice(2)); 127 + mkdirSync(configHome, { recursive: true }); 128 + const result = run('vet fast-cache-invalidation --trust --confirm', undefined, { ...noAgentEnv, XDG_CONFIG_HOME: configHome }); 129 + // Should NOT contain the agent gate error 130 + expect(result.stderr).not.toContain('vit vet is for human review'); 131 + rmSync(configHome, { recursive: true, force: true }); 132 + }); 36 133 }); 37 134 });