Skip to content

Releases: xunholy/promptzero

v0.799.0

Choose a tag to compare

@github-actions github-actions released this 08 Jul 01:04
v0.799.0
de88358

v0.799.0 — wigle_upload: authenticated wardrive-CSV upload to wigle.net

Added (capability — roadmap P2-26, upload half)

  • wigle_upload POSTs a WigleWifi-1.4 wardrive CSV (the output of
    wigle_wardrive_export) to the WiGLE v2 file-upload API. This is PromptZero's
    first outward-network-egress tool, so it is deliberately conservative and
    double-gated:

    • Gate 1 (config opt-in): wigle.upload_enabled defaults false; the tool
      refuses until the operator arms it — the egress capability is off out of
      the box.
    • Gate 2 (dispatch): classified risk.High, so every call goes through the
      confirmation + audit gates (over MCP it also requires
      PROMPTZERO_MCP_ALLOW_HIGH).
      Credentials (WiGLE API name + token) come from wigle.api_name/api_token or the
      WIGLE_API_NAME / WIGLE_API_TOKEN env vars — never from tool arguments, and
      never echoed into results or error messages.
  • Client (internal/wigle.Client): multipart/form-data POST, HTTP Basic auth
    (API name = user, token = password — WiGLE's real auth; not OAuth), distinct
    401 (auth) and 429 (rate-limit) reporting, success:false surfaced as an error,
    request (32 MiB) and response (1 MiB) bounded, donate opt-in (off by default).
    The endpoint is overridable so the whole test suite runs against an httptest
    mock — no live egress occurs during development or CI.

Out of scope (P2-26 remainder): GPS auto-stamping (flipper_gps_fix + Marauder
scan stamping) is hardware-dependent and still open.

Verified

  • task ci green: golangci-lint 0 issues, go vet, build, task test:full (full
    suite, -race) 0 fail, govulncheck clean. task eval 17/17. All five release
    cross-compile targets build green.
  • 13 offline tests (7 client + 6 handler): success/auth/multipart/donate, 401,
    429, success:false, malformed body, input guards; disabled-by-default and
    missing-credential refusals, and a token-not-leaked assertion on the result.
  • Gated-tool guardrails updated: register(High), registry-size pin 702->703,
    gatedTools golden set; classify==spec and risk-coverage invariants hold.

v0.798.0

Choose a tag to compare

@github-actions github-actions released this 01 Jul 06:13
v0.798.0
d1cd41c

v0.798.0 — web-share: carry the token in the URL fragment, not a query string

Fixed (security + functional — --web-share)

  • --web-share printed the bearer token in a query string
    (http://host:port/?token=). Two problems:

    1. Secret leak. A ?token= URL lands in the server access log, any
      reverse-proxy log, browser history, and the Referer header of any outbound
      request the page makes — exactly the leakage the rest of the codebase
      avoids (/ws refuses query-string tokens; the client scrubs the token from
      history after reading it). --web-share reintroduced it.

    2. The shared URL didn't work. The web client's authBootstrap reads the token
      only from the URL FRAGMENT (#token=), never the query string, so a teammate
      opening the ?token= link was dropped at a manual token prompt.

    Both fixed by carrying the token in the fragment (#token=): a fragment is never
    sent to the server (no log/proxy/Referer leak), is scrubbed from history by the
    existing client code, and is the form the client actually parses. Extracted
    into webShareURL so the invariant is unit-tested.

Found via the internal audit sweep of the web session/token lifecycle. The rest
verified sound: constant-time token compare (subtle.ConstantTimeCompare),
operator-supplied token, crypto/rand correlation ids, gated tool dispatch via
the agent RunTool, no CSRF-via-GET, no cookies.

Verified

  • task ci green: golangci-lint 0 issues, go vet, build, task test:full (full
    suite, -race) 0 fail, govulncheck clean. task eval 17/17. All five release
    cross-compile targets build green.
  • TestWebShareURL_UsesFragmentNotQuery: asserts #token= and never ?token=.

v0.797.0

Choose a tag to compare

@github-actions github-actions released this 01 Jul 06:13
v0.797.0
137637d

v0.797.0 — audit: VerifyChain reports a poisoned success value as a chain break

Fixed (safety — audit tamper-evidence robustness)

  • VerifyChain scanned the success column into a bare Go bool. Legitimate rows
    always store 0/1 (schema DEFAULT 1), but success and entry_hash are
    independent columns an attacker with direct SQLite write can set separately.
    Setting success to NULL or an out-of-range integer (e.g. 2) makes rows.Scan
    fail ("couldn't convert 2 into type bool"), so VerifyChain returns an error
    and ABORTS the entire walk instead of the designed "chain broken at row N"
    verdict. A single UPDATE ... SET success=NULL turns the forensic integrity
    check into a self-inflicted DoS.

    This is fail-loud, not fail-open — it can never read a tampered log as
    Valid=true — but it degrades tamper-evidence: an operator gets an opaque type
    error instead of a pinpointed break, and the verifier can't report which rows
    are broken. Fix: scan success as sql.NullInt64 (matching the sibling
    duration_ms) and feed success.Int64 == 1 into chainHash. Behaviour-preserving
    for legitimate 0/1 rows; any poisoned value now flows into the hash recompute,
    fails the comparison, and is correctly reported as a chain break
    (Valid=false, FirstBrokenID=id).

Found via the internal audit sweep of the query DSL + hash chain. The rest
verified sound: chain pre-image injectivity (length-prefixed fields), verify
recomputes rather than trusting the stored hash, reorder/deletion/truncation +
CheckpointAnchor detection, fully-parameterized WHERE builder, capped LIMIT,
writeMu + flock single-writer, CSV formula-injection guard.

Verified

  • task ci green: golangci-lint 0 issues, go vet, build, task test:full (full
    suite, -race) 0 fail, govulncheck clean. task eval 17/17. All five release
    cross-compile targets build green.
  • TestVerifyChain_PoisonedSuccessReportsBreak (success=2 and success=NULL):
    reports a break at the poisoned row without erroring; proven to fail pre-fix
    (both aborted with a bool scan error).

Follow-up (LOW, banked): Query/QueryFiltered share the bare-bool scan, where a
NULL-success row is silently dropped from results; barely reachable, tracked
separately.

v0.796.0

Choose a tag to compare

@github-actions github-actions released this 01 Jul 03:04
v0.796.0
5cd6ba9

v0.796.0 — rules: don't consume cooldown when a fire dispatched nothing

Fixed (correctness — reactive rules engine)

  • Engine.Handle bumped lastFire and fires for a matched rule BEFORE dispatching
    its actions. When the rule's only action was an ActionTool dropped because the
    global in-flight cap (maxToolActions=8) was saturated, nothing actually ran —
    yet the rule was recorded as fired and its cooldown clock started. The real
    damage is the cooldown: the NEXT genuinely-matching event inside the window is
    then wrongly suppressed, so a saturation blip silently swallows a real
    trigger. /rules list also over-reported Fires/LastFire.

    Handle now records the fire optimistically under the lock (so a concurrent
    Handle for the same rule is still cooldown-suppressed) and rolls it back when
    fire() reports nothing dispatched: fires-- and lastFire restored to its prior
    value (or deleted). The restore is guarded by an Equal(bump) check so a
    concurrent successful fire that legitimately advanced lastFire is never
    clobbered. fire() now returns whether at least one action dispatched (a
    webhook/log ran, or a tool slot was reserved); a missing dispatcher, unknown
    kind, or saturation-drop counts as not-dispatched.

  • Negative Rule.Cooldown is clamped to 0 at Register (with a warn), so an
    operator sign typo can't slip past the Cooldown > 0 suppression guard and
    silently disable rate-limiting.

Found via the internal audit sweep of the automation engines (v0.795.0 fixed the
campaign fail-open from the same sweep; this clears the two banked rules items).

Verified

  • task ci green: golangci-lint 0 issues, go vet, build, task test:full (full
    suite, -race) 0 fail, govulncheck clean. task eval 17/17. All five release
    cross-compile targets build green.
  • TestEngine_SaturationDropDoesNotConsumeCooldown (-race): saturate the cap,
    fire a tool-only rule, assert Fires==0 / LastFire unset; free the cap and
    assert the same event now fires. Proven to fail pre-fix (the drop consumed the
    cooldown and suppressed the next event). TestEngine_NegativeCooldownClampedToZero.

v0.795.0

Choose a tag to compare

@github-actions github-actions released this 01 Jul 00:06
v0.795.0
278dd04

v0.795.0 — campaign: reject an inert when-gate (fail-open fix)

Fixed (safety — campaign conditional gate fails open)

  • A campaign step's when clause is evaluated against its predecessor's output,
    and that evaluation lives entirely inside the Runner's if step.DependsOn != ""
    block. A step authored with a when: but no depends_on: never reaches the
    when check — the Runner runs its tool UNCONDITIONALLY. when is exactly how an
    operator makes a destructive tool conditional (e.g. when: contains "AUTHORIZED"), so a missing depends_on turns an intended gate into a silently-
    inert one: a fail-open.

    ParseYAML now rejects When != "" && DependsOn == "" at load, matching the
    package's validate-at-parse philosophy (duplicate ids, unresolved / self /
    forward depends_on, cycles, and malformed/negative timeouts already fail at
    load). A when with no predecessor has no output to evaluate against, so it is
    meaningless as well as unsafe. ParseYAML is the sole production construction
    path (the web campaign API, ParseFile, and eval all route through it — no
    struct-literal bypass), so the parse-time rejection fully closes the fail-open.

Found via the internal audit sweep of the automation engines (rules / campaign /
workflows). The rest verified sound: tool dispatch routes through RunTool's
audit+confirm gates; rules template-into-params is operator-scoped and non-
recursive; cooldown is race-free; inFlight is leak-safe; the campaign dependency
graph rejects dup/unresolved/self/forward/cyclic/malformed-timeout at load.

Verified

  • task ci green: golangci-lint 0 issues, go vet, build, task test:full (full
    suite, -race) 0 fail, govulncheck clean. task eval 17/17. All five release
    cross-compile targets build green.
  • TestParseYAML_RejectsWhenWithoutDependsOn — proven to fail pre-fix (the
    campaign parsed clean and the step would have run unconditionally).

v0.794.0

Choose a tag to compare

@github-actions github-actions released this 30 Jun 21:06
v0.794.0
ccdc84e

v0.794.0 — risk: centralize dispatch escalation + guard the classify/spec invariant

Changed (architecture / safety hardening — behavior-preserving)
The dispatch safety gates are re-implemented per surface (the agent Run loop,
agent.RunTool, the MCP server), each resolving a tool's effective risk
independently. That copy-paste is how they have drifted before — run_payload
escalation was once missing from RunTool, and the MCP audit rail was missing
until v0.792.0. This release removes two pieces of that drift surface without
changing any gate, threshold, or level.

  • Centralized run_payload escalation. All three sites duplicated "if the tool is
    run_payload, resolve the target path and take max(base, derived)" so a payload
    dispatching to a Critical op (a .sub transmit, badusb script, evil-portal
    launch) is gated at its real level. The rule now lives in one place,
    risk.EscalateForPath(tool, base, path); each surface computes its base as
    before and routes through the helper, so they can no longer drift on whether or
    how they escalate.

  • Guarded the classify==spec invariant. The Run loop gates on risk.Classify
    alone while RunTool and MCP gate on Spec.Risk (max Classify). They agree for
    all 702 registered tools today, and that agreement is exactly what makes the
    Run loop's Classify-only resolution safe — but nothing enforced it.
    TestRiskClassificationMatchesSpec now fails CI if a tool's Classify value (the
    hand-maintained risk.toolLevels map) disagrees with its registry Spec.Risk, so
    a tool registered Critical but mapped lower can't be silently under-gated on
    the Run-loop path. Complements TestRiskCoverage (which only checks a
    classification exists, not that its value matches).

Found while hardening the dispatch surfaces after the v0.792.0 MCP audit fix.

Verified

  • task ci green: golangci-lint 0 issues, go vet, build, task test:full (full
    suite, -race) 0 fail, govulncheck clean. task eval 17/17. All five release
    cross-compile targets build green.
  • TestEscalateForPath: 10 table cases (each extension→level, never-lowers, empty
    path, non-dispatcher tool unchanged). TestRiskClassificationMatchesSpec proven
    (via an injected runtime Classify override) to catch a divergence. The existing
    payload_risk / runtool / MCP consent suites still pass — proof the three sites
    are behavior-identical.

v0.793.0

Choose a tag to compare

@github-actions github-actions released this 30 Jun 18:14
v0.793.0
9f00778

v0.793.0 — targetmem: cap retained rows to bound table growth

Changed (robustness — bounded persistent storage)

  • The target-memory store grew without bound across sessions: MaxRecent bounds
    only the per-call read, and while Remember upserts on the (identifier, kind)
    primary key — so re-observing the same badge/SSID dedupes — a flood of
    DISTINCT attacker-presented identifiers (a scan loop against many spoofed
    UIDs/SSIDs, or a long-lived install) would grow ~/.promptzero/targetmem.db
    without limit.

    Adds a retention cap mirroring snapshot.Rotate: after each write Remember
    prunes to the MaxTargets (50000) most-recently-seen rows, deleting the oldest
    by last_seen. A cheap COUNT(*) guards the DELETE so the common already-under-cap
    case does no work; last_seen is stored RFC3339 so the ORDER BY is chronological
    and uses idx_targets_last_seen. 50000 small rows is generous for any realistic
    pentest history while bounding a hostile flood to a few MB. The cap is a Store
    field defaulting to MaxTargets, overridable for tests.

Found via the internal audit sweep (the last remaining LOW; the DB is per-user
0o600 and dedupes re-observations, so this is defense-in-depth against a
distinct-identifier flood, not a live exploit).

Verified

  • task ci green: golangci-lint 0 issues, go vet, build, task test:full (full
    suite, -race) 0 fail, govulncheck clean. task eval 17/17. All five release
    cross-compile targets build green.
  • TestRemember_PrunesToMaxTargets inserts past a low cap and asserts the oldest
    rows are evicted while the newest survive; proven to fail pre-fix (5 rows
    retained instead of 3).

v0.792.0

Choose a tag to compare

@github-actions github-actions released this 30 Jun 18:14
v0.792.0
b67b509

v0.792.0 — mcp: enforce audit fail-closed before running a tool

Fixed (safety — audit fail-closed on the MCP surface)

  • The MCP dispatch path gated tools on consentDecision (the env-driven
    PROMPTZERO_MCP_ALLOW_HIGH / ALLOW_CRITICAL opt-in) but never called
    audit.RequireOpen — the rail the REPL and --web dispatch both apply. Audit
    logging is wired best-effort: if audit.Open fails (a locked SQLite WAL from
    another promptzero process, an unwritable ~/.promptzero, an unset $HOME), the
    MCP server continues with a nil audit log and the RecordCtx calls silently
    no-op. So with the operator opted in via env AND the audit log failed to open,
    a High/Critical MCP tool (NFC / sub-GHz transmit, run_payload deploy) ran with
    NO audit record — a fail-open relative to the documented "every destructive
    action is audited" invariant, and a silent divergence from the REPL/web
    surfaces where the identical scenario is refused.

    Adds audit.RequireOpen(s.audit, effectiveLevel) before the consent gate, so the
    MCP surface fails closed identically to the agent path: risk≥High is refused
    when no audit log is wired; Low/Medium tools are unaffected. The run_payload
    Critical escalation is already applied to effectiveLevel before this gate.

Found via the internal audit sweep (bootstrap + web-route surfaces, otherwise
clean).

Verified

  • task ci green: golangci-lint 0 issues, go vet, build, task test:full (full
    suite, -race) 0 fail, govulncheck clean. task eval 17/17. All five release
    cross-compile targets build green.
  • TestServer_CallTool_AuditFailClosedWhenNoAuditLog: a High tool with
    ALLOW_HIGH=1 but no audit log is refused with "audit log not initialized" and
    the handler never runs; -race clean, proven to fail pre-fix. The MCP test
    harness now wires an audit log by default (production does); the five consent
    matrix tests still pass.

v0.791.0

Choose a tag to compare

@github-actions github-actions released this 30 Jun 15:12
v0.791.0
f9c4f3d

v0.791.0 — sanitize unterminated C1 sequences (quarantine) + /debug device strings

Fixed (safety — terminal-control / prompt-injection sanitization)

  • quarantine: strip UNTERMINATED C1 control strings. The prompt-injection
    sanitizer's ansiC1RE matched only OSC/DCS/SOS/PM/APC sequences carrying their
    BEL/ST terminator, so an attacker could omit the terminator (e.g. a crafted
    SSID/NFC field surfaced via a quarantined tool result) — ansiC1RE wouldn't
    match, otherControlsRE stripped only the leading ESC, and the body survived as
    readable text ("]0;PWNED ..."), defeating the "strips the full sequence
    including the payload" guarantee. New ansiC1UntermRE strips the introducer plus
    its rest-of-line payload (bounded to the current line so one unterminated
    sequence can't blank multi-line tool output), applied after ansiC1RE.

  • obs /debug: sanitize device-reported strings before rendering. kv() rendered
    the Flipper firmware-model string (and port path) straight to the operator's
    terminal; a compromised Flipper could report a model with ANSI escapes or
    newlines to inject terminal-control sequences or break the /debug box. kv now
    runs every value through quarantine.SanitizeControlChars and flattens the
    whitespace control chars the sanitizer preserves for tabular output.

Found via the internal audit sweep (targetmem + obs surfaces, otherwise clean —
SQLite-backed targetmem has no path-traversal surface, parameterized queries,
quarantined recall output).

Verified

  • task ci green: golangci-lint 0 issues, go vet, build, task test:full
    (full suite, -race) 0 fail, govulncheck clean. task eval 17/17. All five
    release cross-compile targets build green.
  • TestSanitizeControlChars_StripsUnterminatedC1 (quarantine) +
    TestRender_SanitizesDeviceReportedModel (obs), both proven to fail pre-fix;
    agent-package forwarder test updated to the stricter expectation.

v0.790.0

Choose a tag to compare

@github-actions github-actions released this 30 Jun 12:06
v0.790.0
82fb170

v0.790.0 — watch: scope rule personas; no dispatch after shutdown

Fixed (correctness — file-watch automation)

  • The auto-dispatch path is sound (a fired rule runs its prompt through the
    gated agent loop ai.Run, so read-only / confirm / audit / risk all apply);
    these are robustness / least-privilege fixes around it.

  • Watch-rule persona persistence (least privilege): when a watch rule named a
    persona the REPL set it via ai.SetPersona and never restored it, so the
    rule's (possibly broader) persona and its tool scope silently persisted into
    the operator's subsequent interactive turns. applyWatchPersona now applies
    the rule's persona only for its turn and restores the prior persona after
    dispatch. The dispatcher waits for the REPL to be idle first, so no
    concurrent user turn observes the swap.

  • Debounced dispatch firing after shutdown: flushTimers (on ctx cancel)
    Stop()s pending timers, but a timer that had already fired and was blocked
    acquiring mu would still dispatch once flushTimers released the lock — a
    handler running after Run returned. A new Watcher.stopped flag, set under mu
    by flushTimers and checked by the fired goroutine after it re-acquires mu,
    makes it bail. Benign today (the REPL event consumer has already exited) but
    closed.

Found via the internal audit sweep.

Verified

  • task ci green: golangci-lint 0 issues, go vet, build, task test:full
    (full suite, -race) 0 fail, govulncheck clean. task eval 17/17. All five
    release cross-compile targets build green.
  • TestApplyWatchPersona_ScopesAndRestores; TestWatcher_StoppedSuppressesFiredTimer
    (deterministic, -race clean, proven to FAIL pre-fix).