import { randomUUID } from 'crypto'; import { canonicalJson, computeSignerFingerprint, sha256Hex } from './decision-log'; import { signDagNode, verifyReceiptDag, type ReceiptCapabilityLevel, type ReceiptDagEnvelope, type ReceiptDagNode } from './receipts/dag'; import type { SpendDecision } from './types'; export type OperatingPackVertical = 'law'; export type OperatingPackActionKind = | 'intake' | 'conflict_check' | 'document_summary' | 'billing_reconciliation' | 'client_update_draft' | 'send_client_email' | 'trust_account_action'; export interface OperatingPackSigningKeys { privateKey: Uint8Array; publicKey: Uint8Array; } export interface OperatingPackReviewerCascadeOptions { enabled?: boolean; drafterModel?: string; reviewerModel?: string; triggerThreshold?: number; drafterMaxCostCents?: number; reviewerMaxCostCents?: number; } export interface OperatingPackOptions { vertical: OperatingPackVertical; workflowId: string; signingKeys: OperatingPackSigningKeys; approvedCapabilities?: ReceiptCapabilityLevel[]; reviewerCascade?: OperatingPackReviewerCascadeOptions; maxWorkflowCostCents?: number; } export interface OperatingPackAction { actionId: string; kind: OperatingPackActionKind; capability?: ReceiptCapabilityLevel; highRisk?: boolean; highRiskClassifierScore?: number; keywordRisk?: boolean; modelIds?: string[]; tokenCounts?: { inputTokens?: number; outputTokens?: number }; totalCostCents?: number; metadata?: Record; reviewerVerdict?: 'drafter_only' | 'review_required' | 'approved' | 'rejected'; } export interface OperatingPackGuardResult { allowed: boolean; reason: string; node: ReceiptDagNode; dag: ReceiptDagEnvelope; reviewerCascadeTriggered: boolean; spentCents: number; } export interface OperatingPackSkillTemplate { slug: string; title: string; capability: ReceiptCapabilityLevel; reviewerCascade: boolean; markdown: string; } export interface OperatingPackDefinition { vertical: OperatingPackVertical; title: string; counselReviewPending: boolean; agentsMd: string; skills: Record; sourceOfTruth: string; checklist: string[]; capabilityTable: Array<{ capability: ReceiptCapabilityLevel; use: string; blockedExamples: string[] }>; } const CAPABILITY_RANK: Record = { READ_ONLY: 0, TRANSACT: 1, ADMIN: 2, ORCHESTRATE: 3, }; const DATA_PLANE_KEYS = /^(title|summary|prompt|completion|content|messages|input|output|text|raw|source|notes|draft|body|transcript|document|client_content|clientContent|matter_description|email_body|file_text)$/i; const SAFE_METADATA_KEYS = new Set([ 'action_id', 'action_kind', 'billing_code', 'capability', 'cost_cents', 'duration_ms', 'event', 'jurisdiction', 'matter_hash', 'model', 'model_id', 'outcome', 'practice_area', 'review_status', 'risk_score', 'token_count', 'workflow_id', ]); export const LAW_OPERATING_PACK: OperatingPackDefinition = { vertical: 'law', title: 'AgentGuard Law Operating Pack', counselReviewPending: true, agentsMd: `# AgentGuard Law Operating Pack\n\nCounsel review pending: this file is operating scaffolding for legal workflows and should be reviewed by your counsel before production use.\n\n## Guardrails\n\n- ABA Model Rule 1.6: protect client information. Do not paste client content into tools that are not approved for the matter.\n- Conflicts: run conflict checks before intake work advances to drafting or client communication.\n- No unauthorized practice of law: agents prepare drafts and metadata. A licensed attorney reviews legal judgment.\n- Draft before send: client-facing emails and filings are drafts until a human approves dispatch.\n- Retention: keep signed AgentGuard receipts and matter metadata according to firm policy. Do not store prompt or completion content in receipts.\n- Trust account actions: trust transfers, disbursement instructions, and ledger changes require ADMIN capability.\n\n## Capability Map\n\n- READ_ONLY: intake classification, conflict pre-checks, document summaries, billing reconciliation previews.\n- TRANSACT: create a client-update draft or prepare a dispatch packet.\n- ADMIN: trust-account actions, retention policy changes, capability changes.\n- ORCHESTRATE: assign or revoke agent work across matters.\n`, skills: { intake: { slug: 'intake', title: 'Intake Skill', capability: 'READ_ONLY', reviewerCascade: false, markdown: `# Intake Skill\n\nPurpose: classify a new inquiry using matter metadata only.\n\nInputs: jurisdiction, practice area, referral source, matter hash, urgency flag.\n\nRules:\n- Do not ask the model to decide representation.\n- Do not place client narrative text in AgentGuard receipts.\n- Output a routing label, missing-information checklist, and conflict-check reminder.\n`, }, 'conflict-check': { slug: 'conflict-check', title: 'Conflict Check Skill', capability: 'READ_ONLY', reviewerCascade: true, markdown: `# Conflict Check Skill\n\nPurpose: prepare a conflict pre-check packet from firm metadata.\n\nInputs: matter hash, party hashes, jurisdiction, practice area, intake source.\n\nRules:\n- Match on firm-approved metadata systems only.\n- Refuse if party identity data is incomplete.\n- Reviewer Cascade triggers when a possible match, former-client marker, adverse-party marker, or trust-account marker appears.\n`, }, 'document-summary': { slug: 'document-summary', title: 'Document Summary Skill', capability: 'READ_ONLY', reviewerCascade: true, markdown: `# Document Summary Skill\n\nPurpose: produce a working summary for attorney review.\n\nInputs: document hash, document type, jurisdiction, matter hash, token counts.\n\nRules:\n- Cite document sections or refuse when citations are unavailable.\n- Mark all output as draft work product for attorney review.\n- Reviewer Cascade triggers for settlement, privilege, indemnity, non-compete, waiver, trust, sanctions, or deadline markers.\n`, }, 'billing-reconciliation': { slug: 'billing-reconciliation', title: 'Billing Reconciliation Skill', capability: 'READ_ONLY', reviewerCascade: false, markdown: `# Billing Reconciliation Skill\n\nPurpose: compare time-entry metadata against matter budgets and billing codes.\n\nInputs: billing code, matter hash, time-entry hashes, amount cents, policy cap.\n\nRules:\n- Do not move money.\n- Do not alter trust ledgers.\n- Escalate to ADMIN before any trust-account action.\n`, }, 'client-update-draft': { slug: 'client-update-draft', title: 'Client Update Draft Skill', capability: 'TRANSACT', reviewerCascade: true, markdown: `# Client Update Draft Skill\n\nPurpose: draft a client update for attorney review.\n\nInputs: matter hash, milestone label, jurisdiction, practice area, cited record hashes.\n\nRules:\n- Draft before send. Never dispatch directly.\n- Include a review status field before any client-facing use.\n- Reviewer Cascade triggers for deadline, settlement, privilege, fee, trust, or adverse-party markers.\n`, }, }, sourceOfTruth: `# Source Of Truth Layout\n\nUse this local layout inside the firm environment.\n\n- matters//metadata.json\n- matters//parties.json\n- matters//documents/.json\n- matters//receipts/*.json\n- policies/agentguard-policy.yaml\n- logs/agentguard-decisions.ndjson\n\nAgentGuard receipts store metadata, hashes, costs, model identifiers, capability decisions, and signatures. They do not store prompt or completion content.\n`, checklist: [ 'Review AGENTS.md with firm counsel before production use.', 'Define matter hash and party hash conventions.', 'Map each skill to a capability tier.', 'Enable Reviewer Cascade for client updates, conflicts, and document summaries.', 'Set a per-matter and per-day spend cap.', 'Store signed receipts with matter metadata only.', 'Test trust-account actions with ADMIN capability denied by default.', ], capabilityTable: [ { capability: 'READ_ONLY', use: 'Classify, summarize, reconcile, and prepare metadata-only packets.', blockedExamples: ['Sending client email', 'Trust ledger update'] }, { capability: 'TRANSACT', use: 'Create dispatch-ready drafts that still require human approval.', blockedExamples: ['Trust transfer', 'Policy change'] }, { capability: 'ADMIN', use: 'Trust-account actions, retention changes, and capability grants.', blockedExamples: ['Unreviewed client dispatch'] }, { capability: 'ORCHESTRATE', use: 'Assign, revoke, or coordinate multiple matter agents.', blockedExamples: ['Bypassing capability review'] }, ], }; export function createOperatingPackEnvelope(opts: OperatingPackOptions): OperatingPackEnvelope { return new OperatingPackEnvelope(opts); } export class OperatingPackEnvelope { private readonly nodes: ReceiptDagNode[] = []; private readonly approvedCapabilities: Set; private spentCents = 0; constructor(private readonly opts: OperatingPackOptions) { if (opts.vertical !== 'law') throw new Error('Only the law operating pack is bundled in this release'); if (!safeLabel(opts.workflowId)) throw new Error('Operating Pack workflowId is required'); this.approvedCapabilities = new Set(opts.approvedCapabilities ?? ['READ_ONLY']); } getPack(): OperatingPackDefinition { return LAW_OPERATING_PACK; } async guardAction(input: OperatingPackAction): Promise { assertAction(input); assertMetadataOnly(input.metadata ?? {}); const capability = input.capability ?? capabilityForAction(input.kind); const cost = safeNonNegativeInteger(input.totalCostCents); const spendExceeded = Number.isSafeInteger(this.opts.maxWorkflowCostCents) && this.opts.maxWorkflowCostCents! >= 0 && this.spentCents + cost > this.opts.maxWorkflowCostCents!; const capabilityAllowed = this.hasCapability(capability); const allowed = capabilityAllowed && !spendExceeded; const reason = !capabilityAllowed ? `Capability ${capability} is required for ${input.kind}` : spendExceeded ? 'Operating Pack workflow spend cap reached' : 'Operating Pack action allowed'; if (allowed) this.spentCents += cost; const triggers = allowed ? reviewerCascadeTriggers(input, this.opts) : []; const node = await this.signActionNode(input, capability, allowed, reason, triggers); this.nodes.push(node); return { allowed, reason, node, dag: this.getReceiptDag(), reviewerCascadeTriggered: triggers.length > 0, spentCents: this.spentCents, }; } getReceiptDag(): ReceiptDagEnvelope { const root = this.nodes[this.nodes.length - 1]; return { nodes: [...this.nodes], rootId: root?.entryHash ?? '0'.repeat(64), workflowId: workflowHash(this.opts.workflowId) }; } async verifyDag(publicKeyHex?: string): Promise>> { return verifyReceiptDag(this.getReceiptDag(), publicKeyHex ?? Buffer.from(this.opts.signingKeys.publicKey).toString('hex')); } outboundPayloads(): Record[] { return []; } signerFingerprint(): string { return computeSignerFingerprint(this.opts.signingKeys.publicKey); } private hasCapability(capability: ReceiptCapabilityLevel): boolean { for (const granted of this.approvedCapabilities) { if (CAPABILITY_RANK[granted] >= CAPABILITY_RANK[capability]) return true; } return false; } private async signActionNode(input: OperatingPackAction, capability: ReceiptCapabilityLevel, allowed: boolean, reason: string, triggers: string[]): Promise { const decision = operatingPackDecision(input, this.opts, capability, allowed, reason, triggers, this.spentCents); return signDagNode({ sequence: this.nodes.length, decision, privateKey: this.opts.signingKeys.privateKey, publicKey: this.opts.signingKeys.publicKey, parents: this.nodes.length ? [this.nodes[this.nodes.length - 1] as ReceiptDagNode] : [], workflowId: workflowHash(this.opts.workflowId), trustScore: allowed ? (triggers.length ? 0.9 : 0.94) : 0.2, blocked: !allowed, capability, }); } } function operatingPackDecision(input: OperatingPackAction, opts: OperatingPackOptions, capability: ReceiptCapabilityLevel, allowed: boolean, reason: string, triggers: string[], spentCents: number): SpendDecision { const cost = safeNonNegativeInteger(input.totalCostCents); const inputTokens = safeNonNegativeInteger(input.tokenCounts?.inputTokens); const outputTokens = safeNonNegativeInteger(input.tokenCounts?.outputTokens); const receipt: Record = { type: 'agent_operating_pack_action', vertical: opts.vertical, counselReviewPending: LAW_OPERATING_PACK.counselReviewPending, workflowHash: workflowHash(opts.workflowId), actionId: safeLabel(input.actionId) ?? hashValue(input.actionId).slice(0, 16), actionKind: input.kind, capability, blocked: !allowed, modelIds: safeModelIds(input.modelIds ?? []), inputTokens, outputTokens, totalCostCents: cost, metadata: filterReceiptMetadata({ ...(input.metadata ?? {}), action_kind: input.kind, capability }), }; if (triggers.length > 0) receipt.reviewerCascade = buildReviewerCascadeReceipt(input, opts, triggers); return { decisionId: randomUUID(), timestamp: new Date().toISOString(), action: allowed ? 'allow' : 'block', triggeredCap: allowed ? null : { window: 'per_call', amountCents: cost, action: 'block', reason }, triggeredScopeKey: null, projectedCents: cost, windowSpendBefore: Math.max(0, spentCents - (allowed ? cost : 0)), windowSpendAfter: spentCents, provider: 'unknown', modelRequested: 'operating-pack', modelResolved: 'operating-pack', policyId: 'agentguard-operating-pack-law', policyVersion: 1, enforcementMode: 'enforce', reasons: [reason], entryType: 'outcome', outcomeReceipt: receipt, }; } function buildReviewerCascadeReceipt(input: OperatingPackAction, opts: OperatingPackOptions, triggers: string[]): Record { const cascade = opts.reviewerCascade ?? {}; return { outcome: input.kind, drafter: { model: safeLabel(cascade.drafterModel) ?? 'openai/gpt-4o-mini', maxCostCents: safeNonNegativeInteger(cascade.drafterMaxCostCents ?? 25), }, reviewer: { model: safeLabel(cascade.reviewerModel) ?? 'anthropic/claude-sonnet-4-6', maxCostCents: safeNonNegativeInteger(cascade.reviewerMaxCostCents ?? 75), }, triggerFired: triggers, reviewerVerdict: safeLabel(input.reviewerVerdict) ?? 'review_required', }; } function reviewerCascadeTriggers(input: OperatingPackAction, opts: OperatingPackOptions): string[] { if (opts.reviewerCascade?.enabled === false) return []; const threshold = opts.reviewerCascade?.triggerThreshold ?? 0.55; const triggers: string[] = []; if (input.highRisk === true) triggers.push('high_risk_action'); if (input.keywordRisk === true) triggers.push('keyword_prefilter'); if (typeof input.highRiskClassifierScore === 'number' && input.highRiskClassifierScore >= threshold) triggers.push(`risk_score_above:${threshold}`); if (['conflict_check', 'document_summary', 'client_update_draft', 'send_client_email', 'trust_account_action'].includes(input.kind)) triggers.push('law_pack_default'); return [...new Set(triggers)].sort(); } function capabilityForAction(kind: OperatingPackActionKind): ReceiptCapabilityLevel { if (kind === 'trust_account_action') return 'ADMIN'; if (kind === 'send_client_email' || kind === 'client_update_draft') return 'TRANSACT'; return 'READ_ONLY'; } function assertAction(input: OperatingPackAction): void { assertPlainRecord(input, 'action'); if (!safeLabel(input.actionId)) throw new Error('Operating Pack actionId is required'); const allowedKinds: OperatingPackActionKind[] = ['intake', 'conflict_check', 'document_summary', 'billing_reconciliation', 'client_update_draft', 'send_client_email', 'trust_account_action']; if (!allowedKinds.includes(input.kind)) throw new Error('Operating Pack action kind is not supported'); if (input.capability && !(input.capability in CAPABILITY_RANK)) throw new Error('Operating Pack capability is not supported'); if (input.tokenCounts) validateTokens(input.tokenCounts.inputTokens ?? 0, input.tokenCounts.outputTokens ?? 0); if (input.totalCostCents != null && (!Number.isSafeInteger(input.totalCostCents) || input.totalCostCents < 0)) throw new Error('Operating Pack totalCostCents must be a non-negative safe integer'); } export function assertOperatingPackMetadataOnly(value: unknown, path = 'metadata'): void { assertMetadataOnly(value, path); } function assertMetadataOnly(value: unknown, path = 'metadata'): void { if (value == null) return; if (Array.isArray(value)) { for (let i = 0; i < value.length; i++) assertMetadataOnly(value[i], `${path}[${i}]`); return; } if (typeof value !== 'object') return; for (const [key, child] of Object.entries(value as Record)) { if (DATA_PLANE_KEYS.test(key)) throw new Error(`Operating Pack metadata cannot include data-plane field: ${path}.${key}`); assertMetadataOnly(child, `${path}.${key}`); } } function filterReceiptMetadata(metadata: Record): Record { assertMetadataOnly(metadata); const out: Record = {}; for (const [key, value] of Object.entries(metadata)) { if (!SAFE_METADATA_KEYS.has(key)) continue; if (value == null || typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') out[key] = value; } return out; } function assertPlainRecord(value: unknown, name: string): void { if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error(`Operating Pack ${name} must be an object`); } function validateTokens(inputTokens: number, outputTokens: number): void { if (!Number.isSafeInteger(inputTokens) || inputTokens < 0) throw new Error('inputTokens must be a non-negative safe integer'); if (!Number.isSafeInteger(outputTokens) || outputTokens < 0) throw new Error('outputTokens must be a non-negative safe integer'); } function workflowHash(value: string): string { return hashValue(`workflow:${value}`); } function hashValue(value: string): string { return sha256Hex(canonicalJson({ value })); } function safeLabel(value: unknown): string | undefined { if (typeof value !== 'string') return undefined; const trimmed = value.trim(); if (!trimmed || !/^[A-Za-z0-9_.:/-]{1,180}$/.test(trimmed)) return undefined; return trimmed; } function safeModelIds(models: string[]): string[] { return models.map((model) => safeLabel(model)).filter(Boolean) as string[]; } function safeNonNegativeInteger(value: unknown): number { return Number.isSafeInteger(value) && (value as number) >= 0 ? value as number : 0; } export function operatingPackSignerFingerprint(opts: OperatingPackOptions): string | null { return opts.signingKeys ? computeSignerFingerprint(opts.signingKeys.publicKey) : null; }