Skip to content

Agent runtime defaults review

Decision document for the owner. The table below is the review as written on 2026-10-23; the owner then decided rows 1, 2, 3, 6, 10 and 12 (flipped) and row 11 (kept off), recorded at the end of each of those rows, and nothing else changed. See docs/configuration.md (Agent-runtime defaults set on 2026-10-23). Checked 2026-10-23 against the uncommitted working tree (phases 1 and 2 present, phase 3 in progress), so a key named here may move into a typed settings class; the property names are the contract.

How to read this

  • Profiles. personal is the shipped Spring profile (application-personal.yml): one person, one laptop, often a small local model, the user is also the approver. team and enterprise are not shipped profiles; they describe deployments. Team: one tenant, several trusted users, an admin exists (compose or lite stack). Enterprise: multi-tenant or regulated, Keycloak, Helm, several replicas, audit obligations.
  • Current default is read from core/app-config/src/main/resources/application.yml, application-personal.yml and the code @Value/entity defaults, not from docs.
  • Test on the real path means a test that drives a turn (or a job) through the real guard chain with the control switched on. ApprovalTurnHarness (agents-tools e2e/) is the only such rig, and its own javadoc lists what it does NOT exercise: tool classification, hooks, taint, argument policies, the effect journal, the circuit breaker, the buffered depths (CAREFUL/RIGOROUS/PLANNED), the semantic judge and Spring context assembly. So the answer for every row below is "no turn-level test"; the column names the closest tests that do exist.
  • Approval cost has dropped. Phase 1 (approval continuation, app.approvals.continuation.enabled: true) now resumes the task after a decision, with real-path tests (ApprovalContinuationE2ETest, ApprovalParkedTurnE2ETest, AgentJobApprovalContinuationE2ETest). Controls that add approvals (taint enforce, trifecta require-approval, unclassified require-approval, SUPERVISED) were much costlier before it. This is the main reason some recommendations below are stronger than the audit's.
  • No recommendation weakens a current default. Where personal already differs from the base, the base is not loosened.

Decision table

P = personal, T = team, E = enterprise. "obs" = observe/warn only. "Inert until used" = turning it on changes no behaviour until someone defines a hook, policy or pack.

# Control and current default What it does Cost, latency, false-positive risk Closest tests (no turn-level one) P / T / E Rollout precondition
1 Agent hooks app.agent-hooks.enabled = false Declarative BEFORE/AFTER tool and model hooks, ON_ERROR, ON_RUN_END; webhook hooks (https only, SSRF-guarded). A hook "allow" never approves anything. Off: nothing read. On with no hooks: one cached lookup (15 s). Each synchronous hook point shares a 2 s budget (hard cap 5 s per webhook). A mis-written deny hook refuses legitimate calls. AgentToolGuardHooksTest (real guard chain), AgentHookRuntimeTest off / on / on Inert until used, so low risk. Before on: set webhook.allowed-hosts and signing-secret; add a harness test where a deny hook and a require-approval hook fire inside a turn. Decision 2026-10-23 (owner): flipped. Base true, personal false. Proved by AgentRuntimeDefaultsConfigTest (effective base and personal configuration, component active) and AgentToolGuardHooksTest. Set the webhook allow-list and signing secret before defining a webhook hook.
2 Tool argument policies app.tool-arg-policies.enabled = false Per-agent tighten-only constraints on tool arguments (type, enum, range, regex, URL host, path prefix), refused before the tool runs. Cache 30 s. Inert until a policy exists. A refused call burns a tool step and the model must retry. ToolArgPolicyCallbackTest, AgentToolGovernanceArgPolicyTest off / on / on Decide the lookup-failure rule first: ToolArgPolicyCallback.evaluate still fails OPEN when the policy lookup throws, while evaluation errors refuse and app.guardrails.fail-mode is closed. Make lookup failure follow fail-mode before enterprise relies on it. Decision 2026-10-23 (owner): flipped. Base true, personal false. A policy lookup failure already follows app.guardrails.fail-mode (ToolArgPolicyCallbackTest). Proved by AgentRuntimeDefaultsConfigTest.
3 Taint tracking app.agents.taint.mode = off (personal: enforce) After a turn reads content an outsider could write (web, email, file, third-party MCP), a consequential tool call needs approval (enforce) or is only recorded (observe). Classification memoised 60 s; observe writes one audit row per tool per tainted turn. Enforce: every "search the web, then write/send" task parks. With continuation this is a prompt, not a dead end, but it is still a prompt. Unclassified tools count as sinks. http-get-allow-hosts is unset, so a GET to any host is not a sink. AgentTaintPolicyTest, TaintGateInjectionTest enforce (as is) / obs, then enforce / obs, then enforce Run observe for about two weeks and review the audit rows for the research-then-write pattern. Set http-get-allow-hosts. Add a harness test with taint enforce: read, then a gated write parks, is approved, and the agent finishes. Decision 2026-10-23 (owner): flipped to observe. Base observe (behaviour unchanged, audit rows are the observe period), personal stays enforce. enforce for team and enterprise waits for the audit review. Proved by AgentRuntimeDefaultsConfigTest and AgentTaintPolicyTest.
4 Lethal-trifecta enforcement app.tools.classification.trifecta-enforcement = warn Detects an agent holding private-data read + untrusted-content ingestion + egress. require-approval forces the SUPERVISED tier on save; block rejects the save. require-approval makes the agent SUPERVISED, which gates EVERY tool call (not just the dangerous leg). Agents on Auto-select (empty allow-list, the Simple-mode default) are not analysed unless trifecta-analyse-auto-select is true, and that runs live MCP discovery. TrifectaEnforcementServiceTest, AgentServiceTrifectaEnforcementTest, LethalTrifectaDetectorTest warn / warn / require-approval Decide whether SUPERVISED (gate every call) is the right penalty or a narrower "gate the egress leg" tier is needed. Enterprise also needs trifecta-analyse-auto-select: true, otherwise the control skips the default agent shape. Start with warn metrics to size the affected agents.
5 MCP tool-definition pinning app.mcp.tool-pinning.mode = warn Hashes the tool list an agent sees; flags a change (audit mcp.tools.changed). enforce: a changed registration contributes no tools until an admin approves. Warn already records the baseline, so no extra cost. Enforce: a vendor's harmless description edit silences that server's tools until approved. McpToolPinServiceTest, McpToolPinControllerTest, McpToolPinIntegrationTest (pin service and a local HTTP MCP server) warn / warn / enforce An alert on mcp.tools.changed and a named approver with a runbook. Enforce for one tenant first. Personal has no second person to approve, so keep warn.
6 Skills supply-chain enforce app.skills.supply-chain.enforce = false (scan itself is on) A custom skill whose content changed since an admin approved it, or that scans HIGH and was never approved, is withheld from load_skill and the skill list. Scan is already on, so enforce is free to run. Friction: every edit to a custom skill withholds it until re-approved; personal skill learning drafts skills continuously. SkillSupplyChainServiceTest, SkillScannerTest, SkillIntegrityTest, SkillLifecycleServiceTest off / on / on Confirm the admin re-approve path is reachable in the console and that built-in skills are never withheld (documented, assert it). Watch the "would be withheld" count under warn for a week before turning on for team. Decision 2026-10-23 (owner): flipped. Base true, personal false (scan and pins stay on). Conditions met and asserted in SkillSupplyChainEnforceDefaultTest: built-in skills are never withheld (list, load_skill, report, overview); a withheld skill appears in the review queue and a workspace admin (PERM_MANAGE_AGENTS or platform admin) re-approves it through POST /api/skill-security/skills/{name}/approve, which the Skills page banner and the platform-admin Skill security page call (skillSecurityComponents.test.tsx).
7 Unclassified-tool action app.tools.classification.unclassified-action = allow A tool with no classification row: allow (counted), require-approval, or deny. deny blocks every new third-party MCP tool until an admin classifies it. require-approval parks it. Metric agent.tool.classification.unclassified_allowed shows the exposure. TOOL_MCP_ANNOTATIONS_ENABLED (off) would classify many MCP tools from their safety hints and shrink the set. AgentToolGovernanceTest covers allow, require-approval, deny and fallback (unit, not a turn) allow / allow, then require-approval / require-approval (deny for regulated) Read the unclassified_allowed counter for two weeks; enable MCP annotation classification first; confirm the classification admin screen. The confirmation-gate-exempt-tools list is a separate bypass and should be reviewed with this.
8 Verifier escalation (L4) app.chat.escalation.enabled = false A buffered-depth turn served by the agent's economy model is checked deterministically and re-run once on the primary model on failure. Only matters when an agent has an economy model (cost routing is per-agent opt-in) AND depth is not FAST. Failure re-runs the turn: extra latency and tokens, bounded to once. Replay safety comes from tool classification. VerifierEscalationRouterTest, ChatClientResolverEscalationTest, AgentChatServiceBufferedDepthTest (deep-stubbed ChatClient) off / off / on where cost routing is used At least one agent with an economy model and CAREFUL or higher. Watch chat.router.decision and the wasted-token counter. A flip with no such agent changes nothing.
9 Swarm goal verifier app.swarm.verifier.enabled = false After delegate_parallel, one extra model call checks the joined results against the goal and re-dispatches unanswered sub-tasks (default one repair round, 30 s per call). One extra call per swarm plus up to one repair round of worker calls, inside the $2 run budget. Global flag, not per agent. Verdict can be UNVERIFIED on timeout. SwarmVerifierTest, DefaultCollaboratorToolFactoryTest off / off / on Only worth enabling where teams actually use delegate_parallel. Measure repair-round rate and spend on a pilot; confirm the run budget covers the verifier call.
10 Context compaction app.context.compaction.enabled = false Keeps the prompt inside the model's window: above 60% old tool results become restorable placeholders; above 85% the oldest turns become one summary. Stored history is not deleted. Summary is one cheap-model call (20 s timeout, at most 3 per run; failure = skip). Changes the prompt prefix, so provider prompt caches miss after a compaction. Summary quality on a 4B local model is poor. Separate in-turn result clearing is already on. ChatContextCompactionTest, ContextBudgetPlannerTest, ToolLedgerCompactionTest on / on / on after soak A 100k-token soak or a harness test with an oversized history; set the auxiliary compaction model (app.auxiliary-model.compaction); verify restore_tool_result stays bound to the same agent and session. Without it a long coding or browser task overflows the window, which is a harder failure than a lossy summary. Decision 2026-10-23 (owner): flipped. On in base and personal. Conditions met in ContextCompactionDefaultOnHarnessTest: an oversized history triggers compaction, cleared results restore through restore_tool_result, a failed or blank summary call is skipped safely and not retried every round, and with app.auxiliary-model.compaction unset the agent's own model writes the summary.
11 Turn journal app.turn-journal.enabled = false (${APP_TURN_JOURNAL_ENABLED:false} in application.yml; also app.turn-journal.max-list = 100) Hash-chained, append-only record of a turn's stages (intake, guard, model call, tool call, approval, verify, commit) in turn_journal_event, written for every reasoning depth (FAST, CAREFUL, RIGOROUS, PLANNED) and for delegated or specialist turns, with APPROVAL events and the decision of a continued turn. Synchronous Postgres inserts from the thread that finishes each stage (several per turn; not measured, ADR-0020 says measure with the 32-user benchmark before defaulting it on). No retention purge exists for turn_journal_event (no delete in main code, and an update trigger forbids edits), so the table grows without bound until the operator prunes it. Outcome labelling is now honest: JournalingToolCallback records ok, error, blocked or parked from what the platform answered. The turn id is returned in the X-Journal-Turn-Id response header while the journal is on, and the inspector lists a session's turns; not yet done: audit-seal integration, workflow runs and non-chat job steps are not journaled. TurnJournalChainTest, TurnJournalSessionTest, JournalingToolCallbackTest, ChatTurnExecutorTurnJournalTest, AgentChatServiceBufferedJournalTest, AgentChatStreamJournalHeaderTest, TurnJournalControllerTest off / off / on only after a retention job and a write-cost measurement The earlier "do not enable as built" verdict (wrong outcome labels, FAST-only, id not surfaced) no longer holds for the current tree. It can be switched on, with eyes open: the table is unbounded until a retention job exists, the synchronous writes are unmeasured, and it is not sealed. Enterprise: on once retention is in place. Team: optional, if someone will prune the table. The personal docs correctly leave it off. Decision 2026-10-23 (owner): kept off: write cost unmeasured, retention purge now exists (app.turn-journal.retention-days, default 90).
12 Job step journal and resume AGENT_JOB_STEP_JOURNAL_ENABLED, RESUME_FROM_JOURNAL, RESUME_RERUN_INCOMPLETE_TURN, ownership = all false Journal alone is observability. Resume re-queues an orphaned job whose last turn the journal proves complete (rerun flag also re-runs an unfinished turn). Ownership adds cross-replica leases and adoption. Journal: Postgres rows per step, 14-day purge. Resume is the risky part: a rerun can re-bill a model call, and effect dedupe depends on app.tools.effect-journal, which defaults to the memory store with a 30-minute TTL, so it does not survive the restart that caused the orphan. AgentJobStepJournalResumeTest, AgentJobStepJournalSafetyTest, JobKillSimulationTest, StepJournalRetentionPurgeTest (I did not check whether any use a real Postgres) journal on, resume off / journal on, resume off / all on after the precondition Resume and rerun need a durable effect-journal store; confirm one exists and is wired. Then a kill-and-resume run against Postgres. Ownership needs identical settings on every replica. Personal laptops that sleep get the journal (cheap) but not auto-rerun. Decision 2026-10-23 (owner): journal flipped on everywhere (AGENT_JOB_STEP_JOURNAL_ENABLED=true, base and personal; fail-soft, 14-day purge). Resume, rerun and ownership stay false. Proved by AgentRuntimeDefaultsConfigTest.
13 Learning loop app.skills.learning.enabled = false; app.learning.proposals/lessons/preferences/routines/review.enabled unset (personal: all true) Background distillation of skills, lessons, preferences and routines into proposals a human approves. Drafted skills are replayed against their own turn. Background LLM calls; caps of 6 proposals per agent per day, 30 min cooldown, 50 pending. Risk is noise and, on a shared agent, whose behaviour is being learned and who approves. Everything is propose-then-approve, so nothing self-applies. SkillLearningServiceTest, SkillSelfCheckTest, SkillLearningWiringTest on (as is) / off / off, opt-in per tenant Verify proposals are scoped to the person whose behaviour produced them and approved by that person, not pooled across users of a shared agent. Provide the approval queue UI to the people who will use it.
14 Moderation moderation.enabled = false (a platform setting row, not yml; no row is seeded) Platform-admin inbound gate: policy packs FOUL_LANGUAGE, RESTRICTED_CONTENT, PII, HARASSMENT, EXTREMISM, SELF_HARM, ILLEGAL, CSAM; thresholds review 50, block 90; optional LLM semantic layer (off). BLOCK or QUEUE_FOR_REVIEW short-circuits the turn. Pattern packs are cheap; the semantic layer adds one LLM call per message. False positives: PII and foul-language packs block ordinary messages (emails, quoted text). Output side: on FAST, output checks are audit-only. The in-code comment says moderation fails open on error while app.guardrails.fail-mode says closed: reconcile. ModerationServiceTest, ModerationPolicyServiceTest, AgentChatServiceModerationTest off / off, then review-only on ILLEGAL and SELF_HARM / on: ILLEGAL, CSAM, EXTREMISM, SELF_HARM, HARASSMENT; PII review-only A staffed review queue, thresholds tuned on replayed real traffic, semantic layer left off until its cost is measured. Because the master switch is a DB setting, a profile default needs a seeded row or a yml bridge; none exists. New agents already get the jailbreak guardrail (app.agents.default-guardrails).
15 Injection screening FLAG vs BLOCK gateway content policy: screening ON, action FLAG, PII redaction OFF (hardcoded in LlmGatewayContentPolicy.unconfiguredDefault; tenant row overrides) Regex screen of the current turn's user messages before the model call; FLAG audit-logs and proceeds, BLOCK rejects the call before any provider request. Tool-result injection is a different control (semantic judge). FLAG is free and harmless. BLOCK false positives: anyone discussing or testing prompt injection, pasted security text. Lookup failure falls back to FLAG, so BLOCK is not guaranteed under a database error. LlmGatewayContentPolicyAdvisorTest, LlmGatewayContentPolicyControllerTest FLAG / FLAG / BLOCK after a FLAG review Count FLAG audit rows per 1000 turns and sample for false positives; check what the user sees on a BLOCK. The default is a hardcoded constant, so a per-profile default needs a property (phase 3 owners) rather than a code edit.
16 Default reasoningDepth = FAST (Agent.java, AgentCreationService, V43 column default) Depth selects the pipeline: FAST streams tokens with no verifier and audit-only output guardrails. CAREFUL buffers the reply and adds one self-consistency check (one revise). RIGOROUS adds a Reflexion pass and a per-tool-call verifier (fail-closed by default now). PLANNED adds one planning call. CAREFUL and up lose token streaming, so first-byte latency is the whole reply plus extra calls. RIGOROUS adds a model call per tool call and each rejection burns a tool step. On a small local model the verifier is the same weak model and, being fail-closed, a blank verdict refuses a legitimate call. The verifier still judges only the current user message, so "yes, send it" is judged as "yes". AgentChatServiceBufferedDepthTest (deep-stubbed ChatClient), ChatClientToolCallVerifierServiceTest, AnswerConsistencyCheckerTest FAST / FAST, RIGOROUS for agents with irreversible or egress tools / CAREFUL for external-facing agents, RIGOROUS for side-effecting agents A turn-level test for CAREFUL and RIGOROUS (the harness excludes them), an agreed latency budget, and a decision on the verifier's history blindness. Better than a global flip: choose the depth at agent creation from its tool set.

Notes

  1. Cheapest wins first. Rows 1 and 2 are inert until used, so enabling them for team and enterprise costs nothing and unlocks the feature (saving hooks or policies answers 409 while off). Row 6 enforce and row 10 are the next lowest risk. Rows 3, 4, 7 and 5 change what users see and need a measured observe period. Row 11 should not be turned on at all until fixed.
  2. Observe before enforce is already built in for taint (3), pinning (5, warn records pins), supply chain (6, scan on), trifecta (4, warn) and unclassified tools (7, counter). The decision for each is the duration of the observe period and who reads the data, not new code.
  3. Approvals now close the loop, so the cost of rows 3, 4, 7 and the SUPERVISED tier is a prompt per gated call, not a stalled task. Decision (owner): SUPERVISED no longer needs approval for a read-only call in personal, and still gates every call elsewhere. The setting is app.guardrails.supervised.gate-read-only: true in the base application.yml (SUPERVISED gates every call, including web_search: unchanged, nothing weakened), false in application-personal.yml (one person who is also the approver; every read-only prompt was the main reason a task felt stalled). It is a security key in the settings catalog (guardrails.supervised.readOnlyCalls), so a workspace or an agent can only choose the stricter gate. With it false, a call is let through without a person only when ALL hold: the tool has an explicit classification row that says read-only (never an unclassified tool, never an MCP server's own read-only hint), the agent is positively judged not to hold the lethal trifecta, and nothing else asks for approval (taint, a procedure, a hook, the confirmation gate). Argument policies, hooks, budget and step limits and the circuit breaker still run on it, and each such call is audited (tool.supervised.auto_allowed) and journaled as auto-allowed: read-only under SUPERVISED. What this does NOT lift: remember_fact is classified as a write (WRITE_TENANT), so it is still asked about; an agent that holds the trifecta keeps full gating (row 4 require-approval therefore still means every call is gated for such an agent in every profile); an Auto-select agent is judged by the trifecta analysis only with trifecta-analyse-auto-select on. Team and enterprise keep the base value true; flip it per deployment with APP_GUARDRAILS_SUPERVISED_GATE_READ_ONLY once the audit rows have been read. Test: SupervisedReadOnlyE2ETest.
  4. Personal stays light on purpose. One person is also the approver and the model is small: keep depth FAST, pinning in warn, trifecta in warn, hooks and policies off (nobody authors them), and rely on taint enforce plus the OS sandbox, which are already on. The only personal change recommended is row 10 (compaction) and the job step journal in row 12.
  5. Evidence gap applies to all 16 rows. Every "flip" should land with a harness test that sets the control on and drives a turn; today a green unit suite says the component works in isolation, not that a turn uses it.
  6. Not verified. Nothing was built or run for this document. Test names come from the repository, not from reading each body; costs are structural (calls per turn), not measured; "team" and "enterprise" are described, not shipped profiles; I did not check whether row 12's tests touch a real Postgres or whether row 13's proposals are per-user.