@margay/ccl-core 1.3.6-alpha.1 → 1.3.6-alpha.3
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.js +3242 -2499
- package/dist/sdk/errorDiagnostics.d.ts +14 -0
- package/dist/sdk/errorDiagnostics.js +86 -0
- package/dist/sdk/interactionResponseSchema.d.ts +16 -0
- package/dist/sdk/interactionResponseSchema.js +13 -0
- package/dist/sdk/query.d.ts +2 -0
- package/dist/sdk/query.js +30 -8
- package/dist/sdk/types.d.ts +6 -0
- package/package.json +1 -1
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare function sanitizeErrorText(text: string): string;
|
|
2
|
+
export type RecoveryOutcome = {
|
|
3
|
+
status: 'needs_user' | 'blocked_external';
|
|
4
|
+
reason: string;
|
|
5
|
+
attempts: number;
|
|
6
|
+
};
|
|
7
|
+
/** Optional wire extension. Omit invalid data; never spread arbitrary payloads. */
|
|
8
|
+
export declare function sanitizeRecoveryOutcome(value: unknown): RecoveryOutcome | undefined;
|
|
9
|
+
export declare function describeError(value: unknown): string;
|
|
10
|
+
export declare function errorDetailsList(errors: unknown): string[];
|
|
11
|
+
/** Preserve successful data and the wire shape; sanitize diagnostic fields only. */
|
|
12
|
+
export declare function sanitizeErrorEnvelope<T extends {
|
|
13
|
+
type: string;
|
|
14
|
+
}>(message: T): T;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/** Shared CLI/SDK host diagnostics. Never serialize arbitrary error/request objects. */
|
|
2
|
+
const MAX_DETAIL = 4096;
|
|
3
|
+
const MAX_SCAN = 65_536;
|
|
4
|
+
// Credential syntax redaction, NOT semantic diagnosis or retry classification.
|
|
5
|
+
export function sanitizeErrorText(text) {
|
|
6
|
+
return text.slice(0, MAX_SCAN)
|
|
7
|
+
.replace(/\b(?:Bearer|Basic)\s+[^\s,;"'<>]+/gi, '[REDACTED]')
|
|
8
|
+
.replace(/\bsk-[a-zA-Z0-9_-]+/g, '[REDACTED]')
|
|
9
|
+
.replace(/\beyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+/g, '[REDACTED]')
|
|
10
|
+
.replace(/((?:["']?)(?:api[_-]?key|access[_-]?token|refresh[_-]?token|id[_-]?token|token|password|secret|authorization|cookie)(?:["']?)\s*[:=]\s*)(?:"[^"\n]*"|'[^'\n]*'|[^\s&,;]+)/gi, '$1[REDACTED]')
|
|
11
|
+
.replace(/(https?:\/\/)[^\s/@]+:[^\s/@]+@/gi, '$1[REDACTED]@')
|
|
12
|
+
.slice(0, MAX_DETAIL);
|
|
13
|
+
}
|
|
14
|
+
function own(value, key) {
|
|
15
|
+
// Accessors/custom toString/proxies must not break the error handler itself.
|
|
16
|
+
try {
|
|
17
|
+
return Object.getOwnPropertyDescriptor(value, key)?.value;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** Optional wire extension. Omit invalid data; never spread arbitrary payloads. */
|
|
24
|
+
export function sanitizeRecoveryOutcome(value) {
|
|
25
|
+
if (!value || typeof value !== 'object')
|
|
26
|
+
return undefined;
|
|
27
|
+
const status = own(value, 'status'), reason = own(value, 'reason'), attempts = own(value, 'attempts');
|
|
28
|
+
if ((status !== 'needs_user' && status !== 'blocked_external') || typeof reason !== 'string' ||
|
|
29
|
+
!Number.isSafeInteger(attempts) || attempts < 0)
|
|
30
|
+
return undefined;
|
|
31
|
+
return { status, reason: sanitizeErrorText(reason), attempts: attempts };
|
|
32
|
+
}
|
|
33
|
+
export function describeError(value) {
|
|
34
|
+
const seen = new Set();
|
|
35
|
+
const parts = [];
|
|
36
|
+
function visit(node, depth) {
|
|
37
|
+
if (depth > 4 || parts.length >= 16)
|
|
38
|
+
return;
|
|
39
|
+
if (typeof node === 'string') {
|
|
40
|
+
if (node.trim())
|
|
41
|
+
parts.push(sanitizeErrorText(node));
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (typeof node === 'number' || typeof node === 'boolean') {
|
|
45
|
+
parts.push(String(node));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (!node || typeof node !== 'object' || seen.has(node))
|
|
49
|
+
return;
|
|
50
|
+
seen.add(node);
|
|
51
|
+
const message = own(node, 'message');
|
|
52
|
+
if (typeof message === 'string')
|
|
53
|
+
visit(message, depth + 1);
|
|
54
|
+
for (const key of ['code', 'status', 'statusCode', 'requestId', 'request_id']) {
|
|
55
|
+
const field = own(node, key);
|
|
56
|
+
if (typeof field === 'string' || (typeof field === 'number' && Number.isFinite(field))) {
|
|
57
|
+
parts.push(`${key}=${sanitizeErrorText(String(field))}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
visit(own(node, 'error'), depth + 1);
|
|
61
|
+
visit(own(node, 'cause'), depth + 1);
|
|
62
|
+
}
|
|
63
|
+
visit(value, 0);
|
|
64
|
+
return [...new Set(parts)].join('; ').slice(0, MAX_DETAIL) ||
|
|
65
|
+
'Error details were not supplied. Check the service logs for this request; no cause or automatic retry was inferred.';
|
|
66
|
+
}
|
|
67
|
+
export function errorDetailsList(errors) {
|
|
68
|
+
return Array.isArray(errors) ? errors.slice(0, 16).map(describeError) : [describeError(errors)];
|
|
69
|
+
}
|
|
70
|
+
/** Preserve successful data and the wire shape; sanitize diagnostic fields only. */
|
|
71
|
+
export function sanitizeErrorEnvelope(message) {
|
|
72
|
+
const wire = message;
|
|
73
|
+
if (wire.type === 'result' && wire.is_error === true) {
|
|
74
|
+
return {
|
|
75
|
+
...message,
|
|
76
|
+
...(wire.errors !== undefined ? { errors: errorDetailsList(wire.errors) } : {}),
|
|
77
|
+
...(typeof wire.result === 'string' ? { result: sanitizeErrorText(wire.result) } : {}),
|
|
78
|
+
...(wire.recovery !== undefined ? { recovery: sanitizeRecoveryOutcome(wire.recovery) } : {}),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const assistant = message;
|
|
82
|
+
if (assistant.type === 'assistant' && assistant.error !== undefined && Array.isArray(assistant.message?.content)) {
|
|
83
|
+
return { ...message, message: { ...assistant.message, content: assistant.message.content.map(block => block?.type === 'text' && typeof block.text === 'string' ? { ...block, text: sanitizeErrorText(block.text) } : block) } };
|
|
84
|
+
}
|
|
85
|
+
return message;
|
|
86
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { z } from 'zod/v4';
|
|
2
|
+
export declare const interactionResponseSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
3
|
+
outcome: z.ZodLiteral<"clarification_requested">;
|
|
4
|
+
intent: z.ZodEnum<{
|
|
5
|
+
clarify: "clarify";
|
|
6
|
+
finish_plan: "finish_plan";
|
|
7
|
+
}>;
|
|
8
|
+
answers: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
9
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
10
|
+
outcome: z.ZodLiteral<"cancelled">;
|
|
11
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
12
|
+
outcome: z.ZodLiteral<"interaction_unavailable">;
|
|
13
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
14
|
+
outcome: z.ZodLiteral<"timed_out">;
|
|
15
|
+
}, z.core.$strict>]>;
|
|
16
|
+
export type InteractionResponse = z.infer<typeof interactionResponseSchema>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { z } from 'zod/v4';
|
|
2
|
+
// Additive field on deny-shaped wire responses for older host compatibility.
|
|
3
|
+
// It never turns a permission denial into authorization to execute a tool.
|
|
4
|
+
export const interactionResponseSchema = z.discriminatedUnion('outcome', [
|
|
5
|
+
z.object({
|
|
6
|
+
outcome: z.literal('clarification_requested'),
|
|
7
|
+
intent: z.enum(['clarify', 'finish_plan']),
|
|
8
|
+
answers: z.record(z.string(), z.string()),
|
|
9
|
+
}).strict(),
|
|
10
|
+
z.object({ outcome: z.literal('cancelled') }).strict(),
|
|
11
|
+
z.object({ outcome: z.literal('interaction_unavailable') }).strict(),
|
|
12
|
+
z.object({ outcome: z.literal('timed_out') }).strict(),
|
|
13
|
+
]);
|
package/dist/sdk/query.d.ts
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
* the model re-generated. Deny works symmetrically. One-shot: the record
|
|
26
26
|
* is deleted after it is applied.
|
|
27
27
|
*/
|
|
28
|
+
import { type InteractionResponse } from './interactionResponseSchema.js';
|
|
28
29
|
import type { CanUseToolFn, PendingDecision, QueryOptions, SDKEvent, SessionStore, Transport } from './types.js';
|
|
29
30
|
/** Builds the CLI argv for a query. Exported for unit tests. */
|
|
30
31
|
export declare function buildCliArgs(options: QueryOptions): string[];
|
|
@@ -43,6 +44,7 @@ type PermissionDecisionWire = {
|
|
|
43
44
|
} | {
|
|
44
45
|
behavior: 'deny';
|
|
45
46
|
message: string;
|
|
47
|
+
interaction?: InteractionResponse;
|
|
46
48
|
interrupt?: boolean;
|
|
47
49
|
toolUseID: string;
|
|
48
50
|
};
|
package/dist/sdk/query.js
CHANGED
|
@@ -26,6 +26,8 @@
|
|
|
26
26
|
* is deleted after it is applied.
|
|
27
27
|
*/
|
|
28
28
|
import { existsSync, mkdtempSync, symlinkSync } from 'fs';
|
|
29
|
+
import { describeError, errorDetailsList, sanitizeErrorText, sanitizeRecoveryOutcome } from './errorDiagnostics.js';
|
|
30
|
+
import { interactionResponseSchema } from './interactionResponseSchema.js';
|
|
29
31
|
import { tmpdir } from 'os';
|
|
30
32
|
import { dirname, join, resolve } from 'path';
|
|
31
33
|
import { fileURLToPath } from 'url';
|
|
@@ -147,9 +149,12 @@ function resolveCliPath(options) {
|
|
|
147
149
|
* tool does not execute. `pending` is surfaced to the caller for suspend.
|
|
148
150
|
*/
|
|
149
151
|
export async function decideFailClosed(callback, toolName, input, context, timeoutMs) {
|
|
152
|
+
let timedOut = false;
|
|
150
153
|
const deny = (message, failClosedReason) => ({
|
|
151
154
|
kind: 'decision',
|
|
152
|
-
decision: { behavior: 'deny', message, toolUseID: context.toolUseId
|
|
155
|
+
decision: { behavior: 'deny', message, toolUseID: context.toolUseId,
|
|
156
|
+
...(toolName === 'AskUserQuestion' ? { interaction: { outcome: timedOut ? 'timed_out' : 'interaction_unavailable' } } : {}),
|
|
157
|
+
},
|
|
153
158
|
failClosedReason,
|
|
154
159
|
});
|
|
155
160
|
let raw;
|
|
@@ -161,13 +166,13 @@ export async function decideFailClosed(callback, toolName, input, context, timeo
|
|
|
161
166
|
? await Promise.race([
|
|
162
167
|
callbackPromise,
|
|
163
168
|
new Promise((_, reject) => {
|
|
164
|
-
timer = setTimeout(() => reject(new Error(`canUseTool timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
169
|
+
timer = setTimeout(() => { timedOut = true; reject(new Error(`canUseTool timed out after ${timeoutMs}ms`)); }, timeoutMs);
|
|
165
170
|
}),
|
|
166
171
|
])
|
|
167
172
|
: await callbackPromise;
|
|
168
173
|
}
|
|
169
174
|
catch (error) {
|
|
170
|
-
return deny(`
|
|
175
|
+
return deny(`Permission callback failed; no user rejection or permission was inferred: ${describeError(error)}`, 'callback_error');
|
|
171
176
|
}
|
|
172
177
|
finally {
|
|
173
178
|
if (timer)
|
|
@@ -190,16 +195,22 @@ export async function decideFailClosed(callback, toolName, input, context, timeo
|
|
|
190
195
|
},
|
|
191
196
|
};
|
|
192
197
|
}
|
|
193
|
-
case 'deny':
|
|
198
|
+
case 'deny': {
|
|
199
|
+
const interaction = interactionResponseSchema.safeParse(raw.interaction);
|
|
200
|
+
if (raw.interaction !== undefined && !interaction.success) {
|
|
201
|
+
return deny('Interaction response was invalid; no user choice was inferred.', 'unrecognized_result');
|
|
202
|
+
}
|
|
194
203
|
return {
|
|
195
204
|
kind: 'decision',
|
|
196
205
|
decision: {
|
|
197
206
|
behavior: 'deny',
|
|
198
|
-
message: raw.message ?? 'Denied by host',
|
|
207
|
+
message: sanitizeErrorText(raw.message ?? 'Denied by host'),
|
|
208
|
+
...(toolName === 'AskUserQuestion' && interaction.success ? { interaction: interaction.data } : {}),
|
|
199
209
|
...(raw.interrupt !== undefined && { interrupt: raw.interrupt }),
|
|
200
210
|
toolUseID: context.toolUseId,
|
|
201
211
|
},
|
|
202
212
|
};
|
|
213
|
+
}
|
|
203
214
|
case 'pending':
|
|
204
215
|
return { kind: 'pending' };
|
|
205
216
|
default:
|
|
@@ -355,10 +366,15 @@ export async function* queryWithTransport(prompt, options, transport) {
|
|
|
355
366
|
if (block.type === 'tool_result') {
|
|
356
367
|
const toolUseId = String(block.tool_use_id ?? '');
|
|
357
368
|
const isError = block.is_error === true;
|
|
369
|
+
const resultContent = isError
|
|
370
|
+
? typeof block.content === 'string' ? sanitizeErrorText(block.content)
|
|
371
|
+
: Array.isArray(block.content) ? block.content.map(part => part?.type === 'text' && typeof part.text === 'string' ? { ...part, text: sanitizeErrorText(part.text) } : part)
|
|
372
|
+
: describeError(block.content)
|
|
373
|
+
: block.content;
|
|
358
374
|
yield {
|
|
359
375
|
type: 'tool_result',
|
|
360
376
|
toolUseId,
|
|
361
|
-
content:
|
|
377
|
+
content: resultContent,
|
|
362
378
|
isError,
|
|
363
379
|
sessionId,
|
|
364
380
|
};
|
|
@@ -366,7 +382,7 @@ export async function* queryWithTransport(prompt, options, transport) {
|
|
|
366
382
|
(() => hooks.onToolUse({
|
|
367
383
|
toolUseId,
|
|
368
384
|
toolName: toolNamesById.get(toolUseId),
|
|
369
|
-
content:
|
|
385
|
+
content: resultContent,
|
|
370
386
|
isError,
|
|
371
387
|
sessionId,
|
|
372
388
|
})));
|
|
@@ -461,6 +477,7 @@ export async function* queryWithTransport(prompt, options, transport) {
|
|
|
461
477
|
behavior: 'deny',
|
|
462
478
|
message: 'Denied (fail-closed): no canUseTool callback configured',
|
|
463
479
|
toolUseID: toolUseId,
|
|
480
|
+
...(toolName === 'AskUserQuestion' ? { interaction: { outcome: 'interaction_unavailable' } } : {}),
|
|
464
481
|
},
|
|
465
482
|
failClosedReason: 'no_callback',
|
|
466
483
|
};
|
|
@@ -538,6 +555,7 @@ export async function* queryWithTransport(prompt, options, transport) {
|
|
|
538
555
|
requestId,
|
|
539
556
|
toolUseId,
|
|
540
557
|
behavior: outcome.decision.behavior,
|
|
558
|
+
...(outcome.decision.behavior === 'deny' && outcome.decision.interaction ? { interaction: outcome.decision.interaction } : {}),
|
|
541
559
|
source: 'callback',
|
|
542
560
|
...(outcome.failClosedReason && {
|
|
543
561
|
failClosedReason: outcome.failClosedReason,
|
|
@@ -587,8 +605,12 @@ export async function* queryWithTransport(prompt, options, transport) {
|
|
|
587
605
|
? message.duration_ms
|
|
588
606
|
: undefined,
|
|
589
607
|
isError: message.is_error === true,
|
|
608
|
+
...(message.errors !== undefined ? { errors: errorDetailsList(message.errors) } : {}),
|
|
609
|
+
...(message.recovery !== undefined ? { recovery: sanitizeRecoveryOutcome(message.recovery) } : {}),
|
|
590
610
|
sessionId,
|
|
591
|
-
result: typeof message.result === 'string'
|
|
611
|
+
result: typeof message.result === 'string'
|
|
612
|
+
? message.is_error === true ? sanitizeErrorText(message.result) : message.result
|
|
613
|
+
: undefined,
|
|
592
614
|
// slice sdk-json-schema-output (G4): surface the CLI's schema-validated structured
|
|
593
615
|
// output (present only when the query ran with jsonSchema).
|
|
594
616
|
structuredOutput: message
|
package/dist/sdk/types.d.ts
CHANGED
|
@@ -25,6 +25,8 @@ export type CanUseToolResult = {
|
|
|
25
25
|
} | {
|
|
26
26
|
behavior: 'deny';
|
|
27
27
|
message?: string;
|
|
28
|
+
/** AskUserQuestion only; missing field preserves legacy deny behavior. */
|
|
29
|
+
interaction?: import('./interactionResponseSchema.js').InteractionResponse;
|
|
28
30
|
/** Also interrupt the current turn instead of letting the agent continue. */
|
|
29
31
|
interrupt?: boolean;
|
|
30
32
|
} | {
|
|
@@ -304,6 +306,7 @@ export type SDKEvent = {
|
|
|
304
306
|
behavior: 'allow' | 'deny';
|
|
305
307
|
/** Why the SDK decided this way when the host callback did not decide. */
|
|
306
308
|
failClosedReason?: string;
|
|
309
|
+
interaction?: import('./interactionResponseSchema.js').InteractionResponse;
|
|
307
310
|
/** 'pending_decision' when resolved by resumeWithDecision, not the live callback. */
|
|
308
311
|
source?: 'callback' | 'pending_decision';
|
|
309
312
|
} | {
|
|
@@ -324,6 +327,9 @@ export type SDKEvent = {
|
|
|
324
327
|
isError: boolean;
|
|
325
328
|
sessionId?: string;
|
|
326
329
|
result?: string;
|
|
330
|
+
/** Sanitized execution diagnostics; absent on older CLI result envelopes. */
|
|
331
|
+
errors?: string[];
|
|
332
|
+
recovery?: import('./errorDiagnostics.js').RecoveryOutcome;
|
|
327
333
|
/**
|
|
328
334
|
* slice sdk-json-schema-output (G4): the schema-validated structured output, present only
|
|
329
335
|
* when the query was run with `jsonSchema`. undefined otherwise. subtype
|
package/package.json
CHANGED