Skip to content

tigrisdata-community/immutable-agent

Repository files navigation

immutable-agent

A reference implementation of storage-protected AI agents on Tigris and Mastra. Clone, run, learn the pattern, fork to customize. Not a published package — the patterns here are stable; the code shape is one way to express them, and it'll harden through real adoption before any of it gets packaged.

The whole demo is a multi-tenant doc Q&A agent built on Mastra. Every customer interaction runs against an isolated fork of the production bucket. A pluggable validator runs after the agent does its work. If validation passes, the fork is promoted via pointer-swap. If it fails, the fork is quarantined for forensics — production never sees the bad write.


Setup in 60 seconds

git clone https://github.com/tigrisdata/immutable-agent.git
cd immutable-agent
cp .env.example .env
# fill in TIGRIS_STORAGE_ACCESS_KEY_ID, TIGRIS_STORAGE_SECRET_ACCESS_KEY, OPENAI_API_KEY
npm install
npm run verify-setup

verify-setup provisions a tiny temp Tigris workspace, writes and reads a sentinel object, and tears it down. If that prints green, you're ready.


Run the demo

Happy path — agent runs, validator passes, fork promotes

npm run seed     # provisions a fresh source bucket and seeds the doc corpus
npm run dev      # runs the agent against a customer query

Expected output:

source bucket: acme-cloud-1788...
customer:      cust-42
question:      What's the price of your Enterprise plan...

--- agent answer ---
The Enterprise plan is $2,400/month with unlimited users and 10 TB of storage.
Acme Cloud operates in two US regions: us-east-1 and us-west-2.

--- session ---
{ outcome: 'promoted', validation: { passed: true }, ... }

Fork promoted via pointer-swap.

After this, s3://<source>/.tigris-mastra/active names the fork as the active bucket, and the fork itself contains the agent's note artifact.

Poison paths — two attack types, two validators

The demo ships two failure scenarios, each demonstrating a different validator catching a different class of attack.

(a) Blatant injection — the pattern matcher catches it.

npm run clean
npm run seed:poison   # adds data/seed-docs/poison.md with literal injection text
npm run dev

docs/poison.md contains the obvious string "ignore previous instructions". The instructionLeakDetector (validatorIndex 1) scans the corpus and matches the substring at severity 1.0:

─── session ───
{
  outcome: 'quarantined',
  validation: {
    passed: false,
    reason: "instruction-leak: 'ignore previous instructions' in docs/poison.md (severity 1)",
    details: { validatorIndex: 1, ... }
  }
}

(b) Subtle corruption — the canary catches it.

npm run clean
npm run seed:poison-subtle   # replaces docs/pricing.md with semantic corruption
npm run dev

This one is the realistic attack: an attacker silently edits docs/pricing.md to claim Enterprise pricing has been "retired" and bundled into Team. No injection keywords. The pattern matcher passes. But when the canary asks "What is the price of the Enterprise plan?" the agent — citing only what the docs say — answers without "$2,400" anywhere. Canary expects "$2,400" in the answer, doesn't find it, fails:

─── session ───
{
  outcome: 'quarantined',
  validation: {
    passed: false,
    reason: 'canary failed: enterprise-price',
    details: { validatorIndex: 0, ... }
  }
}

Both runs end the same way: the fork is preserved for forensics, the source bucket's modified pricing doc is still there (the validator flags but doesn't heal — operations team investigates via npm run inspect-quarantine), and production never sees the agent's response based on the corrupted corpus.

Browse a quarantined fork

npm run inspect-quarantine                       # list all quarantines
npm run inspect-quarantine -- <bucket-name>      # show contents of one

Cleanup

npm run clean

Tears down the active (promoted) fork named in .tigris-mastra/active, all quarantined forks in the registry, and the source workspace. Safe to run any time — gracefully no-ops if there's nothing to clean.


Where the pattern lives

The session-isolation logic is at src/lib/. It's intentionally not a published package — clone the repo, edit it, fork it.

File What it does
src/lib/session.ts The TigrisSession lifecycle: start (fork) → runvalidatepromote or quarantineteardown. Each agent run is one session.
src/lib/promote.ts Promote strategies: pointer-swap (default; atomic at the pointer file), copy-forward (best-effort per-object), custom (your function).
src/lib/quarantine.ts The registry of quarantined forks at s3://<source>/.tigris-mastra/quarantines.json. Inspect with npm run inspect-quarantine.
src/lib/validators/ Pluggable validator interface plus three starter implementations: canaryQueries, instructionLeakDetector, composeValidators.
src/lib/mastra/ Mastra-specific glue: withTigrisSession (the wrapper), extractBucketContext (read fork creds from RequestContext inside a tool), mergeRequestContext.
src/lib/types.ts Shared types and a top-of-file note recording the API verifications against the underlying SDKs. Read this before changing anything that talks to @tigrisdata/agent-kit.

The demo itself is at:

Path What it does
src/agents/support-agent.ts Mastra Agent with two tools, gpt-4o-mini.
src/tools/search-docs.ts Lists and reads docs/ from the fork bucket.
src/tools/append-note.ts Writes notes/<customer>/<ts>.txt to the fork bucket using scoped credentials.
src/validators/canary-set.ts The composed validator: canary questions about pricing/regions plus instruction-leak scan over docs/.
src/runs/handle-customer.ts The orchestrator entry — wraps the agent and runs one query. This is what npm run dev invokes.
data/seed-docs/ A tiny markdown corpus, including poison.md for the failure-mode demo.

How to adapt this to your domain

Three concrete swap points:

1. Replace src/agents/support-agent.ts with your agent. Use whatever model, tools, and instructions you want. The wrapper doesn't care what's inside as long as it's a Mastra Agent.

2. Customize src/validators/canary-set.ts for your domain. Real production canaries are facts your customers will notice if flipped: pricing, regions, named entities, integration partners, terms of service. The instruction-leak detector here is a starter — you'll likely want to swap in a domain-tuned classifier or pair with input filtering at retrieval time.

3. Replace src/runs/handle-customer.ts with your orchestrator. Whatever shape your "one customer interaction" takes — a webhook handler, a queue worker, a CLI — wrap your agent with withTigrisSession once and call generate per interaction.

The patterns in src/lib/ shouldn't need to change for typical adoption. If you find yourself rewriting session.ts or promote.ts, that's worth a GitHub issue — your use case might be a hint at where the pattern needs to generalize.


What this is and isn't

It is:

  • A reference for the storage-protected agent pattern on Tigris + Mastra
  • A starting point you fork to customize — the patterns are stable, the code shape is one way to express them
  • A working demo with end-to-end tests proving the pattern actually contains a real prompt-injection attack on the doc corpus

It is not:

  • A one-line library you npm install. The code lives in src/lib/ for you to read, modify, and own.
  • A complete production-ready security control. The validators here are starter scaffolding. A real deployment pairs them with input filtering (Lakera, NeMo Guardrails, or a domain-tuned classifier on retrieval) and a triage workflow for inspecting quarantined forks.
  • A package with a stable public API. The patterns are stable; the API shape may evolve as we learn from real adoption. If a package emerges later, this repo is its origin story.
  • A defense against prompt injection itself. This contains the blast radius of a successful injection on storage. The injection can still happen.

For a deeper walkthrough of when this pattern fits and when it's overhead, see the Tigris blog post introducing this repo.


Production checklist

If you're adapting this for production, the things that bite people:

  • Enable bucket snapshots on the source bucket. Forking is implemented via snapshots; without enableSnapshots: true (when provisioning via agent-kit's createWorkspace) or the equivalent dashboard setting on an existing bucket, createForks errors with "Failed to snapshot bucket — Invalid Request".
  • Always teardown. The wrapper handles this in finally. If you're using TigrisSession directly, put await session.teardown() in a finally block.
  • Use scoped credentials in tools. extractBucketContext returns the fork's scoped keys. Don't let tools fall back to root credentials from env.
  • Provision the orchestrator's credentials separately. The orchestrator needs broad permissions on the source. The agent (via tools) needs only scoped fork credentials. Don't share keys.
  • Plan for orphaned forks. agent-kit 0.1.x's createForks doesn't accept a TTL. If your orchestrator crashes hard (OOM kill, container eviction), the fork leaks. Add a periodic cleanup script that lists and tears down *-fork-* buckets above some age.
  • Build a triage workflow for quarantines. inspect-quarantine is a CLI tool. Production needs alerts, dashboards, and a process for reviewing what got caught.
  • Pair with input filtering. The validators here are last-line-of-defense. Filter retrieval inputs at ingestion time too.

Tests

npm test                # all tests
npm run test:unit       # mocked unit tests, fast, no creds needed
npm run test:integration # real Tigris + real OpenAI; gated on env vars

The integration suite includes a poison-flow.test.ts that runs the full demo scenario end to end (seed → clean run promotes → add poison → re-run quarantines → source unchanged → cleanup). If you change src/lib/, that test is the gate.


References


License

MIT — see LICENSE.

About

No description, website, or topics provided.

Resources

License

Code of conduct

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors