@agentpolicyspecification/core 0.1.0

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.
Files changed (94) hide show
  1. package/LICENSE +187 -0
  2. package/README.md +13 -0
  3. package/coverage/clover.xml +458 -0
  4. package/coverage/coverage-final.json +7 -0
  5. package/coverage/lcov-report/base.css +224 -0
  6. package/coverage/lcov-report/block-navigation.js +87 -0
  7. package/coverage/lcov-report/favicon.png +0 -0
  8. package/coverage/lcov-report/index.html +146 -0
  9. package/coverage/lcov-report/prettify.css +1 -0
  10. package/coverage/lcov-report/prettify.js +2 -0
  11. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  12. package/coverage/lcov-report/sorter.js +210 -0
  13. package/coverage/lcov-report/src/core/errors.ts.html +217 -0
  14. package/coverage/lcov-report/src/core/index.html +146 -0
  15. package/coverage/lcov-report/src/core/policy.ts.html +142 -0
  16. package/coverage/lcov-report/src/core/types.ts.html +364 -0
  17. package/coverage/lcov-report/src/engine/aps-engine.ts.html +703 -0
  18. package/coverage/lcov-report/src/engine/index.html +131 -0
  19. package/coverage/lcov-report/src/engine/policy-set.ts.html +115 -0
  20. package/coverage/lcov-report/src/index.html +116 -0
  21. package/coverage/lcov-report/src/index.ts.html +244 -0
  22. package/coverage/lcov.info +558 -0
  23. package/dist/core/errors.d.ts +29 -0
  24. package/dist/core/errors.d.ts.map +1 -0
  25. package/dist/core/errors.js +21 -0
  26. package/dist/core/errors.js.map +1 -0
  27. package/dist/core/policy.d.ts +17 -0
  28. package/dist/core/policy.d.ts.map +1 -0
  29. package/dist/core/policy.js +2 -0
  30. package/dist/core/policy.js.map +1 -0
  31. package/dist/core/types.d.ts +67 -0
  32. package/dist/core/types.d.ts.map +1 -0
  33. package/dist/core/types.js +3 -0
  34. package/dist/core/types.js.map +1 -0
  35. package/dist/engine/aps-engine.d.ts +22 -0
  36. package/dist/engine/aps-engine.d.ts.map +1 -0
  37. package/dist/engine/aps-engine.js +167 -0
  38. package/dist/engine/aps-engine.js.map +1 -0
  39. package/dist/engine/policy-set.d.ts +9 -0
  40. package/dist/engine/policy-set.d.ts.map +1 -0
  41. package/dist/engine/policy-set.js +2 -0
  42. package/dist/engine/policy-set.js.map +1 -0
  43. package/dist/generated/base.d.ts +7 -0
  44. package/dist/generated/base.d.ts.map +1 -0
  45. package/dist/generated/base.js +4 -0
  46. package/dist/generated/base.js.map +1 -0
  47. package/dist/generated/dsl-policy.d.ts +130 -0
  48. package/dist/generated/dsl-policy.d.ts.map +1 -0
  49. package/dist/generated/dsl-policy.js +4 -0
  50. package/dist/generated/dsl-policy.js.map +1 -0
  51. package/dist/generated/input-context.d.ts +48 -0
  52. package/dist/generated/input-context.d.ts.map +1 -0
  53. package/dist/generated/input-context.js +4 -0
  54. package/dist/generated/input-context.js.map +1 -0
  55. package/dist/generated/output-context.d.ts +42 -0
  56. package/dist/generated/output-context.d.ts.map +1 -0
  57. package/dist/generated/output-context.js +4 -0
  58. package/dist/generated/output-context.js.map +1 -0
  59. package/dist/generated/policy-decision.d.ts +95 -0
  60. package/dist/generated/policy-decision.d.ts.map +1 -0
  61. package/dist/generated/policy-decision.js +4 -0
  62. package/dist/generated/policy-decision.js.map +1 -0
  63. package/dist/generated/policy-set.d.ts +139 -0
  64. package/dist/generated/policy-set.d.ts.map +1 -0
  65. package/dist/generated/policy-set.js +4 -0
  66. package/dist/generated/policy-set.js.map +1 -0
  67. package/dist/generated/tool-call-context.d.ts +52 -0
  68. package/dist/generated/tool-call-context.d.ts.map +1 -0
  69. package/dist/generated/tool-call-context.js +4 -0
  70. package/dist/generated/tool-call-context.js.map +1 -0
  71. package/dist/index.d.ts +13 -0
  72. package/dist/index.d.ts.map +1 -0
  73. package/dist/index.js +3 -0
  74. package/dist/index.js.map +1 -0
  75. package/examples/basic-usage.ts +89 -0
  76. package/jest.config.js +20 -0
  77. package/package.json +46 -0
  78. package/scripts/generate-types.mjs +24 -0
  79. package/src/core/errors.ts +44 -0
  80. package/src/core/policy.ts +19 -0
  81. package/src/core/types.ts +93 -0
  82. package/src/engine/aps-engine.ts +206 -0
  83. package/src/engine/policy-set.ts +10 -0
  84. package/src/generated/base.ts +9 -0
  85. package/src/generated/dsl-policy.ts +133 -0
  86. package/src/generated/input-context.ts +51 -0
  87. package/src/generated/output-context.ts +45 -0
  88. package/src/generated/policy-decision.ts +98 -0
  89. package/src/generated/policy-set.ts +142 -0
  90. package/src/generated/tool-call-context.ts +55 -0
  91. package/src/index.ts +53 -0
  92. package/test/aps-engine.test.ts +264 -0
  93. package/tsconfig.json +22 -0
  94. package/tsconfig.test.json +10 -0
@@ -0,0 +1,55 @@
1
+ /* eslint-disable */
2
+ // This file is auto-generated from tool-call-context.schema.json. Do not edit manually.
3
+
4
+ /**
5
+ * The evaluation input provided to policies at the Tool Call Interception point.
6
+ */
7
+ export type ToolCallContext = ApsBase & {
8
+ /**
9
+ * The name of the tool the LLM has requested to invoke.
10
+ */
11
+ tool_name: string;
12
+ /**
13
+ * The arguments provided by the LLM for the tool invocation.
14
+ */
15
+ arguments: {
16
+ [k: string]: unknown;
17
+ };
18
+ calling_message: AssistantMessage;
19
+ metadata: Metadata;
20
+ };
21
+
22
+ /**
23
+ * Base schema for the Agent Policy Specification v0.1.0. All other APS schemas extend this schema. Defines shared types used across the specification.
24
+ */
25
+ export interface ApsBase {
26
+ [k: string]: unknown;
27
+ }
28
+ /**
29
+ * A message produced by the LLM (role must be 'assistant').
30
+ */
31
+ export interface AssistantMessage {
32
+ role: "assistant";
33
+ /**
34
+ * The text content of the assistant message.
35
+ */
36
+ content: string;
37
+ }
38
+ /**
39
+ * Common metadata attached to every APS context object.
40
+ */
41
+ export interface Metadata {
42
+ /**
43
+ * Unique identifier for the agent that owns this session.
44
+ */
45
+ agent_id: string;
46
+ /**
47
+ * Unique identifier for the current session.
48
+ */
49
+ session_id: string;
50
+ /**
51
+ * ISO 8601 timestamp of when the interception occurred.
52
+ */
53
+ timestamp: string;
54
+ [k: string]: unknown;
55
+ }
package/src/index.ts ADDED
@@ -0,0 +1,53 @@
1
+ export type {
2
+ Message,
3
+ InputContext,
4
+ } from "./generated/input-context.js";
5
+
6
+ export type {
7
+ OutputContext,
8
+ Metadata,
9
+ } from './generated/output-context.js';
10
+
11
+ export type {
12
+ ToolCallContext,
13
+ AssistantMessage,
14
+ } from './generated/tool-call-context.js';
15
+
16
+ export type {
17
+ PolicyDecision,
18
+ AllowDecision,
19
+ DenyDecision,
20
+ RedactDecision,
21
+ TransformDecision,
22
+ AuditDecision,
23
+ Redaction,
24
+ Transformation,
25
+ } from './generated/policy-decision.js';
26
+
27
+ export type {
28
+ DSLPolicy,
29
+ Condition,
30
+ EqualsCondition,
31
+ ContainsCondition,
32
+ NotInCondition,
33
+ GreaterThanCondition,
34
+ AlwaysCondition,
35
+ } from './generated/dsl-policy.js';
36
+
37
+ export type {
38
+ PolicySet as JsonPolicySet,
39
+ PolicyEntry,
40
+ } from './generated/policy-set.js';
41
+
42
+ export type { InputPolicy, ToolCallPolicy, OutputPolicy } from "./core/policy.js";
43
+
44
+ export {
45
+ PolicyDenialError,
46
+ PolicyEvaluationError,
47
+ } from "./core/errors.js";
48
+ export type { AuditRecord, InterceptionPoint } from "./core/errors.js";
49
+
50
+
51
+ export type { PolicySet, OnErrorBehavior } from "./engine/policy-set.js";
52
+ export { ApsEngine } from "./engine/aps-engine.js";
53
+ export type { ApsEngineOptions, AuditHandler } from "./engine/aps-engine.js";
@@ -0,0 +1,264 @@
1
+ import { jest, describe, it, expect } from "@jest/globals";
2
+ import { ApsEngine } from "../src/engine/aps-engine.js";
3
+ import { PolicyDenialError, PolicyEvaluationError } from "../src/core/errors.js";
4
+ import type { AuditRecord } from "../src/core/errors.js";
5
+ import type { InputPolicy, ToolCallPolicy, OutputPolicy } from "../src/core/policy.js";
6
+ import { InputContext } from "../src/generated/input-context.js";
7
+ import { ToolCallContext } from "../src/generated/tool-call-context.js";
8
+ import { OutputContext } from "../src/generated/output-context.js";
9
+ import { PolicyDecision } from "../src/generated/policy-decision.js";
10
+
11
+ // ─── Fixtures ─────────────────────────────────────────────────────────────────
12
+
13
+ const inputCtx: InputContext = {
14
+ messages: [{ role: "user", content: "hello" }],
15
+ metadata: { agent_id: "a1", session_id: "s1", timestamp: "2026-01-01T00:00:00Z" },
16
+ };
17
+
18
+ const toolCallCtx: ToolCallContext = {
19
+ tool_name: "read_file",
20
+ arguments: { path: "/data.txt" },
21
+ calling_message: { role: "assistant", content: "I will read the file." },
22
+ metadata: { agent_id: "a1", session_id: "s1", timestamp: "2026-01-01T00:00:00Z" },
23
+ };
24
+
25
+ const outputCtx: OutputContext = {
26
+ response: { role: "assistant", content: "Here is the result." },
27
+ metadata: { agent_id: "a1", session_id: "s1", timestamp: "2026-01-01T00:00:00Z" },
28
+ };
29
+
30
+ function makeInputPolicy(id: string, decision: PolicyDecision): InputPolicy {
31
+ return { id, evaluate: () => decision };
32
+ }
33
+
34
+ function makeToolCallPolicy(id: string, decision: PolicyDecision): ToolCallPolicy {
35
+ return { id, evaluate: () => decision };
36
+ }
37
+
38
+ function makeOutputPolicy(id: string, decision: PolicyDecision): OutputPolicy {
39
+ return { id, evaluate: () => decision };
40
+ }
41
+
42
+ // ─── evaluateInput ────────────────────────────────────────────────────────────
43
+
44
+ describe("ApsEngine.evaluateInput", () => {
45
+ it("resolves when all input policies allow", async () => {
46
+ const engine = new ApsEngine({
47
+ policySet: {
48
+ input: [
49
+ makeInputPolicy("p1", { decision: "allow" }),
50
+ makeInputPolicy("p2", { decision: "allow" }),
51
+ ],
52
+ },
53
+ });
54
+ await expect(engine.evaluateInput(inputCtx)).resolves.toBeUndefined();
55
+ });
56
+
57
+ it("throws PolicyDenialError when a policy denies", async () => {
58
+ const engine = new ApsEngine({
59
+ policySet: {
60
+ input: [makeInputPolicy("deny-policy", { decision: "deny", reason: "Not allowed" })],
61
+ },
62
+ });
63
+ await expect(engine.evaluateInput(inputCtx)).rejects.toThrow(PolicyDenialError);
64
+ });
65
+
66
+ it("sets the correct interception_point on denial", async () => {
67
+ const engine = new ApsEngine({
68
+ policySet: { input: [makeInputPolicy("p", { decision: "deny" })] },
69
+ });
70
+ try {
71
+ await engine.evaluateInput(inputCtx);
72
+ } catch (err) {
73
+ expect(err).toBeInstanceOf(PolicyDenialError);
74
+ expect((err as PolicyDenialError).interception_point).toBe("input");
75
+ }
76
+ });
77
+
78
+ it("stops evaluating after the first deny", async () => {
79
+ const secondPolicy: InputPolicy = {
80
+ id: "second",
81
+ evaluate: jest.fn(() => ({ decision: "allow" as const })),
82
+ };
83
+ const engine = new ApsEngine({
84
+ policySet: {
85
+ input: [
86
+ makeInputPolicy("deny", { decision: "deny" }),
87
+ secondPolicy,
88
+ ],
89
+ },
90
+ });
91
+ await expect(engine.evaluateInput(inputCtx)).rejects.toThrow(PolicyDenialError);
92
+ expect(secondPolicy.evaluate).not.toHaveBeenCalled();
93
+ });
94
+
95
+ it("resolves with no input policies configured", async () => {
96
+ const engine = new ApsEngine({ policySet: {} });
97
+ await expect(engine.evaluateInput(inputCtx)).resolves.toBeUndefined();
98
+ });
99
+ });
100
+
101
+ // ─── evaluateToolCall ─────────────────────────────────────────────────────────
102
+
103
+ describe("ApsEngine.evaluateToolCall", () => {
104
+ it("resolves when the tool call is allowed", async () => {
105
+ const engine = new ApsEngine({
106
+ policySet: { tool_call: [makeToolCallPolicy("p", { decision: "allow" })] },
107
+ });
108
+ await expect(engine.evaluateToolCall(toolCallCtx)).resolves.toBeUndefined();
109
+ });
110
+
111
+ it("throws PolicyDenialError when the tool call is denied", async () => {
112
+ const engine = new ApsEngine({
113
+ policySet: {
114
+ tool_call: [
115
+ makeToolCallPolicy("block-tool", { decision: "deny", reason: "Tool not permitted" }),
116
+ ],
117
+ },
118
+ });
119
+ await expect(engine.evaluateToolCall(toolCallCtx)).rejects.toThrow(PolicyDenialError);
120
+ });
121
+
122
+ it("sets interception_point to tool_call on denial", async () => {
123
+ const engine = new ApsEngine({
124
+ policySet: { tool_call: [makeToolCallPolicy("p", { decision: "deny" })] },
125
+ });
126
+ try {
127
+ await engine.evaluateToolCall(toolCallCtx);
128
+ } catch (err) {
129
+ expect((err as PolicyDenialError).interception_point).toBe("tool_call");
130
+ }
131
+ });
132
+ });
133
+
134
+ // ─── evaluateOutput ───────────────────────────────────────────────────────────
135
+
136
+ describe("ApsEngine.evaluateOutput", () => {
137
+ it("resolves when output policy allows", async () => {
138
+ const engine = new ApsEngine({
139
+ policySet: { output: [makeOutputPolicy("p", { decision: "allow" })] },
140
+ });
141
+ await expect(engine.evaluateOutput(outputCtx)).resolves.toBeUndefined();
142
+ });
143
+
144
+ it("throws PolicyDenialError when output is denied", async () => {
145
+ const engine = new ApsEngine({
146
+ policySet: {
147
+ output: [makeOutputPolicy("block-output", { decision: "deny", reason: "Unsafe content" })],
148
+ },
149
+ });
150
+ await expect(engine.evaluateOutput(outputCtx)).rejects.toThrow(PolicyDenialError);
151
+ });
152
+ });
153
+
154
+ // ─── Audit ────────────────────────────────────────────────────────────────────
155
+
156
+ describe("ApsEngine audit", () => {
157
+ it("calls onAudit for audit decisions and continues", async () => {
158
+ const auditRecords: AuditRecord[] = [];
159
+ const engine = new ApsEngine({
160
+ policySet: {
161
+ input: [
162
+ makeInputPolicy("audit-policy", { decision: "audit", reason: "Logging" }),
163
+ makeInputPolicy("allow-policy", { decision: "allow" }),
164
+ ],
165
+ },
166
+ onAudit: record => { auditRecords.push(record); },
167
+ });
168
+
169
+ await engine.evaluateInput(inputCtx);
170
+
171
+ expect(auditRecords).toHaveLength(1);
172
+ expect(auditRecords[0]!.decision).toBe("audit");
173
+ expect(auditRecords[0]!.policy_id).toBe("audit-policy");
174
+ expect(auditRecords[0]!.interception_point).toBe("input");
175
+ });
176
+
177
+ it("calls onAudit when a policy denies", async () => {
178
+ const auditRecords: AuditRecord[] = [];
179
+ const engine = new ApsEngine({
180
+ policySet: {
181
+ input: [makeInputPolicy("deny-policy", { decision: "deny", reason: "Blocked" })],
182
+ },
183
+ onAudit: record => { auditRecords.push(record); },
184
+ });
185
+
186
+ await expect(engine.evaluateInput(inputCtx)).rejects.toThrow(PolicyDenialError);
187
+ expect(auditRecords).toHaveLength(1);
188
+ expect(auditRecords[0]!.decision).toBe("deny");
189
+ });
190
+
191
+ it("calls onAudit for redact decisions and continues", async () => {
192
+ const auditRecords: AuditRecord[] = [];
193
+ const engine = new ApsEngine({
194
+ policySet: {
195
+ input: [
196
+ makeInputPolicy("redact-policy", {
197
+ decision: "redact",
198
+ redactions: [{ field: "messages[0].content", strategy: "mask", replacement: "[REDACTED]" }],
199
+ }),
200
+ ],
201
+ },
202
+ onAudit: record => { auditRecords.push(record); },
203
+ });
204
+
205
+ await engine.evaluateInput(inputCtx);
206
+
207
+ expect(auditRecords).toHaveLength(1);
208
+ expect(auditRecords[0]!.decision).toBe("redact");
209
+ });
210
+ });
211
+
212
+ // ─── Error handling ───────────────────────────────────────────────────────────
213
+
214
+ describe("ApsEngine error handling", () => {
215
+ it("throws PolicyEvaluationError when a policy throws (on_error: deny)", async () => {
216
+ const brokenPolicy: InputPolicy = {
217
+ id: "broken",
218
+ evaluate: () => { throw new Error("Internal failure"); },
219
+ };
220
+ const engine = new ApsEngine({
221
+ policySet: { on_error: "deny", input: [brokenPolicy] },
222
+ });
223
+ await expect(engine.evaluateInput(inputCtx)).rejects.toThrow(PolicyEvaluationError);
224
+ });
225
+
226
+ it("continues when a policy throws and on_error is allow", async () => {
227
+ const brokenPolicy: InputPolicy = {
228
+ id: "broken",
229
+ evaluate: () => { throw new Error("Internal failure"); },
230
+ };
231
+ const engine = new ApsEngine({
232
+ policySet: { on_error: "allow", input: [brokenPolicy] },
233
+ });
234
+ await expect(engine.evaluateInput(inputCtx)).resolves.toBeUndefined();
235
+ });
236
+
237
+ it("defaults to deny when on_error is not set and a policy throws", async () => {
238
+ const brokenPolicy: InputPolicy = {
239
+ id: "broken",
240
+ evaluate: () => { throw new Error("Internal failure"); },
241
+ };
242
+ const engine = new ApsEngine({
243
+ policySet: { input: [brokenPolicy] },
244
+ });
245
+ await expect(engine.evaluateInput(inputCtx)).rejects.toThrow(PolicyEvaluationError);
246
+ });
247
+
248
+ it("sets the correct policy_id on PolicyEvaluationError", async () => {
249
+ const engine = new ApsEngine({
250
+ policySet: {
251
+ input: [{
252
+ id: "my-failing-policy",
253
+ evaluate: () => { throw new Error("boom"); },
254
+ }],
255
+ },
256
+ });
257
+ try {
258
+ await engine.evaluateInput(inputCtx);
259
+ } catch (err) {
260
+ expect(err).toBeInstanceOf(PolicyEvaluationError);
261
+ expect((err as PolicyEvaluationError).policy_id).toBe("my-failing-policy");
262
+ }
263
+ });
264
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "lib": ["ES2022"],
7
+ "outDir": "./dist",
8
+ "rootDir": "./src",
9
+ "declaration": true,
10
+ "declarationMap": true,
11
+ "sourceMap": true,
12
+ "strict": true,
13
+ "noUncheckedIndexedAccess": true,
14
+ "noImplicitOverride": true,
15
+ "exactOptionalPropertyTypes": true,
16
+ "forceConsistentCasingInFileNames": true,
17
+ "esModuleInterop": true,
18
+ "skipLibCheck": true
19
+ },
20
+ "include": ["src/**/*"],
21
+ "exclude": ["node_modules", "dist", "examples"]
22
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "rootDir": ".",
5
+ "outDir": "./dist-test",
6
+ "isolatedModules": true,
7
+ "types": ["jest"]
8
+ },
9
+ "include": ["src/**/*", "test/**/*"]
10
+ }