X
Xtrom
ProductSecurityAboutCareersConsole ↗← Home
Blog

Notes on securing AI agents

Identity, runtime enforcement, and hard problems in the agentic enterprise.

Engineering · Milestone

Xtrom v0.1.0-alpha.w4 — Phase 0 is done

Fourth north-star milestone — and the last one of Phase 0. The foundation is locked. Every invariant we promised is now either CI-enforced, differential-fuzzed, or LocalStack-verified. The next milestone opens Phase 1: the console-visible surface of everything the engine is now enforcing.

The full Rust stack, end-to-end, on every shipped bundle

An integration test in engine-bundle now composes bundle bytes → Policy → Engine::authorize against every example bundle we ship under schemas/policy/examples/. Each test pins the rule intent: unknown actions are scope-denied, destructive-in-prod steps up, secrets to non-allowlisted hosts are blocked, untrusted taint into HIGH sinks is blocked, HIGH-flagged verbs step up. A regression here means we shipped a bundle whose canonical example no longer demonstrates its rule — and the build fails.

OTel span shape — frozen as a v1 wire contract

Every enforcement decision emits a single span, xtrom.enforcement.decide, with a locked attribute set: tenant, agent, action, verdict, reason_code, policy_version, env. That is the entire vocabulary. A SIEM or a Grafana dashboard can pin on it and know we will not break it. Crucially: no argument values, no request bodies, no PII. A static forbidden-prefix list rejects any attempt to leak them, enforced at the dataclass boundary before the exporter is even called. And if the exporter itself fails — the network is out, the OTLP collector is down — the exception is swallowed. Observability failures cannot change the verdict; that is what invariant #1 means.

Multi-region residency — one policy, N retention envelopes

A single Xtrom deployment now supports tenants pinned to different AWS regions with different retention requirements. RetentionPolicy.per_region_days lets one policy encode "7 years in EU, 3 years in US" without a second sink subclass — eu-west-1 tenants get their 2555-day Object Lock envelope, US tenants get theirs. The sink validates the region string at construction; the boto3 client handles endpoint routing.

Where Phase 0 leaves us

Enforcement: Rules 0/1/2/3 in Rust, 10 000 differential-fuzz runs per commit, bundle loader with e2e proof. Identity: four IdP brokers with real OIDC HTTP. Audit: WORM sink proven against real S3 via LocalStack, multi-region ready. Observability: OTel v1 shape frozen. Supply chain: cosign keyless + SBOM + SLSA-3 on every tagged release. Corpus: ITDR wire format v1 + 10 seeds. That is what "trust by construction" delivered in four weeks.

Phase 1 begins next

The engine is enforcing; the console must now show it. Weeks 5–16 land the customer-visible surface: the decisions view (every ALLOW/BLOCK/STEP_UP with its trace), the policy editor (author + verify + sign), the approvals queue (Rule 3/5 step-ups with SLA timers), the corpus viewer (every attack pattern the engine recognizes, with its schema). Every one of those maps one-to-one to a Phase-0 primitive we already shipped. Nothing is being invented from scratch.

Engineering · Milestone

Xtrom v0.1.0-alpha.w3 — brokers speak OIDC, bundles parse in Rust, WORM proves itself

Third north-star milestone. The four IdP brokers move from stable interfaces to real OIDC round-trips (against captured vendor fixtures). The Rust engine gains a second differential-target: a bundle loader that parses schemas/policy/v1.json and translates it into an engine_core::Policy that our existing Engine::authorize can execute. And the WORM audit sink is now proven against a real S3 API surface via LocalStack — cross-tenant isolation, idempotence, and retention envelope all validated in CI.

OIDC brokers — one interface, one transport, four vendors

Every broker (Okta, Entra, Ping, Auth0) now performs OIDC discovery → client-credentials token exchange against the vendor's actual endpoint. The transport is a stdlib-only UrllibTransport (no third-party HTTP library in the data-plane wheel), and it refuses non-https URLs at the transport layer. Tests inject a FixtureTransport backed by captured discovery + token JSON — no live network in CI, but the round-trip code path is real. 19 new tests pass on every commit.

Bundle loader in Rust — the second differential target

A new engine-bundle crate parses the signed policy-bundle wire format we froze in Week 1. The bundle → Policy translation is a pure function of the bundle bytes: same bundle in, same Policy struct out, byte-for-byte. Every one of the five shipped example bundles under schemas/policy/examples/ parses without error. This is the surface the future WASM playground will run on — a customer will paste a bundle, and the same Rust code the data-plane runs will tell them exactly what it would enforce.

WORM audit — proven, not promised

Week 2 shipped the WORM sink with in-memory tests. Week 3 proves it against a real S3 API. A new CI job spins LocalStack, creates an Object-Lock bucket, and asserts four properties: writes land under the correct tenant=<x> prefix; queries are tenant-scoped with no cross-tenant leakage; duplicate event_id PUTs are idempotent; ObjectLockRetainUntilDate is attached and readable. It is a blocking gate — if any of those regress, the release does not ship.

What's landing next

Week 4 closes Phase 0 and locks the foundation. Immediate work: Rust integration test that composes engine-bundle + engine-core end-to-end across all five shipped bundles; legal review kickoff on the W1 DPA/EULA/SLA drafts; multi-region readiness pass on the audit sink. Then Phase 1 — the console-visible surface of everything the engine is now enforcing.

Engineering · Milestone

Xtrom v0.1.0-alpha.w2 — four rules, four brokers, WORM audit, SLSA-3

Second north-star milestone. Week 1 locked the invariants; this week we filled in the surface underneath them. The Rust enforcement engine now covers four rules, four federated IdP brokers land as stable interfaces with production sinks, the ITDR corpus wire format is frozen at v1, and every tagged release now ships SLSA build-level-3 provenance alongside the cosign signature.

Two more rules — same specification, two implementations

Rule 2 denies secret-tagged data flowing to any host not on the tenant's egress allowlist. Rule 3 forces a human-approved step-up on any destructive action taken in a prod environment (think rds_delete, s3_delete_bucket, ssm_run_command). Both are ported into the Rust engine-core and are simultaneously present in the Python reference. The differential-fuzz harness now generates hostile hosts, AWS-key-shaped secrets, and destructive actions — and 10 000 seeded runs still diverge zero times. Two independent implementations agreeing on ten thousand inputs is not a coincidence. It is a specification.

Four IdP brokers, one interface

Okta, Entra ID, Ping, and Auth0 broker skeletons now live under xtrom/brokers/, all conforming to the IdPBroker ABC we froze in Week 1. Every broker config supports env:, kms:, and vault: secret-reference indirection — plaintext secrets never sit in a policy bundle. A resolve_backend() factory dispatches by name. Ten broker contract tests pass on every commit.

WORM audit — S3 Object Lock, tenant-isolated by construction

The audit sink is now more than an interface. InMemoryWormAuditSink for tests, S3WormAuditSink for production. Backend uses S3 Object Lock in Compliance mode with a per-tenant retention envelope; writes are idempotent via If-None-Match: "*" (a duplicate is a no-op, not a mutation). The key layout — {prefix}/tenant=X/kind=Y/dt=YYYY-MM-DD/{event_id}.json — has every component regex-guarded against traversal. Nine sink contract tests pass in CI.

ITDR corpus — a wire format for adversary telemetry

Every attack pattern Xtrom recognizes now has a public schema. schemas/corpus/v1.json is a JSON Schema Draft 2020-12 envelope covering attack class, MITRE ATT&CK-for-AI-Agents stage, signals, mitigations, and attribution. Ten seed entries ship this week — each mapping one-to-one against a shipped rule. Corpus bundles ship in the same signed envelope as policy bundles: one verifier, one transparency-log trail, one revocation path.

SLSA-3 — the second signature

The Week 1 cosign signature proves who built the artifact. SLSA-3 proves how. The new .github/workflows/slsa3.yml invokes the slsa-github-generator reusable workflow on every v* tag and produces a non-falsifiable in-toto attestation of the build. Any reviewer can verify it in one command:

slsa-verifier verify-artifact xtrom-src.tar.gz \
  --provenance-path xtrom-src.tar.gz.intoto.jsonl \
  --source-uri github.com/saurabhpanday57/xtrom \
  --source-tag v0.1.0-alpha.w2
Cosign + SLSA-3 together mean: you can prove the code came from this repo, that it was built by this workflow on GitHub-hosted infrastructure, and that no human intervened between commit and image. That's what a CISO wants to see before an agent runs in their VPC.

What's landing next

Weeks 3–4 wire real HTTP layers behind the four IdP brokers (with captured fixtures for integration testing), start the Rust port of PolicyEngine.evaluate() — the second differential-fuzz target — and land LocalStack-backed cross-tenant integration tests against the S3 WORM sink. Then we open Phase 1 and start the console-visible surface of everything the engine is now enforcing.

Engineering · Milestone

Xtrom v0.1.0-alpha.w1 — three product invariants, now locked by the compiler

This week we shipped the first north-star milestone: Xtrom v0.1.0-alpha.w1. It is not a marketing beat — it is the moment the product's three non-negotiable properties stopped being promises in a PDF and became compile-time and CI-time gates. If the enforcement path ever regresses on determinism, phones home from the customer's VPC, or ships an unsigned artifact, the build fails. That is what "trust by construction" means.

The three invariants

They live in product/PRODUCT.md and are the source of truth for every architectural decision that follows:

scripts/check_invariants.py statically scans the enforcement crates on every commit. Rust code is compiled with unsafe_code = "deny", clippy::unwrap_used = "deny", clippy::panic = "deny". A regression is a red build — not a code review debate.

Signed policy bundles — a public wire format

The signed policy bundle is now a first-class, versioned wire format: schemas/policy/v1.json (JSON Schema Draft 2020-12), five example bundles, a validator that runs in CI. Tenants and agents are lowercase kebab-case regex-enforced. Capabilities are explicit; no wildcards. Every deployment carries the policy version in every audit event so an auditor can replay any decision from history.

Keyless-signed supply chain, Sigstore + SBOM

Every image Xtrom ships is signed by Fulcio-issued short-lived certificates tied to a specific workflow-file OIDC identity, with an in-toto attestation of a CycloneDX SBOM, and a public entry in the Sigstore Rekor transparency log. There are no long-lived signing keys anywhere in our infrastructure. Anyone in the world can verify our releases with one cosign verify command — see SUPPLY-CHAIN.md. That is not something we can put a slide about; it is either provable or it isn't.

The Rust port has begun — and it matches Python bit-for-bit

The engine's hot path is being ported from the Python reference to a Rust workspace (product/xtrom-engine-rs/) — the language the production data plane will ship in. This week's port covers Rule 0 (least-privilege scope) and Rule 1 (taint / information-flow control): together they kill capability abuse and indirect prompt injection, the two most common agent-attack classes.

The moat is the harness, not the port. engine-fuzz generates deterministic randomized (action, args, policy) triples, adjudicates each on both engines, and asserts the verdicts match. This week: 10 000 runs, zero divergences. The gate is now permanent CI — every commit re-runs it.

Two independent implementations agreeing on 10 000 seeded inputs is not a coincidence — it is a specification. That is the sort of evidence we want a CISO to have.

What's landing next

Weeks 2–4 finish the invariants (fed IdP interfaces, WORM audit sink, SLSA-3 provenance), port the remaining rules, and open the first ITDR-corpus commits under the same signed-bundle wire format. Weeks 5–16 are the first design-partner-ready surface. If your agents touch your cloud, your code, or your customers' data, we would love to walk you through the guarantees — get in touch.

Manifesto

Why every AI agent needs an identity

Something quietly broke in enterprise security this year. Companies didn't just adopt AI — they gave it hands. Coding and ops agents now run with real AWS keys, GitHub tokens, and production database access. They read untrusted content — a repo file, a web page, a tool response — and then act on it.

That combination is new, and our tools weren't built for it. Identity and access management was designed for two kinds of actor: humans, and static service accounts that do one predictable thing. An AI agent is neither. It reasons, it improvises, it uses tools — and it can be talked into things by a poisoned document it read three steps ago.

The uncomfortable truth about guardrails

The first instinct is to bolt a content filter onto the model — a "guardrail" that reads the text and guesses whether it's malicious. But natural language is unbounded. Any classifier that blocks today's phrasing is bypassed tomorrow by a rephrase, an encoding, or an instruction buried in a retrieved file. This is the same movie the industry watched with signature-based antivirus and polymorphic malware. Detection alone loses.

Detectors score. The action boundary is the guarantee.

So we start from a different premise: treat the model as permanently untrusted, and enforce deterministically at the moment it tries to act. Using taint tracking — information-flow control — we label every piece of data by provenance and propagate it. If the amount of a wire transfer, or the recipient of an email, traces back to untrusted content, the action is blocked — no matter how the request was worded. That's not a probability. It's a property.

From a security feature to a system of record

Blocking bad actions is necessary but not sufficient. To govern agents the way enterprises govern people, you need the full lifecycle: discover every agent identity, right-size its entitlements, enforce at runtime, detect compromise, and prove what happened. That's identity — and identity is a system of record, not a filter. Systems of record are where trust, audit, and durability live.

Machine identities already outnumber humans many times over, and agents are accelerating that curve. The company that becomes the neutral record of what every agent is allowed to do — and what it did — is building infrastructure, not a checkbox.

Why now, and why us

The regulation is coming (the EU AI Act will require agent inventory and audit), the attack surface is live today, and the category is still forming. We come at it from security-platform and SOC operations — the exact muscle this demands. We'd rather build it in the open, with design partners, against real privileged-agent workloads.

If your agents are touching your cloud, your code, or your customers' data, we'd love to show you what deterministic, identity-first governance looks like. Request early access →

Coming soon

Coming soon

A MITRE ATT&CK for AI agents

Toward a shared taxonomy of agent attacks — indirect injection, tool-arg poisoning, scope escalation, and more.

Coming soon

How taint tracking stops indirect injection

An engineering deep-dive on information-flow control at the agent action boundary.

Coming soon

CIEM for agents: granted vs. used

Why most agents are wildly over-privileged, and how to right-size without breaking workflows.

Coming soon

Zero standing secrets for AI agents

Just-in-time, scoped credentials — and why long-lived keys are the wrong default.

Get new posts

Occasional, technical, no spam. Notes on securing the agentic enterprise.

Subscribe
HomeAboutSecurityPrivacyTermsDPAEULAAUPSLACareersBlogContact © 2026 Xtrom, Inc.