Skip to content

feat(proactive): daily briefing as a read-only autonomous agent turn (Phase 2) - #5

Merged
tyxben merged 1 commit into
mainfrom
feat/proactive-briefing
Jun 13, 2026
Merged

feat(proactive): daily briefing as a read-only autonomous agent turn (Phase 2)#5
tyxben merged 1 commit into
mainfrom
feat/proactive-briefing

Conversation

@tyxben

@tyxben tyxben commented Jun 13, 2026

Copy link
Copy Markdown
Owner

What & why

Phase 2 first slice — the first "agent acts on its own clock" feature. At a configured local time, the agent runs one turn that pulls a brief (prompt asks it to call messageinfra.get_briefing and summarise in zh with a > spoken line) and pushes the result to connected surfaces. Opt-in, off by default.

Security model — autonomous-origin sandbox (new tool_guard primitive)

This was the key design decision (chosen over "fetch + tool-less summary" and "doc-only"): an unattended, self-clocked turn has no human to approve a gate and runs over untrusted content (whatever get_briefing returns), so it must not be able to act.

tool_guard._AUTONOMOUS_ORIGINS ({"briefing"}): gate() REJECTS any name-keyed / requires_confirmation / write-side-effect tool from these origins regardless of mode (even off), audited REJECTED-AUTONOMOUS. So an injected briefing can read (get_briefing/web_search/read_file) but never shell/write/exfil. The check runs before the OFF short-circuit, so default installs are protected. Reusable for all future proactive features.

(Arcana's create_chat_session has no per-session tool allowlist, so the gate is the right enforcement point.)

Changes

  • proactive.py (new): seconds_until_daily + run_briefing_once + briefing_loop.
  • server.py: startup starts the loop (retained, cancelled on shutdown); _broadcast_frame fan-out; _chat_in_flight_cm.
  • config.example.yaml + CLAUDE.md: documented.

Adversarial review → all findings fixed

A 2-lens review (lifecycle/concurrency + fan-out/security/cost) verified the loop, cancellation, and cost are sound, and found:

  • HIGH — full-tool autonomous turn over untrusted content + default-off gate → injection→shell. → read-only autonomous sandbox (above).
  • MED — brief bypassed _chat_in_flight, so a self-upgrade could re-exec mid-brief. → wrapped in _chat_in_flight_cm.
  • MED + MED — per-delta relay fan-out (separate loop) reorders frames, and an unsolicited stream clobbers an in-flight user turn's shared bubble. → collect the turn and push one {"type":"response","kind":"briefing"} frame instead of per-delta streaming.
  • LOW — quoted enabled: "false" was truthy. → strict-parsed.

Testing

uv run pytest -q563 passed. Sandbox probe verified: briefing-origin shell/write/MCP-write all REJECTED in off mode; get_briefing AUTO; local origin unaffected.

Follow-ups

Proactive TTS auto-play on the response frame; relay/Telegram surfaces (MCP is server-loop-only today); a persistent inbox so a brief fired with no console connected isn't lost.

…(Phase 2)

The first "agent acts on its own clock" feature. At a configured local time,
server.py starts proactive.briefing_loop (retained task, cancelled on shutdown)
which runs one agent turn that pulls a brief (prompt asks it to call the
messageinfra.get_briefing MCP tool + summarise in zh with a `>` spoken line)
and pushes the result to connected surfaces. Opt-in (off by default).

Security model — autonomous-origin sandbox (new tool_guard primitive, chosen
deliberately over alternatives): an unattended self-clocked turn has no human to
approve a gate and runs over untrusted content, so it must not be able to act.
tool_guard._AUTONOMOUS_ORIGINS ({"briefing"}): gate() REJECTS any name-keyed /
requires_confirmation / write-side-effect tool from these origins regardless of
mode (even off), audited REJECTED-AUTONOMOUS. So an injected briefing can read
(get_briefing/web_search/read_file) but never shell/write/exfil. The check runs
BEFORE the OFF short-circuit, so default installs are protected. Reusable for
all future proactive features.

Hardening from a 2-lens adversarial review (loop/cancellation/cost verified
sound; these are the real findings, all fixed):
- HIGH: full-tool autonomous turn over untrusted content + default-off gate →
  injection→shell. Fixed by the read-only autonomous sandbox above (Arcana's
  create_chat_session has no per-session tool allowlist, so the gate is the
  right enforcement point).
- MED: brief bypassed _chat_in_flight → a self-upgrade could re-exec mid-brief.
  Now wrapped in _chat_in_flight_cm so self_upgrade defers the re-exec.
- MED+MED: per-delta relay fan-out (separate loop) reorders, and an unsolicited
  stream clobbers an in-flight user turn's shared bubble. Fixed by collecting
  the turn and pushing ONE self-contained {"type":"response","kind":"briefing"}
  frame instead of streaming per-delta.
- LOW: quoted `enabled: "false"` was truthy. Now strict-parsed.

config.example.yaml + CLAUDE.md documented. Follow-ups noted: proactive TTS
auto-play, relay/Telegram surfaces, persistent inbox.

563 pytest green (+autonomous-origin + proactive tests). Sandbox probe verified:
briefing-origin shell/write/MCP-write all REJECTED in off mode; get_briefing
AUTO; local origin unaffected.
@tyxben
tyxben merged commit 7d778e2 into main Jun 13, 2026
1 check passed
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