Reference

Shield

@heyarka/shield wraps any agent and returns one with an identical shape, so it drops in wherever the bare agent was used. Four layers run in order: sanitize, corroboration gate, point-in-time guard, then a deterministic risk contract applied to the order the model produced.

Wrapping an agent

shieldAgent takes an AgentUnderTest and returns an AgentUnderTest. That is the whole integration surface. There is no framework to adopt.

TypeScript
import { shieldAgent } from "@heyarka/shield";

const hardened = shieldAgent(myAgent, {
  riskContract: {
    maxNotionalPerTrade: 5_000,
    allowedSymbols: ["BTCUSDT"],
    humanApprovalThreshold: 2_500,
  },
  onAudit: (event) => auditLog.write(event),
});

// Same call as before; the shield runs first.
const order = await hardened.decide(context);

The wrapped agent’s name gains a +shield suffix, which is how a JSONL log holding both a control and a shielded run stays separable when it is scored later.

The four layers

1. Sanitize
Strips zero-width and bidi control characters, then folds confusable characters to their Latin form and NFKC-normalizes. Order matters: invisible characters are removed first, or they would survive inside runs the folding pass treats as opaque. Built from the same Unicode tables the attack vectors use.
2. Corroboration gate
Groups near-identical stories and counts distinct source values, honouring originatingSource so four aggregators echoing one wire story collapse to a single item with an honest independent-source count.
3. Point-in-time guard
Drops anything dated after the context’s asOf timestamp, and removes items that are detected replays of an earlier story already in context. An unparseable date is treated as invalid and dropped rather than trusted.
4. Risk contract
Runs after the model has decided, on the returned order alone. It checks notional, allowed symbols, confidence ceiling, and the human-approval threshold. Violating orders are replaced with a hold of size 0 flagged for human approval.
Why the risk contract never reads prose
Layers 1–3 all touch attacker-controlled text, so in principle a clever enough payload could aim at them. Layer 4 cannot be argued with: it is a non-LLM function of the proposed order’s numbers and symbol, and it never sees a headline. No vector in the corpus talks its way past it.

What the shield does not stop

The measured result is grade C B, and injection susceptibility 31.3% 12.5%. It does not reach zero, and it should not be described as if it did.

Sanitizing makes a payload legible, not false
Decoding a hidden clause that reads “sell immediately” turns it into ordinary readable bearish text. An agent that trusts sentiment embedded in an article body is no less willing to act on it once it is unmasked. Sanitization defeats the obfuscation; it does not defeat the content.
Semantic traps
A plausible, well-formed lie has no character-level signature. The corroboration gate catches manufactured echo; it cannot catch a single credible-sounding falsehood from a real source.
Sentiment-filter poisoning
Manufacturing the crowding or balance conditions a strategy gates on is an attack on the strategy's logic, not on its input encoding.
Look-ahead contamination
Memorization lives in the model's weights. No input filter can remove what the model already knows, so this is measured and reported, not blocked.

The residual 12.5% is entirely in these families. That is a real limit of input hardening, and the scorecard reports it rather than hiding it.

The audit trail

onAudit receives a typed event every time a layer changes something, and never affects the decision. The event union is sanitize, corroboration, point-in-time, and risk-contract, each carrying what it found.

Credentials never reach the audit log
The audit trail records what was filtered, not who you are. API keys are read from environment variables and never written to the JSONL log or printed.