@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,23 @@
|
|
|
1
|
+
# Executive brief template
|
|
2
|
+
|
|
3
|
+
One to two pages for a business owner. No reproduction steps; every claim still cites its evidence id, so a reader can follow it into the full report. Keep the four `##` headings; `bravos report check` reads them.
|
|
4
|
+
|
|
5
|
+
```markdown
|
|
6
|
+
# <Application> security brief — <month year>
|
|
7
|
+
|
|
8
|
+
## Executive assessment
|
|
9
|
+
|
|
10
|
+
<Three to five sentences: the overall position, the single most important risk, what was demonstrated versus reviewed, and the strength worth keeping.>
|
|
11
|
+
|
|
12
|
+
## Main risks
|
|
13
|
+
|
|
14
|
+
<At most five, most important first. Each: one sentence of consequence in business terms, its evidence label, and its F-id or EXP ids.>
|
|
15
|
+
|
|
16
|
+
## Decisions needed
|
|
17
|
+
|
|
18
|
+
<What the reader must decide, fund or accept, each with its cost or effort and its deadline if any. Accepting a risk is the owner's decision to record; the brief only frames it.>
|
|
19
|
+
|
|
20
|
+
## What this rests on
|
|
21
|
+
|
|
22
|
+
<Scope in two sentences, the coverage with denominators, what was not tested, and where the full evidence is.>
|
|
23
|
+
```
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Finding template: a write-up a team can follow
|
|
2
|
+
|
|
3
|
+
Use this shape for a single-finding report, and for each finding inside an assessment. Write it so someone who was not in the session can understand the issue, stand up the same environment, reproduce the issue in minutes, and check the fix, without asking you anything.
|
|
4
|
+
|
|
5
|
+
`bravos report check` reads the `**Status:**` and `**Severity:**` lines, and looks for the parts below either as a heading or as a bold `**Label:**` line. Use the headings. They read better than a wall of labels.
|
|
6
|
+
|
|
7
|
+
```markdown
|
|
8
|
+
# <Who can do what to what, in plain words: "Any namespace Writer can forge the result of a Nexus operation it did not start">
|
|
9
|
+
|
|
10
|
+
**Status:** reproduced at runtime, on <which setup> (see Evidence)
|
|
11
|
+
**Severity:** high
|
|
12
|
+
**Affects:** <product, version or commit tested; what was reviewed versus what was run>
|
|
13
|
+
**Tested by:** <name>, <date>, on <where: a local test server at 127.0.0.1>
|
|
14
|
+
|
|
15
|
+
## In short
|
|
16
|
+
<Three or four sentences a non-specialist can follow: what the feature does, what goes wrong, who can do it, what they get. End with how quickly it reproduces.>
|
|
17
|
+
|
|
18
|
+
## What the attacker wants, and what they need
|
|
19
|
+
**Attacker intent.** <What a real attacker would use this for, in business terms: two or three concrete examples.>
|
|
20
|
+
**What the attacker needs:** <role and permission, the identifiers they need and how they get them, network position, configuration preconditions.>
|
|
21
|
+
**What the attacker does not need:** <admin rights, secrets, special network access — say it, it is what makes the finding matter.>
|
|
22
|
+
|
|
23
|
+
## Why this is a vulnerability
|
|
24
|
+
<The promise the system makes, and how it is broken. If the team has declined similar issues before, say plainly why this one is different.>
|
|
25
|
+
|
|
26
|
+
## What we found
|
|
27
|
+
<Each weakness as a numbered item: one plain sentence of what is wrong, then *Code:* file:line and the function. Mention related observations as such, not as separate claims.>
|
|
28
|
+
|
|
29
|
+
## How the issues chain, step by step
|
|
30
|
+
<Numbered steps from the attacker's first action to the impact, each naming which weakness it uses.>
|
|
31
|
+
|
|
32
|
+
## Impact
|
|
33
|
+
**What we showed:** <only what a run demonstrated>
|
|
34
|
+
**What follows from the code but we did not run:** <supported by analysis>
|
|
35
|
+
**What is limited:** <boundaries that held, prerequisites that cap it>
|
|
36
|
+
**Severity: <level>.** <Why, in context. A CVSS line, if any, with its version and full vector.>
|
|
37
|
+
|
|
38
|
+
## Test environment: how it was set up
|
|
39
|
+
<Numbered, copy-pasteable: how the product was built, every config change with the exact snippet, dynamic or feature flags, how keys and accounts were made, what was seeded (tenants, namespaces, endpoints, users), and any helper you wrote to test it (a stub handler, a gRPC client or worker): what it does and how to install and run it. Point to the copies of the config files kept with the evidence.>
|
|
40
|
+
|
|
41
|
+
## Roles and permissions used
|
|
42
|
+
| Account | Permission | Used for | Result |
|
|
43
|
+
|---|---|---|---|
|
|
44
|
+
<one row per identity: the attacker's, the controls (anonymous, read-only, another tenant), the setup-only admin>
|
|
45
|
+
|
|
46
|
+
## Reproduce it with cxg
|
|
47
|
+
1. Install: `cargo install cert-x-gen` (and what the template needs, e.g. Python 3); `cxg --version`.
|
|
48
|
+
2. The template: where it is; `cxg template validate <file>`.
|
|
49
|
+
3. The environment variables it reads, one per line, with what each is.
|
|
50
|
+
4. The exact command, e.g. `cxg scan --scope http://<host>:<port> --templates <file> --timeout 120 --output <name> --output-format json` (give the scope as a URL with its port).
|
|
51
|
+
5. The expected output, quoted from a real run, with the line that proves it marked.
|
|
52
|
+
|
|
53
|
+
## Reproduce it by hand
|
|
54
|
+
<Numbered granular steps: each request as a copy-pasteable command (curl, a short script), the response to expect after each, and the controls that must fail (anonymous, read-only, another tenant, a garbage input). Any helper script in full.>
|
|
55
|
+
|
|
56
|
+
## What held
|
|
57
|
+
<Controls that worked: they tell the team what not to break while fixing.>
|
|
58
|
+
|
|
59
|
+
## How to fix it, and how to check the fix
|
|
60
|
+
**Fix:** <numbered changes at the cause, with the function names>
|
|
61
|
+
**Check:** <the replay that must now fail, the legitimate path that must still work>
|
|
62
|
+
|
|
63
|
+
## Evidence
|
|
64
|
+
**Status:** <the evidence label word for word> (<the evidence ids from bravos report data>).
|
|
65
|
+
<Where the run output, templates, scripts and config copies are; what each file shows. Keys and tokens are never included.>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Rules
|
|
69
|
+
|
|
70
|
+
- **Title:** who can do what to what. "Broken access control in X" is a category, not a title.
|
|
71
|
+
- **Status:** one of the four labels, word for word: `reproduced at runtime`, `control held at runtime`, `established by code or configuration review`, `suspected, awaiting validation`. The `## Evidence` section repeats it with the evidence ids; the top line may say "(see Evidence)". Never claim more than the cited ids support.
|
|
72
|
+
- **Evidence ids** (`RUN-1/F-1`, `LED-…`, `EXP-…`) belong in the Evidence section, not in the prose. In the body, say "reproduced on our test server" and let the reader find the proof at the end.
|
|
73
|
+
- **Commands are exact.** Real ports, real paths, real flag spellings, environment variables named. Tokens appear as `$WRITER`, never as values. Run each command, or say plainly which one was not run.
|
|
74
|
+
- **The cxg reproduction** is a template the team can run unchanged. When the issue is proven by a script, turn the script into a cxg template (bravos-templatize) and replay it (bravos-prove, with the person's authorization) before you quote its output.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Retest template
|
|
2
|
+
|
|
3
|
+
Records whether a fix held. The original finding stays as it was; the retest is added beside it. Keep the four `##` headings and the bold labels; `bravos report check` reads them.
|
|
4
|
+
|
|
5
|
+
```markdown
|
|
6
|
+
# Retest — F-03 Organization members can download invoices belonging to another organization
|
|
7
|
+
|
|
8
|
+
## Retest summary
|
|
9
|
+
|
|
10
|
+
<One paragraph: fixed, partly fixed, or not fixed; on which build; what still remains exposed.>
|
|
11
|
+
|
|
12
|
+
## Original finding
|
|
13
|
+
|
|
14
|
+
<The original finding block, unchanged (finding.md shape), with its original evidence ids.>
|
|
15
|
+
|
|
16
|
+
## Retest
|
|
17
|
+
|
|
18
|
+
**Retested build:** commit <sha>, <environment>
|
|
19
|
+
**Replay:** <the original template or steps, rerun; the run id, e.g. RUN-7/X-1>
|
|
20
|
+
**Result:** control held at runtime (RUN-7/X-1, LED-89166523)
|
|
21
|
+
**Bypass variants:** <the variants tried — another role, another identifier format, the export path — and each outcome>
|
|
22
|
+
**Legitimate behaviour:** <the same-organization download still works, with its evidence id, or "not checked">
|
|
23
|
+
|
|
24
|
+
## Residual exposure
|
|
25
|
+
|
|
26
|
+
<What remains: related paths not yet fixed, variants not tried, or "none established". Name the next action.>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**Result** uses the same four evidence labels as findings: `control held at runtime` when the replay was refused, `reproduced at runtime` when the fix did not hold, and so on, followed by the ids.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Writing style for bravos reports
|
|
2
|
+
|
|
3
|
+
**The test:** someone who was not in the session can understand the issue, stand up the same environment, reproduce it in minutes, fix its cause and show closure, without asking the author what anything means.
|
|
4
|
+
|
|
5
|
+
The usual reader is a team: the product's own security or engineering team, or a vendor receiving a report. Write for them, not for bravos. A report that reads like a checklist, with evidence ids in every sentence, gets put down.
|
|
6
|
+
|
|
7
|
+
## Write it like this
|
|
8
|
+
|
|
9
|
+
- **Plain words first.** Explain the feature in one sentence before saying what is wrong with it. Use the product's own names (callback token, namespace, Writer), not bravos's (EXP-34, claim, asset).
|
|
10
|
+
- **Lead with the consequence.** The first paragraph says who can do what to whom, and how quickly it reproduces.
|
|
11
|
+
- **Say what the attacker wants.** Two or three concrete things a real attacker would do with this. Then what they need, and what they do not need: no admin, no secret, no special network position. That second list is often what makes a team accept the finding.
|
|
12
|
+
- **Tell the chain as numbered steps.** One step per action, each naming the weakness it uses.
|
|
13
|
+
- **Show the whole environment.** Name the build, every config change (with the snippet), feature flags, how keys and accounts were made, what was seeded, and every helper you wrote, such as a stub handler or a gRPC worker: what it does, and how to install and run it. If a step needs a file, say where the copy is.
|
|
14
|
+
- **List the roles in a table.** Who held which permission, what each was used for, and what happened. The controls that were refused belong in it too.
|
|
15
|
+
- **Give two ways to reproduce:**
|
|
16
|
+
- **With cxg:** install with `cargo install cert-x-gen`, then the exact `cxg scan` command and the output to expect.
|
|
17
|
+
- **By hand:** each request as a command, with the response to expect after it.
|
|
18
|
+
- **Separate what was shown from what follows.** "What we showed", "What follows from the code but we did not run", "What is limited". A "not reproduced" says what was tried.
|
|
19
|
+
- **Say why it is a vulnerability.** Name the promise the system makes and how it breaks. If similar issues were declined before, say why this one is different.
|
|
20
|
+
- **Name the controls that held.** They tell the team what not to break while fixing.
|
|
21
|
+
- **Keep severity and priority apart.** Severity is the technical consequence. Priority adds exposure and prerequisites. A CVSS line carries its version and full vector, and never stands alone.
|
|
22
|
+
|
|
23
|
+
## Avoid
|
|
24
|
+
|
|
25
|
+
- Evidence ids in the prose. They go in the Evidence section at the end.
|
|
26
|
+
- A wall of `**Label:**` lines. Use headings and short paragraphs.
|
|
27
|
+
- Jargon the reader has to look up, and bravos's internal names.
|
|
28
|
+
- Hedging a result you reproduced, and overstating one you did not.
|
|
29
|
+
- Filler: "it is important to note", restating a heading, apologising for length.
|
|
30
|
+
- Any live secret, token or password. Write `$WRITER`; cite where a secret is, never its value.
|
|
31
|
+
|
|
32
|
+
## The model paragraph
|
|
33
|
+
|
|
34
|
+
A fictional finding, abbreviated, in the voice to aim for:
|
|
35
|
+
|
|
36
|
+
> **Organization members can download invoices belonging to another organization**
|
|
37
|
+
>
|
|
38
|
+
> A standard member of Organization A downloaded an invoice that belongs to Organization B, just by sending its invoice number to the download endpoint. All they needed was a normal account and the invoice number.
|
|
39
|
+
>
|
|
40
|
+
> The download handler looks the invoice up by number alone. The invoice list does check the organization, which is why the problem never shows in the normal interface.
|
|
41
|
+
>
|
|
42
|
+
> We reproduced this with two test organizations. We did not establish whether invoice numbers can be guessed at scale.
|
|
43
|
+
>
|
|
44
|
+
> Fix: check the organization before returning the invoice. Then confirm that a foreign-organization request is refused, and that a same-organization download still works.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: bravos-sast
|
|
3
|
+
description: "Run licensed static analysis (bravos-cli) over the repo after the threat model is in place, read each finding's join to guardlink claims and its reachability, and gate on findings new since the base. When bravos-cli or its licence is absent, say plainly that SAST is unavailable and what would enable it. Use when asked for SAST, static analysis, dependency, secret or IaC findings, and when bravos-ship reaches its SAST step."
|
|
4
|
+
---
|
|
5
|
+
# Bravos SAST: static findings joined to the threat model
|
|
6
|
+
|
|
7
|
+
## When
|
|
8
|
+
|
|
9
|
+
- The user asks for SAST, a code scan, dependency CVEs, leaked secrets or IaC checks.
|
|
10
|
+
- bravos-ship reaches its SAST step on a branch.
|
|
11
|
+
- A finding from another scanner needs placing against the threat model.
|
|
12
|
+
|
|
13
|
+
## Do
|
|
14
|
+
|
|
15
|
+
**Plan check first.** SAST is on the Team plan. Run `bravos licence --json` once per session (the briefing's `Plan:` line says the same); if `licensed` is false, tell the person in two sentences what this skill would do here and that it needs a Team licence — `bravos account activate` starts a 30-day trial with no card, or https://account.bugb.io/pricing — then stop, and do nothing else from this skill. A licence in grace still counts.
|
|
16
|
+
|
|
17
|
+
1. Run `bravos doctor`. SAST needs both `bravos-cli` found and a licence that verifies to a paid tier (`bravos licence` shows it: `BRAVOS_LICENCE`, or `licence.jwt` in `$BRAVOS_HOME`, `~/.bravos` or `~/.bugb`). `bravos-cli` is the analyzer bundle `bravos setup` stages under `$BRAVOS_HOME/analyzer/` (a machine that moved from the Python bravos may still have it inside that install; `bravos doctor` prints whichever it found). If it is missing, set it up yourself: `bravos setup --check`, show the person `bravos setup --dry-run --analyzer`, and with their go-ahead run `bravos setup --analyzer` (it fetches the signed bundle through the account portal and verifies it before anything lands). A missing licence is theirs to activate (`bravos account activate`); never invent or copy one. If either is still missing, tell the user SAST is unavailable, name the missing piece, and stop.
|
|
18
|
+
2. Annotate first: the analyzer reads guardlink's `@exposes` claims, so its recall depends on them. If the briefing shows the files in scope as unannotated, run bravos-annotate on them before analyzing.
|
|
19
|
+
3. Analyze with `bravos sast run` (add `--scope a/,b/` for part of the tree, `--min-severity`, `--timeout`): it runs `bravos-cli analyze . --ci --fail-on never --allow-incomplete-analysis --codegraph reuse -f json,sarif` under the bundle's own environment into `.bravos/sast/<run>/report.{json,sarif}` (not committed; reports can point at secrets, CI uploads the SARIF instead) and writes `meta.json` beside them, which the briefing's `SAST:` line and `bravos sast` read.
|
|
20
|
+
4. Read the exit code first: 0 no finding at or above the level; 1 findings; 3 the licence was refused or the analysis failed; 4 a required stage failed. Exit 3 and 4 are unavailable or incomplete, never clean.
|
|
21
|
+
5. Read the findings through `bravos sast candidates` (`--json` for the full list, `--all` to include unknown reach): reachable findings joined to a claim (with its state), reachable ones not in the threat model yet, dependencies with whether the vulnerable function is reached, and secrets.
|
|
22
|
+
6. Sort them: a finding joined to an open `@exposes` is that same risk, not a new one: hand it to bravos-templatize, whose template can target the finding's route and taint path; an unjoined reachable finding is a gap in the threat model: hand it to bravos-annotate to record honestly as an `@exposes` with its `@audit`; an unreachable one is reported as such, not dropped; a secret is reported for rotation and never becomes a pentest candidate.
|
|
23
|
+
7. Gate what is new: analyze the base too (or use the committed baseline) and run `bravos-cli diff .bravos/sast/<run>/report.json <base report.json> --fail-on-new`. Exit 4 means the two reports use different finding-identity schemes: re-run the base with the same analyzer, never read it as no new findings.
|
|
24
|
+
8. Report by severity: joined versus unjoined, reachable versus unreachable, new versus already on the base, and what was not analyzed.
|
|
25
|
+
|
|
26
|
+
## Never
|
|
27
|
+
|
|
28
|
+
- Call the code clean unless `bravos-cli analyze` exited 0, and then only at the level it was run with.
|
|
29
|
+
- Treat a licence refusal, a missing binary or a crashed run as a pass.
|
|
30
|
+
- Baseline, suppress or delete a finding to make the gate pass; that is the human's decision.
|
|
31
|
+
- Treat a SAST finding as a confirmed exploit, or write `@confirmed`, `@accepts` or `@entitles`; a static finding is a lead, and only a reproduced run confirms.
|
|
32
|
+
- Run `guardlink annotate` with an agent flag to fill a gap; hand it to bravos-annotate.
|
|
33
|
+
- Commit a SAST report, or quote a secret's value anywhere; name the rule, file and line only.
|
|
34
|
+
|
|
35
|
+
## Done when
|
|
36
|
+
|
|
37
|
+
- Either SAST ran and its exit code and every finding's join and reachability were reported, or you said exactly why it could not run.
|
|
38
|
+
- New findings since the base are listed, and `bravos-cli diff` exited 0 or each new one is reported as open.
|
|
39
|
+
- Unjoined reachable findings were handed to bravos-annotate.
|
|
40
|
+
|
|
41
|
+
## What it leaves for the next skill
|
|
42
|
+
|
|
43
|
+
- The SARIF and JSON paths and the list of new and unjoined findings, for bravos-ship and bravos-cicd; `bravos sast candidates` for bravos-annotate (unjoined leads), bravos-templatize and bravos-prove (joined findings with their routes), and bravos-recheck (rescans after a fix).
|
|
44
|
+
|
|
45
|
+
## Go deeper
|
|
46
|
+
|
|
47
|
+
- `{{SKILLS_ROOT}}/bravos-annotate/SKILL.md` — recording an unjoined finding as a claim.
|
|
48
|
+
- `{{SKILLS_ROOT}}/bravos-ship/SKILL.md` — where SAST sits in the pre-PR gate.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: bravos-schedule
|
|
3
|
+
description: "Set up recurring security upkeep with each harness's own mechanism: an in-session /loop driven by a committed .claude/loop.md while the user works, the nightly GitHub Actions workflow for work while the laptop is closed, or a Claude Code cloud scheduled task the user creates. The nightly can also replay retest kits against a target the committed, reviewed scope authorizes; loops never test. Use when asked to keep annotating while I work, run something nightly or on a schedule, or check the threat model periodically."
|
|
4
|
+
---
|
|
5
|
+
# Bravos schedule: recurring upkeep, on the harness's own clock
|
|
6
|
+
|
|
7
|
+
## When
|
|
8
|
+
|
|
9
|
+
- The user asks to keep annotating or checking while they work.
|
|
10
|
+
- The user asks for nightly, overnight, weekly or periodic security work.
|
|
11
|
+
- The team wants the worklist to shrink without spending interactive sessions on it.
|
|
12
|
+
|
|
13
|
+
## Do
|
|
14
|
+
|
|
15
|
+
**Plan check first.** Scheduled upkeep is on the Team plan. Run `bravos licence --json` once per session (the briefing's `Plan:` line says the same); if `licensed` is false, tell the person in two sentences what this skill would do here and that it needs a Team licence — `bravos account activate` starts a 30-day trial with no card, or https://account.bugb.io/pricing — then stop, and do nothing else from this skill. A licence in grace still counts.
|
|
16
|
+
|
|
17
|
+
1. If the user wants a goal reached in rounds with rests ("100% coverage", "every exposure", subagents per round), that is bravos-campaign; hand it over. Otherwise map the intent to one mechanism and say which: while this session is open → an in-session loop; while the laptop is closed, any harness → the nightly GitHub Actions workflow; while the laptop is closed, Claude Code with a cloud account → a cloud scheduled task the user creates.
|
|
18
|
+
2. In-session loop (Claude Code only): copy `{{SKILL_DIR}}/loop.md` to `.claude/loop.md`, adjust the steps to the user's request, and commit it. Tell the user to start it with a bare `/loop`, which reads that file; it runs only while the session is open, stops when it exits, and expires after seven days. Codex has no equivalent; offer the nightly workflow instead.
|
|
19
|
+
3. Nightly workflow: run `bravos init --ci` (or `bravos sync --ci` in a repo bravos already set up), keep the variant for the team's harness (`bravos-nightly-claude.yml` or `bravos-nightly-codex.yml`) and delete the other. Tell the user the secret it needs (`ANTHROPIC_API_KEY` or `OPENAI_API_KEY`) and to allow GitHub Actions to create pull requests in the repo settings.
|
|
20
|
+
4. Cloud scheduled task: a skill cannot create one. Give the user the prompt to paste into `/schedule`: follow bravos-annotate in slice mode on the next unannotated files, run its lint gate, and open one pull request whose body carries bravos-ship's Security gate section. Tell them the task clones the default branch and runs at most hourly.
|
|
21
|
+
5. State the spend before the user starts any of them: files per run (the default slice is five) times runs per day, and that each run spends model tokens.
|
|
22
|
+
6. Nightly scan, opt-in: it replays the kits in `.guardlink/cxg-replay/` and records outcomes through `guardlink hypothesis confirm --from-scan`, and it runs only when `.guardlink/scope.yaml` is committed with an `authorization_attestation` a human typed and reviewed in a PR, and the repo variable `BRAVOS_SCAN_TARGET` is a loopback URL the job starts (repo variable `BRAVOS_APP_START`) or a host in that scope's `host_allowlist`. Otherwise the step prints why it skipped. Tell the user which variables to set and that the scope file is their standing authorization.
|
|
23
|
+
|
|
24
|
+
## Never
|
|
25
|
+
|
|
26
|
+
- Create, enable or change a schedule the user did not ask for, or at a faster cadence than they agreed.
|
|
27
|
+
- Schedule a live test from a loop, or add a nightly scan the user did not ask for. The nightly scan never generates probes, never uses `--destructive-ok`, and never runs without the committed scope's human-written attestation.
|
|
28
|
+
- Write an API key, token or session into a file or a workflow; secrets live in the repo's secret store.
|
|
29
|
+
- Let a loop push, open a pull request, or write `@accepts`, `@entitles` or `@confirmed`.
|
|
30
|
+
|
|
31
|
+
## Done when
|
|
32
|
+
|
|
33
|
+
- The user knows which mechanism was set up, how to start or stop it, what it spends, and when it expires.
|
|
34
|
+
- `.claude/loop.md` or the workflow file is committed, and any secret it needs is named but not written.
|
|
35
|
+
- You said whether the nightly scan is on, what authorizes it, and that loops never test.
|
|
36
|
+
|
|
37
|
+
## What it leaves for the next skill
|
|
38
|
+
|
|
39
|
+
- The loop file or workflow path, and the secrets and settings the user still has to add, for bravos-cicd.
|
|
40
|
+
|
|
41
|
+
## Go deeper
|
|
42
|
+
|
|
43
|
+
- `{{SKILL_DIR}}/loop.md` — the default iteration for a bare `/loop`.
|
|
44
|
+
- `{{SKILLS_ROOT}}/bravos-cicd/SKILL.md` — the workflows themselves.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# bravos security upkeep loop
|
|
2
|
+
|
|
3
|
+
Each iteration, in this session:
|
|
4
|
+
|
|
5
|
+
1. Read what changed since the last iteration: `git diff --name-only` against the commit the
|
|
6
|
+
previous iteration ended on (on the first iteration, against the merge base with the default
|
|
7
|
+
branch).
|
|
8
|
+
2. If any changed file is security-relevant and its claims are missing or stale, follow
|
|
9
|
+
bravos-annotate with the diff playbook on those files only, and run its lint gate until it
|
|
10
|
+
reports 0 errors.
|
|
11
|
+
3. Run bravos-ship's checks without opening anything: guardlink validate, lint since the merge
|
|
12
|
+
base, and diff against the merge base. Report new exposure as open.
|
|
13
|
+
4. Print one line: files annotated, coverage before and after, lint errors, new exposures.
|
|
14
|
+
|
|
15
|
+
Never open or update a pull request, push, run a live test, or write `@accepts`, `@entitles` or
|
|
16
|
+
`@confirmed` from this loop. If nothing security-relevant changed, say so in one line and stop.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: bravos-ship
|
|
3
|
+
description: "The no-mistakes security gate before a pull request: validate the threat model, lint the branch's new claims, fail on new exposure against the base, list undefended paths, annotate the change's gaps, and refuse to ship a confirmed-exploitable claim on a changed path. Use before `gh pr create`, `gh pr ready` or pushing for review, and whenever asked for a pre-PR, pre-merge or pre-push security check."
|
|
4
|
+
---
|
|
5
|
+
# Bravos ship: the security gate before a pull request
|
|
6
|
+
|
|
7
|
+
## When
|
|
8
|
+
|
|
9
|
+
- You are about to run `gh pr create` or `gh pr ready`, or push a branch for review.
|
|
10
|
+
- The user asks for a pre-PR, pre-merge or pre-push security check.
|
|
11
|
+
- The user asks to open a PR and has not mentioned the gate: suggest it, then run it.
|
|
12
|
+
|
|
13
|
+
## Do
|
|
14
|
+
|
|
15
|
+
**Plan check first.** The pre-PR gate is on the Team plan. Run `bravos licence --json` once per session (the briefing's `Plan:` line says the same); if `licensed` is false, tell the person in two sentences what this skill would do here and that it needs a Team licence — `bravos account activate` starts a 30-day trial with no card, or https://account.bugb.io/pricing — then stop, and do nothing else from this skill. A licence in grace still counts.
|
|
16
|
+
|
|
17
|
+
1. Find the base. Commit or stash work in progress first; the gate judges `HEAD`. The default branch is `git symbolic-ref -q refs/remotes/origin/HEAD` (strip `refs/remotes/origin/`; if it prints nothing, use `main`). Run `git fetch origin <default>` so the merge-base is current (if the fetch fails, offline or with no `origin`, say so and continue with the local ref), then `<base>` = `git merge-base HEAD origin/<default>`. Every gate below is deterministic and spends no model. Read the effort: the user's words ("at high"), else a `bravos effort: <level>` line in `AGENTS.md` / `CLAUDE.md`, else medium. It sets a starting budget, not a limit; go beyond it when the work needs it and say why in one line. Starting budget — low: the deterministic gates (steps 2–4); medium: also the code-graph gaps; high: also SAST; max: also bravos-recheck on the changed paths.
|
|
18
|
+
2. Run `guardlink validate .` and fix what it reports.
|
|
19
|
+
3. Run `guardlink lint . --since <base> --json`. This judges every claim on the branch, not only yours. Apply an entry's `fix` only to a line this session wrote, and rerun until `summary.errors` is 0. For `accepts-written`, `entitles-written` or `confirmed-without-evidence` on any other line, do not touch it: stop and report it to the user.
|
|
20
|
+
4. Run `guardlink diff <base> --fail-on-new --markdown`. Exit 0 means no new unmitigated exposure. A non-zero exit lists new exposures with no `@mitigates`; an `@exposes` plus `@audit` written on this branch is new exposure and stays listed by design. Do not try to turn it green: report each listed exposure as open in the Security gate section and stop there for that exposure. Fixing the code or accepting the risk is the human's decision.
|
|
21
|
+
5. Run `guardlink paths --json` and report how many undefended source-to-sink paths it lists. This is informational in this version and does not block. In a bravos workspace, also read the cross-service edges (`bravos workspace show --json`, `export.edges`) whose `dstMember` is this service and whose target id is declared in a file this branch changed: report them as "callers in other services" in the Security gate section, so the reviewer checks those callers too.
|
|
22
|
+
6. If the code graph is present, call `codegraph_annotation_gaps` on the changed files and send each gap to bravos-annotate (diff playbook).
|
|
23
|
+
7. Run `guardlink ci -f json`. A `confirmed[]` entry whose `location.file` is in `git diff --name-only <base>...HEAD` blocks the PR unless a `@ticket` line written by a human carries it. Check who wrote the `@ticket` line with the MCP tool `guardlink_blame` on the file, or `git log -L <line>,<line>:<file>`, or ask the user. A code change does not clear it: only a human removes the confirmation after a reproduction shows the exploit no longer works. If it blocks, tell the user the threat, asset and file:line, and do not ship.
|
|
24
|
+
8. Rerun steps 2–4 until validate and lint exit 0, and diff exits 0 or every new exposure it lists is reported as open.
|
|
25
|
+
9. If the repo has `.guardlink/report.json`, refresh it with `guardlink parse . -o .guardlink/report.json` and include it in the change.
|
|
26
|
+
10. Run bravos-sast now if `bravos-cli` and its licence are present (its report goes to `.bravos/sast/<run>/`, read with `bravos sast candidates`); otherwise note in the gate section that SAST is unavailable and was not run.
|
|
27
|
+
11. Write the "Security gate" section:
|
|
28
|
+
validate ✔ · lint 0 errors · diff: no new exposure (or: <k> new exposures open, each listed as `#<threat> on #<asset>`) · undefended paths <n> · confirmed on changed paths <k>
|
|
29
|
+
states: <n> open / <m> mitigated · untested <u> / confirmed <k> · stale <s> (guardlink ci)
|
|
30
|
+
Not tested: <what no tool exercised, e.g. no live run, SAST not run>
|
|
31
|
+
If the user asked for a PR, open it with this section in the body; otherwise print the section and stop.
|
|
32
|
+
12. When the opt-in pre-PR hook is installed (`bravos init --pre-pr-hook`), it reruns these gates on `gh pr create` and `gh pr ready` and blocks on lint errors, on a new exposure whose threat and asset ids are not both named in the PR body, on a confirmed claim on a changed file with no `@ticket` for it in that file, and on any gate that fails to run (unknown); let it run. Before the PR exists, `node .bravos/hooks/pre-pr.mjs --check` runs the same gates by hand and lists the new exposures the body must disclose.
|
|
33
|
+
|
|
34
|
+
## Never
|
|
35
|
+
|
|
36
|
+
- Write `@accepts` to clear a finding, or write a `@ticket` line on the human's behalf.
|
|
37
|
+
- Write a `@mitigates` whose control you did not verify in the code, to make `diff` pass.
|
|
38
|
+
- Remove or edit a `@confirmed` line; never treat a code change as clearing one.
|
|
39
|
+
- Apply a lint `fix` to a line this session did not write. For `accepts-written`, `entitles-written` or `confirmed-without-evidence` on someone else's line, never remove or downgrade it: stop and report it.
|
|
40
|
+
- Call the PR clean, secure or safe unless every gate exited 0, and even then say what was not tested.
|
|
41
|
+
- Skip the confirmed check in step 7.
|
|
42
|
+
- Bypass, disable or work around the pre-PR hook.
|
|
43
|
+
|
|
44
|
+
## Done when
|
|
45
|
+
|
|
46
|
+
- On the final `HEAD`, `guardlink validate .` and `guardlink lint . --since <base> --json` (0 errors) exit 0, and `guardlink diff <base> --fail-on-new --markdown` exits 0 or every new exposure it lists is reported to the user as open.
|
|
47
|
+
- No `confirmed[]` entry sits on a changed file without a human-written `@ticket`, or you told the user the PR is blocked.
|
|
48
|
+
- The "Security gate" section reports the three states honestly: exposures open vs mitigated (none accepted by you), untested vs confirmed vs refuted per `guardlink ci`, verified vs stale. It names the tool behind each line and has a "Not tested" line.
|
|
49
|
+
|
|
50
|
+
## What it leaves for the next skill
|
|
51
|
+
|
|
52
|
+
- The Security gate section (in the PR body if one was asked for), the open exposures for the human to decide on, and the undefended-path count for the next bravos-annotate round.
|
|
53
|
+
|
|
54
|
+
## Go deeper
|
|
55
|
+
|
|
56
|
+
- `docs/GUARDLINK_REFERENCE.md` — verbs, including `@ticket`.
|
|
57
|
+
- `.guardlink/README.md` — placement rules for this repo.
|
|
58
|
+
- `{{SKILLS_ROOT}}/guardlink-annotate-diff/SKILL.md` — the playbook for a change's gaps; `{{SKILLS_ROOT}}/guardlink-annotate-exploitable/SKILL.md` for the evidence bar. guardlink writes both under `.claude/skills/`, and `bravos init` mirrors them into `.agents/skills/` for Codex; otherwise `guardlink annotate --stdout --playbook <id> "<scope>"` prints the same method.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: bravos-templatize
|
|
3
|
+
description: "Turn the threat model's open exposures into correct, testable cxg templates in this session: follow guardlink's own translate method (the generated guardlink-translate skill, else the guardlink_translate MCP tool, else guardlink translate --stdout), validate what you write, keep replay kits apart from scan templates, steer cxg's own probe generation with repo-specific operator guidance, and record classes with no oracle as gaps. Use when asked to write pentest templates, make a finding testable, or prepare a retest kit."
|
|
4
|
+
---
|
|
5
|
+
# Bravos templatize: claims become testable templates, nothing runs
|
|
6
|
+
|
|
7
|
+
## When
|
|
8
|
+
|
|
9
|
+
- The user asks for pentest or cxg templates, or to make an exposure testable.
|
|
10
|
+
- bravos-annotate or bravos-ship handed you new `@exposes` claims with no test.
|
|
11
|
+
- A confirmed threat needs a retest kit before its fix is rechecked.
|
|
12
|
+
|
|
13
|
+
## Do
|
|
14
|
+
|
|
15
|
+
1. Read the open exposures: `guardlink ci -f json` (`exposures[]`, `confirmed[]`) or the MCP tool `guardlink_lookup` with `unmitigated`. Pick the ones a request or a local invocation can show, and leave pure design or process gaps for an `@audit` note. Read the effort: the user's words ("at high"), else a `bravos effort: <level>` line in `AGENTS.md` / `CLAUDE.md`, else medium. It sets a starting budget, not a limit; go beyond it when the work needs it and say why in one line. Starting budget — low: exposures in changed files; medium: the top 5 from `guardlink hypothesis next --json`; high: the top 20; max: every open exposure.
|
|
16
|
+
2. Get the method, in this order. First open `{{SKILLS_ROOT}}/guardlink-translate/SKILL.md` and follow it (`guardlink sync` writes it; if it is missing, say `guardlink sync` would add it). Else call the MCP tool `guardlink_translate` (server `guardlink`) with `prompt: "<scope>"`; it returns the prompt, its output locations and a separate execution step. Else, from a shell, run `guardlink translate --stdout "<scope>"` only if `guardlink translate --help` lists `--stdout`, and follow the prompt it prints. You are the agent: do the work here. If none of the three is available, tell the user this guardlink offers no way to hand the method to the agent already in session (guardlink PR #98 adds `--stdout` back), and stop. Never fall back to a bare `guardlink translate`: it launches Claude Code.
|
|
17
|
+
3. Find the request that reaches each sink before you write its template. With the code graph (its MCP tools, or `bravos graph <question> --json` — same answers; see bravos-codegraph) (the briefing's `Code graph:` line says `current`, or `diverged` until a `no-exec` rebuild, bravos-codegraph step 2): `codegraph_call_chain` from the entry points (`codegraph_entry_points`, `codegraph_routes`) to the claim's symbol gives the route or RPC method, its handler and the parameters that flow into the sink; `codegraph_auth_boundary` on that route gives the identity the request needs (anonymous, a signed-in tenant, admin). Target that route with those parameters and the matching auth profile, put the chain (entry → … → sink) in the template's notes, and when several entry points reach the sink template the least privileged first. When `codegraph_entry_reach` says no entry point reaches it at high confidence, write no template: record "unreachable from any entry point per code graph" in the README and hand the claim to bravos-annotate to re-rate. Read `verdict` and `confidence` first; `unknown` or `insufficient_resolution` is not determined, never unreachable. Without the graph, trace the route from the handler by reading the code and say "code graph unavailable; traced by reading". A SAST finding joined to the claim (`bravos sast candidates`) gives its `route`, `entryPoint` and taint path too; use it alongside the graph's chain, and note it in the template. SAST findings not in the threat model go to bravos-annotate first, since templates come from claims. With no SAST report or SAST unavailable, carry on without it.
|
|
18
|
+
4. Write the templates under `.guardlink/cxg-templates/`, one per threat, following the engine contract and evidence shape the method gives. Keep every check non-destructive and scoped to confirming its one threat. Scanner templates (YAML, Python, shell) go there; JavaScript probes (`async function cxgProbe(cxg)`, `--contract probe`) go under `.guardlink/cxg-replay/` instead, because `cxg scan --template-dir` runs any probe it finds under cxg's default scope, not `.guardlink/scope.yaml`. In a probe, reach the target only through `cxg.fetch*` with well-formed arguments: cxg refuses at run time a probe that uses WebSocket, XMLHttpRequest, EventSource, `navigator.sendBeacon`, dynamic `import()`, or script or `innerHTML` injection, and `cxg template validate` does not catch it.
|
|
19
|
+
5. Validate: `cxg template validate .guardlink/cxg-templates/ --recursive --json`, and the same for `.guardlink/cxg-replay/`. It exits 1 when any template fails; a banner line can precede the JSON, so read from the first `{` or `[`. Fix each error and rerun until it reports none.
|
|
20
|
+
6. Record repo context for cxg's own generator. Copy `{{SKILL_DIR}}/operator-guidance.md` to `.guardlink/operator-guidance.md` if it is not there, and fill each section from the code: framework, auth shape, test identities, known false positives, out-of-bounds routes. bravos-prove passes it as `--skills-file` when cxg generates probes for exposures that have no kit.
|
|
21
|
+
7. For a confirmed threat that needs a retest kit, write a hand-written replay probe under `.guardlink/cxg-replay/`, kept apart from the scan templates. It carries `// @attach_scheme: path`, a `// @requires_identities:` line naming every login it needs (both sides of an IDOR; `anonymous` is the reserved no-login role), and no claim-identity stamps, so `--template-dir .guardlink/cxg-replay/` replays it against the same route. Keep that folder JavaScript probes only (a README is fine): one scanner file (`.yaml`, `.py`, `.sh`) there makes cxg refuse the whole replay run before it starts. cxg reads the folder recursively.
|
|
22
|
+
8. Apply the oracle rule: a vulnerability class cxg cannot judge from a response has no oracle. Record it in `.guardlink/cxg-templates/README.md` as unsupported, with the threat id and why, instead of writing a template that pretends to prove it. An invariant threat (`#inv-…`, from intent.md) maps to the cxg class that can judge it: another user's or tenant's data → `idor`; a role or permission gained → `privilege_escalation`; a field the caller should not set → `mass_assignment`; a check bypassed by concurrent requests → `race_condition`; a workflow step skipped or reordered → `business_logic`. Record each invariant's class in the README.
|
|
23
|
+
9. Commit the templates, the README and the filled guidance together, and hand the template ids and threat ids to bravos-prove.
|
|
24
|
+
|
|
25
|
+
## Never
|
|
26
|
+
|
|
27
|
+
- Run `guardlink translate` without `--stdout`, or with `--headless --agent` or an agent flag: each launches another agent. Use the skill, the MCP tool or `--stdout`.
|
|
28
|
+
- Run a template, a scan or any probe; this skill writes and validates only.
|
|
29
|
+
- Write a destructive check, or one that reaches beyond its single threat.
|
|
30
|
+
- Put `@exposure_*` or `@threat_id` stamps in a hand-written template; cxg stamps claim identity itself.
|
|
31
|
+
- Fake proof for a class with no oracle; record it as unsupported.
|
|
32
|
+
- Write `@accepts`, `@entitles` or `@confirmed`.
|
|
33
|
+
|
|
34
|
+
## Done when
|
|
35
|
+
|
|
36
|
+
- `cxg template validate --recursive --json` reports no errors for `.guardlink/cxg-templates/` and `.guardlink/cxg-replay/`, and the replay folder holds only JavaScript probes.
|
|
37
|
+
- `.guardlink/cxg-templates/README.md` maps each threat to its template, lists unsupported classes with reasons, and lists audit-only items.
|
|
38
|
+
- `.guardlink/operator-guidance.md` is filled from the code, not left as the starter.
|
|
39
|
+
- You told the user that nothing was run, so every claim stays untested.
|
|
40
|
+
|
|
41
|
+
## What it leaves for the next skill
|
|
42
|
+
|
|
43
|
+
- The scan-template and replay-kit directories, the guidance file path, and the template and threat ids, for bravos-prove, with each template's route and auth profile (from the code graph when it was available).
|
|
44
|
+
|
|
45
|
+
## Go deeper
|
|
46
|
+
|
|
47
|
+
- `{{SKILLS_ROOT}}/guardlink-translate/SKILL.md` — the translate method and the engine contract.
|
|
48
|
+
- `{{SKILL_DIR}}/operator-guidance.md` — what cxg's generator needs to know about this repo.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Operator guidance for cxg (this repository)
|
|
2
|
+
|
|
3
|
+
Copy to `.guardlink/operator-guidance.md`, fill in each section from what the code actually does,
|
|
4
|
+
and pass it to `cxg pentest run --skills-file .guardlink/operator-guidance.md`. cxg appends it to
|
|
5
|
+
its template-generation prompt (12k characters at most), so keep it factual and short.
|
|
6
|
+
|
|
7
|
+
## Framework and routing
|
|
8
|
+
<!-- e.g. Express 4 with routers mounted in src/server.ts; Next.js app router under app/api -->
|
|
9
|
+
|
|
10
|
+
## Authentication shape
|
|
11
|
+
<!-- How a session is carried (cookie name, bearer header), where login lives, how roles and tenants are encoded -->
|
|
12
|
+
|
|
13
|
+
## Test identities and fixtures
|
|
14
|
+
<!-- The profiles bravos-env captured and what each may do; seed data a check can rely on -->
|
|
15
|
+
|
|
16
|
+
## Known false positives
|
|
17
|
+
<!-- Responses that look like a finding but are not: a 200 login page on auth failure, a soft 404, an echo endpoint -->
|
|
18
|
+
|
|
19
|
+
## Out of bounds
|
|
20
|
+
<!-- Routes and behaviours no probe may touch, whatever the scope allows -->
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// bravos (managed by bravos init/sync; edits are kept, bravos sync --force replaces them)
|
|
2
|
+
// @comment -- "omp extension: omp runs no Claude Code or Codex hooks, so this does bravos's session start itself. On session_start it runs the repo's .bravos/hooks briefing code in-process (read-only guardlink, git and gh facts), shows the banner as a widget above the editor, and on the first prompt removes the widget and hands the briefing to the model as a hidden custom message. Never throws into omp."
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
|
|
6
|
+
export default function bravos(pi) {
|
|
7
|
+
let pending = null;
|
|
8
|
+
let delivered = false;
|
|
9
|
+
|
|
10
|
+
const load = (cwd) => {
|
|
11
|
+
pending ??= (async () => {
|
|
12
|
+
const lib = await import(pathToFileURL(join(cwd, '.bravos', 'hooks', 'lib', 'briefing.mjs')).href);
|
|
13
|
+
const inputs = await lib.collect(cwd, process.env);
|
|
14
|
+
const banner = process.env.BRAVOS_BANNER === 'off'
|
|
15
|
+
? null
|
|
16
|
+
: lib.buildBanner(inputs, {
|
|
17
|
+
harness: 'omp',
|
|
18
|
+
version: lib.readVersion(join(cwd, '.bravos', 'hooks')),
|
|
19
|
+
color: !process.env.NO_COLOR && process.env.BRAVOS_BANNER_COLOR !== 'off',
|
|
20
|
+
});
|
|
21
|
+
return { briefing: lib.buildBriefing(inputs), banner };
|
|
22
|
+
})().catch((e) => ({ briefing: `bravos briefing failed: ${e instanceof Error ? e.message : String(e)}`, banner: null }));
|
|
23
|
+
return pending;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// omp cuts a widget at 10 lines, so the banner is shown as consecutive widgets of 10 lines each.
|
|
27
|
+
const PER_WIDGET = 10;
|
|
28
|
+
let shown = 0;
|
|
29
|
+
const widget = (ctx, lines) => {
|
|
30
|
+
try {
|
|
31
|
+
if (!ctx?.ui || typeof ctx.ui.setWidget !== 'function') return;
|
|
32
|
+
if (lines === undefined) {
|
|
33
|
+
for (let k = 0; k < shown; k++) ctx.ui.setWidget(`bravos-${k}`, undefined);
|
|
34
|
+
shown = 0;
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
for (let k = 0; k * PER_WIDGET < lines.length; k++) ctx.ui.setWidget(`bravos-${k}`, lines.slice(k * PER_WIDGET, (k + 1) * PER_WIDGET));
|
|
38
|
+
shown = Math.ceil(lines.length / PER_WIDGET);
|
|
39
|
+
} catch {
|
|
40
|
+
/* no UI in this mode */
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const start = (_event, ctx) => {
|
|
45
|
+
pending = null;
|
|
46
|
+
delivered = false;
|
|
47
|
+
// Not awaited: the session opens at once and the banner appears when the facts are in (a few seconds).
|
|
48
|
+
load(ctx.cwd).then(({ banner }) => {
|
|
49
|
+
if (banner && !delivered) widget(ctx, banner.split('\n'));
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
pi.on('session_start', start);
|
|
53
|
+
pi.on('session_switch', start);
|
|
54
|
+
|
|
55
|
+
pi.on('before_agent_start', async (_event, ctx) => {
|
|
56
|
+
if (delivered) return undefined;
|
|
57
|
+
delivered = true;
|
|
58
|
+
const { briefing } = await load(ctx.cwd);
|
|
59
|
+
widget(ctx, undefined);
|
|
60
|
+
return { message: { customType: 'bravos-briefing', content: briefing, display: false } };
|
|
61
|
+
});
|
|
62
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// bravos (managed by bravos init/sync; edits are kept, bravos sync --force replaces them)
|
|
2
|
+
// @comment -- "opencode 2.x plugin (default export { id, setup }): opencode runs no Claude Code or Codex hooks, so this does bravos's session start itself. setup() runs the repo's .bravos/hooks briefing code in-process (read-only guardlink, git and gh facts) and registers session hooks that append the briefing to the system prompt of each model request and of compaction. Never throws into opencode."
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
id: 'bravos',
|
|
8
|
+
setup: async (api) => {
|
|
9
|
+
const root = api?.location?.directory ?? process.cwd();
|
|
10
|
+
let briefing = null;
|
|
11
|
+
const ready = (async () => {
|
|
12
|
+
try {
|
|
13
|
+
const lib = await import(pathToFileURL(join(root, '.bravos', 'hooks', 'lib', 'briefing.mjs')).href);
|
|
14
|
+
briefing = lib.buildBriefing(await lib.collect(root, process.env));
|
|
15
|
+
} catch (e) {
|
|
16
|
+
briefing = `bravos briefing failed: ${e instanceof Error ? e.message : String(e)}`;
|
|
17
|
+
}
|
|
18
|
+
})();
|
|
19
|
+
const add = async (request) => {
|
|
20
|
+
await ready;
|
|
21
|
+
if (briefing && Array.isArray(request?.system)) request.system.push({ type: 'text', text: briefing });
|
|
22
|
+
};
|
|
23
|
+
const handles = [];
|
|
24
|
+
for (const name of ['context', 'compaction']) {
|
|
25
|
+
try {
|
|
26
|
+
handles.push(await api.session.hook(name, add));
|
|
27
|
+
} catch {
|
|
28
|
+
/* this opencode has no such hook */
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return () => Promise.all(handles.map((h) => h?.dispose?.())).then(() => undefined);
|
|
32
|
+
},
|
|
33
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; img-src data:; base-uri 'none'; form-action 'none'">
|
|
7
|
+
<title>__TITLE__</title>
|
|
8
|
+
<!-- bravos (written by bravos report export from report.md; edit the markdown, not this file) -->
|
|
9
|
+
<style>
|
|
10
|
+
:root {
|
|
11
|
+
--green: #33d49d; --navy: #1f3943; --blue: #0360a2; --light: #f0f0f0; --steel: #55899e; --red: #ea1d1d; --slate: #3b6779; --amber: #f0b429;
|
|
12
|
+
--bg: #f6f8f9; --card: #ffffff; --ink: #10222a; --muted: #4c6b78; --line: #d8e2e6; --chip: #e9f2f4; --accent: #0360a2; --code: #eef3f5;
|
|
13
|
+
}
|
|
14
|
+
@media (prefers-color-scheme: dark) {
|
|
15
|
+
:root:not([data-theme="light"]) { --bg: #0e1b21; --card: #15262e; --ink: #f0f0f0; --muted: #8fb0bd; --line: #24404b; --chip: #1f3943; --accent: #33d49d; --code: #10232b; }
|
|
16
|
+
}
|
|
17
|
+
:root[data-theme="dark"] { --bg: #0e1b21; --card: #15262e; --ink: #f0f0f0; --muted: #8fb0bd; --line: #24404b; --chip: #1f3943; --accent: #33d49d; --code: #10232b; }
|
|
18
|
+
* { box-sizing: border-box; }
|
|
19
|
+
body { margin: 0; background: var(--bg); color: var(--ink); font: 15px/1.6 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; }
|
|
20
|
+
header { background: #0e1b21; color: var(--light); padding: 22px 16px; }
|
|
21
|
+
.brand { display: flex; gap: 18px; align-items: center; flex-wrap: wrap; max-width: 1040px; margin: 0 auto; }
|
|
22
|
+
pre.art { margin: 0; font: 8px/1.05 ui-monospace, "SF Mono", Menlo, monospace; white-space: pre; color: var(--green); }
|
|
23
|
+
.brand .meta { font-size: 13px; color: #8fb0bd; }
|
|
24
|
+
.brand .meta b { color: var(--light); font-weight: 600; }
|
|
25
|
+
.layout { max-width: 1040px; margin: 0 auto; padding: 16px; display: grid; grid-template-columns: 220px minmax(0, 1fr); gap: 28px; }
|
|
26
|
+
nav.toc { position: sticky; top: 16px; align-self: start; font-size: 13px; }
|
|
27
|
+
nav.toc .h { text-transform: uppercase; letter-spacing: .06em; color: var(--muted); font-size: 11px; margin: 0 0 8px; }
|
|
28
|
+
nav.toc a { display: block; color: var(--ink); text-decoration: none; padding: 3px 0 3px 8px; border-left: 2px solid var(--line); }
|
|
29
|
+
nav.toc a.l3 { padding-left: 18px; color: var(--muted); font-family: ui-monospace, Menlo, monospace; font-size: 12px; }
|
|
30
|
+
nav.toc a:hover { border-left-color: var(--accent); color: var(--accent); }
|
|
31
|
+
article { min-width: 0; }
|
|
32
|
+
article h1 { font-size: 28px; line-height: 1.25; margin: 8px 0 18px; }
|
|
33
|
+
article h2 { font-size: 20px; margin: 36px 0 10px; padding-bottom: 6px; border-bottom: 2px solid var(--green); }
|
|
34
|
+
article h3 { font-size: 17px; margin: 0 0 10px; }
|
|
35
|
+
article h4 { font-size: 15px; margin: 18px 0 6px; }
|
|
36
|
+
article p { margin: 8px 0; }
|
|
37
|
+
article p.field { margin: 4px 0; }
|
|
38
|
+
article p.field strong:first-child { color: var(--muted); font-weight: 600; }
|
|
39
|
+
section.finding { background: var(--card); border: 1px solid var(--line); border-left: 4px solid var(--blue); border-radius: 10px; padding: 16px 18px; margin: 16px 0; }
|
|
40
|
+
.chip { display: inline-block; padding: 0 8px; border-radius: 99px; font-size: 12px; font-weight: 600; line-height: 20px; vertical-align: baseline; }
|
|
41
|
+
.lv-repro { background: var(--red); color: #fff; }
|
|
42
|
+
.lv-held { background: var(--green); color: #0e1b21; }
|
|
43
|
+
.lv-review { background: var(--blue); color: #fff; }
|
|
44
|
+
.lv-suspect { background: var(--chip); color: var(--muted); border: 1px solid var(--line); }
|
|
45
|
+
.sev-critical, .sev-high { background: var(--red); color: #fff; text-transform: uppercase; }
|
|
46
|
+
.sev-medium { background: var(--amber); color: #0e1b21; text-transform: uppercase; }
|
|
47
|
+
.sev-low, .sev-informational, .sev-info { background: var(--steel); color: #fff; text-transform: uppercase; }
|
|
48
|
+
.eid { font: 12px ui-monospace, Menlo, monospace; background: var(--chip); padding: 0 4px; border-radius: 4px; }
|
|
49
|
+
code { font: 13px ui-monospace, "SF Mono", Menlo, monospace; background: var(--code); padding: 1px 4px; border-radius: 4px; }
|
|
50
|
+
pre { background: var(--code); border: 1px solid var(--line); border-radius: 8px; padding: 10px 12px; overflow-x: auto; }
|
|
51
|
+
pre code { background: none; padding: 0; white-space: pre; }
|
|
52
|
+
pre[data-lang="mermaid"]::before { content: "diagram (mermaid source)"; display: block; font-size: 11px; color: var(--muted); margin-bottom: 6px; }
|
|
53
|
+
blockquote { margin: 10px 0; padding: 4px 14px; border-left: 3px solid var(--steel); color: var(--muted); }
|
|
54
|
+
.table-wrap { overflow-x: auto; margin: 10px 0; }
|
|
55
|
+
table { border-collapse: collapse; width: 100%; font-size: 14px; }
|
|
56
|
+
th, td { border: 1px solid var(--line); padding: 6px 9px; text-align: left; vertical-align: top; }
|
|
57
|
+
th { background: var(--chip); }
|
|
58
|
+
a { color: var(--accent); }
|
|
59
|
+
footer { max-width: 1040px; margin: 0 auto; padding: 16px; color: var(--muted); font-size: 12px; border-top: 1px solid var(--line); }
|
|
60
|
+
@media (max-width: 820px) { .layout { grid-template-columns: 1fr; } nav.toc { position: static; } }
|
|
61
|
+
@media print {
|
|
62
|
+
:root { --bg: #fff; --card: #fff; --ink: #10222a; --muted: #4c6b78; --line: #ccd8dd; --chip: #edf3f5; --code: #f3f6f7; }
|
|
63
|
+
@page { margin: 16mm 14mm; }
|
|
64
|
+
body { font-size: 11pt; }
|
|
65
|
+
header { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
|
66
|
+
.chip, th { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
|
67
|
+
.layout { display: block; padding: 0; }
|
|
68
|
+
nav.toc { display: none; }
|
|
69
|
+
/* Headings stay with what follows; code, tables and short findings are not split. */
|
|
70
|
+
h1, h2, h3, h4 { break-after: avoid; }
|
|
71
|
+
pre, table, blockquote { break-inside: avoid; }
|
|
72
|
+
section.finding { border-left-width: 3px; }
|
|
73
|
+
pre { white-space: pre-wrap; word-break: break-word; }
|
|
74
|
+
a { color: inherit; text-decoration: none; }
|
|
75
|
+
}
|
|
76
|
+
</style>
|
|
77
|
+
</head>
|
|
78
|
+
<body>
|
|
79
|
+
<header><div class="brand"><pre class="art" aria-hidden="true">__ART__</pre><div class="meta">__META__</div></div></header>
|
|
80
|
+
<div class="layout">
|
|
81
|
+
<nav class="toc" aria-label="Contents"><p class="h">Contents</p>__TOC__</nav>
|
|
82
|
+
<article>
|
|
83
|
+
__BODY__
|
|
84
|
+
</article>
|
|
85
|
+
</div>
|
|
86
|
+
<footer>__FOOTER__</footer>
|
|
87
|
+
</body>
|
|
88
|
+
</html>
|