open source is social v-it.org
at main 314 lines 12 kB view raw
1// SPDX-License-Identifier: MIT 2// Copyright (c) 2026 sol pbc 3 4import { requireDid } from '../lib/config.js'; 5import { CAP_COLLECTION, SKILL_COLLECTION, VOUCH_COLLECTION } from '../lib/constants.js'; 6import { TID } from '@atproto/common-web'; 7import { restoreAgent } from '../lib/oauth.js'; 8import { appendLog, readBeaconSet, readFollowing, readLog } from '../lib/vit-dir.js'; 9import { resolveRef, REF_PATTERN } from '../lib/cap-ref.js'; 10import { isSkillRef, isValidSkillRef, nameFromSkillRef } from '../lib/skill-ref.js'; 11import { mark, name } from '../lib/brand.js'; 12import { resolvePds, listRecordsFromPds, batchQuery } from '../lib/pds.js'; 13import { loadConfig } from '../lib/config.js'; 14import { jsonOk, jsonError } from '../lib/json-output.js'; 15 16export default function register(program) { 17 program 18 .command('vouch') 19 .argument('<ref>', 'Cap or skill reference (e.g. fast-cache-invalidation or skill-agent-test-patterns)') 20 .description('Publicly endorse a vetted cap or skill, or signal demand with --kind want') 21 .option('--did <did>', 'DID to use') 22 .option('--kind <kind>', 'Vouch intent: endorse (default, quality signal) or want (demand signal)') 23 .option('--json', 'Output as JSON') 24 .option('-v, --verbose', 'Show step-by-step details') 25 .action(async (ref, opts) => { 26 try { 27 const { verbose } = opts; 28 const vlog = opts.json ? (...a) => console.error(...a) : console.log; 29 const isSkill = isSkillRef(ref); 30 31 // Validate --kind if provided 32 if (opts.kind) { 33 const validVouchKinds = ['endorse', 'want']; 34 if (!validVouchKinds.includes(opts.kind)) { 35 if (opts.json) { 36 jsonError(`--kind must be one of: ${validVouchKinds.join(', ')}`); 37 return; 38 } 39 console.error(`error: --kind must be one of: ${validVouchKinds.join(', ')}`); 40 process.exitCode = 1; 41 return; 42 } 43 } 44 45 // Validate ref format 46 if (isSkill) { 47 if (!isValidSkillRef(ref)) { 48 if (opts.json) { 49 jsonError('invalid skill ref', 'expected format: skill-{name}'); 50 return; 51 } 52 console.error('invalid skill ref. expected format: skill-{name} (lowercase letters, numbers, hyphens)'); 53 process.exitCode = 1; 54 return; 55 } 56 } else { 57 if (!REF_PATTERN.test(ref)) { 58 if (opts.json) { 59 jsonError('invalid ref', 'expected three lowercase words with dashes'); 60 return; 61 } 62 console.error('invalid ref. expected three lowercase words with dashes (e.g. fast-cache-invalidation)'); 63 process.exitCode = 1; 64 return; 65 } 66 } 67 68 if (opts.json && !(opts.did || loadConfig().did)) { 69 jsonError('no DID configured', "run 'vit login <handle>' first"); 70 return; 71 } 72 const did = requireDid(opts); 73 if (!did) return; 74 if (verbose) vlog(`[verbose] DID: ${did}`); 75 76 if (isSkill) { 77 // Skill vouch — no beacon required, check trusted first 78 const trusted = readLog('trusted.jsonl'); 79 const trustedEntry = trusted.find(e => e.ref === ref); 80 if (!trustedEntry) { 81 if (opts.json) { 82 jsonError(`skill '${ref}' is not yet vetted`, `run 'vit vet ${ref}' first`); 83 return; 84 } 85 console.error(`skill '${ref}' is not yet vetted. vet it first:`); 86 console.error(''); 87 console.error(` vit vet ${ref}`); 88 console.error(''); 89 console.error('after reviewing, trust it with:'); 90 console.error(''); 91 console.error(` vit vet ${ref} --trust`); 92 process.exitCode = 1; 93 return; 94 } 95 if (verbose) vlog(`[verbose] trusted entry found, uri: ${trustedEntry.uri}`); 96 97 const skillName = nameFromSkillRef(ref); 98 99 const { agent } = await restoreAgent(did); 100 if (verbose) vlog('[verbose] session restored'); 101 102 const following = readFollowing(); 103 const dids = following.map(e => e.did); 104 dids.push(did); 105 106 const allRecords = await batchQuery(dids, async (repoDid) => { 107 const pds = await resolvePds(repoDid); 108 if (verbose) vlog(`[verbose] ${repoDid}: resolved PDS ${pds}`); 109 return (await listRecordsFromPds(pds, repoDid, SKILL_COLLECTION, 50)).records; 110 }, { verbose }); 111 112 let match = null; 113 for (const records of allRecords) { 114 for (const rec of records) { 115 if (rec.value.name === skillName) { 116 if (!match || (rec.value.createdAt || '') > (match.value.createdAt || '')) { 117 match = rec; 118 } 119 } 120 } 121 } 122 123 if (!match) { 124 if (opts.json) { 125 jsonError(`no skill found with ref '${ref}'`); 126 return; 127 } 128 console.error(`no skill found with ref '${ref}' from followed accounts.`); 129 console.error(''); 130 console.error('hint: skills appear from accounts you follow and your own.'); 131 console.error(` vit following check who you're following`); 132 console.error(` vit explore skills browse skills network-wide`); 133 process.exitCode = 1; 134 return; 135 } 136 137 const now = new Date().toISOString(); 138 const vouchRecord = { 139 $type: VOUCH_COLLECTION, 140 subject: { 141 uri: match.uri, 142 cid: match.cid, 143 }, 144 createdAt: now, 145 ref, 146 // No beacon for skill vouches 147 }; 148 if (verbose) vlog(`[verbose] creating vouch for ${match.uri}`); 149 const rkey = TID.nextStr(); 150 const res = await agent.com.atproto.repo.putRecord({ 151 repo: did, 152 collection: VOUCH_COLLECTION, 153 rkey, 154 record: vouchRecord, 155 validate: true, 156 }); 157 158 try { 159 appendLog('vouched.jsonl', { 160 ref, 161 uri: match.uri, 162 cid: match.cid, 163 vouchUri: res.data.uri, 164 ts: now, 165 }); 166 } catch (logErr) { 167 console.error('warning: failed to write vouched.jsonl:', logErr.message); 168 } 169 if (verbose) vlog('[verbose] logged to vouched.jsonl'); 170 171 if (opts.json) { 172 jsonOk({ ref, uri: match.uri, vouchUri: res.data.uri }); 173 return; 174 } 175 console.log(`${mark} vouched: ${ref} (${match.uri})`); 176 } else { 177 const vouchKind = opts.kind || 'endorse'; 178 const isWant = vouchKind === 'want'; 179 180 // Cap vouch — beacon required unless want-vouching (demand signal) 181 const beaconSet = readBeaconSet(); 182 183 if (!isWant) { 184 if (beaconSet.size === 0) { 185 if (opts.json) { 186 jsonError('no beacon set', "run 'vit init' first"); 187 return; 188 } 189 console.error(`no beacon set. run '${name} init' in a project directory first.`); 190 process.exitCode = 1; 191 return; 192 } 193 if (verbose) vlog(`[verbose] beacons: ${[...beaconSet].join(', ')}`); 194 195 const trusted = readLog('trusted.jsonl'); 196 const trustedEntry = trusted.find(e => e.ref === ref); 197 if (!trustedEntry) { 198 if (opts.json) { 199 jsonError(`cap '${ref}' is not yet vetted`, `run 'vit vet ${ref}' first`); 200 return; 201 } 202 console.error(`cap '${ref}' is not yet vetted. vet it first:`); 203 console.error(''); 204 console.error(` vit vet ${ref}`); 205 console.error(''); 206 console.error('after reviewing, trust it with:'); 207 console.error(''); 208 console.error(` vit vet ${ref} --trust`); 209 process.exitCode = 1; 210 return; 211 } 212 if (verbose) vlog(`[verbose] trusted entry found, uri: ${trustedEntry.uri}`); 213 } 214 215 const { agent } = await restoreAgent(did); 216 if (verbose) vlog('[verbose] session restored'); 217 218 const following = readFollowing(); 219 const dids = following.map(e => e.did); 220 dids.push(did); 221 222 const allRecords = await batchQuery(dids, async (repoDid) => { 223 const pds = await resolvePds(repoDid); 224 if (verbose) vlog(`[verbose] ${repoDid}: resolved PDS ${pds}`); 225 return (await listRecordsFromPds(pds, repoDid, CAP_COLLECTION, 50)).records; 226 }, { verbose }); 227 228 let match = null; 229 for (const records of allRecords) { 230 for (const rec of records) { 231 if (!isWant && !beaconSet.has(rec.value.beacon)) continue; 232 const recRef = resolveRef(rec.value, rec.cid); 233 if (recRef === ref) { 234 if (!match || (rec.value.createdAt || '') > (match.value.createdAt || '')) { 235 match = rec; 236 } 237 } 238 } 239 } 240 241 if (!match) { 242 if (opts.json) { 243 jsonError(`no cap found with ref '${ref}'${!isWant ? ' for this beacon' : ''}`); 244 return; 245 } 246 console.error(`no cap found with ref '${ref}'${!isWant ? ' for this beacon' : ''}.`); 247 console.error(''); 248 console.error('hint: caps only appear from accounts you follow and your own.'); 249 console.error(` vit following check who you're following`); 250 console.error(` vit explore cap ${ref} search the network-wide index`); 251 process.exitCode = 1; 252 return; 253 } 254 255 const now = new Date().toISOString(); 256 const projBeacon = beaconSet.size > 0 ? [...beaconSet][0] : (match.value.beacon || null); 257 const vouchRecord = { 258 $type: VOUCH_COLLECTION, 259 subject: { 260 uri: match.uri, 261 cid: match.cid, 262 }, 263 createdAt: now, 264 ref, 265 kind: vouchKind, 266 }; 267 if (projBeacon) vouchRecord.beacon = projBeacon; 268 if (verbose) vlog(`[verbose] creating vouch (${vouchKind}) for ${match.uri}`); 269 const rkey = TID.nextStr(); 270 const res = await agent.com.atproto.repo.putRecord({ 271 repo: did, 272 collection: VOUCH_COLLECTION, 273 rkey, 274 record: vouchRecord, 275 validate: false, 276 }); 277 278 try { 279 appendLog('vouched.jsonl', { 280 ref, 281 uri: match.uri, 282 cid: match.cid, 283 vouchUri: res.data.uri, 284 kind: vouchKind, 285 beacon: projBeacon, 286 ts: now, 287 }); 288 } catch (logErr) { 289 console.error('warning: failed to write vouched.jsonl:', logErr.message); 290 } 291 if (verbose) vlog('[verbose] logged to vouched.jsonl'); 292 293 if (opts.json) { 294 jsonOk({ ref, uri: match.uri, vouchUri: res.data.uri, kind: vouchKind }); 295 return; 296 } 297 if (isWant) { 298 console.log(`${mark} vouched (want): ${ref}`); 299 console.log(` demand signal recorded. vouch count visible in vit explore vouches.`); 300 } else { 301 console.log(`${mark} vouched: ${ref} (${match.uri})`); 302 } 303 } 304 } catch (err) { 305 const msg = err instanceof Error ? err.message : String(err); 306 if (opts.json) { 307 jsonError(msg); 308 return; 309 } 310 console.error(msg); 311 process.exitCode = 1; 312 } 313 }); 314}