HushSpec/Docs
HUSHED · MMXXVI
Documentation v1.0
v1.0.0referencestable

SDK API Contract#

The four reference SDKs -- Rust (hushspec), TypeScript (@hushspec/core), Python (hushspec) and Go (github.com/backbay-labs/hush/packages/go/hushspec) -- are meant to be isomorphic: the same policy, the same decision, the same evidence, and as far as each language allows, the same names for the same things. This page is the contract. It lists, capability area by capability area, the entry point each SDK publishes today, what the four are required to agree on, and where they deliberately differ because the language does.

Names below describe the v1 release surface. Source links on the website are pinned to its recorded source revision. Three tests pin the core of this surface so a rename is a deliberate act rather than a silent divergence:

The other three SDKs are differentially fuzzed against Rust, so Rust has no pinning test of its own; crates/hushspec/src/lib.rs is its surface.

Reading the tables#

  • A bare name is exported at the package root (hushspec::parse, import { parse }, from hushspec import parse, hushspec.Parse).
  • A qualified name is reachable only through its module (hushspec::signing::sign_policy, hushspec.adapters.*).
  • -- means the capability does not exist in that SDK under any name.
  • Rust items marked (feature) need the named Cargo feature; Python items marked (extra) need the named optional dependency.

Result conventions#

The largest deliberate difference. Each SDK reports failure the way its language does, and the shape differs even though the outcome does not.

SDKFallible call returnsThrowing variant
RustResult<T, E> with a thiserror error enumnone -- ? is the idiom
TypeScriptParseResult / ResolveResult: { ok: true, value } or { ok: false, error, code }parseOrThrow, resolvePolicyOrThrow
Python(ok, value | ErrorMessage) tuplesparse_or_raise, resolve_or_raise, resolve_with_options_or_raise
Go(T, error)none -- if err != nil is the idiom

Only four Python functions use the tuple convention: parse, resolve, resolve_file and resolve_with_options; everything else returns a result object. Likewise in TypeScript only parse and resolve return the ok union -- resolveWithOptions, merge, the evaluate* family and compilePolicy return directly and throw.

Verification APIs preserve a reason code for a failed check. Rust represents failure as an error result; the other three use an outcome value. Do not confuse a successfully returned outcome object with a valid signature:

SDKVerification result
RustResult<Verified, VerifyError>; VerifyError::reason_code()
TypeScriptVerificationOutcome: { ok: true, ... } or { ok: false, reason, detail }
PythonVerifyResult (.valid, .reason)
GoVerifyResult (.OK, .Reason)

Parse and validate#

OperationRustTypeScriptPythonGoSemanticsNotes
Parse YAML to a documentHushSpec::parseparse, parseOrThrowparse, parse_or_raiseParseFail-closed: unknown members at any depth, YAML aliases, merge keys, duplicate keys, multi-document streams and yes/no booleans are all parse errors (core spec 2.4)Rust uses serde deny_unknown_fields; the ports check the closed key sets of their generated contract
Optional string in the documentOption<String>string | undefinedstr | None*stringAn absent property and one written as "" are distinct: the canonical form keeps the empty string, so the two hash differentlyAn optional enum is a plain string in Go; "" is not one of its values
Serialize back to YAMLHushSpec::to_yaml----MarshalRound-trips a parsed documentTS and Python callers use their own YAML library
YAML profile check aloneschema::yaml_profile_violationyamlProfileViolation----Reports the profile violation without a full parsePython and Go fold the check into parse / Parse
Validate a documentvalidatevalidatevalidateValidateTypes, enums, uniqueness, numeric bounds, the regex profile and when conditions. Never throws; collects every error
Validation resultValidationResult::is_validValidationResult.validValidationResult.is_valid(*ValidationResult).IsValidA boolean plus errors and warningsPython's is a property, Go's a method, TS's a plain field
One validation errorValidationError (enum)ValidationError (.code)ValidationError (.code, .kind)ValidationError (.Code, .Kind)Carries the document path and messageRust's is a typed enum with no code string; see Error codes
Condition validationconditions::validate_conditionvalidateCondition, validateConditionsvalidate_condition, validate_conditionsValidateCondition, ValidateConditionsA malformed when is a document error, not a runtime deny
Regex profilecompile_profile_regexisSafeRegexis_safe_regexCompileProfileRegexThe ReDoS-safe profile of core spec 3.14; a pattern outside it is E005All four answer by compiling under the profile: Rust and Go return the compiled regex, TS and Python a boolean, and the four accept the same patterns
Document limitsschema::MAX_DOCUMENT_BYTES, MAX_DOCUMENT_DEPTH, MAX_NODE_COUNTMAX_DOCUMENT_BYTES, MAX_DOCUMENT_DEPTH, MAX_NODE_COUNTparse.MAX_DOCUMENT_BYTES, parse.MAX_DOCUMENT_DEPTH, parse.MAX_NODE_COUNTMaxDocumentBytes, MaxDocumentNestingDepth, MaxDocumentNodeCount1 MiB, depth 32, 100 000 nodes -- identical in all fourGo spells the depth limit MaxDocumentNestingDepth; Python's three are module-level, not in __all__
Governance findingsvalidate_governance, GovernanceFinding, GovernanceSeverity------Separation of duties, overdue review, changelog order (core spec 2.5)Rust and h2h audit only. The metadata date format (E011) is checked inside validate in all four

Merge, resolve, verify-on-load, digest pins#

OperationRustTypeScriptPythonGoSemanticsNotes
Merge two documentsmergemergemergeMergedeep_merge (default), merge, replace, per merge spec 4.1
Resolve extendsresolve_with_loader, resolve_from_pathresolve, resolveFromFileresolve, resolve_fileResolve, ResolveFileFolds the chain root to leaf. A cycle, a missing base or an over-deep chain is a refusal, never a partial document
Resolve with provenanceresolve_with_options, resolve_path_with_optionsresolveWithOptions, resolveFromFileWithOptionsresolve_with_options (+ _or_raise)ResolveWithOptions, ResolveFileWithOptionsReturns a Resolution: the folded document, its canonical content_hash, and one chain link per hopThe Level 4 entry point -- a receipt needs the Resolution, not the document
Resolution valueResolution, ChainLinkResolution, ChainLinkResolution, ChainLinkResolution, ChainLink{source, content_hash, signature} per hop, leaf last
Wrap an already-resolved documentResolution::from_resolvedresolutionFromResolvedResolutionNewResolutionFromResolvedAn in-memory leaf records source: "memory"
In-memory sourceMEMORY_SOURCEMEMORY_SOURCE (INLINE_POLICY_SOURCE deprecated alias)MEMORY_SOURCEMemorySource"memory"
Verify-on-load optionsResolveOptions (require_signature, keyring, verify)ResolveOptions (requireSignature, keyring, verify)ResolveOptions (require_signature, keyring, verify)ResolveOptions (RequireSignature, Keyring, Verify)Signing spec 6.5: every hop is checked, the load fails closed when a signature is required and absent or bad, and the outcome is recorded in receipt.policy.signatureRust's keyring / verify fields need the signing feature; Python's need the signing extra
Signature outcome per hopSignatureStatusSignatureStatusSignatureStatusSignatureStatus{verified, key_id, verified_at, reason}verified_at is the verifier's clock, not the envelope's signed_at
Load-time reason setSignatureStatus::reasonLoadReasonCode, isLoadReasonCode, loadReasonOfLOAD_REASON_CODES, load_reason_ofLoadReasonCodes, LoadReasonOfThe closed set a hop's reason may carry: the signing spec 6.5 load-time codes (missing_signature, no_keyring, signing_unavailable, digest_mismatch, invalid_pin) plus the section 6.4 envelope checksA reason outside the set reads as missing_signature: the hop proved nothing
Signature locatorSignatureLocatordefaultSignatureLocatordefault_signature_locatorDefaultSignatureLocator<policy>.sig beside the document
Digest pinsplit_digest_pin, own_content_hashsplitDigestPinresolve.DIGEST_PIN_MARKERReasonInvalidPin, ReasonDigestMismatch, OwnContentHashextends: "<ref>#sha256:<hex>". Every fragment is read as a pin, so one that is not exactly sha256: plus 64 lowercase hex digits is invalid_pin (core spec 2.3). A mismatch always rejects, and a matching pin satisfies require_signature for that hopEnforced identically in all four; only the helper spelling differs
Built-in rulesetsload_builtin, BUILTIN_NAMESloadBuiltin, BUILTIN_NAMESload_builtin, BUILTIN_NAMESLoadBuiltin, BuiltinNamesbuiltin:<name> and builtin:library/<vertical>/<name> resolve with no file systemGenerated from rulesets/ and library/ by scripts/generate_*_builtins.py
Composite loadercreate_composite_loadercreateCompositeLoader, createBuiltinLoadercreate_composite_loader, create_builtin_loaderResolveLoaderBuiltin first, then fileGo takes a loader function rather than a factory
HTTPS loaderresolve::http::load_from_https, resolve::http::fetch_signature (feature http)createHttpLoader, createSyncHttpLoadercreate_http_loader, fetch_signatureNewHTTPLoader, ValidateURLhttps: only; host allowlist; every resolved address checked against the reserved-range block list after DNS, then pinned for the connection; no redirects; 1 MiB cap; connect and read timeouts; ETag revalidation; <url>.sig then <stem>.sig sidecar lookup (core spec 2.6, signing spec 7.1, security spec)Identical rule set in all four SDKs. Python and Go loaders are opt-in through ResolveOptions/loader registration; supply the loader or a pre-resolved Resolution
Resolve failure reasonResolveErrorResolveError, resolveErrorReasonResolveRejectedResolveReason, InvalidPinError, NotFoundError, CycleError, MaxDepthErrorA machine-readable reason (invalid_pin, not_found, cycle, max_depth, ...) rather than a bare string

Compiled policies#

OperationRustTypeScriptPythonGoSemanticsNotes
Compile a documentCompiledPolicy::compilecompilePolicycompile_policyCompilePolicyEvery regex, path glob, host pattern, tool set, when condition, severity table and detector is prepared once. Decisions, traces, hashes and receipts are unchanged from the uncompiled path
Compile a ResolutionCompiledPolicy::from_resolutioncompileResolutioncompile_policy (accepts either)CompilePolicy (takes resolution.Spec)Keep the Resolution for guard/receipt provenance; Go compilation takes its document, not the resolution itself
Compile errorCompileErrorCompileErrorCompileErrorCompileErrorStrict by default: a pattern outside the regex profile fails at compile time, naming the offending rule pathNon-strict keeps the evaluator's deferred deny: TS { strict: false }, Python strict=False
Cached content hashCompiledPolicy::content_hashCompiledPolicy (cached on first use)CompiledPolicy (cached)(*CompiledPolicy).ContentHashComputed once, reused by every receipt
Loading facadePolicy (from_path, from_str, resolve, verify, compile)------load -> resolve -> verify -> validate -> compile as one chainRust only; the other three compose the free functions
Cache behind the free functions(explicit)WeakMap keyed on the documentsmall compiled-policy cache(explicit)Hold a compiled policy for repeated evaluation instead of assuming a cache behind every free functionRust and Go make the caller hold the CompiledPolicy

Evaluation#

OperationRustTypeScriptPythonGoSemanticsNotes
Evaluateevaluate, CompiledPolicy::evaluateevaluateevaluateEvaluate, (*CompiledPolicy).Evaluateallow, warn or deny with matched_rule and reason. Precedence deny > warn > allow; an unknown action type denies; no early return on an allowlist matchCore spec 5 and 6.1
Evaluate with a recorded traceevaluate::evaluate_tracedevaluateTracedevaluate_tracedEvaluateTracedThe trace is recorded during evaluation, never reconstructed: every applicable block in evaluation order under the receipt schema's closed rule_block idsReceipt spec 4.3 -- the Level 4 requirement
Evaluate with runtime contextevaluate_with_contextevaluateWithContextevaluate_with_contextEvaluateWithContextSupplies the RuntimeContext a when clause reads
Evaluate with detectionevaluate_with_detectionevaluateWithDetectionevaluate_with_detectionEvaluateWithDetectionRuns the detector registry, then folds the result into the decisionTraced forms: evaluate_with_detection_traced, evaluateWithDetectionTraced, EvaluateWithDetectionTraced
ActionEvaluationActionEvaluationActionEvaluationActionEvaluationAction{type, target, content?, origin?, posture?, args_size?, url?, network?, timeout_ms?, context?}Rust's field is action_type, serialized as type; Go's Content is a *string so absent and empty stay distinct
ResultEvaluationResult, DecisionEvaluationResult, DecisionEvaluationResult, DecisionEvaluationResult, Decision{decision, matched_rule, reason, origin_profile, posture}Go's values are DecisionAllow, DecisionWarn, DecisionDeny
Traced resultevaluate::TracedEvaluationTracedEvaluationTracedEvaluationTracedEvaluationThe result plus the ordered rule_trace
Unknown-action sentinelevaluate::UNKNOWN_ACTION_TYPE_RULEUNKNOWN_ACTION_TYPE_RULEUNKNOWN_ACTION_TYPE_RULEUnknownActionTypeRule"__unknown_action_type__"
Normalizationevaluate::normalize_host, normalize_path, host_pattern_matches, path_glob_matches, punycode_encodenormalizeHost, normalizePath, hostPatternMatches, pathGlobMatches, punycodeEncodenormalize_host, normalize_path, host_pattern_matches, path_glob_matches, punycode_encodeNormalizeHost, NormalizePath, HostPatternMatches, PathGlobMatches, PunycodeEncodeCore spec 3.14, byte for byte across the fourglob_matches is crate-private in Rust

Conditions#

OperationRustTypeScriptPythonGoSemanticsNotes
Condition valueConditionConditionConditionConditiontime_window, context, all_of, any_of, not, capability, rate
Evaluate a conditionevaluate_conditionevaluateConditionevaluate_conditionEvaluateConditionAn unevaluable condition means active: the rule block still runs and can still denyFail-closed -- a when clause never turns a deny into an allow by failing
With posture capabilitiesevaluate_condition_with_capabilitiesevaluateConditionWithCapabilitiesevaluate_condition_with_capabilitiesEvaluateConditionWithCapabilitieswhen.capability holds when the effective posture state grants that capability (core spec 3.13)
Capability identifieris_capability_identifierisCapabilityIdentifieris_capability_identifierIsCapabilityIdentifierThe grammar a capability name must matchLint L021 flags a capability no posture state grants
Rate conditionRateCondition, RateComparisonRateCondition, RateComparison, RATE_COMPARISONSRateCondition, RateComparisonRateCondition, RateComparison, RateComparisonGte, RateComparisonLt, RateComparisons{counter, threshold, comparison} against RuntimeContext.counters, supplied by the engine. A missing counter is unevaluable, so the block stays activeThe closed comparison set is gte, lt
Runtime contextRuntimeContextRuntimeContextRuntimeContextRuntimeContextuser, environment, deployment, agent, session, request, custom, counters, current_time
Nesting limitconditions::MAX_NESTING_DEPTHMAX_NESTING_DEPTHconditions.MAX_NESTING_DEPTHMaxNestingDepth8
Timezone checkconditions::timezone_is_knowntimezoneIsKnowntimezone_is_knownTimezoneIsKnownAn unknown zone is a validation error, not a silent UTC fallbackPython needs tzdata where there is no system zoneinfo

Detection#

OperationRustTypeScriptPythonGoSemanticsNotes
Detector interfaceDetectorDetectorDetectorDetectorname, category, detect
RegistryDetectorRegistryDetectorRegistryDetectorRegistryDetectorRegistry, NewDetectorRegistryCustom detectors register beside the built-ins
Default registryDetectorRegistry::with_defaults, default_detector_registryDetectorRegistry.withDefaultsdefault_detector_registryWithDefaultDetectors, NewDefaultDetectorRegistryThe same four detectors in the same orderNewDefaultDetectorRegistry is the isomorphism alias of WithDefaultDetectors; both build the same registry
Regex detectorsRegexInjectionDetector, RegexJailbreakDetector, RegexExfiltrationDetectorthe same threethe same threeNewRegexInjectionDetector, NewRegexJailbreakDetector, NewRegexExfiltrationDetectorregex_injection@1, regex_jailbreak@1, regex_exfiltration@1
heuristic_injection@1detection::HeuristicInjectionDetectorHeuristicInjectionDetectorHeuristicInjectionDetectorNewHeuristicInjectionDetectorThe normative integer-scored detector of detection spec 3.5. The signal table and the uppercase rule are fixed, so every engine reproduces the same score for the same inputRust does not re-export it at the crate root -- reach it as hushspec::detection::HeuristicInjectionDetector
Detector namedetection::HEURISTIC_DETECTOR_NAMEHEURISTIC_DETECTOR_NAMEHEURISTIC_DETECTOR_NAMEHeuristicDetectorName"heuristic_injection"The @1 suffix is appended from a private DETECTOR_ID_VERSION in all four, so a receipt's detector_id reads heuristic_injection@1
Signal tabledetection::HEURISTIC_FAMILIES, HEURISTIC_UPPERCASE_WEIGHT, HEURISTIC_UPPERCASE_MIN_LETTERS, HEURISTIC_UPPERCASE_MIN_PERCENTthe same fourHEURISTIC_FAMILIES (weights module-level)HeuristicFamilies, HeuristicUppercaseWeight, HeuristicUppercaseMinLetters, HeuristicUppercaseMinPercentPublished so a third-party engine can reproduce the score without reading the source
Level from scoreDetectorLevel::from_scoredetectorLevelDetectionResultDetectorLevelFromScorenone, low, suspicious, high, critical
Result typesDetectionResult, DetectionCategory, MatchedPattern, DetectorEvaluationthe same fourthe same fourthe same fourdetection_trace in a receipt is built from DetectorEvaluation

Canonical form and content hash#

OperationRustTypeScriptPythonGoSemanticsNotes
Canonical JSON of a documentcanonical_jsoncanonicalJsoncanonical_jsonCanonicalJSONSchema defaults materialized, then RFC 8785 (JCS). Byte-identical across the four for the same resolved documentCanonical form spec
Canonical JSON of any valuecanonical_json_value, serialize_jcscanonicalizeValuecanonical_json_valueDigestOfPlain JCS with no schema step -- what receipts and log entries hash
Content hashcontent_hashcontentHashcontent_hashContentHashsha256: plus 64 lowercase hex over the canonical form of the resolved documentThe portable identity of a policy; h2h hash prints it
Hash prefixCONTENT_HASH_PREFIXinlineinlineunexported"sha256:"
Single-hop hashown_content_hashinternalinternalOwnContentHashOne hop with extends and merge_strategy stripped -- what a digest pin compares
ErrorCanonicalErrorCanonicalErrorCanonicalErrorerrorA value JCS cannot represent (NaN, a non-string key) is an error, never a lossy encoding

Receipts and receipt hash#

OperationRustTypeScriptPythonGoSemanticsNotes
Format versionRECEIPT_VERSIONRECEIPT_VERSIONRECEIPT_VERSIONReceiptVersion"0.2"
Evaluate and recordevaluate_auditedevaluateAuditedevaluate_auditedEvaluateAuditedTakes a Resolution, an action, an AuditConfig and an AuditContext; returns a format 0.2 receiptContent is never carried -- only action.content_hash and content_size. Go returns (DecisionReceipt, error), the error for a resolution with no canonical form, where the other three raise
From a bare documentevaluate_audited_specevaluateAuditedSpecevaluate_audited_specEvaluateAuditedSpecResolves the leaf as memory first
Receipt valueDecisionReceiptDecisionReceiptDecisionReceiptDecisionReceiptreceipt_version, UUID v7 receipt_id, millisecond timestamp with time_source, actor, policy, action, decision, recorded rule_trace, detection_trace, required enforcementValidates against hushspec-receipt.v1.schema.json
Parse a receiptDecisionReceipt::parseparseReceiptparse_receiptParseReceiptAccepts exactly what the 0.2 schema accepts; every receipts/invalid/ vector is rejected
Validate a receipt in handDecisionReceipt::validateinternalinternal(*DecisionReceipt).ValidateThe structural rules of the 0.2 schema a typed model cannot express -- closed enums, string patterns, non-empty members, non-negative sizes -- reported all at once rather than first-failureParsing already applies them, so this is for a receipt built or mutated in memory; TypeScript and Python run the same checks inside their parse
Rule-block enuminternalinternalinternalReceiptRuleBlocks$defs.RuleEvaluation.rule_block: the twelve keys of rules, then the five engine stages of receipt spec 4.3 item 5The bare spellings are normative; 0.1 mixed egress and rules.egress
Receipt hashDecisionReceipt::receipt_hashreceiptHashreceipt_hash(*DecisionReceipt).ReceiptHashsha256: over the JCS form of the receipt -- what the log chains and the signer signs
Receipt canonical JSONDecisionReceipt::canonical_jsonreceiptCanonicalJsonreceipt_to_dict + canonical_json_value(*DecisionReceipt).CanonicalJSONTS re-exports canonicalJson as receiptCanonicalJson so the policy one stays unambiguous
Policy summarypolicy_summary, PolicySummarypolicySummary, PolicySummarypolicy_summary, PolicySummaryNewPolicySummary, PolicySummarycontent_hash, extends_chain, signature -- the policy identity every receipt embeds
Policy hashcontent_hashcomputePolicyHashcompute_policy_hashComputePolicyHashThe canonical sha256: hash; 0.1's per-SDK digest is gone
Unverified-policy receiptunverified_policy_receipt, POLICY_UNVERIFIED_RULEunverifiedPolicyReceipt, POLICY_UNVERIFIED_RULEunverified_policy_receipt, POLICY_SIGNATURE_RULEUnverifiedPolicyReceipt, PolicyUnverifiedRuleThe receipt a refused guard emits for every action; matched_rule is "__hushspec_policy_unverified__"TS exports both spellings -- POLICY_SIGNATURE_RULE aliases POLICY_UNVERIFIED_RULE
Provider-failure rulePOLICY_PROVIDER_RULEPOLICY_PROVIDER_RULEPOLICY_PROVIDER_RULEPolicyProviderRulematched_rule "__hushspec_policy_provider__": the policy provider could not serve a policy to evaluate against (core spec 6.2)Emitted by the TypeScript guard, which asks its provider per action; the other three take each reload as a push and keep the policy in force, and export the constant for receipt readers
Deterministic ids and timedeterministic_uuid_v7, format_timestampdeterministicUuidV7, uuidV7, formatTimestampdeterministic_uuid_v7, format_timestampDeterministicUUIDv7, NewUUIDv7, FormatTimestampThe fixed inputs fixtures/receipts/expected/README.md pins, so the expected-receipt vectors reproduce byte for byte
Audit config and contextAuditConfig, AuditContext, ActorAuditConfig, AuditContext, ActorAuditConfig, AuditContext, ActorAuditConfig, DefaultAuditConfig, AuditContext, ActorAuditConfig is {enabled, include_rule_trace, record_duration}0.1's redact_content is gone: a 0.2 receipt never carries content

Hash-linked log#

OperationRustTypeScriptPythonGoSemanticsNotes
Format versionLOG_VERSIONLOG_VERSIONLOG_VERSIONLogVersion"0.1"
Genesis hashGENESIS_HASHGENESIS_HASHGENESIS_HASHGenesisHashsha256: plus 64 zeros -- prev_hash of the first entry
Chained sinkChainedFileSink::openChainedFileSinkChainedFileSinkOpenChainedFileSinkAppends JSONL with an fsync and an exclusive lock per entry; rotation carries prev_hash through a log_started entry; per-entry signatures optional
Lock timeoutChainedFileSink::LOCK_TIMEOUTLOCK_TIMEOUT_MSlog.LOCK_TIMEOUT_SECONDSLogLockTimeoutFive seconds in all four: an append that cannot take the lock in that time is an error, never an unlocked writeEach SDK spells the unit its own way; Python's is module-level, not in __all__
EntryLogEntry, EntryType, PayloadLogEntry, EntryType, PayloadLogEntry, EntryTypeLogEntry, EntryType, LogPayloadreceipt, policy_loaded, policy_swapped, log_started
Entry hashLogEntry::compute_entry_hashcomputeEntryHashLogEntry(*LogEntry).ComputeEntryHashJCS over the entry minus its own hash
Verify one fileverify_logverifyLogverify_logVerifyLogReports the first break by line number; a broken chain is never a partial passLevel 5 requires naming the line
Verify a rotated setverify_logs, verify_log_filesverifyLogs, verifyLogFilesverify_logs, verify_log_filesVerifyLogs, VerifyLogFilesOldest first; the link between files is checked too
ReportLogVerifyReport, LogErrorLogVerifyReport, LogBreakLogVerifyReportLogVerifyReport, LogErrorCounters plus the break's file, line and message
Policy eventsPolicyEvent::loaded, PolicyEvent::swappedpolicyLoadedEvent, policySwappedEventPolicyEvent, policy_event_to_dictNewPolicyLoadedEvent, NewPolicySwappedEventWhich policy came into force when, and what it replacedDelivered through the sink's record_policy_event
SDK identitySdkInfo::this_sdk (hushspec-rs)thisSdk (@hushspec/core)SdkInfo (hushspec-python)ThisSDK, SDKName (hushspec-go)Recorded in every entry (log spec 6)Deliberately distinct -- this member says who wrote the line

Signing, keyrings, receipt signing#

Rust needs the signing Cargo feature; Python needs the signing extra (pip install "hushspec[signing]"), without which every entry point below raises SigningUnavailable rather than reporting an unverified signature as good.

OperationRust (feature signing)TypeScriptPython (extra signing)GoSemanticsNotes
Envelope versionsigning::FORMAT_VERSIONSIGNATURE_FORMAT_VERSIONENVELOPE_FORMAT_VERSIONSignatureFormatVersion"0.2"Format 0.1 signed file bytes and cannot be verified by 0.2
Algorithmsigning::ALGORITHMSIGNATURE_ALGORITHMSIGNATURE_ALGORITHMSignatureAlgorithm"ed25519"
Sign a policysigning::sign_policysignPolicysign_policySignPolicySigns the content hash of the resolved policy, not the file's bytes: reformatting keeps a signature valid, changing a base reached through extends invalidates it
Verify a policysigning::verify_policyverifyPolicyverify_policyVerifyPolicyReturns valid, or invalid with one of the eleven closed reason codes of signing spec 6.4All four pass all 18 fixtures/signing/vectors.yaml cases
Sign / verify a bare hashsigning::sign_content_hash, verify_content_hashsignContentHash, verifyContentHashsign_content_hash, verify_content_hashSignContentHash, VerifyContentHashThe primitive the policy, receipt and log signers share
Envelopesigning::EnvelopeEnvelope, parseEnvelope, envelopeSigningInputEnvelope, parse_envelope, signing_inputEnvelope, ParseEnvelope, MarshalEnvelopehushspec-signature.v1.schema.json
Reason codessigning::ReasonCode (ALL, as_str, from_code)ReasonCodeREASON_CODESReasonMalformedEnvelope ... ReasonPolicyVersionRollbackThe closed set of signing spec 6.4 -- identical strings in all fourRust's is an enum with a &'static str wire form; TS's is a type-level union only
Keyringsigning::KeyringKeyring, loadKeyring, keyringFromPublicKeyKeyring, load_keyringKeyring, LoadKeyring, KeyringFromPublicKeyhushspec-keyring.v1.schema.json, with retirement and revocation
Key idsigning::key_idkeyIdFromPublicKeykey_id_from_public_keyKeyIDFromPublicKeySHA-256 of the SPKI DER -- never chosen by the signer
Keypairsigning::generate_keypairgenerateKeypaircryptographyParsePrivateKeyPEM, MarshalPrivateKeyPEMPKCS#8 and SubjectPublicKeyInfo PEM
Sign a receiptsigning::sign_receiptsignReceiptsign_receiptSignReceiptSigns the receipt hashSignedReceipt in all four
Verify a receiptsigning::verify_receiptverifyReceiptverify_receiptVerifyReceipt
Clock skew defaultsigning::DEFAULT_MAX_CLOCK_SKEW_SECONDSDEFAULT_MAX_CLOCK_SKEW_SECONDSverify_policyDefaultMaxClockSkewSeconds300 seconds

Policy bundles#

OperationRust (feature signing)TypeScriptPython (extra signing)GoSemanticsNotes
Parse a bundleDsseEnvelope::parseparseBundleparse_bundleParseBundleA DSSE envelope over an in-toto Statement v1Readable by generic DSSE and in-toto tooling
Verify a bundleverify_bundleverifyBundleverify_bundleVerifyBundleThe four ordered checks of bundle spec 5.2, returning valid or one of the seven closed reason codes of 5.4All four pass all 10 fixtures/bundle/vectors.yaml cases
Reason codesBundleReasonBUNDLE_REASONS, BundleReasonBUNDLE_REASON_CODESBundleReasons, BundleReasonMalformed ...malformed_bundle, unknown_key_id, key_revoked, key_retired, dsse_signature_mismatch, subject_digest_mismatch, policy_mismatch
Statement and predicateStatement, PolicyBundlePredicate, SubjectBundleStatement, PolicyBundlePredicate, BundleSubjecthushspec.bundleBundleStatement, PolicyBundlePredicate, BundleSubjectThe subject is the canonical form of the resolved document; the predicate carries every hop with its hash and signature status
PAEpaepaehushspec.bundleBundlePAEDSSE pre-authentication encoding
Create and signbuild_statement, sign_statement, unsigned_envelope------Production is Rust and h2h bundle create onlyVerification is what a relying party depends on, and all four verify
Version and typesBUNDLE_VERSION, PAYLOAD_TYPE, STATEMENT_TYPE, PREDICATE_TYPEBUNDLE_VERSION, BUNDLE_PAYLOAD_TYPE, BUNDLE_STATEMENT_TYPE, BUNDLE_PREDICATE_TYPEBUNDLE_VERSION, PAYLOAD_TYPE, STATEMENT_TYPE, PREDICATE_TYPEBundleVersion, BundlePayloadType, BundleStatementType, BundlePredicateType"0.1" plus the three URIsTS prefixes the three type constants

Guard, enforcement modes, refused state#

OperationRustTypeScriptPythonGoSemanticsNotes
The enforcement pointHushGuardHushGuardHushGuardGuardCompiled policy, enforcement mode, warn channel, sink, observers, actor and clock behind check / evaluate / enforceGo spells it Guard -- HushGuard would stutter as hushspec.HushGuard
ConstructHushGuard::from_path, from_policy, from_resolution, builderHushGuard.fromFile, fromYaml, fromProviderHushGuard.from_file, from_providerNewGuard, NewGuardFromFile, NewGuardFromProviderCompiles once at constructionRust uses a builder (HushGuardBuilder); the others use options structs or keyword arguments
Check without throwingcheck -> GuardDecisioncheck -> booleancheck -> boolCheck -> (GuardDecision, error)Branch on the language's proceed result; Go errors also stop dispatchThese return types are deliberately not identical
Full gate outcomecheck -> GuardDecisiongate -> GateOutcomegate -> GateOutcomeCheck -> (GuardDecision, error)Rust allowed(), TS/Python proceed, Go Allowed()Receipts reach configured sinks; do not assume every return object embeds one
Enforceenforce -> Result<_, Denied>enforce (throws HushSpecDenied)enforce (raises HushSpecDenied)Check plus GuardDecision.AllowedA deny stops the tool call before its body runsGo has no exceptions, so the caller branches on Allowed()
Record without enforcingevaluateevaluateevaluateEvaluateMonitor-mode observation
Enforcement modeEnforcementMode, EnforcementConfigEnforcementMode, EnforcementConfigEnforcementMode, EnforcementConfigEnforcementMode, GuardOptions.RuleOverridesenforce or monitor, with per-rule-path overrides; longest prefix winsMonitor mode is refused without an observer or a sink that auditing writes receipts to: an unrecorded observation is not evidence
OutcomeEnforcementOutcome, EnforcementSummaryEnforcementOutcome, EnforcementSummaryEnforcementOutcome, EnforcementSummaryEnforcementOutcome, EnforcementSummary, ImpliedEnforcementallowed, confirmed, blocked, would_block -- required on every receipt
Rule-path prefix matchmatches_rule_path_prefixmatchesRulePathPrefixmatches_rule_path_prefixMatchesRulePathPrefixHow an override key matches a matched_rule
Receipt clock trustHushGuardBuilder::time_sourcetimeSourcetime_sourceGuardOptions.TimeSourceThe time_source every receipt the guard emits carries (receipt spec 3.3); system by defaultThe enum is closed, so a value outside it is refused when the guard is built
Warn confirmationon_warn (WarnHandler)onWarn (WarnHandler)on_warnGuardOptions.OnWarn (WarnHandler)Absent, a warn denies (core spec 6)The one place the guard is stricter than the evaluator
Refused staterefused, refusaldenials carry POLICY_SIGNATURE_RULEHushGuard.refusal(*Guard).Refused, GuardRefusalA policy that fails verification under require_signature does not fail construction -- it builds a guard that denies every action with __hushspec_policy_unverified__ and an unverified-policy receiptFailing to build would tempt a caller into running with no policy at all
Hot swapswap_policyswapPolicyswap_policy, swap_resolutionSwapPolicyAtomic. A new policy that will not validate, compile or prove itself leaves the last good one in force and is reported through on_error; a policy that does prove itself leaves the refused stateA swap never enters the refused state: keeping a policy that verified beats adopting one that did not
Panic and refusalalways enforcealways enforcealways enforcealways enforceNeither monitor mode nor a warn handler can let a panic-mode or refused-policy deny through

Observers and metrics#

OperationRustTypeScriptPythonGoSemanticsNotes
Observer interfaceEvaluationObserverEvaluationObserverEvaluationObserverEvaluationObserveron_policy_loaded, on_evaluation, on_error, all defaulted. An observer sees every decision and can change noneAction content is stripped inside the fan-out, before any observer sees it, and the event's content_redacted flag records that it happened. No SDK offers a way to turn it off
Fan-out wrapperObservableEvaluatorObservableEvaluatorObservableEvaluatorNewObservableEvaluatorOne evaluation, every registered observer. evaluate() routes through the detection pipeline, so a detection: escalation is never reported as the base decision
evaluation.completed shapeEvaluationCompletedEventEvaluationCompletedEventevent dictObserverEvent, EvaluationObservationtype, timestamp, the redacted action, content_redacted when it applied, result, duration_us, and enforcement and receipt when the guard produced themThe same JSON in all four, so one collector pipeline reads a JSON line or a webhook payload from any SDK
JSON linesJsonLineObserverJsonLineObserverJsonLineObserverNewJSONLineObserverOne JSON object per event
Console / stderrStderrObserverConsoleObserverConsoleObserverNewStderrObserver, NewDenyOnlyStderrObserverHuman-readableTS and Python kept ConsoleObserver; Rust and Go say where it writes
MetricsMetricsCollector, MetricsSnapshotMetricsCollectorMetricsCollectorNewMetricsCollector, MetricsSnapshotCounters by decision, action type and rule block, plus a latency histogram
Prometheus expositionrender_prometheustoPrometheusto_prometheusRenderPrometheusThe hushspec_evaluate_total, hushspec_evaluate_duration_us, hushspec_rule_match_total and hushspec_policy_load_total seriesThe one method whose name is not isomorphic: Rust and Go say render, TS and Python say to
Latency bucketsDURATION_BUCKETS_USDURATION_BUCKETS_USDURATION_BUCKETS_USDefaultDurationBucketsUs10, 25, 50, 100, 250, 500, 1000, 5000, 10000 microseconds, labelled by action_typeOne list in all four, so a recording rule written against one reads the others
Percentile window--DURATION_WINDOWDURATION_WINDOW--10000 recent samples behind the average and the 99th percentileRust and Go report the histogram alone; the counters and the histogram are exact in all four
WebhookWebhookObserver (feature http)----NewWebhookObserverBounded queue; drops with a counter rather than blocking an evaluation

Providers and hot reload#

OperationRustTypeScriptPythonGoSemanticsNotes
Provider interfacePolicyProviderPolicyProviderPolicyProviderPolicyProviderRust/Python/Go load a Resolution; TypeScript load() returns Promise<HushSpec> and exposes resolution(), current(), watch() and stop()Preserve the verified chain when adopting a provider result; apply trust requirements on every reload
File providerFileProviderFileProviderFileProviderNewFileProvider
HTTP providerHttpProvider (feature http)HttpProviderHttpProviderNewHTTPProviderBounded HTTPS loader with explicit trust/host configurationProviders must not accept arbitrary model-selected URLs
Callback providerclosureclosureCallbackProviderinterface
WatcherPolicyWatcherPolicyWatcherPolicyWatcherNewPolicyWatcher, PolicyWatcherStats one file per tick; delivers only on a real change (mtime and content hash)
PollerPolicyPollerPolicyPollerPolicyPollerNewPolicyPollerReloads through any provider on an interval; delivers only on a content_hash change
Manual tickPolicyHandlePollerOptionscheck_onceCheckOnceFor tests, and for callers driving their own loop
Panic sentinel per tickpanic_sentinelpanicSentinelpanic_sentinelReloadOptions.PanicSentinelThe kill switch is checked on the same tick as the reload, and fails closed: a sentinel whose absence cannot be proven arms panic modeA watching TypeScript FileProvider checks it on its own tick, because a file watcher wakes only when the policy itself changes
Reload intervalsDEFAULT_WATCH_INTERVAL, DEFAULT_POLL_INTERVALDEFAULT_POLL_INTERVAL_MS; the watcher is event-driven, DEFAULT_SENTINEL_INTERVAL_MS is its panic-sentinel tickDEFAULT_WATCH_INTERVAL_S, DEFAULT_POLL_INTERVAL_SDefaultWatchInterval, DefaultPollInterval1 second for a watcher (a stat unless the file moved), 60 seconds for a poller (a full load, possibly remote)One pair in all four, so the same swap is picked up at the same rate
Failure handlingon_error plus last good policyonError plus last good policyon_error plus last good policyReportError plus last good policyA reload that cannot be read, parsed, resolved, verified or compiled leaves the policy in force untouched, reports, and retriesThere is never a window with no policy
Commit orderchange callback, then commitchange callback, then commitchange callback, then commitchange callback, then commitA driver records a reload as its current snapshot only after the subscriber accepted it, so a rejected document is offered again on the next tick rather than served by current()
Adopted chainsresolved under the provider's optionsre-checked against the guard's requireSignaturere-checked against require_signaturere-checked against GuardOptions.RequireSignatureA hop passes when it is builtin:, pinned by a matching digest, or validly signed (signing spec 6.5)The digest-pin evidence is in-memory only and never reaches a receipt or a bundle

Receipt sinks#

OperationRustTypeScriptPythonGoSemanticsNotes
Sink interfaceReceiptSinkReceiptSinkReceiptSinkReceiptSinksend(receipt), plus an optional record_policy_eventA sink that fails must not break an evaluation; the guard reports it as sink.error naming the sink. Rust adds a defaulted name() a sink may override; all four otherwise report the sink's own type name
FileFileReceiptSinkFileReceiptSinkFileReceiptSinkNewFileReceiptSinkJSONL
StderrStderrReceiptSinkStderrReceiptSink (alias of ConsoleReceiptSink)StderrReceiptSinkStderrReceiptSinkStderrReceiptSink is the isomorphic name; TS keeps ConsoleReceiptSink as the original and pins the alias by identity
FilteredFilteredSinkFilteredSinkFilteredSinkNewFilteredSink, NewDenyOnlySinkRoute only the decisions you keep
MultiMultiSinkMultiSinkMultiSinkNewMultiSink
CallbackCallbackSinkCallbackSinkCallbackSinkNewCallbackSink
NullNullSinkNullSinkNullSinkNullSink
Chained (hash-linked)ChainedFileSinkChainedFileSinkChainedFileSinkOpenChainedFileSinkSee Hash-linked log
OTLPOtlpSink, OtlpConfig (feature otlp)OtlpReceiptSinkOtlpReceiptSinkNewOTLPReceiptSink, OTLPOptionsPOST <endpoint>/v1/logs: one logRecord per entry, INFO/WARN/ERROR by decision, body.stringValue the entry's canonical JSON, and the same hushspec.* attributes and resource attributes in all four. /v1/logs is appended only when the endpoint does not already point at the signal, and an endpoint that is not http or https with a host is refused when the sink is configuredBackground thread or goroutine, bounded queue, 64-entry batches, a first retry backoff of 100ms doubling per attempt. A transport error and the four statuses OTLP/HTTP names as retryable -- 429, 502, 503 and 504 -- are retried; every other status is final, because the collector will answer the same bytes the same way. Export never blocks an evaluation; overflow, a final failure, an exhausted retry and a batch that will not serialize each drop, count and report as sink.error rather than silently losing evidence

Framework adapters#

FrameworkRustTypeScriptPythonGoSemanticsNotes
Anthropic / Claude--mapClaudeToolToAction, createSecureToolHandleradapters.map_claude_tool_to_action, adapters.create_secure_tool_handlerMapClaudeToolToAction, CreateSecureToolHandlerMaps a tool_use block onto the action a policy evaluates: bash to shell_command, text editor to file_read / file_write, computer to computer_use, web_fetch to egress on the host, mcp__server__tool to the inner tool nameNo adapter imports the framework it adapts -- blocks are read structurally
OpenAI--mapOpenAIToolCall, createOpenAIGuardadapters.map_openai_tool_call, adapters.create_openai_guardMapOpenAIToolCall, GuardedOpenAIToolHandler
MCP--mapMCPToolCall, extractDomain, createMCPGuardadapters.map_mcp_tool_call, adapters.extract_domain, adapters.create_mcp_guardMapMCPToolCall, ExtractDomain, GuardedMCPToolHandlerExplicit shared aliases and extraction keys map file, command and egress calls; unknown names remain tool_call under the original name, and canonical-JSON arguments record args_sizefixtures/adapters/mcp-contract.json is run by all three SDKs
Vercel AI SDK--mapVercelToolCall, createVercelGuard----Gates each tool's execute
LangChain--mapLangChainToolCall, wrapLangChainTool, createLangChainCallbackHandleradapters.hush_tool--Python tool_call decorators measure the actual positional and keyword arguments; another action type requires action_mapper(args, kwargs)TS wraps with a proxy, so the tool keeps its prototype, fields and instanceof
CrewAI----adapters.secure_tool--Python tool_call decorators measure the actual positional and keyword arguments; another action type requires action_mapper(args, kwargs)A missing or malformed mapper stops the body before it runs
Generic------ToolActionMapper[T], ToolHandler[T], GuardedToolHandler[T]Go's adapters are one generic wrapper plus three mappers

Rust has no adapter module. Its analogue is the worked example cargo run --example guarded_agent --features otlp, which wires a policy through a guard, a chained sink and an OTLP sink.

Panic mode#

OperationRustTypeScriptPythonGoSemanticsNotes
Activate / deactivateactivate_panic, deactivate_panicactivatePanic, deactivatePanicactivate_panic, deactivate_panicActivatePanic, DeactivatePanicWhile active, every evaluation returns deny with __hushspec_panic__
Queryis_panic_activeisPanicActiveis_panic_activeIsPanicActive
Sentinel filecheck_panic_sentinelwatcher optioncheck_panic_sentinelCheckPanicSentinelFail-closed: an I/O error reading the sentinel is treated as present
Deny-all policypanic_policypanicPolicypanic_policyPanicRuleThe document panic mode evaluates as
Scoped statePanicState (new, shared)------Rust can mint an independent latch per policy instead of the process-wide onePanicState::default() is the shared latch, so the free functions keep working
Rule nameevaluate::PANIC_RULEPANIC_RULEPANIC_RULEPanicRule"__hushspec_panic__"

Version constants#

ConstantRustTypeScriptPythonGoValue
Spec version writtenHUSHSPEC_VERSIONHUSHSPEC_VERSIONHUSHSPEC_VERSIONVersion"1.0.0"
Minors acceptedversion::HUSHSPEC_SUPPORTED_MINORSHUSHSPEC_SUPPORTED_MINORSHUSHSPEC_SUPPORTED_MINORS, SUPPORTED_MINORSSupportedMinors["0.1", "0.2", "1.0"]
Representative versionsversion::HUSHSPEC_SUPPORTED_VERSIONSHUSHSPEC_SUPPORTED_VERSIONS, SUPPORTED_VERSIONSHUSHSPEC_SUPPORTED_VERSIONS, SUPPORTED_VERSIONSSupportedVersions["0.1.0", "0.2.0", "1.0.0"]
Acceptance testversion::is_supportedisSupportedis_supportedIsSupportedAccepts every X.Y.Z of a supported minor (core spec 2.2)
Minor of a versionversion::supported_minorsupportedMinorsupported_minorSupportedMinor
Package identityCargo metadataSDK_NAME, SDK_VERSION__version__SDKNameThe SDK's own release, distinct from the spec version
Artifact formatsRECEIPT_VERSION, LOG_VERSION, signing::FORMAT_VERSION, signing::KEYRING_VERSION, BUNDLE_VERSION, REPORT_VERSIONRECEIPT_VERSION, LOG_VERSION, SIGNATURE_FORMAT_VERSION, KEYRING_VERSION, BUNDLE_VERSIONRECEIPT_VERSION, LOG_VERSION, ENVELOPE_FORMAT_VERSION, KEYRING_VERSION, BUNDLE_VERSIONReceiptVersion, LogVersion, SignatureFormatVersion, KeyringFormatVersion, BundleVersion0.2, 0.1, 0.2, 0.2, 0.1

SUPPORTED_VERSIONS in TypeScript and Python, and SUPPORTED_MINORS in Python, are aliases of the HUSHSPEC_-prefixed names, kept because the shorter spelling predates the prefix. The exports tests assert identity rather than equality, so they cannot drift apart.

Error codes#

Every fixtures/<module>/invalid/ vector carries a <name>.expect.yaml sidecar naming the code its refusal must report, drawn from spec/registries/error-codes.yaml. All four fixture runners assert it -- see the SDK Conformance Matrix.

CodeMeaningRustTypeScriptPythonGo
E000Input could not be readCLIErrorCodeERROR_IOErrorCodeInput
E001YAML parse, profile, shape, type or unknown-member errorCLI, testkitErrorCodeERROR_PARSEErrorCodeParse
E002Unsupported hushspec versionValidationError::UnsupportedVersionErrorCodeERROR_UNSUPPORTED_VERSIONErrorCodeUnsupportedVersion
E003Duplicate secret pattern nameValidationError::DuplicatePatternNameErrorCodeERROR_DUPLICATE_PATTERN_NAMEErrorCodeDuplicatePatternName
E004Constraint violationValidationError::CustomErrorCodeERROR_CONSTRAINT_VIOLATIONErrorCodeConstraint
E005Regex outside the HushSpec profileValidationError::InvalidRegexErrorCodeERROR_INVALID_REGEXErrorCodeInvalidRegex
E010extends resolution failedResolveErrorResolveResult.codeERROR_EXTENDSErrorCodeExtends
E011A metadata date that is not an ISO 8601 calendar dateValidationError::InvalidDateErrorCodeERROR_INVALID_DATEErrorCodeInvalidDate

Three deliberate differences:

  • Rust's library does not carry the code strings. ValidationError is a typed enum; the mapping to E00x lives in hushspec_testkit::expect::validation_error_code and in crates/hushspec-cli/src/cmd_validate.rs. An embedder that needs codes maps the enum itself or uses the testkit helper.
  • TypeScript's ErrorCode is a type, not a value. There is no runtime ERROR_CODES array; the codes appear on ParseResult.code, ResolveResult.code and ValidationError.code.
  • Python and Go expose both. hushspec.ERROR_CODES (a tuple) and hushspec.ErrorCodes (a slice) list the eight in registry order, beside named constants and per-value .code / .Code members. Go adds ErrorCodeOf(err) and RegistryErrorCode(kind).

The signing (11 codes), bundle (7 codes) and resolve reason sets are separate closed registries and never overlap with E0xx. They are normative in the signing and bundle specifications.

Cross-SDK invariants#

These are the properties the four SDKs must share. They are not aspirations: each is enforced by a check that fails CI.

InvariantEnforced by
Identical decisions. For any document and action, all four return the same decision, matched_rule, reason, origin_profile and posture.The shared corpus (fixtures/{core,posture,origins,detection}/evaluation) run natively by each SDK, plus hushspec-difftest over 500 generated policy groups per commit, comparing each port against the in-process evaluator.
Identical canonical form and content hash. The same resolved document canonicalizes to the same bytes and hashes to the same sha256: in all four.fixtures/core/hash/ (16 vectors) run by all four; scripts/check_cross_sdk_roundtrip.py; content_hash compared per group by hushspec-difftest.
Byte-identical receipts after JCS under fixed inputs. With the actor, clock, receipt id and audit config that fixtures/receipts/expected/README.md pins, every evaluation case produces the committed receipt byte for byte after RFC 8785.fixtures/receipts/expected/<module>/<fixture>/<case>.json run by all four; receipt_hash compared per group by hushspec-difftest.
Identical recorded rule traces. The same entries, in the same order, under the same closed rule_block ids.The expect.rule_trace assertions of evaluator-test format 0.2, plus the expected receipts.
Identical reason codes. The 11 signing reasons, the 7 bundle reasons and the resolve reasons are the same strings everywhere.fixtures/signing/vectors.yaml (18 cases) and fixtures/bundle/vectors.yaml (10 cases), each asserting the exact code, run by all four.
Identical error codes. Every invalid/ vector is rejected with the code its sidecar names.The four fixture runners, against the .expect.yaml sidecars.
Identical public names. One concept, one name across the four, modulo language casing.tests/exports.test.ts, tests/test_public_surface.py, isomorphism_test.go.
Identical parse refusals. The YAML profile, the document limits and the closed key sets reject the same inputs.fixtures/*/invalid/ plus the generated contract (scripts/generate_sdk_contracts.py), checked for drift by the generated-sources CI job.

Anyone can reproduce these outside this repository: every release attaches hushspec-conformance-<version>.tar.gz with the prose, the schemas, the vectors and fixtures/MANIFEST.json, which pins the corpus by digest. See Conformance Levels and the Conformance Statement template.

Fail-closed rules the four share#

Stated once, because they are why the surface looks the way it does.

  1. An invalid document is never a partially valid one. Parse and validate reject; they do not repair, coerce, or drop unknown members.
  2. An ambiguous rule denies. An unknown action type, an unknown guard type and a malformed pattern all deny.
  3. An unevaluable when means the block is active. A condition that cannot be decided never removes a rule from consideration.
  4. A warn with no confirmation channel denies (core spec 6).
  5. A required signature that cannot be verified refuses the whole policy, and the refusal is itself recorded as a receipt.
  6. A failed reload keeps the last good policy. There is no window in which no policy is in force.
  7. Monitor mode needs somewhere to record. A guard in monitor mode with no observer, and no sink that auditing writes receipts to, is refused at construction.
  8. Panic mode and a refused policy always enforce. Neither monitor mode nor a warn handler can let them through.
  9. Evidence is never silently dropped. A full OTLP queue, a rejected export, an exhausted retry and a batch that will not serialize each drop with a counter and a sink.error event; a sink that fails does not break the evaluation but does surface through on_error.

Loading documentation index…

↑↓ navigate↵ openesc close