@bugb-technologies/bravos 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +70 -0
- package/README.md +225 -2
- package/dist/account.js +140 -0
- package/dist/artifacts.js +450 -0
- package/dist/ci.js +52 -0
- package/dist/cli.js +963 -0
- package/dist/codegraph.js +268 -0
- package/dist/cursor.js +70 -0
- package/dist/detect.js +26 -0
- package/dist/doctor.js +99 -0
- package/dist/global.js +95 -0
- package/dist/graphbin.js +136 -0
- package/dist/guardlink.js +123 -0
- package/dist/guide.js +255 -0
- package/dist/help.js +334 -0
- package/dist/hooks.js +167 -0
- package/dist/install.js +209 -0
- package/dist/licence.js +41 -0
- package/dist/lock.js +50 -0
- package/dist/mcp.js +139 -0
- package/dist/migrate.js +275 -0
- package/dist/omp.js +46 -0
- package/dist/opencode.js +118 -0
- package/dist/paths.js +33 -0
- package/dist/plugin.js +101 -0
- package/dist/report/check.js +252 -0
- package/dist/report/data.js +401 -0
- package/dist/report/export.js +105 -0
- package/dist/report/markdown.js +310 -0
- package/dist/report/meta.js +48 -0
- package/dist/report/pdf.js +96 -0
- package/dist/review-server.js +7 -0
- package/dist/review.js +274 -0
- package/dist/setup.js +121 -0
- package/dist/skills.js +81 -0
- package/dist/types.js +9 -0
- package/dist/uninstall.js +192 -0
- package/dist/versions.js +8 -0
- package/dist/watch-frame.js +593 -0
- package/dist/watch.js +873 -0
- package/dist/workspace.js +316 -0
- package/docs/GUIDE.md +323 -0
- package/hooks/autopilot.mjs +45 -0
- package/hooks/lib/autopilot.mjs +191 -0
- package/hooks/lib/briefing.mjs +750 -0
- package/hooks/lib/licence.mjs +452 -0
- package/hooks/lib/pre-pr.mjs +209 -0
- package/hooks/lib/sast.mjs +207 -0
- package/hooks/lib/setup.mjs +107 -0
- package/hooks/lib/workspace.mjs +78 -0
- package/hooks/pre-pr.mjs +54 -0
- package/hooks/session-start.mjs +82 -0
- package/package.json +49 -4
- package/skills/bravos/SKILL.md +63 -0
- package/skills/bravos-annotate/SKILL.md +56 -0
- package/skills/bravos-annotate/scripts/lib/pick.mjs +127 -0
- package/skills/bravos-annotate/scripts/slice.mjs +141 -0
- package/skills/bravos-campaign/SKILL.md +49 -0
- package/skills/bravos-cicd/SKILL.md +48 -0
- package/skills/bravos-codegraph/SKILL.md +57 -0
- package/skills/bravos-context/SKILL.md +51 -0
- package/skills/bravos-context/intent.example.md +48 -0
- package/skills/bravos-env/SKILL.md +48 -0
- package/skills/bravos-env/scope.example.yaml +50 -0
- package/skills/bravos-prove/SKILL.md +52 -0
- package/skills/bravos-recheck/SKILL.md +47 -0
- package/skills/bravos-report/SKILL.md +53 -0
- package/skills/bravos-report/assessment.md +54 -0
- package/skills/bravos-report/brief.md +23 -0
- package/skills/bravos-report/finding.md +74 -0
- package/skills/bravos-report/retest.md +29 -0
- package/skills/bravos-report/style.md +44 -0
- package/skills/bravos-sast/SKILL.md +48 -0
- package/skills/bravos-schedule/SKILL.md +44 -0
- package/skills/bravos-schedule/loop.md +16 -0
- package/skills/bravos-ship/SKILL.md +58 -0
- package/skills/bravos-templatize/SKILL.md +48 -0
- package/skills/bravos-templatize/operator-guidance.md +20 -0
- package/templates/omp/bravos.js +62 -0
- package/templates/opencode/bravos.js +33 -0
- package/templates/report/report.html +88 -0
- package/templates/review/review.html +327 -0
- package/templates/workflows/bravos-nightly-claude.yml +173 -0
- package/templates/workflows/bravos-nightly-codex.yml +172 -0
- package/templates/workflows/bravos-pr-gate.yml +142 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
4
|
+
import { whichBin } from './guardlink.js';
|
|
5
|
+
import { BravosError } from './types.js';
|
|
6
|
+
// @comment -- "bravos workspace: one application made of several repositories. Membership (name, path, service, URL per member) lives in $BRAVOS_HOME/workspaces.json, this CLI's own registry; guardlink link-project writes each member's .guardlink/workspace.yaml; the merged view (per-member counts, the calls that cross services, references nothing defines, ids two members define) is computed from each member's own guardlink parse, so no other CLI is needed. A machine-local snapshot under .bravos/workspace/ in each member and in the app folder is what hooks, watch and reports read. argv spawns only, no shell."
|
|
7
|
+
export const WORKSPACE_SCHEMA = 'bravos.workspace/v1';
|
|
8
|
+
export const EXPORT_SCHEMA = 'bravos.workspace-export/v1';
|
|
9
|
+
export const WORKSPACE_DIR = join('.bravos', 'workspace');
|
|
10
|
+
/** `PATH[=service[@url]]`, the form bravos takes: `PATH=@URL` gives a URL and no service, a bare `PATH` neither. */
|
|
11
|
+
export function parseMemberSpec(spec, cwd = process.cwd()) {
|
|
12
|
+
const eq = spec.indexOf('=');
|
|
13
|
+
const rawPath = eq < 0 ? spec : spec.slice(0, eq);
|
|
14
|
+
const rest = eq < 0 ? '' : spec.slice(eq + 1);
|
|
15
|
+
const at = rest.indexOf('@');
|
|
16
|
+
const service = at < 0 ? rest : rest.slice(0, at);
|
|
17
|
+
const url = at < 0 ? '' : rest.slice(at + 1);
|
|
18
|
+
if (!rawPath)
|
|
19
|
+
throw new BravosError(`member "${spec}" has no path`);
|
|
20
|
+
if (url && !/^https?:\/\/[^\s/]+/.test(url))
|
|
21
|
+
throw new BravosError(`member "${spec}": the URL must start with http:// or https://`);
|
|
22
|
+
return { path: resolve(cwd, rawPath), service, url };
|
|
23
|
+
}
|
|
24
|
+
// @comment -- "Refuses a member path that does not exist, is not a directory, or is not a git repository (bravos requires .git; a worktree's .git file counts)."
|
|
25
|
+
export function checkMemberPath(path) {
|
|
26
|
+
if (!existsSync(path) || !statSync(path).isDirectory())
|
|
27
|
+
throw new BravosError(`${path} is not a folder`);
|
|
28
|
+
if (!existsSync(join(path, '.git')))
|
|
29
|
+
throw new BravosError(`${path} is not a git repository (bravos workspaces need one per member)`);
|
|
30
|
+
}
|
|
31
|
+
/** The app folder: the one parent every member sits directly under, else null. An explicit folder wins. */
|
|
32
|
+
export function appFolderFor(paths, explicit) {
|
|
33
|
+
if (explicit)
|
|
34
|
+
return resolve(explicit);
|
|
35
|
+
const parents = [...new Set(paths.map((p) => dirname(resolve(p))))];
|
|
36
|
+
return paths.length > 1 && parents.length === 1 ? parents[0] : null;
|
|
37
|
+
}
|
|
38
|
+
const run = (cmd, args, cwd, env, timeout = 120000) => spawnSync(cmd, args, { cwd, env, encoding: 'utf8', timeout, maxBuffer: 64 * 1024 * 1024, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
39
|
+
const firstLine = (s) => (s ?? '').split('\n').map((l) => l.trim()).find((l) => l && !l.startsWith('note:')) ?? '';
|
|
40
|
+
// ---------------------------------------------------------------------------------------------
|
|
41
|
+
// The registry: $BRAVOS_HOME/workspaces.json
|
|
42
|
+
// ---------------------------------------------------------------------------------------------
|
|
43
|
+
export const REGISTRY_SCHEMA = 'bravos.workspaces/v1';
|
|
44
|
+
const NAME_RE = /^[a-z0-9][a-z0-9._-]{0,63}$/;
|
|
45
|
+
export const registryFile = (env) => {
|
|
46
|
+
const home = env.BRAVOS_HOME || (env.HOME ? join(env.HOME, '.bravos') : null);
|
|
47
|
+
if (!home)
|
|
48
|
+
throw new BravosError('no home directory: set BRAVOS_HOME to where bravos keeps its workspaces');
|
|
49
|
+
return join(home, 'workspaces.json');
|
|
50
|
+
};
|
|
51
|
+
export function readRegistry(env) {
|
|
52
|
+
const file = registryFile(env);
|
|
53
|
+
if (!existsSync(file))
|
|
54
|
+
return { schema: REGISTRY_SCHEMA, workspaces: {} };
|
|
55
|
+
let doc;
|
|
56
|
+
try {
|
|
57
|
+
doc = JSON.parse(readFileSync(file, 'utf8'));
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
throw new BravosError(`${file} is not valid JSON; fix it or delete it, nothing was written`);
|
|
61
|
+
}
|
|
62
|
+
if (doc?.schema !== REGISTRY_SCHEMA || typeof doc.workspaces !== 'object' || doc.workspaces === null)
|
|
63
|
+
throw new BravosError(`${file} is not a ${REGISTRY_SCHEMA} document; fix it or delete it`);
|
|
64
|
+
const workspaces = {};
|
|
65
|
+
for (const [k, v] of Object.entries(doc.workspaces))
|
|
66
|
+
workspaces[k] = { name: String(v?.name ?? k), createdAt: String(v?.createdAt ?? ''), members: membersOf(v) };
|
|
67
|
+
return { schema: REGISTRY_SCHEMA, workspaces };
|
|
68
|
+
}
|
|
69
|
+
// @comment -- "Writes the registry atomically (temp file, rename) with mode 0600: it holds machine-local paths and service URLs, nothing a teammate needs and nothing to share."
|
|
70
|
+
export function writeRegistry(env, reg) {
|
|
71
|
+
const file = registryFile(env);
|
|
72
|
+
mkdirSync(dirname(file), { recursive: true, mode: 0o700 });
|
|
73
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
74
|
+
writeFileSync(tmp, JSON.stringify(reg, null, 2) + '\n', { mode: 0o600 });
|
|
75
|
+
renameSync(tmp, file);
|
|
76
|
+
}
|
|
77
|
+
export function getWorkspace(env, name) {
|
|
78
|
+
const ws = readRegistry(env).workspaces[name];
|
|
79
|
+
if (!ws)
|
|
80
|
+
throw new BravosError(`no workspace ${JSON.stringify(name)}: bravos workspace create ${name} --member PATH ... makes one${Object.keys(readRegistry(env).workspaces).length ? ` (known: ${Object.keys(readRegistry(env).workspaces).join(', ')})` : ''}`);
|
|
81
|
+
return ws;
|
|
82
|
+
}
|
|
83
|
+
const uniqueName = (base, taken) => {
|
|
84
|
+
let n = base;
|
|
85
|
+
for (let i = 2; taken.has(n); i++)
|
|
86
|
+
n = `${base}-${i}`;
|
|
87
|
+
return n;
|
|
88
|
+
};
|
|
89
|
+
const toMember = (spec, taken) => {
|
|
90
|
+
const name = uniqueName(memberName(spec.path), taken);
|
|
91
|
+
taken.add(name);
|
|
92
|
+
return { name, path: resolve(spec.path), service: spec.service || name, url: spec.url };
|
|
93
|
+
};
|
|
94
|
+
export function createWorkspace(env, name, specs) {
|
|
95
|
+
if (!NAME_RE.test(name))
|
|
96
|
+
throw new BravosError(`a workspace name is lowercase letters, digits, dots, dashes and underscores, got ${JSON.stringify(name)}`);
|
|
97
|
+
const reg = readRegistry(env);
|
|
98
|
+
if (reg.workspaces[name])
|
|
99
|
+
throw new BravosError(`workspace ${name} already exists: bravos workspace add/remove change it, bravos workspace show ${name} lists it`);
|
|
100
|
+
const taken = new Set();
|
|
101
|
+
const members = specs.map((s) => toMember(s, taken));
|
|
102
|
+
const paths = new Set();
|
|
103
|
+
for (const m of members) {
|
|
104
|
+
if (paths.has(m.path))
|
|
105
|
+
throw new BravosError(`${m.path} is given twice`);
|
|
106
|
+
paths.add(m.path);
|
|
107
|
+
}
|
|
108
|
+
reg.workspaces[name] = { name, createdAt: new Date().toISOString(), members };
|
|
109
|
+
writeRegistry(env, reg);
|
|
110
|
+
return members;
|
|
111
|
+
}
|
|
112
|
+
export function addMember(env, name, spec) {
|
|
113
|
+
const reg = readRegistry(env);
|
|
114
|
+
const ws = reg.workspaces[name];
|
|
115
|
+
if (!ws)
|
|
116
|
+
throw new BravosError(`no workspace ${JSON.stringify(name)}`);
|
|
117
|
+
if (ws.members.some((m) => m.path === resolve(spec.path)))
|
|
118
|
+
throw new BravosError(`${spec.path} is already a member of ${name}`);
|
|
119
|
+
const m = toMember(spec, new Set(ws.members.map((x) => x.name)));
|
|
120
|
+
ws.members.push(m);
|
|
121
|
+
writeRegistry(env, reg);
|
|
122
|
+
return m;
|
|
123
|
+
}
|
|
124
|
+
export function removeMember(env, name, member, cwd = process.cwd()) {
|
|
125
|
+
const reg = readRegistry(env);
|
|
126
|
+
const ws = reg.workspaces[name];
|
|
127
|
+
if (!ws)
|
|
128
|
+
throw new BravosError(`no workspace ${JSON.stringify(name)}`);
|
|
129
|
+
const i = ws.members.findIndex((m) => m.name === member || m.service === member || m.path === resolve(cwd, member));
|
|
130
|
+
if (i < 0)
|
|
131
|
+
throw new BravosError(`${member} is not a member of ${name} (members: ${ws.members.map((m) => m.name).join(', ')})`);
|
|
132
|
+
const [gone] = ws.members.splice(i, 1);
|
|
133
|
+
writeRegistry(env, reg);
|
|
134
|
+
return gone;
|
|
135
|
+
}
|
|
136
|
+
export function setTarget(env, name, service, url) {
|
|
137
|
+
if (!/^https?:\/\//.test(url))
|
|
138
|
+
throw new BravosError(`a target URL starts with http:// or https://, got ${JSON.stringify(url)}`);
|
|
139
|
+
const reg = readRegistry(env);
|
|
140
|
+
const ws = reg.workspaces[name];
|
|
141
|
+
if (!ws)
|
|
142
|
+
throw new BravosError(`no workspace ${JSON.stringify(name)}`);
|
|
143
|
+
const m = ws.members.find((x) => x.service === service || x.name === service);
|
|
144
|
+
if (!m)
|
|
145
|
+
throw new BravosError(`${service} is not a service of ${name} (services: ${ws.members.map((x) => x.service).join(', ')})`);
|
|
146
|
+
m.url = url;
|
|
147
|
+
writeRegistry(env, reg);
|
|
148
|
+
return m;
|
|
149
|
+
}
|
|
150
|
+
// @comment -- "Runs guardlink link-project (fresh link, --add or --remove) so each member's .guardlink/workspace.yaml and agent files name the workspace and its siblings; returns guardlink's own output, or a note when guardlink is missing or there is only one member."
|
|
151
|
+
export function linkProject(args, env, cwd = process.cwd()) {
|
|
152
|
+
if (!whichBin('guardlink', env))
|
|
153
|
+
return 'guardlink not on PATH: member workspace.yaml files not written';
|
|
154
|
+
const r = run('guardlink', ['link-project', ...args], cwd, env);
|
|
155
|
+
if (r.status !== 0)
|
|
156
|
+
throw new BravosError(`guardlink link-project failed: ${firstLine(r.stderr) || firstLine(r.stdout) || `exit ${r.status}`}`);
|
|
157
|
+
return firstLine(r.stdout) || 'guardlink link-project done';
|
|
158
|
+
}
|
|
159
|
+
const membersOf = (doc) => (Array.isArray(doc?.members) ? doc.members : []).map((m) => ({ name: String(m.name), path: String(m.path), service: String(m.service ?? m.service_name ?? ''), url: String(m.url ?? '') }));
|
|
160
|
+
/** Ids the coding-agent configuration generates in every repo (guardlink's agent block); the same id in two members is expected, not a clash. */
|
|
161
|
+
const SHARED_ACTOR = /^(claude-code|codex|cursor|copilot|gemini|opencode|omp|antigravity|coding-agent)(-|$)/;
|
|
162
|
+
// @comment -- "One member's model as guardlink parse prints it: the ids it defines (assets, threats, controls, actors — by kind), its flows, its exposure counts, and HEAD. A member whose parse fails reads as an error, never as empty."
|
|
163
|
+
export function readMemberModel(path, env) {
|
|
164
|
+
const out = { ids: new Map(), flows: [], exposures: 0, bySeverity: {}, head: null, error: null };
|
|
165
|
+
const head = spawnSync('git', ['rev-parse', 'HEAD'], { cwd: path, env, encoding: 'utf8', timeout: 5000 });
|
|
166
|
+
out.head = head.status === 0 ? head.stdout.trim() : null;
|
|
167
|
+
if (!whichBin('guardlink', env)) {
|
|
168
|
+
out.error = 'guardlink not on PATH';
|
|
169
|
+
return out;
|
|
170
|
+
}
|
|
171
|
+
const r = run('guardlink', ['parse', '.', '--no-pretty'], path, env);
|
|
172
|
+
if (r.status !== 0) {
|
|
173
|
+
out.error = `guardlink parse failed: ${firstLine(r.stderr) || firstLine(r.stdout) || `exit ${r.status}`}`;
|
|
174
|
+
return out;
|
|
175
|
+
}
|
|
176
|
+
let doc;
|
|
177
|
+
try {
|
|
178
|
+
doc = JSON.parse(r.stdout);
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
out.error = 'guardlink parse printed no JSON';
|
|
182
|
+
return out;
|
|
183
|
+
}
|
|
184
|
+
for (const kind of ['assets', 'threats', 'controls', 'actors'])
|
|
185
|
+
for (const d of Array.isArray(doc?.[kind]) ? doc[kind] : [])
|
|
186
|
+
if (typeof d?.id === 'string')
|
|
187
|
+
out.ids.set(d.id, kind);
|
|
188
|
+
for (const f of Array.isArray(doc?.flows) ? doc.flows : [])
|
|
189
|
+
out.flows.push({ source: String(f?.source ?? ''), target: String(f?.target ?? ''), mechanism: String(f?.mechanism ?? ''), file: `${f?.location?.file ?? '?'}${f?.location?.line ? `:${f.location.line}` : ''}` });
|
|
190
|
+
for (const e of Array.isArray(doc?.exposures) ? doc.exposures : []) {
|
|
191
|
+
out.exposures++;
|
|
192
|
+
const sev = String(e?.severity ?? 'unset');
|
|
193
|
+
out.bySeverity[sev] = (out.bySeverity[sev] ?? 0) + 1;
|
|
194
|
+
}
|
|
195
|
+
return out;
|
|
196
|
+
}
|
|
197
|
+
// @comment -- "The merged view, from the members' own models: an edge is a flow whose #id end is defined in another member (de-duplicated by src, dst and mechanism); an unresolved reference is a #id no member defines; a clash is an id two members define, except the coding-agent actors every repo generates. Per member: exposure counts, HEAD, and any read error."
|
|
198
|
+
export function computeExport(name, members, models, at = new Date().toISOString()) {
|
|
199
|
+
const owners = new Map();
|
|
200
|
+
for (const m of members)
|
|
201
|
+
for (const id of models[m.name]?.ids.keys() ?? [])
|
|
202
|
+
owners.set(id, [...(owners.get(id) ?? []), m.name]);
|
|
203
|
+
const edges = [];
|
|
204
|
+
const seen = new Set();
|
|
205
|
+
const unresolved = [];
|
|
206
|
+
const ref = (s) => (s.startsWith('#') ? s.slice(1) : null);
|
|
207
|
+
for (const m of members) {
|
|
208
|
+
for (const f of models[m.name]?.flows ?? []) {
|
|
209
|
+
const srcId = ref(f.source);
|
|
210
|
+
const dstId = ref(f.target);
|
|
211
|
+
const srcOwner = srcId ? (models[m.name]?.ids.has(srcId) ? m.name : owners.get(srcId)?.[0] ?? null) : m.name;
|
|
212
|
+
const dstOwner = dstId ? (models[m.name]?.ids.has(dstId) ? m.name : owners.get(dstId)?.[0] ?? null) : m.name;
|
|
213
|
+
if (srcId && !srcOwner)
|
|
214
|
+
unresolved.push({ member: m.name, reference: srcId, flow: 'src' });
|
|
215
|
+
if (dstId && !dstOwner)
|
|
216
|
+
unresolved.push({ member: m.name, reference: dstId, flow: 'dst' });
|
|
217
|
+
if (!srcOwner || !dstOwner || srcOwner === dstOwner)
|
|
218
|
+
continue;
|
|
219
|
+
const key = `${srcId ?? f.source}|${dstId ?? f.target}|${f.mechanism}`;
|
|
220
|
+
if (seen.has(key))
|
|
221
|
+
continue;
|
|
222
|
+
seen.add(key);
|
|
223
|
+
edges.push({ src: srcId ?? f.source, dst: dstId ?? f.target, srcMember: srcOwner, dstMember: dstOwner, mechanism: f.mechanism, declaredIn: `${m.name}/${f.file}` });
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const clashes = [];
|
|
227
|
+
for (const [id, ms] of owners) {
|
|
228
|
+
if (ms.length < 2)
|
|
229
|
+
continue;
|
|
230
|
+
const kind = models[ms[0]]?.ids.get(id) ?? '';
|
|
231
|
+
if (kind === 'actors' && SHARED_ACTOR.test(id))
|
|
232
|
+
continue;
|
|
233
|
+
clashes.push({ kind, id, members: ms });
|
|
234
|
+
}
|
|
235
|
+
const out = {};
|
|
236
|
+
for (const m of members) {
|
|
237
|
+
const md = models[m.name];
|
|
238
|
+
out[m.name] = { exposures: md?.exposures ?? 0, bySeverity: md?.bySeverity ?? {}, head: md?.head ?? null, ...(md?.error ? { error: md.error } : {}) };
|
|
239
|
+
}
|
|
240
|
+
return { schema: EXPORT_SCHEMA, name, at, members: out, edges, unresolved: unresolved.filter((u, i, a) => a.findIndex((x) => x.member === u.member && x.reference === u.reference) === i), clashes: clashes.sort((a, b) => a.id.localeCompare(b.id)) };
|
|
241
|
+
}
|
|
242
|
+
const dirFor = (root) => join(root, WORKSPACE_DIR);
|
|
243
|
+
// @comment -- "Writes the snapshot (and the export cache when given) into root/.bravos/workspace/, creating its .gitignore (*) first: paths are machine-local and never committed."
|
|
244
|
+
export function writeSnapshot(root, snap, exp) {
|
|
245
|
+
const dir = dirFor(root);
|
|
246
|
+
mkdirSync(dir, { recursive: true });
|
|
247
|
+
if (!existsSync(join(dir, '.gitignore')))
|
|
248
|
+
writeFileSync(join(dir, '.gitignore'), '*\n');
|
|
249
|
+
writeFileSync(join(dir, 'workspace.json'), JSON.stringify(snap, null, 2) + '\n');
|
|
250
|
+
if (exp)
|
|
251
|
+
writeFileSync(join(dir, 'export.json'), JSON.stringify(exp, null, 2) + '\n');
|
|
252
|
+
}
|
|
253
|
+
export function readSnapshot(root) {
|
|
254
|
+
try {
|
|
255
|
+
const s = JSON.parse(readFileSync(join(dirFor(root), 'workspace.json'), 'utf8'));
|
|
256
|
+
return s?.schema === WORKSPACE_SCHEMA && Array.isArray(s.members) ? s : null;
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
export function readExport(root) {
|
|
263
|
+
try {
|
|
264
|
+
const s = JSON.parse(readFileSync(join(dirFor(root), 'export.json'), 'utf8'));
|
|
265
|
+
return s?.schema === EXPORT_SCHEMA ? s : null;
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
/** Every place a snapshot belongs: each member, and the app folder when there is one. */
|
|
272
|
+
export const snapshotRoots = (snap) => [...snap.members.map((m) => m.path), ...(snap.appFolder ? [snap.appFolder] : [])];
|
|
273
|
+
// @comment -- "Reads the workspace from the registry, computes the merged export from each member's own model (a member that fails to parse is reported, not dropped), and writes the snapshot and cache to every member and the app folder. Returns both."
|
|
274
|
+
export function refresh(name, env, appFolder) {
|
|
275
|
+
const ws = getWorkspace(env, name);
|
|
276
|
+
const members = ws.members;
|
|
277
|
+
const models = {};
|
|
278
|
+
for (const m of members)
|
|
279
|
+
models[m.name] = existsSync(m.path) ? readMemberModel(m.path, env) : { ids: new Map(), flows: [], exposures: 0, bySeverity: {}, head: null, error: 'path is gone' };
|
|
280
|
+
const exp = computeExport(name, members, models);
|
|
281
|
+
const errors = members.filter((m) => models[m.name].error).map((m) => `${m.name}: ${models[m.name].error}`);
|
|
282
|
+
const snap = { schema: WORKSPACE_SCHEMA, name, appFolder, members, at: new Date().toISOString() };
|
|
283
|
+
for (const root of snapshotRoots(snap))
|
|
284
|
+
if (existsSync(root))
|
|
285
|
+
writeSnapshot(root, snap, exp);
|
|
286
|
+
return { snap, exp, exportError: errors.length ? errors.join('; ') : null };
|
|
287
|
+
}
|
|
288
|
+
/** Lines describing a workspace for `bravos workspace show`. */
|
|
289
|
+
export function describe(snap, exp) {
|
|
290
|
+
const lines = [`Workspace ${snap.name}: ${snap.members.length} service${snap.members.length === 1 ? '' : 's'}${snap.appFolder ? ` · app folder ${snap.appFolder}` : ' · no app folder'}`];
|
|
291
|
+
for (const m of snap.members) {
|
|
292
|
+
const e = exp?.members[m.name];
|
|
293
|
+
const sev = e ? Object.entries(e.bySeverity).filter(([, n]) => n).map(([k, n]) => `${n} ${k}`).join(', ') : '';
|
|
294
|
+
const state = [existsSync(join(m.path, '.guardlink', 'workspace.yaml')) ? 'linked' : 'not linked', existsSync(join(m.path, '.bravos')) ? 'bravos' : 'no bravos'].join(', ');
|
|
295
|
+
lines.push(` ${m.name.padEnd(14)} ${(m.service || '-').padEnd(12)} ${(m.url || 'no URL').padEnd(26)} ${e ? `${e.exposures} exposures${sev ? ` (${sev})` : ''}` : 'exposures ?'} [${state}]`);
|
|
296
|
+
lines.push(` ${''.padEnd(14)} ${m.path}`);
|
|
297
|
+
}
|
|
298
|
+
if (!exp)
|
|
299
|
+
lines.push('Cross-service edges: unknown (bravos workspace export failed or not run)');
|
|
300
|
+
else {
|
|
301
|
+
lines.push(`Cross-service edges: ${exp.edges.length}`);
|
|
302
|
+
for (const e of exp.edges)
|
|
303
|
+
lines.push(` ${e.srcMember}:${e.src} -> ${e.dstMember}:${e.dst}${e.mechanism ? ` via ${e.mechanism}` : ''} (${e.declaredIn})`);
|
|
304
|
+
if (exp.unresolved.length)
|
|
305
|
+
lines.push(`Unresolved references: ${exp.unresolved.length}`, ...exp.unresolved.slice(0, 10).map((u) => ` ${u.member}: ${u.reference}`));
|
|
306
|
+
if (exp.clashes.length)
|
|
307
|
+
lines.push(`Clashes (an id defined differently in two services): ${exp.clashes.length}`, ...exp.clashes.slice(0, 10).map((c) => ` ${c.id}: ${c.members.join(', ')}`));
|
|
308
|
+
}
|
|
309
|
+
const missingUrl = snap.members.filter((m) => !m.url).map((m) => m.name);
|
|
310
|
+
if (missingUrl.length)
|
|
311
|
+
lines.push(`No URL for: ${missingUrl.join(', ')} → bravos workspace target ${snap.name} <service>=http://127.0.0.1:<port>`);
|
|
312
|
+
return lines;
|
|
313
|
+
}
|
|
314
|
+
/** The member a path belongs to, by path. */
|
|
315
|
+
export const memberAt = (snap, root) => snap.members.find((m) => resolve(m.path) === resolve(root)) ?? null;
|
|
316
|
+
export const memberName = (path) => basename(resolve(path));
|
package/docs/GUIDE.md
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
# bravos guide
|
|
2
|
+
|
|
3
|
+
_Generated from `src/guide.ts` by `bravos guide --markdown`; edit the source, not this file._
|
|
4
|
+
|
|
5
|
+
Skills are written in Claude Code syntax (`/bravos-ship`); in Codex use `$bravos-ship`, in omp `/skill:bravos-ship`.
|
|
6
|
+
|
|
7
|
+
## What bravos is and how a session starts
|
|
8
|
+
|
|
9
|
+
bravos is a skills pack for coding agents. It makes the agent in your session a power user of guardlink (the threat model, written as annotations beside the code), cxg (pentests as code, against targets you own), the code graph and licensed SAST. There is no bravos server and no bravos database: the threat model lives in your repo, the evidence comes from the real tools, and your team shares it through git.
|
|
10
|
+
|
|
11
|
+
Every claim moves through one loop: an annotation says code is exposed (or mitigated) → it becomes a cxg template → an authorized run tests it against a target you own → the result is recorded in guardlink → it is rechecked when the code changes.
|
|
12
|
+
|
|
13
|
+
### At session start
|
|
14
|
+
|
|
15
|
+
A hook gathers the repo's security picture (open exposures, confirmed exploits, what to test next, stale claims, tickets, teammates' open PRs, intent) and hands it to the model, so you never need a command to load it. On a fresh start you also see the banner; the model does not.
|
|
16
|
+
|
|
17
|
+
Claude Code shows the banner above the first prompt. Codex lists a two-line summary as a hook warning (F2 to view). omp shows the banner above the editor until your first prompt. opencode gets the briefing for the model only. Antigravity (agy) gets the briefing for the model only. Anywhere else, the bravos skill loads the briefing when you run it.
|
|
18
|
+
|
|
19
|
+
### Running a skill
|
|
20
|
+
|
|
21
|
+
Claude Code and opencode: /bravos-ship. Codex: $bravos-ship. omp: /skill:bravos-ship. Elsewhere, ask for it by name ("use the bravos-ship skill"). You can also just say what you want ("is this PR safe to open?") and the agent picks the skill.
|
|
22
|
+
|
|
23
|
+
### Plans
|
|
24
|
+
|
|
25
|
+
Free (researcher) needs no licence: context, annotate, templatize, env, prove, recheck, report and watch, in one repository. Team, licensed per developer (Solo is one seat), adds the pre-PR gate and autopilot, the CI and nightly workflows, scheduled upkeep, multi-repository workspaces, campaigns, SAST and the code graph. The briefing opens every session with a Plan: line; bravos licence shows the same in a shell. bravos account activate signs you in once in the browser and writes the licence (a 30-day Team trial, no card, when the account has no plan); bravos account import <file> installs a downloaded one. Without a licence a paid command explains what it would do and how to start the trial, a paid hook stands down with one line and never blocks, and a paid skill stops at its first step and says so.
|
|
26
|
+
|
|
27
|
+
### Effort
|
|
28
|
+
|
|
29
|
+
Every skill reads one dial: low, medium (default), high or max. Say it ("annotate at high") or put a line `bravos effort: high` in AGENTS.md or CLAUDE.md. These are starting budgets, not limits: the agent goes further when the work needs it and says why. Only the safety rules are hard.
|
|
30
|
+
|
|
31
|
+
### Installing
|
|
32
|
+
|
|
33
|
+
npm install -g @bugb-technologies/bravos (Node 20 or newer). Then bravos account activate signs you in once in the browser and writes the licence — a 30-day Team trial when the account has no plan; skip it to stay on Free — bravos init in a repository writes the skills, hooks and MCP wiring, and bravos setup puts the analyzer and the code graph on the machine (Team plan). guardlink and cxg come from bravos setup --tools -y, or npm and cargo by hand. If you used the Python bravos, rename its command so bravos means this one: mv ~/.local/bin/bravos ~/.local/bin/bravos-py (this CLI still finds its code graph and analyzer), or remove it with pipx or uv.
|
|
34
|
+
|
|
35
|
+
### Your first day
|
|
36
|
+
|
|
37
|
+
Run bravos next in a shell (free): it shows which of the nine steps are done here, in the order they build on each other — the code graph first (a machine step every later skill reads), then what the app intends, annotation coverage, SAST (which reads the annotations), how findings are proven and a test target, then the gates (autopilot, the pre-PR gate, CI, cheap to switch on any time) — what each unlocks, and the exact command for the next one. The banner shows the next step too, and every harness gives it to the agent, which offers a few starts the first time and suggests the next step at natural moments.
|
|
38
|
+
|
|
39
|
+
Or just ask the agent "help me get started" (or run /bravos, $bravos in Codex): it gives a short tour fitted to this repo. BRAVOS_COACH=off stops the coaching; the Setup line stays.
|
|
40
|
+
|
|
41
|
+
Keep bravos watch running in a second terminal pane beside your agent (free, read-only, no model): the setup score, open and confirmed exposures, what to test next, stale claims and autopilot nudges, redrawn within seconds of a change. It also shows the hypothesis ledger, boundaries, the model's size, the code graph's call resolution and entry points, and the branch's commits. Keys: n copies the next command, r opens the intent review, d guardlink's dashboard, g asks the code graph, t shows test-next, i shows the recorded intent, u refreshes now, j and k scroll, q quits. Three columns from 150 wide, two from 110, one in an 80-column split.
|
|
42
|
+
|
|
43
|
+
### Hands-off: autopilot
|
|
44
|
+
|
|
45
|
+
bravos init --autopilot (or sync --autopilot) makes upkeep happen without anyone asking. At the end of each turn a check sends the agent back once if the changes left code with no annotation, touched code under a confirmed or stale claim, or put open exposures on changed files. The pre-PR gate runs on every gh pr create.
|
|
46
|
+
|
|
47
|
+
Proving follows your choice, not a fixed rule: the first time it comes up the agent asks you once — prove automatically on your loopback target, ask each time, on a schedule, or not now — and records your words in .guardlink/proving.md. Say "ask me first" or "stop auto-proving" at any time to change it. Non-loopback targets still need a reviewed scope and your typed authorization every time.
|
|
48
|
+
|
|
49
|
+
### Free views (no tokens)
|
|
50
|
+
|
|
51
|
+
In a shell: `bravos guide` (this guide), `bravos doctor` (which tools are installed), `guardlink status .`, `guardlink ci -f json`, `guardlink hypothesis next`. None of these runs a model.
|
|
52
|
+
|
|
53
|
+
## What to do, by role
|
|
54
|
+
|
|
55
|
+
### Developer
|
|
56
|
+
|
|
57
|
+
Skills: `/bravos` · `/bravos-annotate` · `/bravos-ship`
|
|
58
|
+
|
|
59
|
+
Work as usual; the briefing tells the agent what is open around your code. When you write or change an endpoint, an auth check, a query, file or process handling, crypto or config parsing, ask the agent to annotate it (bravos-annotate) so the threat model keeps up. Before you open a pull request, run bravos-ship: it validates the model, lints your new claims, lists new exposure the PR must disclose, and refuses to ship a confirmed exploit on a changed path. After you fix a finding, run bravos-recheck.
|
|
60
|
+
|
|
61
|
+
### Security engineer
|
|
62
|
+
|
|
63
|
+
Skills: `/bravos-context` · `/bravos-templatize` · `/bravos-env` · `/bravos-prove` · `/bravos-recheck`
|
|
64
|
+
|
|
65
|
+
Start by recording what the product intends (bravos-context), so severities reflect impact on this product, not code patterns. Then turn the top untested exposures into cxg templates (bravos-templatize), bring up a target you own with a reviewed scope and your typed authorization (bravos-env), and run the test (bravos-prove). Outcomes go back into guardlink's hypothesis ledger as confirmed or refuted, with the evidence. After a fix, bravos-recheck replays the retest kit. bravos-sast adds licensed static analysis joined to the threat model.
|
|
66
|
+
|
|
67
|
+
### Team lead or reviewer
|
|
68
|
+
|
|
69
|
+
Skills: `/bravos-campaign` · `/bravos-schedule` · `/bravos-cicd`
|
|
70
|
+
|
|
71
|
+
Turn on bravos init --autopilot so the threat model keeps up as people work, with no commands to remember, and settle once with the team how findings should be proven (it is recorded in .guardlink/proving.md and shows in every briefing).
|
|
72
|
+
|
|
73
|
+
Coverage and findings are visible to everyone from the repo itself. To raise coverage or clear the backlog, run bravos-campaign with a goal ("coverage 100%", "test every open exposure"): it plans disjoint batches, runs one subagent per batch, gates each, rests, and reports before-and-after numbers. Use bravos-schedule for recurring upkeep (an in-session loop or the nightly workflow) and bravos-cicd for the PR gate and code-scanning upload. Acceptances (@accepts) and entitlements are yours to write; agents never write them.
|
|
74
|
+
|
|
75
|
+
### New to the repo
|
|
76
|
+
|
|
77
|
+
Skills: `/bravos` · `/bravos-context` · `/bravos-codegraph`
|
|
78
|
+
|
|
79
|
+
Run bravos first: it reads the briefing, explains the loop in five lines and routes you. If the banner says "intent none", run bravos-context: a short interview about purpose, users and roles, the data that matters, what must never happen, and what looks dangerous but is intended. bravos-codegraph answers "where are the routes", "what does this handler reach", "is this code live" without reading the whole tree.
|
|
80
|
+
|
|
81
|
+
### CI owner
|
|
82
|
+
|
|
83
|
+
Skills: `/bravos-cicd` · `/bravos-schedule`
|
|
84
|
+
|
|
85
|
+
bravos init --ci writes the PR gate and nightly workflows. bravos-cicd audits existing workflows: broken guardlink calls, script injection from pull-request text, missing SARIF upload. The nightly can replay retest kits only against a target the committed, reviewed scope authorizes.
|
|
86
|
+
|
|
87
|
+
## What to do, by task
|
|
88
|
+
|
|
89
|
+
### Where do I start? What is open?
|
|
90
|
+
|
|
91
|
+
Skills: `/bravos`
|
|
92
|
+
|
|
93
|
+
Reads the briefing and names the one next step, with the reason.
|
|
94
|
+
|
|
95
|
+
### Teach bravos what the app is for
|
|
96
|
+
|
|
97
|
+
Skills: `/bravos-context`
|
|
98
|
+
|
|
99
|
+
Writes .guardlink/intent.md: invariants (INV-n) that become threats, and intended-by-design items (INT-n) that get an "intended?" audit instead of a high rating.
|
|
100
|
+
|
|
101
|
+
### Threat-model someone else's project
|
|
102
|
+
|
|
103
|
+
Skills: `/bravos-context` · `/bravos-annotate`
|
|
104
|
+
|
|
105
|
+
bravos-context assumes you are researching software you do not own (an open-source project, a vendor's code): it drafts the intent from upstream's own docs with links and asks only the scope decisions — which configuration to rate against (shipped defaults or the hardened setup upstream recommends), single organisation or multi-tenant, which attackers are in scope, and the intended-looking behaviours upstream does not document. intent.md records them as Assumptions, and severities are rated against them. Say "this is our product" for the owner interview instead.
|
|
106
|
+
|
|
107
|
+
### Answer bravos's questions as a team
|
|
108
|
+
|
|
109
|
+
Skills: `/bravos-context`
|
|
110
|
+
|
|
111
|
+
bravos-context opens its questions as a bravos page (bravos review intent): pre-filled with bravos's suggestions and their sources; change what is wrong, make the calls marked 'needs your call' (Confirm stays off until you do), and the facts bravos drafted stay folded unless you open one to object. Each person's answers save to .guardlink/intent-answers/<name>.json; teammates run bravos review intent after pulling and commit theirs. bravos review intent --summary shows where people agree and disagree; only what a named person agreed to is marked confirmed. On Claude Code, --share puts the same page on a private claude.ai link instead.
|
|
112
|
+
|
|
113
|
+
### I wrote security-relevant code
|
|
114
|
+
|
|
115
|
+
Skills: `/bravos-annotate`
|
|
116
|
+
|
|
117
|
+
Annotates what you changed to guardlink's evidence bar and runs the lint gate until clean.
|
|
118
|
+
|
|
119
|
+
### Cover the whole repo
|
|
120
|
+
|
|
121
|
+
Skills: `/bravos-campaign`
|
|
122
|
+
|
|
123
|
+
Rounds of batches with subagents and rests, until the coverage goal or until you stop it.
|
|
124
|
+
|
|
125
|
+
### Is this exposure real?
|
|
126
|
+
|
|
127
|
+
Skills: `/bravos-templatize` · `/bravos-env` · `/bravos-prove`
|
|
128
|
+
|
|
129
|
+
Template → authorized target → run. Only a reproduced run confirms; reading code never does.
|
|
130
|
+
|
|
131
|
+
### I fixed a finding
|
|
132
|
+
|
|
133
|
+
Skills: `/bravos-recheck`
|
|
134
|
+
|
|
135
|
+
Re-verifies stale claims, replays the retest kit, and checks the fix added no new exposure.
|
|
136
|
+
|
|
137
|
+
### I am about to open a PR
|
|
138
|
+
|
|
139
|
+
Skills: `/bravos-ship`
|
|
140
|
+
|
|
141
|
+
The no-mistakes gate: validate, lint since the merge base, disclose new exposure, block confirmed exploits on changed paths.
|
|
142
|
+
|
|
143
|
+
### Find the routes, callers or blast radius
|
|
144
|
+
|
|
145
|
+
Skills: `/bravos-codegraph`
|
|
146
|
+
|
|
147
|
+
One bounded question to the code graph instead of grepping. Needs codegraph-mcp (pip install bravos; licence-free). With it, other skills get sharper on their own: bravos-templatize targets the route and auth that actually reach each sink, bravos-recheck follows a fix's callers and blast radius into other files, bravos-context drafts the real public surface, and bravos-prove tests the most reachable exposures first. Without it they read the code and say so.
|
|
148
|
+
|
|
149
|
+
### Scan for dependency CVEs, secrets, IaC
|
|
150
|
+
|
|
151
|
+
Skills: `/bravos-sast`
|
|
152
|
+
|
|
153
|
+
Licensed bravos-cli: Semgrep rules, request-to-sink taint over the code graph, dependency CVEs with whether the vulnerable function is reached, secrets (git history too) and IaC. It reads the GuardLink threat model as input, so annotate first. Says plainly when it is unavailable.
|
|
154
|
+
|
|
155
|
+
### What did SAST find, and what happens to it
|
|
156
|
+
|
|
157
|
+
Skills: `/bravos-sast` · `/bravos-annotate` · `/bravos-templatize` · `/bravos-prove` · `/bravos-recheck`
|
|
158
|
+
|
|
159
|
+
bravos sast candidates (free, no model) reads the newest run in .bravos/sast/: reachable findings joined to a claim feed bravos-templatize and bravos-prove (the route and taint path to target, tested first); reachable findings not in the threat model are leads for bravos-annotate; bravos-recheck rescans the changed files after a fix. A static finding is a lead, never a confirmation; only a run confirms. The briefing's SAST line shows the latest run, or that SAST is unavailable.
|
|
160
|
+
|
|
161
|
+
### Keep it fresh while I work, or nightly
|
|
162
|
+
|
|
163
|
+
Skills: `/bravos-schedule`
|
|
164
|
+
|
|
165
|
+
An in-session /loop from .claude/loop.md, the nightly workflow, or a cloud task you create. Loops never send test traffic.
|
|
166
|
+
|
|
167
|
+
### Add or fix security CI
|
|
168
|
+
|
|
169
|
+
Skills: `/bravos-cicd`
|
|
170
|
+
|
|
171
|
+
PR gate, nightly annotation, SARIF to code scanning, script-injection fixes.
|
|
172
|
+
|
|
173
|
+
### An app made of several repositories (microservices)
|
|
174
|
+
|
|
175
|
+
Skills: `/bravos-annotate` · `/bravos-codegraph` · `/bravos-prove`
|
|
176
|
+
|
|
177
|
+
Clone the services side by side in one folder, then run bravos workspace create NAME --member ./gateway=gateway@http://127.0.0.1:8080 --member ./orders=orders@http://127.0.0.1:8081 (bravos workspace plus guardlink link-project plus bravos in each repo and in the shared folder). Work on a service from its own repo; ask questions that cross services from the shared folder, where the briefing and bravos watch show every service. Declare each call to another service as @flows #<this>-<x> -> #<other>-<y> via http (plain ids, not #repo.tag). Build the code graph over the shared folder so it links one service's client calls to another's handlers. bravos report data --workspace writes one report across the services.
|
|
178
|
+
|
|
179
|
+
### Write up the assessment, a finding or a retest
|
|
180
|
+
|
|
181
|
+
Skills: `/bravos-report`
|
|
182
|
+
|
|
183
|
+
Ask "write the assessment report" (or a finding, a retest, an executive brief). bravos report data gathers the evidence; the agent writes report.md in the eight-section reading order, with every proof claim citing its evidence; bravos report check refuses a finding labelled reproduced without a run or ledger record; bravos report export writes md, txt, html and pdf. bravos watch shows the newest report (p to read it, o to open the pdf or html).
|
|
184
|
+
|
|
185
|
+
## Every skill
|
|
186
|
+
|
|
187
|
+
### bravos
|
|
188
|
+
|
|
189
|
+
Front door: reads the security picture, explains the evidence loop, routes to the right skill.
|
|
190
|
+
|
|
191
|
+
### bravos-context
|
|
192
|
+
|
|
193
|
+
Learns what the product intends from the repo and a short interview; records invariants, intended behaviour, actors, assets and boundaries.
|
|
194
|
+
|
|
195
|
+
### bravos-annotate
|
|
196
|
+
|
|
197
|
+
Picks a budgeted slice (entry points and trust code first), follows guardlink's own playbook, writes annotations to the evidence bar, lints until clean, reports the coverage delta.
|
|
198
|
+
|
|
199
|
+
### bravos-codegraph
|
|
200
|
+
|
|
201
|
+
Bounded questions to codegraph-mcp: routes, attack surface, sinks a symbol reaches, liveness, blast radius, annotation gaps in a change.
|
|
202
|
+
|
|
203
|
+
### bravos-ship
|
|
204
|
+
|
|
205
|
+
The pre-PR gate. Optional PreToolUse hook (bravos init --pre-pr-hook) runs it on every gh pr create / gh pr ready.
|
|
206
|
+
|
|
207
|
+
### bravos-templatize
|
|
208
|
+
|
|
209
|
+
Turns open exposures into validated cxg templates following guardlink's translate method; keeps replay kits apart from scan templates; records classes with no oracle as gaps.
|
|
210
|
+
|
|
211
|
+
### bravos-env
|
|
212
|
+
|
|
213
|
+
Brings up a target you own: loopback by default, a committed reviewed scope.yaml, your typed authorization, a real session captured or imported.
|
|
214
|
+
|
|
215
|
+
### bravos-prove
|
|
216
|
+
|
|
217
|
+
Runs the authorized test, reads cxg's report and exit code, writes outcomes only through guardlink's hypothesis ledger.
|
|
218
|
+
|
|
219
|
+
### bravos-recheck
|
|
220
|
+
|
|
221
|
+
After a fix: stale claims, retest-kit replay, no new exposure, what to test next.
|
|
222
|
+
|
|
223
|
+
### bravos-sast
|
|
224
|
+
|
|
225
|
+
Licensed static analysis joined to guardlink claims and reachability; gates on findings new since the base.
|
|
226
|
+
|
|
227
|
+
### bravos-campaign
|
|
228
|
+
|
|
229
|
+
A goal over many rounds: disjoint batches, one subagent each, a gate per batch, rests, a final before-and-after.
|
|
230
|
+
|
|
231
|
+
### bravos-schedule
|
|
232
|
+
|
|
233
|
+
Recurring upkeep with each harness's own mechanism; never live tests from a loop.
|
|
234
|
+
|
|
235
|
+
### bravos-cicd
|
|
236
|
+
|
|
237
|
+
Audits and writes the security workflows.
|
|
238
|
+
|
|
239
|
+
### bravos-report
|
|
240
|
+
|
|
241
|
+
Writes the assessment, a single finding, a retest or an executive brief from the evidence bravos report data gathers; each finding's evidence level is computed from the ledger and runs, bravos report check gates the report, and bravos report export renders md, txt, html and pdf. Reports stay in .bravos/reports/, not committed.
|
|
242
|
+
|
|
243
|
+
## Reading a finding
|
|
244
|
+
|
|
245
|
+
### open / mitigated / accepted
|
|
246
|
+
|
|
247
|
+
Does a control cover it? Open means no control is recorded; mitigated names the control; accepted is a human's written decision to live with it, never an agent's.
|
|
248
|
+
|
|
249
|
+
### untested / confirmed / refuted
|
|
250
|
+
|
|
251
|
+
Has an authorized run tested it? Confirmed means a run reproduced it, with evidence; refuted means a run showed it does not hold. Untested is neither cleared nor proven.
|
|
252
|
+
|
|
253
|
+
### verified / stale
|
|
254
|
+
|
|
255
|
+
Is the claim still true of today's code? Stale means the code under it changed since it was last checked; bravos-recheck clears it.
|
|
256
|
+
|
|
257
|
+
### Severity
|
|
258
|
+
|
|
259
|
+
Rated by impact on this product × reach × ease, with the reason written down. Without .guardlink/intent.md, severities are defaults, not calibrated.
|
|
260
|
+
|
|
261
|
+
### unknown is not zero
|
|
262
|
+
|
|
263
|
+
When a tool is unavailable the briefing says "unknown", never 0. Nothing is called clean unless a gate exited 0.
|
|
264
|
+
|
|
265
|
+
## The hard rules
|
|
266
|
+
|
|
267
|
+
### Authorization
|
|
268
|
+
|
|
269
|
+
No request goes to a host other than loopback without a committed, reviewed scope.yaml and your own typed authorization naming the target.
|
|
270
|
+
|
|
271
|
+
### Destructive tests
|
|
272
|
+
|
|
273
|
+
DoS and other destructive classes run only against a disposable loopback target.
|
|
274
|
+
|
|
275
|
+
### Credentials
|
|
276
|
+
|
|
277
|
+
Never invented. Test users and sessions are seeded or captured for real, or the run waits for you.
|
|
278
|
+
|
|
279
|
+
### No hidden agents
|
|
280
|
+
|
|
281
|
+
The agent in your session does the work; bravos never spawns another agent CLI. Model-API probe generation in cxg runs only after you agree to the spend.
|
|
282
|
+
|
|
283
|
+
### Governance
|
|
284
|
+
|
|
285
|
+
Agents never write @accepts or @entitles (a human's decision) and never write @confirmed without reproduction evidence in hand.
|
|
286
|
+
|
|
287
|
+
## Harnesses
|
|
288
|
+
|
|
289
|
+
### Claude Code
|
|
290
|
+
|
|
291
|
+
Skills in .claude/skills, SessionStart hook in .claude/settings.json (banner + briefing), MCP in .mcp.json, optional pre-PR PreToolUse hook. Also as a plugin.
|
|
292
|
+
|
|
293
|
+
### Codex
|
|
294
|
+
|
|
295
|
+
Skills in .agents/skills, hook in .codex/hooks.json (trust it once in /hooks), MCP in .codex/config.toml. Also as a plugin.
|
|
296
|
+
|
|
297
|
+
### omp
|
|
298
|
+
|
|
299
|
+
Reads the Claude Code skills and MCP; .omp/extensions/bravos.js shows the banner and hands over the briefing.
|
|
300
|
+
|
|
301
|
+
### opencode
|
|
302
|
+
|
|
303
|
+
Finds the skills in .claude/skills and .agents/skills through its skill tool; bravos adds /bravos… commands in .opencode/commands that load them, a plugin in .opencode/plugins (opencode 2.x) that adds the briefing to the system prompt, and guardlink MCP in opencode.json.
|
|
304
|
+
|
|
305
|
+
### Antigravity (agy)
|
|
306
|
+
|
|
307
|
+
Reads .agents/skills; .agents/hooks.json carries a bravos SessionStart group that injects the briefing.
|
|
308
|
+
|
|
309
|
+
### Cursor
|
|
310
|
+
|
|
311
|
+
.cursor/rules/bravos.mdc points at the skills when the repo has a .cursor/ folder.
|
|
312
|
+
|
|
313
|
+
### VS Code (Copilot agent)
|
|
314
|
+
|
|
315
|
+
Reads hooks from .claude/settings.json (preview), so the briefing can load there too.
|
|
316
|
+
|
|
317
|
+
### Every repo, or one copy of each skill
|
|
318
|
+
|
|
319
|
+
bravos init --global installs for your user: skills in ~/.claude/skills (linked from ~/.agents/skills) and a user hook that briefs repos with a threat model but no bravos install. bravos sync --link-skills makes a repo's .agents/skills a link to .claude/skills, so tools that read both (Warp) list each skill once.
|
|
320
|
+
|
|
321
|
+
### Banner switches
|
|
322
|
+
|
|
323
|
+
BRAVOS_BANNER=off hides it; BRAVOS_BANNER_COLOR=off or NO_COLOR prints it plain. The briefing for the model is unchanged either way.
|