crewly 1.11.6 → 1.12.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/config/skills/agent/onboarding/synthesize-hierarchy/SKILL.md +65 -0
- package/config/skills/agent/onboarding/synthesize-hierarchy/execute.sh +61 -0
- package/config/skills/agent/web-search/SKILL.md +70 -0
- package/config/skills/agent/web-search/execute.sh +170 -0
- package/config/skills/agent/web-search/skill.json +23 -0
- package/dist/backend/backend/src/constants.d.ts +12 -0
- package/dist/backend/backend/src/constants.d.ts.map +1 -1
- package/dist/backend/backend/src/constants.js +12 -0
- package/dist/backend/backend/src/constants.js.map +1 -1
- package/dist/backend/backend/src/controllers/cloud/cloud.controller.d.ts +22 -0
- package/dist/backend/backend/src/controllers/cloud/cloud.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/cloud/cloud.controller.js +58 -0
- package/dist/backend/backend/src/controllers/cloud/cloud.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/cloud/cloud.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/cloud/cloud.routes.js +3 -1
- package/dist/backend/backend/src/controllers/cloud/cloud.routes.js.map +1 -1
- package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.controller.d.ts +27 -0
- package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.controller.js +108 -0
- package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.routes.d.ts +6 -2
- package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.routes.js +9 -3
- package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.routes.js.map +1 -1
- package/dist/backend/backend/src/index.d.ts.map +1 -1
- package/dist/backend/backend/src/index.js +36 -2
- package/dist/backend/backend/src/index.js.map +1 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.d.ts +18 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.js +24 -2
- package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.js.map +1 -1
- package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.d.ts +102 -0
- package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.js +164 -0
- package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.js.map +1 -0
- package/dist/backend/backend/src/services/fission/fission-guard.service.d.ts +21 -0
- package/dist/backend/backend/src/services/fission/fission-guard.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/fission/fission-guard.service.js +30 -0
- package/dist/backend/backend/src/services/fission/fission-guard.service.js.map +1 -1
- package/dist/backend/backend/src/services/intent-task/intent-classifier.rules.d.ts +4 -0
- package/dist/backend/backend/src/services/intent-task/intent-classifier.rules.d.ts.map +1 -1
- package/dist/backend/backend/src/services/intent-task/intent-classifier.rules.js +8 -0
- package/dist/backend/backend/src/services/intent-task/intent-classifier.rules.js.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/onboarding/materialize-team.d.ts +79 -58
- package/dist/backend/backend/src/services/orchestrator/onboarding/materialize-team.d.ts.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/onboarding/materialize-team.js +140 -65
- package/dist/backend/backend/src/services/orchestrator/onboarding/materialize-team.js.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/onboarding/synthesize-hierarchy.d.ts +117 -0
- package/dist/backend/backend/src/services/orchestrator/onboarding/synthesize-hierarchy.d.ts.map +1 -0
- package/dist/backend/backend/src/services/orchestrator/onboarding/synthesize-hierarchy.js +189 -0
- package/dist/backend/backend/src/services/orchestrator/onboarding/synthesize-hierarchy.js.map +1 -0
- package/dist/backend/backend/src/services/orchestrator/onboarding-mode-loader.d.ts.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/onboarding-mode-loader.js +1 -0
- package/dist/backend/backend/src/services/orchestrator/onboarding-mode-loader.js.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/onboarding-mode.skill-allowlist.d.ts.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/onboarding-mode.skill-allowlist.js +2 -0
- package/dist/backend/backend/src/services/orchestrator/onboarding-mode.skill-allowlist.js.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/prompts/onboarding-mode.prompt.d.ts.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/prompts/onboarding-mode.prompt.js +17 -1
- package/dist/backend/backend/src/services/orchestrator/prompts/onboarding-mode.prompt.js.map +1 -1
- package/dist/backend/backend/src/services/reconciler/reconcile-rules.d.ts +50 -0
- package/dist/backend/backend/src/services/reconciler/reconcile-rules.d.ts.map +1 -1
- package/dist/backend/backend/src/services/reconciler/reconcile-rules.js +71 -0
- package/dist/backend/backend/src/services/reconciler/reconcile-rules.js.map +1 -1
- package/dist/backend/backend/src/services/reconciler/reconciler.service.d.ts +18 -0
- package/dist/backend/backend/src/services/reconciler/reconciler.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/reconciler/reconciler.service.js +75 -1
- package/dist/backend/backend/src/services/reconciler/reconciler.service.js.map +1 -1
- package/dist/backend/backend/src/services/session/pty/pty-session-backend.d.ts +115 -0
- package/dist/backend/backend/src/services/session/pty/pty-session-backend.d.ts.map +1 -1
- package/dist/backend/backend/src/services/session/pty/pty-session-backend.js +189 -3
- package/dist/backend/backend/src/services/session/pty/pty-session-backend.js.map +1 -1
- package/dist/backend/backend/src/services/session/pty/pty-session.d.ts +28 -0
- package/dist/backend/backend/src/services/session/pty/pty-session.d.ts.map +1 -1
- package/dist/backend/backend/src/services/session/pty/pty-session.js +61 -1
- package/dist/backend/backend/src/services/session/pty/pty-session.js.map +1 -1
- package/dist/backend/backend/src/services/template/template.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/template/template.service.js +67 -2
- package/dist/backend/backend/src/services/template/template.service.js.map +1 -1
- package/dist/backend/backend/src/services/v3/cascade-request-status.d.ts +19 -1
- package/dist/backend/backend/src/services/v3/cascade-request-status.d.ts.map +1 -1
- package/dist/backend/backend/src/services/v3/cascade-request-status.js +39 -2
- package/dist/backend/backend/src/services/v3/cascade-request-status.js.map +1 -1
- package/dist/backend/backend/src/services/v3/escalation-router.service.d.ts +41 -0
- package/dist/backend/backend/src/services/v3/escalation-router.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/v3/escalation-router.service.js +169 -0
- package/dist/backend/backend/src/services/v3/escalation-router.service.js.map +1 -1
- package/dist/backend/backend/src/services/v3/request-cascade.subscriber.d.ts +4 -1
- package/dist/backend/backend/src/services/v3/request-cascade.subscriber.d.ts.map +1 -1
- package/dist/backend/backend/src/services/v3/request-cascade.subscriber.js +21 -0
- package/dist/backend/backend/src/services/v3/request-cascade.subscriber.js.map +1 -1
- package/dist/backend/backend/src/types/intent-task.types.d.ts.map +1 -1
- package/dist/backend/backend/src/types/intent-task.types.js +8 -0
- package/dist/backend/backend/src/types/intent-task.types.js.map +1 -1
- package/dist/backend/backend/src/types/v2/request.types.d.ts +1 -1
- package/dist/backend/backend/src/types/v2/request.types.d.ts.map +1 -1
- package/dist/backend/backend/src/types/v2/request.types.js +1 -0
- package/dist/backend/backend/src/types/v2/request.types.js.map +1 -1
- package/dist/cli/backend/src/constants.d.ts +12 -0
- package/dist/cli/backend/src/constants.d.ts.map +1 -1
- package/dist/cli/backend/src/constants.js +12 -0
- package/dist/cli/backend/src/constants.js.map +1 -1
- package/package.json +9 -3
- package/packages/crewly-agent/README.md +27 -0
- package/packages/crewly-agent/bin/crewly-agent +33 -0
- package/packages/crewly-agent/package.json +39 -0
- package/packages/crewly-agent/src/cli.ts +168 -0
- package/packages/crewly-agent/src/runtime/agent-runner.service.test.ts +2355 -0
- package/packages/crewly-agent/src/runtime/agent-runner.service.ts +1827 -0
- package/packages/crewly-agent/src/runtime/agent-stream.service.test.ts +153 -0
- package/packages/crewly-agent/src/runtime/agent-stream.service.ts +225 -0
- package/packages/crewly-agent/src/runtime/agent-worker.test.ts +171 -0
- package/packages/crewly-agent/src/runtime/agent-worker.ts +193 -0
- package/packages/crewly-agent/src/runtime/api-client.ts +143 -0
- package/packages/crewly-agent/src/runtime/approval-queue.service.ts +307 -0
- package/packages/crewly-agent/src/runtime/audit-log.service.test.ts +208 -0
- package/packages/crewly-agent/src/runtime/audit-log.service.ts +332 -0
- package/packages/crewly-agent/src/runtime/audit-trail.service.test.ts +178 -0
- package/packages/crewly-agent/src/runtime/audit-trail.service.ts +151 -0
- package/packages/crewly-agent/src/runtime/auditor-tools.test.ts +274 -0
- package/packages/crewly-agent/src/runtime/auditor-tools.ts +311 -0
- package/packages/crewly-agent/src/runtime/cloud-config.ts +67 -0
- package/packages/crewly-agent/src/runtime/deepseek-sse-transform.test.ts +165 -0
- package/packages/crewly-agent/src/runtime/deepseek-sse-transform.ts +168 -0
- package/packages/crewly-agent/src/runtime/env-isolation.service.ts +246 -0
- package/packages/crewly-agent/src/runtime/in-process-log-buffer.test.ts +280 -0
- package/packages/crewly-agent/src/runtime/in-process-log-buffer.ts +317 -0
- package/packages/crewly-agent/src/runtime/index.ts +38 -0
- package/packages/crewly-agent/src/runtime/mcp-tool-bridge.test.ts +352 -0
- package/packages/crewly-agent/src/runtime/mcp-tool-bridge.ts +244 -0
- package/packages/crewly-agent/src/runtime/model-manager.test.ts +326 -0
- package/packages/crewly-agent/src/runtime/model-manager.ts +363 -0
- package/packages/crewly-agent/src/runtime/output-filter.service.ts +175 -0
- package/packages/crewly-agent/src/runtime/prompt-guard.service.ts +303 -0
- package/packages/crewly-agent/src/runtime/rate-limiter.test.ts +228 -0
- package/packages/crewly-agent/src/runtime/rate-limiter.ts +353 -0
- package/packages/crewly-agent/src/runtime/tool-registry.test.ts +2510 -0
- package/packages/crewly-agent/src/runtime/tool-registry.ts +2104 -0
- package/packages/crewly-agent/src/runtime/types.test.ts +519 -0
- package/packages/crewly-agent/src/runtime/types.ts +637 -0
- package/packages/crewly-agent/src/runtime/web-search.tool.test.ts +131 -0
- package/packages/crewly-agent/src/runtime/web-search.tool.ts +140 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt Guard Service — Prompt Injection Protection
|
|
3
|
+
*
|
|
4
|
+
* Detects and blocks prompt injection attempts that try to extract API keys,
|
|
5
|
+
* secrets, or sensitive environment variables through agent commands.
|
|
6
|
+
*
|
|
7
|
+
* Integrates with the tool registry to block dangerous bash commands and
|
|
8
|
+
* logs blocked attempts to the audit trail.
|
|
9
|
+
*
|
|
10
|
+
* @module services/agent/crewly-agent/prompt-guard.service
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* const guard = new PromptGuardService();
|
|
15
|
+
* const result = guard.checkCommand('echo $ANTHROPIC_API_KEY');
|
|
16
|
+
* // { blocked: true, reason: 'Key extraction attempt: echo env var', pattern: '...' }
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { AuditEntry, ToolSensitivity } from './types.js';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A pattern that detects key extraction attempts.
|
|
24
|
+
*/
|
|
25
|
+
export interface GuardPattern {
|
|
26
|
+
/** Regex to match the dangerous command or prompt */
|
|
27
|
+
pattern: RegExp;
|
|
28
|
+
/** Human-readable reason for blocking */
|
|
29
|
+
reason: string;
|
|
30
|
+
/** Category for audit logging */
|
|
31
|
+
category: 'env_extraction' | 'key_dump' | 'prompt_injection' | 'file_exfiltration';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Result of a prompt guard check.
|
|
36
|
+
*/
|
|
37
|
+
export interface GuardCheckResult {
|
|
38
|
+
/** Whether the command/prompt was blocked */
|
|
39
|
+
blocked: boolean;
|
|
40
|
+
/** Reason for blocking (empty if not blocked) */
|
|
41
|
+
reason: string;
|
|
42
|
+
/** Category of the threat (undefined if not blocked) */
|
|
43
|
+
category?: GuardPattern['category'];
|
|
44
|
+
/** The matched pattern source (for audit logging) */
|
|
45
|
+
matchedPattern?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Patterns that detect attempts to extract API keys or secrets via bash commands.
|
|
50
|
+
*/
|
|
51
|
+
export const KEY_EXTRACTION_PATTERNS: readonly GuardPattern[] = [
|
|
52
|
+
// Direct env var echo/print
|
|
53
|
+
{
|
|
54
|
+
pattern: /\becho\s+\$[A-Z_]*(?:KEY|SECRET|TOKEN|PASSWORD|CREDENTIAL|AUTH)/i,
|
|
55
|
+
reason: 'Attempt to echo sensitive environment variable',
|
|
56
|
+
category: 'env_extraction',
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
pattern: /\bprintf\s+.*\$[A-Z_]*(?:KEY|SECRET|TOKEN|PASSWORD|CREDENTIAL|AUTH)/i,
|
|
60
|
+
reason: 'Attempt to printf sensitive environment variable',
|
|
61
|
+
category: 'env_extraction',
|
|
62
|
+
},
|
|
63
|
+
// env/printenv with grep for secrets
|
|
64
|
+
{
|
|
65
|
+
pattern: /\benv\b.*\|\s*grep\s+.*(?:KEY|SECRET|TOKEN|PASSWORD|API|AUTH)/i,
|
|
66
|
+
reason: 'Attempt to grep env for secrets',
|
|
67
|
+
category: 'env_extraction',
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
pattern: /\bprintenv\b.*(?:KEY|SECRET|TOKEN|PASSWORD|API|AUTH)/i,
|
|
71
|
+
reason: 'Attempt to printenv sensitive variable',
|
|
72
|
+
category: 'env_extraction',
|
|
73
|
+
},
|
|
74
|
+
// set | grep (bash set command dumps all vars)
|
|
75
|
+
{
|
|
76
|
+
pattern: /\bset\b\s*\|\s*grep\s+.*(?:KEY|SECRET|TOKEN|PASSWORD|API|AUTH)/i,
|
|
77
|
+
reason: 'Attempt to dump shell variables and grep for secrets',
|
|
78
|
+
category: 'env_extraction',
|
|
79
|
+
},
|
|
80
|
+
// Reading sensitive config files (must come before bare env/printenv to avoid early match on "cat .env")
|
|
81
|
+
{
|
|
82
|
+
pattern: /\bcat\s+.*\.env\b/i,
|
|
83
|
+
reason: 'Attempt to read .env file',
|
|
84
|
+
category: 'file_exfiltration',
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
pattern: /\bcat\s+.*(?:credentials|secrets|\.aws\/credentials|\.npmrc|\.netrc|\.pgpass)/i,
|
|
88
|
+
reason: 'Attempt to read credentials file',
|
|
89
|
+
category: 'file_exfiltration',
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
pattern: /\bcat\s+.*\/etc\/shadow\b/i,
|
|
93
|
+
reason: 'Attempt to read system password file',
|
|
94
|
+
category: 'file_exfiltration',
|
|
95
|
+
},
|
|
96
|
+
// Direct env dump
|
|
97
|
+
{
|
|
98
|
+
pattern: /\benv\s*$/i,
|
|
99
|
+
reason: 'Attempt to dump all environment variables',
|
|
100
|
+
category: 'key_dump',
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
pattern: /\bprintenv\s*$/i,
|
|
104
|
+
reason: 'Attempt to dump all environment variables',
|
|
105
|
+
category: 'key_dump',
|
|
106
|
+
},
|
|
107
|
+
// compgen dumps shell vars/functions
|
|
108
|
+
{
|
|
109
|
+
pattern: /\bcompgen\s+-[ev]/i,
|
|
110
|
+
reason: 'Attempt to dump shell variables via compgen',
|
|
111
|
+
category: 'key_dump',
|
|
112
|
+
},
|
|
113
|
+
// base64 encoding secrets for exfiltration
|
|
114
|
+
{
|
|
115
|
+
pattern: /\bbase64\b.*\$[A-Z_]*(?:KEY|SECRET|TOKEN)/i,
|
|
116
|
+
reason: 'Attempt to base64 encode secret for exfiltration',
|
|
117
|
+
category: 'file_exfiltration',
|
|
118
|
+
},
|
|
119
|
+
// curl/wget exfiltration of env vars
|
|
120
|
+
{
|
|
121
|
+
pattern: /\b(?:curl|wget)\b.*\$[A-Z_]*(?:KEY|SECRET|TOKEN)/i,
|
|
122
|
+
reason: 'Attempt to exfiltrate secret via HTTP request',
|
|
123
|
+
category: 'file_exfiltration',
|
|
124
|
+
},
|
|
125
|
+
// Python/Node one-liners to access env
|
|
126
|
+
{
|
|
127
|
+
pattern: /\bpython[23]?\s+-c\s+.*os\.environ/i,
|
|
128
|
+
reason: 'Attempt to access env via Python subprocess',
|
|
129
|
+
category: 'env_extraction',
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
pattern: /\bnode\s+-e\s+.*process\.env/i,
|
|
133
|
+
reason: 'Attempt to access env via Node subprocess',
|
|
134
|
+
category: 'env_extraction',
|
|
135
|
+
},
|
|
136
|
+
// Specific key variable names in $() or backtick subshells
|
|
137
|
+
{
|
|
138
|
+
pattern: /\$\(.*(?:ANTHROPIC_API_KEY|OPENAI_API_KEY|GOOGLE_API_KEY|AWS_SECRET|STRIPE_SECRET)/i,
|
|
139
|
+
reason: 'Attempt to expand sensitive env var in subshell',
|
|
140
|
+
category: 'env_extraction',
|
|
141
|
+
},
|
|
142
|
+
] as const;
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Prompt-level injection patterns (detected in natural language prompts, not just commands).
|
|
146
|
+
*/
|
|
147
|
+
export const PROMPT_INJECTION_PATTERNS: readonly GuardPattern[] = [
|
|
148
|
+
{
|
|
149
|
+
pattern: /(?:print|show|reveal|tell\s+me|display|output|give\s+me|share)\s+(?:(?:your|the|my|me\s+the)\s+)?(?:api[_ ]?key|secret|token|password|credentials)/i,
|
|
150
|
+
reason: 'Prompt injection: request to reveal API key/secret',
|
|
151
|
+
category: 'prompt_injection',
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
pattern: /(?:what\s+is|what's)\s+(?:your|the|my)\s+(?:api[_ ]?key|secret[_ ]?key|auth[_ ]?token|password)/i,
|
|
155
|
+
reason: 'Prompt injection: question about API key/secret',
|
|
156
|
+
category: 'prompt_injection',
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
pattern: /ignore\s+(?:previous|all|your)\s+(?:instructions|rules|safety)/i,
|
|
160
|
+
reason: 'Prompt injection: instruction override attempt',
|
|
161
|
+
category: 'prompt_injection',
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
pattern: /(?:override|bypass|disable|turn\s+off)\s+(?:security|safety|filter|guardrail|redaction)/i,
|
|
165
|
+
reason: 'Prompt injection: attempt to disable security filters',
|
|
166
|
+
category: 'prompt_injection',
|
|
167
|
+
},
|
|
168
|
+
] as const;
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Additional blocked command patterns for tool-registry.ts integration.
|
|
172
|
+
* These extend the existing BLOCKED_COMMAND_PATTERNS with key extraction blocks.
|
|
173
|
+
*/
|
|
174
|
+
export const KEY_EXTRACTION_BLOCKED_COMMANDS: RegExp[] = [
|
|
175
|
+
/\benv\s*$/i,
|
|
176
|
+
/\bprintenv\s*$/i,
|
|
177
|
+
/\benv\b.*\|\s*grep\s+.*(?:KEY|SECRET|TOKEN|PASSWORD|API)/i,
|
|
178
|
+
/\bset\b\s*\|\s*grep\s+.*(?:KEY|SECRET|TOKEN|PASSWORD|API)/i,
|
|
179
|
+
/\bcompgen\s+-[ev]/i,
|
|
180
|
+
/\bcat\s+.*\.env\b/i,
|
|
181
|
+
/\bcat\s+.*(?:credentials|secrets|\.aws\/credentials|\.npmrc|\.netrc|\.pgpass)/i,
|
|
182
|
+
];
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Service that detects and blocks prompt injection and key extraction attempts.
|
|
186
|
+
*/
|
|
187
|
+
export class PromptGuardService {
|
|
188
|
+
private readonly commandPatterns: readonly GuardPattern[];
|
|
189
|
+
private readonly promptPatterns: readonly GuardPattern[];
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Creates a new PromptGuardService.
|
|
193
|
+
*
|
|
194
|
+
* @param additionalCommandPatterns - Extra command-level patterns
|
|
195
|
+
* @param additionalPromptPatterns - Extra prompt-level patterns
|
|
196
|
+
*/
|
|
197
|
+
constructor(
|
|
198
|
+
additionalCommandPatterns?: GuardPattern[],
|
|
199
|
+
additionalPromptPatterns?: GuardPattern[],
|
|
200
|
+
) {
|
|
201
|
+
this.commandPatterns = additionalCommandPatterns
|
|
202
|
+
? [...KEY_EXTRACTION_PATTERNS, ...additionalCommandPatterns]
|
|
203
|
+
: KEY_EXTRACTION_PATTERNS;
|
|
204
|
+
this.promptPatterns = additionalPromptPatterns
|
|
205
|
+
? [...PROMPT_INJECTION_PATTERNS, ...additionalPromptPatterns]
|
|
206
|
+
: PROMPT_INJECTION_PATTERNS;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Checks a bash command for key extraction attempts.
|
|
211
|
+
*
|
|
212
|
+
* @param command - Raw bash command string
|
|
213
|
+
* @returns Guard check result
|
|
214
|
+
*/
|
|
215
|
+
checkCommand(command: string): GuardCheckResult {
|
|
216
|
+
if (!command) {
|
|
217
|
+
return { blocked: false, reason: '' };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
for (const guard of this.commandPatterns) {
|
|
221
|
+
if (guard.pattern.test(command)) {
|
|
222
|
+
return {
|
|
223
|
+
blocked: true,
|
|
224
|
+
reason: guard.reason,
|
|
225
|
+
category: guard.category,
|
|
226
|
+
matchedPattern: guard.pattern.source,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return { blocked: false, reason: '' };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Checks a user/agent prompt for injection attempts targeting secrets.
|
|
236
|
+
*
|
|
237
|
+
* @param prompt - The text prompt or message
|
|
238
|
+
* @returns Guard check result
|
|
239
|
+
*/
|
|
240
|
+
checkPrompt(prompt: string): GuardCheckResult {
|
|
241
|
+
if (!prompt) {
|
|
242
|
+
return { blocked: false, reason: '' };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
for (const guard of this.promptPatterns) {
|
|
246
|
+
if (guard.pattern.test(prompt)) {
|
|
247
|
+
return {
|
|
248
|
+
blocked: true,
|
|
249
|
+
reason: guard.reason,
|
|
250
|
+
category: guard.category,
|
|
251
|
+
matchedPattern: guard.pattern.source,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return { blocked: false, reason: '' };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Checks both command and prompt patterns.
|
|
261
|
+
* Use this for comprehensive scanning of any agent input.
|
|
262
|
+
*
|
|
263
|
+
* @param text - Text to check (command or prompt)
|
|
264
|
+
* @returns Guard check result
|
|
265
|
+
*/
|
|
266
|
+
check(text: string): GuardCheckResult {
|
|
267
|
+
const cmdResult = this.checkCommand(text);
|
|
268
|
+
if (cmdResult.blocked) return cmdResult;
|
|
269
|
+
return this.checkPrompt(text);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Creates an audit entry for a blocked attempt.
|
|
274
|
+
*
|
|
275
|
+
* @param sessionName - Agent session that triggered the block
|
|
276
|
+
* @param toolName - Tool that was used (e.g. 'bash_exec')
|
|
277
|
+
* @param command - The blocked command
|
|
278
|
+
* @param guardResult - The guard check result
|
|
279
|
+
* @returns AuditEntry suitable for the audit log
|
|
280
|
+
*/
|
|
281
|
+
createAuditEntry(
|
|
282
|
+
sessionName: string,
|
|
283
|
+
toolName: string,
|
|
284
|
+
command: string,
|
|
285
|
+
guardResult: GuardCheckResult,
|
|
286
|
+
): AuditEntry {
|
|
287
|
+
return {
|
|
288
|
+
timestamp: new Date().toISOString(),
|
|
289
|
+
sessionName,
|
|
290
|
+
toolName,
|
|
291
|
+
sensitivity: 'destructive' as ToolSensitivity,
|
|
292
|
+
args: {
|
|
293
|
+
command,
|
|
294
|
+
blockedReason: guardResult.reason,
|
|
295
|
+
category: guardResult.category || 'unknown',
|
|
296
|
+
matchedPattern: guardResult.matchedPattern || '',
|
|
297
|
+
},
|
|
298
|
+
success: false,
|
|
299
|
+
error: `Blocked by prompt guard: ${guardResult.reason}`,
|
|
300
|
+
durationMs: 0,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import { RateLimiter, RATE_LIMITER_DEFAULTS } from './rate-limiter.js';
|
|
3
|
+
|
|
4
|
+
describe('RateLimiter', () => {
|
|
5
|
+
let limiter: RateLimiter<string>;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
vi.useFakeTimers();
|
|
9
|
+
limiter = new RateLimiter<string>({
|
|
10
|
+
maxRequestsPerWindow: 3,
|
|
11
|
+
windowMs: 10_000,
|
|
12
|
+
maxRetries: 2,
|
|
13
|
+
initialBackoffMs: 100,
|
|
14
|
+
backoffMultiplier: 2,
|
|
15
|
+
maxBackoffMs: 1000,
|
|
16
|
+
coalesceWindowMs: 50,
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
limiter.reset();
|
|
22
|
+
vi.useRealTimers();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('defaults', () => {
|
|
26
|
+
it('should have sensible default config', () => {
|
|
27
|
+
const defaultLimiter = new RateLimiter<string>();
|
|
28
|
+
const config = defaultLimiter.getConfig();
|
|
29
|
+
expect(config.maxRequestsPerWindow).toBe(RATE_LIMITER_DEFAULTS.maxRequestsPerWindow);
|
|
30
|
+
expect(config.windowMs).toBe(RATE_LIMITER_DEFAULTS.windowMs);
|
|
31
|
+
expect(config.maxRetries).toBe(RATE_LIMITER_DEFAULTS.maxRetries);
|
|
32
|
+
defaultLimiter.reset();
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('basic enqueue', () => {
|
|
37
|
+
it('should process a single message', async () => {
|
|
38
|
+
const handler = vi.fn<(msg: string) => Promise<string>>().mockResolvedValue('ok');
|
|
39
|
+
const resultP = limiter.enqueue('hello', undefined, handler);
|
|
40
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
41
|
+
const result = await resultP;
|
|
42
|
+
expect(result).toBe('ok');
|
|
43
|
+
expect(handler).toHaveBeenCalledWith('hello', undefined);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should pass metadata through', async () => {
|
|
47
|
+
const handler = vi.fn<(msg: string, meta?: Record<string, string>) => Promise<string>>().mockResolvedValue('ok');
|
|
48
|
+
const meta = { channelId: 'C1' };
|
|
49
|
+
const resultP = limiter.enqueue('hello', meta, handler);
|
|
50
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
51
|
+
await resultP;
|
|
52
|
+
expect(handler).toHaveBeenCalledWith('hello', meta);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('message coalescing', () => {
|
|
57
|
+
it('should coalesce messages arriving within the coalesce window', async () => {
|
|
58
|
+
const handler = vi.fn<(msg: string) => Promise<string>>().mockResolvedValue('ok');
|
|
59
|
+
|
|
60
|
+
// Enqueue 3 messages rapidly (within 50ms coalesce window)
|
|
61
|
+
const p1 = limiter.enqueue('msg1', undefined, handler);
|
|
62
|
+
const p2 = limiter.enqueue('msg2', undefined, handler);
|
|
63
|
+
const p3 = limiter.enqueue('msg3', undefined, handler);
|
|
64
|
+
|
|
65
|
+
// Advance past coalesce window
|
|
66
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
67
|
+
|
|
68
|
+
const [r1, r2, r3] = await Promise.all([p1, p2, p3]);
|
|
69
|
+
|
|
70
|
+
// All should get the same result
|
|
71
|
+
expect(r1).toBe('ok');
|
|
72
|
+
expect(r2).toBe('ok');
|
|
73
|
+
expect(r3).toBe('ok');
|
|
74
|
+
|
|
75
|
+
// Handler called only once (messages coalesced)
|
|
76
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
77
|
+
|
|
78
|
+
// The coalesced message should mention all 3
|
|
79
|
+
const callArg = handler.mock.calls[0][0];
|
|
80
|
+
expect(callArg).toContain('3 messages received');
|
|
81
|
+
expect(callArg).toContain('msg1');
|
|
82
|
+
expect(callArg).toContain('msg2');
|
|
83
|
+
expect(callArg).toContain('msg3');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should not coalesce a single message', async () => {
|
|
87
|
+
const handler = vi.fn<(msg: string) => Promise<string>>().mockResolvedValue('ok');
|
|
88
|
+
|
|
89
|
+
const p = limiter.enqueue('single', undefined, handler);
|
|
90
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
91
|
+
await p;
|
|
92
|
+
|
|
93
|
+
expect(handler).toHaveBeenCalledWith('single', undefined);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('429 retry', () => {
|
|
98
|
+
it('should retry on quota exceeded error', async () => {
|
|
99
|
+
const handler = vi.fn<(msg: string) => Promise<string>>()
|
|
100
|
+
.mockRejectedValueOnce(new Error('429 Too Many Requests'))
|
|
101
|
+
.mockResolvedValueOnce('recovered');
|
|
102
|
+
|
|
103
|
+
const resultP = limiter.enqueue('test', undefined, handler);
|
|
104
|
+
// Advance past coalesce window
|
|
105
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
106
|
+
// Advance past backoff (100ms)
|
|
107
|
+
await vi.advanceTimersByTimeAsync(200);
|
|
108
|
+
const result = await resultP;
|
|
109
|
+
|
|
110
|
+
expect(result).toBe('recovered');
|
|
111
|
+
expect(handler).toHaveBeenCalledTimes(2);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should retry on quota exceeded message', async () => {
|
|
115
|
+
const handler = vi.fn<(msg: string) => Promise<string>>()
|
|
116
|
+
.mockRejectedValueOnce(new Error('You exceeded your current quota'))
|
|
117
|
+
.mockResolvedValueOnce('ok');
|
|
118
|
+
|
|
119
|
+
const resultP = limiter.enqueue('test', undefined, handler);
|
|
120
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
121
|
+
await vi.advanceTimersByTimeAsync(200);
|
|
122
|
+
const result = await resultP;
|
|
123
|
+
|
|
124
|
+
expect(result).toBe('ok');
|
|
125
|
+
expect(handler).toHaveBeenCalledTimes(2);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should retry on RESOURCE_EXHAUSTED', async () => {
|
|
129
|
+
const handler = vi.fn<(msg: string) => Promise<string>>()
|
|
130
|
+
.mockRejectedValueOnce(new Error('RESOURCE_EXHAUSTED: quota limit reached'))
|
|
131
|
+
.mockResolvedValueOnce('ok');
|
|
132
|
+
|
|
133
|
+
const resultP = limiter.enqueue('test', undefined, handler);
|
|
134
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
135
|
+
await vi.advanceTimersByTimeAsync(200);
|
|
136
|
+
const result = await resultP;
|
|
137
|
+
|
|
138
|
+
expect(result).toBe('ok');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should NOT retry on non-quota errors', async () => {
|
|
142
|
+
const handler = vi.fn<(msg: string) => Promise<string>>()
|
|
143
|
+
.mockRejectedValue(new Error('Invalid API key'));
|
|
144
|
+
|
|
145
|
+
// Attach catch immediately to prevent unhandled rejection warning
|
|
146
|
+
let caughtError: Error | null = null;
|
|
147
|
+
const resultP = limiter.enqueue('test', undefined, handler)
|
|
148
|
+
.catch((e: Error) => { caughtError = e; return 'caught' as string; });
|
|
149
|
+
|
|
150
|
+
for (let i = 0; i < 10; i++) {
|
|
151
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
152
|
+
}
|
|
153
|
+
await resultP;
|
|
154
|
+
|
|
155
|
+
expect(caughtError).not.toBeNull();
|
|
156
|
+
expect(caughtError!.message).toBe('Invalid API key');
|
|
157
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should reject after max retries exhausted', async () => {
|
|
161
|
+
const handler = vi.fn<(msg: string) => Promise<string>>()
|
|
162
|
+
.mockRejectedValue(new Error('429 rate limited'));
|
|
163
|
+
|
|
164
|
+
let caughtError: Error | null = null;
|
|
165
|
+
const resultP = limiter.enqueue('test', undefined, handler)
|
|
166
|
+
.catch((e: Error) => { caughtError = e; return 'caught' as string; });
|
|
167
|
+
|
|
168
|
+
for (let i = 0; i < 20; i++) {
|
|
169
|
+
await vi.advanceTimersByTimeAsync(200);
|
|
170
|
+
}
|
|
171
|
+
await resultP;
|
|
172
|
+
|
|
173
|
+
expect(caughtError).not.toBeNull();
|
|
174
|
+
expect(caughtError!.message).toContain('Rate limit retries exhausted');
|
|
175
|
+
expect(handler).toHaveBeenCalledTimes(3); // initial + 2 retries
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('should reject all coalesced messages on retry exhaustion', async () => {
|
|
179
|
+
const handler = vi.fn<(msg: string) => Promise<string>>()
|
|
180
|
+
.mockRejectedValue(new Error('429'));
|
|
181
|
+
|
|
182
|
+
let err1: Error | null = null;
|
|
183
|
+
let err2: Error | null = null;
|
|
184
|
+
const p1 = limiter.enqueue('a', undefined, handler)
|
|
185
|
+
.catch((e: Error) => { err1 = e; return 'caught' as string; });
|
|
186
|
+
const p2 = limiter.enqueue('b', undefined, handler)
|
|
187
|
+
.catch((e: Error) => { err2 = e; return 'caught' as string; });
|
|
188
|
+
|
|
189
|
+
for (let i = 0; i < 20; i++) {
|
|
190
|
+
await vi.advanceTimersByTimeAsync(200);
|
|
191
|
+
}
|
|
192
|
+
await Promise.all([p1, p2]);
|
|
193
|
+
|
|
194
|
+
expect(err1).not.toBeNull();
|
|
195
|
+
expect(err1!.message).toContain('Rate limit retries exhausted');
|
|
196
|
+
expect(err2).not.toBeNull();
|
|
197
|
+
expect(err2!.message).toContain('Rate limit retries exhausted');
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe('rate limiting (window enforcement)', () => {
|
|
202
|
+
it('should track requests in window', async () => {
|
|
203
|
+
const handler = vi.fn<(msg: string) => Promise<string>>().mockResolvedValue('ok');
|
|
204
|
+
|
|
205
|
+
expect(limiter.getRequestCountInWindow()).toBe(0);
|
|
206
|
+
|
|
207
|
+
const p1 = limiter.enqueue('a', undefined, handler);
|
|
208
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
209
|
+
await p1;
|
|
210
|
+
expect(limiter.getRequestCountInWindow()).toBe(1);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe('reset', () => {
|
|
215
|
+
it('should clear all state', async () => {
|
|
216
|
+
const handler = vi.fn<(msg: string) => Promise<string>>().mockResolvedValue('ok');
|
|
217
|
+
|
|
218
|
+
const p = limiter.enqueue('test', undefined, handler);
|
|
219
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
220
|
+
await p;
|
|
221
|
+
|
|
222
|
+
limiter.reset();
|
|
223
|
+
expect(limiter.getQueueLength()).toBe(0);
|
|
224
|
+
expect(limiter.isProcessing()).toBe(false);
|
|
225
|
+
expect(limiter.getRequestCountInWindow()).toBe(0);
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
});
|