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,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Worker Process
|
|
3
|
+
*
|
|
4
|
+
* Standalone Node.js child process that wraps AgentRunnerService for
|
|
5
|
+
* isolated agent execution. Communicates with the parent process via
|
|
6
|
+
* IPC messages (Node.js child_process.fork() protocol).
|
|
7
|
+
*
|
|
8
|
+
* This enables hot-reload of agent code without restarting the main
|
|
9
|
+
* backend process, and crash isolation so a worker failure doesn't
|
|
10
|
+
* bring down the server.
|
|
11
|
+
*
|
|
12
|
+
* @module services/agent/crewly-agent/agent-worker
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { AgentRunnerService } from './agent-runner.service.js';
|
|
16
|
+
import type {
|
|
17
|
+
CrewlyAgentConfig,
|
|
18
|
+
AgentRunResult,
|
|
19
|
+
StreamingEventCallbacks,
|
|
20
|
+
} from './types.js';
|
|
21
|
+
|
|
22
|
+
// ===== IPC Message Types =====
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Messages sent from the parent process to this worker.
|
|
26
|
+
*/
|
|
27
|
+
export type ParentMessage =
|
|
28
|
+
| { type: 'init'; config: CrewlyAgentConfig }
|
|
29
|
+
| { type: 'run'; message: string; conversationId?: string; metadata?: Record<string, string> }
|
|
30
|
+
| { type: 'abort' }
|
|
31
|
+
| { type: 'get-state' }
|
|
32
|
+
| { type: 'shutdown' };
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Messages sent from this worker to the parent process.
|
|
36
|
+
*/
|
|
37
|
+
export type WorkerMessage =
|
|
38
|
+
| { type: 'ready' }
|
|
39
|
+
| { type: 'result'; data: AgentRunResult }
|
|
40
|
+
| { type: 'error'; error: string; code?: string }
|
|
41
|
+
| { type: 'log'; level: 'debug' | 'info' | 'warn' | 'error'; message: string }
|
|
42
|
+
| { type: 'stream'; event: 'text'; data: { chunk: string } }
|
|
43
|
+
| { type: 'stream'; event: 'toolStart'; data: { toolName: string; args: Record<string, unknown> } }
|
|
44
|
+
| { type: 'stream'; event: 'toolFinish'; data: { toolName: string; args: Record<string, unknown>; result: unknown; durationMs: number } }
|
|
45
|
+
| { type: 'stream'; event: 'stepFinish'; data: { stepIndex: number; hasToolCalls: boolean } }
|
|
46
|
+
| { type: 'state'; data: { historyLength: number; isProcessing: boolean; isInitialized: boolean } };
|
|
47
|
+
|
|
48
|
+
// ===== Worker State =====
|
|
49
|
+
|
|
50
|
+
let runner: AgentRunnerService | null = null;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Send a typed message to the parent process.
|
|
54
|
+
*
|
|
55
|
+
* @param msg - Worker message to send
|
|
56
|
+
*/
|
|
57
|
+
function send(msg: WorkerMessage): void {
|
|
58
|
+
if (process.send) {
|
|
59
|
+
process.send(msg);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Build streaming callbacks that forward events to the parent via IPC.
|
|
65
|
+
*
|
|
66
|
+
* @returns StreamingEventCallbacks that emit IPC messages
|
|
67
|
+
*/
|
|
68
|
+
function buildStreamingCallbacks(): StreamingEventCallbacks {
|
|
69
|
+
return {
|
|
70
|
+
onTextChunk: (chunk: string) => {
|
|
71
|
+
send({ type: 'stream', event: 'text', data: { chunk } });
|
|
72
|
+
},
|
|
73
|
+
onToolCallStart: (toolName: string, args: Record<string, unknown>) => {
|
|
74
|
+
send({ type: 'stream', event: 'toolStart', data: { toolName, args } });
|
|
75
|
+
},
|
|
76
|
+
onToolCallFinish: (toolName: string, args: Record<string, unknown>, result: unknown, durationMs: number) => {
|
|
77
|
+
send({ type: 'stream', event: 'toolFinish', data: { toolName, args, result, durationMs } });
|
|
78
|
+
},
|
|
79
|
+
onStepFinish: (stepIndex: number, hasToolCalls: boolean) => {
|
|
80
|
+
send({ type: 'stream', event: 'stepFinish', data: { stepIndex, hasToolCalls } });
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** AbortController for the currently executing run */
|
|
86
|
+
let currentAbort: AbortController | null = null;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Handle incoming messages from the parent process.
|
|
90
|
+
*
|
|
91
|
+
* @param msg - Parent message received via IPC
|
|
92
|
+
*/
|
|
93
|
+
async function handleMessage(msg: ParentMessage): Promise<void> {
|
|
94
|
+
switch (msg.type) {
|
|
95
|
+
case 'init': {
|
|
96
|
+
try {
|
|
97
|
+
runner = new AgentRunnerService(msg.config);
|
|
98
|
+
await runner.initialize();
|
|
99
|
+
send({ type: 'ready' });
|
|
100
|
+
send({ type: 'log', level: 'info', message: `Worker initialized (${msg.config.model.provider}/${msg.config.model.modelId})` });
|
|
101
|
+
} catch (err) {
|
|
102
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
103
|
+
send({ type: 'error', error: `Init failed: ${errMsg}`, code: 'INIT_FAILED' });
|
|
104
|
+
runner = null;
|
|
105
|
+
}
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
case 'run': {
|
|
110
|
+
if (!runner || !runner.isInitialized()) {
|
|
111
|
+
send({ type: 'error', error: 'Worker not initialized', code: 'NOT_INITIALIZED' });
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
currentAbort = new AbortController();
|
|
116
|
+
const streamingCallbacks = buildStreamingCallbacks();
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const result = await runner.run(msg.message, msg.conversationId, msg.metadata, {
|
|
120
|
+
abortSignal: currentAbort.signal,
|
|
121
|
+
streaming: streamingCallbacks,
|
|
122
|
+
});
|
|
123
|
+
currentAbort = null;
|
|
124
|
+
send({ type: 'result', data: result });
|
|
125
|
+
} catch (err) {
|
|
126
|
+
currentAbort = null;
|
|
127
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
128
|
+
send({ type: 'error', error: errMsg, code: 'RUN_FAILED' });
|
|
129
|
+
}
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
case 'abort': {
|
|
134
|
+
if (currentAbort) {
|
|
135
|
+
currentAbort.abort();
|
|
136
|
+
currentAbort = null;
|
|
137
|
+
send({ type: 'log', level: 'warn', message: 'Run aborted by parent' });
|
|
138
|
+
}
|
|
139
|
+
if (runner) {
|
|
140
|
+
runner.abortCurrentRun();
|
|
141
|
+
}
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
case 'get-state': {
|
|
146
|
+
send({
|
|
147
|
+
type: 'state',
|
|
148
|
+
data: {
|
|
149
|
+
historyLength: runner ? runner.getHistoryLength() : 0,
|
|
150
|
+
isProcessing: runner ? runner.isProcessing() : false,
|
|
151
|
+
isInitialized: runner ? runner.isInitialized() : false,
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
case 'shutdown': {
|
|
158
|
+
send({ type: 'log', level: 'info', message: 'Worker shutting down' });
|
|
159
|
+
if (runner) {
|
|
160
|
+
await runner.shutdown();
|
|
161
|
+
runner = null;
|
|
162
|
+
}
|
|
163
|
+
// Give IPC a moment to flush, then exit
|
|
164
|
+
setTimeout(() => process.exit(0), 100);
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ===== Process Setup =====
|
|
171
|
+
|
|
172
|
+
// Listen for IPC messages from parent
|
|
173
|
+
process.on('message', (msg: ParentMessage) => {
|
|
174
|
+
handleMessage(msg).catch((err) => {
|
|
175
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
176
|
+
send({ type: 'error', error: `Unhandled worker error: ${errMsg}`, code: 'UNHANDLED' });
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Handle uncaught exceptions — report to parent before crashing
|
|
181
|
+
process.on('uncaughtException', (err) => {
|
|
182
|
+
send({ type: 'error', error: `Worker crash (uncaughtException): ${err.message}`, code: 'CRASH' });
|
|
183
|
+
setTimeout(() => process.exit(1), 100);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
process.on('unhandledRejection', (reason) => {
|
|
187
|
+
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
188
|
+
send({ type: 'error', error: `Worker crash (unhandledRejection): ${msg}`, code: 'CRASH' });
|
|
189
|
+
setTimeout(() => process.exit(1), 100);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// Signal ready to receive init
|
|
193
|
+
send({ type: 'log', level: 'info', message: 'Agent worker process started, awaiting init' });
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crewly Agent API Client
|
|
3
|
+
*
|
|
4
|
+
* Lightweight HTTP client for calling the Crewly backend REST API.
|
|
5
|
+
* Replaces the bash curl wrapper used by orchestrator skill scripts
|
|
6
|
+
* with direct fetch() calls for in-process tool execution.
|
|
7
|
+
*
|
|
8
|
+
* @module services/agent/crewly-agent/api-client
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { CREWLY_AGENT_DEFAULTS, type ApiCallResult } from './types.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* HTTP client for the Crewly backend REST API.
|
|
15
|
+
*
|
|
16
|
+
* Each tool in the ToolRegistry uses this client to make API calls
|
|
17
|
+
* instead of shelling out to bash scripts.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```typescript
|
|
21
|
+
* const client = new CrewlyApiClient('http://localhost:8787', 'crewly-orc');
|
|
22
|
+
* const result = await client.get('/teams');
|
|
23
|
+
* const result2 = await client.post('/terminal/agent-sam/deliver', { message: 'Hello' });
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export class CrewlyApiClient {
|
|
27
|
+
private baseUrl: string;
|
|
28
|
+
private sessionName: string;
|
|
29
|
+
private timeoutMs: number;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Create a new API client instance.
|
|
33
|
+
*
|
|
34
|
+
* @param baseUrl - Base URL for the Crewly API (e.g., 'http://localhost:8787')
|
|
35
|
+
* @param sessionName - Agent session name for the X-Agent-Session header
|
|
36
|
+
* @param timeoutMs - Request timeout in milliseconds
|
|
37
|
+
*/
|
|
38
|
+
constructor(
|
|
39
|
+
baseUrl: string = CREWLY_AGENT_DEFAULTS.API_BASE_URL,
|
|
40
|
+
sessionName: string = 'crewly-orc',
|
|
41
|
+
timeoutMs: number = CREWLY_AGENT_DEFAULTS.API_TIMEOUT_MS,
|
|
42
|
+
) {
|
|
43
|
+
this.baseUrl = baseUrl.replace(/\/$/, '');
|
|
44
|
+
this.sessionName = sessionName;
|
|
45
|
+
this.timeoutMs = timeoutMs;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Make a GET request to the Crewly API.
|
|
50
|
+
*
|
|
51
|
+
* @param endpoint - API endpoint path (e.g., '/teams')
|
|
52
|
+
* @returns API call result with parsed JSON data
|
|
53
|
+
*/
|
|
54
|
+
async get<T = unknown>(endpoint: string): Promise<ApiCallResult<T>> {
|
|
55
|
+
return this.request<T>('GET', endpoint);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Make a POST request to the Crewly API.
|
|
60
|
+
*
|
|
61
|
+
* @param endpoint - API endpoint path (e.g., '/terminal/agent-sam/deliver')
|
|
62
|
+
* @param body - Request body to send as JSON
|
|
63
|
+
* @returns API call result with parsed JSON data
|
|
64
|
+
*/
|
|
65
|
+
async post<T = unknown>(endpoint: string, body: unknown): Promise<ApiCallResult<T>> {
|
|
66
|
+
return this.request<T>('POST', endpoint, body);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Make a DELETE request to the Crewly API.
|
|
71
|
+
*
|
|
72
|
+
* @param endpoint - API endpoint path (e.g., '/schedule/check-123')
|
|
73
|
+
* @returns API call result with parsed JSON data
|
|
74
|
+
*/
|
|
75
|
+
async delete<T = unknown>(endpoint: string): Promise<ApiCallResult<T>> {
|
|
76
|
+
return this.request<T>('DELETE', endpoint);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Internal request method handling fetch, timeout, and error mapping.
|
|
81
|
+
*
|
|
82
|
+
* @param method - HTTP method
|
|
83
|
+
* @param endpoint - API endpoint path
|
|
84
|
+
* @param body - Optional request body
|
|
85
|
+
* @returns Parsed API call result
|
|
86
|
+
*/
|
|
87
|
+
private async request<T>(method: string, endpoint: string, body?: unknown): Promise<ApiCallResult<T>> {
|
|
88
|
+
const url = `${this.baseUrl}/api${endpoint}`;
|
|
89
|
+
const controller = new AbortController();
|
|
90
|
+
const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const headers: Record<string, string> = {
|
|
94
|
+
'Content-Type': 'application/json',
|
|
95
|
+
};
|
|
96
|
+
if (this.sessionName) {
|
|
97
|
+
headers['X-Agent-Session'] = this.sessionName;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const init: RequestInit = {
|
|
101
|
+
method,
|
|
102
|
+
headers,
|
|
103
|
+
signal: controller.signal,
|
|
104
|
+
};
|
|
105
|
+
if (body !== undefined) {
|
|
106
|
+
init.body = JSON.stringify(body);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const response = await fetch(url, init);
|
|
110
|
+
const responseBody = await response.text();
|
|
111
|
+
|
|
112
|
+
let data: T | undefined;
|
|
113
|
+
try {
|
|
114
|
+
const parsed = JSON.parse(responseBody);
|
|
115
|
+
data = parsed.data !== undefined ? parsed.data : parsed;
|
|
116
|
+
} catch {
|
|
117
|
+
data = responseBody as unknown as T;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (response.ok) {
|
|
121
|
+
return { success: true, data, status: response.status };
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
success: false,
|
|
125
|
+
error: typeof data === 'object' && data !== null && 'error' in data
|
|
126
|
+
? String((data as Record<string, unknown>).error)
|
|
127
|
+
: `HTTP ${response.status}`,
|
|
128
|
+
status: response.status,
|
|
129
|
+
};
|
|
130
|
+
} catch (error) {
|
|
131
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
132
|
+
return { success: false, error: `Request timeout after ${this.timeoutMs}ms`, status: 0 };
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
success: false,
|
|
136
|
+
error: error instanceof Error ? error.message : String(error),
|
|
137
|
+
status: 0,
|
|
138
|
+
};
|
|
139
|
+
} finally {
|
|
140
|
+
clearTimeout(timeout);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Approval Queue Service
|
|
3
|
+
*
|
|
4
|
+
* Manages pending tool approval requests for the Crewly Agent runtime.
|
|
5
|
+
* When a tool requires approval (based on SecurityPolicy.requireApproval),
|
|
6
|
+
* execution is blocked and a PendingApproval entry is created. External
|
|
7
|
+
* callers (API, auditor) can then approve or reject the request.
|
|
8
|
+
*
|
|
9
|
+
* @module services/agent/crewly-agent/approval-queue.service
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { ToolSensitivity } from './types.js';
|
|
13
|
+
|
|
14
|
+
/** Maximum number of pending approvals before oldest are auto-rejected */
|
|
15
|
+
const MAX_PENDING_APPROVALS = 100;
|
|
16
|
+
|
|
17
|
+
/** Default TTL for pending approvals in milliseconds (10 minutes) */
|
|
18
|
+
const DEFAULT_APPROVAL_TTL_MS = 10 * 60 * 1000;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Status of an approval request.
|
|
22
|
+
*/
|
|
23
|
+
export type ApprovalStatus = 'pending' | 'approved' | 'rejected' | 'expired';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A single pending approval entry.
|
|
27
|
+
*/
|
|
28
|
+
export interface PendingApproval {
|
|
29
|
+
/** Unique approval ID */
|
|
30
|
+
id: string;
|
|
31
|
+
/** Agent session that requested the tool call */
|
|
32
|
+
sessionName: string;
|
|
33
|
+
/** Name of the tool awaiting approval */
|
|
34
|
+
toolName: string;
|
|
35
|
+
/** Sensitivity classification of the tool */
|
|
36
|
+
sensitivity: ToolSensitivity;
|
|
37
|
+
/** Sanitized arguments passed to the tool */
|
|
38
|
+
args: Record<string, unknown>;
|
|
39
|
+
/** Current status */
|
|
40
|
+
status: ApprovalStatus;
|
|
41
|
+
/** ISO timestamp when the approval was requested */
|
|
42
|
+
requestedAt: string;
|
|
43
|
+
/** ISO timestamp when the approval was resolved (approved/rejected/expired) */
|
|
44
|
+
resolvedAt?: string;
|
|
45
|
+
/** Who resolved the approval (e.g. 'api', 'auditor', 'auto-expire') */
|
|
46
|
+
resolvedBy?: string;
|
|
47
|
+
/** Reason for rejection (if rejected) */
|
|
48
|
+
reason?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Result of resolving (approve/reject) an approval request.
|
|
53
|
+
*/
|
|
54
|
+
export interface ApprovalResolution {
|
|
55
|
+
/** Whether the resolution was successful */
|
|
56
|
+
success: boolean;
|
|
57
|
+
/** The resolved approval entry */
|
|
58
|
+
approval?: PendingApproval;
|
|
59
|
+
/** Error message if resolution failed */
|
|
60
|
+
error?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* In-memory approval queue for tool execution requests.
|
|
65
|
+
*
|
|
66
|
+
* Stores pending approvals and provides approve/reject operations.
|
|
67
|
+
* Approvals that exceed the TTL are automatically marked as expired
|
|
68
|
+
* when queried.
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```typescript
|
|
72
|
+
* const queue = new ApprovalQueueService();
|
|
73
|
+
* const approval = queue.enqueue('session-1', 'edit_file', 'destructive', { path: '/foo' });
|
|
74
|
+
* // Later, approve it:
|
|
75
|
+
* const result = queue.approve(approval.id, 'api');
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
export class ApprovalQueueService {
|
|
79
|
+
private static instance: ApprovalQueueService | null = null;
|
|
80
|
+
private approvals: Map<string, PendingApproval> = new Map();
|
|
81
|
+
private idCounter = 0;
|
|
82
|
+
private ttlMs: number;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Get the shared singleton instance.
|
|
86
|
+
* All agent-runners and API controllers share the same queue.
|
|
87
|
+
*
|
|
88
|
+
* @returns The shared ApprovalQueueService instance
|
|
89
|
+
*/
|
|
90
|
+
static getInstance(): ApprovalQueueService {
|
|
91
|
+
if (!ApprovalQueueService.instance) {
|
|
92
|
+
ApprovalQueueService.instance = new ApprovalQueueService();
|
|
93
|
+
}
|
|
94
|
+
return ApprovalQueueService.instance;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Reset the singleton instance (for testing only).
|
|
99
|
+
*/
|
|
100
|
+
static resetInstance(): void {
|
|
101
|
+
ApprovalQueueService.instance = null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Create a new ApprovalQueueService.
|
|
106
|
+
*
|
|
107
|
+
* @param ttlMs - Time-to-live for pending approvals in milliseconds
|
|
108
|
+
*/
|
|
109
|
+
constructor(ttlMs: number = DEFAULT_APPROVAL_TTL_MS) {
|
|
110
|
+
this.ttlMs = ttlMs;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Add a new approval request to the queue.
|
|
115
|
+
*
|
|
116
|
+
* @param sessionName - Agent session requesting approval
|
|
117
|
+
* @param toolName - Name of the tool
|
|
118
|
+
* @param sensitivity - Sensitivity classification
|
|
119
|
+
* @param args - Sanitized tool arguments
|
|
120
|
+
* @returns The created PendingApproval entry
|
|
121
|
+
*/
|
|
122
|
+
enqueue(
|
|
123
|
+
sessionName: string,
|
|
124
|
+
toolName: string,
|
|
125
|
+
sensitivity: ToolSensitivity,
|
|
126
|
+
args: Record<string, unknown>,
|
|
127
|
+
): PendingApproval {
|
|
128
|
+
this.expireStale();
|
|
129
|
+
|
|
130
|
+
// Enforce max pending limit — reject oldest if full
|
|
131
|
+
if (this.getPendingCount() >= MAX_PENDING_APPROVALS) {
|
|
132
|
+
const oldest = this.getOldestPending();
|
|
133
|
+
if (oldest) {
|
|
134
|
+
oldest.status = 'rejected';
|
|
135
|
+
oldest.resolvedAt = new Date().toISOString();
|
|
136
|
+
oldest.resolvedBy = 'auto-overflow';
|
|
137
|
+
oldest.reason = 'Approval queue overflow — auto-rejected oldest entry';
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
this.idCounter++;
|
|
142
|
+
const id = `approval-${this.idCounter}-${Date.now()}`;
|
|
143
|
+
|
|
144
|
+
const approval: PendingApproval = {
|
|
145
|
+
id,
|
|
146
|
+
sessionName,
|
|
147
|
+
toolName,
|
|
148
|
+
sensitivity,
|
|
149
|
+
args,
|
|
150
|
+
status: 'pending',
|
|
151
|
+
requestedAt: new Date().toISOString(),
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
this.approvals.set(id, approval);
|
|
155
|
+
return approval;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Approve a pending approval request.
|
|
160
|
+
*
|
|
161
|
+
* @param id - Approval ID to approve
|
|
162
|
+
* @param resolvedBy - Who is approving (e.g. 'api', 'auditor')
|
|
163
|
+
* @returns Resolution result
|
|
164
|
+
*/
|
|
165
|
+
approve(id: string, resolvedBy: string = 'api'): ApprovalResolution {
|
|
166
|
+
return this.resolve(id, 'approved', resolvedBy);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Reject a pending approval request.
|
|
171
|
+
*
|
|
172
|
+
* @param id - Approval ID to reject
|
|
173
|
+
* @param resolvedBy - Who is rejecting
|
|
174
|
+
* @param reason - Reason for rejection
|
|
175
|
+
* @returns Resolution result
|
|
176
|
+
*/
|
|
177
|
+
reject(id: string, resolvedBy: string = 'api', reason?: string): ApprovalResolution {
|
|
178
|
+
return this.resolve(id, 'rejected', resolvedBy, reason);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Get all pending approval requests.
|
|
183
|
+
*
|
|
184
|
+
* @param sessionName - Optional filter by session name
|
|
185
|
+
* @returns Array of pending approvals
|
|
186
|
+
*/
|
|
187
|
+
getPending(sessionName?: string): PendingApproval[] {
|
|
188
|
+
this.expireStale();
|
|
189
|
+
const pending: PendingApproval[] = [];
|
|
190
|
+
for (const approval of this.approvals.values()) {
|
|
191
|
+
if (approval.status !== 'pending') continue;
|
|
192
|
+
if (sessionName && approval.sessionName !== sessionName) continue;
|
|
193
|
+
pending.push({ ...approval });
|
|
194
|
+
}
|
|
195
|
+
return pending;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Get a specific approval by ID.
|
|
200
|
+
*
|
|
201
|
+
* @param id - Approval ID
|
|
202
|
+
* @returns The approval entry or undefined
|
|
203
|
+
*/
|
|
204
|
+
getById(id: string): PendingApproval | undefined {
|
|
205
|
+
const approval = this.approvals.get(id);
|
|
206
|
+
if (!approval) return undefined;
|
|
207
|
+
return { ...approval };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Get the count of currently pending approvals.
|
|
212
|
+
*
|
|
213
|
+
* @returns Number of pending approvals
|
|
214
|
+
*/
|
|
215
|
+
getPendingCount(): number {
|
|
216
|
+
let count = 0;
|
|
217
|
+
for (const approval of this.approvals.values()) {
|
|
218
|
+
if (approval.status === 'pending') count++;
|
|
219
|
+
}
|
|
220
|
+
return count;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Clear all approvals (for testing or reset).
|
|
225
|
+
*/
|
|
226
|
+
clear(): void {
|
|
227
|
+
this.approvals.clear();
|
|
228
|
+
this.idCounter = 0;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Resolve a pending approval with the given status.
|
|
233
|
+
*
|
|
234
|
+
* @param id - Approval ID
|
|
235
|
+
* @param status - New status
|
|
236
|
+
* @param resolvedBy - Who resolved it
|
|
237
|
+
* @param reason - Optional reason (for rejections)
|
|
238
|
+
* @returns Resolution result
|
|
239
|
+
*/
|
|
240
|
+
private resolve(
|
|
241
|
+
id: string,
|
|
242
|
+
status: 'approved' | 'rejected',
|
|
243
|
+
resolvedBy: string,
|
|
244
|
+
reason?: string,
|
|
245
|
+
): ApprovalResolution {
|
|
246
|
+
const approval = this.approvals.get(id);
|
|
247
|
+
if (!approval) {
|
|
248
|
+
return { success: false, error: `Approval '${id}' not found` };
|
|
249
|
+
}
|
|
250
|
+
if (approval.status !== 'pending') {
|
|
251
|
+
return {
|
|
252
|
+
success: false,
|
|
253
|
+
error: `Approval '${id}' is already ${approval.status}`,
|
|
254
|
+
approval: { ...approval },
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
approval.status = status;
|
|
259
|
+
approval.resolvedAt = new Date().toISOString();
|
|
260
|
+
approval.resolvedBy = resolvedBy;
|
|
261
|
+
if (reason) approval.reason = reason;
|
|
262
|
+
|
|
263
|
+
return { success: true, approval: { ...approval } };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Expire stale pending approvals that have exceeded the TTL.
|
|
268
|
+
*/
|
|
269
|
+
private expireStale(): void {
|
|
270
|
+
const now = Date.now();
|
|
271
|
+
const resolvedRetentionMs = this.ttlMs * 3; // Keep resolved entries 3x TTL then prune
|
|
272
|
+
|
|
273
|
+
for (const [id, approval] of this.approvals.entries()) {
|
|
274
|
+
if (approval.status === 'pending') {
|
|
275
|
+
const requestedAt = new Date(approval.requestedAt).getTime();
|
|
276
|
+
if (now - requestedAt > this.ttlMs) {
|
|
277
|
+
approval.status = 'expired';
|
|
278
|
+
approval.resolvedAt = new Date().toISOString();
|
|
279
|
+
approval.resolvedBy = 'auto-expire';
|
|
280
|
+
approval.reason = 'Approval request expired';
|
|
281
|
+
}
|
|
282
|
+
} else if (approval.resolvedAt) {
|
|
283
|
+
// Prune resolved entries that are older than retention window
|
|
284
|
+
const resolvedAt = new Date(approval.resolvedAt).getTime();
|
|
285
|
+
if (now - resolvedAt > resolvedRetentionMs) {
|
|
286
|
+
this.approvals.delete(id);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Get the oldest pending approval.
|
|
294
|
+
*
|
|
295
|
+
* @returns Oldest pending approval or undefined
|
|
296
|
+
*/
|
|
297
|
+
private getOldestPending(): PendingApproval | undefined {
|
|
298
|
+
let oldest: PendingApproval | undefined;
|
|
299
|
+
for (const approval of this.approvals.values()) {
|
|
300
|
+
if (approval.status !== 'pending') continue;
|
|
301
|
+
if (!oldest || approval.requestedAt < oldest.requestedAt) {
|
|
302
|
+
oldest = approval;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return oldest;
|
|
306
|
+
}
|
|
307
|
+
}
|