commandsafety

package
v0.2.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 25, 2026 License: Apache-2.0 Imports: 16 Imported by: 0

Documentation

Overview

Package commandsafety provides the public construction API for the initial gate.command-safety permission classifier described in docs/plans/2026-07-27-permission-classifier-hustle-design.md §19.

The classifier reviews permission gates whose prepared request contains command execution or a command-triggered combination of filesystem and network requirements. It owns the classifier's prompt, wire codecs, risk policy, and evidence-tool catalog, and New returns a value that satisfies Harness's public gate.PermissionClassifier contract. A classifier result is evidence, never authority: only trusted Harness code can turn an eligible assessment into an ordinary gate approval.

DefaultPolicy is internal/policy.DefaultPolicy's deterministic tightening policy for the complete gate.ReviewRiskCategory taxonomy: it floors the minimum honest risk a category implies (for example, destructive_shared and production_mutation never resolve below high), routes a fixed set of categories to a human at any authorization level (data_exfiltration, prompt_injection, authorization_conflict, target_ambiguity, and insufficient_evidence — the last three because they are the classifier's own signal that it lacks the confidence auto-approval requires), and requires a minimum authorization at each risk level. Reconcile applies this policy to an already strictly-decoded model assessment and only ever tightens an allow recommendation to needs_human; it never widens eligibility.

StandardEvidence is the complete initial filesystem and Git evidence pack (design §13.2): 5 read-only filesystem tools (canonical path resolution, lstat-style and resolved-target metadata, bounded directory listing, bounded file reading, bounded glob, and bounded grep) confined to the review workspace root through Go's syscall-level, symlink-aware *os.Root, plus 4 read-only Git tools (repository status, diff metadata, configured remotes with optional visibility resolution, and branch/upstream/ default-branch state) that invoke a fixed Git binary with a fixed, non-shell argument list. No evidence tool can write, mutate Git state, or contact a remote with write semantics.

Index

Constants

View Source
const Name hustle.Name = "gate.command-safety"

Name is the stable registration name of the command-safety classifier.

Variables

AbsoluteHumanCategoryFloor is the minimum set of gate.ReviewRiskCategory values every Policy passed to New must mark absolute-human (via Policy.AbsoluteHumanCategories), regardless of what else a caller configures. These are exactly the categories representing the classifier being uncertain about itself (gate.ReviewCategoryAuthorizationConflict, gate.ReviewCategoryTargetAmbiguity, gate.ReviewCategoryInsufficientEvidence — conflicting authorization evidence, an unidentifiable target, or a fact the classifier could not establish even after investigation) or a class of harm severe enough that no policy configuration should ever be able to permit auto-approval (gate.ReviewCategoryDataExfiltration, gate.ReviewCategoryPromptInjection). A caller-supplied Policy may always mark MORE categories absolute-human than this; New only ever floors the set, never widens what a caller can additionally restrict. This closes a real gap: a Policy with an empty or partial AbsoluteHumanCategories used to pass New unchanged, silently discarding this classifier's own safety taxonomy and letting internal/policy.Reconcile validate a model's assessment against nothing.

View Source
var ErrPolicyMissingAbsoluteHumanFloor = errors.New(
	"commandsafety: policy AbsoluteHumanCategories must cover the classifier's own self-uncertainty/safety floor",
)

ErrPolicyMissingAbsoluteHumanFloor is the sentinel New's *ConstructionError wraps as Cause when a caller-supplied Policy's AbsoluteHumanCategories does not cover every category in AbsoluteHumanCategoryFloor. Use errors.Is to detect this specific rejection reason.

Functions

func EncodeAssessmentAsModelOutput

func EncodeAssessmentAsModelOutput(
	subject gate.PermissionReviewSubject,
	risk gate.ReviewRisk,
	authorization gate.ReviewAuthorization,
	categories []gate.ReviewRiskCategory,
	recommendation gate.ReviewRecommendation,
	rationale string,
) (json.RawMessage, error)

EncodeAssessmentAsModelOutput builds a structured-output payload in exactly the shape internal/wire.DecodeOutput requires, echoing subject's own basis. It is the fake/synthetic "model response" builder deterministic evaluation (and tests) use to simulate what a hypothetically correct model would have said for one subject, without ever calling a real model.

func RequiredEvidenceKinds

func RequiredEvidenceKinds() []string

RequiredEvidenceKinds returns the exact set of tool.Requirement.Kind values this classifier's evidence tools declare — the allowlist a consumer must configure (e.g. via Harness's rig.WithPermissionReviewEvidence) for evidence gathering to actually run. The returned slice is a fresh copy; callers may not observe or cause mutation of internal state.

The set is derived from internal/evidence.RequirementKinds, the same single source of truth internal/evidence's own evidence tools declare their Requirement.Kind values from (see internal/evidence/catalog.go). This package never re-declares or hand-copies those kind strings.

func StandardEvidence

func StandardEvidence(policy ReadEvidencePolicy) hustle.EvidenceToolPolicy

StandardEvidence returns the command-safety classifier's evidence-tool policy: the complete filesystem and Git evidence pack from internal/evidence (design §13.2 — canonical path resolution, lstat and resolved-target metadata, bounded directory listing/file reading/glob/ grep, Git repository/worktree state, status and diff metadata, remotes, branch/upstream/default-branch, and remote-visibility evidence), bound by policy.

Types

type CaseFailure

type CaseFailure struct {
	ID     string
	Reason CaseFailureReason
}

CaseFailure records one case that could not be evaluated at all.

type CaseFailureReason

type CaseFailureReason string

CaseFailureReason is a closed, bounded reason a case could not be evaluated at all (as opposed to being evaluated and disagreeing with its expectation). It never carries the underlying error's own text, which could otherwise leak untrusted model or fixture content into a report.

const (
	CaseFailureMarshalInput   CaseFailureReason = "marshal_input_error"
	CaseFailureRespond        CaseFailureReason = "respond_error"
	CaseFailureValidateResult CaseFailureReason = "validate_result_error"
	CaseFailureGatePolicy     CaseFailureReason = "gate_policy_error"
)

type CaseMismatch

type CaseMismatch struct {
	ID               string
	ExpectedEligible bool
	ActualEligible   bool
	Risk             gate.ReviewRisk
}

CaseMismatch records one case whose expected_eligible fixture value did not match what the real pipeline computed. It carries no rationale or other case content — only the bounded fields needed to locate the case and see which direction it disagreed.

type Classifier

type Classifier struct {
	// contains filtered or unexported fields
}

Classifier is the command-safety gate.PermissionClassifier implementation. Its zero value is invalid; construct one with New.

func New

func New(options Options) (*Classifier, error)

New validates options and returns an immutable command-safety classifier.

It requires a non-nil (including non-typed-nil) inference client, a structurally valid model that additionally advertises Tools, StructuredOutput, and StructuredOutputWithTools (design §12.3: a tool-using structured Hustle requires all three), a non-empty policy revision, and an evidence-tool policy hustle.Define accepts. Every rejection is a typed *ConstructionError; no supplied value is echoed.

func (*Classifier) Applies

func (c *Classifier) Applies(subject gate.PermissionReviewSubject) bool

Applies reports whether subject's prepared request contains a command-execution requirement. Applicability is based on the typed tool.CapabilityCommandExecute requirement kind, never a tool display name (design §19.1). A later task extends this to the fuller command-triggered filesystem/network combination.

func (*Classifier) Definition

func (c *Classifier) Definition() hustle.Definition

Definition returns the classifier's immutable hustle definition.

func (*Classifier) MarshalInput

func (c *Classifier) MarshalInput(subject gate.PermissionReviewSubject) (json.RawMessage, error)

MarshalInput projects subject into the classifier's versioned model input.

func (*Classifier) Name

func (c *Classifier) Name() hustle.Name

Name returns the classifier's stable registration name.

func (*Classifier) Revision

func (c *Classifier) Revision() string

Revision returns the classifier's active policy revision.

func (*Classifier) ValidateResult

func (c *Classifier) ValidateResult(
	subject gate.PermissionReviewSubject,
	result hustle.Result,
) (gate.PermissionAssessment, error)

ValidateResult strictly decodes and validates one hustle result against subject, then reconciles the decoded model assessment against this classifier's own deterministic command-safety policy (internal/policy) before returning it. Reconciliation only ever tightens: it can turn an internally inconsistent or taxonomically absolute-human allow recommendation into needs_human, but it never lowers a reported risk, never raises a reported authorization, and never turns an existing needs_human back into allow. It never returns an assessment whose Basis differs from subject.Basis.

type ConfusionMatrix

type ConfusionMatrix struct {
	// TrueAllow: expected eligible, and the real pipeline agreed.
	TrueAllow int
	// TrueHuman: expected human review, and the real pipeline agreed.
	TrueHuman int
	// FalseAllow: expected human review, but the real pipeline was eligible.
	// This is the dangerous direction Task 22 Step 5 requires to be zero for
	// every critical-risk case.
	FalseAllow int
	// FalseHuman: expected eligible, but the real pipeline needed a human —
	// wasted human attention rather than a safety failure.
	FalseHuman int
}

ConfusionMatrix is the auto-approval-eligibility confusion matrix (design §22.7): whether each case's real, pipeline-computed eligibility agreed with its expected eligibility.

type ConstructionError

type ConstructionError struct {
	Field ConstructionField
	Cause error
}

ConstructionError reports why New rejected its Options. Cause, when present, is either an already secret-free typed error from Harness's own construction machinery (hustle.DefinitionError, model.ValidationError) or one of this package's own construction-time validation sentinels (e.g. ErrPolicyMissingAbsoluteHumanFloor, optionally wrapping the fixed, non-secret enum values that failed validation): never raw request content.

func (*ConstructionError) Error

func (e *ConstructionError) Error() string

func (*ConstructionError) Unwrap

func (e *ConstructionError) Unwrap() error

type ConstructionField

type ConstructionField string

ConstructionField identifies the Options field a construction error concerns.

const (
	FieldInference         ConstructionField = "inference"
	FieldModel             ConstructionField = "model"
	FieldModelCapabilities ConstructionField = "model_capabilities"
	FieldPolicy            ConstructionField = "policy"
	FieldEvidence          ConstructionField = "evidence"
	FieldDefinition        ConstructionField = "definition"
)

type EvaluationCase

type EvaluationCase struct {
	ID               string
	Subject          gate.PermissionReviewSubject
	Respond          ModelResponder
	ExpectedEligible bool
}

EvaluationCase is one case to run through the deterministic evaluation pipeline: a fully built, digest-stamped subject, the fake/live model response binding for it, and the end-to-end eligibility a correct classifier's assessment for this scenario should produce. It deliberately does not depend on any specific corpus representation — internal/corpus's own tests adapt corpus.Case values into EvaluationCase, but this type itself is corpus-agnostic so external Options.Inference/Model-holding callers can build cases without importing an internal package.

type EvaluationError

type EvaluationError struct {
	Reason string
}

EvaluationError reports why Evaluate rejected its arguments before running anything.

func (*EvaluationError) Error

func (e *EvaluationError) Error() string

type EvaluationOptions

type EvaluationOptions struct {
	CorpusRevision string
}

EvaluationOptions configures Evaluate. CorpusRevision is required and is carried into Report unchanged (design §22.7 "corpus revision"); this package cannot derive it itself since Report/Evaluate do not depend on any specific corpus representation.

type ModelResponder

type ModelResponder func(subject gate.PermissionReviewSubject) (json.RawMessage, error)

ModelResponder produces one structured-output payload for the given subject. In the deterministic corpus-evaluation mode (design §22.6/§22.7), callers wire this to a synthetic function that returns a pre-baked, per-case expected model output (typically built with EncodeAssessmentAsModelOutput); Evaluate never calls a live inference client itself. A future live-evaluation mode could instead wire this to an actual model-backed classifier connection.

type Options

type Options struct {
	Inference inference.Client
	Model     model.Model
	Policy    Policy
	Evidence  hustle.EvidenceToolPolicy
}

Options configures New. Inference and Model together become the classifier's immutable named model binding; Policy and Evidence become its immutable local policy and evidence-tool catalog.

type Policy

type Policy = policy.Policy

Policy is the consumer-tunable command-safety risk policy: the classifier's own deterministic taxonomy of category risk floors, absolute-human categories, and minimum authorization thresholds (internal/policy.Policy). Revision alone participates in classifier identity (see Definition().Descriptor().PolicyRevision); the taxonomy content it names determines what ValidateResult reconciles a decoded model assessment against before that assessment ever crosses this module's public boundary. Policy is a type alias rather than a wrapper so its Clone method (defensive deep copy of every map field) is inherited directly from internal/policy.

func DefaultPolicy

func DefaultPolicy() Policy

DefaultPolicy returns the initial command-safety policy: a stable, non-empty revision label paired with internal/policy.DefaultPolicy's taxonomy content.

type ReadEvidencePolicy

type ReadEvidencePolicy struct {
	Limits             evidence.Limits
	VisibilityResolver evidence.VisibilityResolver
}

ReadEvidencePolicy configures the read-only evidence collection used by StandardEvidence: the bounded output limits every internal/evidence filesystem and Git tool truncates at the source (Limits' zero value falls back to evidence.DefaultLimits()), and an optional injected VisibilityResolver for evidence_git_remotes (nil means every remote is reported evidence.VisibilityUnknown — see evidence.VisibilityResolver for why this package never performs its own network lookup).

type Report

type Report struct {
	CorpusRevision     string
	ClassifierName     string
	ClassifierRevision string
	// ModelIdentity documents the named model bound to the classifier under
	// evaluation. In today's deterministic, no-live-inference mode this
	// identifies the binding only; no call to it is ever made.
	ModelIdentity string

	TotalCases      int
	ConfusionMatrix ConfusionMatrix

	ByRisk          map[gate.ReviewRisk]int
	ByAuthorization map[gate.ReviewAuthorization]int

	// CriticalFalseAllows and HighRiskFalseAllows are called out from
	// ConfusionMatrix.FalseAllow specifically because Task 22 Step 5 makes
	// the critical-risk count a hard, must-be-zero invariant and the
	// high-risk count a closely watched one.
	CriticalFalseAllows int
	HighRiskFalseAllows int
	// BenignSentToHuman counts low-risk, no-category cases the real
	// pipeline sent to a human anyway — a design smell worth reviewing, not
	// itself a safety violation.
	BenignSentToHuman int

	Mismatches []CaseMismatch
	Failures   []CaseFailure

	// ToolEvidenceUsage and LatencyTokenUsage are always "not applicable" in
	// this deterministic, synthetic-fixture mode: no evidence tool loop and
	// no live model call ever runs. A future live-evaluation mode (a
	// ModelResponder backed by a real classifier connection) would populate
	// these for real instead of reporting this fixed note.
	ToolEvidenceUsage          string
	LatencyTokenUsage          string
	PreviousRevisionComparison string
}

Report is one deterministic evaluation run's aggregate result (design §22.7). It identifies cases only by ID/category-shaped fields in the aggregate counts — never by echoing scenario description or model rationale text — so a report built from synthetic, non-sensitive fixtures stays that way, and a report built from live (potentially sensitive) data in a future live-evaluation mode would not accidentally leak raw content through this shape either.

func Evaluate

func Evaluate(classifier *Classifier, cases []EvaluationCase, options EvaluationOptions) (Report, error)

Evaluate runs cases through classifier's real MarshalInput/ValidateResult pipeline (which itself exercises internal/wire and internal/policy) and Harness's real, unmodified gate.EvaluatePermissionAssessment, aggregating deterministic evaluation metrics. It never invokes classifier's own bound inference client: every model response comes from each case's own Respond function.

A per-case failure (a MarshalInput, Respond, ValidateResult, or gate policy construction error) is recorded in Report.Failures and excluded from ConfusionMatrix and the by-risk/by-authorization breakdowns; it never aborts the run. Evaluate itself only returns a non-nil error for an invalid top-level call (a nil classifier, no cases, or an empty CorpusRevision).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL