@agentuity/cli 0.1.24 → 0.1.26
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/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/cmd/ai/cadence/index.d.ts.map +1 -1
- package/dist/cmd/ai/cadence/index.js +8 -2
- package/dist/cmd/ai/cadence/index.js.map +1 -1
- package/dist/cmd/ai/index.d.ts.map +1 -1
- package/dist/cmd/ai/index.js +8 -1
- package/dist/cmd/ai/index.js.map +1 -1
- package/dist/cmd/build/patch/index.d.ts.map +1 -1
- package/dist/cmd/build/patch/index.js +4 -0
- package/dist/cmd/build/patch/index.js.map +1 -1
- package/dist/cmd/build/patch/otel-llm.d.ts +10 -0
- package/dist/cmd/build/patch/otel-llm.d.ts.map +1 -0
- package/dist/cmd/build/patch/otel-llm.js +374 -0
- package/dist/cmd/build/patch/otel-llm.js.map +1 -0
- package/dist/cmd/cloud/db/create.js +3 -3
- package/dist/cmd/cloud/db/create.js.map +1 -1
- package/dist/cmd/cloud/deploy.d.ts.map +1 -1
- package/dist/cmd/cloud/deploy.js +55 -2
- package/dist/cmd/cloud/deploy.js.map +1 -1
- package/dist/cmd/cloud/env/pull.d.ts.map +1 -1
- package/dist/cmd/cloud/env/pull.js +26 -17
- package/dist/cmd/cloud/env/pull.js.map +1 -1
- package/dist/cmd/cloud/eval-run/list.d.ts.map +1 -1
- package/dist/cmd/cloud/eval-run/list.js +5 -1
- package/dist/cmd/cloud/eval-run/list.js.map +1 -1
- package/dist/cmd/cloud/queue/dlq.d.ts.map +1 -1
- package/dist/cmd/cloud/queue/dlq.js.map +1 -1
- package/dist/cmd/cloud/sandbox/download.d.ts.map +1 -1
- package/dist/cmd/cloud/sandbox/download.js +8 -3
- package/dist/cmd/cloud/sandbox/download.js.map +1 -1
- package/dist/cmd/cloud/sandbox/snapshot/build.d.ts.map +1 -1
- package/dist/cmd/cloud/sandbox/snapshot/build.js +52 -35
- package/dist/cmd/cloud/sandbox/snapshot/build.js.map +1 -1
- package/dist/cmd/cloud/sandbox/upload.d.ts.map +1 -1
- package/dist/cmd/cloud/sandbox/upload.js +8 -1
- package/dist/cmd/cloud/sandbox/upload.js.map +1 -1
- package/dist/cmd/profile/create.js +2 -2
- package/dist/cmd/profile/create.js.map +1 -1
- package/dist/cmd/project/create.d.ts.map +1 -1
- package/dist/cmd/project/create.js +6 -3
- package/dist/cmd/project/create.js.map +1 -1
- package/dist/utils/deps.d.ts +8 -0
- package/dist/utils/deps.d.ts.map +1 -0
- package/dist/utils/deps.js +36 -0
- package/dist/utils/deps.js.map +1 -0
- package/package.json +6 -6
- package/src/cli.ts +1 -5
- package/src/cmd/ai/cadence/index.ts +8 -2
- package/src/cmd/ai/index.ts +8 -1
- package/src/cmd/build/patch/index.ts +4 -0
- package/src/cmd/build/patch/otel-llm.ts +421 -0
- package/src/cmd/cloud/db/create.ts +3 -3
- package/src/cmd/cloud/deploy.ts +77 -1
- package/src/cmd/cloud/env/pull.ts +29 -19
- package/src/cmd/cloud/eval-run/list.ts +5 -1
- package/src/cmd/cloud/queue/dlq.ts +11 -10
- package/src/cmd/cloud/sandbox/download.ts +9 -3
- package/src/cmd/cloud/sandbox/snapshot/build.ts +71 -44
- package/src/cmd/cloud/sandbox/upload.ts +9 -1
- package/src/cmd/profile/create.ts +2 -2
- package/src/cmd/project/create.ts +6 -3
- package/src/utils/deps.ts +54 -0
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build-time patches for OpenTelemetry LLM instrumentation.
|
|
3
|
+
*
|
|
4
|
+
* These patches wrap LLM SDK methods (OpenAI, Anthropic, etc.) with OTel spans
|
|
5
|
+
* at build time, since runtime instrumentation (traceloop) doesn't work with
|
|
6
|
+
* bundled code.
|
|
7
|
+
*/
|
|
8
|
+
import { type PatchModule } from './_util';
|
|
9
|
+
|
|
10
|
+
interface OtelPatchConfig {
|
|
11
|
+
provider: string;
|
|
12
|
+
className: string;
|
|
13
|
+
inputTokensField: string;
|
|
14
|
+
outputTokensField: string;
|
|
15
|
+
/** Field path for response ID (e.g., 'id' for OpenAI) */
|
|
16
|
+
responseIdField?: string;
|
|
17
|
+
/** Field path for finish reason (e.g., 'choices[0].finish_reason' for OpenAI) */
|
|
18
|
+
finishReasonPath?: string;
|
|
19
|
+
/** Field path for response content (e.g., 'choices[0].message.content' for OpenAI) */
|
|
20
|
+
responseContentPath?: string;
|
|
21
|
+
/** Field name in request for messages (e.g., 'messages' for OpenAI) */
|
|
22
|
+
requestMessagesField?: string;
|
|
23
|
+
/** Field path for streaming delta content (e.g., 'choices[0].delta.content' for OpenAI) */
|
|
24
|
+
streamDeltaContentPath?: string;
|
|
25
|
+
/** Field path for streaming finish reason (e.g., 'choices[0].finish_reason' for OpenAI) */
|
|
26
|
+
streamFinishReasonPath?: string;
|
|
27
|
+
/** Field path for streaming usage in final chunk (e.g., 'usage' for OpenAI with stream_options) */
|
|
28
|
+
streamUsagePath?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Generate the OTel wrapper code for LLM chat completions.
|
|
33
|
+
* This creates a span with GenAI semantic conventions.
|
|
34
|
+
*/
|
|
35
|
+
function generateChatCompletionsWrapper(config: OtelPatchConfig): string {
|
|
36
|
+
const {
|
|
37
|
+
provider,
|
|
38
|
+
className,
|
|
39
|
+
inputTokensField,
|
|
40
|
+
outputTokensField,
|
|
41
|
+
responseIdField = 'id',
|
|
42
|
+
finishReasonPath,
|
|
43
|
+
responseContentPath,
|
|
44
|
+
requestMessagesField = 'messages',
|
|
45
|
+
streamDeltaContentPath,
|
|
46
|
+
streamFinishReasonPath,
|
|
47
|
+
streamUsagePath = 'usage',
|
|
48
|
+
} = config;
|
|
49
|
+
|
|
50
|
+
// Generate code to extract nested field (e.g., 'choices[0].finish_reason')
|
|
51
|
+
const generateFieldAccess = (path: string | undefined, varName: string): string => {
|
|
52
|
+
if (!path) return 'undefined';
|
|
53
|
+
// Convert path like 'choices[0].finish_reason' to safe access
|
|
54
|
+
const parts = path.split('.');
|
|
55
|
+
let code = varName;
|
|
56
|
+
for (const part of parts) {
|
|
57
|
+
const match = part.match(/^(\w+)\[(\d+)\]$/);
|
|
58
|
+
if (match) {
|
|
59
|
+
code = `(${code}?.${match[1]}?.[${match[2]}])`;
|
|
60
|
+
} else {
|
|
61
|
+
code = `(${code}?.${part})`;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return code;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const finishReasonCode = generateFieldAccess(finishReasonPath, 'response');
|
|
68
|
+
const responseContentCode = generateFieldAccess(responseContentPath, 'response');
|
|
69
|
+
const streamDeltaCode = generateFieldAccess(streamDeltaContentPath, 'chunk');
|
|
70
|
+
const streamFinishCode = generateFieldAccess(streamFinishReasonPath, 'chunk');
|
|
71
|
+
const streamUsageCode = generateFieldAccess(streamUsagePath, 'chunk');
|
|
72
|
+
|
|
73
|
+
return `
|
|
74
|
+
import * as _otel_api from '@opentelemetry/api';
|
|
75
|
+
|
|
76
|
+
const _ATTR_GEN_AI_SYSTEM = 'gen_ai.system';
|
|
77
|
+
const _ATTR_GEN_AI_REQUEST_MODEL = 'gen_ai.request.model';
|
|
78
|
+
const _ATTR_GEN_AI_REQUEST_MAX_TOKENS = 'gen_ai.request.max_tokens';
|
|
79
|
+
const _ATTR_GEN_AI_REQUEST_TEMPERATURE = 'gen_ai.request.temperature';
|
|
80
|
+
const _ATTR_GEN_AI_REQUEST_TOP_P = 'gen_ai.request.top_p';
|
|
81
|
+
const _ATTR_GEN_AI_REQUEST_FREQUENCY_PENALTY = 'gen_ai.request.frequency_penalty';
|
|
82
|
+
const _ATTR_GEN_AI_REQUEST_PRESENCE_PENALTY = 'gen_ai.request.presence_penalty';
|
|
83
|
+
const _ATTR_GEN_AI_RESPONSE_MODEL = 'gen_ai.response.model';
|
|
84
|
+
const _ATTR_GEN_AI_RESPONSE_ID = 'gen_ai.response.id';
|
|
85
|
+
const _ATTR_GEN_AI_RESPONSE_FINISH_REASONS = 'gen_ai.response.finish_reasons';
|
|
86
|
+
const _ATTR_GEN_AI_USAGE_INPUT_TOKENS = 'gen_ai.usage.input_tokens';
|
|
87
|
+
const _ATTR_GEN_AI_USAGE_OUTPUT_TOKENS = 'gen_ai.usage.output_tokens';
|
|
88
|
+
const _ATTR_GEN_AI_OPERATION_NAME = 'gen_ai.operation.name';
|
|
89
|
+
const _ATTR_GEN_AI_REQUEST_MESSAGES = 'gen_ai.request.messages';
|
|
90
|
+
const _ATTR_GEN_AI_RESPONSE_TEXT = 'gen_ai.response.text';
|
|
91
|
+
|
|
92
|
+
const _otel_tracer = _otel_api.trace.getTracer('@agentuity/otel-llm', '1.0.0');
|
|
93
|
+
|
|
94
|
+
function _wrapAsyncIterator(iterator, span, inputTokensField, outputTokensField) {
|
|
95
|
+
let contentChunks = [];
|
|
96
|
+
let finishReason = null;
|
|
97
|
+
let usage = null;
|
|
98
|
+
let model = null;
|
|
99
|
+
let responseId = null;
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
[Symbol.asyncIterator]() {
|
|
103
|
+
return this;
|
|
104
|
+
},
|
|
105
|
+
async next() {
|
|
106
|
+
try {
|
|
107
|
+
const result = await iterator.next();
|
|
108
|
+
if (result.done) {
|
|
109
|
+
// Stream complete - finalize span
|
|
110
|
+
if (contentChunks.length > 0) {
|
|
111
|
+
span.setAttribute(_ATTR_GEN_AI_RESPONSE_TEXT, contentChunks.join(''));
|
|
112
|
+
}
|
|
113
|
+
if (finishReason) {
|
|
114
|
+
span.setAttribute(_ATTR_GEN_AI_RESPONSE_FINISH_REASONS, JSON.stringify([finishReason]));
|
|
115
|
+
}
|
|
116
|
+
if (model) {
|
|
117
|
+
span.setAttribute(_ATTR_GEN_AI_RESPONSE_MODEL, model);
|
|
118
|
+
}
|
|
119
|
+
if (responseId) {
|
|
120
|
+
span.setAttribute(_ATTR_GEN_AI_RESPONSE_ID, responseId);
|
|
121
|
+
}
|
|
122
|
+
if (usage) {
|
|
123
|
+
if (usage[inputTokensField] !== undefined) {
|
|
124
|
+
span.setAttribute(_ATTR_GEN_AI_USAGE_INPUT_TOKENS, usage[inputTokensField]);
|
|
125
|
+
}
|
|
126
|
+
if (usage[outputTokensField] !== undefined) {
|
|
127
|
+
span.setAttribute(_ATTR_GEN_AI_USAGE_OUTPUT_TOKENS, usage[outputTokensField]);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
span.setStatus({ code: _otel_api.SpanStatusCode.OK });
|
|
131
|
+
span.end();
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const chunk = result.value;
|
|
136
|
+
|
|
137
|
+
// Capture model and id from first chunk
|
|
138
|
+
if (chunk.model && !model) {
|
|
139
|
+
model = chunk.model;
|
|
140
|
+
}
|
|
141
|
+
if (chunk.id && !responseId) {
|
|
142
|
+
responseId = chunk.id;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Capture delta content
|
|
146
|
+
const deltaContent = ${streamDeltaCode};
|
|
147
|
+
if (deltaContent) {
|
|
148
|
+
contentChunks.push(deltaContent);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Capture finish reason
|
|
152
|
+
const chunkFinishReason = ${streamFinishCode};
|
|
153
|
+
if (chunkFinishReason) {
|
|
154
|
+
finishReason = chunkFinishReason;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Capture usage (usually in final chunk with stream_options)
|
|
158
|
+
const chunkUsage = ${streamUsageCode};
|
|
159
|
+
if (chunkUsage) {
|
|
160
|
+
usage = chunkUsage;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return result;
|
|
164
|
+
} catch (error) {
|
|
165
|
+
span.setStatus({ code: _otel_api.SpanStatusCode.ERROR, message: error?.message });
|
|
166
|
+
span.recordException(error);
|
|
167
|
+
span.end();
|
|
168
|
+
throw error;
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
async return(value) {
|
|
172
|
+
span.setStatus({ code: _otel_api.SpanStatusCode.OK });
|
|
173
|
+
span.end();
|
|
174
|
+
if (iterator.return) {
|
|
175
|
+
return iterator.return(value);
|
|
176
|
+
}
|
|
177
|
+
return { done: true, value };
|
|
178
|
+
},
|
|
179
|
+
async throw(error) {
|
|
180
|
+
span.setStatus({ code: _otel_api.SpanStatusCode.ERROR, message: error?.message });
|
|
181
|
+
span.recordException(error);
|
|
182
|
+
span.end();
|
|
183
|
+
if (iterator.throw) {
|
|
184
|
+
return iterator.throw(error);
|
|
185
|
+
}
|
|
186
|
+
throw error;
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function _wrapStream(stream, span, inputTokensField, outputTokensField) {
|
|
192
|
+
// Get the original iterator
|
|
193
|
+
const originalIterator = stream[Symbol.asyncIterator]();
|
|
194
|
+
const wrappedIterator = _wrapAsyncIterator(originalIterator, span, inputTokensField, outputTokensField);
|
|
195
|
+
|
|
196
|
+
// Return a proxy that wraps the async iterator but preserves other properties/methods
|
|
197
|
+
return new Proxy(stream, {
|
|
198
|
+
get(target, prop) {
|
|
199
|
+
if (prop === Symbol.asyncIterator) {
|
|
200
|
+
return () => wrappedIterator;
|
|
201
|
+
}
|
|
202
|
+
// Preserve other stream methods like tee(), toReadableStream(), etc.
|
|
203
|
+
const value = target[prop];
|
|
204
|
+
if (typeof value === 'function') {
|
|
205
|
+
return value.bind(target);
|
|
206
|
+
}
|
|
207
|
+
return value;
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Safely patch the class if it exists
|
|
213
|
+
let _original_create;
|
|
214
|
+
try {
|
|
215
|
+
if (typeof ${className} === 'undefined' || !${className}.prototype || typeof ${className}.prototype.create !== 'function') {
|
|
216
|
+
console.debug('[Agentuity OTel] Skipping patch: ${className}.prototype.create not found or not a function');
|
|
217
|
+
} else {
|
|
218
|
+
_original_create = ${className}.prototype.create;
|
|
219
|
+
${className}.prototype.create = _agentuity_otel_create;
|
|
220
|
+
}
|
|
221
|
+
} catch (e) {
|
|
222
|
+
console.debug('[Agentuity OTel] Failed to patch ${className}:', e?.message || e);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function _agentuity_otel_create(body, options) {
|
|
226
|
+
// If patching failed, _original_create won't be set - this shouldn't happen but handle gracefully
|
|
227
|
+
if (!_original_create) {
|
|
228
|
+
throw new Error('[Agentuity OTel] ${className}.prototype.create was not properly patched');
|
|
229
|
+
}
|
|
230
|
+
const attributes = {
|
|
231
|
+
[_ATTR_GEN_AI_SYSTEM]: '${provider}',
|
|
232
|
+
[_ATTR_GEN_AI_OPERATION_NAME]: 'chat',
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
if (body.model) {
|
|
236
|
+
attributes[_ATTR_GEN_AI_REQUEST_MODEL] = body.model;
|
|
237
|
+
}
|
|
238
|
+
if (body.max_tokens) {
|
|
239
|
+
attributes[_ATTR_GEN_AI_REQUEST_MAX_TOKENS] = body.max_tokens;
|
|
240
|
+
}
|
|
241
|
+
if (body.temperature !== undefined) {
|
|
242
|
+
attributes[_ATTR_GEN_AI_REQUEST_TEMPERATURE] = body.temperature;
|
|
243
|
+
}
|
|
244
|
+
if (body.top_p !== undefined) {
|
|
245
|
+
attributes[_ATTR_GEN_AI_REQUEST_TOP_P] = body.top_p;
|
|
246
|
+
}
|
|
247
|
+
if (body.frequency_penalty !== undefined) {
|
|
248
|
+
attributes[_ATTR_GEN_AI_REQUEST_FREQUENCY_PENALTY] = body.frequency_penalty;
|
|
249
|
+
}
|
|
250
|
+
if (body.presence_penalty !== undefined) {
|
|
251
|
+
attributes[_ATTR_GEN_AI_REQUEST_PRESENCE_PENALTY] = body.presence_penalty;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Capture request messages
|
|
255
|
+
if (body.${requestMessagesField} && Array.isArray(body.${requestMessagesField})) {
|
|
256
|
+
try {
|
|
257
|
+
attributes[_ATTR_GEN_AI_REQUEST_MESSAGES] = JSON.stringify(body.${requestMessagesField});
|
|
258
|
+
} catch (e) {
|
|
259
|
+
// Ignore serialization errors
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const spanName = body.model ? \`chat \${body.model}\` : 'chat';
|
|
264
|
+
|
|
265
|
+
return _otel_tracer.startActiveSpan(spanName, { attributes, kind: _otel_api.SpanKind.CLIENT }, (span) => {
|
|
266
|
+
let result;
|
|
267
|
+
try {
|
|
268
|
+
result = _original_create.call(this, body, options);
|
|
269
|
+
} catch (error) {
|
|
270
|
+
span.setStatus({ code: _otel_api.SpanStatusCode.ERROR, message: error?.message });
|
|
271
|
+
span.recordException(error);
|
|
272
|
+
span.end();
|
|
273
|
+
throw error;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Handle streaming responses
|
|
277
|
+
if (body.stream) {
|
|
278
|
+
// Result is a Promise that resolves to a Stream
|
|
279
|
+
if (result && typeof result.then === 'function') {
|
|
280
|
+
return result.then((stream) => {
|
|
281
|
+
try {
|
|
282
|
+
return _wrapStream(stream, span, '${inputTokensField}', '${outputTokensField}');
|
|
283
|
+
} catch (error) {
|
|
284
|
+
span.setStatus({ code: _otel_api.SpanStatusCode.ERROR, message: error?.message });
|
|
285
|
+
span.recordException(error);
|
|
286
|
+
span.end();
|
|
287
|
+
throw error;
|
|
288
|
+
}
|
|
289
|
+
}).catch((error) => {
|
|
290
|
+
span.setStatus({ code: _otel_api.SpanStatusCode.ERROR, message: error?.message });
|
|
291
|
+
span.recordException(error);
|
|
292
|
+
span.end();
|
|
293
|
+
throw error;
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
// Result is already a Stream - wrap in try/catch for synchronous failures
|
|
297
|
+
try {
|
|
298
|
+
return _wrapStream(result, span, '${inputTokensField}', '${outputTokensField}');
|
|
299
|
+
} catch (error) {
|
|
300
|
+
span.setStatus({ code: _otel_api.SpanStatusCode.ERROR, message: error?.message });
|
|
301
|
+
span.recordException(error);
|
|
302
|
+
span.end();
|
|
303
|
+
throw error;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Handle non-streaming responses
|
|
308
|
+
if (result && typeof result.then === 'function') {
|
|
309
|
+
return result.then((response) => {
|
|
310
|
+
if (response) {
|
|
311
|
+
if (response.model) {
|
|
312
|
+
span.setAttribute(_ATTR_GEN_AI_RESPONSE_MODEL, response.model);
|
|
313
|
+
}
|
|
314
|
+
if (response.${responseIdField}) {
|
|
315
|
+
span.setAttribute(_ATTR_GEN_AI_RESPONSE_ID, response.${responseIdField});
|
|
316
|
+
}
|
|
317
|
+
if (response.usage) {
|
|
318
|
+
if (response.usage.${inputTokensField} !== undefined) {
|
|
319
|
+
span.setAttribute(_ATTR_GEN_AI_USAGE_INPUT_TOKENS, response.usage.${inputTokensField});
|
|
320
|
+
}
|
|
321
|
+
if (response.usage.${outputTokensField} !== undefined) {
|
|
322
|
+
span.setAttribute(_ATTR_GEN_AI_USAGE_OUTPUT_TOKENS, response.usage.${outputTokensField});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
// Extract finish reason
|
|
326
|
+
const finishReason = ${finishReasonCode};
|
|
327
|
+
if (finishReason) {
|
|
328
|
+
span.setAttribute(_ATTR_GEN_AI_RESPONSE_FINISH_REASONS, JSON.stringify([finishReason]));
|
|
329
|
+
}
|
|
330
|
+
// Extract response content
|
|
331
|
+
const responseContent = ${responseContentCode};
|
|
332
|
+
if (responseContent) {
|
|
333
|
+
span.setAttribute(_ATTR_GEN_AI_RESPONSE_TEXT, responseContent);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
span.setStatus({ code: _otel_api.SpanStatusCode.OK });
|
|
337
|
+
span.end();
|
|
338
|
+
return response;
|
|
339
|
+
}).catch((error) => {
|
|
340
|
+
span.setStatus({ code: _otel_api.SpanStatusCode.ERROR, message: error?.message });
|
|
341
|
+
span.recordException(error);
|
|
342
|
+
span.end();
|
|
343
|
+
throw error;
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
span.end();
|
|
348
|
+
return result;
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
`;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export function generatePatches(): Map<string, PatchModule> {
|
|
355
|
+
const patches = new Map<string, PatchModule>();
|
|
356
|
+
|
|
357
|
+
// OpenAI Chat Completions - patch resources/chat/completions/completions.mjs
|
|
358
|
+
patches.set('openai:otel', {
|
|
359
|
+
module: 'openai',
|
|
360
|
+
filename: 'resources/chat/completions/completions',
|
|
361
|
+
body: {
|
|
362
|
+
after: generateChatCompletionsWrapper({
|
|
363
|
+
provider: 'openai',
|
|
364
|
+
className: 'Completions',
|
|
365
|
+
inputTokensField: 'prompt_tokens',
|
|
366
|
+
outputTokensField: 'completion_tokens',
|
|
367
|
+
responseIdField: 'id',
|
|
368
|
+
finishReasonPath: 'choices[0].finish_reason',
|
|
369
|
+
responseContentPath: 'choices[0].message.content',
|
|
370
|
+
requestMessagesField: 'messages',
|
|
371
|
+
streamDeltaContentPath: 'choices[0].delta.content',
|
|
372
|
+
streamFinishReasonPath: 'choices[0].finish_reason',
|
|
373
|
+
streamUsagePath: 'usage',
|
|
374
|
+
}),
|
|
375
|
+
},
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
// Anthropic Messages - patch resources/messages.mjs
|
|
379
|
+
patches.set('@anthropic-ai/sdk:otel', {
|
|
380
|
+
module: '@anthropic-ai/sdk',
|
|
381
|
+
filename: 'resources/messages',
|
|
382
|
+
body: {
|
|
383
|
+
after: generateChatCompletionsWrapper({
|
|
384
|
+
provider: 'anthropic',
|
|
385
|
+
className: 'Messages',
|
|
386
|
+
inputTokensField: 'input_tokens',
|
|
387
|
+
outputTokensField: 'output_tokens',
|
|
388
|
+
responseIdField: 'id',
|
|
389
|
+
finishReasonPath: 'stop_reason',
|
|
390
|
+
responseContentPath: 'content[0].text',
|
|
391
|
+
requestMessagesField: 'messages',
|
|
392
|
+
streamDeltaContentPath: 'delta.text',
|
|
393
|
+
streamFinishReasonPath: 'delta.stop_reason',
|
|
394
|
+
streamUsagePath: 'usage',
|
|
395
|
+
}),
|
|
396
|
+
},
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
// Groq Chat Completions - patch resources/chat/completions.mjs
|
|
400
|
+
patches.set('groq-sdk:otel', {
|
|
401
|
+
module: 'groq-sdk',
|
|
402
|
+
filename: 'resources/chat/completions',
|
|
403
|
+
body: {
|
|
404
|
+
after: generateChatCompletionsWrapper({
|
|
405
|
+
provider: 'groq',
|
|
406
|
+
className: 'Completions',
|
|
407
|
+
inputTokensField: 'prompt_tokens',
|
|
408
|
+
outputTokensField: 'completion_tokens',
|
|
409
|
+
responseIdField: 'id',
|
|
410
|
+
finishReasonPath: 'choices[0].finish_reason',
|
|
411
|
+
responseContentPath: 'choices[0].message.content',
|
|
412
|
+
requestMessagesField: 'messages',
|
|
413
|
+
streamDeltaContentPath: 'choices[0].delta.content',
|
|
414
|
+
streamFinishReasonPath: 'choices[0].finish_reason',
|
|
415
|
+
streamUsagePath: 'x_groq.usage',
|
|
416
|
+
}),
|
|
417
|
+
},
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
return patches;
|
|
421
|
+
}
|
|
@@ -16,10 +16,10 @@ export const createSubcommand = defineSubcommand({
|
|
|
16
16
|
idempotent: false,
|
|
17
17
|
requires: { auth: true, org: true, region: true },
|
|
18
18
|
examples: [
|
|
19
|
-
{ command: getCommand('cloud db create'), description: 'Create new
|
|
19
|
+
{ command: getCommand('cloud db create'), description: 'Create new database' },
|
|
20
20
|
{ command: getCommand('cloud db new'), description: 'Run new command' },
|
|
21
|
-
{ command: getCommand('cloud db create --name my-db'), description: 'Create new
|
|
22
|
-
{ command: getCommand('--dry-run cloud db create'), description: 'Create new
|
|
21
|
+
{ command: getCommand('cloud db create --name my-db'), description: 'Create new database' },
|
|
22
|
+
{ command: getCommand('--dry-run cloud db create'), description: 'Create new database' },
|
|
23
23
|
],
|
|
24
24
|
schema: {
|
|
25
25
|
options: z.object({
|
package/src/cmd/cloud/deploy.ts
CHANGED
|
@@ -33,12 +33,14 @@ import {
|
|
|
33
33
|
projectDeploymentUpdate,
|
|
34
34
|
projectDeploymentComplete,
|
|
35
35
|
projectDeploymentStatus,
|
|
36
|
+
projectDeploymentMalwareCheck,
|
|
36
37
|
validateResources,
|
|
37
38
|
type Deployment,
|
|
38
39
|
type BuildMetadata,
|
|
39
40
|
type DeploymentInstructions,
|
|
40
41
|
type DeploymentComplete,
|
|
41
42
|
type DeploymentStatusResult,
|
|
43
|
+
type MalwareCheckResult,
|
|
42
44
|
getAppBaseURL,
|
|
43
45
|
} from '@agentuity/server';
|
|
44
46
|
import {
|
|
@@ -51,11 +53,12 @@ import { zipDir } from '../../utils/zip';
|
|
|
51
53
|
import { encryptFIPSKEMDEMStream } from '../../crypto/box';
|
|
52
54
|
import { getCommand } from '../../command-prefix';
|
|
53
55
|
import * as domain from '../../domain';
|
|
54
|
-
import { ErrorCode } from '../../errors';
|
|
56
|
+
import { ErrorCode, getExitCode } from '../../errors';
|
|
55
57
|
import { typecheck } from '../build/typecheck';
|
|
56
58
|
import { BuildReportCollector, setGlobalCollector, clearGlobalCollector } from '../../build-report';
|
|
57
59
|
import { runForkedDeploy } from './deploy-fork';
|
|
58
60
|
import { validateAptDependencies } from '../../utils/apt-validator';
|
|
61
|
+
import { extractDependencies } from '../../utils/deps';
|
|
59
62
|
|
|
60
63
|
const DeploymentCancelledError = StructuredError(
|
|
61
64
|
'DeploymentCancelled',
|
|
@@ -167,6 +170,7 @@ export const deploySubcommand = createSubcommand({
|
|
|
167
170
|
let instructions: DeploymentInstructions | undefined;
|
|
168
171
|
let complete: DeploymentComplete | undefined;
|
|
169
172
|
let statusResult: DeploymentStatusResult | undefined;
|
|
173
|
+
let malwareCheckPromise: Promise<MalwareCheckResult | null> | undefined;
|
|
170
174
|
const logs: string[] = [];
|
|
171
175
|
|
|
172
176
|
const sdkKey = await loadProjectSDKKey(ctx.logger, ctx.projectDir);
|
|
@@ -326,6 +330,35 @@ export const deploySubcommand = createSubcommand({
|
|
|
326
330
|
}
|
|
327
331
|
}
|
|
328
332
|
|
|
333
|
+
// Start malware check async (runs in parallel with build)
|
|
334
|
+
if (deployment) {
|
|
335
|
+
malwareCheckPromise = (async () => {
|
|
336
|
+
try {
|
|
337
|
+
logger.debug('Starting malware dependency check');
|
|
338
|
+
const packages = await extractDependencies(projectDir, logger);
|
|
339
|
+
if (packages.length === 0) {
|
|
340
|
+
logger.debug('No packages to check for malware');
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
logger.debug('Checking %d packages for malware', packages.length);
|
|
344
|
+
const result = await projectDeploymentMalwareCheck(
|
|
345
|
+
apiClient,
|
|
346
|
+
deployment!.id,
|
|
347
|
+
packages
|
|
348
|
+
);
|
|
349
|
+
logger.debug(
|
|
350
|
+
'Malware check complete: action=%s, flagged=%d',
|
|
351
|
+
result.action,
|
|
352
|
+
result.summary.flagged
|
|
353
|
+
);
|
|
354
|
+
return result;
|
|
355
|
+
} catch (error) {
|
|
356
|
+
logger.warn('Malware check failed: %s', error);
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
})();
|
|
360
|
+
}
|
|
361
|
+
|
|
329
362
|
try {
|
|
330
363
|
await saveProjectDir(projectDir);
|
|
331
364
|
|
|
@@ -528,6 +561,49 @@ export const deploySubcommand = createSubcommand({
|
|
|
528
561
|
}
|
|
529
562
|
},
|
|
530
563
|
},
|
|
564
|
+
{
|
|
565
|
+
label: 'Security Scan',
|
|
566
|
+
run: async () => {
|
|
567
|
+
if (!malwareCheckPromise) {
|
|
568
|
+
return stepSkipped('malware check not started');
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const result = await malwareCheckPromise;
|
|
572
|
+
if (!result) {
|
|
573
|
+
return stepSkipped('malware check unavailable');
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (result.action === 'block' && result.findings.length > 0) {
|
|
577
|
+
if (opts.reportFile) {
|
|
578
|
+
for (const finding of result.findings) {
|
|
579
|
+
collector.addGeneralError(
|
|
580
|
+
'deploy',
|
|
581
|
+
`Malicious package: ${finding.name}@${finding.version} (${finding.reason})`
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
await collector.forceWrite();
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const packageList = result.findings
|
|
588
|
+
.map((f) => `• ${f.name}@${f.version} (${f.reason})`)
|
|
589
|
+
.join('\n');
|
|
590
|
+
|
|
591
|
+
// Pause step UI to cleanly render error box
|
|
592
|
+
pauseStepUI(true);
|
|
593
|
+
|
|
594
|
+
tui.newline();
|
|
595
|
+
tui.errorBox(
|
|
596
|
+
'Malicious Packages Detected',
|
|
597
|
+
`Your deployment was blocked because it contains known malicious packages:\n\n${packageList}\n\nRemove these packages from your project and try again.`
|
|
598
|
+
);
|
|
599
|
+
tui.newline();
|
|
600
|
+
|
|
601
|
+
process.exit(getExitCode(ErrorCode.MALWARE_DETECTED));
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
return stepSuccess([`Scanned ${result.summary.scanned} packages`]);
|
|
605
|
+
},
|
|
606
|
+
},
|
|
531
607
|
{
|
|
532
608
|
label: 'Encrypt and Upload Deployment',
|
|
533
609
|
run: async (stepCtx: StepContext) => {
|
|
@@ -56,6 +56,7 @@ export const pullSubcommand = createSubcommand({
|
|
|
56
56
|
|
|
57
57
|
let cloudEnv: Record<string, string>;
|
|
58
58
|
let scope: 'project' | 'org';
|
|
59
|
+
let cloudApiKey: string | undefined;
|
|
59
60
|
|
|
60
61
|
if (useOrgScope) {
|
|
61
62
|
// Organization scope
|
|
@@ -70,6 +71,7 @@ export const pullSubcommand = createSubcommand({
|
|
|
70
71
|
|
|
71
72
|
cloudEnv = { ...orgData.env, ...orgData.secrets };
|
|
72
73
|
scope = 'org';
|
|
74
|
+
cloudApiKey = undefined; // Orgs don't have api_key
|
|
73
75
|
} else {
|
|
74
76
|
// Project scope
|
|
75
77
|
if (!project) {
|
|
@@ -84,31 +86,16 @@ export const pullSubcommand = createSubcommand({
|
|
|
84
86
|
|
|
85
87
|
cloudEnv = { ...projectData.env, ...projectData.secrets };
|
|
86
88
|
scope = 'project';
|
|
87
|
-
|
|
88
|
-
// Write AGENTUITY_SDK_KEY to .env if present and missing locally (project scope only)
|
|
89
|
-
if (projectData.api_key) {
|
|
90
|
-
const dotEnvPath = join(projectDir, '.env');
|
|
91
|
-
const dotEnv = await readEnvFile(dotEnvPath);
|
|
92
|
-
|
|
93
|
-
if (!dotEnv.AGENTUITY_SDK_KEY) {
|
|
94
|
-
dotEnv.AGENTUITY_SDK_KEY = projectData.api_key;
|
|
95
|
-
await writeEnvFile(dotEnvPath, dotEnv, {
|
|
96
|
-
addComment: (key) => {
|
|
97
|
-
if (key === 'AGENTUITY_SDK_KEY') {
|
|
98
|
-
return 'AGENTUITY_SDK_KEY is a sensitive value and should not be committed to version control.';
|
|
99
|
-
}
|
|
100
|
-
return null;
|
|
101
|
-
},
|
|
102
|
-
});
|
|
103
|
-
tui.info(`Wrote AGENTUITY_SDK_KEY to ${dotEnvPath}`);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
89
|
+
cloudApiKey = projectData.api_key;
|
|
106
90
|
}
|
|
107
91
|
|
|
108
92
|
// Target file is always .env
|
|
109
93
|
const targetEnvPath = await findExistingEnvFile(projectDir);
|
|
110
94
|
const localEnv = await readEnvFile(targetEnvPath);
|
|
111
95
|
|
|
96
|
+
// Preserve local AGENTUITY_SDK_KEY before writing (since it will be skipped in the first write)
|
|
97
|
+
const localSdkKey = localEnv.AGENTUITY_SDK_KEY;
|
|
98
|
+
|
|
112
99
|
// Merge: cloud values override local if force=true, otherwise keep local
|
|
113
100
|
let mergedEnv: Record<string, string>;
|
|
114
101
|
if (opts?.force) {
|
|
@@ -124,6 +111,29 @@ export const pullSubcommand = createSubcommand({
|
|
|
124
111
|
skipKeys: Object.keys(mergedEnv).filter(isReservedAgentuityKey),
|
|
125
112
|
});
|
|
126
113
|
|
|
114
|
+
// Restore AGENTUITY_SDK_KEY to .env (cloud is source of truth, fallback to local)
|
|
115
|
+
// The key was removed by the write above since it's in skipKeys, so we need to restore it
|
|
116
|
+
const dotEnvPath = join(projectDir, '.env');
|
|
117
|
+
const dotEnv = await readEnvFile(dotEnvPath);
|
|
118
|
+
|
|
119
|
+
// Cloud is source of truth: use cloud api_key if available, otherwise fallback to local
|
|
120
|
+
// For org scope, only restore if local key exists (orgs don't have api_key)
|
|
121
|
+
const sdkKeyToWrite = cloudApiKey || localSdkKey;
|
|
122
|
+
if (sdkKeyToWrite) {
|
|
123
|
+
dotEnv.AGENTUITY_SDK_KEY = sdkKeyToWrite;
|
|
124
|
+
await writeEnvFile(dotEnvPath, dotEnv, {
|
|
125
|
+
addComment: (key) => {
|
|
126
|
+
if (key === 'AGENTUITY_SDK_KEY') {
|
|
127
|
+
return 'AGENTUITY_SDK_KEY is a sensitive value and should not be committed to version control.';
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
if (cloudApiKey && cloudApiKey !== localSdkKey) {
|
|
133
|
+
tui.info(`Wrote AGENTUITY_SDK_KEY to ${dotEnvPath}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
127
137
|
const count = Object.keys(cloudEnv).length;
|
|
128
138
|
const scopeLabel = useOrgScope ? 'organization' : 'project';
|
|
129
139
|
tui.success(
|
|
@@ -127,7 +127,11 @@ export const listSubcommand = createSubcommand({
|
|
|
127
127
|
Agent: r.agentIdentifier || '-',
|
|
128
128
|
Success: r.success ? '✓' : '✗',
|
|
129
129
|
Pending: r.pending ? '⏳' : '✓',
|
|
130
|
-
Reason: reason
|
|
130
|
+
Reason: reason
|
|
131
|
+
? reason.length > 30
|
|
132
|
+
? reason.substring(0, 27) + '...'
|
|
133
|
+
: reason
|
|
134
|
+
: '-',
|
|
131
135
|
Created: new Date(r.createdAt).toLocaleString(),
|
|
132
136
|
};
|
|
133
137
|
});
|
|
@@ -67,16 +67,17 @@ const listDlqSubcommand = createSubcommand({
|
|
|
67
67
|
tui.info('No messages in dead letter queue');
|
|
68
68
|
} else {
|
|
69
69
|
const tableData = result.messages.map((m: DeadLetterMessage) => {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
|
|
70
|
+
const timestamp =
|
|
71
|
+
m.moved_at ?? m.original_published_at ?? m.published_at ?? m.created_at;
|
|
72
|
+
return {
|
|
73
|
+
ID: m.id.substring(0, 8) + '...',
|
|
74
|
+
Offset: m.offset,
|
|
75
|
+
Reason: m.failure_reason?.substring(0, 30) || 'Unknown',
|
|
76
|
+
Attempts: m.delivery_attempts,
|
|
77
|
+
'Failed At': timestamp ? new Date(timestamp).toLocaleString() : 'N/A',
|
|
78
|
+
};
|
|
79
|
+
});
|
|
80
|
+
tui.table(tableData, ['ID', 'Offset', 'Reason', 'Attempts', 'Failed At']);
|
|
80
81
|
}
|
|
81
82
|
}
|
|
82
83
|
|