agentlock-shared 0.1.0 → 0.2.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 +1 -1
- package/.turbo/turbo-test.log +57 -15
- package/dist/__tests__/crypto.test.js +137 -47
- package/dist/__tests__/crypto.test.js.map +1 -1
- package/dist/__tests__/messaging.test.d.ts +2 -0
- package/dist/__tests__/messaging.test.d.ts.map +1 -0
- package/dist/__tests__/messaging.test.js +75 -0
- package/dist/__tests__/messaging.test.js.map +1 -0
- package/dist/__tests__/policy.test.js +124 -7
- package/dist/__tests__/policy.test.js.map +1 -1
- package/dist/__tests__/signing.test (# Edit conflict 2026-04-01 z3etfmC #).js +51 -0
- package/dist/__tests__/signing.test.js (# Edit conflict 2026-04-01 4rndy9C #).map +1 -0
- package/dist/crypto.d.ts +36 -0
- package/dist/crypto.d.ts.map +1 -1
- package/dist/crypto.js +150 -5
- package/dist/crypto.js.map +1 -1
- package/dist/plans.d.ts +4 -0
- package/dist/plans.d.ts.map +1 -1
- package/dist/plans.js +16 -0
- package/dist/plans.js.map +1 -1
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +54 -29
- package/dist/policy.js.map +1 -1
- package/dist/redact.d.ts.map +1 -1
- package/dist/redact.js +21 -4
- package/dist/redact.js.map +1 -1
- package/dist/schemas.d.ts +72 -11
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +62 -10
- package/dist/schemas.js.map +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/crypto.test.ts +169 -0
- package/src/__tests__/messaging.test.ts +83 -0
- package/src/__tests__/policy.test.ts +141 -7
- package/src/crypto.ts +153 -5
- package/src/plans.ts +20 -0
- package/src/policy.ts +58 -28
- package/src/redact.ts +20 -3
- package/src/schemas.ts +121 -53
- package/src/types.ts +1 -0
package/src/policy.ts
CHANGED
|
@@ -21,6 +21,7 @@ export const DEFAULT_POLICY_RULES: PolicyRules = {
|
|
|
21
21
|
{ action_type: 'write', decision: 'REQUIRE_APPROVAL' },
|
|
22
22
|
{ action_type: 'financial', decision: 'REQUIRE_APPROVAL' },
|
|
23
23
|
{ action_type: 'admin', decision: 'BLOCK' },
|
|
24
|
+
{ tool: 'mcp.list_tools', decision: 'ALLOW' },
|
|
24
25
|
],
|
|
25
26
|
http: {
|
|
26
27
|
allowedDomains: [],
|
|
@@ -28,19 +29,35 @@ export const DEFAULT_POLICY_RULES: PolicyRules = {
|
|
|
28
29
|
blockList: [],
|
|
29
30
|
},
|
|
30
31
|
limits: {
|
|
31
|
-
maxActionsPerHour: 100,
|
|
32
|
+
maxActionsPerHour: 100, // Enforced in gateway via check_rate_limit RPC
|
|
32
33
|
},
|
|
33
34
|
};
|
|
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
|
+
|
|
35
43
|
export function evaluatePolicy(
|
|
36
44
|
action: AgentActionRequest,
|
|
37
45
|
rules: PolicyRules
|
|
38
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
|
+
|
|
39
56
|
const risk_level = RISK_MAP[action.action_type] ?? 'medium';
|
|
40
57
|
|
|
41
58
|
// Browser tools: browser.open always requires approval
|
|
42
|
-
if (
|
|
43
|
-
if (
|
|
59
|
+
if (normalizedTool.startsWith('browser.')) {
|
|
60
|
+
if (normalizedTool === 'browser.open') {
|
|
44
61
|
return {
|
|
45
62
|
decision: 'REQUIRE_APPROVAL',
|
|
46
63
|
risk_level: 'medium',
|
|
@@ -57,28 +74,32 @@ export function evaluatePolicy(
|
|
|
57
74
|
};
|
|
58
75
|
}
|
|
59
76
|
|
|
60
|
-
// MCP tools: list_tools
|
|
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
|
-
}
|
|
77
|
+
// MCP tools: list_tools handled via default rules (can be overridden by custom policies)
|
|
68
78
|
|
|
69
|
-
|
|
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;
|
|
70
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
|
+
}
|
|
71
89
|
if (url) {
|
|
72
90
|
try {
|
|
73
|
-
const domain = new URL(url).hostname;
|
|
91
|
+
const domain = new URL(url).hostname.replace(/\.$/, '').toLowerCase();
|
|
74
92
|
// Use exact match or proper subdomain match (preceded by a dot)
|
|
75
93
|
// to prevent "not-trusted.com" from matching allowlist entry "trusted.com"
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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))) {
|
|
79
100
|
return { decision: 'BLOCK', risk_level: 'critical', reason: `Domain ${domain} is in block list` };
|
|
80
101
|
}
|
|
81
|
-
if (
|
|
102
|
+
if (httpRules.allowedDomains.length === 0) {
|
|
82
103
|
// No allowlist configured: safe default is REQUIRE_APPROVAL, not ALLOW.
|
|
83
104
|
// This prevents agents from exfiltrating data to arbitrary domains.
|
|
84
105
|
return {
|
|
@@ -87,7 +108,7 @@ export function evaluatePolicy(
|
|
|
87
108
|
reason: 'HTTP allowlist not configured — approval required for all HTTP calls',
|
|
88
109
|
};
|
|
89
110
|
}
|
|
90
|
-
if (!
|
|
111
|
+
if (!httpRules.allowedDomains.some((d) => matchesDomain(domain, d))) {
|
|
91
112
|
return { decision: 'BLOCK', risk_level, reason: `Domain ${domain} not in allowed list` };
|
|
92
113
|
}
|
|
93
114
|
} catch {
|
|
@@ -95,7 +116,7 @@ export function evaluatePolicy(
|
|
|
95
116
|
}
|
|
96
117
|
}
|
|
97
118
|
const method = (action.payload.method as string | undefined)?.toUpperCase();
|
|
98
|
-
if (method && !
|
|
119
|
+
if (method && !httpRules.allowedMethods.includes(method)) {
|
|
99
120
|
return { decision: 'BLOCK', risk_level, reason: `HTTP method ${method} not allowed` };
|
|
100
121
|
}
|
|
101
122
|
}
|
|
@@ -112,14 +133,19 @@ export function evaluatePolicy(
|
|
|
112
133
|
};
|
|
113
134
|
}
|
|
114
135
|
|
|
115
|
-
// Most specific: tool-specific rule
|
|
116
|
-
let matched = rules.rules.find((r) => r.tool
|
|
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);
|
|
117
138
|
// Then action-type rule
|
|
118
139
|
if (!matched) matched = rules.rules.find((r) => r.action_type === action.action_type);
|
|
119
140
|
|
|
120
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
|
+
}
|
|
121
147
|
return {
|
|
122
|
-
decision:
|
|
148
|
+
decision: finalDecision,
|
|
123
149
|
risk_level,
|
|
124
150
|
reason: `Matched rule: ${matched.action_type ?? matched.tool}`,
|
|
125
151
|
matched_rule: matched,
|
|
@@ -129,6 +155,11 @@ export function evaluatePolicy(
|
|
|
129
155
|
const defaultDecision: PolicyDecision =
|
|
130
156
|
rules.defaultMode === 'allow' ? 'ALLOW' : rules.defaultMode === 'block' ? 'BLOCK' : 'REQUIRE_APPROVAL';
|
|
131
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
|
+
|
|
132
163
|
return { decision: defaultDecision, risk_level, reason: 'Default policy' };
|
|
133
164
|
}
|
|
134
165
|
|
|
@@ -138,10 +169,11 @@ export function buildActionPreview(action: AgentActionRequest): {
|
|
|
138
169
|
impact?: string;
|
|
139
170
|
cost_estimate?: number;
|
|
140
171
|
} {
|
|
172
|
+
const normalizedTool = action.tool.toLowerCase();
|
|
141
173
|
let summary = `${action.action_type.toUpperCase()} via ${action.tool}`;
|
|
142
174
|
let target: string | undefined;
|
|
143
175
|
|
|
144
|
-
if (
|
|
176
|
+
if (normalizedTool.split('.')[0] === 'http') {
|
|
145
177
|
const url = action.payload.url as string | undefined;
|
|
146
178
|
const method = action.payload.method as string | undefined;
|
|
147
179
|
if (url) {
|
|
@@ -152,7 +184,7 @@ export function buildActionPreview(action: AgentActionRequest): {
|
|
|
152
184
|
}
|
|
153
185
|
summary = `${method?.toUpperCase() ?? 'HTTP'} request to ${target}`;
|
|
154
186
|
}
|
|
155
|
-
} else if (
|
|
187
|
+
} else if (normalizedTool === 'browser.open') {
|
|
156
188
|
const url = action.payload.url as string | undefined;
|
|
157
189
|
if (url) {
|
|
158
190
|
try {
|
|
@@ -164,17 +196,15 @@ export function buildActionPreview(action: AgentActionRequest): {
|
|
|
164
196
|
} else {
|
|
165
197
|
summary = 'Open browser session';
|
|
166
198
|
}
|
|
167
|
-
} else if (
|
|
199
|
+
} else if (normalizedTool === 'mcp.list_tools') {
|
|
168
200
|
const server = action.payload.server as string | undefined;
|
|
169
201
|
target = server;
|
|
170
202
|
summary = `List available tools on MCP server "${server ?? 'unknown'}"`;
|
|
171
|
-
} else if (
|
|
203
|
+
} else if (normalizedTool === 'mcp.call_tool') {
|
|
172
204
|
const server = action.payload.server as string | undefined;
|
|
173
205
|
const method = action.payload.method as string | undefined;
|
|
174
206
|
target = server;
|
|
175
207
|
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
208
|
}
|
|
179
209
|
|
|
180
210
|
return {
|
package/src/redact.ts
CHANGED
|
@@ -52,7 +52,15 @@ const SECRET_SUBSTRINGS = [
|
|
|
52
52
|
'connection_string',
|
|
53
53
|
'database_url',
|
|
54
54
|
'access_key',
|
|
55
|
-
'
|
|
55
|
+
'auth_token',
|
|
56
|
+
'refresh_key',
|
|
57
|
+
'session_secret',
|
|
58
|
+
'webhook_secret',
|
|
59
|
+
'jwt',
|
|
60
|
+
'oauth',
|
|
61
|
+
'ssn',
|
|
62
|
+
'credit_card',
|
|
63
|
+
'routing_number',
|
|
56
64
|
];
|
|
57
65
|
|
|
58
66
|
const REDACTED = '[REDACTED]';
|
|
@@ -73,6 +81,13 @@ const SECRET_VALUE_PATTERNS = [
|
|
|
73
81
|
/^sk-ant-[a-zA-Z0-9_-]{20,}$/, // Anthropic API keys
|
|
74
82
|
/^SG\.[a-zA-Z0-9_-]{20,}$/, // SendGrid API keys
|
|
75
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
|
|
76
91
|
];
|
|
77
92
|
|
|
78
93
|
function isSecretField(key: string): boolean {
|
|
@@ -86,7 +101,9 @@ function isSecretValue(value: string): boolean {
|
|
|
86
101
|
}
|
|
87
102
|
|
|
88
103
|
export function redact(obj: unknown, depth = 0): unknown {
|
|
89
|
-
|
|
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;
|
|
90
107
|
if (obj === null || obj === undefined) return obj;
|
|
91
108
|
if (typeof obj === 'string') return isSecretValue(obj) ? REDACTED : obj;
|
|
92
109
|
if (typeof obj !== 'object') return obj;
|
|
@@ -102,7 +119,7 @@ export function redact(obj: unknown, depth = 0): unknown {
|
|
|
102
119
|
export function redactHeaders(headers: Record<string, string>): Record<string, string> {
|
|
103
120
|
const result: Record<string, string> = {};
|
|
104
121
|
for (const [key, value] of Object.entries(headers)) {
|
|
105
|
-
result[key] = isSecretField(key) ? REDACTED : value;
|
|
122
|
+
result[key] = (isSecretField(key) || isSecretValue(value)) ? REDACTED : value;
|
|
106
123
|
}
|
|
107
124
|
return result;
|
|
108
125
|
}
|
package/src/schemas.ts
CHANGED
|
@@ -1,53 +1,121 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
|
|
3
|
-
/** Max payload size: 64KB when serialized */
|
|
4
|
-
const MAX_PAYLOAD_SIZE = 65_536;
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
+
});
|