crewly 1.11.6 → 1.12.1
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 +167 -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,363 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crewly Agent Model Manager
|
|
3
|
+
*
|
|
4
|
+
* Multi-provider model factory that creates AI SDK model instances
|
|
5
|
+
* from configuration. Supports Anthropic, OpenAI, Google, DeepSeek, and
|
|
6
|
+
* Ollama providers.
|
|
7
|
+
*
|
|
8
|
+
* API keys for cloud providers are resolved through the settings service
|
|
9
|
+
* (skill → runtime → global → env var) and injected into process.env so the
|
|
10
|
+
* provider SDKs can pick them up:
|
|
11
|
+
* - ANTHROPIC_API_KEY
|
|
12
|
+
* - OPENAI_API_KEY
|
|
13
|
+
* - GOOGLE_GENERATIVE_AI_API_KEY
|
|
14
|
+
* - DEEPSEEK_API_KEY (DeepSeek; served via OpenAI-compatible API)
|
|
15
|
+
*
|
|
16
|
+
* Ollama runs locally and does not require an API key.
|
|
17
|
+
* Configure the Ollama base URL via OLLAMA_BASE_URL (default: http://localhost:11434).
|
|
18
|
+
*
|
|
19
|
+
* @module services/agent/crewly-agent/model-manager
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { LanguageModel } from 'ai';
|
|
23
|
+
import { type ModelConfig, type ModelProvider, CREWLY_AGENT_DEFAULTS } from './types.js';
|
|
24
|
+
/**
|
|
25
|
+
* Standalone runtime resolves API keys from environment variables only — it
|
|
26
|
+
* does not depend on OSS's settings service. Anyone running this binary on
|
|
27
|
+
* a user machine sets keys via env (.env, shell profile, or the harness that
|
|
28
|
+
* spawns the process), keeping the runtime credential-isolated.
|
|
29
|
+
*
|
|
30
|
+
* OSS-side bridging code can still pre-set env vars before spawning the
|
|
31
|
+
* subprocess, which gives users the same "configure key in UI" experience
|
|
32
|
+
* without coupling the runtime to OSS internals.
|
|
33
|
+
*/
|
|
34
|
+
type ApiKeyProvider = 'gemini' | 'anthropic' | 'openai' | 'deepseek';
|
|
35
|
+
|
|
36
|
+
interface SettingsServiceLike {
|
|
37
|
+
getApiKey(provider: ApiKeyProvider, context: { runtime: string }): Promise<string | null>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getSettingsService(): SettingsServiceLike {
|
|
41
|
+
return {
|
|
42
|
+
getApiKey: async (provider) => {
|
|
43
|
+
switch (provider) {
|
|
44
|
+
case 'gemini':
|
|
45
|
+
return process.env.GOOGLE_GENERATIVE_AI_API_KEY ?? process.env.GEMINI_API_KEY ?? null;
|
|
46
|
+
case 'anthropic':
|
|
47
|
+
return process.env.ANTHROPIC_API_KEY ?? null;
|
|
48
|
+
case 'openai':
|
|
49
|
+
return process.env.OPENAI_API_KEY ?? null;
|
|
50
|
+
case 'deepseek':
|
|
51
|
+
return process.env.DEEPSEEK_API_KEY ?? null;
|
|
52
|
+
default:
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
import { teeAndParse, type ParsedDeepseekSse } from './deepseek-sse-transform.js';
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Base URL for DeepSeek's OpenAI-compatible chat completions endpoint.
|
|
62
|
+
* DeepSeek implements /chat/completions only — not /responses — so we
|
|
63
|
+
* must route via the `.chat(modelId)` factory of @ai-sdk/openai's
|
|
64
|
+
* createOpenAI() return value. Extracted per CLAUDE.md no-hardcoded-values rule.
|
|
65
|
+
*/
|
|
66
|
+
const DEEPSEEK_BASE_URL = 'https://api.deepseek.com/v1';
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Factory for creating AI SDK language model instances from configuration.
|
|
70
|
+
*
|
|
71
|
+
* Uses dynamic imports to avoid loading provider SDKs that aren't needed,
|
|
72
|
+
* keeping the startup cost minimal when only one provider is used.
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```typescript
|
|
76
|
+
* const manager = new ModelManager();
|
|
77
|
+
* const model = await manager.getModel({ provider: 'anthropic', modelId: 'claude-sonnet-4-20250514' });
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
export class ModelManager {
|
|
81
|
+
/** Cached provider module references to avoid re-importing */
|
|
82
|
+
private providerCache = new Map<ModelProvider, (modelId: string) => LanguageModel>();
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Per-instance buffer of parsed DeepSeek-R1 reasoning_content from in-flight
|
|
86
|
+
* and recently-completed HTTP calls. Each `streamText` call to a DeepSeek
|
|
87
|
+
* model triggers one or more fetch calls (one per agentic step); each fetch
|
|
88
|
+
* appends its parsed reasoning to this buffer. Consumer (agent-runner) reads
|
|
89
|
+
* via `consumeDeepseekReasoning()` after `await streamResult` and the buffer
|
|
90
|
+
* resets to `''` on read.
|
|
91
|
+
*
|
|
92
|
+
* **Concurrency:** AgentRunner uses one ModelManager per session and calls
|
|
93
|
+
* streamText serially per session (the rate limiter enforces this).
|
|
94
|
+
* Cross-session concurrency is not a concern because each AgentRunner gets
|
|
95
|
+
* its own ModelManager via `new ModelManager()` in the constructor.
|
|
96
|
+
*/
|
|
97
|
+
private deepseekReasoningBuffer = '';
|
|
98
|
+
|
|
99
|
+
/** Tracks in-flight parsed-SSE handles so we can wait for drain on consume. */
|
|
100
|
+
private deepseekParsedHandles: ParsedDeepseekSse[] = [];
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Get an AI SDK language model instance for the given configuration.
|
|
104
|
+
*
|
|
105
|
+
* Resolves API keys from settings (with override chain) and injects them
|
|
106
|
+
* into the environment before creating the model, so provider SDKs can find them.
|
|
107
|
+
*
|
|
108
|
+
* @param config - Model configuration specifying provider and model ID
|
|
109
|
+
* @returns AI SDK LanguageModel instance ready for use with generateText
|
|
110
|
+
* @throws Error if the provider is unknown or the SDK is not installed
|
|
111
|
+
*/
|
|
112
|
+
async getModel(config: ModelConfig = CREWLY_AGENT_DEFAULTS.DEFAULT_MODEL): Promise<LanguageModel> {
|
|
113
|
+
// Ensure the provider's API key is in the environment from settings
|
|
114
|
+
await this.ensureApiKeyInEnv(config.provider);
|
|
115
|
+
|
|
116
|
+
const providerFn = await this.getProviderFunction(config.provider);
|
|
117
|
+
return providerFn(config.modelId);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Get provider function, using cache for repeated calls.
|
|
122
|
+
*
|
|
123
|
+
* @param provider - Provider name
|
|
124
|
+
* @returns Function that creates a model from a model ID string
|
|
125
|
+
*/
|
|
126
|
+
private async getProviderFunction(provider: ModelProvider): Promise<(modelId: string) => LanguageModel> {
|
|
127
|
+
const cached = this.providerCache.get(provider);
|
|
128
|
+
if (cached) return cached;
|
|
129
|
+
|
|
130
|
+
let providerFn: (modelId: string) => LanguageModel;
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
switch (provider) {
|
|
134
|
+
case 'anthropic': {
|
|
135
|
+
const { anthropic } = await import('@ai-sdk/anthropic');
|
|
136
|
+
providerFn = (modelId: string) => anthropic(modelId);
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
case 'openai': {
|
|
140
|
+
const { openai } = await import('@ai-sdk/openai');
|
|
141
|
+
providerFn = (modelId: string) => openai(modelId);
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
case 'google': {
|
|
145
|
+
const { google } = await import('@ai-sdk/google');
|
|
146
|
+
providerFn = (modelId: string) => google(modelId);
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
case 'ollama': {
|
|
150
|
+
const { createOllama } = await import('ollama-ai-provider');
|
|
151
|
+
const baseURL = process.env.OLLAMA_BASE_URL || CREWLY_AGENT_DEFAULTS.OLLAMA_BASE_URL;
|
|
152
|
+
const ollamaProvider = createOllama({ baseURL });
|
|
153
|
+
// ollama-ai-provider exports LanguageModelV1 which is compatible but
|
|
154
|
+
// doesn't extend the newer LanguageModel union — safe to cast.
|
|
155
|
+
providerFn = (modelId: string) => ollamaProvider(modelId) as unknown as LanguageModel;
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
case 'deepseek': {
|
|
159
|
+
// DeepSeek API is OpenAI-compatible — reuse the OpenAI SDK with a custom baseURL.
|
|
160
|
+
// IMPORTANT: must call deepseekProvider.chat(modelId), NOT deepseekProvider(modelId).
|
|
161
|
+
// The bare function-call form on @ai-sdk/openai >=3.x routes to /responses, which
|
|
162
|
+
// DeepSeek does not implement — it only exposes /chat/completions. The .chat factory
|
|
163
|
+
// forces the chat-completions path. See PR #400 review for full trace.
|
|
164
|
+
//
|
|
165
|
+
// **I2 — reasoning_content extraction:**
|
|
166
|
+
// Pass a custom `fetch` that tees the SSE response body. One branch flows
|
|
167
|
+
// through to AI SDK unmodified (zero impact on existing behavior); the other
|
|
168
|
+
// branch is parsed for `delta.reasoning_content` text and accumulated into
|
|
169
|
+
// `this.deepseekReasoningBuffer`, which agent-runner consumes after
|
|
170
|
+
// streamResult drains. See deepseek-sse-transform.ts for the parser.
|
|
171
|
+
const { createOpenAI } = await import('@ai-sdk/openai');
|
|
172
|
+
const customFetch = this.makeDeepseekFetch();
|
|
173
|
+
const deepseekProvider = createOpenAI({
|
|
174
|
+
baseURL: DEEPSEEK_BASE_URL,
|
|
175
|
+
apiKey: process.env.DEEPSEEK_API_KEY,
|
|
176
|
+
fetch: customFetch as unknown as typeof globalThis.fetch,
|
|
177
|
+
});
|
|
178
|
+
providerFn = (modelId: string) => deepseekProvider.chat(modelId);
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
default:
|
|
182
|
+
throw new Error(`Unknown model provider: ${provider}`);
|
|
183
|
+
}
|
|
184
|
+
} catch (error) {
|
|
185
|
+
if (error instanceof Error && error.message.startsWith('Unknown model provider:')) {
|
|
186
|
+
throw error;
|
|
187
|
+
}
|
|
188
|
+
throw new Error(
|
|
189
|
+
`Failed to load provider SDK for '${provider}'. Is the package installed? ` +
|
|
190
|
+
`Original error: ${error instanceof Error ? error.message : String(error)}`
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
this.providerCache.set(provider, providerFn);
|
|
195
|
+
return providerFn;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Check which providers have API keys configured (settings + env vars).
|
|
200
|
+
*
|
|
201
|
+
* @returns Object indicating which providers are available
|
|
202
|
+
*/
|
|
203
|
+
async getAvailableProviders(): Promise<Record<ModelProvider, boolean>> {
|
|
204
|
+
const settingsService = getSettingsService();
|
|
205
|
+
const context = { runtime: 'crewly-agent' };
|
|
206
|
+
|
|
207
|
+
const [geminiKey, anthropicKey, openaiKey, deepseekKey] = await Promise.all([
|
|
208
|
+
settingsService.getApiKey('gemini', context),
|
|
209
|
+
settingsService.getApiKey('anthropic', context),
|
|
210
|
+
settingsService.getApiKey('openai', context),
|
|
211
|
+
settingsService.getApiKey('deepseek', context),
|
|
212
|
+
]);
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
anthropic: !!anthropicKey,
|
|
216
|
+
openai: !!openaiKey,
|
|
217
|
+
google: !!geminiKey,
|
|
218
|
+
ollama: true, // Ollama runs locally, always "available" if installed
|
|
219
|
+
deepseek: !!deepseekKey,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Map model provider name to API key provider name.
|
|
225
|
+
* Only applicable for cloud providers wired through the settings service
|
|
226
|
+
* (anthropic, openai, google, deepseek). Ollama runs locally and is
|
|
227
|
+
* excluded.
|
|
228
|
+
*
|
|
229
|
+
* @param provider - Cloud model provider
|
|
230
|
+
* @returns Corresponding ApiKeyProvider name
|
|
231
|
+
*/
|
|
232
|
+
private static providerToApiKeyProvider(provider: Exclude<ModelProvider, 'ollama'>): ApiKeyProvider {
|
|
233
|
+
return provider === 'google' ? 'gemini' : provider;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Ensure the API key for a provider is available in process.env
|
|
238
|
+
* by resolving it from settings if not already present.
|
|
239
|
+
*
|
|
240
|
+
* No-op for 'ollama' (runs locally, no key required). All other providers —
|
|
241
|
+
* including 'deepseek' — flow through the settings service so users can
|
|
242
|
+
* configure them from the UI without touching env vars.
|
|
243
|
+
*
|
|
244
|
+
* @param provider - The model provider
|
|
245
|
+
*/
|
|
246
|
+
private async ensureApiKeyInEnv(provider: ModelProvider): Promise<void> {
|
|
247
|
+
if (provider === 'ollama') return; // Ollama is local, no API key needed
|
|
248
|
+
const apiKeyProvider = ModelManager.providerToApiKeyProvider(provider);
|
|
249
|
+
const settingsService = getSettingsService();
|
|
250
|
+
const key = await settingsService.getApiKey(apiKeyProvider, { runtime: 'crewly-agent' });
|
|
251
|
+
|
|
252
|
+
if (!key) return;
|
|
253
|
+
|
|
254
|
+
// Set env vars so the provider SDKs can find them.
|
|
255
|
+
// Always override — settings keys take priority over stale env vars.
|
|
256
|
+
switch (provider) {
|
|
257
|
+
case 'anthropic':
|
|
258
|
+
process.env.ANTHROPIC_API_KEY = key;
|
|
259
|
+
break;
|
|
260
|
+
case 'openai':
|
|
261
|
+
process.env.OPENAI_API_KEY = key;
|
|
262
|
+
break;
|
|
263
|
+
case 'google':
|
|
264
|
+
process.env.GOOGLE_GENERATIVE_AI_API_KEY = key;
|
|
265
|
+
break;
|
|
266
|
+
case 'deepseek':
|
|
267
|
+
process.env.DEEPSEEK_API_KEY = key;
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Clear the provider cache (useful for testing or reconfiguration).
|
|
274
|
+
* Also clears any buffered DeepSeek reasoning so a fresh test/run starts clean.
|
|
275
|
+
*/
|
|
276
|
+
clearCache(): void {
|
|
277
|
+
this.providerCache.clear();
|
|
278
|
+
this.deepseekReasoningBuffer = '';
|
|
279
|
+
this.deepseekParsedHandles = [];
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Build the custom fetch wrapper for the DeepSeek provider.
|
|
284
|
+
*
|
|
285
|
+
* Returns a function with the same signature as `globalThis.fetch` that:
|
|
286
|
+
* 1. Calls native fetch with the supplied input/init.
|
|
287
|
+
* 2. If the response is a streaming SSE body, tees it and parses one branch
|
|
288
|
+
* for `delta.reasoning_content`, accumulating into `this.deepseekReasoningBuffer`.
|
|
289
|
+
* 3. Returns a new Response wrapping the un-tampered passthrough branch as
|
|
290
|
+
* its body, so AI SDK consumes exactly the bytes DeepSeek sent.
|
|
291
|
+
*
|
|
292
|
+
* If the response is not an SSE stream (e.g. error JSON, no body), it is
|
|
293
|
+
* returned unchanged.
|
|
294
|
+
*
|
|
295
|
+
* **Why a method, not a free function:** the wrapper closes over `this` to
|
|
296
|
+
* append to the per-instance reasoning buffer. Each ModelManager instance
|
|
297
|
+
* has its own buffer (one per AgentRunner per session).
|
|
298
|
+
*/
|
|
299
|
+
private makeDeepseekFetch(): (input: unknown, init?: unknown) => Promise<Response> {
|
|
300
|
+
return async (input: unknown, init?: unknown): Promise<Response> => {
|
|
301
|
+
// Cast through `any` because @ai-sdk/openai's fetch type is the standard
|
|
302
|
+
// global fetch shape; we re-export native fetch behavior identically.
|
|
303
|
+
const response: Response = await (globalThis.fetch as any)(input, init);
|
|
304
|
+
|
|
305
|
+
// Only intercept successful streaming responses. Non-stream errors
|
|
306
|
+
// (4xx/5xx with JSON body) and empty bodies pass through untouched.
|
|
307
|
+
if (!response.ok || !response.body) return response;
|
|
308
|
+
const contentType = response.headers.get('content-type') ?? '';
|
|
309
|
+
if (!contentType.includes('text/event-stream')) return response;
|
|
310
|
+
|
|
311
|
+
const parsed = teeAndParse(response.body);
|
|
312
|
+
this.deepseekParsedHandles.push(parsed);
|
|
313
|
+
|
|
314
|
+
// Wrap into a new Response with the passthrough branch as body.
|
|
315
|
+
// Headers, status, and statusText are copied so AI SDK sees an identical
|
|
316
|
+
// response shape.
|
|
317
|
+
return new Response(parsed.passthroughBody, {
|
|
318
|
+
status: response.status,
|
|
319
|
+
statusText: response.statusText,
|
|
320
|
+
headers: response.headers,
|
|
321
|
+
});
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Drain any in-flight DeepSeek SSE parser branches and return the accumulated
|
|
327
|
+
* `reasoning_content` string. Resets the buffer to `''` on each call (consume
|
|
328
|
+
* semantics).
|
|
329
|
+
*
|
|
330
|
+
* **Caller contract:** call AFTER `await streamResult` resolves in agent-runner.
|
|
331
|
+
* The AI SDK consumer branch must have been fully drained for the parser branch
|
|
332
|
+
* (which lags slightly due to tee buffering) to have caught up.
|
|
333
|
+
*
|
|
334
|
+
* **Multi-step accumulation:** if a single `streamText` call made multiple HTTP
|
|
335
|
+
* calls (one per agentic step), reasoning from all steps is concatenated in
|
|
336
|
+
* call order — not separated by step boundary. This matches the user-facing
|
|
337
|
+
* mental model of "what was the model's full chain of thought for this turn."
|
|
338
|
+
*
|
|
339
|
+
* **Returns `null` if no reasoning was captured.** Empty string means a fetch
|
|
340
|
+
* happened but produced no reasoning content (e.g. non-R1 DeepSeek model).
|
|
341
|
+
*/
|
|
342
|
+
async consumeDeepseekReasoning(): Promise<string | null> {
|
|
343
|
+
const handles = this.deepseekParsedHandles;
|
|
344
|
+
this.deepseekParsedHandles = [];
|
|
345
|
+
if (handles.length === 0) {
|
|
346
|
+
const buffered = this.deepseekReasoningBuffer;
|
|
347
|
+
this.deepseekReasoningBuffer = '';
|
|
348
|
+
return buffered || null;
|
|
349
|
+
}
|
|
350
|
+
// Wait for all parser branches to finish draining (they should already be
|
|
351
|
+
// done if AI SDK consumer drained, but allow up to one event-loop tick).
|
|
352
|
+
for (const h of handles) {
|
|
353
|
+
if (!h.isDrained()) {
|
|
354
|
+
// Yield once so the parser background reader can flush.
|
|
355
|
+
await new Promise((r) => setImmediate(r));
|
|
356
|
+
}
|
|
357
|
+
this.deepseekReasoningBuffer += h.getReasoning();
|
|
358
|
+
}
|
|
359
|
+
const out = this.deepseekReasoningBuffer;
|
|
360
|
+
this.deepseekReasoningBuffer = '';
|
|
361
|
+
return out || null;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output Filter Service — API Key Redaction
|
|
3
|
+
*
|
|
4
|
+
* Scans all agent text output for API key patterns and replaces them
|
|
5
|
+
* with [REDACTED] before the output reaches users or logs.
|
|
6
|
+
*
|
|
7
|
+
* Detects patterns from major providers (OpenAI, Anthropic, Google, AWS)
|
|
8
|
+
* as well as generic key/token/secret assignments.
|
|
9
|
+
*
|
|
10
|
+
* @module services/agent/crewly-agent/output-filter.service
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* const filter = new OutputFilterService();
|
|
15
|
+
* const safe = filter.redact('My key is sk-abc123xyz');
|
|
16
|
+
* // => 'My key is [REDACTED]'
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A single API key detection pattern with a label for audit logging.
|
|
22
|
+
*/
|
|
23
|
+
export interface KeyPattern {
|
|
24
|
+
/** Regex to match the key in text */
|
|
25
|
+
pattern: RegExp;
|
|
26
|
+
/** Human-readable label for audit/logging */
|
|
27
|
+
label: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Result of scanning text for API keys.
|
|
32
|
+
*/
|
|
33
|
+
export interface ScanResult {
|
|
34
|
+
/** Whether any keys were detected */
|
|
35
|
+
detected: boolean;
|
|
36
|
+
/** Number of keys found */
|
|
37
|
+
count: number;
|
|
38
|
+
/** Labels of the matched patterns */
|
|
39
|
+
matchedPatterns: string[];
|
|
40
|
+
/** Redacted text with keys replaced */
|
|
41
|
+
redactedText: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* API key patterns for major providers and generic secrets.
|
|
46
|
+
*
|
|
47
|
+
* Each pattern uses word boundaries or lookbehind/lookahead to minimize
|
|
48
|
+
* false positives while catching real key formats.
|
|
49
|
+
*/
|
|
50
|
+
export const API_KEY_PATTERNS: readonly KeyPattern[] = [
|
|
51
|
+
// OpenAI: sk-<org>-<rest> or sk-<48+ chars>
|
|
52
|
+
{ pattern: /\bsk-[A-Za-z0-9_-]{20,}/g, label: 'OpenAI API Key' },
|
|
53
|
+
// Anthropic: sk-ant-api03-<rest>
|
|
54
|
+
{ pattern: /\bsk-ant-[A-Za-z0-9_-]{20,}/g, label: 'Anthropic API Key' },
|
|
55
|
+
// Google AI: AIza<rest>
|
|
56
|
+
{ pattern: /\bAIza[A-Za-z0-9_-]{30,}/g, label: 'Google API Key' },
|
|
57
|
+
// AWS Access Key: AKIA<16 alphanumeric>
|
|
58
|
+
{ pattern: /\bAKIA[A-Z0-9]{16}\b/g, label: 'AWS Access Key' },
|
|
59
|
+
// AWS Secret Key (often follows access key)
|
|
60
|
+
{ pattern: /(?<=aws_secret_access_key\s*[=:]\s*)[A-Za-z0-9/+=]{30,}/g, label: 'AWS Secret Key' },
|
|
61
|
+
// GitHub tokens: ghp_, gho_, ghu_, ghs_, ghr_
|
|
62
|
+
{ pattern: /\bgh[pousr]_[A-Za-z0-9_]{30,}/g, label: 'GitHub Token' },
|
|
63
|
+
// Stripe: sk_live_ or sk_test_
|
|
64
|
+
{ pattern: /\bsk_(live|test)_[A-Za-z0-9]{20,}/g, label: 'Stripe API Key' },
|
|
65
|
+
// Supabase anon/service keys (JWT-like, start with eyJ)
|
|
66
|
+
{ pattern: /\beyJ[A-Za-z0-9_-]{50,}\.[A-Za-z0-9_-]{50,}\.[A-Za-z0-9_-]{20,}/g, label: 'JWT Token' },
|
|
67
|
+
// Generic key=value patterns (key, token, secret, password, api_key, apikey)
|
|
68
|
+
{ pattern: /(?<=(?:api[_-]?key|api[_-]?secret|api[_-]?token|secret[_-]?key|access[_-]?token|auth[_-]?token|password|secret)\s*[=:]\s*["']?)[A-Za-z0-9_/+=.-]{16,}/gi, label: 'Generic Secret' },
|
|
69
|
+
// Environment variable assignments with sensitive names
|
|
70
|
+
{ pattern: /(?<=(?:ANTHROPIC_API_KEY|OPENAI_API_KEY|GOOGLE_API_KEY|AWS_SECRET_ACCESS_KEY|STRIPE_SECRET_KEY|SUPABASE_SERVICE_ROLE_KEY|DATABASE_URL|REDIS_URL)\s*=\s*["']?)[^\s"']{8,}/g, label: 'Environment Variable Secret' },
|
|
71
|
+
] as const;
|
|
72
|
+
|
|
73
|
+
/** Replacement text for redacted keys */
|
|
74
|
+
export const REDACTION_PLACEHOLDER = '[REDACTED]';
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Service that scans and redacts API keys from agent output text.
|
|
78
|
+
*/
|
|
79
|
+
export class OutputFilterService {
|
|
80
|
+
private readonly patterns: readonly KeyPattern[];
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Creates a new OutputFilterService.
|
|
84
|
+
* @param customPatterns - Optional additional patterns to detect (appended to defaults)
|
|
85
|
+
*/
|
|
86
|
+
constructor(customPatterns?: KeyPattern[]) {
|
|
87
|
+
this.patterns = customPatterns
|
|
88
|
+
? [...API_KEY_PATTERNS, ...customPatterns]
|
|
89
|
+
: API_KEY_PATTERNS;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Scans text for API keys and returns detailed results.
|
|
94
|
+
*
|
|
95
|
+
* @param text - Text to scan for API keys
|
|
96
|
+
* @returns Scan result with detection info and redacted text
|
|
97
|
+
*/
|
|
98
|
+
scan(text: string): ScanResult {
|
|
99
|
+
if (!text) {
|
|
100
|
+
return { detected: false, count: 0, matchedPatterns: [], redactedText: text };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let redacted = text;
|
|
104
|
+
let totalCount = 0;
|
|
105
|
+
const matchedLabels: Set<string> = new Set();
|
|
106
|
+
|
|
107
|
+
for (const { pattern, label } of this.patterns) {
|
|
108
|
+
// Reset regex lastIndex for global patterns
|
|
109
|
+
const regex = new RegExp(pattern.source, pattern.flags);
|
|
110
|
+
const matches = redacted.match(regex);
|
|
111
|
+
if (matches) {
|
|
112
|
+
totalCount += matches.length;
|
|
113
|
+
matchedLabels.add(label);
|
|
114
|
+
redacted = redacted.replace(regex, REDACTION_PLACEHOLDER);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
detected: totalCount > 0,
|
|
120
|
+
count: totalCount,
|
|
121
|
+
matchedPatterns: Array.from(matchedLabels),
|
|
122
|
+
redactedText: redacted,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Redacts all API keys in the given text.
|
|
128
|
+
* Convenience method that returns only the redacted string.
|
|
129
|
+
*
|
|
130
|
+
* @param text - Text to redact
|
|
131
|
+
* @returns Text with all detected API keys replaced with [REDACTED]
|
|
132
|
+
*/
|
|
133
|
+
redact(text: string): string {
|
|
134
|
+
return this.scan(text).redactedText;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Checks if text contains any API keys without modifying it.
|
|
139
|
+
*
|
|
140
|
+
* @param text - Text to check
|
|
141
|
+
* @returns True if any API key patterns are detected
|
|
142
|
+
*/
|
|
143
|
+
containsKeys(text: string): boolean {
|
|
144
|
+
if (!text) return false;
|
|
145
|
+
for (const { pattern } of this.patterns) {
|
|
146
|
+
const regex = new RegExp(pattern.source, pattern.flags);
|
|
147
|
+
if (regex.test(text)) return true;
|
|
148
|
+
}
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Redacts API keys from a structured object (recursively scans string values).
|
|
154
|
+
* Useful for sanitizing tool call arguments and results before logging.
|
|
155
|
+
*
|
|
156
|
+
* @param obj - Object to scan and redact
|
|
157
|
+
* @returns Deep copy with all string values redacted
|
|
158
|
+
*/
|
|
159
|
+
redactObject(obj: unknown): unknown {
|
|
160
|
+
if (typeof obj === 'string') {
|
|
161
|
+
return this.redact(obj);
|
|
162
|
+
}
|
|
163
|
+
if (Array.isArray(obj)) {
|
|
164
|
+
return obj.map((item) => this.redactObject(item));
|
|
165
|
+
}
|
|
166
|
+
if (typeof obj === 'object' && obj !== null) {
|
|
167
|
+
const result: Record<string, unknown> = {};
|
|
168
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
169
|
+
result[key] = this.redactObject(value);
|
|
170
|
+
}
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
173
|
+
return obj;
|
|
174
|
+
}
|
|
175
|
+
}
|