outcome-cli 1.0.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.
- package/README.md +261 -0
- package/package.json +95 -0
- package/src/agents/README.md +139 -0
- package/src/agents/adapters/anthropic.adapter.ts +166 -0
- package/src/agents/adapters/dalle.adapter.ts +145 -0
- package/src/agents/adapters/gemini.adapter.ts +134 -0
- package/src/agents/adapters/imagen.adapter.ts +106 -0
- package/src/agents/adapters/nano-banana.adapter.ts +129 -0
- package/src/agents/adapters/openai.adapter.ts +165 -0
- package/src/agents/adapters/veo.adapter.ts +130 -0
- package/src/agents/agent.schema.property.test.ts +379 -0
- package/src/agents/agent.schema.test.ts +148 -0
- package/src/agents/agent.schema.ts +263 -0
- package/src/agents/index.ts +60 -0
- package/src/agents/registered-agent.schema.ts +356 -0
- package/src/agents/registry.ts +97 -0
- package/src/agents/tournament-configs.property.test.ts +266 -0
- package/src/cli/README.md +145 -0
- package/src/cli/commands/define.ts +79 -0
- package/src/cli/commands/list.ts +46 -0
- package/src/cli/commands/logs.ts +83 -0
- package/src/cli/commands/run.ts +416 -0
- package/src/cli/commands/verify.ts +110 -0
- package/src/cli/index.ts +81 -0
- package/src/config/README.md +128 -0
- package/src/config/env.ts +262 -0
- package/src/config/index.ts +19 -0
- package/src/eval/README.md +318 -0
- package/src/eval/ai-judge.test.ts +435 -0
- package/src/eval/ai-judge.ts +368 -0
- package/src/eval/code-validators.ts +414 -0
- package/src/eval/evaluateOutcome.property.test.ts +1174 -0
- package/src/eval/evaluateOutcome.ts +591 -0
- package/src/eval/immigration-validators.ts +122 -0
- package/src/eval/index.ts +90 -0
- package/src/eval/judge-cache.ts +402 -0
- package/src/eval/tournament-validators.property.test.ts +439 -0
- package/src/eval/validators.property.test.ts +1118 -0
- package/src/eval/validators.ts +1199 -0
- package/src/eval/weighted-scorer.ts +285 -0
- package/src/index.ts +17 -0
- package/src/league/README.md +188 -0
- package/src/league/health-check.ts +353 -0
- package/src/league/index.ts +93 -0
- package/src/league/killAgent.ts +151 -0
- package/src/league/league.test.ts +1151 -0
- package/src/league/runLeague.ts +843 -0
- package/src/league/scoreAgent.ts +175 -0
- package/src/modules/omnibridge/__tests__/.gitkeep +1 -0
- package/src/modules/omnibridge/__tests__/auth-tunnel.property.test.ts +524 -0
- package/src/modules/omnibridge/__tests__/deterministic-logger.property.test.ts +965 -0
- package/src/modules/omnibridge/__tests__/ghost-api.property.test.ts +461 -0
- package/src/modules/omnibridge/__tests__/omnibridge-integration.test.ts +542 -0
- package/src/modules/omnibridge/__tests__/parallel-executor.property.test.ts +671 -0
- package/src/modules/omnibridge/__tests__/semantic-normalizer.property.test.ts +521 -0
- package/src/modules/omnibridge/__tests__/semantic-normalizer.test.ts +254 -0
- package/src/modules/omnibridge/__tests__/session-vault.property.test.ts +367 -0
- package/src/modules/omnibridge/__tests__/shadow-session.property.test.ts +523 -0
- package/src/modules/omnibridge/__tests__/triangulation-engine.property.test.ts +292 -0
- package/src/modules/omnibridge/__tests__/verification-engine.property.test.ts +769 -0
- package/src/modules/omnibridge/api/.gitkeep +1 -0
- package/src/modules/omnibridge/api/ghost-api.ts +1087 -0
- package/src/modules/omnibridge/auth/.gitkeep +1 -0
- package/src/modules/omnibridge/auth/auth-tunnel.ts +843 -0
- package/src/modules/omnibridge/auth/session-vault.ts +577 -0
- package/src/modules/omnibridge/core/.gitkeep +1 -0
- package/src/modules/omnibridge/core/semantic-normalizer.ts +702 -0
- package/src/modules/omnibridge/core/triangulation-engine.ts +530 -0
- package/src/modules/omnibridge/core/types.ts +610 -0
- package/src/modules/omnibridge/execution/.gitkeep +1 -0
- package/src/modules/omnibridge/execution/deterministic-logger.ts +629 -0
- package/src/modules/omnibridge/execution/parallel-executor.ts +542 -0
- package/src/modules/omnibridge/execution/shadow-session.ts +794 -0
- package/src/modules/omnibridge/index.ts +212 -0
- package/src/modules/omnibridge/omnibridge.ts +510 -0
- package/src/modules/omnibridge/verification/.gitkeep +1 -0
- package/src/modules/omnibridge/verification/verification-engine.ts +783 -0
- package/src/outcomes/README.md +75 -0
- package/src/outcomes/acquire-pilot-customer.ts +297 -0
- package/src/outcomes/code-delivery-outcomes.ts +89 -0
- package/src/outcomes/code-outcomes.ts +256 -0
- package/src/outcomes/code_review_battle.test.ts +135 -0
- package/src/outcomes/code_review_battle.ts +135 -0
- package/src/outcomes/cold_email_battle.ts +97 -0
- package/src/outcomes/content_creation_battle.ts +160 -0
- package/src/outcomes/f1_stem_opt_compliance.ts +61 -0
- package/src/outcomes/index.ts +107 -0
- package/src/outcomes/lead_gen_battle.test.ts +113 -0
- package/src/outcomes/lead_gen_battle.ts +99 -0
- package/src/outcomes/outcome.schema.property.test.ts +229 -0
- package/src/outcomes/outcome.schema.ts +187 -0
- package/src/outcomes/qualified_sales_interest.ts +118 -0
- package/src/outcomes/swarm_planner.property.test.ts +370 -0
- package/src/outcomes/swarm_planner.ts +96 -0
- package/src/outcomes/web_extraction.ts +234 -0
- package/src/runtime/README.md +220 -0
- package/src/runtime/agentRunner.test.ts +341 -0
- package/src/runtime/agentRunner.ts +746 -0
- package/src/runtime/claudeAdapter.ts +232 -0
- package/src/runtime/costTracker.ts +123 -0
- package/src/runtime/index.ts +34 -0
- package/src/runtime/modelAdapter.property.test.ts +305 -0
- package/src/runtime/modelAdapter.ts +144 -0
- package/src/runtime/openaiAdapter.ts +235 -0
- package/src/utils/README.md +122 -0
- package/src/utils/command-runner.ts +134 -0
- package/src/utils/cost-guard.ts +379 -0
- package/src/utils/errors.test.ts +290 -0
- package/src/utils/errors.ts +442 -0
- package/src/utils/index.ts +37 -0
- package/src/utils/logger.test.ts +361 -0
- package/src/utils/logger.ts +419 -0
- package/src/utils/output-parsers.ts +216 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit Tests for Semantic Normalizer
|
|
3
|
+
*
|
|
4
|
+
* Tests for HTML parsing, noise stripping, Intent_ID assignment, and form extraction.
|
|
5
|
+
* Requirements: 1.1-1.6
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, test, expect } from 'vitest';
|
|
9
|
+
import { SemanticNormalizer } from '../core/semantic-normalizer.js';
|
|
10
|
+
|
|
11
|
+
const normalizer = new SemanticNormalizer();
|
|
12
|
+
|
|
13
|
+
describe('SemanticNormalizer', () => {
|
|
14
|
+
describe('stripNoise', () => {
|
|
15
|
+
test('removes script tags', () => {
|
|
16
|
+
const html = '<html><body><script>alert("test")</script><p>Content</p></body></html>';
|
|
17
|
+
const result = normalizer.stripNoise(html);
|
|
18
|
+
expect(result).not.toContain('<script>');
|
|
19
|
+
expect(result).toContain('<p>Content</p>');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('removes style tags', () => {
|
|
23
|
+
const html = '<html><head><style>.test { color: red; }</style></head><body><p>Content</p></body></html>';
|
|
24
|
+
const result = normalizer.stripNoise(html);
|
|
25
|
+
expect(result).not.toContain('<style>');
|
|
26
|
+
expect(result).toContain('<p>Content</p>');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('removes inline styles', () => {
|
|
30
|
+
const html = '<html><body><div style="color: red; margin: 10px;">Content</div></body></html>';
|
|
31
|
+
const result = normalizer.stripNoise(html);
|
|
32
|
+
expect(result).not.toContain('style=');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('removes tracking attributes', () => {
|
|
36
|
+
const html = '<html><body><div data-tracking="click" data-analytics="view">Content</div></body></html>';
|
|
37
|
+
const result = normalizer.stripNoise(html);
|
|
38
|
+
expect(result).not.toContain('data-tracking');
|
|
39
|
+
expect(result).not.toContain('data-analytics');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('removes meta tags', () => {
|
|
43
|
+
const html = '<html><head><meta name="description" content="test"><meta name="keywords" content="test"></head><body><p>Content</p></body></html>';
|
|
44
|
+
const result = normalizer.stripNoise(html);
|
|
45
|
+
expect(result).not.toContain('<meta');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('removes stylesheet links', () => {
|
|
49
|
+
const html = '<html><head><link rel="stylesheet" href="styles.css"></head><body><p>Content</p></body></html>';
|
|
50
|
+
const result = normalizer.stripNoise(html);
|
|
51
|
+
expect(result).not.toContain('stylesheet');
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('assignIntentId', () => {
|
|
56
|
+
test('assigns ACTION category for button elements', () => {
|
|
57
|
+
const result = normalizer.assignIntentId('button', undefined, {}, 'Submit', []);
|
|
58
|
+
expect(result.category).toBe('ACTION');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('assigns NAV category for anchor elements', () => {
|
|
62
|
+
const result = normalizer.assignIntentId('a', undefined, { href: '/home' }, 'Home', []);
|
|
63
|
+
expect(result.category).toBe('NAV');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('assigns INPUT category for input elements', () => {
|
|
67
|
+
const result = normalizer.assignIntentId('input', undefined, { type: 'text' }, '', []);
|
|
68
|
+
expect(result.category).toBe('INPUT');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('prioritizes ARIA role over tag name', () => {
|
|
72
|
+
// div with role="button" should be ACTION, not DISPLAY
|
|
73
|
+
const result = normalizer.assignIntentId('div', 'button', {}, 'Click me', []);
|
|
74
|
+
expect(result.category).toBe('ACTION');
|
|
75
|
+
expect(result.confidence).toBe(0.95);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('infers LOGIN purpose from text content', () => {
|
|
79
|
+
const result = normalizer.assignIntentId('button', undefined, {}, 'Login', []);
|
|
80
|
+
expect(result.purpose).toBe('LOGIN');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('infers SEARCH purpose from placeholder', () => {
|
|
84
|
+
const result = normalizer.assignIntentId('input', undefined, { placeholder: 'Search...' }, '', []);
|
|
85
|
+
expect(result.purpose).toBe('SEARCH');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('infers EMAIL_INPUT purpose from type', () => {
|
|
89
|
+
const result = normalizer.assignIntentId('input', undefined, { type: 'email' }, '', []);
|
|
90
|
+
expect(result.purpose).toBe('EMAIL_INPUT');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('infers PASSWORD_INPUT purpose from type', () => {
|
|
94
|
+
const result = normalizer.assignIntentId('input', undefined, { type: 'password' }, '', []);
|
|
95
|
+
expect(result.purpose).toBe('PASSWORD_INPUT');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('increases confidence with more signals', () => {
|
|
99
|
+
const lowConfidence = normalizer.assignIntentId('div', undefined, {}, '', []);
|
|
100
|
+
const highConfidence = normalizer.assignIntentId('div', undefined, { id: 'main' }, 'Content', ['context']);
|
|
101
|
+
expect(highConfidence.confidence).toBeGreaterThan(lowConfidence.confidence);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
describe('normalize', () => {
|
|
107
|
+
test('extracts semantic elements from HTML', () => {
|
|
108
|
+
const html = `
|
|
109
|
+
<html>
|
|
110
|
+
<body>
|
|
111
|
+
<button id="submit-btn">Submit</button>
|
|
112
|
+
<a href="/home">Home</a>
|
|
113
|
+
<input type="text" name="username" placeholder="Username">
|
|
114
|
+
</body>
|
|
115
|
+
</html>
|
|
116
|
+
`;
|
|
117
|
+
const result = normalizer.normalize(html, 'https://test.com');
|
|
118
|
+
|
|
119
|
+
expect(result.elements.length).toBeGreaterThan(0);
|
|
120
|
+
expect(result.sourceUrl).toBe('https://test.com');
|
|
121
|
+
expect(result.createdAt).toBeDefined();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('extracts forms with fields', () => {
|
|
125
|
+
const html = `
|
|
126
|
+
<html>
|
|
127
|
+
<body>
|
|
128
|
+
<form action="/login" method="POST">
|
|
129
|
+
<label for="email">Email</label>
|
|
130
|
+
<input type="email" id="email" name="email" required>
|
|
131
|
+
<label for="password">Password</label>
|
|
132
|
+
<input type="password" id="password" name="password" required minlength="8">
|
|
133
|
+
<button type="submit">Login</button>
|
|
134
|
+
</form>
|
|
135
|
+
</body>
|
|
136
|
+
</html>
|
|
137
|
+
`;
|
|
138
|
+
const result = normalizer.normalize(html, 'https://test.com');
|
|
139
|
+
|
|
140
|
+
expect(result.forms.length).toBe(1);
|
|
141
|
+
const form = result.forms[0];
|
|
142
|
+
expect(form.action).toBe('/login');
|
|
143
|
+
expect(form.method).toBe('POST');
|
|
144
|
+
expect(form.fields.length).toBe(2);
|
|
145
|
+
|
|
146
|
+
const emailField = form.fields.find((f) => f.name === 'email');
|
|
147
|
+
expect(emailField).toBeDefined();
|
|
148
|
+
expect(emailField?.required).toBe(true);
|
|
149
|
+
expect(emailField?.type).toBe('email');
|
|
150
|
+
|
|
151
|
+
const passwordField = form.fields.find((f) => f.name === 'password');
|
|
152
|
+
expect(passwordField).toBeDefined();
|
|
153
|
+
expect(passwordField?.required).toBe(true);
|
|
154
|
+
expect(passwordField?.validationRules).toContain('minlength:8');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('extracts navigation links', () => {
|
|
158
|
+
const html = `
|
|
159
|
+
<html>
|
|
160
|
+
<body>
|
|
161
|
+
<nav>
|
|
162
|
+
<a href="/home">Home</a>
|
|
163
|
+
<a href="/about">About</a>
|
|
164
|
+
<a href="/contact">Contact</a>
|
|
165
|
+
</nav>
|
|
166
|
+
</body>
|
|
167
|
+
</html>
|
|
168
|
+
`;
|
|
169
|
+
const result = normalizer.normalize(html, 'https://test.com');
|
|
170
|
+
|
|
171
|
+
expect(result.navigation.primaryLinks.length).toBe(3);
|
|
172
|
+
expect(result.navigation.primaryLinks.map((l) => l.label)).toContain('Home');
|
|
173
|
+
expect(result.navigation.primaryLinks.map((l) => l.label)).toContain('About');
|
|
174
|
+
expect(result.navigation.primaryLinks.map((l) => l.label)).toContain('Contact');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test('calculates build time', () => {
|
|
178
|
+
const html = '<html><body><p>Test</p></body></html>';
|
|
179
|
+
const result = normalizer.normalize(html, 'https://test.com');
|
|
180
|
+
|
|
181
|
+
expect(result.buildTimeMs).toBeGreaterThanOrEqual(0);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('calculates token reduction', () => {
|
|
185
|
+
const html = `
|
|
186
|
+
<html>
|
|
187
|
+
<head>
|
|
188
|
+
<style>.test { color: red; background: blue; margin: 10px; padding: 20px; }</style>
|
|
189
|
+
<script>console.log("tracking"); analytics.track("pageview");</script>
|
|
190
|
+
<meta name="description" content="A very long description that takes up tokens">
|
|
191
|
+
</head>
|
|
192
|
+
<body>
|
|
193
|
+
<div style="color: red; margin: 10px; padding: 20px; background: white;">
|
|
194
|
+
<p>Simple content</p>
|
|
195
|
+
</div>
|
|
196
|
+
</body>
|
|
197
|
+
</html>
|
|
198
|
+
`;
|
|
199
|
+
const result = normalizer.normalize(html, 'https://test.com');
|
|
200
|
+
|
|
201
|
+
// Token reduction should be calculated
|
|
202
|
+
expect(typeof result.tokenReduction).toBe('number');
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test('handles elements with ARIA roles', () => {
|
|
206
|
+
const html = `
|
|
207
|
+
<html>
|
|
208
|
+
<body>
|
|
209
|
+
<div role="button" id="custom-btn">Click me</div>
|
|
210
|
+
<div role="navigation">
|
|
211
|
+
<span role="link">Nav item</span>
|
|
212
|
+
</div>
|
|
213
|
+
</body>
|
|
214
|
+
</html>
|
|
215
|
+
`;
|
|
216
|
+
const result = normalizer.normalize(html, 'https://test.com');
|
|
217
|
+
|
|
218
|
+
const buttonElement = result.elements.find((e) => e.ariaRole === 'button');
|
|
219
|
+
expect(buttonElement).toBeDefined();
|
|
220
|
+
expect(buttonElement?.intentId).toContain('ACTION');
|
|
221
|
+
|
|
222
|
+
const navElement = result.elements.find((e) => e.ariaRole === 'navigation');
|
|
223
|
+
expect(navElement).toBeDefined();
|
|
224
|
+
expect(navElement?.intentId).toContain('NAV');
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test('skips hidden elements', () => {
|
|
228
|
+
const html = `
|
|
229
|
+
<html>
|
|
230
|
+
<body>
|
|
231
|
+
<button hidden>Hidden button</button>
|
|
232
|
+
<button aria-hidden="true">Also hidden</button>
|
|
233
|
+
<button>Visible button</button>
|
|
234
|
+
</body>
|
|
235
|
+
</html>
|
|
236
|
+
`;
|
|
237
|
+
const result = normalizer.normalize(html, 'https://test.com');
|
|
238
|
+
|
|
239
|
+
const buttons = result.elements.filter((e) => e.tagName === 'button');
|
|
240
|
+
expect(buttons.length).toBe(1);
|
|
241
|
+
expect(buttons[0].label).toBe('Visible button');
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
describe('countTokens', () => {
|
|
246
|
+
test('approximates token count', () => {
|
|
247
|
+
const text = 'Hello world this is a test';
|
|
248
|
+
const tokens = normalizer.countTokens(text);
|
|
249
|
+
// ~4 chars per token, 26 chars = ~7 tokens
|
|
250
|
+
expect(tokens).toBeGreaterThan(0);
|
|
251
|
+
expect(tokens).toBeLessThan(text.length);
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
});
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Property-Based Tests for Session Vault
|
|
3
|
+
*
|
|
4
|
+
* These tests validate the correctness properties defined in the design document.
|
|
5
|
+
* Each property test runs minimum 100 iterations with randomly generated inputs.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, test, expect, beforeEach } from 'vitest';
|
|
9
|
+
import * as fc from 'fast-check';
|
|
10
|
+
import {
|
|
11
|
+
SessionVault,
|
|
12
|
+
createSessionVault,
|
|
13
|
+
generateEncryptionKey,
|
|
14
|
+
type RawSessionData,
|
|
15
|
+
} from '../auth/session-vault.js';
|
|
16
|
+
import type { ActionScope, CapabilityToken } from '../core/types.js';
|
|
17
|
+
|
|
18
|
+
// =============================================================================
|
|
19
|
+
// Test Setup
|
|
20
|
+
// =============================================================================
|
|
21
|
+
|
|
22
|
+
// Note: Each property test creates its own vault instance to ensure isolation
|
|
23
|
+
// The beforeEach vault is only used for tests that explicitly need shared state
|
|
24
|
+
|
|
25
|
+
let vault: SessionVault;
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
vault = createSessionVault({
|
|
29
|
+
encryptionKey: generateEncryptionKey(),
|
|
30
|
+
defaultTokenExpirationMs: 3600000, // 1 hour
|
|
31
|
+
defaultMaxExecutions: 100,
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// =============================================================================
|
|
36
|
+
// Arbitraries (Test Data Generators)
|
|
37
|
+
// =============================================================================
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Generate a valid domain name.
|
|
41
|
+
*/
|
|
42
|
+
const domainArb = fc.stringMatching(/^[a-z][a-z0-9-]{2,20}\.(com|org|net|io)$/);
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Generate raw session data (simulating cookies, localStorage, sessionStorage).
|
|
46
|
+
*/
|
|
47
|
+
const rawSessionDataArb: fc.Arbitrary<RawSessionData> = fc.record({
|
|
48
|
+
cookies: fc.string({ minLength: 10, maxLength: 500 }),
|
|
49
|
+
localStorage: fc.string({ minLength: 10, maxLength: 500 }),
|
|
50
|
+
sessionStorage: fc.string({ minLength: 10, maxLength: 500 }),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Generate action names.
|
|
55
|
+
*/
|
|
56
|
+
const actionNameArb = fc.stringMatching(/^[a-z][a-z_]{2,20}$/);
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Fixed base timestamp for deterministic test behavior.
|
|
60
|
+
*/
|
|
61
|
+
const BASE_TIME = 1700000000000;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Generate an action scope.
|
|
65
|
+
* Uses fixed timestamps relative to BASE_TIME for deterministic tests.
|
|
66
|
+
*/
|
|
67
|
+
const actionScopeArb: fc.Arbitrary<Partial<ActionScope>> = fc.record({
|
|
68
|
+
allowedActions: fc.array(actionNameArb, { minLength: 1, maxLength: 10 }),
|
|
69
|
+
blockedActions: fc.array(actionNameArb, { minLength: 0, maxLength: 5 }),
|
|
70
|
+
maxExecutions: fc.integer({ min: 1, max: 1000 }),
|
|
71
|
+
expiresAt: fc.integer({ min: BASE_TIME + 60000, max: BASE_TIME + 86400000 }),
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Generate credential-like strings that should NEVER appear in tokens.
|
|
76
|
+
*/
|
|
77
|
+
const credentialPatternArb = fc.oneof(
|
|
78
|
+
fc.constant('password: "secret123"'),
|
|
79
|
+
fc.constant('api_key: "sk_live_abc123xyz"'),
|
|
80
|
+
fc.constant('secret: "my-secret-value"'),
|
|
81
|
+
fc.constant('Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9'),
|
|
82
|
+
fc.constant('Basic dXNlcm5hbWU6cGFzc3dvcmQ='),
|
|
83
|
+
fc.constant('cookie: "session_id=abc123"'),
|
|
84
|
+
fc.constant('session: "authenticated_session_token"')
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
// =============================================================================
|
|
88
|
+
// Property Tests
|
|
89
|
+
// =============================================================================
|
|
90
|
+
|
|
91
|
+
describe('Session Vault Property Tests', () => {
|
|
92
|
+
/**
|
|
93
|
+
* **Feature: omnibridge, Property 12: Zero-Knowledge Credentials**
|
|
94
|
+
*
|
|
95
|
+
* *For any* Capability_Token issued to an agent, the token SHALL NOT contain
|
|
96
|
+
* raw credentials (passwords, API keys, session secrets)—only capability references.
|
|
97
|
+
*
|
|
98
|
+
* **Validates: Requirements 6.1**
|
|
99
|
+
*/
|
|
100
|
+
test.each([{ numRuns: 100 }])(
|
|
101
|
+
'Property 12: Zero-Knowledge Credentials - tokens never contain raw credentials',
|
|
102
|
+
async () => {
|
|
103
|
+
await fc.assert(
|
|
104
|
+
fc.asyncProperty(
|
|
105
|
+
domainArb,
|
|
106
|
+
rawSessionDataArb,
|
|
107
|
+
actionScopeArb,
|
|
108
|
+
async (domain, sessionData, scope) => {
|
|
109
|
+
// Store session with potentially sensitive data
|
|
110
|
+
await vault.store(domain, sessionData);
|
|
111
|
+
|
|
112
|
+
// Issue a capability token
|
|
113
|
+
const token = await vault.issueToken(domain, scope);
|
|
114
|
+
|
|
115
|
+
// PROPERTY: Token must NOT contain any raw credentials
|
|
116
|
+
const containsCredentials = vault.tokenContainsCredentials(token);
|
|
117
|
+
expect(containsCredentials).toBe(false);
|
|
118
|
+
|
|
119
|
+
// Additional checks: token should only contain capability info
|
|
120
|
+
expect(token.id).toMatch(/^cap_[a-f0-9]{32}$/);
|
|
121
|
+
expect(token.domain).toBe(domain);
|
|
122
|
+
expect(token.scope).toBeDefined();
|
|
123
|
+
expect(token.issuedAt).toBeGreaterThan(0);
|
|
124
|
+
expect(token.executionCount).toBe(0);
|
|
125
|
+
|
|
126
|
+
// Token should NOT contain the raw session data
|
|
127
|
+
const tokenStr = JSON.stringify(token);
|
|
128
|
+
expect(tokenStr).not.toContain(sessionData.cookies);
|
|
129
|
+
expect(tokenStr).not.toContain(sessionData.localStorage);
|
|
130
|
+
expect(tokenStr).not.toContain(sessionData.sessionStorage);
|
|
131
|
+
|
|
132
|
+
// Clean up for next iteration
|
|
133
|
+
vault.clear();
|
|
134
|
+
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
),
|
|
138
|
+
{ numRuns: 100 }
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* **Feature: omnibridge, Property 13: Capability Token Scoping**
|
|
145
|
+
*
|
|
146
|
+
* *For any* Capability_Token, attempting an action in `blockedActions` SHALL fail
|
|
147
|
+
* with a scope_violation error, while actions in `allowedActions` SHALL succeed.
|
|
148
|
+
*
|
|
149
|
+
* **Validates: Requirements 6.2, 6.3**
|
|
150
|
+
*/
|
|
151
|
+
test.each([{ numRuns: 100 }])(
|
|
152
|
+
'Property 13: Capability Token Scoping - blocked actions fail, allowed actions succeed',
|
|
153
|
+
async () => {
|
|
154
|
+
await fc.assert(
|
|
155
|
+
fc.asyncProperty(
|
|
156
|
+
domainArb,
|
|
157
|
+
rawSessionDataArb,
|
|
158
|
+
fc.array(actionNameArb, { minLength: 2, maxLength: 10 }),
|
|
159
|
+
fc.array(actionNameArb, { minLength: 1, maxLength: 5 }),
|
|
160
|
+
async (domain, sessionData, allowedActions, blockedActions) => {
|
|
161
|
+
// Ensure no overlap between allowed and blocked
|
|
162
|
+
const uniqueAllowed = allowedActions.filter((a) => !blockedActions.includes(a));
|
|
163
|
+
if (uniqueAllowed.length === 0) {
|
|
164
|
+
return true; // Skip if no unique allowed actions
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
await vault.store(domain, sessionData);
|
|
168
|
+
|
|
169
|
+
const token = await vault.issueToken(domain, {
|
|
170
|
+
allowedActions: uniqueAllowed,
|
|
171
|
+
blockedActions,
|
|
172
|
+
maxExecutions: 1000,
|
|
173
|
+
expiresAt: Date.now() + 3600000,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// PROPERTY: Allowed actions should succeed
|
|
177
|
+
for (const action of uniqueAllowed) {
|
|
178
|
+
const result = vault.checkScope(token.id, action);
|
|
179
|
+
expect(result.allowed).toBe(true);
|
|
180
|
+
expect(result.error).toBeUndefined();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// PROPERTY: Blocked actions should fail with scope_violation
|
|
184
|
+
for (const action of blockedActions) {
|
|
185
|
+
const result = vault.checkScope(token.id, action);
|
|
186
|
+
expect(result.allowed).toBe(false);
|
|
187
|
+
expect(result.error).toBeDefined();
|
|
188
|
+
expect(result.error?.type).toBe('scope_violation');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
vault.clear();
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
),
|
|
195
|
+
{ numRuns: 100 }
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* **Feature: omnibridge, Property 14: Scope Violation Termination**
|
|
202
|
+
*
|
|
203
|
+
* *For any* scope violation attempt, the Session_Vault SHALL immediately revoke
|
|
204
|
+
* the Capability_Token AND terminate the associated Shadow_Session.
|
|
205
|
+
*
|
|
206
|
+
* **Validates: Requirements 6.7**
|
|
207
|
+
*/
|
|
208
|
+
test.each([{ numRuns: 100 }])(
|
|
209
|
+
'Property 14: Scope Violation Termination - violations revoke token immediately',
|
|
210
|
+
async () => {
|
|
211
|
+
await fc.assert(
|
|
212
|
+
fc.asyncProperty(
|
|
213
|
+
domainArb,
|
|
214
|
+
rawSessionDataArb,
|
|
215
|
+
fc.array(actionNameArb, { minLength: 1, maxLength: 5 }),
|
|
216
|
+
actionNameArb,
|
|
217
|
+
async (domain, sessionData, allowedActions, blockedAction) => {
|
|
218
|
+
// Ensure blocked action is not in allowed list
|
|
219
|
+
if (allowedActions.includes(blockedAction)) {
|
|
220
|
+
return true; // Skip this case
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
await vault.store(domain, sessionData);
|
|
224
|
+
|
|
225
|
+
const token = await vault.issueToken(domain, {
|
|
226
|
+
allowedActions,
|
|
227
|
+
blockedActions: [blockedAction],
|
|
228
|
+
maxExecutions: 100,
|
|
229
|
+
expiresAt: Date.now() + 3600000,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// Token should be valid initially
|
|
233
|
+
expect(vault.getToken(token.id)).not.toBeNull();
|
|
234
|
+
expect(vault.isTokenRevoked(token.id)).toBe(false);
|
|
235
|
+
|
|
236
|
+
// Attempt a scope violation
|
|
237
|
+
const violationResult = await vault.handleScopeViolation(token.id, blockedAction);
|
|
238
|
+
|
|
239
|
+
// PROPERTY: Token must be revoked immediately
|
|
240
|
+
expect(violationResult.terminated).toBe(true);
|
|
241
|
+
expect(violationResult.error.type).toBe('scope_violation');
|
|
242
|
+
// Type guard for scope_violation error
|
|
243
|
+
if (violationResult.error.type === 'scope_violation') {
|
|
244
|
+
expect(violationResult.error.attemptedAction).toBe(blockedAction);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// PROPERTY: Token should no longer be retrievable
|
|
248
|
+
expect(vault.getToken(token.id)).toBeNull();
|
|
249
|
+
expect(vault.isTokenRevoked(token.id)).toBe(true);
|
|
250
|
+
|
|
251
|
+
// PROPERTY: Further operations with this token should fail
|
|
252
|
+
const checkResult = vault.checkScope(token.id, allowedActions[0]);
|
|
253
|
+
expect(checkResult.allowed).toBe(false);
|
|
254
|
+
|
|
255
|
+
vault.clear();
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
),
|
|
259
|
+
{ numRuns: 100 }
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// =============================================================================
|
|
266
|
+
// Additional Unit Tests for Edge Cases
|
|
267
|
+
// =============================================================================
|
|
268
|
+
|
|
269
|
+
describe('Session Vault Unit Tests', () => {
|
|
270
|
+
test('encryption round-trip preserves data', async () => {
|
|
271
|
+
const testData = 'sensitive_cookie_data=abc123; session_id=xyz789';
|
|
272
|
+
|
|
273
|
+
const encrypted = vault.encrypt(testData);
|
|
274
|
+
const decrypted = vault.decrypt(encrypted);
|
|
275
|
+
|
|
276
|
+
expect(decrypted).toBe(testData);
|
|
277
|
+
expect(encrypted.algorithm).toBe('AES-256-GCM');
|
|
278
|
+
expect(encrypted.iv).toBeDefined();
|
|
279
|
+
expect(encrypted.data).not.toBe(testData);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test('session storage and retrieval works correctly', async () => {
|
|
283
|
+
const domain = 'example.com';
|
|
284
|
+
const sessionData: RawSessionData = {
|
|
285
|
+
cookies: 'session_id=abc123',
|
|
286
|
+
localStorage: '{"user": "test"}',
|
|
287
|
+
sessionStorage: '{"temp": "data"}',
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
await vault.store(domain, sessionData);
|
|
291
|
+
|
|
292
|
+
const retrieved = await vault.retrieve(domain);
|
|
293
|
+
expect(retrieved).not.toBeNull();
|
|
294
|
+
expect(retrieved?.expiresAt).toBeGreaterThan(Date.now());
|
|
295
|
+
|
|
296
|
+
const decrypted = await vault.retrieveDecrypted(domain);
|
|
297
|
+
expect(decrypted).toEqual(sessionData);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test('expired sessions are not retrieved', async () => {
|
|
301
|
+
const domain = 'expired.com';
|
|
302
|
+
const sessionData: RawSessionData = {
|
|
303
|
+
cookies: 'test',
|
|
304
|
+
localStorage: 'test',
|
|
305
|
+
sessionStorage: 'test',
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
// Store with past expiration
|
|
309
|
+
await vault.store(domain, sessionData, Date.now() - 1000);
|
|
310
|
+
|
|
311
|
+
const retrieved = await vault.retrieve(domain);
|
|
312
|
+
expect(retrieved).toBeNull();
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test('token revocation on bounty completion', async () => {
|
|
316
|
+
const domain = 'bounty.com';
|
|
317
|
+
const sessionData: RawSessionData = {
|
|
318
|
+
cookies: 'test',
|
|
319
|
+
localStorage: 'test',
|
|
320
|
+
sessionStorage: 'test',
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
await vault.store(domain, sessionData);
|
|
324
|
+
|
|
325
|
+
// Issue multiple tokens
|
|
326
|
+
const token1 = await vault.issueToken(domain, { allowedActions: ['read'] });
|
|
327
|
+
const token2 = await vault.issueToken(domain, { allowedActions: ['write'] });
|
|
328
|
+
|
|
329
|
+
// Both tokens should be valid
|
|
330
|
+
expect(vault.getToken(token1.id)).not.toBeNull();
|
|
331
|
+
expect(vault.getToken(token2.id)).not.toBeNull();
|
|
332
|
+
|
|
333
|
+
// Revoke all tokens for domain (bounty completion)
|
|
334
|
+
const revokedCount = vault.revokeAllTokensForDomain(domain);
|
|
335
|
+
|
|
336
|
+
expect(revokedCount).toBe(2);
|
|
337
|
+
expect(vault.getToken(token1.id)).toBeNull();
|
|
338
|
+
expect(vault.getToken(token2.id)).toBeNull();
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test('access logs are recorded correctly', async () => {
|
|
342
|
+
const domain = 'logged.com';
|
|
343
|
+
const sessionData: RawSessionData = {
|
|
344
|
+
cookies: 'test',
|
|
345
|
+
localStorage: 'test',
|
|
346
|
+
sessionStorage: 'test',
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
await vault.store(domain, sessionData);
|
|
350
|
+
const token = await vault.issueToken(domain, { allowedActions: ['read'] });
|
|
351
|
+
await vault.useToken(token.id, 'read');
|
|
352
|
+
vault.revokeToken(token.id);
|
|
353
|
+
|
|
354
|
+
const logs = vault.getAccessLogs(domain);
|
|
355
|
+
|
|
356
|
+
// Should have: store, issue, use, revoke
|
|
357
|
+
expect(logs.length).toBeGreaterThanOrEqual(4);
|
|
358
|
+
expect(logs.some((l) => l.action === 'issued')).toBe(true);
|
|
359
|
+
expect(logs.some((l) => l.action === 'used')).toBe(true);
|
|
360
|
+
expect(logs.some((l) => l.action === 'revoked')).toBe(true);
|
|
361
|
+
|
|
362
|
+
// Logs should not contain actual credentials
|
|
363
|
+
for (const log of logs) {
|
|
364
|
+
expect(log.details).not.toContain(sessionData.cookies);
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
});
|