npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@agent-assistant/policy

v0.3.17

Published

Action classification, gating, and audit contracts for agent assistants

Readme

@agent-assistant/policy

Status: IMPLEMENTED Version: 0.2.0 (pre-1.0, provisional) Spec: docs/specs/v1-policy-spec.md Implementation plan: docs/architecture/v1-policy-implementation-plan.md


What This Package Does

@agent-assistant/policy is the classification, gating, and audit layer for assistant actions — the boundary between "the assistant decided to act" and "the action actually executes."

It provides:

  • PolicyEngine — evaluates actions against registered policy rules, applies risk classification, and returns structured decisions (allow, deny, require_approval, escalate)
  • Risk classificationRiskClassifier interface with a pluggable classify function; defaultRiskClassifier returns medium for all unclassified actions
  • Policy rule registration — products register PolicyRule objects with priority ordering; evaluation is first-match-wins
  • Approval contractApprovalHint on require_approval decisions; ApprovalResolution for recording outcomes after approval flows complete
  • Audit hooksAuditSink interface called on every evaluate() call; every decision is recorded regardless of outcome
  • InMemoryAuditSink — test adapter with an accessible events array; no external infrastructure required
  • Proactive action flagAction.proactive is a required field; rules may apply stricter gating to proactive actions
  • Fallback decision — configurable per engine instance; defaults to require_approval (default-block posture: unclassified/unmatched actions are gated behind approval rather than silently allowed or denied)

This package does not own approval UX, approval workflows, scheduling, notification flows, session lifecycle, message delivery, persistent rule storage, or product-specific action catalogs. All of that stays in product code or other packages.


Installation

npm install @agent-assistant/policy

No @agent-assistant/* runtime dependencies. Only nanoid is required at runtime.


Quick Start

import { createActionPolicy, InMemoryAuditSink } from '@agent-assistant/policy';
import type { PolicyRule, RiskClassifier } from '@agent-assistant/policy';

const auditSink = new InMemoryAuditSink();

// Supply a product-specific classifier
const classifier: RiskClassifier = {
  classify(action) {
    switch (action.type) {
      case 'send_email': return 'high';
      case 'create_draft': return 'medium';
      case 'read_inbox': return 'low';
      default: return 'medium';
    }
  },
};

const policyEngine = createActionPolicy({ classifier, auditSink });

// Register rules
policyEngine.registerRule({
  id: 'deny-critical',
  priority: 1,
  description: 'Deny all critical-risk actions in v1',
  evaluate(action, riskLevel) {
    if (riskLevel === 'critical') {
      return {
        action: 'deny',
        ruleId: 'deny-critical',
        riskLevel,
        reason: 'Critical actions are not permitted.',
      };
    }
    return null; // defer to next rule
  },
});

policyEngine.registerRule({
  id: 'require-approval-high',
  priority: 10,
  description: 'Require human approval for high-risk actions',
  evaluate(action, riskLevel) {
    if (riskLevel === 'high') {
      return {
        action: 'require_approval',
        ruleId: 'require-approval-high',
        riskLevel,
        reason: 'High-risk actions require explicit human approval.',
        approvalHint: {
          approver: 'user',
          prompt: `The assistant is about to: ${action.description}. Approve?`,
        },
      };
    }
    return null;
  },
});

// Evaluate an action before executing it
const action = {
  id: 'act-001',
  type: 'send_email',
  description: 'Send follow-up to stakeholders',
  sessionId: 'sess-abc',
  userId: 'user-xyz',
  proactive: false,
};

// evaluate() returns EvaluationResult: { decision, auditEventId }
const { decision, auditEventId } = await policyEngine.evaluate(action);

if (decision.action === 'allow') {
  // execute the action
} else if (decision.action === 'require_approval') {
  // enter approval flow using decision.approvalHint, then record resolution:
  // await policyEngine.recordApproval(auditEventId, { approved: true, resolvedAt: ... });
} else if (decision.action === 'deny') {
  // surface denial to user
} else if (decision.action === 'escalate') {
  // route to configured escalation target
}

Risk Levels

| Level | Meaning | Default gating | |---|---|---| | low | Reversible, internal, no external side effects | Auto-approve | | medium | External but limited blast radius | Auto-approve with audit | | high | Significant external consequences; hard to reverse | Require human approval | | critical | Irreversible, broad impact, or affects shared state | Escalate or deny |

Products override gating behavior through registered policy rules. The defaults above describe intent, not enforcement — enforcement is through the rules you register.


Risk Classifier

interface RiskClassifier {
  classify(action: Action): RiskLevel | Promise<RiskLevel>;
}

The defaultRiskClassifier returns medium for all actions. Pass your own classifier to createActionPolicy:

const policyEngine = createActionPolicy({ classifier: myClassifier });

Classifiers may be async — useful when external context (e.g., target branch protection, PR size) informs the risk level.


Policy Rules

Rules are product-supplied. The engine evaluates them in priority order (lower number = higher priority). The first rule returning a non-null decision wins. If no rule matches, the fallback decision applies.

interface PolicyRule {
  id: string;
  priority?: number; // default 100; lower evaluates first
  evaluate(
    action: Action,
    riskLevel: RiskLevel,
    context: PolicyEvaluationContext
  ): PolicyDecision | null | Promise<PolicyDecision | null>;
  description?: string;
}

Return null to defer to the next rule. This is how you compose rules without conflicts.

Rule management:

policyEngine.registerRule(rule);      // register; throws PolicyError if id already exists
policyEngine.removeRule('rule-id');   // remove; throws RuleNotFoundError if not found
policyEngine.listRules();             // returns rules sorted by priority, then registration order

Decisions

interface PolicyDecision {
  action: 'allow' | 'deny' | 'require_approval' | 'escalate';
  ruleId: string;
  riskLevel: RiskLevel;
  reason?: string;
  approvalHint?: ApprovalHint; // present when action is 'require_approval'
}

| Decision | Caller behavior | |---|---| | allow | Execute the action | | deny | Do not execute; surface a denial reason to the user | | require_approval | Block execution; enter approval flow using approvalHint | | escalate | Block execution; notify configured escalation target |


Approval Contract

When a rule returns require_approval, it may include an ApprovalHint:

interface ApprovalHint {
  approver?: string;   // suggested approver role (e.g., 'workspace_admin', 'user')
  timeoutMs?: number;  // suggested timeout before auto-escalating
  prompt?: string;     // message to present to the approver
}

After the product resolves the approval flow, record the outcome using engine.recordApproval():

interface ApprovalResolution {
  approved: boolean;
  approvedBy?: string;
  resolvedAt: string; // ISO-8601
  comment?: string;
}

// auditEventId comes from the EvaluationResult returned by evaluate()
await policyEngine.recordApproval(auditEventId, {
  approved: true,
  approvedBy: 'user-xyz',
  resolvedAt: new Date().toISOString(),
  comment: 'Approved after review.',
});

recordApproval() emits a new AuditEvent to the configured sink with the original action, decision, and the ApprovalResolution populated in the approval field. Throws PolicyError if the auditEventId is unknown (evicted from the bounded in-memory map after 1000 evaluations).


Proactive Action Gating

Action.proactive is a required, non-optional boolean. Callers must be explicit about whether an action originated from a user turn or from a proactive engine.

// In a proactive capability handler:
const action: Action = {
  id: nanoid(),
  type: 'proactive_follow_up',
  description: 'Proactive check-in on stale thread',
  sessionId: wakeUpContext.sessionId,
  userId: sessionUserId,
  proactive: true, // required
};

const { decision, auditEventId } = await policyEngine.evaluate(action);

Policy rules receive context.proactive and can apply stricter gating:

policyEngine.registerRule({
  id: 'proactive-high-require-approval',
  priority: 5,
  evaluate(action, riskLevel, context) {
    if (context.proactive && (riskLevel === 'high' || riskLevel === 'critical')) {
      return {
        action: 'require_approval',
        ruleId: 'proactive-high-require-approval',
        riskLevel,
        approvalHint: {
          approver: 'user',
          prompt: `The assistant is about to take a proactive action: ${action.description}. Approve?`,
        },
      };
    }
    return null;
  },
});

Audit Hooks

Every evaluate() call records an AuditEvent, regardless of the decision:

interface AuditEvent {
  id: string;
  action: Action;
  riskLevel: RiskLevel;
  decision: PolicyDecision;
  evaluatedAt: string; // ISO-8601
  approval?: ApprovalResolution; // populated by the product after approval resolution
}

interface AuditSink {
  record(event: AuditEvent): Promise<void>;
}

For tests and local development: use InMemoryAuditSink:

const sink = new InMemoryAuditSink();
const engine = createActionPolicy({ auditSink: sink });

// After evaluate():
console.log(sink.events); // AuditEvent[]
sink.clear();             // reset

For production: implement AuditSink against your own persistence backend (database, log aggregator, cloud audit service).

No-op sink when audit is not needed:

const engine = createActionPolicy({ auditSink: { record: async () => {} } });

Wiring Traits to Policy

The policy package does not read traits directly. Products map trait values to policy configuration at setup time:

import { createActionPolicy } from '@agent-assistant/policy';

const policyEngine = createActionPolicy({
  fallbackDecision: traits.riskTolerance === 'cautious' ? 'deny' : 'require_approval',
  classifier: buildClassifierFromTraits(traits),
});

Fallback Decision

When no registered rule produces a non-null decision, the engine applies the fallback:

// Default fallback: require_approval
const engine = createActionPolicy();

// Override to deny all unmatched actions:
const strictEngine = createActionPolicy({ fallbackDecision: 'deny' });

// Override to allow all unmatched actions (permissive dev setup):
const permissiveEngine = createActionPolicy({ fallbackDecision: 'allow' });

The fallback is recorded in the audit event with ruleId: 'fallback'.


Error Types

// Base policy error
class PolicyError extends Error { cause?: unknown }

// Thrown by removeRule() when ruleId is not found
class RuleNotFoundError extends PolicyError { ruleId: string }

// Thrown when the risk classifier throws or returns an invalid value
class ClassificationError extends PolicyError { cause?: unknown }

What Stays Outside This Package

| Concern | Where it lives | |---|---| | Product-specific action type catalogs | Product repos | | Commercial tier and pricing enforcement | Product repos | | Customer-specific escalation chains | Product repos | | Approval UX (modals, Slack buttons, email) | Product repos | | Approval workflow state and timeouts | Product repos | | User authentication and identity | Relay foundation (relayauth) | | Fleet-level rate limiting | Relay foundation / cloud infra | | Content moderation and safety filtering | External services / product repos | | Session lifecycle | @agent-assistant/sessions | | Outbound message delivery | @agent-assistant/surfaces + Relay runtime | | Hosted audit pipelines and storage | AgentWorkforce/cloud | | Persistent rule storage | Deferred to v1.1 | | Time-based auto-escalation | Deferred to v1.1 |


Package Structure

packages/policy/
  package.json        — nanoid runtime dep only
  tsconfig.json
  src/
    types.ts          — Action, RiskLevel, RiskClassifier, PolicyRule, PolicyDecision,
                        EvaluationResult, PolicyEvaluationContext, ApprovalHint,
                        ApprovalResolution, AuditEvent, AuditSink, InMemoryAuditSink,
                        error classes
    policy.ts         — createActionPolicy factory and PolicyEngine implementation
    index.ts          — public re-exports
    policy.test.ts    — 64 tests
  README.md

POLICY_PACKAGE_DIRECTION_READY