@agent-assistant/policy 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ export type { Action, RiskLevel, RiskClassifier, PolicyRule, PolicyDecision, PolicyEvaluationContext, PolicyEngineConfig, PolicyEngine, ApprovalHint, ApprovalResolution, AuditEvent, AuditSink, EvaluationResult, } from './types.js';
2
+ export { defaultRiskClassifier, InMemoryAuditSink, PolicyError, RuleNotFoundError, ClassificationError, } from './types.js';
3
+ export { createActionPolicy } from './policy.js';
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { defaultRiskClassifier, InMemoryAuditSink, PolicyError, RuleNotFoundError, ClassificationError, } from './types.js';
2
+ export { createActionPolicy } from './policy.js';
@@ -0,0 +1,2 @@
1
+ import { type PolicyEngine, type PolicyEngineConfig } from './types.js';
2
+ export declare function createActionPolicy(config?: PolicyEngineConfig): PolicyEngine;
package/dist/policy.js ADDED
@@ -0,0 +1,129 @@
1
+ import { nanoid } from 'nanoid';
2
+ import { ClassificationError, PolicyError, RuleNotFoundError, defaultRiskClassifier, } from './types.js';
3
+ const VALID_RISK_LEVELS = new Set(['low', 'medium', 'high', 'critical']);
4
+ /** Cap for the bounded eval-record map used for approval correlation. */
5
+ const EVAL_RECORD_CAP = 1000;
6
+ export function createActionPolicy(config) {
7
+ // ── Internal state ────────────────────────────────────────────────────────
8
+ const rules = [];
9
+ let registrationCounter = 0;
10
+ const resolvedClassifier = config?.classifier ?? defaultRiskClassifier;
11
+ const resolvedFallback = config?.fallbackDecision ?? 'require_approval';
12
+ const resolvedAuditSink = config?.auditSink ?? { record: async () => { } };
13
+ // Bounded map for approval correlation (reconciliation Decision 1).
14
+ const evalRecords = new Map();
15
+ const evalRecordOrder = [];
16
+ // ── Helpers ───────────────────────────────────────────────────────────────
17
+ function getSortedRules() {
18
+ return [...rules].sort((a, b) => {
19
+ const pa = a.rule.priority ?? 100;
20
+ const pb = b.rule.priority ?? 100;
21
+ if (pa !== pb)
22
+ return pa - pb;
23
+ return a.registrationIndex - b.registrationIndex;
24
+ });
25
+ }
26
+ function storeEvalRecord(id, record) {
27
+ evalRecords.set(id, record);
28
+ evalRecordOrder.push(id);
29
+ if (evalRecordOrder.length > EVAL_RECORD_CAP) {
30
+ const oldest = evalRecordOrder.shift();
31
+ evalRecords.delete(oldest);
32
+ }
33
+ }
34
+ // ── Engine ────────────────────────────────────────────────────────────────
35
+ return {
36
+ registerRule(rule) {
37
+ if (rules.some((r) => r.rule.id === rule.id)) {
38
+ // Decision 5: throw PolicyError, not RuleNotFoundError
39
+ throw new PolicyError(`Rule with id '${rule.id}' is already registered.`);
40
+ }
41
+ rules.push({ rule, registrationIndex: registrationCounter++ });
42
+ },
43
+ removeRule(ruleId) {
44
+ const index = rules.findIndex((r) => r.rule.id === ruleId);
45
+ if (index === -1) {
46
+ throw new RuleNotFoundError(ruleId);
47
+ }
48
+ rules.splice(index, 1);
49
+ },
50
+ listRules() {
51
+ return getSortedRules().map((r) => r.rule);
52
+ },
53
+ async evaluate(action) {
54
+ // 1. Classify risk — validate return value (reconciliation Decision 2)
55
+ let riskLevel;
56
+ try {
57
+ const raw = await resolvedClassifier.classify(action);
58
+ if (!VALID_RISK_LEVELS.has(raw)) {
59
+ throw new ClassificationError(`Classifier returned invalid risk level '${raw}' for action '${action.id}'. ` +
60
+ `Expected one of: low, medium, high, critical.`);
61
+ }
62
+ riskLevel = raw;
63
+ }
64
+ catch (err) {
65
+ if (err instanceof ClassificationError)
66
+ throw err;
67
+ throw new ClassificationError(`Classifier failed for action '${action.id}'`, err);
68
+ }
69
+ // 2. Build evaluation context (no workspaceId per reconciliation Decision 4)
70
+ const context = {
71
+ sessionId: action.sessionId,
72
+ userId: action.userId,
73
+ proactive: action.proactive,
74
+ metadata: action.metadata,
75
+ };
76
+ // 3. Evaluate rules in priority order (first-match-wins)
77
+ let decision = null;
78
+ for (const { rule } of getSortedRules()) {
79
+ try {
80
+ const result = await rule.evaluate(action, riskLevel, context);
81
+ if (result !== null) {
82
+ decision = result;
83
+ break;
84
+ }
85
+ }
86
+ catch (err) {
87
+ throw new PolicyError(`Rule '${rule.id}' threw during evaluation`, err);
88
+ }
89
+ }
90
+ // 4. Apply fallback when no rule matched
91
+ if (decision === null) {
92
+ decision = {
93
+ action: resolvedFallback,
94
+ ruleId: 'fallback',
95
+ riskLevel,
96
+ reason: 'No registered rule matched this action.',
97
+ };
98
+ }
99
+ // 5. Record audit event
100
+ const auditEventId = nanoid();
101
+ const auditEvent = {
102
+ id: auditEventId,
103
+ action,
104
+ riskLevel,
105
+ decision,
106
+ evaluatedAt: new Date().toISOString(),
107
+ };
108
+ await resolvedAuditSink.record(auditEvent);
109
+ // 6. Store eval record for approval correlation (bounded, reconciliation Decision 1)
110
+ storeEvalRecord(auditEventId, { action, riskLevel, decision });
111
+ return { decision, auditEventId };
112
+ },
113
+ async recordApproval(auditEventId, resolution) {
114
+ const record = evalRecords.get(auditEventId);
115
+ if (!record) {
116
+ throw new PolicyError(`Unknown auditEventId '${auditEventId}'. Cannot record approval resolution.`);
117
+ }
118
+ const approvalAuditEvent = {
119
+ id: nanoid(),
120
+ action: record.action,
121
+ riskLevel: record.riskLevel,
122
+ decision: record.decision,
123
+ evaluatedAt: new Date().toISOString(),
124
+ approval: resolution,
125
+ };
126
+ await resolvedAuditSink.record(approvalAuditEvent);
127
+ },
128
+ };
129
+ }
@@ -0,0 +1,100 @@
1
+ export interface Action {
2
+ id: string;
3
+ type: string;
4
+ description: string;
5
+ sessionId: string;
6
+ userId: string;
7
+ proactive: boolean;
8
+ metadata?: Record<string, unknown>;
9
+ }
10
+ export type RiskLevel = 'low' | 'medium' | 'high' | 'critical';
11
+ export interface RiskClassifier {
12
+ classify(action: Action): RiskLevel | Promise<RiskLevel>;
13
+ }
14
+ export declare const defaultRiskClassifier: RiskClassifier;
15
+ export interface PolicyEvaluationContext {
16
+ sessionId: string;
17
+ userId: string;
18
+ proactive: boolean;
19
+ metadata?: Record<string, unknown>;
20
+ }
21
+ export interface ApprovalHint {
22
+ approver?: string;
23
+ timeoutMs?: number;
24
+ prompt?: string;
25
+ }
26
+ export interface PolicyDecision {
27
+ action: 'allow' | 'deny' | 'require_approval' | 'escalate';
28
+ ruleId: string;
29
+ riskLevel: RiskLevel;
30
+ reason?: string;
31
+ approvalHint?: ApprovalHint;
32
+ }
33
+ export interface PolicyRule {
34
+ id: string;
35
+ priority?: number;
36
+ evaluate(action: Action, riskLevel: RiskLevel, context: PolicyEvaluationContext): PolicyDecision | null | Promise<PolicyDecision | null>;
37
+ description?: string;
38
+ }
39
+ export interface ApprovalResolution {
40
+ approved: boolean;
41
+ approvedBy?: string;
42
+ resolvedAt: string;
43
+ comment?: string;
44
+ }
45
+ export interface AuditEvent {
46
+ id: string;
47
+ action: Action;
48
+ riskLevel: RiskLevel;
49
+ decision: PolicyDecision;
50
+ evaluatedAt: string;
51
+ approval?: ApprovalResolution;
52
+ }
53
+ export interface AuditSink {
54
+ record(event: AuditEvent): Promise<void>;
55
+ }
56
+ /**
57
+ * Returned by evaluate(). Contains the policy decision and the audit event ID
58
+ * for approval correlation (reconciliation Decision 1).
59
+ */
60
+ export interface EvaluationResult {
61
+ decision: PolicyDecision;
62
+ auditEventId: string;
63
+ }
64
+ export interface PolicyEngineConfig {
65
+ classifier?: RiskClassifier;
66
+ fallbackDecision?: PolicyDecision['action'];
67
+ auditSink?: AuditSink;
68
+ }
69
+ export interface PolicyEngine {
70
+ evaluate(action: Action): Promise<EvaluationResult>;
71
+ registerRule(rule: PolicyRule): void;
72
+ removeRule(ruleId: string): void;
73
+ listRules(): PolicyRule[];
74
+ /**
75
+ * Record an approval resolution against a prior evaluation's audit event.
76
+ * Emits a new AuditEvent to the sink with the original action, decision,
77
+ * and the provided ApprovalResolution populated in the `approval` field.
78
+ *
79
+ * The engine stores a bounded map of auditEventId -> { action, riskLevel, decision }
80
+ * from recent evaluations (cap: 1000, evicts oldest). Throws PolicyError if the
81
+ * auditEventId is unknown or has been evicted.
82
+ */
83
+ recordApproval(auditEventId: string, resolution: ApprovalResolution): Promise<void>;
84
+ }
85
+ export declare class PolicyError extends Error {
86
+ readonly cause?: unknown | undefined;
87
+ constructor(message: string, cause?: unknown | undefined);
88
+ }
89
+ export declare class RuleNotFoundError extends PolicyError {
90
+ readonly ruleId: string;
91
+ constructor(ruleId: string);
92
+ }
93
+ export declare class ClassificationError extends PolicyError {
94
+ constructor(message: string, cause?: unknown);
95
+ }
96
+ export declare class InMemoryAuditSink implements AuditSink {
97
+ readonly events: AuditEvent[];
98
+ record(event: AuditEvent): Promise<void>;
99
+ clear(): void;
100
+ }
package/dist/types.js ADDED
@@ -0,0 +1,37 @@
1
+ // ─── Action and Classification ───────────────────────────────────────────────
2
+ export const defaultRiskClassifier = {
3
+ classify: (_action) => 'medium',
4
+ };
5
+ // ─── Error Types ──────────────────────────────────────────────────────────────
6
+ export class PolicyError extends Error {
7
+ cause;
8
+ constructor(message, cause) {
9
+ super(message);
10
+ this.cause = cause;
11
+ this.name = 'PolicyError';
12
+ }
13
+ }
14
+ export class RuleNotFoundError extends PolicyError {
15
+ ruleId;
16
+ constructor(ruleId) {
17
+ super(`Policy rule not found: ${ruleId}`);
18
+ this.ruleId = ruleId;
19
+ this.name = 'RuleNotFoundError';
20
+ }
21
+ }
22
+ export class ClassificationError extends PolicyError {
23
+ constructor(message, cause) {
24
+ super(message, cause);
25
+ this.name = 'ClassificationError';
26
+ }
27
+ }
28
+ // ─── InMemoryAuditSink ────────────────────────────────────────────────────────
29
+ export class InMemoryAuditSink {
30
+ events = [];
31
+ async record(event) {
32
+ this.events.push(event);
33
+ }
34
+ clear() {
35
+ this.events.length = 0;
36
+ }
37
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-assistant/policy",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Action classification, gating, and audit contracts for agent assistants",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",