feat(agent): enterprise desktop agent hardening#80
Conversation
Pre-execution tool interception, per-action approval queue, session lifecycle, offline fallback, SSE events, mTLS, unified error format, decision caching, Python SDK parity, posture-aware rulesets, and operational Helm improvements across 79 files. Phase 1 — Security gaps: - Move Spine Ed25519 seeds to K8s Secrets (secretKeyRef) - Remove deprecated Decision boolean fields, migrate to status-only - Add NATS securityContext (runAsNonRoot, drop ALL, readOnlyRootFilesystem) - Add mTLS client cert verification to hushd (with_client_cert_verifier) - Wrap SQLite calls with spawn_blocking to avoid async starvation - Add RequestBodyLimitLayer (configurable, default 1MB) - Add /ready readiness probe (SQLite ping, engine, signing key) Phase 2 — Desktop agent UX: - Pre-execution tool guard (tool-preflight/handler.ts) with preventDefault - Per-action approval queue (approval.rs) with TTL, OS notifications - LRU decision cache keyed on event+resource+policy_version - Offline fallback with cached policy, audit queue, degraded provenance - Session lifecycle (create/heartbeat/terminate) with tray display - SSE event subscription for policy/violation/posture events Phase 3 — Operational hardening: - Helm PDB, HPA, bridge liveness probes, rolling update strategy - v1.2.0 posture-aware ruleset (restricted/standard/elevated) - prompt_injection + jailbreak guards in strict/ai-agent rulesets - Secret leak patterns for Anthropic, Stripe, GCP, Azure, GitHub, GitLab - Unified V1Error JSON envelope across all hushd API handlers - Comprehensive tests for instruction_hierarchy (31) and jailbreak (10) Phase 4 — Enterprise polish: - Python SDK: extends resolution, PromptInjection/Jailbreak guards, regex - Windows forbidden paths (cfg!(windows) + YAML) - Pod topology + anti-affinity for hushd - Desktop agent deployment guide (mdbook) - Extract shared text_utils module from 4 duplicated sources - Deep merge for secret_leak with additional_patterns/remove_patterns Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5d5b79337f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Rust: - daemon.rs: AuditQueue Vec -> VecDeque for O(1) pop_front - daemon.rs: fix re-queue ordering to discard oldest on truncation - daemon.rs: fix spurious restart when child handle is None after stop - approval.rs: cap HashMap at 500 entries to prevent unbounded growth - session.rs: surface non-404 heartbeat errors instead of swallowing TypeScript: - Remove all `any` types from plugin.ts and engine.ts - Replace unsafe `as string` casts with typeof narrowing in handlers - Remove redundant type assertions in createEventData switch cases - Fix decision-cache TTL test that passed by accident (fake timers called after set, so epoch 0 was always past real expiry) Python: - SecretLeakGuard: fix openai_key pattern false-positiving on any sk-* - SecretLeakGuard/PatchIntegrityGuard: raise ValueError on invalid regex (was silently dropping patterns — fail-open, not fail-closed) - PromptInjectionGuard: remove markdown code fence false positive, tighten "you are now" to require suspicious role words - Policy merge: fix settings override losing child's explicit false - PostureConfig: validate state references in transitions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rust: - approval.rs: emit Expired events in list_pending/resolve so tray badge stays current - daemon.rs: re-queue drained audit events on flush send failure instead of dropping them permanently - secret_leak.rs: merge_with now folds base additional_patterns into child when child has explicit patterns TypeScript: - strike-cell.ts: remove unused denyDecision import - secret-leak.ts: fix duplicate char in regex character class - decision-cache.test.ts: remove unused decisionCache import Python: - Remove 7 unused imports across guards and tests - policy.py: replace hardcoded monorepo rulesets path with _find_rulesets_dir() that works when installed via pip Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 92eb7cd587
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
- daemon.rs: reverse requeue order so new events go first; truncation now sheds oldest failed events instead of newest arrivals - approval.rs: evict oldest pending entry when queue is full and all entries are pending, preventing unbounded HashMap growth - policy.py: replace silent `except Exception: pass` with warnings.warn so importlib.resources failures are visible Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- events.rs: use SSE `event:` field for daemon event type dispatch; hushd sends type via the SSE protocol field, not in the JSON data payload, so DaemonEvent deserialization was always failing silently. Added handle_sse_message that injects the type key from the SSE event field before deserializing. Includes tests for both daemon and audit event paths. - secret_leak.rs: restructure merge_with to branch early on whether child has explicit patterns, eliminating the dead-code path where base effective patterns were computed then immediately discarded. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Base remove_patterns now exempt patterns the child explicitly added via additional_patterns, preserving child intent to add detection coverage. Also fixes cargo fmt formatting in the retain closure. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- events.rs: replace unwrap_or_else(json!({})) with proper error
propagation so malformed JSON in daemon SSE events returns an error
instead of silently creating phantom DaemonEvent with all-None fields.
Added test verifying malformed JSON is rejected.
- forbidden_path.rs: remove cfg!(windows) gate on Windows forbidden
paths so the Rust API matches the YAML rulesets which include them
unconditionally. On non-Windows these globs never match.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a382671019
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
- api_server.rs: replace :id with {id} in all route paths for axum 0.8
matchit syntax (routes were registering literal colon segments, 404ing)
- approval.rs: clamp ttl_secs to i64::MAX before cast to prevent
negative duration on pathological input
- daemon.rs: wrap std::fs::create_dir_all and std::fs::write in
spawn_blocking to avoid blocking the tokio runtime on policy sync
- session.rs: fix CreateSessionResponse and GetSessionResponse to match
hushd's wrapped { session: SessionContext } envelope format; extract
posture/budget from the session state map
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Align axum dep to 0.8 (matching workspace root/lockfile), add route param test - Thread session_id into policy eval requests for posture tracking - Document approval architecture split (gateway vs desktop agent) - Return structured deny with reason "hushd_unavailable" on offline (fail-closed) - Token-based preflight tool classification, fail-closed on unknown tools - Harden ApprovalQueue: MAX_TTL_SECS cap, checked_add_signed, QueueFull error - Add mTLS integration tests with rcgen (require_client_cert mode) - Fix docs accuracy: approval response format, offline behavior, approval flow Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Run `cargo vendor` to add rcgen + transitive deps (yasna, asn1-rs, der-parser, oid-registry, x509-parser) needed for mTLS tests - Fix duplicate `"` in Azure Key Vault regex character class (secret-leak.ts) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 67b4ab4d08
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
Addressed remaining hardening items:
Local validation:
|
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.
|
|
||
| let body = format!("Posture changed from {} to {}", old_posture, new_posture); | ||
| notifications::show_notification(&app_for_sse, "Posture Transition", &body); | ||
| } |
There was a problem hiding this comment.
Posture transition event doesn't update session manager state
Medium Severity
When a SessionPostureTransition SSE event arrives, the handler builds a custom summary string with the new posture and updates the tray, but never writes the new posture back into the SessionManager's SessionState.posture field. Since SessionManager has no public method to update posture, the internal state remains stale. The /api/v1/agent/health endpoint (which calls session_manager.state().await) will return the old posture until the next heartbeat cycle — up to 30 seconds later — creating an inconsistency between the tray display and the health API.


Summary
Phase 1: Security gaps closed
spawn_blocking(no more async starvation)RequestBodyLimitLayer/readyreadiness probe with deep health checksPhase 2: Desktop agent UX
preventDefaultfor destructive opsPhase 3: Operational hardening
V1ErrorJSON envelope across all hushd API handlersPhase 4: Enterprise polish
text_utilsmodule (deduplicated from 4 sources)additional_patterns/remove_patternsStats
cargo test --workspace— all passing (0 failures)cargo clippy --workspace -- -D warnings— cleancargo fmt --all -- --check— cleanTest plan
cargo test --workspacepassescargo clippy --workspace -- -D warningscleanhelm templaterenders all new resources (PDB, HPA, spine-secrets, readiness probe)npx vitest run)pytest packages/sdk/hush-py/tests)🤖 Generated with Claude Code
Note
High Risk
Touches core policy enforcement and security decision paths (session IDs, fail-closed behavior, new guards, approvals) and adds new network/backoff/background tasks; regressions could incorrectly allow/deny actions or break agent↔daemon interoperability.
Overview
Adds posture-aware session lifecycle management to the Tauri agent (create/heartbeat/terminate) and threads
session_idthroughevaluate_policy_check, with normalization of action types/targets and explicit fail-closed behavior whenhushdis unreachable or returns errors (optional debug inclusion of daemon error bodies).Introduces a per-action approval queue (
ApprovalQueue) with TTL/cleanup, OS notifications, tray badge integration, and a local API surface (/api/v1/approval/*) to submit/status/resolve/list pending approvals; the agent health/settings APIs now expose session state and a debug flag.Improves resilience/operations by adding a policy cache synced from
hushdand an offline audit queue that flushes on startup/reconnect, plus richer SSE handling that distinguishes daemon-level events (policy updates, violations, posture transitions) from audit events and updates tray/notifications accordingly.Hardens rules and guards: adds a new
clawdstrike:ai-agent-postureruleset and expands existing rulesets/ForbiddenPathGuardfor Windows credential/registry paths; addsShellCommandGuardfor dangerous command patterns and forbidden-path touches; expandsSecretLeakGuardpatterns and addsadditional_patterns/remove_patternsmerge semantics. Also updates agent integration mappings to canonical action types (egress,shell,mcp_tool) and bumps related Rust deps (notablyaxum0.8 in the agent, plus new crypto/x509 deps forrcgen).Written by Cursor Bugbot for commit c69fc8f. This will update automatically on new commits. Configure here.