Skip to content

fix: harden secrets, audit logging, agent-run recovery, CI, and token budgets - #22

Merged
rgxdev merged 2 commits into
mainfrom
claude/nyxelos-hardening-vzz9u3
Jul 3, 2026
Merged

rgxdev merged 2 commits into
mainfrom
claude/nyxelos-hardening-vzz9u3

Conversation

@rgxdev

@rgxdev rgxdev commented Jul 3, 2026

Copy link
Copy Markdown
Member

Summary

Five hardening work packages toward a reliable agentic OS, kept minimal and scoped to existing patterns already established in this codebase (see docs/SECURITY_AUDIT.md).

1. Secrets hardening

  • mcpServer.env/mcpServer.oauthState were the last plaintext secret-shaped columns (per SECURITY_AUDIT.md SEC-01) — encrypted at rest using the same AES-256-GCM envelope already used for modelInstallation.apiKey/knowledgeBaseConfig.obsidianApiKey (packages/db/src/crypto.ts, new encryptJsonNullable/decryptJsonNullable helpers). Schema columns changed from jsonb/json-mode text to plain encrypted text (migration included for both dialects).
  • Added toClientSafeMcpServer/toClientSafeKnowledgeBaseConfig mappers (mirroring the existing toClientSafeInstallation) and wired them into every router procedure that was returning raw secrets to the browser — this closed several leaks beyond the two named tables: models.installCustom/installOpenRouter/setModelEnabled/addModelToInstallation/removeModelFromInstallation/installCli/importSource were all returning the undecorated DB row (including apiKey), and knowledgeBase.overview/updateConfig were spreading the raw obsidianApiKey alongside the *Set boolean.
  • NYXEL_ENCRYPTION_KEY production requirement (assertProductionSecret) was already in place from a prior session — untouched here.

2. Audit log redaction

  • apps/server/src/audit.ts's logAudit now redacts secret-shaped keys (apiKey, token, accessToken, refreshToken, authorization, password, secret, cookie — case/separator-insensitive, arbitrarily nested) and caps serialized size (20k chars) before every audit_log write. inputHash is unaffected — it's computed from the raw value upstream in permissions.ts before this redaction runs.

3. Durable agent run recovery

  • Added workerId/heartbeatAt/leaseUntil/cancelRequestedAt to agent_run (both dialects). executeManagedTask claims a lease at start and renews it on a 30s heartbeat; cancelAgentRun always sets DB state (it already did for status, now also cancelRequestedAt) regardless of whether the run's AbortController is reachable in this process.
  • scheduler.ts's new checkStaleAgentRuns() recovers runs whose lease expired (or never had one — e.g. pre-dates this feature) into failed, and fails the linked task too so the Goal Orchestrator naturally re-drives it. Runs on every scheduler tick and once at startup.

4. CI/test gate cleanup

  • Deleted 5 dead /tests files that imported nonexistent modules (leftover scaffolding from an earlier layout).
  • Added a root "test": "bun test" script and a CI job that runs it after lint/typecheck.
  • Added hermetic, fetch-mocked tests for local model detection (packages/model-providers/src/detect.ts previously had none and made real network calls to localhost ports).
  • secret-scan.yml (gitleaks) already existed and is already non-blocking (continue-on-error: true) — verified, unchanged.

5. Token budget controls

  • Knowledge-base prompt-injection char caps are now configurable via NYXEL_KB_PROMPT_CONTEXT_MAX_CHARS/NYXEL_KB_PROMPT_SECTION_MAX_CHARS (same defaults as before when unset).
  • Added an aggregate output-byte cap (MAX_TOTAL_OUTPUT_BYTES) to the builtin search/codebase/usages tool, additive truncated field on the response.

Out of scope / left alone

  • Plugin process isolation: ADR-0007 already documents the current in-process, permission-scoped design honestly (not claimed as solved) — no runtime change made here.
  • 15 pre-existing, unrelated test failures (plugins.test.ts, skills-resolve.test.ts, seo-analyzer.test.ts — a shared test-fixture DB setup issue) and 2 pre-existing typecheck failures (apps/web's chat-top-bar.tsx, packages/create-nyxel's missing @types/node) — all predate this branch, verified against origin/main, not touched.
  • A pre-existing duplicate-column bug across two early Postgres migrations (0007/0008 both add chat.archived_at) was discovered while verifying the new migrations against a fresh Postgres 16 instance — unrelated to this change, not fixed (out of scope), noted here for visibility.

Test plan

  • bun run typecheck — clean on every touched package (only the two pre-existing, unrelated failures above remain)
  • bun run lint (biome) — clean on every touched file; 3 pre-existing lint findings surfaced in workspace-settings-panel.tsx (2 accessibility, 1 hook-deps) because the file was touched, but verified present on origin/main before this change too
  • bun run test — 192 pass / 15 pre-existing unrelated fail (verified against origin/main baseline)
  • Both new Postgres migrations (0042, 0043) applied against a live local Postgres 16 instance; confirmed pre-existing legacy plaintext data survives the jsonb→encrypted-text conversion via the existing legacy-plaintext fallback in crypto.ts
  • New tests added for every behavior changed (secrets-at-rest, client-safe mappers, audit redaction, stale-run recovery, cancel-without-controller, local model detection, token budgets)

🤖 Generated with Claude Code


Generated by Claude Code

Secrets:
- Encrypt mcpServer.env/oauthState at rest (AES-256-GCM, same envelope as
  modelInstallation.apiKey/knowledgeBaseConfig.obsidianApiKey) via new
  encryptJsonNullable/decryptJsonNullable helpers in packages/db/src/crypto.ts.
- Add toClientSafeMcpServer/toClientSafeKnowledgeBaseConfig mappers and wire
  them (plus the existing toClientSafeInstallation) into every router
  procedure that previously returned raw secrets to the browser.

Audit log:
- Centralize input/output redaction (apiKey/token/accessToken/refreshToken/
  authorization/password/secret/cookie, case-insensitive, nested) and a
  serialized-size cap in apps/server/src/audit.ts's logAudit, ahead of every
  audit_log write. inputHash is unaffected (computed from the raw value
  before redaction).

Agent run recovery:
- Add workerId/heartbeatAt/leaseUntil/cancelRequestedAt to agent_run (both
  dialects). executeManagedTask claims a lease and renews it on a heartbeat
  interval; cancelAgentRun always sets cancelRequestedAt/status in the DB
  regardless of in-memory controller reachability. scheduler.ts's new
  checkStaleAgentRuns recovers runs whose lease expired (or never had one)
  on every tick and once at startup.

CI/tests:
- Delete 5 dead /tests files that imported nonexistent modules; add a root
  "test": "bun test" script and a CI job that runs it after lint/typecheck;
  add hermetic fetch-mocked tests for local model detection.

Token budgets:
- Make the knowledge-base prompt injection char caps env-configurable; add
  an aggregate output-byte cap to the builtin search/codebase/usages tool.

Tests added for every behavior changed; typecheck/lint clean on all touched
files (verified against the pre-existing baseline); both Postgres migrations
verified against a live Postgres 16 instance, including that pre-existing
legacy plaintext data survives the jsonb->text conversion.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019tXH1XWxA1CsvoVr29pPHR
@rgxdev
rgxdev marked this pull request as ready for review July 3, 2026 10:26
@rgxdev rgxdev changed the title Harden secrets, audit logging, agent-run recovery, CI, and token budgets fix: harden secrets, audit logging, agent-run recovery, CI, and token budgets Jul 3, 2026
…ndings)

Three pre-existing, repo-wide CI blockers surfaced once this PR's files
were touched (Typecheck/Build/Build-web/Lint all fail on origin/main
today, independent of this change):

- packages/create-nyxel/tsconfig.json: @types/node was a declared
  devDependency but never auto-included (this TypeScript version doesn't
  auto-discover @types/* the way earlier versions did) — add the explicit
  `types: ["node"]` the compiler itself suggests.
- apps/web/src/components/ui/dropdown-menu.tsx: onOpenAutoFocus is read
  and forwarded by @radix-ui/react-menu's content impl at runtime but
  omitted from this version's public prop type — widen the local wrapper's
  type to match the real, working API instead of the narrower public one.
- apps/web/src/components/workspace-settings-panel.tsx: associate the two
  preset/model checkbox labels with their inputs via htmlFor/id, and
  replace a dead `eslint-disable-next-line` (this project uses Biome, not
  ESLint) with a real biome-ignore for the intentional exhaustive-deps
  exclusion.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019tXH1XWxA1CsvoVr29pPHR
@rgxdev
rgxdev merged commit 56151d9 into main Jul 3, 2026
9 of 11 checks passed
@rgxdev
rgxdev deleted the claude/nyxelos-hardening-vzz9u3 branch July 3, 2026 10:36
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.

2 participants