@wezynlia/taint-guard
v0.1.0
Published
Prevent private tool outputs from leaking into unsafe agent actions.
Maintainers
Readme
TaintGuard
Data-flow control and taint tracking for tool-using AI agents.
TaintGuard tracks where agent data comes from, labels sensitive tool outputs, and enforces policies before that data reaches external tools, logs, model responses, or other sinks.
TaintGuard is an application-layer control. It complements, but does not replace, authorization, sandboxing, secret management, or infrastructure security.
Install
npm install @wezynlia/taint-guardQuick Start
import {
createTaintGuard,
deny,
redact,
requireApproval
} from "@wezynlia/taint-guard";
const guard = createTaintGuard({
policies: [
deny(["secret", "credentials"]).to("*"),
deny("customer_data").to("external"),
redact("personal_data").to("log"),
requireApproval("internal").to(["github", "slack", "email"])
]
});
const readDocument = guard.sourceTool(
{
name: "docs.read",
sourceType: "document",
labels: ["internal"]
},
async ({ path }: { path: string }) => readFile(path)
);
const sendSlack = guard.sinkTool(
{
name: "slack.sendMessage",
sinkType: "slack",
external: true
},
async (message: { channel: string; text: string }) =>
slack.sendMessage(message)
);
const document = await readDocument({ path: "pricing-policy.md" });
// Throws ApprovalRequiredError before the Slack tool executes.
await sendSlack({
channel: "#public-support",
text: document as unknown as string
});Explicit Taint Propagation
Use taint for values that do not originate from a wrapped source tool:
const customer = guard.taint(customerRecord, {
source: {
name: "crm.getCustomer",
type: "database",
labels: ["customer_data"]
}
});Use combine to merge provenance:
const summary = guard.combine(
[customer, internalNote] as const,
(record, note) => `${record.name}: ${note}`
);
summary.labels; // ["customer_data", "internal"]Policy Model
Policies match labels against sink names, sink types, wildcard patterns, or the
special external target. When several policies match, the most restrictive
action wins:
block > redact > approval_required > allowNo match defaults to allow. Custom predicates support context-specific rules:
deny("internal")
.when(({ sink }) => sink.metadata?.visibility === "public")
.because("Internal data cannot be posted to public channels.")
.to("slack");Decisions
Call guard.check(value, sink) to inspect a flow without executing a tool. It
returns one of:
allowblockredactapproval_required
Sink wrappers throw TaintFlowError or ApprovalRequiredError before the
underlying tool executes. Redaction policies pass sanitized input to the tool.
Audit Logging
import { createTaintGuard, JsonlAuditLogger } from "@wezynlia/taint-guard";
const guard = createTaintGuard({
policies,
auditLogger: new JsonlAuditLogger(".taintguard/audit.jsonl"),
runId: "run_123"
});Every policy check produces a structured audit event. MemoryAuditLogger and
CompositeAuditLogger are also included.
Secret Redaction
The default redactor detects common bearer tokens, OpenAI-style keys, GitHub
tokens, JWTs, private keys, connection-string passwords, and credit-card-like
values. Add custom detectors with createRedactor.
v0.1 Scope
- Explicit taint and provenance
- Label and source merging
- Policy DSL with wildcard and predicate support
- Source and sink tool wrappers
- Allow, block, redact, and approval-required decisions
- Structured errors
- JSONL audit logging
- Basic secret redaction
- ESM, CommonJS, and TypeScript declarations
Field-level taint, automatic model-output tracking, provider adapters, MCP integration, and semantic leak detection are intentionally outside v0.1. See the architecture notes for module boundaries and decision semantics.
Security
Do not rely on TaintGuard as the only security boundary. Values lose their
runtime taint if application code manually extracts and copies the raw value
without using combine or a wrapped flow. Taint propagation in v0.1 is
explicit by design.
License
MIT
