@nexrall/code-core 1.4.22 → 1.4.24
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/agent/agentTypes.d.ts +35 -2
- package/dist/agent/agentTypes.d.ts.map +1 -1
- package/dist/agent/agentTypes.js +241 -6
- package/dist/agent/loop.d.ts +56 -0
- package/dist/agent/loop.d.ts.map +1 -1
- package/dist/agent/loop.js +286 -24
- package/dist/agent/securityLint.d.ts +27 -0
- package/dist/agent/securityLint.d.ts.map +1 -0
- package/dist/agent/securityLint.js +195 -0
- package/dist/api/client.d.ts +12 -0
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +351 -77
- package/dist/auth/index.d.ts +21 -0
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +53 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/tools/executor.d.ts.map +1 -1
- package/dist/tools/executor.js +124 -18
- package/dist/types.d.ts +16 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/agent/loop.js
CHANGED
|
@@ -35,9 +35,14 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.VERIFY_CMD_RE = exports.WRITE_TOOL_NAMES = void 0;
|
|
37
37
|
exports.resolveMaxIterations = resolveMaxIterations;
|
|
38
|
+
exports.createLimiter = createLimiter;
|
|
39
|
+
exports.extractSubTaskText = extractSubTaskText;
|
|
40
|
+
exports.capSubTaskText = capSubTaskText;
|
|
41
|
+
exports.summariseSubTaskProgress = summariseSubTaskProgress;
|
|
38
42
|
exports.contextWindowFor = contextWindowFor;
|
|
39
43
|
exports.compactionThresholds = compactionThresholds;
|
|
40
44
|
exports.estimateBodyBytes = estimateBodyBytes;
|
|
45
|
+
exports.allowsTestOnlyWrite = allowsTestOnlyWrite;
|
|
41
46
|
exports.findSafeCutIndex = findSafeCutIndex;
|
|
42
47
|
exports.transcriptOf = transcriptOf;
|
|
43
48
|
exports.createLedger = createLedger;
|
|
@@ -241,6 +246,58 @@ async function withFileLock(absPath, fn) {
|
|
|
241
246
|
_fileLocks.delete(absPath);
|
|
242
247
|
}
|
|
243
248
|
}
|
|
249
|
+
// ─── Sub-agent fan-out limiter ────────────────────────────────────────────────
|
|
250
|
+
//
|
|
251
|
+
// Tool calls in one turn run via Promise.all with no ceiling. For ordinary tools
|
|
252
|
+
// that is right — they're cheap and mostly I/O — but a `task` call spawns a WHOLE
|
|
253
|
+
// nested agent loop: its own model stream, its own tool executions, its own
|
|
254
|
+
// sub-process spawns. A model that emits ten `task` blocks in one turn therefore
|
|
255
|
+
// starts ten concurrent agents, each billing tokens and competing for the same
|
|
256
|
+
// CPU, file handles and API rate limit. The practical symptoms are the ones users
|
|
257
|
+
// report as "it got slow and then stalled": every sub-agent's stream slows, some
|
|
258
|
+
// trip their own stall watchdog, and one shared rate limit is spread across ten
|
|
259
|
+
// callers.
|
|
260
|
+
//
|
|
261
|
+
// Anthropic hit the same wall and capped Claude Code's concurrent subagents at 20
|
|
262
|
+
// (v2.1.217, July 2026); community guidance settles far lower, around 3-5, because
|
|
263
|
+
// past that the synthesis overhead cancels the parallelism. We default to 4:
|
|
264
|
+
// enough for genuine fan-out (the case sub-agents exist for), low enough that a
|
|
265
|
+
// runaway `task` burst degrades into a queue instead of a thundering herd.
|
|
266
|
+
//
|
|
267
|
+
// This is a QUEUE, not a rejection: every sub-task still runs, just at most N at a
|
|
268
|
+
// time. Failing the excess would be worse than serialising it.
|
|
269
|
+
const MAX_CONCURRENT_SUBTASKS = (() => {
|
|
270
|
+
const raw = Number(process.env.NEXRALL_MAX_CONCURRENT_SUBTASKS);
|
|
271
|
+
return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 4;
|
|
272
|
+
})();
|
|
273
|
+
/**
|
|
274
|
+
* Minimal concurrency gate. Hand-rolled rather than pulling in `p-limit` because
|
|
275
|
+
* the CLI ships as a single esbuild bundle with no node_modules, and this is a
|
|
276
|
+
* dozen lines.
|
|
277
|
+
*/
|
|
278
|
+
function createLimiter(max) {
|
|
279
|
+
let active = 0;
|
|
280
|
+
const queue = [];
|
|
281
|
+
const release = () => {
|
|
282
|
+
active--;
|
|
283
|
+
queue.shift()?.();
|
|
284
|
+
};
|
|
285
|
+
return async (fn) => {
|
|
286
|
+
if (active >= max)
|
|
287
|
+
await new Promise((resolve) => queue.push(resolve));
|
|
288
|
+
active++;
|
|
289
|
+
try {
|
|
290
|
+
return await fn();
|
|
291
|
+
}
|
|
292
|
+
finally {
|
|
293
|
+
release();
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
// Process-wide, deliberately: the limit exists to protect shared resources (CPU,
|
|
298
|
+
// the API rate limit, file handles), and those are shared across every concurrent
|
|
299
|
+
// turn in this process, not just the tool calls of one message.
|
|
300
|
+
const _subTaskLimit = createLimiter(MAX_CONCURRENT_SUBTASKS);
|
|
244
301
|
// ─── Human-readable tool descriptions ────────────────────────────────────────
|
|
245
302
|
function humanDescription(name, input) {
|
|
246
303
|
switch (name) {
|
|
@@ -326,7 +383,15 @@ function humanDescription(name, input) {
|
|
|
326
383
|
}
|
|
327
384
|
}
|
|
328
385
|
// ─── Sub-task runner ──────────────────────────────────────────────────────────
|
|
329
|
-
|
|
386
|
+
// Deepest nesting level allowed to spawn a sub-agent. The main agent runs at
|
|
387
|
+
// depth 0 and the sub-agents it spawns at depth 1.
|
|
388
|
+
//
|
|
389
|
+
// This is 1, not 2. With 2 the guard below (`depth >= MAX_TASK_DEPTH`) let a
|
|
390
|
+
// depth-1 sub-agent spawn depth-2 grandchildren — contradicting both the constant's
|
|
391
|
+
// own comment and the system prompt's promise that "sub-agents cannot spawn further
|
|
392
|
+
// sub-agents", and quietly making the worst-case fan-out quadratic. The value and
|
|
393
|
+
// the documented behaviour now agree.
|
|
394
|
+
const MAX_TASK_DEPTH = 1;
|
|
330
395
|
let _subTaskCounter = 0; // unique per-process id → per-sub-agent todo scope
|
|
331
396
|
// A sub-agent that stalls (hung tool, model provider stuck, infinite tool-call
|
|
332
397
|
// loop bypassing the iteration budget somehow) used to have NO ceiling of its
|
|
@@ -338,13 +403,112 @@ let _subTaskCounter = 0; // unique per-process id → per-sub-agent todo scope
|
|
|
338
403
|
const SUBTASK_TIMEOUT_MS = Number(process.env.NEXRALL_SUBTASK_TIMEOUT_MS) > 0
|
|
339
404
|
? Number(process.env.NEXRALL_SUBTASK_TIMEOUT_MS)
|
|
340
405
|
: 10 * 60 * 1000; // 10 minutes, matching the general industry norm for a stalled-agent cutoff
|
|
406
|
+
/** Cap on the text a sub-task hands back, so one verbose sub-agent can't blow up
|
|
407
|
+
* the PARENT's context in a single tool_result. */
|
|
408
|
+
const SUBTASK_MAX = 48000; // chars (~12k tokens)
|
|
409
|
+
/**
|
|
410
|
+
* Slice `s` to at most `max` UTF-16 units without splitting a surrogate pair.
|
|
411
|
+
*
|
|
412
|
+
* A bare `slice()` can cut between the high and low half of a non-BMP character
|
|
413
|
+
* (emoji, many CJK extension glyphs), producing a lone surrogate — invalid UTF-16
|
|
414
|
+
* that the Anthropic API rejects outright with "no low surrogate in string". That
|
|
415
|
+
* exact failure has already been shipped and fixed once in this codebase; every
|
|
416
|
+
* new cap on model-facing text has to be surrogate-aware from the start.
|
|
417
|
+
*/
|
|
418
|
+
function sliceSafeEnd(s, max) {
|
|
419
|
+
if (s.length <= max)
|
|
420
|
+
return s;
|
|
421
|
+
let end = max;
|
|
422
|
+
const code = s.charCodeAt(end - 1);
|
|
423
|
+
if (code >= 0xd800 && code <= 0xdbff)
|
|
424
|
+
end--; // trailing high surrogate — drop it
|
|
425
|
+
return s.slice(0, end);
|
|
426
|
+
}
|
|
427
|
+
/** Mirror of sliceSafeEnd for a tail slice: never START on a low surrogate. */
|
|
428
|
+
function sliceSafeStart(s, from) {
|
|
429
|
+
if (from <= 0)
|
|
430
|
+
return s;
|
|
431
|
+
let start = from;
|
|
432
|
+
const code = s.charCodeAt(start);
|
|
433
|
+
if (code >= 0xdc00 && code <= 0xdfff)
|
|
434
|
+
start++; // leading low surrogate — drop it
|
|
435
|
+
return s.slice(start);
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Reduce a sub-agent's message history to the text its parent should receive.
|
|
439
|
+
*
|
|
440
|
+
* Pure + exported so the salvage rules can be tested without running a real
|
|
441
|
+
* sub-agent (which needs a live model stream and, for the timeout path, ten
|
|
442
|
+
* minutes of wall clock).
|
|
443
|
+
*
|
|
444
|
+
* `preferLast` — the normal completion path — returns the final assistant
|
|
445
|
+
* message, which is the sub-agent's actual answer.
|
|
446
|
+
*
|
|
447
|
+
* `preferLast: false` is the SALVAGE path, used when the sub-agent was cut off.
|
|
448
|
+
* A stopped sub-agent usually has no closing summary at all (it was killed
|
|
449
|
+
* mid-tool-round), so the last assistant message is frequently empty or a
|
|
450
|
+
* fragment. Concatenating what it did produce is far more useful to the parent
|
|
451
|
+
* model than nothing: it can build on the work instead of redoing it.
|
|
452
|
+
*/
|
|
453
|
+
function extractSubTaskText(messages, preferLast = true) {
|
|
454
|
+
const assistants = messages.filter((m) => m.role === 'assistant');
|
|
455
|
+
const textOf = (m) => (m?.content ?? [])
|
|
456
|
+
.filter((b) => b.type === 'text' && typeof b.text === 'string')
|
|
457
|
+
.map((b) => b.text)
|
|
458
|
+
.join('')
|
|
459
|
+
.trim();
|
|
460
|
+
if (preferLast)
|
|
461
|
+
return textOf(assistants[assistants.length - 1]);
|
|
462
|
+
return assistants.map(textOf).filter(Boolean).join('\n\n').trim();
|
|
463
|
+
}
|
|
464
|
+
/** Apply the parent-context cap to a sub-task's text, keeping head + tail. */
|
|
465
|
+
function capSubTaskText(text, max = SUBTASK_MAX) {
|
|
466
|
+
if (text.length <= max)
|
|
467
|
+
return text;
|
|
468
|
+
const head = sliceSafeEnd(text, Math.floor(max * 0.6));
|
|
469
|
+
const tail = sliceSafeStart(text, text.length - Math.floor(max * 0.4));
|
|
470
|
+
return `${head}\n\n[… sub-task output truncated (${text.length} chars) — kept the beginning and end …]\n\n${tail}`;
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Summarise what a cut-short sub-agent actually accomplished, so the parent model
|
|
474
|
+
* can continue from it rather than starting over.
|
|
475
|
+
*
|
|
476
|
+
* This is the whole point of the salvage path. Previously a timed-out sub-agent
|
|
477
|
+
* returned ONLY an error string: ten minutes of work, dozens of tool calls and
|
|
478
|
+
* any files it wrote were invisible to the parent, which typically responded by
|
|
479
|
+
* re-running the same work from scratch — while the tokens for the discarded run
|
|
480
|
+
* had already been billed in full.
|
|
481
|
+
*/
|
|
482
|
+
function summariseSubTaskProgress(messages) {
|
|
483
|
+
const toolNames = [];
|
|
484
|
+
for (const m of messages) {
|
|
485
|
+
if (m.role !== 'assistant' || !Array.isArray(m.content))
|
|
486
|
+
continue;
|
|
487
|
+
for (const b of m.content) {
|
|
488
|
+
if (b?.type === 'tool_use' && typeof b.name === 'string')
|
|
489
|
+
toolNames.push(b.name);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
if (toolNames.length === 0)
|
|
493
|
+
return '';
|
|
494
|
+
// Collapse to "name ×N" so a 40-call run reads as a short inventory rather
|
|
495
|
+
// than forty repeated lines of the same tool name.
|
|
496
|
+
const counts = new Map();
|
|
497
|
+
for (const n of toolNames)
|
|
498
|
+
counts.set(n, (counts.get(n) ?? 0) + 1);
|
|
499
|
+
const inventory = [...counts.entries()]
|
|
500
|
+
.sort((a, b) => b[1] - a[1])
|
|
501
|
+
.map(([name, n]) => (n > 1 ? `${name} ×${n}` : name))
|
|
502
|
+
.join(', ');
|
|
503
|
+
return `Tool calls completed before it was stopped (${toolNames.length} total): ${inventory}.`;
|
|
504
|
+
}
|
|
341
505
|
async function runSubTask(input, options, agentTypes) {
|
|
342
506
|
const prompt = typeof input.prompt === 'string' ? input.prompt.trim() : '';
|
|
343
507
|
if (!prompt)
|
|
344
508
|
return { error: 'task tool requires a non-empty prompt' };
|
|
345
509
|
const depth = options._depth ?? 0;
|
|
346
510
|
if (depth >= MAX_TASK_DEPTH) {
|
|
347
|
-
return { error:
|
|
511
|
+
return { error: 'Sub-agents cannot spawn further sub-agents. Do this work directly, or report back so the main agent can delegate it.' };
|
|
348
512
|
}
|
|
349
513
|
// Resolve an optional custom agent type (subagent_type).
|
|
350
514
|
const requestedType = typeof input.subagent_type === 'string' ? input.subagent_type : '';
|
|
@@ -365,6 +529,10 @@ async function runSubTask(input, options, agentTypes) {
|
|
|
365
529
|
const gatedPermission = async (req) => {
|
|
366
530
|
if (allowed && !allowed.has(req.tool))
|
|
367
531
|
return false;
|
|
532
|
+
// Path-scoped write restriction (agent.testFilesOnly) — see
|
|
533
|
+
// allowsTestOnlyWrite for the reasoning and its known limit.
|
|
534
|
+
if (agent?.testFilesOnly && !allowsTestOnlyWrite(req.tool, req.input))
|
|
535
|
+
return false;
|
|
368
536
|
return options.requestPermission(req);
|
|
369
537
|
};
|
|
370
538
|
const subMessages = [
|
|
@@ -423,32 +591,57 @@ async function runSubTask(input, options, agentTypes) {
|
|
|
423
591
|
onThinkingDelta: (text) => options.onThinkingDelta?.(text),
|
|
424
592
|
onThinkingProgress: (tok) => options.onThinkingProgress?.(tok),
|
|
425
593
|
});
|
|
426
|
-
//
|
|
427
|
-
//
|
|
428
|
-
//
|
|
429
|
-
//
|
|
594
|
+
// ── Stall timeout: SALVAGE, don't discard ────────────────────────────────
|
|
595
|
+
//
|
|
596
|
+
// The sub-agent hit its wall-clock cap (rather than finishing, or the parent
|
|
597
|
+
// aborting). This used to return ONLY an error string — throwing away
|
|
598
|
+
// everything the sub-agent had produced in up to ten minutes of work. The
|
|
599
|
+
// tokens were billed in full either way, and the parent model, told merely
|
|
600
|
+
// that "it stalled", would routinely re-run the identical work from scratch.
|
|
601
|
+
//
|
|
602
|
+
// The timeout still has to be reported unmistakably (the parent must not
|
|
603
|
+
// mistake a truncated run for a complete answer), but it is reported ALONGSIDE
|
|
604
|
+
// whatever was actually accomplished, not instead of it. `preferLast: false`
|
|
605
|
+
// because a killed sub-agent rarely has a closing summary — its useful output
|
|
606
|
+
// is spread across the assistant turns it did manage to produce.
|
|
430
607
|
if (subAbort.aborted && !options.abortSignal?.aborted) {
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
.
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
const head = text.slice(0, Math.floor(SUBTASK_MAX * 0.6));
|
|
446
|
-
const tail = text.slice(text.length - Math.floor(SUBTASK_MAX * 0.4));
|
|
447
|
-
text = `${head}\n\n[… sub-task output truncated (${text.length} chars) — kept the beginning and end …]\n\n${tail}`;
|
|
608
|
+
const mins = Math.round(SUBTASK_TIMEOUT_MS / 60000);
|
|
609
|
+
const partial = capSubTaskText(extractSubTaskText(result, false));
|
|
610
|
+
const progress = summariseSubTaskProgress(result);
|
|
611
|
+
const sections = [
|
|
612
|
+
`Sub-task STOPPED after ${mins} minutes without completing — treat the following as PARTIAL, unverified work, not a finished answer.`,
|
|
613
|
+
progress,
|
|
614
|
+
partial ? `Partial output before it was stopped:\n\n${partial}` : '',
|
|
615
|
+
'Do NOT simply re-run the same sub-task: build on what is above, or split the remaining work into smaller, more focused sub-tasks.',
|
|
616
|
+
].filter(Boolean);
|
|
617
|
+
// Returned as `error` (not `output`) on purpose: the loop's ledger counts an
|
|
618
|
+
// errored call as a non-effect, which is right — nothing here is verified —
|
|
619
|
+
// and the STALL_LIMIT runaway guard must still see repeated timeouts as
|
|
620
|
+
// failures so a permanently stuck sub-task can't loop forever.
|
|
621
|
+
return { error: sections.join('\n\n') };
|
|
448
622
|
}
|
|
623
|
+
// Normal completion: the final assistant message is the sub-agent's answer.
|
|
624
|
+
const text = capSubTaskText(extractSubTaskText(result, true));
|
|
449
625
|
return { output: text || '(sub-task completed with no text output)' };
|
|
450
626
|
}
|
|
451
627
|
catch (err) {
|
|
628
|
+
// Same salvage rule as the timeout path above, for the other way a sub-agent
|
|
629
|
+
// dies: runAgentLoop throws AgentTurnError when its stream fails, and that
|
|
630
|
+
// error CARRIES the history completed up to the failure precisely so callers
|
|
631
|
+
// don't lose it (see types.ts). Discarding it here — as this catch used to —
|
|
632
|
+
// reproduced the exact waste that class was written to prevent, one level down.
|
|
633
|
+
const salvaged = (0, types_1.salvageHistory)(err);
|
|
634
|
+
if (salvaged) {
|
|
635
|
+
const partial = capSubTaskText(extractSubTaskText(salvaged, false));
|
|
636
|
+
const progress = summariseSubTaskProgress(salvaged);
|
|
637
|
+
const sections = [
|
|
638
|
+
`Sub-task FAILED before completing: ${err.message}`,
|
|
639
|
+
progress,
|
|
640
|
+
partial ? `Partial output before the failure:\n\n${partial}` : '',
|
|
641
|
+
'Treat the above as PARTIAL, unverified work. Build on it rather than re-running the whole sub-task.',
|
|
642
|
+
].filter(Boolean);
|
|
643
|
+
return { error: sections.join('\n\n') };
|
|
644
|
+
}
|
|
452
645
|
return { error: `Sub-task failed: ${err.message}` };
|
|
453
646
|
}
|
|
454
647
|
finally {
|
|
@@ -569,6 +762,56 @@ function resolveVerificationNudge(rawSettings) {
|
|
|
569
762
|
}
|
|
570
763
|
/** Tools that mutate the filesystem — used by the verification nudge (GAP D). */
|
|
571
764
|
exports.WRITE_TOOL_NAMES = new Set(['write_file', 'edit_file', 'multi_edit', 'delete_file', 'move_file', 'copy_file', 'notebook_edit']);
|
|
765
|
+
/**
|
|
766
|
+
* May an agent restricted to `testFilesOnly` perform this tool call?
|
|
767
|
+
*
|
|
768
|
+
* A tool allowlist is all-or-nothing per tool: granting `edit_file` grants it for
|
|
769
|
+
* every path in the repo. The `test-writer` agent needs write access to produce
|
|
770
|
+
* tests, but must NOT be able to "fix" production source so a failing test goes
|
|
771
|
+
* green — the single most common way a test-writing agent destroys the signal it
|
|
772
|
+
* was asked to create. Its prompt says so; this makes it a refusal rather than a
|
|
773
|
+
* request.
|
|
774
|
+
*
|
|
775
|
+
* Pure + exported so the rules are testable directly, without running a real
|
|
776
|
+
* sub-agent.
|
|
777
|
+
*
|
|
778
|
+
* KNOWN LIMIT, stated rather than hidden: this gates the file TOOLS, not `bash`.
|
|
779
|
+
* A determined model could still write source via `bash: echo ... > src/x.ts`.
|
|
780
|
+
* Closing that means parsing shell redirection, which is not reliably doable — so
|
|
781
|
+
* this is a strong guardrail against the realistic failure mode, not a sandbox.
|
|
782
|
+
* Real isolation is the sandbox config (tools/sandbox.ts), a separate mechanism.
|
|
783
|
+
*/
|
|
784
|
+
function allowsTestOnlyWrite(tool, input) {
|
|
785
|
+
// Non-write tools are unaffected: reading, searching and running tests are all
|
|
786
|
+
// essential to writing a test.
|
|
787
|
+
//
|
|
788
|
+
// WRITE_TOOL_NAMES deliberately excludes `create_directory`: isTestFile matches
|
|
789
|
+
// FILE paths, so a legitimate `create_directory('test/helpers')` would be
|
|
790
|
+
// refused and the agent could not scaffold the tree it needs — while an empty
|
|
791
|
+
// directory cannot damage production code, and files placed in it are still
|
|
792
|
+
// checked individually.
|
|
793
|
+
if (!exports.WRITE_TOOL_NAMES.has(tool))
|
|
794
|
+
return true;
|
|
795
|
+
// EVERY path the call could affect must be a test file, not just `path`:
|
|
796
|
+
// move_file takes {source, dest} and copy_file {source, destination}, so
|
|
797
|
+
// checking `path` alone would let `move_file src/index.ts -> /tmp/x` through and
|
|
798
|
+
// remove production code by relocating it.
|
|
799
|
+
//
|
|
800
|
+
// `source` is skipped for notebook_edit specifically, where it is the CELL
|
|
801
|
+
// CONTENT rather than a path — treating a blob of code as a path would refuse
|
|
802
|
+
// every legitimate notebook edit.
|
|
803
|
+
const pathKeys = tool === 'notebook_edit'
|
|
804
|
+
? ['path']
|
|
805
|
+
: ['path', 'source', 'dest', 'destination'];
|
|
806
|
+
const candidates = pathKeys
|
|
807
|
+
.map((k) => input?.[k])
|
|
808
|
+
.filter((v) => typeof v === 'string' && v.length > 0);
|
|
809
|
+
// An unrecognised write shape (no path-like argument at all) is refused rather
|
|
810
|
+
// than allowed through, so a future tool cannot silently become a hole here.
|
|
811
|
+
if (candidates.length === 0)
|
|
812
|
+
return false;
|
|
813
|
+
return candidates.every((p) => (0, testIntegrity_1.isTestFile)(p));
|
|
814
|
+
}
|
|
572
815
|
/** Heuristic: does a bash command look like it's running tests/build/lint/typecheck? (GAP D) */
|
|
573
816
|
exports.VERIFY_CMD_RE = /\b(npm|yarn|pnpm)\s+(run\s+)?(test|build|lint|typecheck|tsc)\b|\bpytest\b|\bgo\s+(test|vet|build)\b|\btsc\b|\beslint\b|\bcargo\s+(test|build|check)\b/i;
|
|
574
817
|
/**
|
|
@@ -1515,7 +1758,26 @@ async function runAgentLoop(initialMessages, options) {
|
|
|
1515
1758
|
result = { error: 'Permission denied by user' };
|
|
1516
1759
|
}
|
|
1517
1760
|
else if (name === 'task') {
|
|
1518
|
-
|
|
1761
|
+
// Gated so a burst of `task` blocks in one message becomes a QUEUE
|
|
1762
|
+
// rather than N concurrent agent loops (see MAX_CONCURRENT_SUBTASKS).
|
|
1763
|
+
//
|
|
1764
|
+
// The gate wraps the CALL, not the inside of runSubTask, and that
|
|
1765
|
+
// placement is the whole trick: runSubTask arms its own 10-minute stall
|
|
1766
|
+
// timeout as its first act, so acquiring the slot inside it would start
|
|
1767
|
+
// that clock while the sub-task was still sitting in the queue. A
|
|
1768
|
+
// sub-task queued behind three long-running siblings could then be
|
|
1769
|
+
// "stopped for stalling" having never executed a single step.
|
|
1770
|
+
//
|
|
1771
|
+
// Only TOP-LEVEL fan-out is gated (`depth === 0`) — a deliberate
|
|
1772
|
+
// deadlock guard, kept even though MAX_TASK_DEPTH currently makes a
|
|
1773
|
+
// nested spawn impossible anyway. A nested spawn would request a slot
|
|
1774
|
+
// while its parent still holds one; if every slot were held by a parent
|
|
1775
|
+
// waiting on a child that can never be scheduled, the run would wedge
|
|
1776
|
+
// permanently. Keeping the limiter acyclic (a holder never re-enters it)
|
|
1777
|
+
// means raising MAX_TASK_DEPTH later can't silently reintroduce that.
|
|
1778
|
+
result = depth === 0
|
|
1779
|
+
? await _subTaskLimit(() => runSubTask(input, options, agentTypes))
|
|
1780
|
+
: await runSubTask(input, options, agentTypes);
|
|
1519
1781
|
}
|
|
1520
1782
|
else {
|
|
1521
1783
|
const pre = runToolHooks(hooks.PreToolUse, 'PreToolUse', name, input, options.workDir);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface SecurityFinding {
|
|
2
|
+
/** Machine-readable class, e.g. 'hardcoded-secret'. */
|
|
3
|
+
kind: string;
|
|
4
|
+
/** One-line explanation aimed at the model, phrased as what to do. */
|
|
5
|
+
message: string;
|
|
6
|
+
/** 1-based line number within the written content. */
|
|
7
|
+
line: number;
|
|
8
|
+
/** The offending line, truncated and with any secret value redacted. */
|
|
9
|
+
sample: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Scan file content for flagrant security problems.
|
|
13
|
+
*
|
|
14
|
+
* Returns at most `max` findings (default 5) — enough to be useful, few enough
|
|
15
|
+
* that the note appended to a tool result stays readable and cheap.
|
|
16
|
+
*/
|
|
17
|
+
export declare function checkSecurity(content: string, max?: number): SecurityFinding[];
|
|
18
|
+
/**
|
|
19
|
+
* Render findings as a note to append to a successful write's tool result.
|
|
20
|
+
*
|
|
21
|
+
* Phrased as a review comment rather than an error: the write HAS happened, and
|
|
22
|
+
* the model is being asked to look again. Deliberately explicit that it may be a
|
|
23
|
+
* false positive — otherwise the model tends to "fix" flagged-but-correct code,
|
|
24
|
+
* which is its own kind of damage.
|
|
25
|
+
*/
|
|
26
|
+
export declare function securityNoteText(findings: SecurityFinding[]): string;
|
|
27
|
+
//# sourceMappingURL=securityLint.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"securityLint.d.ts","sourceRoot":"","sources":["../../src/agent/securityLint.ts"],"names":[],"mappings":"AAuBA,MAAM,WAAW,eAAe;IAC9B,uDAAuD;IACvD,IAAI,EAAE,MAAM,CAAC;IACb,sEAAsE;IACtE,OAAO,EAAE,MAAM,CAAC;IAChB,sDAAsD;IACtD,IAAI,EAAE,MAAM,CAAC;IACb,wEAAwE;IACxE,MAAM,EAAE,MAAM,CAAC;CAChB;AA6GD;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,SAAI,GAAG,eAAe,EAAE,CA6CzE;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,eAAe,EAAE,GAAG,MAAM,CAepE"}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ─── Inline security lint ─────────────────────────────────────────────────────
|
|
3
|
+
//
|
|
4
|
+
// The gap this closes: six checks already run automatically on every write —
|
|
5
|
+
// editCompleteness, crossFile, testIntegrity, claimEvidence, flaky, destructive —
|
|
6
|
+
// and not one of them is about security. For an agent that WRITES production code,
|
|
7
|
+
// nothing stopped it committing a hardcoded credential, an `eval` over
|
|
8
|
+
// user-controlled input, or a SQL string built by concatenation. The only security
|
|
9
|
+
// review available was an agent the user had to know to ask for.
|
|
10
|
+
//
|
|
11
|
+
// DESIGN: WARN, NEVER BLOCK.
|
|
12
|
+
//
|
|
13
|
+
// This runs on every single write, so a false positive is expensive — it would
|
|
14
|
+
// train the model (and the user) to ignore the channel, or worse, stall a
|
|
15
|
+
// legitimate edit. `editCompleteness` can afford to refuse outright because
|
|
16
|
+
// "// ... rest unchanged" is unambiguous; "this looks like SQL injection" is not.
|
|
17
|
+
// So findings are appended to the tool RESULT as a note the model sees and can act
|
|
18
|
+
// on, and the write still succeeds.
|
|
19
|
+
//
|
|
20
|
+
// Consequently every pattern here is tuned for PRECISION over recall. A check that
|
|
21
|
+
// fires on ordinary code was removed rather than loosened. Deep analysis is the
|
|
22
|
+
// `security-auditor` agent's job; this only catches the flagrant cases at the
|
|
23
|
+
// moment they are written.
|
|
24
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
+
exports.checkSecurity = checkSecurity;
|
|
26
|
+
exports.securityNoteText = securityNoteText;
|
|
27
|
+
/** Max characters of the offending line to echo back. */
|
|
28
|
+
const SAMPLE_MAX = 160;
|
|
29
|
+
/**
|
|
30
|
+
* Redact anything that looks like a literal secret value before echoing a line.
|
|
31
|
+
*
|
|
32
|
+
* The whole point of flagging a hardcoded credential is to get it removed — so
|
|
33
|
+
* this must not copy the value into the transcript (and from there into logs, or
|
|
34
|
+
* the next request's context) on the way to reporting it.
|
|
35
|
+
*/
|
|
36
|
+
function redact(line) {
|
|
37
|
+
const masked = line
|
|
38
|
+
// key = "value" / key: 'value' → keep the key, mask the value
|
|
39
|
+
.replace(/(['"`]?[\w.-]*(?:secret|password|passwd|token|api[_-]?key|apikey|auth|credential|private[_-]?key)[\w.-]*['"`]?\s*[:=]\s*)(['"`])([^'"`]{4,})\2/gi, (_m, head, q) => `${head}${q}[REDACTED]${q}`)
|
|
40
|
+
// Bare high-entropy provider tokens appearing anywhere on the line
|
|
41
|
+
.replace(/\b(sk-[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9]{20,}|AKIA[0-9A-Z]{16}|xox[baprs]-[A-Za-z0-9-]{10,})\b/g, '[REDACTED]');
|
|
42
|
+
return masked.length > SAMPLE_MAX ? masked.slice(0, SAMPLE_MAX) + '…' : masked;
|
|
43
|
+
}
|
|
44
|
+
/** Strip string/comment noise that causes false positives, keeping length stable. */
|
|
45
|
+
function isLikelyCommentLine(line) {
|
|
46
|
+
return /^\s*(\/\/|\*|#|--|<!--)/.test(line);
|
|
47
|
+
}
|
|
48
|
+
const RULES = [
|
|
49
|
+
// ── Hardcoded credentials ───────────────────────────────────────────────────
|
|
50
|
+
// Provider-prefixed tokens are near-zero false positive: the prefixes are
|
|
51
|
+
// registered formats, not something that occurs naturally in source. Checked
|
|
52
|
+
// inside comments too — a key commented out is still a committed key.
|
|
53
|
+
{
|
|
54
|
+
kind: 'hardcoded-secret',
|
|
55
|
+
re: /\b(sk-[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9]{20,}|AKIA[0-9A-Z]{16}|xox[baprs]-[A-Za-z0-9-]{10,}|AIza[0-9A-Za-z_-]{30,})\b/,
|
|
56
|
+
message: 'Looks like a real API key/token committed to source. Move it to an environment variable and rotate the exposed key.',
|
|
57
|
+
includeComments: true,
|
|
58
|
+
},
|
|
59
|
+
// NOTE: the `private-key` check is NOT here — it needs to span multiple lines
|
|
60
|
+
// (BEGIN header on one, base64 body on the next), which this per-line loop
|
|
61
|
+
// cannot express. It runs separately in checkSecurity below.
|
|
62
|
+
{
|
|
63
|
+
kind: 'hardcoded-password',
|
|
64
|
+
// An ASSIGNMENT of a credential-ish name to a non-trivial literal.
|
|
65
|
+
//
|
|
66
|
+
// Tightened after measuring against the real backend, where the looser version
|
|
67
|
+
// fired on `missingSecret:'FIREBASE_TOKEN'` — code that NAMES a secret in an
|
|
68
|
+
// error message, the opposite of leaking one. So a value that is itself just a
|
|
69
|
+
// SCREAMING_SNAKE identifier (an env-var name) is excluded, along with the
|
|
70
|
+
// usual placeholder vocabulary. The value must also look like actual secret
|
|
71
|
+
// material: mixed case or digits, not a lone lowercase word.
|
|
72
|
+
re: /(?:password|passwd|secret|api[_-]?key|apikey|access[_-]?token)['"`]?\s*[:=]\s*['"`](?![A-Z0-9_]+['"`])(?!.*(?:\$\{|process\.env|os\.environ|example|changeme|placeholder|redacted|xxx|test|dummy|fake|sample|your[_-]?|<|\*{3}))(?=[^'"`]*[0-9A-Z])[^'"`\s]{10,}['"`]/,
|
|
73
|
+
message: 'Hardcoded credential literal. Read it from the environment/secret store instead, and rotate the exposed value.',
|
|
74
|
+
includeComments: true,
|
|
75
|
+
},
|
|
76
|
+
// ── Injection ───────────────────────────────────────────────────────────────
|
|
77
|
+
{
|
|
78
|
+
kind: 'dynamic-eval',
|
|
79
|
+
// The negative lookbehind for `.` is what makes this usable: `redisClient.eval`
|
|
80
|
+
// (a Redis Lua script), `page.eval` (Playwright), `vm.eval` and friends are
|
|
81
|
+
// METHOD calls on an object and have nothing to do with JavaScript's global
|
|
82
|
+
// eval. Without it, the real backend's Redis idempotency script was flagged.
|
|
83
|
+
// Only a bare `eval(` / `new Function(` with a non-literal argument counts.
|
|
84
|
+
re: /(?<![.\w$])(?:eval|new\s+Function)\s*\(\s*(?!['"`][^'"`]*['"`]\s*\))[^)]*[a-zA-Z_$][\w$]*/,
|
|
85
|
+
message: 'eval / new Function on a non-literal value executes arbitrary code if that value is ever user-controlled. Use an explicit parser or a lookup table.',
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
kind: 'sql-injection',
|
|
89
|
+
// Only fires when the interpolated expression is plausibly REQUEST-DERIVED.
|
|
90
|
+
//
|
|
91
|
+
// The obvious pattern — any `${...}` inside a SQL string — was measured
|
|
92
|
+
// against the real backend and flagged 15 of 183 files, essentially all of
|
|
93
|
+
// them safe and idiomatic: `${sets.join(', ')}` for a dynamic UPDATE, `${field}`
|
|
94
|
+
// for a server-chosen column, `${CONSUMPTION}` for a module constant. At that
|
|
95
|
+
// hit rate the warning is pure noise, and noise is worse than silence because
|
|
96
|
+
// it teaches everyone to skip the channel.
|
|
97
|
+
//
|
|
98
|
+
// So the interpolation must name something that plausibly came from the
|
|
99
|
+
// outside: req/request/params/query/body/input/user/args, or a bare
|
|
100
|
+
// `'...' + ident`. This trades recall for precision on purpose — thorough SQL
|
|
101
|
+
// review is the security-auditor agent's job, not an inline regex's.
|
|
102
|
+
re: /\b(?:SELECT|INSERT\s+INTO|UPDATE|DELETE\s+FROM)\b[^;'"`]{0,160}(?:\$\{\s*(?:req|request|params?|query|body|input|user|args|ctx)\b|['"`]\s*\+\s*(?:req|request|params?|query|body|input|user|args|ctx)\b|%\s*\(\s*(?:request|params?|query|body|input|user)\b)/i,
|
|
103
|
+
message: 'SQL built by interpolating a request-derived value. Use a parameterised query ($1 / ? placeholders) — this is the classic injection sink.',
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
kind: 'command-injection',
|
|
107
|
+
// Shell execution with an interpolated or concatenated argument.
|
|
108
|
+
re: /\b(?:exec|execSync|spawnSync?|system|popen|os\.system|subprocess\.(?:call|run|Popen))\s*\(\s*(?:[`'"][^`'"]*(?:\$\{|['"]\s*\+)|[a-zA-Z_$][\w$]*\s*\+)/,
|
|
109
|
+
message: 'Shell command built from a variable. Pass arguments as an array (no shell), or validate against an allowlist — a value containing ; or $() becomes command execution.',
|
|
110
|
+
},
|
|
111
|
+
// ── Transport / verification ────────────────────────────────────────────────
|
|
112
|
+
{
|
|
113
|
+
kind: 'tls-verification-disabled',
|
|
114
|
+
re: /(?:rejectUnauthorized\s*:\s*false|NODE_TLS_REJECT_UNAUTHORIZED\s*=\s*['"]?0|verify\s*=\s*False|InsecureSkipVerify\s*:\s*true)/,
|
|
115
|
+
message: 'TLS certificate verification is disabled, which removes protection against man-in-the-middle attacks. Trust a specific CA instead if the cert is self-signed.',
|
|
116
|
+
},
|
|
117
|
+
];
|
|
118
|
+
/**
|
|
119
|
+
* Scan file content for flagrant security problems.
|
|
120
|
+
*
|
|
121
|
+
* Returns at most `max` findings (default 5) — enough to be useful, few enough
|
|
122
|
+
* that the note appended to a tool result stays readable and cheap.
|
|
123
|
+
*/
|
|
124
|
+
function checkSecurity(content, max = 5) {
|
|
125
|
+
if (!content)
|
|
126
|
+
return [];
|
|
127
|
+
const findings = [];
|
|
128
|
+
const lines = content.split(/\r?\n/);
|
|
129
|
+
// Private keys are matched across lines, unlike every other rule.
|
|
130
|
+
//
|
|
131
|
+
// A real PEM block is inherently multi-line: the BEGIN header sits on one line
|
|
132
|
+
// and the base64 body on the next. Requiring both on ONE line (which the
|
|
133
|
+
// line-by-line loop below does) meant the single highest-severity finding here
|
|
134
|
+
// only fired for keys embedded in a "\n"-escaped string literal, and missed the
|
|
135
|
+
// far more common case of a key pasted in verbatim.
|
|
136
|
+
const pemIdx = lines.findIndex((l) => /-----BEGIN\s+(?:RSA|EC|DSA|OPENSSH|PGP)?\s*PRIVATE KEY-----/.test(l));
|
|
137
|
+
if (pemIdx !== -1) {
|
|
138
|
+
// Require actual key material nearby, so a bare header — placeholder text in a
|
|
139
|
+
// config UI, documentation, a PEM parser — does not trip it.
|
|
140
|
+
const following = lines.slice(pemIdx, pemIdx + 4).join('\n');
|
|
141
|
+
if (/[A-Za-z0-9+/]{40,}/.test(following.replace(/-----[^-]+-----/g, ''))) {
|
|
142
|
+
findings.push({
|
|
143
|
+
kind: 'private-key',
|
|
144
|
+
message: 'A private key with real key material is being written into source. Store it outside the repo (secret manager / env var) and rotate it.',
|
|
145
|
+
line: pemIdx + 1,
|
|
146
|
+
sample: '-----BEGIN PRIVATE KEY----- [REDACTED]',
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// One finding per (kind, line) at most, and one finding per kind overall —
|
|
151
|
+
// a file with fifty interpolated queries should say "SQL injection" once, not
|
|
152
|
+
// fill the model's context with fifty copies of the same advice.
|
|
153
|
+
const seenKinds = new Set();
|
|
154
|
+
for (let i = 0; i < lines.length && findings.length < max; i++) {
|
|
155
|
+
const line = lines[i];
|
|
156
|
+
if (!line || line.length > 2000)
|
|
157
|
+
continue; // minified/bundled — not hand-written source
|
|
158
|
+
const commentish = isLikelyCommentLine(line);
|
|
159
|
+
for (const rule of RULES) {
|
|
160
|
+
if (seenKinds.has(rule.kind))
|
|
161
|
+
continue;
|
|
162
|
+
if (commentish && !rule.includeComments)
|
|
163
|
+
continue;
|
|
164
|
+
if (!rule.re.test(line))
|
|
165
|
+
continue;
|
|
166
|
+
seenKinds.add(rule.kind);
|
|
167
|
+
findings.push({ kind: rule.kind, message: rule.message, line: i + 1, sample: redact(line.trim()) });
|
|
168
|
+
break; // at most one rule per line
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return findings;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Render findings as a note to append to a successful write's tool result.
|
|
175
|
+
*
|
|
176
|
+
* Phrased as a review comment rather than an error: the write HAS happened, and
|
|
177
|
+
* the model is being asked to look again. Deliberately explicit that it may be a
|
|
178
|
+
* false positive — otherwise the model tends to "fix" flagged-but-correct code,
|
|
179
|
+
* which is its own kind of damage.
|
|
180
|
+
*/
|
|
181
|
+
function securityNoteText(findings) {
|
|
182
|
+
// Opt-out, mirroring NEXRALL_ALLOW_ELIDED_WRITE. Someone working in a codebase
|
|
183
|
+
// that trips a rule constantly (a SQL-builder library, a crypto implementation,
|
|
184
|
+
// a test-fixture directory full of fake keys) needs a way to silence this
|
|
185
|
+
// without disabling the write path itself.
|
|
186
|
+
if (process.env.NEXRALL_SECURITY_LINT === 'off')
|
|
187
|
+
return '';
|
|
188
|
+
if (!findings.length)
|
|
189
|
+
return '';
|
|
190
|
+
const lines = findings.map((f) => ` • line ${f.line} [${f.kind}]: ${f.message}\n ${f.sample}`);
|
|
191
|
+
return (`\n\n⚠ SECURITY REVIEW (${findings.length} finding${findings.length > 1 ? 's' : ''}) — the write succeeded; check these before moving on:\n` +
|
|
192
|
+
lines.join('\n') +
|
|
193
|
+
`\nIf a finding is a false positive (test fixture, placeholder, intentionally dynamic), say so and continue — do NOT rewrite correct code to silence it.`);
|
|
194
|
+
}
|
|
195
|
+
//# sourceMappingURL=securityLint.js.map
|
package/dist/api/client.d.ts
CHANGED
|
@@ -76,6 +76,18 @@ export declare function streamChat(messages: Message[], options: StreamChatOptio
|
|
|
76
76
|
* bounds the cost if this request never lands.
|
|
77
77
|
*/
|
|
78
78
|
export declare function cancelTurn(turnId: string): Promise<void>;
|
|
79
|
+
/**
|
|
80
|
+
* Revoke the stored refresh token server-side.
|
|
81
|
+
*
|
|
82
|
+
* Deleting the local config alone is NOT a logout once refresh tokens exist: the
|
|
83
|
+
* token stays valid for its full 60-day life and can keep minting access tokens
|
|
84
|
+
* for anyone who recovered it (a synced dotfile, a backup, a shared machine).
|
|
85
|
+
* The backend already exposes /api/auth/logout for exactly this.
|
|
86
|
+
*
|
|
87
|
+
* Best-effort and never throws — a failed revoke must not stop the local
|
|
88
|
+
* credentials from being cleared, which is the part the user can see.
|
|
89
|
+
*/
|
|
90
|
+
export declare function revokeRefreshToken(): Promise<void>;
|
|
79
91
|
export declare function getBalance(): Promise<number>;
|
|
80
92
|
export declare function exchangeVscodeCode(code: string): Promise<AuthConfig>;
|
|
81
93
|
export declare function login(email: string, password: string): Promise<AuthConfig>;
|
package/dist/api/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,UAAU,EAA8B,UAAU,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAiErH,eAAO,MAAM,QAAQ,QAAmB,CAAC;AAIzC;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,SAAS;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,EAC5D,OAAO,EAAE,CAAC,EAAE,EACZ,UAAU,EAAE,KAAK,CAAC;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,IAAI,GAAG,SAAS,GACtD,CAAC,EAAE,CAeL;
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,UAAU,EAA8B,UAAU,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAiErH,eAAO,MAAM,QAAQ,QAAmB,CAAC;AAIzC;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,SAAS;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,EAC5D,OAAO,EAAE,CAAC,EAAE,EACZ,UAAU,EAAE,KAAK,CAAC;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,IAAI,GAAG,SAAS,GACtD,CAAC,EAAE,CAeL;AAiGD,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,UAAU,CAAC;IACjB,aAAa,CAAC,EAAE,aAAa,GAAG,IAAI,CAAC;IACrC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IACnC,oFAAoF;IACpF,UAAU,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAAC;IACjG,6EAA6E;IAC7E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,6IAA6I;IAC7I,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;;;;;;;;;;;OAeG;IACH,uBAAuB,CAAC,EAAE,OAAO,CAAC;CACnC;AAiDD,wBAAsB,UAAU,CAC9B,QAAQ,EAAE,OAAO,EAAE,EACnB,OAAO,EAAE,iBAAiB,EAC1B,OAAO,EAAE,CAAC,CAAC,EAAE,QAAQ,KAAK,IAAI,GAC7B,OAAO,CAAC,OAAO,CAAC,CA6jClB;AAID;;;;;;;;;;;GAWG;AACH,wBAAsB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAU9D;AAID;;;;;;;;;;GAUG;AACH,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC,CAYxD;AAID,wBAAsB,UAAU,IAAI,OAAO,CAAC,MAAM,CAAC,CA0BlD;AAID,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CA+B1E;AAID,wBAAsB,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CA0BhF"}
|