Skip to content

Audit-pass follow-up: security/OTP/test hardening#60

Merged
nyo16 merged 7 commits into
masterfrom
fix/audit-pass-followup
Jun 5, 2026
Merged

Audit-pass follow-up: security/OTP/test hardening#60
nyo16 merged 7 commits into
masterfrom
fix/audit-pass-followup

Conversation

@nyo16

@nyo16 nyo16 commented Jun 5, 2026

Copy link
Copy Markdown
Owner

Summary

Follow-up hardening pass addressing review findings on commit #59 (the security/bug/performance audit). Implemented across 4 phases; all changes verified green. 24 files, +471/−120. No behavioral API
breaks.

Changes

Security

  • PathGuard now returns the canonical, symlink-resolved real_path, so file tools open the validated inode instead of re-traversing an attacker-swappable symlink (narrows TOCTOU). ensure_within/2 gained
    a resolved-root fallback to stay idempotent when file_glob/file_grep re-validate wildcard results. The residual TOCTOU window (Erlang's :file API has no O_NOFOLLOW) is documented.
  • web_fetchpin_connection(_, _, nil) now fails closed; no unpinned fetch path can be reintroduced via allow_private_hosts.
  • Atom-exhaustion DoS fixed: String.to_atom/1 on YAML-controlled eval input → String.to_existing_atom/1 + fallback (eval/runner.ex, eval/evaluators/schema.ex), with a regression test proving no atom
    is interned.
  • file_grep — the pure-Elixir fallback now runs under a 5s supervised timeout (ReDoS backstop); the rg path was already safe.
  • UrlGuard — adds 198.18.0.0/15 + RFC 5737 test-nets; new regression tests for decimal/hex/octal/encoded-IP SSRF bypasses.
  • Docs — pattern-guard marked best-effort (not authorization); tool_executor telemetry secret-handling note.

Correctness (OTP / Elixir)

  • get_tool_field/2: ||Map.fetch/2 (falsy-safe for false / 0 / "").
  • Rate limiter: dropped the racy Process.alive? pre-check; safe_acquire/3 catches :exit and fails open with a log + telemetry signal instead of crashing the agent loop.
  • agent_server async load task always replies (try/rescue/catch), so a raising backend can't wedge the caller until timeout.
  • async_nolink {ref, result} completion message is absorbed by a dedicated handle_info clause instead of falling through to the catch-all.
  • shared_state claims: O(1) prepend + reverse on read (matches the discoveries fix).
  • Stream result accumulator: flat O(n) iodata (prepend + reverse) instead of a right-nested improper list.
  • Dropped a :ets.insert try/rescue that masked genuine table bugs.

Tests

  • Removed redundant Process.sleep calls (rely on FIFO GenServer.call serialization or an explicit flush); refute_receive over instant refute_received; start_supervised! for named helper Agents (no
    leak on crash); non-tautological glob-injection assertion; unique telemetry handler IDs + detach; membership asserts on the singleton persistence table; async: true for PathGuard tests.

ETS ownership

  • Documented the intentional run-scoped/ephemeral :public ownership model for the KnowledgeBase and Decisions stores (cross-process tool execution requires :public; access is gated by possession of the
    table reference). Safer slug-index write order.

Verification

  • mix compile --warnings-as-errors
  • mix format --check-formatted
  • mix credo --strict ✅ (no issues)
  • mix test1810 passed, 0 failed, 101 excluded (+4 new tests)

Reviewed by security + Elixir specialist passes; findings addressed.

Deferred (intentional)

  • Bundling the 11-arg run_tool_with_hooks/11 — pure readability; Credo already passes.
  • Full DNS-rebind mock-resolver test — the pin contract is already covered; a full test needs UrlGuard resolver injectability.

nyo16 added 7 commits June 5, 2026 07:21
…-closed, atom-DoS, ReDoS cap, UrlGuard ranges, docs)
…async-load reply-on-crash, async_nolink absorb, claims O(1), iodata flat accumulation, drop needless ets rescue)
…receive, start_supervised!, non-tautological glob assertion, unique telemetry handler IDs+detach, membership asserts, encoded-IP SSRF cases, async path_guard)
…rrect async_nolink completion-clause comment
@nyo16 nyo16 merged commit e5e807c into master Jun 5, 2026
6 checks passed
@nyo16 nyo16 deleted the fix/audit-pass-followup branch June 5, 2026 14:20
nyo16 added a commit that referenced this pull request Jun 22, 2026
The audit content for releases 0.16.2-0.16.5 had piled up under [Unreleased]
and was never versioned (releases were tagged without splitting the changelog).
Split it into dated sections using the tag-snapshot delta as ground truth (the
[Unreleased] content at each tag = that release's cumulative content):

- [0.16.2] 2026-05-16 — provider marshalling, ETS lifecycle, OTP hygiene, telemetry (#58)
- [0.16.3] 2026-05-29 — security pass: RCE gate, SSRF, sandbox + audit findings (#59)
- [0.16.4] 2026-06-05 — security/OTP/test hardening (#60); summarized from the
  commit, since this release added nothing to the changelog at the time
- [0.16.5] 2026-06-12 — InputGuard fail-closed, permissive execute gate, policy bypass (#61)
- [Unreleased] (-> 0.16.6) — perf hot-path hardening (#62) + the docs overhaul (#63)

Bullets were moved verbatim, not rewritten; dates match the git tags exactly.
Added compare-links for each new version. Verified: no [Unreleased] bullet was
dropped, mix docs is 0 warnings, mix format clean.
nyo16 added a commit that referenced this pull request Jun 22, 2026
The audit content for releases 0.16.2-0.16.5 had piled up under [Unreleased]
and was never versioned (releases were tagged without splitting the changelog).
Split it into dated sections using the tag-snapshot delta as ground truth (the
[Unreleased] content at each tag = that release's cumulative content):

- [0.16.2] 2026-05-16 — provider marshalling, ETS lifecycle, OTP hygiene, telemetry (#58)
- [0.16.3] 2026-05-29 — security pass: RCE gate, SSRF, sandbox + audit findings (#59)
- [0.16.4] 2026-06-05 — security/OTP/test hardening (#60); summarized from the
  commit, since this release added nothing to the changelog at the time
- [0.16.5] 2026-06-12 — InputGuard fail-closed, permissive execute gate, policy bypass (#61)
- [Unreleased] (-> 0.16.6) — perf hot-path hardening (#62) + the docs overhaul (#63)

Bullets were moved verbatim, not rewritten; dates match the git tags exactly.
Added compare-links for each new version. Verified: no [Unreleased] bullet was
dropped, mix docs is 0 warnings, mix format clean.
nyo16 added a commit that referenced this pull request Jun 22, 2026
…#64)

* docs: follow-ups — CHANGELOG entries, silent mix docs, 4 new examples

Post-overhaul cleanup (follow-up to #63).

CHANGELOG:
- Add the missing 0.12.13 (custom: provider, #34) and 0.12.12 (memory/context/
  AgentServer fixes) entries — both had release tags but no changelog sections —
  with matching compare-links.

mix docs — now 0 warnings:
- Qualify `Agent.new/2` -> `Nous.Agent.new/2` (resolves & links) in CHANGELOG.
- De-link historical references to since-private/hidden APIs in CHANGELOG and
  AGENTS.md (run_with_tools/6, Gemini.parse_content/1, Model.default_receive_timeout/1,
  Provider.request/3, Plugins.Memory.init/2, Nous.Application, Persistence.ETS.TableOwner).

New examples (all run or degrade gracefully without a provider):
- examples/llm_oneshot.exs — bare Nous.LLM API (generate_text/3, /3 bang, stream_text/3).
- examples/knowledge_base.exs — KB store add/search + KB agent plugin.
- examples/advanced/summarization.exs — auto-compaction via the Summarization plugin.
- examples/advanced/web_tools.exs — WebFetch/SearchScrape/Tavily/Brave tools.
- Indexed all four in examples/README.md.

Docs:
- Clarify the three LiveView examples' distinct roles (patterns reference vs
  complete chat app vs multi-agent dashboard).

Verified: mix format, compile --warnings-as-errors, docs (0 warnings),
credo --strict (clean); new examples run green offline.

Note: v0.16.2-v0.16.5 are tagged but lack CHANGELOG entries and mix.exs @Version
(0.16.1) is behind the latest tag — deferred to a separate version/release
reconciliation pass rather than guessing release notes.

* chore: bump @Version to 0.16.6

mix.exs @Version had been left at 0.16.1 across the v0.16.2-v0.16.5 release
tags (each tagged without bumping it). master is 3 commits past v0.16.5, so the
next release is 0.16.6.

Note: the matching CHANGELOG entries for 0.16.2-0.16.5 are still outstanding
(separate version/release reconciliation).

* docs(CHANGELOG): split accumulated [Unreleased] into 0.16.2-0.16.5

The audit content for releases 0.16.2-0.16.5 had piled up under [Unreleased]
and was never versioned (releases were tagged without splitting the changelog).
Split it into dated sections using the tag-snapshot delta as ground truth (the
[Unreleased] content at each tag = that release's cumulative content):

- [0.16.2] 2026-05-16 — provider marshalling, ETS lifecycle, OTP hygiene, telemetry (#58)
- [0.16.3] 2026-05-29 — security pass: RCE gate, SSRF, sandbox + audit findings (#59)
- [0.16.4] 2026-06-05 — security/OTP/test hardening (#60); summarized from the
  commit, since this release added nothing to the changelog at the time
- [0.16.5] 2026-06-12 — InputGuard fail-closed, permissive execute gate, policy bypass (#61)
- [Unreleased] (-> 0.16.6) — perf hot-path hardening (#62) + the docs overhaul (#63)

Bullets were moved verbatim, not rewritten; dates match the git tags exactly.
Added compare-links for each new version. Verified: no [Unreleased] bullet was
dropped, mix docs is 0 warnings, mix format clean.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant