Skip to content

Security: hxddh/storage-agent-workbench

Security

docs/security.md

Security

Security is a core product requirement.

Secret handling

Secrets include:

  • Model API keys
  • Cloud access keys
  • Cloud secret keys
  • Session tokens
  • Authorization headers
  • Presigned URL credentials
  • Cookies
  • Bearer tokens

Rules:

  1. Secrets must never enter LLM prompts.
  2. Secrets must never be stored in SQLite.
  3. Secrets must never be stored in logs.
  4. Secrets must never be stored in traces.
  5. Secrets must never be stored in reports.
  6. Secrets must never be stored in frontend state longer than needed for submission.
  7. Secrets must be stored only through security/keyring_store — a single AES-256-GCM encrypted local vault (secrets.enc), with the master key protected per-OS by a non-prompting mechanism (Windows DPAPI; an owner-only 0600 key file on macOS/Linux). Not the OS keychain (the ad-hoc-signed, cross-platform app would re-prompt on every update / be absent on headless Linux). Do not move secrets back into the keychain, SQLite, or plaintext.
  8. SQLite may store only secret references (keyring://scope/name).

Tool safety

Rules:

  1. No generic shell tool.
  2. No raw subprocess tool exposed to the Agent.
  3. No raw boto3 client exposed to the Agent.
  4. Cloud operations must go through whitelist tools.
  5. Default mode is readonly.
  6. test-write is a reserved mode: it exists only as a schema enum — no write tool exists anywhere in the MVP, so nothing can write even with the mode set. Any future write tool must enforce the allowed test prefixes (e.g. tmp/agent-test/*, diagnose/*) before performing any write.
  7. Destructive operations are forbidden.
  8. A provider's allowed_buckets / allowed_prefixes scope is enforced server-side — in the agent session tools, the surviving /tools endpoints, and the run executors.

Forbidden:

  • DeleteBucket
  • PutBucketPolicy
  • PutBucketAcl
  • PutLifecycleConfiguration
  • DeleteObjects
  • Recursive delete
  • Mass object mutation
  • Bucket-wide destructive or mutating operation

Analysis safety

Rules:

  1. No bulk object-body downloads. The one bounded exception is preview_object: a single, read-only, sanitized preview of one named object's head (hard cap 1 MiB/call), bounded per turn so it can't be looped into a bulk download, and never persisted. Binary/oversized objects are reported, not decoded.
  2. Full bucket scans require explicit user approval.
  3. Large scans require max_objects or prefix limits.
  4. Reports should show at most 20 sample object keys by default.
  5. Logs should be sanitized before persistence.
  6. Presigned URLs must be redacted before storage or display.

Redaction

Must redact:

  • Access keys
  • Secret keys
  • Session tokens
  • API keys
  • Authorization headers
  • Signatures
  • Presigned URL credentials
  • Sensitive query parameters
  • Cookies
  • Bearer tokens

Audit

Record these events:

  • Tool calls
  • Tool inputs after sanitization
  • Tool outputs after sanitization
  • Analysis SQL
  • Data imports
  • Approval events
  • Report generation

Provider unsupported

S3-compatible providers may not support every AWS S3 API.

Unsupported APIs should be recorded as:

Provider unsupported

They should not be treated as hard failures unless the requested task requires that capability.

Agent dataset analysis

Dataset analysis (access_log_analysis, inventory_analysis, and the agent's analyze_uploaded_file tool) is deterministic — there is no in-run LLM narrator or drill-down agent (both were removed in v0.20.0). The deterministic DuckDB engine is authoritative and produces the metrics + findings; the single conversational agent narrates the sanitized result if the user asks.

  • The conversational agent is given only a bounded, sanitized, aggregated context: run + dataset metadata, the deterministic metrics, and the deterministic findings. Lists are capped at 20 entries and the whole context is asserted to contain no secret-shaped content before it can leave the process. It never reaches raw rows, full key lists, free SQL, or object bodies.
  • Forbidden in the agent context: raw log lines, raw inventory rows, full key lists / >20 sample keys, Authorization headers, cookies, presigned-URL query params, access/secret/session keys, model API keys, unmasked client IPs, and arbitrary SQL result dumps. Client IPs are masked upstream at import.
  • The agent's output is redacted, chain-of-thought-stripped, length-bounded, and coerced to a fixed field set before it is shown or saved. Hidden reasoning, raw prompts, and raw model reasoning are never persisted.
  • The agent may recommend reviewing lifecycle-policy candidates, but must never auto-create/update/delete lifecycle rules or emit bulk-delete commands — same destructive-operation ban as the rest of the app.
  • A missing model provider key fails only the conversational turn cleanly (422); the deterministic run is unaffected.

Account discovery

The account_discovery run type enumerates an account's buckets and their configuration from read-only APIs only. It is deterministic; no bucket list / config is ever sent to an LLM (the conversational agent sees only the sanitized summary + counts via survey_account).

  • AK/SK/session tokens stay in the encrypted secret vault, resolved at call time inside the boto3 client factory; they never enter SQLite, logs, reports, UI state, or any LLM prompt.
  • list_buckets is read-only ListBuckets. It does not call ListObjectsV2, does not scan objects, and does not download object bodies. Object-level listing/get_object are never invoked by this run type.
  • Bucket config snapshot uses only read-only get_* / list_* config APIs (no put/delete/create/update). No logging or inventory is auto-enabled; no lifecycle / policy / ACL / encryption / replication is auto-modified or auto-remediated.
  • Evidence-source discovery only discovers whether inventory / server access logging are configured (plus destination metadata). It never pulls the full inventory report or access log — pulling that evidence is the separate, confirmation-gated managed evidence import flow (see below), never part of discovery. Reserved sources (CloudTrail / Storage Lens / provider access logs) are surfaced as not_implemented, never faked.
  • Bounded scan: processing is capped by max_buckets (default 100, hard cap 500) with optional include/exclude globs; truncation is reported, not silent.
  • Capability vs permission: S3-compatible gaps are provider_unsupported; permission gaps are access_denied — distinct, and neither crashes the run (per-bucket failures are isolated).
  • Persistence is sanitized: the four account-discovery tables store only redaction-passed JSON — never AK/SK/session token/Authorization/cookies/ presigned-URL/model key. Bucket names, inventory destinations, and logging targets pass through the redaction pipeline; reports never contain secrets, signatures, raw object listings, raw inventory rows, or raw access-log content.

Managed evidence import

Pulling inventory / access-log evidence into the analysis path is bounded and confirmation-gated.

  • Discovered sources only. Imports read only the inventory destination (bucket/prefix) or server-access-logging target (bucket/prefix) that account_discovery already found and persisted. The caller cannot supply an arbitrary bucket or object key; the business source bucket is never listed.
  • No business object scan / body download. The only listing is a bounded list_objects_v2 over the evidence destination prefix; the only get_object calls are for evidence files in the confirmed plan (manifest, inventory data files, log objects). No business object body is ever downloaded, no recursive copy / sync, no full bucket scan.
  • Bounded. Selection is capped by max_files (default 1000, hard cap 5000) and max_bytes (default 1 GiB, hard cap 5 GiB). Access-log import REQUIRES a time range. The byte/file budget is enforced again at download (a file larger than the remaining budget aborts the import as failed). Downloads stream to disk (never buffered whole in memory) and gzip evidence is decompressed with an explicit expansion bound — a decompression bomb aborts, it doesn't exhaust the machine.
  • Explicit confirmation. A plan downloads nothing. Download happens only after confirm, which is recorded in approval_events (decision=approved) and audit_logs. There is no hidden auto-confirm; a zero-file or over-limit plan is refused.
  • No mutation. No S3 put/delete/create, no auto-enable of inventory/logging, no lifecycle/policy/ACL/encryption/replication change.
  • Secrets & storage. AK/SK/session token/model key never enter SQLite, logs, reports, UI, or any LLM prompt. Evidence files download to the app data dir (data/runs/{id}/raw/), never the install dir; raw file content never appears in reports (only redacted aggregates from the existing analyzers). The two evidence-import tables store redaction-passed bucket/prefix/key/warnings only.
  • Reuse. Downloaded files feed the existing deterministic inventory_analysis / access_log_analysis importers + analyzers; the import is deterministic by design (no LLM, no agent).
  • Support gaps. ORC inventory is detected_but_not_supported; full inventory manifests with unusual structures degrade to a clean limitation rather than a crash. CloudTrail / Storage Lens / provider access logs remain unimplemented.

Next-action handoff

Turning a proposal into action is gated and reuses existing safe flows.

  • Proposals are not automation. The single /actions/prepare endpoint only validates and prefills; it never creates a run, downloads evidence, confirms an import, mutates a bucket, calls S3, or calls an LLM. There is no hidden auto-run and no hidden auto-confirm. (There is no separate "preview" endpoint — that was removed; prepare is the only handoff step.)
  • Bounds, not gates. action_type is free-form — the agent proposes any concrete next step in its own words; there is deliberately no fixed action_type allowlist (sessions/next_actions.py). Each value is sanitized to a bounded slug (lowercase alphanumeric/_/-, max 64 chars), and a slug carrying a forbidden/destructive token (shell / exec / sql / delete-object / put-bucket-policy / …) is dropped — a proposal must never even suggest a mutating operation. SPECIAL_ACTION_TYPES only selects the purpose-built UI flows (evidence import, session report, composer seeding); it is explicitly not a cap — an unrecognized type simply routes back to the conversational agent, which carries it out with its own read-only tools. Every proposal carries requires_confirmation=true.
  • Existing safe workflows are reused. Most proposals just seed the composer with a natural-language prompt that the conversational agent then handles inline with read-only tools — there is no NewRunForm (it was removed). The one flow that still creates a run is evidence import, which keeps its plan → confirm → run gate in EvidenceImportDialog.
  • No unsafe auto-fill. Access-log evidence import does not auto-fill the time range; the user enters it in the import plan.
  • Sanitized. Proposals, prefills, and assistant proposed_actions are redaction-passed (no secrets, no raw logs/rows); assistant output is chain-of-thought-stripped; a missing model key still fails cleanly.
  • Not a task system. Audit events (next_action_prepared / next_action_opened) are lightweight; there is no assignee, due date, status board, ticket state, or workflow state machine.

StorageOps skill context

The bundled StorageOps skill pack is professional-method context only; it adds no executable capability.

  • Vendored content is data, not code. Only skill-registry.yaml + skills/*/SKILL.md are bundled. No StorageOps tools, helper scripts, CLI, Pi runtime, references/, or templates/ are copied. If a SKILL.md mentions scripts / tools / capture_http_trace / scan_secrets / recommended_tools, that is allowed prose — the Workbench never registers, exposes, imports, or executes them. recommended_tools is dropped at load time.
  • No wrapper execution. read_skill returns a SKILL.md body frontmatter- stripped and length-bounded — no tools, scripts, CLI, or external execution are ever wired up. Script/tool mentions inside a SKILL.md are conceptual prose only; the Workbench never registers or runs them.
  • No raw data / secrets to the model. The catalog + context builder feed the Agent only skill docs + the already-sanitized session/triage context. The raw error blob, raw logs/rows, credentials, model keys, and chain-of-thought are never included. Agent output is redacted + CoT-stripped.
  • Routing is the model's, not a rule engine. The agent sees the catalog (name + one-line description) and chooses which skill to read_skill on demand; there is no lexical selector (it was removed) and no hard-coded error-code → skill mapping. Registry trigger_keywords / auto_route are parsed but currently unconsumed.
  • Human-in-the-loop preserved. Skill-grounded answers still produce only next-action proposals (all require confirmation); nothing auto-runs, auto-confirms, downloads, or mutates. No new tool, API, DB table, subprocess, MCP, or multi-agent runtime is introduced.
  • Public-repo hygiene. Vendored skills + docs/tests use generic content; no real customer / endpoint / bucket / credential data is added.

Error triage

The error-triage assistant adds S3 error diagnosis inside a session without any new dangerous capability.

  • Redaction before anything. Pasted error text is redacted first — shared redactor plus triage-local patterns for SigV4 Signature=/Credential=, cookies, secret/session/API keys in key=value form, and sk- model keys. Only the redacted input + sanitized parsed signals/findings are persisted.
  • Triage is deterministic — no LLM. Error triage is pure pattern-matching + playbook lookup; there is no interpretation-only triage Agent (that was removed). Interpretation, when wanted, comes from the conversational session agent in-thread over the already-sanitized triage context. Triage itself has no tools and calls no model.
  • Triage performs no S3 call. Parsing + playbook matching are local and read-only-by-construction. Any actual cloud check happens only later, if the user explicitly starts an existing diagnostic / config-review / import flow via a next-action proposal (review → prepare → confirm).
  • No automation. Triage never creates a run, downloads evidence, confirms an import, or changes configuration. Next actions are proposals only.
  • Not a ticketing system / FAQ / error-code dictionary. No assignee, board, status machine, or static code-table; just sanitized cases tied to a session.
  • Public-repo hygiene. Docs and tests use only synthetic examples (example.com, fake buckets/ids) — no real customer/endpoint/credential data.

Sessions

Sessions add a persistent working context over the existing runs without adding any new dangerous capability.

  • Deterministic, sanitized summary. The session summary is built only from already-sanitized run artifacts (run_type/status/final_summary, sanitized tool_call outputs, the persisted account profile). It never reads raw access logs, raw inventory rows, evidence file contents, credentials, or chain-of-thought, and it does not call an LLM.
  • Read-only investigator agent. The session agent investigates live with read-only tools (bucket/object listing — bounded + paginated, no bodies — config readers, credential/addressing/TLS/range probes, progressive-disclosure skills) and bounded working memory (sanitized facts/findings/open-questions it records itself). Credentials are resolved server-side and never enter its context; it cannot bulk-download object bodies (the sole bounded exception is preview_object / test_range_get — one sanitized, per-turn- budgeted, text-only read, never a full or recursive download), change configuration, delete or mutate anything, run a shell, run free SQL, reach any destructive S3 op, or see any secret. Output is redacted + chain-of-thought- stripped + bounded; a missing model key fails cleanly and never affects the deterministic summary.
  • Graded execution, never destructive. There is no autonomy toggle: the agent always EXECUTES read-only runs itself (config-review / account-survey — real, audited, read-only, wall-clock-bounded) and narrates the result. EXPENSIVE/data-moving work (cloud evidence import/download, large/full scans) and any MUTATING op are never auto-run — they carry requires_confirmation and the user acts. There is no auto-download, no auto-remediation, no write tool.
  • Safe persistence. Session titles/goals/bucket names, messages, findings, evidence refs, and summaries are all redaction-passed — never AK/SK/session token/Authorization/cookies/presigned URL/model key, never raw logs/rows, never chain-of-thought.
  • Not a PM/kanban/ticketing system. There are no boards, columns, tickets, tasks, assignees, sprints, due dates, labels, notifications, or multi-user/ permission models — only investigation context.

Sidecar local authentication

The sidecar binds localhost only — but loopback is not process isolation: any other process on the same machine could reach the port, and CORS does nothing against non-browser clients. A shared-secret gate closes that gap:

  • The Tauri shell generates a random per-launch token (src-tauri/src/lib.rs) and passes it to the sidecar as STORAGE_AGENT_AUTH_TOKEN; the frontend fetches it via the get_sidecar_token command.
  • When the variable is set, every request must present the token — the X-Sidecar-Token header, or ?token= for the header-less SSE EventSource — except GET /health and CORS OPTIONS preflight. Anything else gets 401. Token comparison is constant-time.
  • The packaged sidecar runs uvicorn with access logging disabled, so the ?token= query parameter can never leak into request logs.
  • When the variable is unset (plain dev/browser runs, the test suite), auth stays open so the local workflow keeps working. The token is defense in depth, not the sole boundary — secrets still never transit the API in plaintext.

Packaging

  • The application bundle contains code and library data only. It must never include .env, the SQLite database, the secret vault, or data/runs/ output.
  • Secrets live in the encrypted vault in the app data dir (never the install/app bundle), alongside the rest of the user data.
  • The packaged sidecar binds localhost only, never enables reload in production, and prints a sanitized startup banner (no secrets, no full paths, no env dump).
  • Tauri spawns only the internal packaged sidecar; no user-controlled shell or subprocess execution is exposed.

There aren't any published security advisories