Skip to content

Commit 0ad72f2

Browse files
feat: secure guide + secure feedback (cli-guide-spec + cli-feedback-spec)
Two agent-first CLI spec adoptions from cli-specs.intrane.fr: ## secure guide (cli-guide-spec v1.0) The embedded mental model: model, loop, concepts, commands, examples, gotchas. JSON by default (agent-first), --human for readable markdown. Embedded in the binary — no network fetch. A pure-CLI tool conforms without HTTP endpoints. An agent that lands on a fresh machine with only the binary reads `secure guide` once and drives the tool with no external docs. This is the mental model that --help (the command catalog) cannot be. Body shape per spec §1: one_liner, model, loop, concepts, commands, examples, gotchas, version, see_also. Advertised in --help. ## secure feedback (cli-feedback-spec v1.0) Relay-only feedback: best-effort POST to https://feedback.intrane.fr/v1/feedback, tagged app=machin-secure. Never fails the caller (exit 0 even if relay is down). FEEDBACK_RELAY=off disables. Client-generated idempotency key (8 random bytes hex). Like remotecmd, machin-secure is a peer tool with no local feedback endpoint, so it adopts the relay-only variant of the spec (§3: skip local write, keep relay write). Usage: secure feedback "<message>" [--kind bug|idea|praise|note] [--context "<what you were doing>"] Verified: - guide: valid JSON with all spec fields (one_liner, model, loop, concepts, commands, examples, gotchas, version, see_also) - guide --human: readable markdown rendering - guide --help: usage - feedback: relayed:1 with real relay, relayed:0 with FEEDBACK_RELAY=off - feedback: never fails (exit 0 even when relay unreachable) - feedback: empty message → error exit 1 - --help advertises both subcommands - 1861 findings byte-identical on test/fixtures (no scan regression) Reference implementations: grepapi (machin, JSON agent-skill), remotmd (Go, relay-only feedback). machin-secure is now listed as a conforming tool for both specs. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent be0daec commit 0ad72f2

2 files changed

Lines changed: 222 additions & 0 deletions

File tree

AGENTS.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,17 @@ findings present. Designed to be piped: `./secure --target . | jq 'select(.sever
160160
It never executes target code and never mutates the target repo.
161161
- `rules.json` is the actual product. The scanner is ~440 lines of MFL
162162
(finding engine + hart report generator + verdict store + CLI).
163+
- **`secure guide`** (v2.3.0, cli-guide-spec v1.0): the embedded mental model —
164+
model, loop, concepts, commands, examples, gotchas. JSON by default (agent-first),
165+
`--human` for readable markdown. Embedded in the binary, no network fetch. A pure-CLI
166+
tool conforms without HTTP endpoints. An agent that lands on a fresh machine with only
167+
the binary reads `secure guide` once and drives the tool with no external docs.
168+
- **`secure feedback`** (v2.3.0, cli-feedback-spec v1.0): relay-only feedback —
169+
best-effort POST to `https://feedback.intrane.fr/v1/feedback`, tagged
170+
`app=machin-secure`. Never fails the caller (exit 0 even if the relay is down).
171+
`FEEDBACK_RELAY=off` disables. Client-generated idempotency key (8 random bytes hex).
172+
Like remotecmd, machin-secure is a peer tool with no local feedback endpoint, so it
173+
adopts the relay-only variant of the spec.
163174

164175
## Known limitations / next steps (only build if actually needed)
165176

src/secure.src

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -989,6 +989,199 @@ func cmd_suppress(a) {
989989
exit(0)
990990
}
991991

992+
// ---- `secure guide` subcommand (cli-guide-spec v1.0) ----
993+
// The embedded mental model: model, loop, concepts, commands, examples, gotchas.
994+
// JSON by default (agent-first); --human for readable markdown. Embedded in the
995+
// binary — no network fetch. A pure-CLI tool conforms without HTTP endpoints.
996+
997+
func guide_json() (g) {
998+
g = "{"
999+
g = g + "\"machin-secure\":\"a free, deterministic, agent-first security scanner (1000 rules, 25 languages, SARIF to GitHub)\","
1000+
g = g + "\"one_liner\":\"A regex-based SAST that runs in your CI on every push for free — no per-developer seat tax, no LLM API calls, your code never leaves your runner. The scan loop is deterministic; the judgment (triage, false-positive suppression) is BYOK — your agent or your engineer decides, and the tool persists the verdict.\","
1001+
g = g + "\"model\":{"
1002+
g = g + "\"split\":\"DETERMINISTIC SCAN LOOP (regex rules, no API calls, free, every push) / BYOK JUDGMENT (your agent or engineer triages findings, files verdicts and suppression patterns that persist across scans). The tool never calls an LLM — the CLI is the agent interface.\","
1003+
g = g + "\"byok_llm\":\"the operator IS the LLM. secure emits structured findings (JSONL); YOUR agent reads them, judges them, and writes back verdicts via `secure verdict` / `secure suppress`. No API key, no token billing, no network call to a model provider.\","
1004+
g = g + "\"no_server\":\"pure CLI — no server, no database, no index. The filesystem is scanned fresh every time. The only state that persists is the verdict store and the patterns store (the agent's own memory, not a cache the tool invents).\","
1005+
g = g + "\"sarif\":\"--sarif emits a SARIF 2.1.0 report for GitHub Code Scanning / the Security tab. JSONL is the default for agent consumption; SARIF is for CI integration.\"},"
1006+
g = g + "\"loop\":["
1007+
g = g + "\"secure --target . --diff-base origin/main --pending --context 3 # scan changed files, only unreviewed findings, with ±3 lines of context\","
1008+
g = g + "\"# agent reads the findings JSONL, applies judgment (keep/drop/suppress pattern)\","
1009+
g = g + "\"secure verdict --stdin < batch_of_keep_drop.jsonl # persist individual verdicts\","
1010+
g = g + "\"secure suppress --rule X --glob '*.vue' --reason 'Vue prop bindings' # persist a pattern for repeated false positives\","
1011+
g = g + "\"# next scan: --pending shows only genuinely new findings — suppressed ones stay suppressed\","
1012+
g = g + "\"# for CI: secure --sarif --diff-base origin/main > results.sarif && upload to GitHub Security tab\"],"
1013+
g = g + "\"concepts\":{"
1014+
g = g + "\"finding\":\"a JSONL object: {id, rule, file, line, severity, cwe, message, snippet, verdict, ctx_before, ctx_after}. The id is a stable SHA-256 of (rule|file|trimmed_line) — pass it to `secure verdict`.\","
1015+
g = g + "\"verdict\":\"the agent's judgment on a specific finding id: 'keep' (confirmed real) or 'drop' (false positive). Dropped findings are suppressed in future scans. Stored in .machin-secure.verdicts.json.\","
1016+
g = g + "\"suppress_pattern\":\"a rule-level false-positive filter: (rule, glob) → suppress. Instead of dropping 30 individual findings from the same rule on *.vue files, file one pattern and they stay dropped. Stored in .machin-secure.patterns.json.\","
1017+
g = g + "\"pending\":\"--pending emits only unreviewed findings (verdict == \\\"\\\"), excluding both keep and drop. The agent's work queue — what's new since last triage.\","
1018+
g = g + "\"context\":\"--context N emits ±N lines of code around each finding as ctx_before/ctx_after arrays, so the agent can triage without a read_file round-trip per finding.\","
1019+
g = g + "\"diff_base\":\"--diff-base REF scans only files changed vs REF (git diff --name-only REF...HEAD). For CI/PR-scoped scans — fast feedback on what changed.\","
1020+
g = g + "\"literal_prefilter\":\"each rule has a required literal substring extracted at load time; the scan loop does a cheap contains() before the expensive regex_match. 5.8x speedup on large files, byte-identical findings.\"},"
1021+
g = g + "\"commands\":{"
1022+
g = g + "\"scan\":[\"secure --target . [--rules rules.json]\",\"secure --target . --diff-base origin/main\",\"secure --target . --pending --context 3\",\"secure --target . --sarif > results.sarif\",\"secure --target . --summary\"],"
1023+
g = g + "\"verdict\":[\"secure verdict --target . <id> <keep|drop> [--reason TEXT]\",\"secure verdict --target . --stdin < batch.jsonl\"],"
1024+
g = g + "\"suppress\":[\"secure suppress --target . --rule RULE --glob GLOB [--reason TEXT]\",\"secure suppress --target . --stdin < batch.jsonl\"],"
1025+
g = g + "\"introspection\":[\"secure guide\",\"secure guide --human\",\"secure --help\",\"secure feedback \\\"<message>\\\" [--kind bug|idea|praise|note]\"]},"
1026+
g = g + "\"examples\":["
1027+
g = g + "{\"goal\":\"scan a PR for new findings\",\"do\":[\"secure --target . --diff-base origin/main --pending --context 3\",\"# agent triages the findings\",\"secure verdict --stdin < keep_drop_batch.jsonl\"]},"
1028+
g = g + "{\"goal\":\"suppress repeated false positives from one rule\",\"do\":[\"secure suppress --target . --rule js-hardcoded-secret --glob '*.vue' --reason 'Vue prop bindings, not secrets'\",\"secure --target . --pending # those findings no longer appear\"]},"
1029+
g = g + "{\"goal\":\"CI: SARIF to GitHub Security tab\",\"do\":[\"secure --target . --diff-base origin/main --sarif > results.sarif\",\"# upload results.sarif via github/codeql-action/upload-sarif\"]},"
1030+
g = g + "{\"goal\":\"full audit of a repo\",\"do\":[\"secure --target . --context 3 # all findings, with code context\",\"secure --target . --summary # just the counts\"]}],"
1031+
g = g + "\"gotchas\":["
1032+
g = g + "\"POSIX ERE only — no PCRE features like (?!...) lookahead; they silently no-op\","
1033+
g = g + "\"\\\\d is NOT supported in POSIX ERE (silently fails to match) — use [0-9] instead; \\\\b, \\\\s, \\\\w DO work\","
1034+
g = g + "\"the tool never calls an LLM — the agent does all triage; the CLI is the agent interface (grepapi split)\","
1035+
g = g + "\"verdicts and patterns are per-repo (.machin-secure.verdicts.json / .machin-secure.patterns.json) — commit them to share triage across the team\","
1036+
g = g + "\"--pending takes precedence over --show-all (even with show-all, pending excludes reviewed findings)\","
1037+
g = g + "\"exit codes: 0 = clean, 1 = error, 2 = high/critical findings present\","
1038+
g = g + "\"stdout = data (JSONL/SARIF/summary), stderr = nothing or logs — pipe-friendly\","
1039+
g = g + "\"the scan loop is regex SAST, not taint tracking — use CodeQL for data-flow analysis (it's free for public repos)\"],"
1040+
g = g + "\"version\":\"" + version + "\","
1041+
g = g + "\"see_also\":[\"secure --help\",\"secure feedback --help\"]"
1042+
g = g + "}"
1043+
}
1044+
1045+
func guide_human() (h) {
1046+
h = "# machin-secure — agent-first security scanner\n\n"
1047+
h = h + "A free, deterministic, regex-based SAST: 1000 rules, 25 languages, SARIF to GitHub.\n"
1048+
h = h + "The scan loop is deterministic and free; the judgment is BYOK — your agent decides.\n\n"
1049+
h = h + "## The model\n\n"
1050+
h = h + "- **Deterministic scan loop**: regex rules, no API calls, runs in CI on every push for free.\n"
1051+
h = h + "- **BYOK judgment**: your agent or engineer triages findings, files verdicts and suppression\n"
1052+
h = h + " patterns that persist across scans. The tool never calls an LLM.\n"
1053+
h = h + "- **No server**: pure CLI, no database, no index. Filesystem scanned fresh every time.\n"
1054+
h = h + "- **SARIF**: --sarif for GitHub Code Scanning; JSONL (default) for agent consumption.\n\n"
1055+
h = h + "## The loop\n\n"
1056+
h = h + "1. `secure --target . --diff-base origin/main --pending --context 3`\n"
1057+
h = h + " → only new findings in changed files, each with ±3 lines of context\n"
1058+
h = h + "2. agent reads findings, applies judgment (keep/drop/suppress pattern)\n"
1059+
h = h + "3. `secure verdict --stdin < batch.jsonl` → persist individual verdicts\n"
1060+
h = h + "4. `secure suppress --rule X --glob '*.vue' --reason '...'` → persist a pattern\n"
1061+
h = h + "5. next scan: --pending shows only genuinely new findings\n\n"
1062+
h = h + "## Commands\n\n"
1063+
h = h + "- `secure --target . [flags]` — scan (JSONL by default)\n"
1064+
h = h + "- `secure verdict <id> <keep|drop>` — persist a finding verdict\n"
1065+
h = h + "- `secure suppress --rule R --glob G` — persist a suppression pattern\n"
1066+
h = h + "- `secure guide [--human]` — this guide\n"
1067+
h = h + "- `secure feedback \"<message>\"` — send feedback to the maintainer\n\n"
1068+
h = h + "## Key flags\n\n"
1069+
h = h + "- `--pending` — only unreviewed findings (verdict == \"\")\n"
1070+
h = h + "- `--context N` — ±N lines of code around each finding\n"
1071+
h = h + "- `--diff-base REF` — scan only files changed vs REF\n"
1072+
h = h + "- `--sarif` — SARIF 2.1.0 output for GitHub Security tab\n"
1073+
h = h + "- `--show-all` — include suppressed findings\n\n"
1074+
h = h + "## Gotchas\n\n"
1075+
h = h + "- POSIX ERE only — no `(?!...)` lookahead, no `\\d` (use `[0-9]`)\n"
1076+
h = h + "- The tool never calls an LLM — the agent does all triage\n"
1077+
h = h + "- Verdicts/patterns are per-repo — commit them to share across the team\n"
1078+
h = h + "- Exit codes: 0 = clean, 1 = error, 2 = high/critical findings present\n"
1079+
h = h + "- Regex SAST, not taint tracking — use CodeQL for data-flow analysis\n\n"
1080+
h = h + "Version: " + version + "\n"
1081+
}
1082+
1083+
func cmd_guide(a) {
1084+
want_human := false
1085+
help := false
1086+
i := 2
1087+
for i < len(a) {
1088+
arg := a[i]
1089+
if arg == "--human" { want_human = true }
1090+
if arg == "--help" || arg == "-h" { help = true }
1091+
i = i + 1
1092+
}
1093+
if help {
1094+
println("usage: secure guide [--human]")
1095+
println("")
1096+
println("Prints the embedded agent skill: the model, the loop, concepts, commands,")
1097+
println("examples, and gotchas. JSON by default (agent-first); --human for readable")
1098+
println("markdown. Embedded in the binary — no network fetch.")
1099+
println("")
1100+
println("An agent that lands on a fresh machine with only the binary reads this once")
1101+
println("and drives the tool with no external docs. This is the mental model that")
1102+
println("--help (the command catalog) cannot be.")
1103+
return
1104+
}
1105+
if want_human {
1106+
println(guide_human())
1107+
} else {
1108+
println(guide_json())
1109+
}
1110+
}
1111+
1112+
// ---- `secure feedback` subcommand (cli-feedback-spec v1.0) ----
1113+
// Relay-only adoption (like remotecmd): best-effort POST to the central relay
1114+
// (feedback.intrane.fr), tagged app=machin-secure. Never fails the caller.
1115+
// FEEDBACK_RELAY=off disables. Idempotent on client-generated id.
1116+
1117+
func cmd_feedback(a) {
1118+
msg := ""
1119+
kind := "note"
1120+
ctx := ""
1121+
help := false
1122+
1123+
i := 2
1124+
for i < len(a) {
1125+
arg := a[i]
1126+
if arg == "--kind" { i = i + 1 ; if i < len(a) { kind = a[i] } }
1127+
if arg == "--context" { i = i + 1 ; if i < len(a) { ctx = a[i] } }
1128+
if arg == "--help" || arg == "-h" { help = true }
1129+
if !has_prefix(arg, "-") && msg == "" { msg = arg }
1130+
i = i + 1
1131+
}
1132+
1133+
if help {
1134+
println("usage: secure feedback \"<message>\" [--kind bug|idea|praise|note] [--context \"<what you were doing>\"]")
1135+
println("")
1136+
println("Sends feedback to the central relay (feedback.intrane.fr), tagged")
1137+
println("app=machin-secure. Best-effort — never fails the caller. The relay")
1138+
println("is open for submission, admin-gated for reading.")
1139+
println("")
1140+
println("FEEDBACK_RELAY=off disables the relay write.")
1141+
println("")
1142+
println("This is a relay-only adoption (machin-secure has no local feedback endpoint).")
1143+
return
1144+
}
1145+
1146+
if msg == "" {
1147+
println("{\"ok\":false,\"error\":\"usage: secure feedback \\\"<message>\\\" [--kind bug|idea|praise|note] [--context TEXT]\"}")
1148+
exit(1)
1149+
}
1150+
1151+
// client-generated idempotency key: 8 random bytes hex-encoded
1152+
id := to_hex(rand_bytes(8))
1153+
1154+
reporter := env("USER")
1155+
if reporter == "" { reporter = "agent" }
1156+
1157+
// build the submission JSON body
1158+
body := "{\"message\":" + json(msg)
1159+
body = body + ",\"app\":\"machin-secure\""
1160+
body = body + ",\"version\":\"" + version + "\""
1161+
body = body + ",\"kind\":" + json(kind)
1162+
body = body + ",\"context\":" + json(ctx)
1163+
body = body + ",\"reporter\":" + json(reporter)
1164+
body = body + ",\"id\":" + json(id)
1165+
body = body + "}"
1166+
1167+
relay := env("FEEDBACK_RELAY")
1168+
if relay == "" { relay = "https://feedback.intrane.fr" }
1169+
1170+
relayed := false
1171+
if relay != "off" {
1172+
resp := https_post(relay + "/v1/feedback", body)
1173+
if resp != "" { relayed = true }
1174+
}
1175+
1176+
out := "{\"ok\":true,\"id\":" + json(id) + ",\"relayed\":" + str(bool_to_int(relayed)) + "}"
1177+
println(out)
1178+
}
1179+
1180+
func bool_to_int(b) (n) {
1181+
n = 0
1182+
if b { n = 1 }
1183+
}
1184+
9921185
// ---- arg parsing ----
9931186

9941187
func main() {
@@ -1004,6 +1197,16 @@ func main() {
10041197
return
10051198
}
10061199

1200+
if len(a) > 1 && a[1] == "guide" {
1201+
cmd_guide(a)
1202+
return
1203+
}
1204+
1205+
if len(a) > 1 && a[1] == "feedback" {
1206+
cmd_feedback(a)
1207+
return
1208+
}
1209+
10071210
target := ""
10081211
rules_path := "rules.json"
10091212
want_hart := false
@@ -1047,6 +1250,8 @@ func main() {
10471250
println("usage: secure [OPTIONS] [TARGET]")
10481251
println(" secure verdict --target PATH <id> <keep|drop> [--reason TEXT]")
10491252
println(" secure suppress --target PATH --rule RULE --glob GLOB [--reason TEXT]")
1253+
println(" secure guide [--human]")
1254+
println(" secure feedback \"<message>\" [--kind bug|idea|praise|note] [--context TEXT]")
10501255
println("")
10511256
println("No LLM call is made by this tool. It emits structured findings; the")
10521257
println("calling agent (which already has its own model) reads them, judges them,")
@@ -1085,6 +1290,12 @@ func main() {
10851290
println("output: one JSON finding per line on stdout (JSONL). Each finding has a")
10861291
println("stable \"id\" — pass it to `secure verdict` to keep or drop it.")
10871292
println("exit: 0 = clean, 1 = error, 2 = high/critical findings present")
1293+
println("")
1294+
println("subcommands:")
1295+
println(" secure guide [--human] the embedded agent skill (cli-guide-spec): model,")
1296+
println(" loop, concepts, commands, examples, gotchas")
1297+
println(" secure feedback \"<msg>\" send feedback to the maintainer (cli-feedback-spec)")
1298+
println(" — best-effort, never fails. FEEDBACK_RELAY=off to disable")
10881299
return
10891300
}
10901301

0 commit comments

Comments
 (0)