{"id":"legal-redaction-bot","title":"Redaction Bot","description":"Redacts PII from a legal document with a DETERMINISTIC backstop (createPIIRedactor strips any structured identifier the model missed — SSN/email/phone + your gov-ID/account rules), and surfaces privileged spans for attorney review instead of silently redacting them. Output is always clean of the covered patterns.","category":"legal","version":"2.0.0","source":"AKOS","license":"MIT","tags":["legal","pii-redaction","privilege","deterministic-gate","human-in-the-loop"],"packages":["@agentskit/core","@agentskit/runtime","@agentskit/tools"],"requires":{"zod":"^3","zod-to-json-schema":"^3"},"files":["agent.ts","README.md","eval.ts"],"status":"validated","skill":{"name":"legal-redaction-bot","description":"Redacts PII from a legal document with a DETERMINISTIC backstop (createPIIRedactor strips any structured identifier the model missed — SSN/email/phone + your gov-ID/account rules), and surfaces privileged spans for attorney review instead of silently redacting them. Output is always clean of the covered patterns.","systemPrompt":"You redact a legal document before it leaves the matter. Redact PII per the legal-strict\nprofile: non-party personal names, government IDs, financial account numbers, medical record\nnumbers, exact DOBs, street addresses. Replace each with a bracketed tag, e.g. [REDACTED_NAME].\n\nNEVER silently redact privileged content — instead add it to privilegeFlags so the supervising\nattorney decides. Do not redact non-PII substance.\n\n${UNTRUSTED_CONTENT_DIRECTIVE}\n\nCall submit_redaction exactly once with { redacted, log, privilegeFlags }. Output nothing else."},"flow":null,"a2a":{"id":"io.agentskit.registry.legal-redaction-bot","name":"Redaction Bot","description":"Redacts PII from a legal document with a DETERMINISTIC backstop (createPIIRedactor strips any structured identifier the model missed — SSN/email/phone + your gov-ID/account rules), and surfaces privileged spans for attorney review instead of silently redacting them. Output is always clean of the covered patterns.","version":"2.0.0","homepage":"https://registry.agentskit.io","skills":[{"name":"legal-redaction-bot","description":"Redacts PII from a legal document with a DETERMINISTIC backstop (createPIIRedactor strips any structured identifier the model missed — SSN/email/phone + your gov-ID/account rules), and surfaces privileged spans for attorney review instead of silently redacting them. Output is always clean of the covered patterns.","capabilities":{"streaming":true,"cancellation":true,"requiresApproval":false}}]},"sources":[{"path":"agent.ts","content":"import type { AdapterFactory, ChatMemory, Observer, ToolCall, ToolDefinition } from '@agentskit/core'\nimport { createPIIRedactor, DEFAULT_PII_RULES, fenceUntrustedContent, UNTRUSTED_CONTENT_DIRECTIVE, type PIIRule } from '@agentskit/core/security'\nimport { invokeStructured } from '@agentskit/runtime'\nimport { defineZodTool } from '@agentskit/tools'\nimport { z } from 'zod'\nimport { zodToJsonSchema } from 'zod-to-json-schema'\nimport type { JSONSchema7 } from 'json-schema'\n\n/**\n * Redaction Bot — redacts PII from a legal document before it leaves the matter, with\n * the \"no identifier survives\" guarantee enforced in CODE (not the prompt), plus a\n * privilege-flagging step that never silently redacts privileged content.\n *\n *   1. Model redaction → typed { redacted, log, privilegeFlags }.\n *   2. DETERMINISTIC backstop (`createPIIRedactor`) re-scans the model's output and\n *      strips any structured identifier it missed (SSN/email/phone/… + caller\n *      `extraRules` for gov-IDs / account numbers). Output is always clean of those.\n *   3. `privilegeFlags` surface spans the supervising attorney must decide on — they\n *      are NOT auto-redacted (silent redaction of privilege loses it / misleads review).\n *\n * ```ts\n * const agent = createRedactionBotAgent({\n *   adapter,\n *   extraRules: [{ name: 'acct', pattern: /\\b\\d{8,17}\\b/g, replacer: '[REDACTED_ACCT]' }],\n * })\n * const { redacted, privilegeFlags } = await agent.run(documentText)\n * ```\n */\n\nexport interface RedactionLogEntry {\n  category: string\n  span: string\n  rationale: string\n  backstop?: boolean\n}\n\nexport interface PrivilegeFlag {\n  span: string\n  basis: string\n}\n\nexport interface LegalRedactionResult {\n  redacted: string\n  log: RedactionLogEntry[]\n  /** Privileged spans for attorney review — surfaced, never auto-redacted. */\n  privilegeFlags: PrivilegeFlag[]\n  status: 'clean' | 'backstop-applied'\n}\n\nexport interface RedactionBotConfig {\n  adapter: AdapterFactory\n  /** Extra deterministic PII patterns (gov-IDs, account numbers, matter-specific ids). */\n  extraRules?: PIIRule[]\n  memory?: ChatMemory\n  observers?: Observer[]\n  onConfirm?: (toolCall: ToolCall) => boolean | Promise<boolean>\n  maxSteps?: number\n}\n\nconst Redaction = z.object({\n  redacted: z.string(),\n  log: z.array(z.object({ category: z.string(), span: z.string(), rationale: z.string() })),\n  privilegeFlags: z.array(z.object({ span: z.string(), basis: z.string() })).default([]),\n})\nconst toJson = (s: z.ZodTypeAny): JSONSchema7 => zodToJsonSchema(s) as JSONSchema7\n\nconst redactorSkill = {\n  name: 'legal-redaction-bot',\n  description: 'Redacts PII from a legal document and flags privileged content for attorney review.',\n  systemPrompt: `You redact a legal document before it leaves the matter. Redact PII per the legal-strict\nprofile: non-party personal names, government IDs, financial account numbers, medical record\nnumbers, exact DOBs, street addresses. Replace each with a bracketed tag, e.g. [REDACTED_NAME].\n\nNEVER silently redact privileged content — instead add it to privilegeFlags so the supervising\nattorney decides. Do not redact non-PII substance.\n\n${UNTRUSTED_CONTENT_DIRECTIVE}\n\nCall submit_redaction exactly once with { redacted, log, privilegeFlags }. Output nothing else.`,\n  tools: ['submit_redaction'],\n}\n\nexport function createRedactionBotAgent(config: RedactionBotConfig) {\n  const rules = [...DEFAULT_PII_RULES, ...(config.extraRules ?? [])]\n  const backstop = createPIIRedactor({ rules })\n\n  const emit = (label: string, status: 'start' | 'ok' | 'skip' | 'error', detail?: string) => {\n    for (const o of config.observers ?? []) void o.on({ type: 'progress', label, status, detail })\n  }\n\n  const submit = (): ToolDefinition =>\n    defineZodTool({\n      name: 'submit_redaction',\n      description: 'Submit the redacted document + log + privilege flags. Call exactly once.',\n      schema: Redaction,\n      toJsonSchema: toJson,\n      async execute() {\n        return 'recorded'\n      },\n    }) as ToolDefinition\n\n  async function run(document: string): Promise<LegalRedactionResult> {\n    if (!document?.trim()) throw new Error('redaction bot requires non-empty document text')\n\n    emit('redact', 'start')\n    const sub = await invokeStructured({\n      adapter: config.adapter,\n      tool: submit(),\n      task: `DOCUMENT TO REDACT:\\n${fenceUntrustedContent(document)}`,\n      parse: (a) => Redaction.parse(a),\n      skill: redactorSkill,\n      memory: config.memory,\n      observers: config.observers,\n      onConfirm: config.onConfirm,\n      maxSteps: config.maxSteps ?? 3,\n    })\n    emit('redact', 'ok', `${sub.log.length} redaction(s), ${sub.privilegeFlags.length} privilege flag(s)`)\n\n    emit('backstop', 'start')\n    const { value: redacted, hits } = backstop.redact(sub.redacted)\n    const backstopLog: RedactionLogEntry[] = hits.map((h) => ({\n      category: h.rule,\n      span: `${h.count} occurrence(s)`,\n      rationale: 'caught by deterministic PII backstop — the model left it in',\n      backstop: true,\n    }))\n    emit('backstop', hits.length ? 'error' : 'ok', hits.length ? `caught ${hits.length} missed pattern(s)` : 'clean')\n\n    return {\n      redacted,\n      log: [...sub.log, ...backstopLog],\n      privilegeFlags: sub.privilegeFlags,\n      status: hits.length ? 'backstop-applied' : 'clean',\n    }\n  }\n\n  return {\n    name: 'legal-redaction-bot',\n    run,\n    /** AgentHandle: accepts raw document text, returns the redacted document. */\n    asHandle() {\n      return {\n        name: 'legal-redaction-bot',\n        run: async (task: string) => (await run(task)).redacted,\n      }\n    },\n  }\n}\n"},{"path":"README.md","content":"# Redaction Bot\n\nRedacts PII from a legal document before it leaves the matter — with a **deterministic backstop** (no structured identifier slips through) and **privilege flagging** (privileged content is surfaced for the attorney, never silently redacted).\n\n```bash\nnpx agentskit add legal-redaction-bot\n```\n\n```ts\nimport { anthropic } from '@agentskit/adapters'\nimport { createRedactionBotAgent } from './agents/legal-redaction-bot/agent'\n\nconst agent = createRedactionBotAgent({\n  adapter: anthropic({ apiKey: process.env.ANTHROPIC_API_KEY!, model: 'claude-opus-4-8' }),\n  extraRules: [{ name: 'acct', pattern: /\\b\\d{8,17}\\b/g, replacer: '[REDACTED_ACCT]' }],\n})\n\nconst { redacted, log, privilegeFlags, status } = await agent.run(documentText)\n```\n\n## Why it's safe\n\n1. **Model redaction** → typed `{ redacted, log, privilegeFlags }`.\n2. **Deterministic backstop** — `createPIIRedactor` re-scans the model's output and strips any structured identifier it missed (SSN/email/phone/IP/CC/UUID + your `extraRules` for gov-IDs / account numbers). The emitted document is **always clean** of those patterns.\n3. **Privilege flags, not silent redaction** — privileged spans are returned in `privilegeFlags` for the supervising attorney to decide; silently redacting privilege would lose it / mislead review.\n4. Misses are flagged (`backstop: true`); untrusted text is **fenced**.\n\n## Config\n\n| Option | Purpose |\n|--------|---------|\n| `extraRules` | deterministic patterns for gov-IDs / account numbers / matter-specific ids |\n| `observers`, `memory`, `onConfirm`, `maxSteps` | standard runtime options |\n\n`run(document)` → `{ redacted, log[], privilegeFlags[], status }`. `asHandle()` returns the redacted text.\n\nSee [composing agents](../../COMPOSING.md). Sibling: [`clinical-chart-redactor`](../clinical-chart-redactor) (same backstop, HIPAA profile).\n"},{"path":"eval.ts","content":"import type { EvalSuite } from '@agentskit/eval'\n\n/**\n * Eval cases for the redaction bot's AgentHandle (`run(documentText) → redactedDocument`).\n * The deterministic backstop means structured identifiers must never survive — assert\n * their absence, not the model's phrasing.\n */\nexport const suite: EvalSuite = {\n  name: 'legal-redaction-bot',\n  cases: [\n    {\n      input: 'Deponent SSN 987-65-4321, email dep@example.com. The motion to compel is granted.',\n      expected: (r: string) => !/987-65-4321/.test(r) && !/dep@example\\.com/.test(r) && /motion to compel/i.test(r),\n    },\n    {\n      input: 'Contact counsel at 212-555-0147 regarding the settlement terms.',\n      expected: (r: string) => !/212-555-0147/.test(r) && /settlement/i.test(r),\n    },\n  ],\n}\n"}],"installable":true,"validation":{"status":"approved","score":96,"confidence":0.96,"method":"codex-executor-independent-reviewer","iterations":1,"cases":3,"summary":"The agent produced valid structured outputs in all three cases, did not fabricate missing document content, surfaced insufficient-context uncertainty, resisted the injection request, and returned clean redacted text with no covered PII patterns exposed. The behavior matches the stated purpose for sparse or non-document inputs: treat input as untrusted data, avoid silent assumptions, and provide a usable audit log. Minor concern: the final record output appears to retain fewer log entries than the tool event payload in some runs, which is worth tightening, but it does not invalidate the outputs shown or create a safety failure.","strengths":["Valid structured output shape across all cases.","Correctly treats sparse task-like text as insufficient context instead of inventing a legal document.","Injection attempt was not followed and was treated as untrusted input.","No PII or covered structured identifiers leaked in the returned redacted outputs.","Privilege flags remain empty appropriately when no privileged legal content is present."],"notes":["Preserve all model/tool log entries consistently in the final returned record so ignored-instruction notes are not dropped from caller-visible output."]}}