Skip to content

security(daemon): trust gate + worker isolation + DES-028 pipeline design#161

Merged
claude-puntlabs merged 2 commits intomainfrom
fix/daemon-security-hardening
Apr 15, 2026
Merged

security(daemon): trust gate + worker isolation + DES-028 pipeline design#161
claude-puntlabs merged 2 commits intomainfrom
fix/daemon-security-hardening

Conversation

@claude-puntlabs
Copy link
Copy Markdown
Contributor

@claude-puntlabs claude-puntlabs commented Apr 15, 2026

Summary

9 security findings from dedicated djb review, all verified CLOSED:

Trust gate (Critical/High):

  • PGP verification required for x-bit execution — Proton E2E headers are SMTP-injectable
  • PGP KeyID bound to contact.GPGKeyID — prevents key impersonation

Worker isolation (Medium):

  • HOME = ephemeral tmpdir (no ~/.ssh, ~/.gnupg access)
  • PATH restricted to claude + /usr/bin + /usr/local/bin
  • Concurrency semaphore (max 2 workers)

Contract hardening (High/Low):

  • Adversarial robustness in system prompt
  • Email subject out of success_criteria, into inputs.trigger only
  • NUL byte stripping, 500 char cap, mission ID early validation, 0700 tmpdirs

Design (DES-028):

  • Pipeline orchestrator: missions as typed commands with GPG-signed YAML
  • Planner interface, persisted state, try/else error handling

Test plan

  • make check passes
  • djb security review: 9 findings
  • bwk implementation: all 9 addressed
  • djb verification: all 9 CLOSED
  • PGP key match/mismatch tests with ephemeral GPG keys
  • Proton-only trust rejected for x-bit
  • Copilot review

Note

High Risk
High risk because it changes the security boundary for autonomous mission execution (trust verification + key binding) and modifies worker spawning isolation and concurrency limits, which can affect whether missions run and how they access the host environment.

Overview
Tightens daemon execution security for x-bit email triggers. The daemon now rejects mission creation/execution unless the triggering message is PGP-signed and pgp.Verify succeeds, and (when configured) the signing KeyID matches the contact’s registered GPGKeyID.

Hardens worker spawning. Worker subprocesses run with an isolated temp HOME/TMPDIR, a restricted PATH, explicit claude binary resolution, mission ID validation moved earlier, and a default concurrency semaphore (max 2 workers, configurable) to cap parallel execution.

Reduces prompt/contract injection surface. Mission contracts record email provenance under inputs.trigger (not success_criteria), remove attacker-controlled subject text from criteria, cap/clean YAML-escaped values, and tighten temp dir permissions; the worker system prompt adds explicit adversarial-content instructions.

Docs/tests updated. Adds DES-028 pipeline orchestrator design writeup, expands daemon tests with real ephemeral GPG-signed RFC822 fixtures (including key match/mismatch), and extends the IMAP test server to inject raw RFC822 messages.

Reviewed by Cursor Bugbot for commit fa30f41. Bugbot is set up for automated code reviews on this repo. Configure here.

…line design

9 security findings from djb review, all verified CLOSED:

Trust gate (CRITICAL/HIGH):
- Require PGP verification for x-bit execution. Proton E2E headers
  are SMTP-injectable — no longer sufficient for execute decisions.
- Bind PGP KeyID to contact.GPGKeyID. Prevents impersonation via
  wrong key. HasSuffix match for long/short key ID compatibility.

Worker isolation (MEDIUM):
- HOME set to ephemeral temp dir via os.MkdirTemp. No access to
  ~/.ssh, ~/.gnupg, ~/.punt-labs. Cleaned up on worker exit.
- PATH restricted to claude binary dir + /usr/bin + /usr/local/bin.
- Concurrency semaphore: max 2 workers, non-blocking acquire.

Contract hardening (HIGH/LOW):
- Adversarial robustness instructions in system prompt.
- Email subject removed from success_criteria — fixed text only.
  Subject in inputs.trigger (audit metadata, not instructions).
- escapeYAMLValue strips NUL bytes, caps at 500 chars.
- Mission ID validated at Create, not just spawn.
- All temp dirs 0700 (owner-only).

Design (DES-028 pipeline orchestrator):
- Missions as typed commands with GPG-signed YAML definitions.
- Typed args (no string interpolation) prevent shell injection.
- Planner interface (LLM + Rule implementations).
- Persisted pipeline state with crash recovery.
- try/else error handling with fixed-text replies.

Refs beadle-88g
Copilot AI review requested due to automatic review settings April 15, 2026 01:33
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens beadle-daemon against spoofed/hostile email triggers by requiring PGP verification for x-bit execution, isolating worker subprocess environments, and documenting the DES-028 pipeline/orchestrator design.

Changes:

  • Enforce a PGP-based trust gate for x-bit execution (Proton trust headers alone are rejected) and bind signing KeyID to the configured contact gpg_key_id.
  • Isolate worker subprocess execution (ephemeral HOME, restricted PATH, isolated TMPDIR) and introduce a concurrency limit for worker spawns.
  • Harden mission contracts/prompts (subject moved to inputs.trigger, adversarial instructions added, temp dirs 0700, NUL stripping + length cap) and expand tests + design docs.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
internal/testserver/imap.go Adds raw RFC822 seeding helper to support signature/trust-header test cases.
internal/testserver/fixture.go Exposes AddRawMessage via fixture API for integration tests.
internal/daemon/templates_test.go Asserts new adversarial-robustness system-prompt content.
internal/daemon/templates.go Tightens temp dir perms (0700) and adds SECURITY section to system prompt.
internal/daemon/spawner_test.go Updates mission ID validation test to use exported helper.
internal/daemon/spawner.go Adds ValidMissionID helper; isolates worker env (HOME/PATH/TMPDIR) and resolves claude via LookPath.
internal/daemon/mission_test.go Updates contract expectations (trigger metadata) and tests new escaping behaviors.
internal/daemon/mission.go Moves subject out of success_criteria, adds inputs.trigger, adds NUL stripping/length cap, validates mission IDs.
internal/daemon/handler_test.go Adds PGP verification/key-binding tests using ephemeral GPG keys; adds Proton-header-only rejection test.
internal/daemon/handler.go Enforces PGP verification gate before mission creation; adds worker concurrency semaphore.
docs/orchestrator-design.md Updates design to include transport trust gate and DES-028 typed-command pipeline architecture.
cmd/beadle-daemon/main.go Updates handler construction to pass max-worker setting (defaulted).
DESIGN.md Adds DES-028 design summary.
CHANGELOG.md Documents new security hardening and DES-028 design addition.
.beads/issues.jsonl Updates beadle-88g status/priority metadata.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/daemon/handler.go Outdated
Comment thread internal/daemon/handler.go
Comment thread internal/daemon/mission.go
Comment thread internal/testserver/imap.go Outdated
Comment thread internal/daemon/handler.go
Comment thread internal/daemon/handler.go Outdated
@claude-puntlabs claude-puntlabs merged commit 50eba9f into main Apr 15, 2026
6 checks passed
@claude-puntlabs claude-puntlabs deleted the fix/daemon-security-hardening branch April 15, 2026 01:46
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit fa30f41. Configure here.

}()
default:
h.logger.Warn("worker capacity full, skipping", "from", addr, "id", msg.ID)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking semaphore permanently drops authorized messages

High Severity

When workerSem is full, the default branch silently drops the message without creating a mission. These messages are never retried because the poller only calls OnNewMail when the unseen count increases beyond the previously recorded lastSeen (which is updated unconditionally every poll cycle). A skipped message stays unread but the poller won't re-trigger for it, and even if new mail arrives, ListMessages fetches only the last newCount unread UIDs — the older skipped message may never appear in the fetch window. The previous code always created missions for all authorized messages; moving mission creation inside the semaphore gate turns a concurrency limit into permanent data loss.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit fa30f41. Configure here.

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