@highflame/policy 2.0.0 → 2.0.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,207 @@
1
+ /**
2
+ * Studio UI Integration Tests
3
+ *
4
+ * These tests simulate exactly how the Studio UI (Overwatch admin dashboard)
5
+ * will use the @highflame/policy npm package.
6
+ */
7
+
8
+ import { describe, it, expect } from 'vitest';
9
+
10
+ // Browser-safe imports (simulating '@highflame/policy/types')
11
+ import {
12
+ EntityType,
13
+ ActionType,
14
+ PolicyBuilder,
15
+ OVERWATCH_SCHEMA,
16
+ PALISADE_SCHEMA,
17
+ OVERWATCH_CONTEXT,
18
+ PALISADE_CONTEXT,
19
+ OverwatchContextKey,
20
+ PalisadeContextKey,
21
+ type ServiceContext,
22
+ type ActionContext,
23
+ } from './types.js';
24
+
25
+ // Node.js only imports (for API routes)
26
+ import {
27
+ PolicyEngine,
28
+ PolicyValidator,
29
+ newEntityUID,
30
+ newEntity,
31
+ } from './index.js';
32
+
33
+ describe('Studio UI Integration Tests', () => {
34
+ /**
35
+ * Test: PolicyBuilder action formatting
36
+ *
37
+ * Tests that simple action names are properly formatted to Cedar syntax.
38
+ * This is the "normal approach" for non-namespaced schemas.
39
+ */
40
+ it('should format simple action names to Cedar syntax', () => {
41
+ // Using simple action name (normal approach)
42
+ const policy = PolicyBuilder.permit()
43
+ .principalType('User')
44
+ .action('call_tool')
45
+ .resourceType('Tool')
46
+ .build();
47
+
48
+ const cedarText = policy.toCedar();
49
+
50
+ // Simple actions should be wrapped as Action::"..."
51
+ expect(cedarText).toContain('action == Action::"call_tool"');
52
+ expect(cedarText).not.toContain('Action::"Action::'); // No double-wrapping
53
+ });
54
+
55
+ /**
56
+ * Test 1: Schema and Context Loading
57
+ *
58
+ * Studio UI needs to load schemas for validation and context metadata
59
+ * for populating form dropdowns.
60
+ */
61
+ it('should load schemas and context metadata for form builders', () => {
62
+ // Schemas should be strings
63
+ expect(typeof OVERWATCH_SCHEMA).toBe('string');
64
+ expect(OVERWATCH_SCHEMA).toContain('namespace Overwatch');
65
+ expect(typeof PALISADE_SCHEMA).toBe('string');
66
+ expect(PALISADE_SCHEMA).toContain('namespace Palisade');
67
+
68
+ // Context metadata should have action definitions
69
+ expect(OVERWATCH_CONTEXT.service).toBe('overwatch');
70
+ expect(OVERWATCH_CONTEXT.actions.length).toBeGreaterThan(0);
71
+
72
+ // Find call_tool action and verify context attributes
73
+ const callToolAction = OVERWATCH_CONTEXT.actions.find(
74
+ (a: ActionContext) => a.name === 'call_tool'
75
+ );
76
+ expect(callToolAction).toBeDefined();
77
+ expect(callToolAction!.context_attributes.length).toBeGreaterThan(0);
78
+
79
+ // Context keys should be available for type-safe form building
80
+ expect(OverwatchContextKey.ToolName).toBe('tool_name');
81
+ expect(OverwatchContextKey.ThreatCount).toBe('threat_count');
82
+ expect(PalisadeContextKey.Severity).toBe('severity');
83
+ expect(PalisadeContextKey.Environment).toBe('environment');
84
+ });
85
+
86
+ /**
87
+ * Test 2: Full Round-Trip - Create Policy → Validate → Evaluate
88
+ *
89
+ * This simulates the complete flow:
90
+ * 1. User creates a policy using PolicyBuilder in the UI
91
+ * 2. API validates the policy against the schema
92
+ * 3. Runtime evaluates the policy
93
+ */
94
+ it('should complete full policy lifecycle for Overwatch', () => {
95
+ // Step 1: User creates policy in form UI
96
+ const policy = PolicyBuilder.permit()
97
+ .principalType('Overwatch::User')
98
+ .action('Overwatch::Action::"call_tool"')
99
+ .resourceType('Overwatch::Tool')
100
+ .whenRaw('context.threat_count < 5')
101
+ .build();
102
+
103
+ const cedarText = policy.toCedar();
104
+ expect(cedarText).toContain('permit');
105
+ expect(cedarText).toContain('Overwatch::User');
106
+
107
+ // Step 2: API validates policy against schema
108
+ const validator = new PolicyValidator(OVERWATCH_SCHEMA);
109
+ const validationResult = validator.validate(cedarText);
110
+ expect(validationResult.valid).toBe(true);
111
+
112
+ // Step 3: Runtime loads and evaluates policy
113
+ const engine = new PolicyEngine({ schema: OVERWATCH_SCHEMA });
114
+ engine.loadPolicies(cedarText);
115
+
116
+ const entities = [
117
+ newEntity('Overwatch::User', 'mcp_client', { user_type: 'external', email: 'test@example.com' }),
118
+ newEntity('Overwatch::Tool', 'shell', { tool_name: 'shell', risk_level: 'high' }),
119
+ ];
120
+
121
+ // Full context as Guardian will provide at runtime
122
+ const baseContext = {
123
+ content: 'ls -la',
124
+ source: 'claudecode',
125
+ event: 'PreToolUse',
126
+ user_email: 'test@example.com',
127
+ tool_name: 'shell',
128
+ mcp_server: 'filesystem',
129
+ mcp_tool: 'shell',
130
+ path: '/workspace',
131
+ cwd: '/workspace',
132
+ workspace_root: '/workspace',
133
+ highest_severity: 'low',
134
+ threat_categories: [],
135
+ threat_types: [],
136
+ yara_threats: [],
137
+ max_threat_severity: 1,
138
+ contains_secrets: false,
139
+ response_content: '',
140
+ };
141
+
142
+ // Should allow when threat_count < 5
143
+ const allowDecision = engine.evaluate({
144
+ principal: newEntityUID('Overwatch::User', 'mcp_client'),
145
+ action: 'Overwatch::Action::"call_tool"',
146
+ resource: newEntityUID('Overwatch::Tool', 'shell'),
147
+ context: { ...baseContext, threat_count: 3 },
148
+ entities,
149
+ });
150
+ expect(allowDecision.effect).toBe('Allow');
151
+
152
+ // Should deny when threat_count >= 5 (no matching permit)
153
+ const denyDecision = engine.evaluate({
154
+ principal: newEntityUID('Overwatch::User', 'mcp_client'),
155
+ action: 'Overwatch::Action::"call_tool"',
156
+ resource: newEntityUID('Overwatch::Tool', 'shell'),
157
+ context: { ...baseContext, threat_count: 10 },
158
+ entities,
159
+ });
160
+ expect(denyDecision.effect).toBe('Deny');
161
+ });
162
+
163
+ /**
164
+ * Test 3: Full Round-Trip for Palisade
165
+ *
166
+ * Same flow but for Palisade ML security policies.
167
+ */
168
+ it('should complete full policy lifecycle for Palisade', () => {
169
+ // Step 1: Create a forbid policy for CRITICAL findings
170
+ const policy = PolicyBuilder.forbid()
171
+ .principalType('Palisade::Scanner')
172
+ .action('Palisade::Action::"load_model"')
173
+ .resourceType('Palisade::Artifact')
174
+ .whenRaw('context.severity == "CRITICAL"')
175
+ .build();
176
+
177
+ const cedarText = policy.toCedar();
178
+
179
+ // Step 2: Validate
180
+ const validator = new PolicyValidator(PALISADE_SCHEMA);
181
+ expect(validator.validate(cedarText).valid).toBe(true);
182
+
183
+ // Step 3: Evaluate
184
+ const engine = new PolicyEngine({ schema: PALISADE_SCHEMA });
185
+ engine.loadPolicies(cedarText);
186
+
187
+ const entities = [
188
+ newEntity('Palisade::Scanner', 'palisade', { scanner_type: 'ml' }),
189
+ newEntity('Palisade::Artifact', 'model.pkl', {
190
+ artifact_format: 'pickle',
191
+ path: '/model.pkl',
192
+ signed: false,
193
+ signer: 'unsigned',
194
+ }),
195
+ ];
196
+
197
+ // Should deny CRITICAL findings
198
+ const decision = engine.evaluate({
199
+ principal: newEntityUID('Palisade::Scanner', 'palisade'),
200
+ action: 'Palisade::Action::"load_model"',
201
+ resource: newEntityUID('Palisade::Artifact', 'model.pkl'),
202
+ context: { severity: 'CRITICAL' },
203
+ entities,
204
+ });
205
+ expect(decision.effect).toBe('Deny');
206
+ });
207
+ });
package/src/types.ts CHANGED
@@ -16,3 +16,20 @@ export * from './builder.js';
16
16
 
17
17
  // Error types - works in browser (no WASM dependency)
18
18
  export * from './errors.js';
19
+
20
+ // Service-specific schemas and context (inlined, browser-safe)
21
+ export {
22
+ OVERWATCH_SCHEMA,
23
+ PALISADE_SCHEMA,
24
+ OVERWATCH_CONTEXT,
25
+ PALISADE_CONTEXT,
26
+ } from './service-schemas.gen.js';
27
+ export type {
28
+ ContextAttribute,
29
+ ActionContext,
30
+ ServiceContext,
31
+ } from './service-schemas.gen.js';
32
+
33
+ // Service-specific context key enums
34
+ export { OverwatchContextKey } from './overwatch-context.gen.js';
35
+ export { PalisadeContextKey } from './palisade-context.gen.js';