#8966·jan

feat(agent): a permission for who may write above the cache line

Author: thinhlpgCreated Sep 16, 2026Updated Sep 17, 2026

Problem

agent.toml already governs what an agent may do. From the scaffolded template at src-tauri/src/core/agent/project.rs:197-203:

toml
# read-only | deny | allow. read-only (default) exposes MCP tools and built-in
# reads; built-in writes/exec go through the permission gate. deny locks down
# ...
# Exposed even under deny; deny-list wins over everything:
deny = []

Nothing governs what may write into the prompt. Every one of these composes into the same string, and none of them declares where it lands:

    assistant instructions  ─┐
    project memory          ─┤
    skills                  ─┼──►  one system prompt  ──►  byte 0 of the request
    MCP-supplied guidance   ─┤
    plan-mode addendum      ─┤
    todo addendum           ─┤
    runtime environment     ─┘

    seven writers · zero declarations · one shared cache line

#8957 and #8958 fix two of these seven. That is a fix, not an invariant — the eighth writer lands next quarter and the regression comes back, because nothing in review says "this goes above the cache line."

Prior art: the failure is compositional

oh-my-pi #11897 is the exact shape of this. Two pi.on("context") handlers each appended a per-request message. Each one was locally correct and reviewed as such.

  0 injectors     baseline                    $0.28 / call
  1 injector      2.90×                       still tolerable
  2 injectors     consumed both rolling         ← the cliff
                  cache breakpoints
  3 injectors     4.3×                        $2.54 / call

  measured across 17 sessions:  $130.02 actual vs $25.69 due  =  80.2% overspend
  one 14-hour session alone:    $1,281 wasted
  98.7% of input tokens written to cache and never read back

No single diff introduced this. Neither handler's author could have seen it, and the agent's output looked completely normal throughout. This is a systemic failure, so it needs a systemic control — the same argument that made [tools] a config section rather than a convention.

Proposed change

Extend the existing permission model to prompt composition, with the same deny-wins semantics.

diff
  [tools]
  default = "read-only"
  deny = []
+
+ [prompt]
+ # Where a component's contribution is placed. Deny-wins, same as [tools].
+ #   "prefix" — above the cache line. Must be constant for the session.
+ #   "tail"   — after the accepted history. Free to change per turn.
+ default = "tail"
+
+ # Components allowed to write into the cached prefix. Everything else is
+ # placed in the tail regardless of what it asks for.
+ prefix_allow = ["assistant_instructions", "tool_schemas"]

Enforced at composition time, not by review:

compose_prompt(components)
  for each component
    placement = policy.resolve(component.id)          # deny-wins
    if placement == prefix
      assert component.is_constant_for_session        # hard failure, not a warning
      prefix.push(component)
    else
      tail.push(component)

Three rules make this hold:

  1. Default to tail. A new writer is safe by default and has to argue its way into the prefix. That is the inversion that stops the eighth writer from being the next #11897.
  2. A prefix component must be constant for the session. Not "usually stable" — constant. Assert it; a component that varies is a bug caught at composition rather than on the invoice.
  3. Deny wins, matching [tools], so an operator can lock a route down without auditing every skill.

Surface the resolved placement in jan cli agent status, next to the tool permissions it already prints (core/cli/mod.rs:535-543). "Which of my skills is writing into the cached prefix" should be one command, not an investigation.

Acceptance criteria

  • A [prompt] section in agent.toml declares placement per component, defaulting to tail.
  • Placement is resolved with deny-wins precedence, matching [tools].
  • Prompt composition enforces the policy; a component cannot reach the prefix without an explicit allow.
  • A component declared prefix that varies within a session causes a hard failure, and a test asserts it.
  • A newly added composer defaults to tail with no config change, and a test asserts the default.
  • jan cli agent status reports the resolved placement for every registered composer.
  • Documented in the agent.toml spec alongside [tools].

Related

  • Runtime counterpart to #8965's compile-time guard test. #8965 catches a new writer in CI; this one bounds the damage when something reaches a user's machine anyway.
  • Turns #8957 and #8958 from fixes into invariants.