6a8a7b31c1
Three generic harness fixes from the web-ui postmortem dataset. Nothing here keys on a language, framework, build tool or task type. 1. Failure attribution. WorkflowFailedEvent carries one primary FailureAttribution (AGENT | HARNESS | WORKFLOW | ENVIRONMENT | PROVIDER | OPERATOR | UNKNOWN), defaulted to UNKNOWN so pre-field events replay unchanged. FailureAttributor is the deterministic reason->layer mapping, used both at emission and when classifying history, so the baseline and the live metric are one measurement. Emission sites set it: failWorkflow derives from the reason unless the caller knows the layer, cancellation is OPERATOR, the server catch-all falls back to HARNESS, a grounding-rejected plan is AGENT. Multi-cause chains stay on FailureTicketOpened — no second causal structure. GET /metrics/failure-attribution (FailureAttributionInspectionService, mirroring ToolReliabilityInspectionService) reports counts, share, UNKNOWN share, the preserved reasons and the ticket categories from the same sessions. Read-only: historical events are classified at READ time and reported as `inferred`, never written back over an append-only log. Baseline over the local log, 122 terminal failures: AGENT 51 (41.8%), OPERATOR 28 (23.0%), WORKFLOW 19 (15.6%), PROVIDER 15 (12.3%), HARNESS 6 (4.9%), ENVIRONMENT 3 (2.5%), UNKNOWN 0. 2. The `~` guard bug. ToolPath is now the ONE canonical normalization rule (expand a leading `~`/`~/`, keep absolutes, anchor relatives on the session working dir). Every filesystem tool, all six plane-2 path rules and the approval preview resolve through it, so policy and existence checks inspect the path the tool will operate on. `~/.gradle/init.d/offline.gradle` used to resolve to `<workspace>/~/.gradle/...`: reported non-existent AND in-workspace, so the reference gate called a real file a hallucination and the out-of-workspace prompt never fired. Containment and external-read approval behaviour are unchanged — the expanded path is simply outside the workspace, where it always belonged. 3. file_copy (#713). A first-class tool with the writer's jail, tier, receipt, replay and CAS pre/post images; static and binary assets no longer move through the model's token stream. Needed one generic split: ParamRole.SOURCE_PATH marks a path a call reads FROM, so containment gates judge both params while write-target gates (read-before-write, stale-write, write scope, write manifest) judge the mutated one. ReadBeforeWriteRule exempts any call declaring a SOURCE_PATH: its content comes from disk, not from memory, and requiring a read of a binary is unsatisfiable. Existing tools declare no SOURCE_PATH, so their behaviour is byte-identical. Tests: ToolPathTest (9), FailureAttributionTest (10), PathNormalizationRuleTest (6), FileCopyToolTest (10), plus a home-relative FileReadTool read. ./gradlew check green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3.4 KiB
3.4 KiB
core/toolintent — AGENTS.md
Purpose
Plane-2 tool-call intent validation: evaluates proposed tool calls against workspace policy rules before execution, records the assessment as an event, and provides WorldProbe for recording environment observations needed by rules.
Ownership
CORREX kernel team. This module enforces Hard Invariant #9 for the tool-call path.
Local Contracts
ToolCallAssessor— evaluates a proposed tool call against all activeToolCallRules; returns aToolCallAssessmentRecord.ToolCallRule— interface for a single validation rule. Built-in rules:PathContainmentRule— tool must write within the workspace root.ReadBeforeWriteRule— file must be read before being overwritten.StaleWriteRule— detects write to a file that has changed on disk since last read.NetworkHostRule— egress must be on the allowlist.ManifestContainmentRule— write target must appear in the write manifest.WriteScopeRule— enforces declared write scope.ReferenceExistsRule— referenced entities must exist.ExecInterpreterRule— interpreter must be in the allowed list.CycleExitRule(incore:validation) — imported separately; not defined here.
WorkspacePolicy— aggregates rules and configuration for a workspace.WorldProbe— performs environment checks (filesystem, network) and records the observations as events immediately (Hard Invariant #9). Never callWorldProbeduring replay.EgressAllowlist— current egress allowlist; rebuilt fromEgressAllowlistProjection(incore:events).ParamValueExtractor— extracts typed parameter values from tool call arguments.candidatePathStrings= every path-like argument (ParamRole.PATH+ParamRole.SOURCE_PATH), used by the containment/existence gates;writeTargetPathStrings= only the paths a call MUTATES (ParamRole.PATH), used by the write-target gates. A tool declaring none of those roles falls back to sniffing path-like strings, soshellis unaffected.RiskMapping— maps rule violations to risk levels forcore:risk.SessionContext— session-scoped context passed to rules during evaluation.
Work Guidance
- Hard Invariant #9: all
WorldProbecalls record observations as events. Replay reads those recorded events — it must not callWorldProbeagain. - Hard Invariant #5: every tool call must be assessed before execution. Assessment result is recorded as
ToolCallAssessmentEventsincore:events. - New rules implement
ToolCallRuleand are registered inWorkspacePolicy. Do not add rule logic directly toToolCallAssessor. - Resolve every model-supplied path through
ToolPath.resolve(core:tools) — the one canonical normalization rule, shared with the filesystem tools. A rule that resolves paths itself will judge a different path than the tool operates on (the~bug:~/xresolved to<workspace>/~/x, so a real home-directory file was reported as a non-existent in-workspace file and the out-of-workspace prompt never fired). - A call that declares a
SOURCE_PATHtakes its content from disk, not from the model, soReadBeforeWriteRuleexempts it: requiring a read of a copied file's bytes is unsatisfiable for binaries and defeats the purpose of the tool.
Verification
./gradlew :core:toolintent:test --rerun-tasks
Child DOX Index
No child AGENTS.md (leaf module).