Files
yhh 493504224a refactor(layers): contracts stop importing the evidence ledger
Three moves close the edges from the contract layer into runtime/evidence:

- The audit records the host emits beside a run (ReadinessAudit,
  OutcomeSample, DelegationAudit) move to contract/hostaudit, so event and
  trajectory carry them without the ledger that computes them.
  ClassifyEvidenceOrigin, which reads delegation text, stays in evidence as
  a function over the record.
- taskcontract and plancontract move from contract to runtime: only the
  agent and completion consume them, and both are built from evidence
  receipts rather than declaring data anything lower reads.
- evidence moves from runtime to safety. It imports nothing above the
  subsystem layer, and builtin tools and the trajectory recorder, both
  subsystems, depend on it.

layer-order drops from 18 to 5.
2026-09-23 20:17:32 -07:00

64 lines
2.6 KiB
Go

package agent
import (
"encoding/json"
"strings"
"reasonix/internal/base/shellparse"
"reasonix/internal/contract/tool"
"reasonix/internal/safety/evidence"
)
// shellVerificationVerdict reads a verification's outcome off what the host
// actually knows: bash's own per-stage statuses when it reported them, then the
// exit status when it can only belong to the check, and inconclusive when a
// later stage of the same command could have decided it instead.
func shellVerificationVerdict(command string, declared bool, pipeStatus []int, err error) string {
switch evidence.VerificationOutcomeFromPipeStatus(command, pipeStatus) {
case evidence.VerificationPassed:
return tool.ShellVerificationPassed
case evidence.VerificationFailed:
return tool.ShellVerificationFailed
}
switch {
// A declared check already passed the whole-command shape gate in preflight,
// so its exit status is the verdict without the name table having a say.
case !declared && !evidence.VerificationExitConclusive(command):
return tool.ShellVerificationInconclusive
case err != nil:
return tool.ShellVerificationFailed
default:
return tool.ShellVerificationPassed
}
}
// hostReadsCheckThroughPipeStatus reports whether the pipe-status probe will
// answer this call's check, which is the only thing the maskable-shape block
// protects against. The shape test parses bash, so a shell whose pipelines it
// cannot read fails it and the block still stands.
func hostReadsCheckThroughPipeStatus(args json.RawMessage) bool {
command := bashCommandFromArgs(args)
if command == "" || isBackgroundTaskCall(string(args)) {
return false
}
// Both halves are required: the probe answers the check, and nothing outside
// that pipeline can swallow a mutation's failure on the way there.
return shellparse.MasksOnlyInsideFinalPipeline(command) &&
evidence.PipeStatusCanDecideVerification(command)
}
// shellVerificationNotice tells the model at the moment it happens that the
// check it just ran cannot be read. The alternative is finding out at the end
// of the turn, from a gate whose only visible cause is a command it watched
// succeed.
func shellVerificationNotice(output string, execution *tool.ShellExecution) string {
if execution == nil || execution.Verification != tool.ShellVerificationInconclusive {
return output
}
const notice = "note: this command's exit status is its last stage's, not the check's, so the host cannot read the check's result from it. Re-run the check on its own if it needs to count as verification."
if strings.TrimSpace(output) == "" {
return notice
}
return output + "\n\n" + notice
}