agentlock-shared 0.2.0 → 0.3.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/dist/__tests__/billing.test.d.ts +2 -0
- package/dist/__tests__/billing.test.d.ts.map +1 -0
- package/dist/__tests__/billing.test.js +31 -0
- package/dist/__tests__/billing.test.js.map +1 -0
- package/dist/__tests__/dns-pinning.test.d.ts +2 -0
- package/dist/__tests__/dns-pinning.test.d.ts.map +1 -0
- package/dist/__tests__/dns-pinning.test.js +33 -0
- package/dist/__tests__/dns-pinning.test.js.map +1 -0
- package/dist/__tests__/llm-classifier-cache-store.test.d.ts +2 -0
- package/dist/__tests__/llm-classifier-cache-store.test.d.ts.map +1 -0
- package/dist/__tests__/llm-classifier-cache-store.test.js +65 -0
- package/dist/__tests__/llm-classifier-cache-store.test.js.map +1 -0
- package/dist/__tests__/llm-classifier-cache.test.d.ts +2 -0
- package/dist/__tests__/llm-classifier-cache.test.d.ts.map +1 -0
- package/dist/__tests__/llm-classifier-cache.test.js +44 -0
- package/dist/__tests__/llm-classifier-cache.test.js.map +1 -0
- package/dist/__tests__/llm-classifier.test.d.ts +2 -0
- package/dist/__tests__/llm-classifier.test.d.ts.map +1 -0
- package/dist/__tests__/llm-classifier.test.js +167 -0
- package/dist/__tests__/llm-classifier.test.js.map +1 -0
- package/dist/__tests__/plans-classifier-limits.test.d.ts +2 -0
- package/dist/__tests__/plans-classifier-limits.test.d.ts.map +1 -0
- package/dist/__tests__/plans-classifier-limits.test.js +22 -0
- package/dist/__tests__/plans-classifier-limits.test.js.map +1 -0
- package/dist/__tests__/policy-category-floor.test.d.ts +2 -0
- package/dist/__tests__/policy-category-floor.test.d.ts.map +1 -0
- package/dist/__tests__/policy-category-floor.test.js +46 -0
- package/dist/__tests__/policy-category-floor.test.js.map +1 -0
- package/dist/__tests__/policy-claude-bash.test.d.ts +2 -0
- package/dist/__tests__/policy-claude-bash.test.d.ts.map +1 -0
- package/dist/__tests__/policy-claude-bash.test.js +401 -0
- package/dist/__tests__/policy-claude-bash.test.js.map +1 -0
- package/dist/__tests__/policy-llm-floor.test.d.ts +2 -0
- package/dist/__tests__/policy-llm-floor.test.d.ts.map +1 -0
- package/dist/__tests__/policy-llm-floor.test.js +107 -0
- package/dist/__tests__/policy-llm-floor.test.js.map +1 -0
- package/dist/__tests__/policy-ssh-e2e.test.d.ts +2 -0
- package/dist/__tests__/policy-ssh-e2e.test.d.ts.map +1 -0
- package/dist/__tests__/policy-ssh-e2e.test.js +89 -0
- package/dist/__tests__/policy-ssh-e2e.test.js.map +1 -0
- package/dist/__tests__/policy-ssh-sessions.test.d.ts +2 -0
- package/dist/__tests__/policy-ssh-sessions.test.d.ts.map +1 -0
- package/dist/__tests__/policy-ssh-sessions.test.js +139 -0
- package/dist/__tests__/policy-ssh-sessions.test.js.map +1 -0
- package/dist/__tests__/policy-ssh.test.d.ts +2 -0
- package/dist/__tests__/policy-ssh.test.d.ts.map +1 -0
- package/dist/__tests__/policy-ssh.test.js +180 -0
- package/dist/__tests__/policy-ssh.test.js.map +1 -0
- package/dist/__tests__/policy.test.js +400 -2
- package/dist/__tests__/policy.test.js.map +1 -1
- package/dist/__tests__/redact.test.js +76 -0
- package/dist/__tests__/redact.test.js.map +1 -1
- package/dist/__tests__/signing.test.js +89 -0
- package/dist/__tests__/signing.test.js.map +1 -1
- package/dist/__tests__/ssh-fingerprint.test.d.ts +2 -0
- package/dist/__tests__/ssh-fingerprint.test.d.ts.map +1 -0
- package/dist/__tests__/ssh-fingerprint.test.js +19 -0
- package/dist/__tests__/ssh-fingerprint.test.js.map +1 -0
- package/dist/__tests__/vpn-route.test.d.ts +2 -0
- package/dist/__tests__/vpn-route.test.d.ts.map +1 -0
- package/dist/__tests__/vpn-route.test.js +72 -0
- package/dist/__tests__/vpn-route.test.js.map +1 -0
- package/dist/__tests__/wireguard.test.d.ts +2 -0
- package/dist/__tests__/wireguard.test.d.ts.map +1 -0
- package/dist/__tests__/wireguard.test.js +114 -0
- package/dist/__tests__/wireguard.test.js.map +1 -0
- package/dist/billing.d.ts +12 -0
- package/dist/billing.d.ts.map +1 -0
- package/dist/billing.js +41 -0
- package/dist/billing.js.map +1 -0
- package/dist/crypto.d.ts +5 -0
- package/dist/crypto.d.ts.map +1 -1
- package/dist/crypto.js +80 -23
- package/dist/crypto.js.map +1 -1
- package/dist/dns-pinning.d.ts +28 -0
- package/dist/dns-pinning.d.ts.map +1 -0
- package/dist/dns-pinning.js +113 -0
- package/dist/dns-pinning.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -1
- package/dist/llm-classifier-cache-store.d.ts +49 -0
- package/dist/llm-classifier-cache-store.d.ts.map +1 -0
- package/dist/llm-classifier-cache-store.js +63 -0
- package/dist/llm-classifier-cache-store.js.map +1 -0
- package/dist/llm-classifier-cache.d.ts +6 -0
- package/dist/llm-classifier-cache.d.ts.map +1 -0
- package/dist/llm-classifier-cache.js +52 -0
- package/dist/llm-classifier-cache.js.map +1 -0
- package/dist/llm-classifier.d.ts +29 -0
- package/dist/llm-classifier.d.ts.map +1 -0
- package/dist/llm-classifier.js +191 -0
- package/dist/llm-classifier.js.map +1 -0
- package/dist/observability.d.ts +36 -0
- package/dist/observability.d.ts.map +1 -0
- package/dist/observability.js +75 -0
- package/dist/observability.js.map +1 -0
- package/dist/plans.d.ts +17 -0
- package/dist/plans.d.ts.map +1 -1
- package/dist/plans.js +36 -14
- package/dist/plans.js.map +1 -1
- package/dist/policy.d.ts +173 -3
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +910 -42
- package/dist/policy.js.map +1 -1
- package/dist/redact.d.ts.map +1 -1
- package/dist/redact.js +83 -3
- package/dist/redact.js.map +1 -1
- package/dist/regex-safety.d.ts +21 -0
- package/dist/regex-safety.d.ts.map +1 -0
- package/dist/regex-safety.js +49 -0
- package/dist/regex-safety.js.map +1 -0
- package/dist/sanitize.d.ts +31 -0
- package/dist/sanitize.d.ts.map +1 -0
- package/dist/sanitize.js +54 -0
- package/dist/sanitize.js.map +1 -0
- package/dist/schemas.d.ts +202 -10
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +91 -1
- package/dist/schemas.js.map +1 -1
- package/dist/signing.d.ts +15 -0
- package/dist/signing.d.ts.map +1 -1
- package/dist/signing.js +53 -4
- package/dist/signing.js.map +1 -1
- package/dist/ssh-fingerprint.d.ts +10 -0
- package/dist/ssh-fingerprint.d.ts.map +1 -0
- package/dist/ssh-fingerprint.js +52 -0
- package/dist/ssh-fingerprint.js.map +1 -0
- package/dist/ssrf.d.ts +36 -0
- package/dist/ssrf.d.ts.map +1 -0
- package/dist/ssrf.js +140 -0
- package/dist/ssrf.js.map +1 -0
- package/dist/types.d.ts +130 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/wireguard.d.ts +63 -0
- package/dist/wireguard.d.ts.map +1 -0
- package/dist/wireguard.js +226 -0
- package/dist/wireguard.js.map +1 -0
- package/package.json +42 -29
- package/.turbo/turbo-build.log +0 -4
- package/.turbo/turbo-test.log +0 -76
- package/dist/__tests__/content-crypto.test.d.ts +0 -2
- package/dist/__tests__/content-crypto.test.d.ts.map +0 -1
- package/dist/__tests__/content-crypto.test.js +0 -117
- package/dist/__tests__/content-crypto.test.js.map +0 -1
- package/dist/__tests__/signing.test (# Edit conflict 2026-04-01 z3etfmC #).js +0 -51
- package/dist/__tests__/signing.test.js (# Edit conflict 2026-04-01 4rndy9C #).map +0 -1
- package/dist/content-crypto.d.ts +0 -24
- package/dist/content-crypto.d.ts.map +0 -1
- package/dist/content-crypto.js +0 -58
- package/dist/content-crypto.js.map +0 -1
- package/src/__tests__/crypto.test.ts +0 -169
- package/src/__tests__/messaging.test.ts +0 -83
- package/src/__tests__/policy.test.ts +0 -222
- package/src/__tests__/redact.test.ts +0 -41
- package/src/__tests__/signing.test.ts +0 -55
- package/src/crypto.ts +0 -235
- package/src/index.ts +0 -8
- package/src/mcp-catalog.ts +0 -181
- package/src/plans.ts +0 -116
- package/src/policy.ts +0 -216
- package/src/redact.ts +0 -131
- package/src/schemas.ts +0 -121
- package/src/signing.ts +0 -120
- package/src/types.ts +0 -213
- package/test-gateway.mjs +0 -47
- package/tsconfig.json +0 -10
- package/vitest.config.ts +0 -8
package/src/policy.ts
DELETED
|
@@ -1,216 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
PolicyRules,
|
|
3
|
-
AgentActionRequest,
|
|
4
|
-
PolicyEvaluationResult,
|
|
5
|
-
RiskLevel,
|
|
6
|
-
PolicyDecision,
|
|
7
|
-
} from './types.js';
|
|
8
|
-
import { redact } from './redact.js';
|
|
9
|
-
|
|
10
|
-
const RISK_MAP: Record<string, RiskLevel> = {
|
|
11
|
-
read: 'low',
|
|
12
|
-
write: 'medium',
|
|
13
|
-
financial: 'high',
|
|
14
|
-
admin: 'critical',
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
export const DEFAULT_POLICY_RULES: PolicyRules = {
|
|
18
|
-
defaultMode: 'require_approval',
|
|
19
|
-
rules: [
|
|
20
|
-
{ action_type: 'read', decision: 'ALLOW' },
|
|
21
|
-
{ action_type: 'write', decision: 'REQUIRE_APPROVAL' },
|
|
22
|
-
{ action_type: 'financial', decision: 'REQUIRE_APPROVAL' },
|
|
23
|
-
{ action_type: 'admin', decision: 'BLOCK' },
|
|
24
|
-
{ tool: 'mcp.list_tools', decision: 'ALLOW' },
|
|
25
|
-
],
|
|
26
|
-
http: {
|
|
27
|
-
allowedDomains: [],
|
|
28
|
-
allowedMethods: ['GET', 'POST', 'PUT', 'DELETE'],
|
|
29
|
-
blockList: [],
|
|
30
|
-
},
|
|
31
|
-
limits: {
|
|
32
|
-
maxActionsPerHour: 100, // Enforced in gateway via check_rate_limit RPC
|
|
33
|
-
},
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
// Default HTTP rules when rules.http is not configured
|
|
37
|
-
const DEFAULT_HTTP_RULES = {
|
|
38
|
-
allowedDomains: [] as string[],
|
|
39
|
-
allowedMethods: ['GET', 'POST', 'PUT', 'DELETE'],
|
|
40
|
-
blockList: [] as string[],
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
export function evaluatePolicy(
|
|
44
|
-
action: AgentActionRequest,
|
|
45
|
-
rules: PolicyRules
|
|
46
|
-
): PolicyEvaluationResult {
|
|
47
|
-
// Block unknown action types (defense-in-depth)
|
|
48
|
-
if (!(action.action_type in RISK_MAP)) {
|
|
49
|
-
return { decision: 'BLOCK', risk_level: 'critical', reason: `Unknown action type: ${action.action_type}` };
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// SECURITY: Normalize tool name to lowercase to prevent case-sensitivity
|
|
53
|
-
// bypasses (e.g., "Http.get" skipping HTTP domain allowlist checks).
|
|
54
|
-
const normalizedTool = action.tool.toLowerCase();
|
|
55
|
-
|
|
56
|
-
const risk_level = RISK_MAP[action.action_type] ?? 'medium';
|
|
57
|
-
|
|
58
|
-
// Browser tools: browser.open always requires approval
|
|
59
|
-
if (normalizedTool.startsWith('browser.')) {
|
|
60
|
-
if (normalizedTool === 'browser.open') {
|
|
61
|
-
return {
|
|
62
|
-
decision: 'REQUIRE_APPROVAL',
|
|
63
|
-
risk_level: 'medium',
|
|
64
|
-
reason: 'Browser sessions always require approval to start',
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
// Other browser.* actions with a valid session are handled at the gateway
|
|
68
|
-
// level (auto-approved). If they reach the policy engine without a session,
|
|
69
|
-
// they should be blocked.
|
|
70
|
-
return {
|
|
71
|
-
decision: 'BLOCK',
|
|
72
|
-
risk_level: 'medium',
|
|
73
|
-
reason: 'Browser actions require an active session (use browser.open first)',
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// MCP tools: list_tools handled via default rules (can be overridden by custom policies)
|
|
78
|
-
|
|
79
|
-
// HTTP tool checks: always enforce domain/method restrictions
|
|
80
|
-
const isHttpTool = normalizedTool.split('.')[0] === 'http';
|
|
81
|
-
if (isHttpTool) {
|
|
82
|
-
const httpRules = rules.http ?? DEFAULT_HTTP_RULES;
|
|
83
|
-
const url = action.payload.url as string | undefined;
|
|
84
|
-
if (!url) {
|
|
85
|
-
// SECURITY: HTTP actions without a URL must be blocked at the policy level.
|
|
86
|
-
// The connector would also reject it, but policy should be the first gate.
|
|
87
|
-
return { decision: 'BLOCK', risk_level: 'critical', reason: 'HTTP actions require a URL in payload' };
|
|
88
|
-
}
|
|
89
|
-
if (url) {
|
|
90
|
-
try {
|
|
91
|
-
const domain = new URL(url).hostname.replace(/\.$/, '').toLowerCase();
|
|
92
|
-
// Use exact match or proper subdomain match (preceded by a dot)
|
|
93
|
-
// to prevent "not-trusted.com" from matching allowlist entry "trusted.com"
|
|
94
|
-
// Both sides are normalized: lowercase + trailing-dot stripped.
|
|
95
|
-
const matchesDomain = (d: string, pattern: string) => {
|
|
96
|
-
const normalizedPattern = pattern.replace(/\.$/, '').toLowerCase();
|
|
97
|
-
return d === normalizedPattern || d.endsWith('.' + normalizedPattern);
|
|
98
|
-
};
|
|
99
|
-
if (httpRules.blockList.some((b) => matchesDomain(domain, b))) {
|
|
100
|
-
return { decision: 'BLOCK', risk_level: 'critical', reason: `Domain ${domain} is in block list` };
|
|
101
|
-
}
|
|
102
|
-
if (httpRules.allowedDomains.length === 0) {
|
|
103
|
-
// No allowlist configured: safe default is REQUIRE_APPROVAL, not ALLOW.
|
|
104
|
-
// This prevents agents from exfiltrating data to arbitrary domains.
|
|
105
|
-
return {
|
|
106
|
-
decision: 'REQUIRE_APPROVAL',
|
|
107
|
-
risk_level,
|
|
108
|
-
reason: 'HTTP allowlist not configured — approval required for all HTTP calls',
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
if (!httpRules.allowedDomains.some((d) => matchesDomain(domain, d))) {
|
|
112
|
-
return { decision: 'BLOCK', risk_level, reason: `Domain ${domain} not in allowed list` };
|
|
113
|
-
}
|
|
114
|
-
} catch {
|
|
115
|
-
return { decision: 'BLOCK', risk_level: 'critical', reason: 'Invalid URL' };
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
const method = (action.payload.method as string | undefined)?.toUpperCase();
|
|
119
|
-
if (method && !httpRules.allowedMethods.includes(method)) {
|
|
120
|
-
return { decision: 'BLOCK', risk_level, reason: `HTTP method ${method} not allowed` };
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
if (
|
|
125
|
-
rules.limits?.maxCostPerAction !== undefined &&
|
|
126
|
-
action.cost_estimate !== undefined &&
|
|
127
|
-
action.cost_estimate > rules.limits.maxCostPerAction
|
|
128
|
-
) {
|
|
129
|
-
return {
|
|
130
|
-
decision: 'BLOCK',
|
|
131
|
-
risk_level: 'high',
|
|
132
|
-
reason: `Cost estimate ${action.cost_estimate} exceeds limit ${rules.limits.maxCostPerAction}`,
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Most specific: tool-specific rule (case-insensitive to prevent bypasses)
|
|
137
|
-
let matched = rules.rules.find((r) => r.tool !== undefined && r.tool.toLowerCase() === normalizedTool);
|
|
138
|
-
// Then action-type rule
|
|
139
|
-
if (!matched) matched = rules.rules.find((r) => r.action_type === action.action_type);
|
|
140
|
-
|
|
141
|
-
if (matched) {
|
|
142
|
-
let finalDecision = matched.decision;
|
|
143
|
-
// SECURITY: Never auto-allow admin/financial actions even via explicit rules
|
|
144
|
-
if (finalDecision === 'ALLOW' && ['admin', 'financial'].includes(action.action_type)) {
|
|
145
|
-
finalDecision = 'REQUIRE_APPROVAL';
|
|
146
|
-
}
|
|
147
|
-
return {
|
|
148
|
-
decision: finalDecision,
|
|
149
|
-
risk_level,
|
|
150
|
-
reason: `Matched rule: ${matched.action_type ?? matched.tool}`,
|
|
151
|
-
matched_rule: matched,
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const defaultDecision: PolicyDecision =
|
|
156
|
-
rules.defaultMode === 'allow' ? 'ALLOW' : rules.defaultMode === 'block' ? 'BLOCK' : 'REQUIRE_APPROVAL';
|
|
157
|
-
|
|
158
|
-
// SECURITY: Never auto-allow high-risk actions via permissive default mode
|
|
159
|
-
if (defaultDecision === 'ALLOW' && ['admin', 'financial'].includes(action.action_type)) {
|
|
160
|
-
return { decision: 'REQUIRE_APPROVAL', risk_level, reason: 'High-risk action types always require approval even with permissive default policy' };
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
return { decision: defaultDecision, risk_level, reason: 'Default policy' };
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
export function buildActionPreview(action: AgentActionRequest): {
|
|
167
|
-
summary: string;
|
|
168
|
-
target?: string;
|
|
169
|
-
impact?: string;
|
|
170
|
-
cost_estimate?: number;
|
|
171
|
-
} {
|
|
172
|
-
const normalizedTool = action.tool.toLowerCase();
|
|
173
|
-
let summary = `${action.action_type.toUpperCase()} via ${action.tool}`;
|
|
174
|
-
let target: string | undefined;
|
|
175
|
-
|
|
176
|
-
if (normalizedTool.split('.')[0] === 'http') {
|
|
177
|
-
const url = action.payload.url as string | undefined;
|
|
178
|
-
const method = action.payload.method as string | undefined;
|
|
179
|
-
if (url) {
|
|
180
|
-
try {
|
|
181
|
-
target = new URL(url).hostname;
|
|
182
|
-
} catch {
|
|
183
|
-
target = url;
|
|
184
|
-
}
|
|
185
|
-
summary = `${method?.toUpperCase() ?? 'HTTP'} request to ${target}`;
|
|
186
|
-
}
|
|
187
|
-
} else if (normalizedTool === 'browser.open') {
|
|
188
|
-
const url = action.payload.url as string | undefined;
|
|
189
|
-
if (url) {
|
|
190
|
-
try {
|
|
191
|
-
target = new URL(url).hostname;
|
|
192
|
-
} catch {
|
|
193
|
-
target = url;
|
|
194
|
-
}
|
|
195
|
-
summary = `Open browser session to ${target}`;
|
|
196
|
-
} else {
|
|
197
|
-
summary = 'Open browser session';
|
|
198
|
-
}
|
|
199
|
-
} else if (normalizedTool === 'mcp.list_tools') {
|
|
200
|
-
const server = action.payload.server as string | undefined;
|
|
201
|
-
target = server;
|
|
202
|
-
summary = `List available tools on MCP server "${server ?? 'unknown'}"`;
|
|
203
|
-
} else if (normalizedTool === 'mcp.call_tool') {
|
|
204
|
-
const server = action.payload.server as string | undefined;
|
|
205
|
-
const method = action.payload.method as string | undefined;
|
|
206
|
-
target = server;
|
|
207
|
-
summary = `Call "${method ?? 'unknown'}" on MCP server "${server ?? 'unknown'}"`;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
return {
|
|
211
|
-
summary,
|
|
212
|
-
target,
|
|
213
|
-
impact: action.action_type === 'write' ? 'Data will be modified' : undefined,
|
|
214
|
-
cost_estimate: action.cost_estimate,
|
|
215
|
-
};
|
|
216
|
-
}
|
package/src/redact.ts
DELETED
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
// Exact-match field names (checked after lowercasing)
|
|
2
|
-
const SECRET_FIELDS = new Set([
|
|
3
|
-
'authorization',
|
|
4
|
-
'api_key',
|
|
5
|
-
'apikey',
|
|
6
|
-
'api-key',
|
|
7
|
-
'token',
|
|
8
|
-
'secret',
|
|
9
|
-
'password',
|
|
10
|
-
'passwd',
|
|
11
|
-
'private_key',
|
|
12
|
-
'privatekey',
|
|
13
|
-
'access_token',
|
|
14
|
-
'refresh_token',
|
|
15
|
-
'client_secret',
|
|
16
|
-
'x-api-key',
|
|
17
|
-
'x-auth-token',
|
|
18
|
-
'credentials',
|
|
19
|
-
'bearer',
|
|
20
|
-
'session_token',
|
|
21
|
-
'session_key',
|
|
22
|
-
'cookie',
|
|
23
|
-
'set-cookie',
|
|
24
|
-
'aws_secret_access_key',
|
|
25
|
-
'aws_session_token',
|
|
26
|
-
'database_url',
|
|
27
|
-
'connection_string',
|
|
28
|
-
'private-key',
|
|
29
|
-
'master_key',
|
|
30
|
-
'encryption_key',
|
|
31
|
-
'signing_key',
|
|
32
|
-
'service_role_key',
|
|
33
|
-
'supabase_service_role_key',
|
|
34
|
-
]);
|
|
35
|
-
|
|
36
|
-
// Substring patterns: if the lowercased key contains any of these, redact
|
|
37
|
-
const SECRET_SUBSTRINGS = [
|
|
38
|
-
'secret',
|
|
39
|
-
'password',
|
|
40
|
-
'passwd',
|
|
41
|
-
'token',
|
|
42
|
-
'api_key',
|
|
43
|
-
'apikey',
|
|
44
|
-
'private_key',
|
|
45
|
-
'privatekey',
|
|
46
|
-
'credential',
|
|
47
|
-
'authorization',
|
|
48
|
-
'auth_key',
|
|
49
|
-
'master_key',
|
|
50
|
-
'encryption_key',
|
|
51
|
-
'signing_key',
|
|
52
|
-
'connection_string',
|
|
53
|
-
'database_url',
|
|
54
|
-
'access_key',
|
|
55
|
-
'auth_token',
|
|
56
|
-
'refresh_key',
|
|
57
|
-
'session_secret',
|
|
58
|
-
'webhook_secret',
|
|
59
|
-
'jwt',
|
|
60
|
-
'oauth',
|
|
61
|
-
'ssn',
|
|
62
|
-
'credit_card',
|
|
63
|
-
'routing_number',
|
|
64
|
-
];
|
|
65
|
-
|
|
66
|
-
const REDACTED = '[REDACTED]';
|
|
67
|
-
|
|
68
|
-
// Value-based patterns to detect secrets regardless of field name
|
|
69
|
-
const SECRET_VALUE_PATTERNS = [
|
|
70
|
-
/^(sk|pk|rk)_(live|test)_[a-zA-Z0-9]{10,}$/, // Stripe keys
|
|
71
|
-
/^r[us]_[a-zA-Z0-9]{20,}$/, // Stripe restricted keys
|
|
72
|
-
/^ghp_[a-zA-Z0-9]{36}$/, // GitHub PATs
|
|
73
|
-
/^github_pat_[a-zA-Z0-9_]{20,}$/, // GitHub fine-grained PATs
|
|
74
|
-
/^gho_[a-zA-Z0-9]{36}$/, // GitHub OAuth tokens
|
|
75
|
-
/^AKIA[A-Z0-9]{16}$/, // AWS access key IDs
|
|
76
|
-
/^eyJ[a-zA-Z0-9_-]{20,}\.[a-zA-Z0-9_-]{20,}/, // JWTs (eyJ prefix)
|
|
77
|
-
/^xox[bpras]-[a-zA-Z0-9-]{10,}$/, // Slack tokens
|
|
78
|
-
/^Bearer\s+[a-zA-Z0-9._\-]{20,}$/, // Bearer tokens
|
|
79
|
-
/^AIza[a-zA-Z0-9_-]{35}$/, // Google API keys
|
|
80
|
-
/^sk-[a-zA-Z0-9]{20,}$/, // OpenAI API keys
|
|
81
|
-
/^sk-ant-[a-zA-Z0-9_-]{20,}$/, // Anthropic API keys
|
|
82
|
-
/^SG\.[a-zA-Z0-9_-]{20,}$/, // SendGrid API keys
|
|
83
|
-
/^SK[a-f0-9]{32}$/, // Twilio API keys
|
|
84
|
-
/-----BEGIN\s+(RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/, // PEM private keys
|
|
85
|
-
/^DefaultEndpointsProtocol=/, // Azure connection strings
|
|
86
|
-
/^[A-Za-z0-9+\/]{43,}={0,2}$/, // Base64-encoded symmetric keys (32+ bytes)
|
|
87
|
-
/^[A-Za-z0-9_-]{43,}={0,2}$/, // URL-safe base64 encoded keys
|
|
88
|
-
/^whsec_[a-zA-Z0-9]{20,}$/, // Stripe webhook secrets
|
|
89
|
-
/^npm_[a-zA-Z0-9]{20,}$/, // npm tokens
|
|
90
|
-
/^vercel_[a-zA-Z0-9]{20,}$/, // Vercel tokens
|
|
91
|
-
];
|
|
92
|
-
|
|
93
|
-
function isSecretField(key: string): boolean {
|
|
94
|
-
const lower = key.toLowerCase();
|
|
95
|
-
if (SECRET_FIELDS.has(lower)) return true;
|
|
96
|
-
return SECRET_SUBSTRINGS.some((sub) => lower.includes(sub));
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function isSecretValue(value: string): boolean {
|
|
100
|
-
return SECRET_VALUE_PATTERNS.some((pattern) => pattern.test(value));
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export function redact(obj: unknown, depth = 0): unknown {
|
|
104
|
-
// SECURITY: At max depth, redact entirely rather than passing data through.
|
|
105
|
-
// Prevents secrets in deeply nested objects from bypassing redaction.
|
|
106
|
-
if (depth > 20) return REDACTED;
|
|
107
|
-
if (obj === null || obj === undefined) return obj;
|
|
108
|
-
if (typeof obj === 'string') return isSecretValue(obj) ? REDACTED : obj;
|
|
109
|
-
if (typeof obj !== 'object') return obj;
|
|
110
|
-
if (Array.isArray(obj)) return obj.map((item) => redact(item, depth + 1));
|
|
111
|
-
|
|
112
|
-
const result: Record<string, unknown> = {};
|
|
113
|
-
for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {
|
|
114
|
-
result[key] = isSecretField(key) ? REDACTED : redact(value, depth + 1);
|
|
115
|
-
}
|
|
116
|
-
return result;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export function redactHeaders(headers: Record<string, string>): Record<string, string> {
|
|
120
|
-
const result: Record<string, string> = {};
|
|
121
|
-
for (const [key, value] of Object.entries(headers)) {
|
|
122
|
-
result[key] = (isSecretField(key) || isSecretValue(value)) ? REDACTED : value;
|
|
123
|
-
}
|
|
124
|
-
return result;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export function sanitizeActionRequest(
|
|
128
|
-
request: Record<string, unknown>
|
|
129
|
-
): Record<string, unknown> {
|
|
130
|
-
return redact(request) as Record<string, unknown>;
|
|
131
|
-
}
|
package/src/schemas.ts
DELETED
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
|
|
3
|
-
/** Max payload size: 64KB when serialized */
|
|
4
|
-
const MAX_PAYLOAD_SIZE = 65_536;
|
|
5
|
-
|
|
6
|
-
/** Maximum length for webhook URLs (standard URL length limit) */
|
|
7
|
-
const MAX_WEBHOOK_URL_LENGTH = 2048;
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Reusable Zod schema for webhook URLs.
|
|
11
|
-
* Enforces: max length 2048, valid URL syntax, HTTPS-only,
|
|
12
|
-
* and rejects private/internal hostnames at parse time.
|
|
13
|
-
*/
|
|
14
|
-
export const WebhookUrlSchema = z
|
|
15
|
-
.string()
|
|
16
|
-
.max(MAX_WEBHOOK_URL_LENGTH, `Webhook URL exceeds maximum length (${MAX_WEBHOOK_URL_LENGTH} characters)`)
|
|
17
|
-
.refine(
|
|
18
|
-
(val) => {
|
|
19
|
-
try {
|
|
20
|
-
const parsed = new URL(val);
|
|
21
|
-
return parsed.protocol === 'https:';
|
|
22
|
-
} catch {
|
|
23
|
-
return false;
|
|
24
|
-
}
|
|
25
|
-
},
|
|
26
|
-
{ message: 'Webhook URL must be a valid HTTPS URL' }
|
|
27
|
-
)
|
|
28
|
-
.refine(
|
|
29
|
-
(val) => {
|
|
30
|
-
try {
|
|
31
|
-
const parsed = new URL(val);
|
|
32
|
-
const hostname = parsed.hostname;
|
|
33
|
-
const privatePatterns = [
|
|
34
|
-
/^127\./, /^10\./, /^172\.(1[6-9]|2\d|3[01])\./,
|
|
35
|
-
/^192\.168\./, /^169\.254\./, /^0\./,
|
|
36
|
-
/^localhost$/i, /\.local$/i, /\.internal$/i,
|
|
37
|
-
];
|
|
38
|
-
return !privatePatterns.some((p) => p.test(hostname));
|
|
39
|
-
} catch {
|
|
40
|
-
return false;
|
|
41
|
-
}
|
|
42
|
-
},
|
|
43
|
-
{ message: 'Webhook URL cannot target private or internal addresses' }
|
|
44
|
-
);
|
|
45
|
-
|
|
46
|
-
export const AgentActionRequestSchema = z.object({
|
|
47
|
-
action_type: z.enum(['read', 'write', 'financial', 'admin']),
|
|
48
|
-
tool: z.string().min(1).max(100).regex(/^[a-zA-Z0-9._\-:]+$/, 'Tool name must be alphanumeric with dots, dashes, underscores, or colons'),
|
|
49
|
-
payload: z.record(z.unknown()).refine(
|
|
50
|
-
(val) => JSON.stringify(val).length <= MAX_PAYLOAD_SIZE,
|
|
51
|
-
{ message: `Payload exceeds maximum size of ${MAX_PAYLOAD_SIZE} bytes` }
|
|
52
|
-
),
|
|
53
|
-
idempotency_key: z.string().max(128).optional(),
|
|
54
|
-
cost_estimate: z.number().nonnegative().optional(),
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
export const RegisterAgentSchema = z.object({
|
|
58
|
-
name: z.string().min(1).max(100),
|
|
59
|
-
environment: z.enum(['development', 'staging', 'production']).default('production'),
|
|
60
|
-
public_key: z.string().min(40),
|
|
61
|
-
allowed_tools: z.array(z.string()).default([]),
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
const DOMAIN_RE = /^(\*\.)?([a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?\.)*[a-zA-Z]{2,}$/;
|
|
65
|
-
|
|
66
|
-
export const PolicyRulesSchema = z.object({
|
|
67
|
-
defaultMode: z.enum(['allow', 'require_approval', 'block']),
|
|
68
|
-
rules: z.array(
|
|
69
|
-
z.object({
|
|
70
|
-
action_type: z.enum(['read', 'write', 'financial', 'admin']).optional(),
|
|
71
|
-
tool: z.string().max(100).regex(/^[a-zA-Z0-9._\-:]+$/, 'Tool name must be alphanumeric with dots, dashes, underscores, or colons').optional(),
|
|
72
|
-
domain: z.string().regex(DOMAIN_RE, 'Invalid domain format').optional(),
|
|
73
|
-
decision: z.enum(['ALLOW', 'REQUIRE_APPROVAL', 'BLOCK']),
|
|
74
|
-
require_two_approvals: z.boolean().optional(),
|
|
75
|
-
allowed_approvers: z.array(z.string().uuid()).optional(),
|
|
76
|
-
}).refine(r => r.action_type || r.tool, { message: 'Rule must specify action_type or tool' })
|
|
77
|
-
).max(100),
|
|
78
|
-
http: z
|
|
79
|
-
.object({
|
|
80
|
-
allowedDomains: z.array(z.string().min(1).max(253).regex(DOMAIN_RE, 'Invalid domain format')),
|
|
81
|
-
allowedMethods: z.array(z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'])),
|
|
82
|
-
blockList: z.array(z.string().min(1).max(253).regex(DOMAIN_RE, 'Invalid domain format')),
|
|
83
|
-
})
|
|
84
|
-
.optional(),
|
|
85
|
-
limits: z
|
|
86
|
-
.object({
|
|
87
|
-
maxCostPerAction: z.number().nonnegative().optional(),
|
|
88
|
-
maxActionsPerHour: z.number().nonnegative().optional(),
|
|
89
|
-
})
|
|
90
|
-
.optional(),
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
export const ApproveRequestSchema = z.object({
|
|
94
|
-
action: z.enum(['approve', 'deny']),
|
|
95
|
-
reason: z.string().max(1000).optional(),
|
|
96
|
-
reply_message: z.string().max(2000).optional(),
|
|
97
|
-
/** Server-side biometric challenge token (mobile clients only) */
|
|
98
|
-
biometric_challenge: z.string().uuid().optional(),
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
/** Max metadata size: 8KB when serialized (prevents storage exhaustion) */
|
|
102
|
-
const MAX_METADATA_SIZE = 8_192;
|
|
103
|
-
|
|
104
|
-
export const SendMessageSchema = z.object({
|
|
105
|
-
content: z.string().min(1).max(4096),
|
|
106
|
-
thread_id: z.string().uuid().optional(),
|
|
107
|
-
expires_at: z.string().datetime().optional(),
|
|
108
|
-
metadata: z.record(z.unknown()).refine(
|
|
109
|
-
(val) => JSON.stringify(val).length <= MAX_METADATA_SIZE,
|
|
110
|
-
{ message: `Metadata exceeds maximum size of ${MAX_METADATA_SIZE} bytes` }
|
|
111
|
-
).optional(),
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
export const AgentSendMessageSchema = z.object({
|
|
115
|
-
content: z.string().min(1).max(4096),
|
|
116
|
-
thread_id: z.string().uuid(),
|
|
117
|
-
metadata: z.record(z.unknown()).refine(
|
|
118
|
-
(val) => JSON.stringify(val).length <= MAX_METADATA_SIZE,
|
|
119
|
-
{ message: `Metadata exceeds maximum size of ${MAX_METADATA_SIZE} bytes` }
|
|
120
|
-
).optional(),
|
|
121
|
-
});
|
package/src/signing.ts
DELETED
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
import nacl from 'tweetnacl';
|
|
2
|
-
import { encodeBase64, decodeBase64, decodeUTF8 } from 'tweetnacl-util';
|
|
3
|
-
|
|
4
|
-
export interface SignedHeaders {
|
|
5
|
-
'x-agent-id': string;
|
|
6
|
-
'x-timestamp': string;
|
|
7
|
-
'x-signature': string;
|
|
8
|
-
'x-nonce'?: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface KeyPair {
|
|
12
|
-
publicKey: string;
|
|
13
|
-
privateKey: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function generateKeypair(): KeyPair {
|
|
17
|
-
const pair = nacl.sign.keyPair();
|
|
18
|
-
return {
|
|
19
|
-
publicKey: encodeBase64(pair.publicKey),
|
|
20
|
-
privateKey: encodeBase64(pair.secretKey),
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Recursively stable-stringify: sorts object keys at every nesting level.
|
|
26
|
-
* This is critical for signature verification — every language SDK must produce
|
|
27
|
-
* the exact same canonical string for the same payload.
|
|
28
|
-
*
|
|
29
|
-
* Bug fixed: JSON.stringify(obj, replacerArray) only serializes keys named in the
|
|
30
|
-
* replacer at every nesting level, so nested objects would be serialized as {}.
|
|
31
|
-
*/
|
|
32
|
-
function stableStringify(val: unknown): string | undefined {
|
|
33
|
-
if (val === undefined) return undefined;
|
|
34
|
-
if (val === null) return 'null';
|
|
35
|
-
if (typeof val === 'number') {
|
|
36
|
-
// NaN, Infinity, -Infinity serialize to null per JSON spec
|
|
37
|
-
if (!Number.isFinite(val)) return 'null';
|
|
38
|
-
return JSON.stringify(val);
|
|
39
|
-
}
|
|
40
|
-
if (typeof val === 'boolean' || typeof val === 'string') return JSON.stringify(val);
|
|
41
|
-
if (Array.isArray(val)) {
|
|
42
|
-
return `[${val.map((v) => stableStringify(v) ?? 'null').join(',')}]`;
|
|
43
|
-
}
|
|
44
|
-
if (typeof val === 'object') {
|
|
45
|
-
const sorted = Object.keys(val as object).sort();
|
|
46
|
-
const pairs: string[] = [];
|
|
47
|
-
for (const k of sorted) {
|
|
48
|
-
const v = stableStringify((val as Record<string, unknown>)[k]);
|
|
49
|
-
if (v !== undefined) {
|
|
50
|
-
pairs.push(`${JSON.stringify(k)}:${v}`);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
return `{${pairs.join(',')}}`;
|
|
54
|
-
}
|
|
55
|
-
return JSON.stringify(val);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function canonicalStringify(obj: Record<string, unknown>): string {
|
|
59
|
-
return stableStringify(obj) ?? '{}';
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export function signRequest(
|
|
63
|
-
body: Record<string, unknown>,
|
|
64
|
-
agentId: string,
|
|
65
|
-
privateKeyBase64: string
|
|
66
|
-
): SignedHeaders {
|
|
67
|
-
const timestamp = Date.now().toString();
|
|
68
|
-
const nonce = encodeBase64(nacl.randomBytes(16));
|
|
69
|
-
const canonical = canonicalStringify(body);
|
|
70
|
-
const message = decodeUTF8(`${canonical}:${timestamp}:${nonce}`);
|
|
71
|
-
|
|
72
|
-
const privateKey = decodeBase64(privateKeyBase64);
|
|
73
|
-
const signature = nacl.sign.detached(message, privateKey);
|
|
74
|
-
|
|
75
|
-
return {
|
|
76
|
-
'x-agent-id': agentId,
|
|
77
|
-
'x-timestamp': timestamp,
|
|
78
|
-
'x-signature': encodeBase64(signature),
|
|
79
|
-
'x-nonce': nonce,
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export function verifyRequest(
|
|
84
|
-
body: Record<string, unknown>,
|
|
85
|
-
headers: {
|
|
86
|
-
'x-agent-id'?: string;
|
|
87
|
-
'x-timestamp'?: string;
|
|
88
|
-
'x-signature'?: string;
|
|
89
|
-
'x-nonce'?: string;
|
|
90
|
-
},
|
|
91
|
-
publicKeyBase64: string,
|
|
92
|
-
maxSkewMs = 5 * 60 * 1000
|
|
93
|
-
): { agentId: string; nonce: string } {
|
|
94
|
-
const agentId = headers['x-agent-id'];
|
|
95
|
-
const timestamp = headers['x-timestamp'];
|
|
96
|
-
const signatureB64 = headers['x-signature'];
|
|
97
|
-
const nonce = headers['x-nonce'];
|
|
98
|
-
|
|
99
|
-
if (!agentId || !timestamp || !signatureB64 || !nonce) {
|
|
100
|
-
throw new Error('Missing required signature headers');
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const ts = parseInt(timestamp, 10);
|
|
104
|
-
const now = Date.now();
|
|
105
|
-
if (Math.abs(now - ts) > maxSkewMs) {
|
|
106
|
-
throw new Error(`Timestamp skew too large: ${Math.abs(now - ts)}ms`);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const canonical = canonicalStringify(body);
|
|
110
|
-
const message = decodeUTF8(`${canonical}:${timestamp}:${nonce}`);
|
|
111
|
-
const signature = decodeBase64(signatureB64);
|
|
112
|
-
const publicKey = decodeBase64(publicKeyBase64);
|
|
113
|
-
|
|
114
|
-
const valid = nacl.sign.detached.verify(message, signature, publicKey);
|
|
115
|
-
if (!valid) {
|
|
116
|
-
throw new Error('Invalid signature');
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
return { agentId, nonce };
|
|
120
|
-
}
|