agentlock-shared 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.
- package/.turbo/turbo-build.log +4 -0
- package/.turbo/turbo-test.log +34 -0
- package/dist/__tests__/content-crypto.test.d.ts +2 -0
- package/dist/__tests__/content-crypto.test.d.ts.map +1 -0
- package/dist/__tests__/content-crypto.test.js +117 -0
- package/dist/__tests__/content-crypto.test.js.map +1 -0
- package/dist/__tests__/crypto.test.d.ts +2 -0
- package/dist/__tests__/crypto.test.d.ts.map +1 -0
- package/dist/__tests__/crypto.test.js +53 -0
- package/dist/__tests__/crypto.test.js.map +1 -0
- package/dist/__tests__/policy.test.d.ts +2 -0
- package/dist/__tests__/policy.test.d.ts.map +1 -0
- package/dist/__tests__/policy.test.js +80 -0
- package/dist/__tests__/policy.test.js.map +1 -0
- package/dist/__tests__/redact.test.d.ts +2 -0
- package/dist/__tests__/redact.test.d.ts.map +1 -0
- package/dist/__tests__/redact.test.js +39 -0
- package/dist/__tests__/redact.test.js.map +1 -0
- package/dist/__tests__/signing.test.d.ts +2 -0
- package/dist/__tests__/signing.test.d.ts.map +1 -0
- package/dist/__tests__/signing.test.js +51 -0
- package/dist/__tests__/signing.test.js.map +1 -0
- package/dist/content-crypto.d.ts +24 -0
- package/dist/content-crypto.d.ts.map +1 -0
- package/dist/content-crypto.js +58 -0
- package/dist/content-crypto.js.map +1 -0
- package/dist/crypto.d.ts +13 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/crypto.js +85 -0
- package/dist/crypto.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-catalog.d.ts +15 -0
- package/dist/mcp-catalog.d.ts.map +1 -0
- package/dist/mcp-catalog.js +160 -0
- package/dist/mcp-catalog.js.map +1 -0
- package/dist/plans.d.ts +24 -0
- package/dist/plans.d.ts.map +1 -0
- package/dist/plans.js +80 -0
- package/dist/plans.js.map +1 -0
- package/dist/policy.d.ts +10 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +168 -0
- package/dist/policy.js.map +1 -0
- package/dist/redact.d.ts +4 -0
- package/dist/redact.d.ts.map +1 -0
- package/dist/redact.js +115 -0
- package/dist/redact.js.map +1 -0
- package/dist/schemas.d.ts +128 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +47 -0
- package/dist/schemas.js.map +1 -0
- package/dist/signing.d.ts +23 -0
- package/dist/signing.d.ts.map +1 -0
- package/dist/signing.js +96 -0
- package/dist/signing.js.map +1 -0
- package/dist/types.d.ts +184 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +29 -0
- package/src/__tests__/policy.test.ts +88 -0
- package/src/__tests__/redact.test.ts +41 -0
- package/src/__tests__/signing.test.ts +55 -0
- package/src/crypto.ts +87 -0
- package/src/index.ts +8 -0
- package/src/mcp-catalog.ts +181 -0
- package/src/plans.ts +96 -0
- package/src/policy.ts +186 -0
- package/src/redact.ts +114 -0
- package/src/schemas.ts +53 -0
- package/src/signing.ts +120 -0
- package/src/types.ts +212 -0
- package/test-gateway.mjs +47 -0
- package/tsconfig.json +10 -0
- package/vitest.config.ts +8 -0
package/src/policy.ts
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
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
|
+
],
|
|
25
|
+
http: {
|
|
26
|
+
allowedDomains: [],
|
|
27
|
+
allowedMethods: ['GET', 'POST', 'PUT', 'DELETE'],
|
|
28
|
+
blockList: [],
|
|
29
|
+
},
|
|
30
|
+
limits: {
|
|
31
|
+
maxActionsPerHour: 100,
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export function evaluatePolicy(
|
|
36
|
+
action: AgentActionRequest,
|
|
37
|
+
rules: PolicyRules
|
|
38
|
+
): PolicyEvaluationResult {
|
|
39
|
+
const risk_level = RISK_MAP[action.action_type] ?? 'medium';
|
|
40
|
+
|
|
41
|
+
// Browser tools: browser.open always requires approval
|
|
42
|
+
if (action.tool.startsWith('browser.')) {
|
|
43
|
+
if (action.tool === 'browser.open') {
|
|
44
|
+
return {
|
|
45
|
+
decision: 'REQUIRE_APPROVAL',
|
|
46
|
+
risk_level: 'medium',
|
|
47
|
+
reason: 'Browser sessions always require approval to start',
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
// Other browser.* actions with a valid session are handled at the gateway
|
|
51
|
+
// level (auto-approved). If they reach the policy engine without a session,
|
|
52
|
+
// they should be blocked.
|
|
53
|
+
return {
|
|
54
|
+
decision: 'BLOCK',
|
|
55
|
+
risk_level: 'medium',
|
|
56
|
+
reason: 'Browser actions require an active session (use browser.open first)',
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// MCP tools: list_tools is a read (low risk), call_tool defers to action_type rules
|
|
61
|
+
if (action.tool === 'mcp.list_tools') {
|
|
62
|
+
return {
|
|
63
|
+
decision: 'ALLOW',
|
|
64
|
+
risk_level: 'low',
|
|
65
|
+
reason: 'MCP tool discovery is read-only',
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (action.tool.split('.')[0] === 'http' && rules.http) {
|
|
70
|
+
const url = action.payload.url as string | undefined;
|
|
71
|
+
if (url) {
|
|
72
|
+
try {
|
|
73
|
+
const domain = new URL(url).hostname;
|
|
74
|
+
// Use exact match or proper subdomain match (preceded by a dot)
|
|
75
|
+
// to prevent "not-trusted.com" from matching allowlist entry "trusted.com"
|
|
76
|
+
const matchesDomain = (d: string, pattern: string) =>
|
|
77
|
+
d === pattern || d.endsWith('.' + pattern);
|
|
78
|
+
if (rules.http.blockList.some((b) => matchesDomain(domain, b))) {
|
|
79
|
+
return { decision: 'BLOCK', risk_level: 'critical', reason: `Domain ${domain} is in block list` };
|
|
80
|
+
}
|
|
81
|
+
if (rules.http.allowedDomains.length === 0) {
|
|
82
|
+
// No allowlist configured: safe default is REQUIRE_APPROVAL, not ALLOW.
|
|
83
|
+
// This prevents agents from exfiltrating data to arbitrary domains.
|
|
84
|
+
return {
|
|
85
|
+
decision: 'REQUIRE_APPROVAL',
|
|
86
|
+
risk_level,
|
|
87
|
+
reason: 'HTTP allowlist not configured — approval required for all HTTP calls',
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
if (!rules.http.allowedDomains.some((d) => matchesDomain(domain, d))) {
|
|
91
|
+
return { decision: 'BLOCK', risk_level, reason: `Domain ${domain} not in allowed list` };
|
|
92
|
+
}
|
|
93
|
+
} catch {
|
|
94
|
+
return { decision: 'BLOCK', risk_level: 'critical', reason: 'Invalid URL' };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const method = (action.payload.method as string | undefined)?.toUpperCase();
|
|
98
|
+
if (method && !rules.http.allowedMethods.includes(method)) {
|
|
99
|
+
return { decision: 'BLOCK', risk_level, reason: `HTTP method ${method} not allowed` };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (
|
|
104
|
+
rules.limits?.maxCostPerAction !== undefined &&
|
|
105
|
+
action.cost_estimate !== undefined &&
|
|
106
|
+
action.cost_estimate > rules.limits.maxCostPerAction
|
|
107
|
+
) {
|
|
108
|
+
return {
|
|
109
|
+
decision: 'BLOCK',
|
|
110
|
+
risk_level: 'high',
|
|
111
|
+
reason: `Cost estimate ${action.cost_estimate} exceeds limit ${rules.limits.maxCostPerAction}`,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Most specific: tool-specific rule
|
|
116
|
+
let matched = rules.rules.find((r) => r.tool === action.tool);
|
|
117
|
+
// Then action-type rule
|
|
118
|
+
if (!matched) matched = rules.rules.find((r) => r.action_type === action.action_type);
|
|
119
|
+
|
|
120
|
+
if (matched) {
|
|
121
|
+
return {
|
|
122
|
+
decision: matched.decision,
|
|
123
|
+
risk_level,
|
|
124
|
+
reason: `Matched rule: ${matched.action_type ?? matched.tool}`,
|
|
125
|
+
matched_rule: matched,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const defaultDecision: PolicyDecision =
|
|
130
|
+
rules.defaultMode === 'allow' ? 'ALLOW' : rules.defaultMode === 'block' ? 'BLOCK' : 'REQUIRE_APPROVAL';
|
|
131
|
+
|
|
132
|
+
return { decision: defaultDecision, risk_level, reason: 'Default policy' };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function buildActionPreview(action: AgentActionRequest): {
|
|
136
|
+
summary: string;
|
|
137
|
+
target?: string;
|
|
138
|
+
impact?: string;
|
|
139
|
+
cost_estimate?: number;
|
|
140
|
+
} {
|
|
141
|
+
let summary = `${action.action_type.toUpperCase()} via ${action.tool}`;
|
|
142
|
+
let target: string | undefined;
|
|
143
|
+
|
|
144
|
+
if (action.tool.split('.')[0] === 'http') {
|
|
145
|
+
const url = action.payload.url as string | undefined;
|
|
146
|
+
const method = action.payload.method as string | undefined;
|
|
147
|
+
if (url) {
|
|
148
|
+
try {
|
|
149
|
+
target = new URL(url).hostname;
|
|
150
|
+
} catch {
|
|
151
|
+
target = url;
|
|
152
|
+
}
|
|
153
|
+
summary = `${method?.toUpperCase() ?? 'HTTP'} request to ${target}`;
|
|
154
|
+
}
|
|
155
|
+
} else if (action.tool === 'browser.open') {
|
|
156
|
+
const url = action.payload.url as string | undefined;
|
|
157
|
+
if (url) {
|
|
158
|
+
try {
|
|
159
|
+
target = new URL(url).hostname;
|
|
160
|
+
} catch {
|
|
161
|
+
target = url;
|
|
162
|
+
}
|
|
163
|
+
summary = `Open browser session to ${target}`;
|
|
164
|
+
} else {
|
|
165
|
+
summary = 'Open browser session';
|
|
166
|
+
}
|
|
167
|
+
} else if (action.tool === 'mcp.list_tools') {
|
|
168
|
+
const server = action.payload.server as string | undefined;
|
|
169
|
+
target = server;
|
|
170
|
+
summary = `List available tools on MCP server "${server ?? 'unknown'}"`;
|
|
171
|
+
} else if (action.tool === 'mcp.call_tool') {
|
|
172
|
+
const server = action.payload.server as string | undefined;
|
|
173
|
+
const method = action.payload.method as string | undefined;
|
|
174
|
+
target = server;
|
|
175
|
+
summary = `Call "${method ?? 'unknown'}" on MCP server "${server ?? 'unknown'}"`;
|
|
176
|
+
} else if (action.tool === 'demo') {
|
|
177
|
+
summary = `Write to demo table: ${JSON.stringify(redact(action.payload)).slice(0, 80)}`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
summary,
|
|
182
|
+
target,
|
|
183
|
+
impact: action.action_type === 'write' ? 'Data will be modified' : undefined,
|
|
184
|
+
cost_estimate: action.cost_estimate,
|
|
185
|
+
};
|
|
186
|
+
}
|
package/src/redact.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
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
|
+
'session_id',
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
const REDACTED = '[REDACTED]';
|
|
59
|
+
|
|
60
|
+
// Value-based patterns to detect secrets regardless of field name
|
|
61
|
+
const SECRET_VALUE_PATTERNS = [
|
|
62
|
+
/^(sk|pk|rk)_(live|test)_[a-zA-Z0-9]{10,}$/, // Stripe keys
|
|
63
|
+
/^r[us]_[a-zA-Z0-9]{20,}$/, // Stripe restricted keys
|
|
64
|
+
/^ghp_[a-zA-Z0-9]{36}$/, // GitHub PATs
|
|
65
|
+
/^github_pat_[a-zA-Z0-9_]{20,}$/, // GitHub fine-grained PATs
|
|
66
|
+
/^gho_[a-zA-Z0-9]{36}$/, // GitHub OAuth tokens
|
|
67
|
+
/^AKIA[A-Z0-9]{16}$/, // AWS access key IDs
|
|
68
|
+
/^eyJ[a-zA-Z0-9_-]{20,}\.[a-zA-Z0-9_-]{20,}/, // JWTs (eyJ prefix)
|
|
69
|
+
/^xox[bpras]-[a-zA-Z0-9-]{10,}$/, // Slack tokens
|
|
70
|
+
/^Bearer\s+[a-zA-Z0-9._\-]{20,}$/, // Bearer tokens
|
|
71
|
+
/^AIza[a-zA-Z0-9_-]{35}$/, // Google API keys
|
|
72
|
+
/^sk-[a-zA-Z0-9]{20,}$/, // OpenAI API keys
|
|
73
|
+
/^sk-ant-[a-zA-Z0-9_-]{20,}$/, // Anthropic API keys
|
|
74
|
+
/^SG\.[a-zA-Z0-9_-]{20,}$/, // SendGrid API keys
|
|
75
|
+
/^SK[a-f0-9]{32}$/, // Twilio API keys
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
function isSecretField(key: string): boolean {
|
|
79
|
+
const lower = key.toLowerCase();
|
|
80
|
+
if (SECRET_FIELDS.has(lower)) return true;
|
|
81
|
+
return SECRET_SUBSTRINGS.some((sub) => lower.includes(sub));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function isSecretValue(value: string): boolean {
|
|
85
|
+
return SECRET_VALUE_PATTERNS.some((pattern) => pattern.test(value));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function redact(obj: unknown, depth = 0): unknown {
|
|
89
|
+
if (depth > 10) return obj;
|
|
90
|
+
if (obj === null || obj === undefined) return obj;
|
|
91
|
+
if (typeof obj === 'string') return isSecretValue(obj) ? REDACTED : obj;
|
|
92
|
+
if (typeof obj !== 'object') return obj;
|
|
93
|
+
if (Array.isArray(obj)) return obj.map((item) => redact(item, depth + 1));
|
|
94
|
+
|
|
95
|
+
const result: Record<string, unknown> = {};
|
|
96
|
+
for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {
|
|
97
|
+
result[key] = isSecretField(key) ? REDACTED : redact(value, depth + 1);
|
|
98
|
+
}
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function redactHeaders(headers: Record<string, string>): Record<string, string> {
|
|
103
|
+
const result: Record<string, string> = {};
|
|
104
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
105
|
+
result[key] = isSecretField(key) ? REDACTED : value;
|
|
106
|
+
}
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function sanitizeActionRequest(
|
|
111
|
+
request: Record<string, unknown>
|
|
112
|
+
): Record<string, unknown> {
|
|
113
|
+
return redact(request) as Record<string, unknown>;
|
|
114
|
+
}
|
package/src/schemas.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/** Max payload size: 64KB when serialized */
|
|
4
|
+
const MAX_PAYLOAD_SIZE = 65_536;
|
|
5
|
+
|
|
6
|
+
export const AgentActionRequestSchema = z.object({
|
|
7
|
+
action_type: z.enum(['read', 'write', 'financial', 'admin']),
|
|
8
|
+
tool: z.string().min(1).max(100).regex(/^[a-zA-Z0-9._\-:]+$/, 'Tool name must be alphanumeric with dots, dashes, underscores, or colons'),
|
|
9
|
+
payload: z.record(z.unknown()).refine(
|
|
10
|
+
(val) => JSON.stringify(val).length <= MAX_PAYLOAD_SIZE,
|
|
11
|
+
{ message: `Payload exceeds maximum size of ${MAX_PAYLOAD_SIZE} bytes` }
|
|
12
|
+
),
|
|
13
|
+
idempotency_key: z.string().max(128).optional(),
|
|
14
|
+
cost_estimate: z.number().optional(),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export const RegisterAgentSchema = z.object({
|
|
18
|
+
name: z.string().min(1).max(100),
|
|
19
|
+
environment: z.enum(['development', 'staging', 'production']).default('production'),
|
|
20
|
+
public_key: z.string().min(40),
|
|
21
|
+
allowed_tools: z.array(z.string()).default([]),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export const PolicyRulesSchema = z.object({
|
|
25
|
+
defaultMode: z.enum(['allow', 'require_approval', 'block']),
|
|
26
|
+
rules: z.array(
|
|
27
|
+
z.object({
|
|
28
|
+
action_type: z.enum(['read', 'write', 'financial', 'admin']).optional(),
|
|
29
|
+
tool: z.string().optional(),
|
|
30
|
+
domain: z.string().optional(),
|
|
31
|
+
decision: z.enum(['ALLOW', 'REQUIRE_APPROVAL', 'BLOCK']),
|
|
32
|
+
require_two_approvals: z.boolean().optional(),
|
|
33
|
+
})
|
|
34
|
+
),
|
|
35
|
+
http: z
|
|
36
|
+
.object({
|
|
37
|
+
allowedDomains: z.array(z.string()),
|
|
38
|
+
allowedMethods: z.array(z.string()),
|
|
39
|
+
blockList: z.array(z.string()),
|
|
40
|
+
})
|
|
41
|
+
.optional(),
|
|
42
|
+
limits: z
|
|
43
|
+
.object({
|
|
44
|
+
maxCostPerAction: z.number().optional(),
|
|
45
|
+
maxActionsPerHour: z.number().optional(),
|
|
46
|
+
})
|
|
47
|
+
.optional(),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
export const ApproveRequestSchema = z.object({
|
|
51
|
+
action: z.enum(['approve', 'deny']),
|
|
52
|
+
reason: z.string().max(1000).optional(),
|
|
53
|
+
});
|
package/src/signing.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
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
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
export type WorkspaceRole = 'owner' | 'admin' | 'approver' | 'member';
|
|
2
|
+
export type AgentStatus = 'active' | 'revoked' | 'suspended';
|
|
3
|
+
export type AgentEnvironment = 'development' | 'staging' | 'production';
|
|
4
|
+
export type ApprovalStatus = 'PENDING' | 'NEEDS_SECOND_APPROVAL' | 'APPROVED' | 'DENIED' | 'EXPIRED' | 'CANCELLED';
|
|
5
|
+
export type ExecutionStatus = 'PENDING' | 'RUNNING' | 'SUCCEEDED' | 'FAILED' | 'UNDONE';
|
|
6
|
+
export type ActionType = 'read' | 'write' | 'financial' | 'admin';
|
|
7
|
+
export type PolicyDecision = 'ALLOW' | 'REQUIRE_APPROVAL' | 'BLOCK';
|
|
8
|
+
export type RiskLevel = 'low' | 'medium' | 'high' | 'critical';
|
|
9
|
+
|
|
10
|
+
export interface Workspace {
|
|
11
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
slug: string;
|
|
14
|
+
safe_mode: boolean;
|
|
15
|
+
safe_mode_enabled_at?: string;
|
|
16
|
+
safe_mode_enabled_by?: string;
|
|
17
|
+
timeline_enabled: boolean;
|
|
18
|
+
audit_log_enabled: boolean;
|
|
19
|
+
retention_days?: number | null;
|
|
20
|
+
created_at: string;
|
|
21
|
+
updated_at: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface WorkspaceMember {
|
|
25
|
+
id: string;
|
|
26
|
+
workspace_id: string;
|
|
27
|
+
user_id: string;
|
|
28
|
+
role: WorkspaceRole;
|
|
29
|
+
created_at: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface Agent {
|
|
33
|
+
id: string;
|
|
34
|
+
workspace_id: string;
|
|
35
|
+
name: string;
|
|
36
|
+
environment: AgentEnvironment;
|
|
37
|
+
public_key: string;
|
|
38
|
+
allowed_tools: string[];
|
|
39
|
+
status: AgentStatus;
|
|
40
|
+
created_by?: string;
|
|
41
|
+
created_at: string;
|
|
42
|
+
updated_at: string;
|
|
43
|
+
last_seen_at?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ApiCredential {
|
|
47
|
+
id: string;
|
|
48
|
+
workspace_id: string;
|
|
49
|
+
name: string;
|
|
50
|
+
connector_type: string;
|
|
51
|
+
last_four?: string;
|
|
52
|
+
created_by?: string;
|
|
53
|
+
created_at: string;
|
|
54
|
+
updated_at: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface Policy {
|
|
58
|
+
id: string;
|
|
59
|
+
workspace_id: string;
|
|
60
|
+
name: string;
|
|
61
|
+
is_default: boolean;
|
|
62
|
+
rules: PolicyRules;
|
|
63
|
+
created_at: string;
|
|
64
|
+
updated_at: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface PolicyRules {
|
|
68
|
+
defaultMode: 'allow' | 'require_approval' | 'block';
|
|
69
|
+
rules: PolicyRule[];
|
|
70
|
+
http?: {
|
|
71
|
+
allowedDomains: string[];
|
|
72
|
+
allowedMethods: string[];
|
|
73
|
+
blockList: string[];
|
|
74
|
+
};
|
|
75
|
+
limits?: {
|
|
76
|
+
maxCostPerAction?: number;
|
|
77
|
+
maxActionsPerHour?: number;
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface PolicyRule {
|
|
82
|
+
action_type?: ActionType;
|
|
83
|
+
tool?: string;
|
|
84
|
+
domain?: string;
|
|
85
|
+
decision: PolicyDecision;
|
|
86
|
+
require_two_approvals?: boolean;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface PolicyEvaluationResult {
|
|
90
|
+
decision: PolicyDecision;
|
|
91
|
+
risk_level: RiskLevel;
|
|
92
|
+
reason: string;
|
|
93
|
+
matched_rule?: PolicyRule;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface ApprovalRequest {
|
|
97
|
+
id: string;
|
|
98
|
+
workspace_id: string;
|
|
99
|
+
agent_id: string;
|
|
100
|
+
status: ApprovalStatus;
|
|
101
|
+
action_type: ActionType;
|
|
102
|
+
tool: string;
|
|
103
|
+
preview: ActionPreview;
|
|
104
|
+
risk_level: RiskLevel;
|
|
105
|
+
policy_decision: string;
|
|
106
|
+
policy_reason?: string;
|
|
107
|
+
expires_at: string;
|
|
108
|
+
requires_two_approvals: boolean;
|
|
109
|
+
approved_by?: string;
|
|
110
|
+
denied_by?: string;
|
|
111
|
+
decided_at?: string;
|
|
112
|
+
second_approved_by?: string;
|
|
113
|
+
second_decided_at?: string;
|
|
114
|
+
request_hash: string;
|
|
115
|
+
request_body: Record<string, unknown>;
|
|
116
|
+
created_at: string;
|
|
117
|
+
updated_at: string;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface ActionPreview {
|
|
121
|
+
summary: string;
|
|
122
|
+
target?: string;
|
|
123
|
+
impact?: string;
|
|
124
|
+
cost_estimate?: number;
|
|
125
|
+
raw_action?: Record<string, unknown>;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface ActionExecution {
|
|
129
|
+
id: string;
|
|
130
|
+
workspace_id: string;
|
|
131
|
+
approval_request_id?: string;
|
|
132
|
+
agent_id: string;
|
|
133
|
+
connector: string;
|
|
134
|
+
action_type: ActionType;
|
|
135
|
+
status: ExecutionStatus;
|
|
136
|
+
sanitized_request: Record<string, unknown>;
|
|
137
|
+
sanitized_response?: Record<string, unknown>;
|
|
138
|
+
undo_supported: boolean;
|
|
139
|
+
error_message?: string;
|
|
140
|
+
executed_at?: string;
|
|
141
|
+
completed_at?: string;
|
|
142
|
+
undone_at?: string;
|
|
143
|
+
created_at: string;
|
|
144
|
+
updated_at: string;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface AuditEvent {
|
|
148
|
+
id: string;
|
|
149
|
+
workspace_id: string;
|
|
150
|
+
event_type: string;
|
|
151
|
+
actor_id?: string;
|
|
152
|
+
actor_type: 'user' | 'agent' | 'system';
|
|
153
|
+
agent_id?: string;
|
|
154
|
+
resource_type?: string;
|
|
155
|
+
resource_id?: string;
|
|
156
|
+
metadata: Record<string, unknown>;
|
|
157
|
+
created_at: string;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export type BrowserSessionStatus = 'active' | 'closed' | 'expired';
|
|
161
|
+
|
|
162
|
+
export type BrowserTool =
|
|
163
|
+
| 'browser.open'
|
|
164
|
+
| 'browser.click'
|
|
165
|
+
| 'browser.type'
|
|
166
|
+
| 'browser.fill_credentials'
|
|
167
|
+
| 'browser.navigate'
|
|
168
|
+
| 'browser.snapshot'
|
|
169
|
+
| 'browser.screenshot'
|
|
170
|
+
| 'browser.press_key'
|
|
171
|
+
| 'browser.select'
|
|
172
|
+
| 'browser.scroll'
|
|
173
|
+
| 'browser.close';
|
|
174
|
+
|
|
175
|
+
export interface BrowserSession {
|
|
176
|
+
id: string;
|
|
177
|
+
workspace_id: string;
|
|
178
|
+
agent_id: string;
|
|
179
|
+
approval_request_id: string;
|
|
180
|
+
status: BrowserSessionStatus;
|
|
181
|
+
allowed_domains: string[];
|
|
182
|
+
action_count: number;
|
|
183
|
+
created_at: string;
|
|
184
|
+
last_activity_at: string;
|
|
185
|
+
expires_at: string;
|
|
186
|
+
closed_at?: string;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export interface BrowserActionResult {
|
|
190
|
+
session_id: string;
|
|
191
|
+
snapshot: string;
|
|
192
|
+
page_url: string;
|
|
193
|
+
page_title: string;
|
|
194
|
+
action_performed: string;
|
|
195
|
+
screenshot?: string;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export interface AgentActionRequest {
|
|
199
|
+
action_type: ActionType;
|
|
200
|
+
tool: string;
|
|
201
|
+
payload: Record<string, unknown>;
|
|
202
|
+
idempotency_key?: string;
|
|
203
|
+
cost_estimate?: number;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export interface GatewayRequestResult {
|
|
207
|
+
request_id: string;
|
|
208
|
+
decision: PolicyDecision;
|
|
209
|
+
status: ApprovalStatus | 'ALLOWED' | 'BLOCKED';
|
|
210
|
+
message?: string;
|
|
211
|
+
expires_at?: string;
|
|
212
|
+
}
|