Skip to content

feat: tail-load global RULES.md into per-turn prompt - #3

Open
benhoverter wants to merge 5 commits into
mainfrom
feat/rules-md-tail-load
Open

feat: tail-load global RULES.md into per-turn prompt#3
benhoverter wants to merge 5 commits into
mainfrom
feat/rules-md-tail-load

Conversation

@benhoverter

Copy link
Copy Markdown
Owner

Summary

Adds a per-turn loader for ~/.openfang/RULES.md that appends user-defined global rules to every agent's system prompt. Hot-reloadable (re-read each turn), 2KB cap, missing file = no-op.

Commit chain

  • d1271c4 feat(types): add read_global_rules() loader with 2KB cap
  • 3746232 feat(runtime): add rules_md field to PromptContext
  • 20e022f feat(runtime): render RULES.md section at position 14.5 (after Workspace Context, before Safety override line)
  • 7ba629b feat(kernel): wire RULES.md loader into per-turn prompt build
  • fd497d5 docs: document global RULES.md mechanism + example template (docs/global-rules.md, docs/templates/RULES.md.example)

Behavior

  • Loader runs each turn during prompt build — edits to ~/.openfang/RULES.md take effect on the next message, no daemon restart.
  • Files larger than 2KB are silently truncated.
  • Missing file renders nothing (no error, no section).
  • Position 14.5 places rules after workspace context but before the Safety section, so Safety still overrides.

Test plan

  • Manual smoke test: dropped - Always end responses with 🦊 in ~/.openfang/RULES.md, sent message to coder-openfang, rule was respected on next turn.
  • 2KB cap test (oversized file truncates without error)
  • Missing-file test (deletion reverts behavior cleanly)

🤖 Generated with Claude Code

Introduce read_global_rules() in openfang-types::config that reads
$OPENFANG_HOME/RULES.md, trims it, and returns None if absent or blank.
Output is truncated to RULES_MD_MAX_CHARS (2000) so a runaway file can't
balloon the system prompt.

Companion to the per-turn reload path: the kernel calls this on every
turn so edits to RULES.md land on the next message without restart.

Includes 4 unit tests: missing file, empty/whitespace, present, oversized.
Adds Option<String> rules_md alongside context_md. Field is per-turn
reloadable; the kernel will populate it from read_global_rules() on
each turn. Default is None so existing call sites keep compiling via
#[derive(Default)].
Insert a new ## Global Rules (RULES.md) section into build_system_prompt
between Workspace Context (14) and Live Context (15). The framing makes
the rules authoritative over earlier guidance, with one explicit
carveout: the Safety section above remains non-negotiable.

Section is elided when ctx.rules_md is None or trims to empty so the
prompt stays clean for users who never write a RULES.md.

Includes 4 unit tests covering presence, blank/None elision, ordering
relative to Safety + Live Context, and subagent inheritance.
Populate PromptContext.rules_md via openfang_types::config::read_global_rules()
at both PromptContext construction sites in kernel.rs. Re-read on every turn
alongside context_md so edits to ~/.openfang/RULES.md take effect on the
very next message — no daemon restart required.
- docs/global-rules.md: full reference for the per-turn RULES.md
  overlay — contract, position in the system prompt (14.5), authority
  + Safety carveout, subagent inheritance, gotchas, worked example.
- docs/templates/RULES.md.example: starter template with common
  patterns (style preferences, channel etiquette, escalation rules)
  all commented out so users uncomment what they want.
- docs/README.md: index updated under Getting Started and Important
  Paths.
benhoverter added a commit that referenced this pull request May 20, 2026
…spatch

Bridge-side plumbing for upstream MCP forwarding (commit #3 of 4).

Lib:
- `ToolDispatcher` gains a default `upstream_tools()` method returning
  the cached upstream MCP tool catalog. Backward-compatible default impl
  for existing dispatchers.
- `Bridge::permitted_tools()` appends upstream tools to the built-in
  surface. Built-ins remain gated by `allowed_tools()`
  (`OPENFANG_BRIDGE_ALLOWED` / `DEFAULT_ALLOWED`); upstream tools are
  ungated here — server-side `mcp_servers` gating already ran when the
  daemon answered `ListUpstream`.
- `Bridge::call_tool()` accepts a call if the tool is either a permitted
  built-in OR a `mcp_*` name that was advertised in the upstream cache.
- Helper `upstream_def_to_tool()` converts the wire-format
  `UpstreamToolDef` to an rmcp `Tool`, with empty-object schema fallback
  for non-object input schemas.

Bin:
- `main()` performs a one-shot `ListUpstream` round-trip immediately
  after `handshake()`, while still owning the stream. Failures are
  logged and downgraded to "no upstream surface this session" — the
  bridge stays alive with built-ins only.
- `list_upstream()` helper factored alongside `handshake()`.
- `spawn_ipc_actor()` takes the cached `Vec<UpstreamToolDef>` and
  stores it on `IpcDispatcher`.
- `IpcDispatcher::call()` mirrors the bridge-side gate: allowed
  built-in OR advertised upstream `mcp_*` name. Anything else is
  refused locally with `NotPermitted` — early-exit hygiene; daemon
  remains the source of truth.

Tests:
- lib: `permitted_tools_appends_upstream_after_builtins`,
  `is_advertised_upstream_matches_only_advertised_names`.
- bin: `list_upstream_handshake_and_mcp_dispatch` (end-to-end fake
  daemon → handshake → ListUpstream → mcp_* round-trip + unadvertised
  rejection), `list_upstream_propagates_error_result`.

Verification:
- `cargo check -p openfang-mcp-bridge` clean.
- `cargo check -p openfang-api` clean (no trait-method exhaustiveness
  regressions on the daemon side).
- `cargo test -p openfang-mcp-bridge --lib --bins` → 16/16 green
  (13 lib + 3 bin).

No behavior change for agents without upstream MCP servers configured —
they get `Ok { tools: vec![] }` from the daemon and the bridge continues
exactly as before.
benhoverter added a commit that referenced this pull request Jul 7, 2026
…ss gaps

Addresses the confirmed findings from the adversarial review of edits 2-5.

CRITICAL — forgeable wake (#1) + reserved-prefix hijack (RightNow-AI#6):
The wake marker is an in-band, agent-writable title prefix on the SHARED task
queue, so any agent with the ordinary task_post tool could forge a wake
(fake sender/target/trigger) that the consumer would dispatch — bypassing both
the agent_send_async allowlist AND the cycle/depth guards. Fixed by splitting
the write path: ordinary task_post now REJECTS WAKE_TASK_PREFIX titles, and a new
privileged task_post_wake (reached only via KernelHandle::wake_post, which the
capability-gated producer calls) is the sole writer into the wake namespace.
Test: test_ordinary_task_post_rejects_forged_wake.

HIGH — self-wake cycle guard bypass (#2):
sender is always a UUID but target could be a NAME, so would_cycle's string
compare missed a self-wake-by-name. The producer now canonicalizes target to its
registered agent-id BEFORE the cycle check (and validates existence), so
self-wake/cycle detection compares like-for-like.

HIGH — unbounded detached LLM loops (#3):
The consumer spawned an unbounded detached agent loop per claimed wake. Added a
MAX_INFLIGHT_WAKES (8) semaphore; a permit is reserved BEFORE each claim, so a
wake is never flipped to in_progress unless a slot is free to run it now.

MEDIUM — consumer trusted payload (RightNow-AI#5):
run_woken_agent_loop now re-checks the claimed envelope's lineage depth against
DEFAULT_MAX_WAKE_DEPTH and refuses (defense-in-depth) before dispatch.

LOW — poison payload stalled the drain (RightNow-AI#9):
claim_wake_for_dispatch now skips an undecodable/unparseable wake (completing it
with an error) and claims the next, bounded by MAX_POISON_SKIPS, instead of
returning early and halting the tick.

DEFERRED (need a policy number or a schema change — flagged for Ben):
- stale in_progress reaper (#4/RightNow-AI#7/RightNow-AI#8/RightNow-AI#14): wants a claimed_at column + lease TTL.
- completed wake-row GC / retention (RightNow-AI#11): task_queue-wide policy.
- per-sender produce budget / rate limit (RightNow-AI#12): ties into inbound-lineage threading.

Full workspace compiles; memory wake+task and runtime tool-def suites green.
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