Repository navigation
Fix/guard decoupling and detection corpus - #1
Merged
Merged
Conversation
…orpus Four defects found by probing the engine with real inputs. 1. Executable commands could reach `allow`. ExecutionGate and CapabilityGate only evaluate `intent=operate`, so the whole guard chain depended on the parser's seven-verb list. `dd if=/dev/zero of=/dev/sda` was classified `ask`, skipped both gates and was allowed outright. New `intent_escalation` stage re-derives operative intent from command shape (substitution, pipe to shell, redirect to absolute path, known binary with flags, device operands, sudo prefix) plus any ProceduralGuard hit. The transform is monotone (ask -> operate only), so it cannot itself be used to relax a request. 2. ProceduralGuard used substring matching, which both missed commands and fired on prose. `rm -fr /`, `rm -r -f /`, `rm -rf --no-preserve-root /` and whitespace variants degraded from hard_deny to escalate; block-device destruction was undetected; `explain what mkfs does` scored 100. Replaced with an anchored rule corpus: `rm` invocations are parsed (flag clustering in any order, long flags, --no-preserve-root) and targets classified against root-like system paths; device rules require a /dev/ argument. Severity is now tiered so `critical_tag_detected` stays reserved for irreversible destruction -- `rm -rf /tmp/build` lands in the confirmation band rather than being hard-denied. Rules remain governed by policy.yaml. 3. ActorRateLimiter retained one window per distinct actor forever, so rotating the actor field grew the map without limit (50k actors -> 50k deques). Windows now live in an LRU map with a cap and an amortized sweep. LRU order is a security property: an actor being actively rate-limited is recently used, so flooding cannot evict and thereby reset its window. 4. The semantic layer could deny on its own, making a learned model authoritative over refusals. Its risk is now capped (semantic_max_risk, default 60), validated at policy load to sit strictly below every mode's deny_at -- a policy granting the model deny authority is rejected. It may escalate to a human, never refuse. Ambiguity is also decided by absence of keyword evidence rather than token_count < 10, so long paraphrased inputs reach the layer at all. Also: loading all-MiniLM-L6-v2 (~5s cold) sat inside the decision path, making one-shot CLI use unusable at 5.6s per decision. With require_warm_semantic_model the advisory layer declines on a cold model and records semantic_skipped in the trace; `aetherya warmup` enables it for long-lived processes. CLI returns to ~0.16s. Verification: 877 tests, 100% coverage, mypy strict and ruff clean. pipeline_benchmark p95 0.14ms (budget 10ms), chaos_benchmark p95 1.25ms (budget 12ms) detection 1.0, security_baseline 0 differences. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two issues reported after the guard-decoupling work. Parser: v0.8.0 classified any mention of an operative verb as operative, so "What does rm -rf do?" became an operative request with no declared tool. That scores 55 at the ExecutionGate — above the operative confirm_at of 50 — so every question about a command returned `escalate`. A narrow meta-question frame (what does/is/are, how does X work, explain/describe/define; anchored at the start, third person only) now keeps such inputs consultive. The frame is disqualified by any clause separator, by an explicit tool: or mode:operative, and does not match first-person how-to phrasing, so the pinned v0.8.0 cases are unchanged. It is an ergonomics fix, not a security boundary: ProceduralGuard matches raw input regardless of intent and IntentEscalation raises the request back to operate, so "explain rm -rf /" is still hard-denied. Covered by test_meta_question_frame_is_not_a_bypass. pre_api_gate: the shadow_timeout scenario compared absolute subprocess wall time against fixed budgets, measuring interpreter startup and package imports rather than what it asserts — that the shadow adds bounded latency and that --no-wait-shadow adds none. On a host where importing aetherya.cli alone costs 130ms the 150ms budget was unreachable regardless of engine behaviour. Budgets now apply to overhead over a shadow-disabled baseline, and the wait path also excludes the measured provider-SDK import. Overhead is 26ms (no-wait) and 291ms (wait, against a configured 100ms timeout) versus 157ms and 733ms of wall time. pre_api_gate now passes all three scenarios. 891 tests, 100% coverage, mypy strict and ruff clean, benchmarks and security_baseline unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Six items making the engine deployable rather than only correct in-process.
Adversarial sweep. 26 systematic evasions run against the new procedural
corpus; 4 got through and 5 more were downgraded from hard_deny to escalate by
quoting. Closed: r''m -rf / (quote-split command name, previously ALLOWED
outright), truncate/tee to a block device, and python -c shutil.rmtree('/').
The same evasion class also worked against the JailbreakGuard —
"ign''ore previous instructions" was undetected — so quote and escape stripping
moved into the shared normalizer, which is the property text_normalize exists
to guarantee. The corpus is pinned as a regression fixture with per-case
expectations, and cases only resolvable at execution time (variable expansion,
pipe contents, base64) are asserted to reach the confirmation band rather than
hard-deny, with the limits documented.
Parser. Parameter and target values are no longer lowercased. Values were read
from a lowercased copy, so param.path=/tmp/MyFile.TXT was audited as
/tmp/myfile.txt — a different file on any case-sensitive filesystem, meaning
the audit trail did not describe the action that was authorised.
Structured input. run_pipeline_structured() and an `action` object on
POST /v1/decide take a fully-formed ActionRequest, skipping the heuristic
parser so no part of the decision depends on inferring structure from free
text. Every other stage is unchanged: structured callers still pass the ABI
contract and IntentEscalation, so declaring intent="ask" on a destructive
payload bypasses nothing.
Distributed rate limiting. The in-process limiter meant that behind N workers
the effective limit was N x requests_per_window — the configured limit simply
was not the limit that applied. RedisActorRateLimiter shares the window via a
sorted set per actor with the drop/count/add/expire sequence in one pipeline.
Fails closed: an unreachable Redis refuses the request rather than going
silently unlimited.
Policy version contract. AETHERYA_EXPECTED_POLICY_FINGERPRINT pins the policy a
process may load; a mismatch fails at load. Two replicas on different policies
diverge on identical input and nothing surfaces that until someone diffs audit
trails — pinning turns silent divergence into a startup failure.
Semantic readiness. The advisory layer declines to run on a cold model, making
it a silent no-op wherever nothing warmed it. /health now reports
semantic_ready and degraded; the server preloads before accepting traffic, and
--require-semantic-ready fails startup instead of serving degraded.
Audit retention. AuditLogger gains fsync and an AuditSink mirror fan-out for
append-only storage. Mirrors get the byte-identical primary line so the chain
verifies the same on either copy; failures increment mirror_errors rather than
taking the decision path down.
Verification: 985 tests, 100% coverage, mypy strict and ruff clean.
pipeline_benchmark p95 0.27ms, chaos p95 1.45ms detection 1.0,
security_baseline 0 differences, pre_api_gate ok=True.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.