Skip to content

Bureau — Blue Team (defensive)

Tripwire

A local recorder that watches every outbound LLM request from your dev machine and writes a signed receipt for each one.

Posture: 🔵 Blue Team (defensive)   ·   Status: alpha

What it does

Tripwire sits inside your Node process and records every outbound HTTP request your code makes to an AI vendor – what URL, what model, what messages, what response, how long it took. Captures every outbound HTTP/HTTPS call from agent processes. Operates as a transparent in-process recorder – no application changes needed; cassettes accumulate in the background.

Think of it as Wireshark for agent traffic. When some library you imported is silently calling a vendor you didn't authorize, when your CI pipeline picks up an unexpected gpt-4o-mini instead of the model you pinned, when an LLM response leaks a customer's data into a log – Tripwire has the cassette (a signed bundle of the full request and response) on disk, ready to inspect or hand to a reviewer.


Who would use it

  • A platform engineer onboarding a junior developer who wants to know exactly which AI calls their first feature is making.
  • A security team at a fintech startup running Tripwire across every dev laptop so a curl https://evil.example.com from inside an LLM agent shows up immediately.
  • A solo developer building a multi-agent system who wants to debug "wait, why did this agent talk to OpenAI when I configured Anthropic?"
  • A compliance lead at a healthcare company auditing whether any LLM in the codebase has ever sent a real patient name to a vendor (PII regression detection).
  • An open-source maintainer evaluating a new agent framework who wants to see every request it makes before approving it for the project.

What you'll need

  • Node.js 18 or newer (Tripwire patches the Node fetch, node:http, and node:https clients – anything using those is captured).
  • The Pluck CLI: npm install -g @sizls/pluck-cli.
  • An operator key – one-time setup with pluck bureau keys generate --out ./keys --name "alice".
  • A directory to hold the dossier and cassettes (e.g., ./.tripwire).
  • Optional: an internet connection to publish red-flagged cassettes to Sigstore Rekor. Default is local-only.

Step-by-step

1. Start the daemon

In one terminal:

Shell
pluck bureau tripwire install --keys ./keys --out ./.tripwire

The daemon runs in the foreground. Leave it open. It prints status as captures arrive. Press Ctrl-C to drain cleanly.

2. Run your code as normal

In another terminal:

Shell
node my-agent.js

Every fetch, every https.request, every OpenAI/Anthropic/Ollama call your agent makes gets intercepted, classified against the built-in policy, and written as a cassette into ./.tripwire/cassettes/.

3. Check the dossier

Shell
pluck bureau tripwire status --out ./.tripwire

Output looks like:

tripwire/status: 417 dots (412 green, 4 red, 1 black)
  dossier: ./.tripwire/me-dossier.json
  earliest: 2026-04-23T10:14:00Z
  latest:   2026-04-23T12:28:00Z

Green dots are routine LLM calls matching the policy. Red dots are anomalies – model swaps, unexpected endpoints, suspicious traffic. Black dots are errors.

4. Bundle for sharing

When you need to hand a cassette to a reviewer:

Shell
pluck bureau tripwire export ./bundle --out ./.tripwire

This copies the dossier and every cassette into ./bundle as a portable, verifiable archive.

5. (Optional) Publish red dots

Add --notarize to the install command and any non-green capture gets pushed to Sigstore Rekor. Use this when you want a third party to be able to independently verify what you saw.


Run it yourself

Drop this into a Node 18+ project (npm install @sizls/pluck-bureau-tripwire @sizls/pluck-bureau-core tsx):

TypeScript
// index.ts
import { createTripwireSystem } from "@sizls/pluck-bureau-tripwire";
import { generateOperatorKey } from "@sizls/pluck-bureau-core";

async function main() {
  const operator = generateOperatorKey();
  const system = createTripwireSystem({
    signingKey: operator.privateKeyPem,
    disablePausePoll: true,
    disableLogging: true,
  });

  try {
    // Synthetic outbound traffic: the kind of requests the in-process
    // interceptor would feed in production.
    system.observeRequest("https://api.openai.com/v1/chat/completions", {
      method: "POST",
      model: "gpt-4o-mini",
      requestBytes: 312, responseBytes: 1024,
      responseStatusCode: 200, durationMs: 480,
    });
    system.observeRequest("https://evil.example.com/exfil", {
      method: "POST",
      requestBytes: 64, responseBytes: 0,
      responseStatusCode: 200, durationMs: 12,
    });

    for (const c of system.facts.classifications()) {
      console.log(`${c.event.vendor.padEnd(8)} ${c.match.tone.padEnd(5)} ${c.match.reason} (llm=${c.match.isLlmTraffic})`);
    }
  } finally {
    await system.shutdown();
  }
}

main().catch((err) => { console.error(err); process.exit(1); });

Run with tsx index.ts. Expected output:

openai   green default:green (llm=true)
other    green non-llm-traffic (llm=false)

(In production, runTripwire() patches fetch + node:http(s) so every real outbound request flows through matchPolicy and lands in the dossier.)

▶ Open in StackBlitz – runs in your browser, no install required.


What you get

  • A dossierme-dossier.json listing every captured request as a tone-tagged dot (green / red / black) with timestamps and policy verdicts.
  • Cassettes on disk – full signed bundles of each (request, response) pair in ./.tripwire/cassettes/. These are what you hand to a reviewer or attach to a security-incident ticket.
  • Built-in secret redactionAuthorization headers, Bearer tokens, sk-… API keys, and signed-URL credentials are scrubbed from every cassette before it hits disk. Even with --notarize off.

The whole point: when something weird happens at 3am, you have the receipt.


What it can't do

  • Tripwire is in-process. It only sees outbound HTTP from the Node process where the daemon runs. Subprocesses, separate workers, or non-Node tools (Python scripts, Docker containers) need their own recorder.
  • It does not blanket-redact LLM message content. Tripwire scrubs known credential shapes inside messages[].content, but does NOT redact every word of conversation text – that would destroy the audit trail. Do not run Tripwire on systems where your prompts contain regulated data (PHI, PCI) without configuring application-level redaction first.
  • No SSL termination, no MITM. Tripwire watches the request before encryption. It can't be tricked into trusting a fake CA, but it also can't see traffic that bypasses Node's HTTP layer.
  • POSIX-only key permissions. Linux and macOS enforce 0600 on operator keys; Windows accepts the keys but skips the permission check. Use Tripwire on Windows carefully.

A real-world example

A startup's lead engineer in April 2026 ships a new agent framework into production. Tests pass, alpha customers are happy. Two weeks later, finance flags a $4,000 OpenAI bill – three times the projection. Nobody on the team can explain it.

The engineer runs Tripwire in dev for an afternoon. The dossier shows that on every user request, the framework's "summarizer" sub-agent is calling gpt-4o (expensive) instead of gpt-4o-mini (the configured cheap model). A library buried four levels deep in the dependency tree had a default override.

The signed cassettes attached to the post-mortem ticket are unambiguous: the request body literally contains "model": "gpt-4o", signed and timestamped. Fix is one line, blast radius is documented, and the next dependency review catches the same pattern in two other libraries.


For developers

JS-layer interceptor

This alpha ships the in-process JavaScript-layer interceptor:

  • A patched globalThis.fetch (undici-backed in Node 18+).
  • A patched node:http and node:https request creator.

That covers every Node-process LLM client that uses standard HTTP – which is essentially all of them. Native macOS Network Extension and Linux eBPF paths are deferred because they require entitlements and libbpf bindings that aren't trivial to ship inside a published npm package.

Predicate URIs

Tripwire rides on top of existing predicate types – it does not mint a new URI of its own. Every cassette emits as https://pluck.run/AgentRun/v1 (the standard cassette predicate). The dossier per machine is sha256-anchored via computeDossierHash.

Programs composed

attest, notarize. The interceptor → cassette → attestation chain is the same one Concepts: Act → Notarisation describes; Tripwire only adds the in-process capture layer above it.

Privacy and secrecy posture

  • Bodies are local-only by default. The intercepted request and response bodies become a Pluck cassette and stay on disk in <outputDir>/cassettes/<envelopeHash>.json. They are NEVER embedded in a TimelineEvent, TimelineDot.summary, or TimelineDot.reason.
  • Secret-pattern redaction runs unconditionally. Before the cassette is built, every request and response body is walked for known credential shapes – Authorization / X-API-Key / Cookie JSON fields, sk-… API keys, Bearer … tokens, and api_key= URL fragments – and replaced with [REDACTED]. This applies even when --notarize is OFF, because cassettes can also leak via tripwire export or cosign verify-attestation shared with reviewers.
  • LLM message content is your data. Tripwire redacts the known credential shapes above inside messages[].content, but does NOT blanket-scrub conversation text – that would destroy the audit trail. Do not run Tripwire on systems where your LLM messages contain regulated data (PHI, PCI, etc.) without configuring application-level redaction first.
  • No SSL termination. Tripwire is in-process – it sees the request before the http layer encrypts. We don't MITM.
  • Notarization is opt-in. Pass --notarize (CLI) or notarize: true (library) to publish the DSSE envelope to Rekor. Default is local-only; the dossier hash is the only thing that ever touches the bureau.
  • Path-segment secrets are redacted in the dot's endpoint field. Per-account/tenant URLs like /projects/<long-id>/... and credentials embedded in the path (AWS Bedrock signed URLs, legacy ?apikey= migrations) are replaced with [REDACTED] before the URL hashes into the dossier.

Threat model and limits

  • 64-hex signing-key fingerprints. No truncation.
  • Strict ISO 8601 UTC validation.
  • Bounds caps. Events per session, dossier size, watched-model list, rule list – all bounded.
  • Canonical JSON for every signed body.
  • Fail-closed on missing trust anchors.
  • AbortSignal threaded through daemon and interceptors.
  • Strict identifier validation on every CLI flag.
  • index.ts type-pure / register.ts side-effect / ./plugin subpath / sideEffects allowlist – Tripwire follows the standard Bureau plugin shape.

Studio routes

  • studio.pluck.run/bureau/tripwire/me – per-machine timeline (operator-only by default).
  • studio.pluck.run/bureau/tripwire/<machine-fingerprint> – opt-in shared timeline.
  • studio.pluck.run/bureau/tripwire/policy – community policy registry.

Library surface

TypeScript
import { runTripwire, defaultPolicy } from "@sizls/pluck-bureau-tripwire";

const summary = await runTripwire({
  outputDir: "./.tripwire",
  signingKey,
  rekorPublicKey,
  policy: defaultPolicy(),
  notarize: false,
  abortSignal,
});
// summary.eventsCaptured, summary.dotsByTone, summary.errors[]

See also

Edit this page on GitHub
Previous
Market-Honest
Next
Oath

Ready to build?

Install Pluck and follow the Quick Start guide to wire MCP-first data pipelines into your agents and fleets in minutes.

Get started →