Skip to content

Commit 6d21228

Browse files
feat: secure telemetry — anonymous usage counts (cli-telemetry-spec v1.0)
The fifth and final agent-first CLI spec. machin-secure now collects honest, opt-out, allow-listed usage counts — disclosed on stderr, inspectable via `secure telemetry`, disabled by default in CI, and silent when off. ## What is sent (allow-list, spec §3.1) tool, version, event (install|run|error), verb, os, arch, exit_class, ts No install_id (omitting is the safer default, §3.3). No identity, no arguments, no file paths, no data — anything not on the allow-list is structurally absent (event.schema.json has additionalProperties: false). ## Disclosure (spec §1) On the first run on a machine, a 4-line notice is printed to stderr: [telemetry] machin-secure sends anonymous usage counts: tool, version, [telemetry] os/arch, which verb ran, and whether it failed. No identity, [telemetry] arguments or data. See `secure telemetry` for the exact payload. [telemetry] Disable: MACHIN_SECURE_TELEMETRY=0 (or DO_NOT_TRACK=1) Never on stdout (stdout is the data contract). Recorded once, not repeated. ## Off-switches (spec §2, checked before any network code) MACHIN_SECURE_TELEMETRY=0 tool-specific opt-out DO_NOT_TRACK=1 cross-vendor convention (MUST honour) secure telemetry --off persisted opt-out CI/GITHUB_ACTIONS/GITLAB_CI/BUILDKITE default disabled in automation ## Transport (spec §5) Single POST over HTTPS, 2s timeout, no retry. Silent on failure — a collector that is down is indistinguishable from one that answered. Never changes exit code, never writes to stdout. Sends after work completes. Endpoint: https://feedback.intrane.fr/v1/telemetry (same relay as feedback, different path). Overridable via MACHIN_SECURE_TELEMETRY_URL. ## secure telemetry (spec §6) Prints the exact next payload from the same code path that sends — inspectable, not trustable. Includes enabled, reason, endpoint, next_payload, disable, notice_shown. Supports --off (persisted opt-out). ## Events (spec §4) install — first run on a machine, once ever (highest-value event) run — a verb completed (exit 0 or 2 = findings, both success) error — a verb exited non-zero (80-119 range, exit_class mapped) At most one event per invocation. No timers, no background schedules. ## Version nudge also respects DO_NOT_TRACK and CI The cli-update-spec nudge now checks DO_NOT_TRACK=1 and CI markers, so DO_NOT_TRACK=1 produces zero outbound connections — verifiable with strace -f -e trace=connect (the stranger test from spec §8). ## Conformance test (spec §8) DO_NOT_TRACK=1 strace ... | grep connect → 0 connects CI=1 strace ... | grep connect → 0 connects MACHIN_SECURE_TELEMETRY=0 → telemetry off (nudge may still fire) secure telemetry → prints real next_payload secure telemetry --off → persisted, survives runs first run → 4-line notice on stderr, install event second run → no notice, run event error (bad target) → error event, exit_class 90 Verified: - telemetry --help: no notice (help is not usage) - telemetry status: enabled, correct next_payload, endpoint, notice_shown - DO_NOT_TRACK=1: 0 outbound connects (strace verified) - CI=1: 0 outbound connects - MACHIN_SECURE_TELEMETRY=0: disabled, reason shown - telemetry --off: persisted, reason "disabled by config" - first run: [telemetry] notice on stderr, stdout pure JSONL (1861 findings) - second run: no notice - install event sent on first run (mock server) - run event sent on second run (mock server) - error event sent on bad target (exit_class 90, mock server) - scan: 1861 findings, byte-identical - guide: telemetry in gotchas + introspection - help-json: telemetry command + env vars - --help: telemetry in usage + subcommands Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent ad93119 commit 6d21228

3 files changed

Lines changed: 272 additions & 4 deletions

File tree

AGENTS.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,19 @@ findings present. Designed to be piped: `./secure --target . | jq 'select(.sever
196196
never blocks. `MACHIN_SECURE_NO_NUDGE=1` disables it. The release workflow
197197
publishes `version.json` (with `sha256[:12]` of the tarball) to both the version
198198
tag and the moving `v2` tag release.
199+
- **`secure telemetry`** (v2.5.0, cli-telemetry-spec v1.0): honest, opt-out,
200+
allow-listed usage counts. Sends `tool`, `version`, `event` (install/run/error),
201+
`verb`, `os`, `arch`, `exit_class`, `ts` — no identity, no arguments, no data.
202+
Disclosed on stderr before the first send (4-line notice, once per machine).
203+
Disabled by default in CI (`CI`, `GITHUB_ACTIONS`, `GITLAB_CI`, `BUILDKITE`).
204+
Off-switches (checked before any network code): `MACHIN_SECURE_TELEMETRY=0`,
205+
`DO_NOT_TRACK=1`, `secure telemetry --off` (persisted). Endpoint overridable
206+
via `MACHIN_SECURE_TELEMETRY_URL` (default: `https://feedback.intrane.fr/v1/telemetry`).
207+
`secure telemetry` prints the exact next payload (from the same code path that
208+
sends) — inspectable, not trustable. Bounded: 2s timeout, no retry, silent on
209+
failure, never changes exit code. No `install_id` (omitting is the safer default).
210+
The version nudge also respects `DO_NOT_TRACK` and CI markers, so `DO_NOT_TRACK=1`
211+
produces zero outbound connections (verifiable with `strace -f -e trace=connect`).
199212

200213
## Known limitations / next steps (only build if actually needed)
201214

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,26 @@ none of the infrastructure, cost, or attack surface.
4040
contract) pointing the agent at [hart.intrane.fr](https://hart.intrane.fr)
4141
the agent authors and publishes its own HTML report with its own model.
4242

43+
## Telemetry
44+
45+
machin-secure collects **anonymous usage counts** — tool, version, which verb
46+
ran, os/arch, and whether it failed. No identity, no arguments, no file paths,
47+
no data. The payload is an allow-list (`event.schema.json`); anything not on it
48+
is structurally absent.
49+
50+
- **Disclosed on stderr** before the first send. `secure telemetry` prints the
51+
exact next payload — inspectable, not trustable.
52+
- **Disabled by default in CI** (`CI`, `GITHUB_ACTIONS`, `GITLAB_CI`,
53+
`BUILDKITE`).
54+
- **Off-switches:** `MACHIN_SECURE_TELEMETRY=0`, `DO_NOT_TRACK=1`,
55+
`secure telemetry --off` (persisted).
56+
- **Endpoint override:** `MACHIN_SECURE_TELEMETRY_URL` (default:
57+
`https://feedback.intrane.fr/v1/telemetry`).
58+
- **Bounded:** 2s timeout, no retry, silent on failure, never changes exit code.
59+
60+
See [cli-telemetry-spec](https://github.com/javimosch/cli-telemetry-spec) for
61+
the full protocol.
62+
4363
## Usage
4464

4565
```sh

src/secure.src

Lines changed: 239 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ func fail(code, typ, msg, suggestion) {
114114
if rec { rec_s = "true" }
115115
body := "{\"ok\":false,\"error\":{\"code\":" + str(code) + ",\"type\":" + json(typ) + ",\"message\":" + json(msg) + ",\"recoverable\":" + rec_s + ",\"suggestions\":" + sug + "}}"
116116
println(body)
117+
tel_finish(code)
117118
exit(code)
118119
}
119120

@@ -1041,7 +1042,7 @@ func guide_json() (g) {
10411042
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\"],"
10421043
g = g + "\"verdict\":[\"secure verdict --target . <id> <keep|drop> [--reason TEXT]\",\"secure verdict --target . --stdin < batch.jsonl\"],"
10431044
g = g + "\"suppress\":[\"secure suppress --target . --rule RULE --glob GLOB [--reason TEXT]\",\"secure suppress --target . --stdin < batch.jsonl\"],"
1044-
g = g + "\"introspection\":[\"secure guide\",\"secure guide --human\",\"secure --help\",\"secure feedback \\\"<message>\\\" [--kind bug|idea|praise|note]\"]},"
1045+
g = g + "\"introspection\":[\"secure guide\",\"secure guide --human\",\"secure --help\",\"secure help-json\",\"secure telemetry\",\"secure update --check\",\"secure feedback \\\"<message>\\\" [--kind bug|idea|praise|note]\"]},"
10451046
g = g + "\"examples\":["
10461047
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\"]},"
10471048
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\"]},"
@@ -1055,7 +1056,8 @@ func guide_json() (g) {
10551056
g = g + "\"--pending takes precedence over --show-all (even with show-all, pending excludes reviewed findings)\","
10561057
g = g + "\"exit codes: 0 = clean, 1 = error, 2 = high/critical findings present\","
10571058
g = g + "\"stdout = data (JSONL/SARIF/summary), stderr = nothing or logs — pipe-friendly\","
1058-
g = g + "\"the scan loop is regex SAST, not taint tracking — use CodeQL for data-flow analysis (it's free for public repos)\"],"
1059+
g = g + "\"the scan loop is regex SAST, not taint tracking — use CodeQL for data-flow analysis (it's free for public repos)\","
1060+
g = g + "\"telemetry: anonymous usage counts (tool, version, verb, os/arch, exit_class) — no identity, args or data. Disable: MACHIN_SECURE_TELEMETRY=0 or DO_NOT_TRACK=1. Inspect: secure telemetry. Disabled in CI by default\"],"
10591061
g = g + "\"version\":\"" + version + "\","
10601062
g = g + "\"see_also\":[\"secure --help\",\"secure feedback --help\"]"
10611063
g = g + "}"
@@ -1215,6 +1217,7 @@ func cmd_help_json(a) {
12151217
h = h + "\"guide\":{\"args\":[],\"flags\":[\"--human\"],\"auth\":false},"
12161218
h = h + "\"feedback\":{\"args\":[\"<message>\"],\"flags\":[\"--kind bug|idea|praise|note\",\"--context TEXT\"],\"auth\":false},"
12171219
h = h + "\"update\":{\"args\":[],\"flags\":[\"--check\",\"--force\"],\"auth\":false},"
1220+
h = h + "\"telemetry\":{\"args\":[],\"flags\":[\"--off\"],\"auth\":false},"
12181221
h = h + "\"help-json\":{\"args\":[],\"flags\":[],\"auth\":false}"
12191222
h = h + "},"
12201223
h = h + "\"exit_codes\":{"
@@ -1227,7 +1230,7 @@ func cmd_help_json(a) {
12271230
h = h + "\"100\":\"external/integration error (relay unreachable)\","
12281231
h = h + "\"110\":\"internal error / bug\""
12291232
h = h + "},"
1230-
h = h + "\"env\":[\"FEEDBACK_RELAY\",\"USER\",\"MACHIN_SECURE_NO_NUDGE\"],"
1233+
h = h + "\"env\":[\"FEEDBACK_RELAY\",\"USER\",\"MACHIN_SECURE_NO_NUDGE\",\"MACHIN_SECURE_TELEMETRY\",\"MACHIN_SECURE_TELEMETRY_URL\",\"DO_NOT_TRACK\"],"
12311234
h = h + "\"see_also\":[\"secure guide\",\"secure --help\"]"
12321235
h = h + "}"
12331236
println(h)
@@ -1339,6 +1342,7 @@ func cmd_update(a) {
13391342

13401343
if want_check {
13411344
println("{\"ok\":true,\"updated\":false,\"local\":" + json(local_ver) + ",\"remote\":" + json(remote_ver) + ",\"up_to_date\":false}")
1345+
tel_finish(5)
13421346
exit(5)
13431347
}
13441348

@@ -1450,6 +1454,12 @@ func strip_quotes(s) (r) {
14501454

14511455
func version_nudge() {
14521456
if env("MACHIN_SECURE_NO_NUDGE") == "1" { return }
1457+
if env("DO_NOT_TRACK") == "1" { return }
1458+
// CI is not a user — don't nudge in automation
1459+
if env("CI") != "" { return }
1460+
if env("GITHUB_ACTIONS") != "" { return }
1461+
if env("GITLAB_CI") != "" { return }
1462+
if env("BUILDKITE") != "" { return }
14531463

14541464
// throttle: check at most once per hour
14551465
home := env("HOME")
@@ -1500,38 +1510,258 @@ func quote_for_shell(s) (r) {
15001510
return r
15011511
}
15021512

1513+
// ---- `secure telemetry` subcommand (cli-telemetry-spec v1.0) ----
1514+
// Honest, opt-out, allow-listed usage counts. No identity, no arguments, no data.
1515+
// Disclosed on stderr before the first send. Disabled by default in CI.
1516+
// Inspectable: `secure telemetry` prints the exact next payload.
1517+
1518+
var telemetry_endpoint = "https://feedback.intrane.fr/v1/telemetry"
1519+
var current_verb = "scan"
1520+
1521+
// tel_config_dir returns the tool's config directory (~/.machin-secure).
1522+
func tel_config_dir() (d) {
1523+
home := env("HOME")
1524+
if home == "" { home = "/tmp" }
1525+
d = home + "/.machin-secure"
1526+
mkdir(d)
1527+
return d
1528+
}
1529+
1530+
// tel_enabled returns true if telemetry is enabled, false if any off-switch is set.
1531+
// Checks ALL switches before any network code (spec §2.1).
1532+
func tel_enabled() (ok) {
1533+
ok = false
1534+
// DO_NOT_TRACK=1 (cross-vendor convention, MUST honour)
1535+
dnt := env("DO_NOT_TRACK")
1536+
if dnt == "1" { return false }
1537+
// MACHIN_SECURE_TELEMETRY=0/false/off/no
1538+
tel := env("MACHIN_SECURE_TELEMETRY")
1539+
if tel == "0" || tel == "false" || tel == "off" || tel == "no" { return false }
1540+
// persisted off flag
1541+
cfg := tel_config_dir()
1542+
off_kind, _, _ := stat(cfg + "/telemetry-off")
1543+
if off_kind == 1 { return false }
1544+
// CI is not a user — default disabled in automation (spec §2.2)
1545+
if env("CI") != "" { return false }
1546+
if env("CONTINUOUS_INTEGRATION") != "" { return false }
1547+
if env("GITHUB_ACTIONS") != "" { return false }
1548+
if env("GITLAB_CI") != "" { return false }
1549+
if env("BUILDKITE") != "" { return false }
1550+
return true
1551+
}
1552+
1553+
// tel_notice prints the 4-line disclosure to stderr, once per machine (spec §1).
1554+
// Uses system("printf ... >&2") because MFL has no stderr builtin.
1555+
func tel_notice() {
1556+
cfg := tel_config_dir()
1557+
notice_file := cfg + "/telemetry-notice-shown"
1558+
nk, _, _ := stat(notice_file)
1559+
if nk == 1 { return }
1560+
// print 4-line notice to stderr
1561+
system("printf '%s\\n' '[telemetry] machin-secure sends anonymous usage counts: tool, version,' '[telemetry] os/arch, which verb ran, and whether it failed. No identity,' '[telemetry] arguments or data. See `secure telemetry` for the exact payload.' '[telemetry] Disable: MACHIN_SECURE_TELEMETRY=0 (or DO_NOT_TRACK=1)' >&2")
1562+
write_file(notice_file, "1")
1563+
}
1564+
1565+
// tel_payload builds the JSON payload from the allow-list (spec §3.1).
1566+
// No install_id — omitting it is the safer default (spec §3.3).
1567+
func tel_payload(event, verb, exit_class) (p) {
1568+
_, os_raw, _ := exec("uname -s")
1569+
os_raw = trim(os_raw)
1570+
os_val := to_lower(os_raw)
1571+
_, arch_raw, _ := exec("uname -m")
1572+
arch_val := trim(arch_raw)
1573+
_, ts_raw, _ := exec("date -u +%Y-%m-%dT%H:%M:%SZ")
1574+
ts_val := trim(ts_raw)
1575+
p = "{\"tool\":\"machin-secure\",\"version\":\"" + version + "\""
1576+
p = p + ",\"event\":" + json(event)
1577+
p = p + ",\"verb\":" + json(verb)
1578+
p = p + ",\"os\":" + json(os_val)
1579+
p = p + ",\"arch\":" + json(arch_val)
1580+
p = p + ",\"exit_class\":" + str(exit_class)
1581+
p = p + ",\"ts\":" + json(ts_val)
1582+
p = p + "}"
1583+
return p
1584+
}
1585+
1586+
// tel_send sends one event. Bounded (2s timeout), silent, no retry (spec §5).
1587+
// Sends after work completes. A failed send is silent and never changes exit code.
1588+
func tel_send(event, verb, exit_class) {
1589+
if tel_enabled() == false { return }
1590+
tel_notice()
1591+
// check if this is the first run → send "install" instead of "run"/"error"
1592+
cfg := tel_config_dir()
1593+
install_file := cfg + "/telemetry-install-sent"
1594+
ik, _, _ := stat(install_file)
1595+
actual_event := event
1596+
if ik != 1 {
1597+
actual_event = "install"
1598+
write_file(install_file, "1")
1599+
}
1600+
payload := tel_payload(actual_event, verb, exit_class)
1601+
// write payload to temp file, POST via curl with 2s timeout, silent
1602+
tmp := "/tmp/ms-tel-" + to_hex(rand_bytes(4)) + ".json"
1603+
write_file(tmp, payload)
1604+
endpoint := env("MACHIN_SECURE_TELEMETRY_URL")
1605+
if endpoint == "" { endpoint = telemetry_endpoint }
1606+
system("curl -s -m 2 -X POST " + endpoint + " -H 'content-type: application/json' --data-binary @" + tmp + " >/dev/null 2>&1 || true")
1607+
remove(tmp)
1608+
}
1609+
1610+
// tel_exit_class maps an exit code to the semantic exit-code CLASS (spec §3.1).
1611+
// 0 and 2 (findings present) → 0 (success). 80-89→80, 90-99→90, 100-109→100, 110+→110.
1612+
func tel_exit_class(code) (c) {
1613+
c = 0
1614+
if code == 0 { return 0 }
1615+
if code == 2 { return 0 }
1616+
if code == 5 { return 0 }
1617+
if code >= 80 && code < 90 { return 80 }
1618+
if code >= 90 && code < 100 { return 90 }
1619+
if code >= 100 && code < 110 { return 100 }
1620+
if code >= 110 { return 110 }
1621+
return 0
1622+
}
1623+
1624+
// tel_is_error returns true if the exit code represents an error (not success/findings).
1625+
func tel_is_error(code) (e) {
1626+
e = false
1627+
if code == 0 { return false }
1628+
if code == 2 { return false }
1629+
if code == 5 { return false }
1630+
return true
1631+
}
1632+
1633+
// tel_finish sends the appropriate event before exiting. Called at every exit point.
1634+
func tel_finish(code) {
1635+
if tel_enabled() == false { return }
1636+
ec := tel_exit_class(code)
1637+
if tel_is_error(code) {
1638+
tel_send("error", current_verb, ec)
1639+
} else {
1640+
tel_send("run", current_verb, ec)
1641+
}
1642+
}
1643+
1644+
// tel_why_disabled returns the reason telemetry is off (spec §6.3).
1645+
func tel_why_disabled() (r) {
1646+
dnt := env("DO_NOT_TRACK")
1647+
if dnt == "1" { return "DO_NOT_TRACK=1" }
1648+
tel := env("MACHIN_SECURE_TELEMETRY")
1649+
if tel == "0" || tel == "false" || tel == "off" || tel == "no" { return "MACHIN_SECURE_TELEMETRY=" + tel }
1650+
cfg := tel_config_dir()
1651+
off_kind, _, _ := stat(cfg + "/telemetry-off")
1652+
if off_kind == 1 { return "disabled by config (secure telemetry --off)" }
1653+
if env("CI") != "" { return "CI detected" }
1654+
if env("CONTINUOUS_INTEGRATION") != "" { return "CI detected" }
1655+
if env("GITHUB_ACTIONS") != "" { return "CI detected" }
1656+
if env("GITLAB_CI") != "" { return "CI detected" }
1657+
if env("BUILDKITE") != "" { return "CI detected" }
1658+
return "unknown"
1659+
}
1660+
1661+
func cmd_telemetry(a) {
1662+
// parse flags
1663+
i := 2
1664+
for i < len(a) {
1665+
arg := a[i]
1666+
if arg == "--off" {
1667+
cfg := tel_config_dir()
1668+
write_file(cfg + "/telemetry-off", "1")
1669+
println("{\"ok\":true,\"data\":{\"enabled\":false,\"reason\":\"disabled by config\"}}")
1670+
return
1671+
}
1672+
if arg == "--help" || arg == "-h" {
1673+
println("usage: secure telemetry [--off]")
1674+
println("")
1675+
println("Print the exact telemetry payload this tool would send, the endpoint,")
1676+
println("and whether it is enabled. No identity, arguments or data are sent —")
1677+
println("only: tool, version, event, verb, os, arch, exit_class, ts.")
1678+
println("")
1679+
println(" --off persist the opt-out (survives future runs)")
1680+
println("")
1681+
println("Disable via env: MACHIN_SECURE_TELEMETRY=0 or DO_NOT_TRACK=1")
1682+
println("Override endpoint: MACHIN_SECURE_TELEMETRY_URL=https://your-collector/e")
1683+
return
1684+
}
1685+
i = i + 1
1686+
}
1687+
1688+
enabled := tel_enabled()
1689+
endpoint := env("MACHIN_SECURE_TELEMETRY_URL")
1690+
if endpoint == "" { endpoint = telemetry_endpoint }
1691+
1692+
// check if notice has been shown
1693+
cfg := tel_config_dir()
1694+
nk, _, _ := stat(cfg + "/telemetry-notice-shown")
1695+
notice_shown := false
1696+
if nk == 1 { notice_shown = true }
1697+
1698+
// build the actual next payload from the same code path that sends
1699+
next_payload := tel_payload("run", "telemetry", 0)
1700+
1701+
en_s := "false"
1702+
reason := tel_why_disabled()
1703+
if enabled { en_s = "true"; reason = "enabled" }
1704+
1705+
out := "{\"ok\":true,\"data\":{\"enabled\":" + en_s
1706+
out = out + ",\"reason\":" + json(reason)
1707+
out = out + ",\"endpoint\":" + json(endpoint)
1708+
out = out + ",\"next_payload\":" + next_payload
1709+
out = out + ",\"disable\":\"MACHIN_SECURE_TELEMETRY=0\""
1710+
out = out + ",\"notice_shown\":" + str(bool_to_int(notice_shown))
1711+
out = out + "}}"
1712+
println(out)
1713+
}
1714+
15031715
// ---- arg parsing ----
15041716

15051717
func main() {
15061718
a := args()
15071719

15081720
if len(a) > 1 && a[1] == "verdict" {
1721+
current_verb = "verdict"
15091722
cmd_verdict(a)
1723+
tel_finish(0)
15101724
return
15111725
}
15121726

15131727
if len(a) > 1 && a[1] == "suppress" {
1728+
current_verb = "suppress"
15141729
cmd_suppress(a)
1730+
tel_finish(0)
15151731
return
15161732
}
15171733

15181734
if len(a) > 1 && a[1] == "guide" {
1735+
current_verb = "guide"
15191736
cmd_guide(a)
1737+
tel_finish(0)
15201738
return
15211739
}
15221740

15231741
if len(a) > 1 && a[1] == "feedback" {
1742+
current_verb = "feedback"
15241743
cmd_feedback(a)
1744+
tel_finish(0)
15251745
return
15261746
}
15271747

15281748
if len(a) > 1 && (a[1] == "help-json" || a[1] == "--help-json") {
1749+
current_verb = "help-json"
15291750
cmd_help_json(a)
1751+
tel_finish(0)
15301752
return
15311753
}
15321754

15331755
if len(a) > 1 && a[1] == "update" {
1756+
current_verb = "update"
15341757
cmd_update(a)
1758+
tel_finish(0)
1759+
return
1760+
}
1761+
1762+
if len(a) > 1 && a[1] == "telemetry" {
1763+
current_verb = "telemetry"
1764+
cmd_telemetry(a)
15351765
return
15361766
}
15371767

@@ -1586,6 +1816,7 @@ func main() {
15861816
println(" secure feedback \"<message>\" [--kind bug|idea|praise|note] [--context TEXT]")
15871817
println(" secure help-json")
15881818
println(" secure update [--check] [--force]")
1819+
println(" secure telemetry [--off]")
15891820
println("")
15901821
println("No LLM call is made by this tool. It emits structured findings; the")
15911822
println("calling agent (which already has its own model) reads them, judges them,")
@@ -1633,6 +1864,9 @@ func main() {
16331864
println(" secure help-json machine-readable command catalog (cli-output-spec §4)")
16341865
println(" secure update [--check] content-hash self-update (cli-update-spec)")
16351866
println(" — verify-then-swap, .bak rollback, no silent auto-update")
1867+
println(" secure telemetry [--off] anonymous usage counts (cli-telemetry-spec)")
1868+
println(" — opt-out, allow-listed, disclosed on stderr. Disable:")
1869+
println(" MACHIN_SECURE_TELEMETRY=0 or DO_NOT_TRACK=1")
16361870
println(" secure feedback \"<msg>\" send feedback to the maintainer (cli-feedback-spec)")
16371871
println(" — best-effort, never fails. FEEDBACK_RELAY=off to disable")
16381872
return
@@ -1690,6 +1924,7 @@ func main() {
16901924
for _, f := range run.findings {
16911925
if f.severity == "critical" || f.severity == "high" { has_high = true }
16921926
}
1693-
if has_high { exit(2) }
1927+
if has_high { tel_finish(2); exit(2) }
1928+
tel_finish(0)
16941929
exit(0)
16951930
}

0 commit comments

Comments
 (0)