opencode-ultra 0.5.0 → 0.5.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/dist/index.js +142 -19
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -14465,7 +14465,11 @@ var PluginConfigSchema = exports_external.object({
|
|
|
14465
14465
|
todo_enforcer: exports_external.object({
|
|
14466
14466
|
maxEnforcements: exports_external.number().min(0).optional()
|
|
14467
14467
|
}).optional(),
|
|
14468
|
-
mcp_api_keys: exports_external.record(exports_external.string(), exports_external.string()).optional()
|
|
14468
|
+
mcp_api_keys: exports_external.record(exports_external.string(), exports_external.string()).optional(),
|
|
14469
|
+
safety: exports_external.object({
|
|
14470
|
+
maxTotalSpawned: exports_external.number().min(1).optional(),
|
|
14471
|
+
agentTimeoutMs: exports_external.number().min(1000).optional()
|
|
14472
|
+
}).optional()
|
|
14469
14473
|
}).passthrough();
|
|
14470
14474
|
function parsePluginConfig(raw) {
|
|
14471
14475
|
const result = PluginConfigSchema.safeParse(raw);
|
|
@@ -27426,16 +27430,89 @@ function resolveCategory(categoryName, configCategories) {
|
|
|
27426
27430
|
return { ...builtin, ...override };
|
|
27427
27431
|
}
|
|
27428
27432
|
|
|
27433
|
+
// src/safety/sanitizer.ts
|
|
27434
|
+
var INJECTION_PATTERNS = [
|
|
27435
|
+
{ pattern: /ignore\s+(?:all\s+)?(?:previous\s+|prior\s+|above\s+)?instructions/i, label: "instruction-override" },
|
|
27436
|
+
{ pattern: /disregard\s+(?:all\s+)?(?:previous\s+|prior\s+)?(?:instructions|rules|guidelines)/i, label: "instruction-override" },
|
|
27437
|
+
{ pattern: /forget\s+(?:all\s+)?(?:previous\s+|prior\s+)?(?:instructions|context|rules)/i, label: "instruction-override" },
|
|
27438
|
+
{ pattern: /override\s+(?:all\s+)?(?:previous\s+|prior\s+)?(?:instructions|rules|system)/i, label: "instruction-override" },
|
|
27439
|
+
{ pattern: /you\s+are\s+now\s+(?:a\s+|an\s+|in\s+)?/i, label: "role-hijack" },
|
|
27440
|
+
{ pattern: /pretend\s+(?:you\s+are|to\s+be|you're)\s+/i, label: "role-hijack" },
|
|
27441
|
+
{ pattern: /act\s+as\s+(?:if\s+)?(?:you\s+(?:are|were)\s+)?/i, label: "role-hijack" },
|
|
27442
|
+
{ pattern: /role[\s-]?play\s+as/i, label: "role-hijack" },
|
|
27443
|
+
{ pattern: /switch\s+(?:to\s+)?(?:your\s+)?(?:new\s+)?(?:role|persona|identity)/i, label: "role-hijack" },
|
|
27444
|
+
{ pattern: /<\/?system(?:\s[^>]*)?>/, label: "fake-tag" },
|
|
27445
|
+
{ pattern: /<\/?instructions?(?:\s[^>]*)?>/, label: "fake-tag" },
|
|
27446
|
+
{ pattern: /<\/?prompt(?:\s[^>]*)?>/, label: "fake-tag" },
|
|
27447
|
+
{ pattern: /<\/?assistant(?:\s[^>]*)?>/, label: "fake-tag" },
|
|
27448
|
+
{ pattern: /<\/?human(?:\s[^>]*)?>/, label: "fake-tag" },
|
|
27449
|
+
{ pattern: /\[SYSTEM\](?:\s*:)?/i, label: "fake-tag" },
|
|
27450
|
+
{ pattern: /\[INST\]|\[\/INST\]/i, label: "fake-tag" },
|
|
27451
|
+
{ pattern: /new\s+(?:system\s+)?instructions?\s*:/i, label: "new-instructions" },
|
|
27452
|
+
{ pattern: /updated?\s+(?:system\s+)?instructions?\s*:/i, label: "new-instructions" },
|
|
27453
|
+
{ pattern: /revised?\s+(?:system\s+)?instructions?\s*:/i, label: "new-instructions" },
|
|
27454
|
+
{ pattern: /(?:run|execute|eval)\s*\(\s*['"`].*(?:rm\s+-rf|curl\s+.*\|\s*(?:sh|bash)|wget\s+.*\|\s*(?:sh|bash))/i, label: "dangerous-command" },
|
|
27455
|
+
{ pattern: /\brm\s+-rf\s+[\/~]/i, label: "dangerous-command" },
|
|
27456
|
+
{ pattern: /(?:\u5168\u3066\u306E|\u3059\u3079\u3066\u306E)?(?:\u524D\u306E|\u4EE5\u524D\u306E)?\u6307\u793A\u3092(?:\u7121\u8996|\u5FD8\u308C|\u53D6\u308A\u6D88)/i, label: "instruction-override-ja" },
|
|
27457
|
+
{ pattern: /(?:\u65B0\u3057\u3044|\u66F4\u65B0\u3055\u308C\u305F)\u6307\u793A\s*[:\uFF1A]/i, label: "new-instructions-ja" }
|
|
27458
|
+
];
|
|
27459
|
+
var SUSPICIOUS_UNICODE = /[\u200B-\u200F\u202A-\u202E\u2060-\u2069\uFEFF\u00AD]/g;
|
|
27460
|
+
function sanitizeAgentOutput(text) {
|
|
27461
|
+
const warnings = [];
|
|
27462
|
+
const invisibleCount = (text.match(SUSPICIOUS_UNICODE) || []).length;
|
|
27463
|
+
if (invisibleCount > 0) {
|
|
27464
|
+
warnings.push(`Stripped ${invisibleCount} suspicious Unicode characters`);
|
|
27465
|
+
text = text.replace(SUSPICIOUS_UNICODE, "");
|
|
27466
|
+
}
|
|
27467
|
+
for (const { pattern, label } of INJECTION_PATTERNS) {
|
|
27468
|
+
if (pattern.test(text)) {
|
|
27469
|
+
warnings.push(`Injection pattern detected: ${label}`);
|
|
27470
|
+
text = text.replace(pattern, (match) => `[SANITIZED:${label}]`);
|
|
27471
|
+
}
|
|
27472
|
+
}
|
|
27473
|
+
const flagged = warnings.length > 0;
|
|
27474
|
+
if (flagged) {
|
|
27475
|
+
log("Sanitizer flagged content", { warnings });
|
|
27476
|
+
}
|
|
27477
|
+
return { text, flagged, warnings };
|
|
27478
|
+
}
|
|
27479
|
+
function sanitizeSpawnResult(result) {
|
|
27480
|
+
const { text, flagged, warnings } = sanitizeAgentOutput(result);
|
|
27481
|
+
if (!flagged)
|
|
27482
|
+
return text;
|
|
27483
|
+
const banner = `
|
|
27484
|
+
|
|
27485
|
+
> **[Safety] Potential prompt injection detected in agent output.**
|
|
27486
|
+
> Patterns: ${warnings.join(", ")}
|
|
27487
|
+
> The flagged content has been neutralized.
|
|
27488
|
+
`;
|
|
27489
|
+
return banner + `
|
|
27490
|
+
` + text;
|
|
27491
|
+
}
|
|
27429
27492
|
// src/tools/spawn-agent.ts
|
|
27493
|
+
var DEFAULT_MAX_TOTAL_SPAWNED = 15;
|
|
27494
|
+
var DEFAULT_AGENT_TIMEOUT_MS = 180000;
|
|
27430
27495
|
function showToast(ctx, title, message, variant = "info") {
|
|
27431
27496
|
const client = ctx.client;
|
|
27432
27497
|
client.tui?.showToast?.({
|
|
27433
27498
|
body: { title, message, variant, duration: 2000 }
|
|
27434
27499
|
})?.catch?.(() => {});
|
|
27435
27500
|
}
|
|
27501
|
+
async function withTimeout(promise3, ms, label) {
|
|
27502
|
+
let timer;
|
|
27503
|
+
const timeout = new Promise((_, reject) => {
|
|
27504
|
+
timer = setTimeout(() => reject(new Error(`Timeout: ${label} exceeded ${ms}ms`)), ms);
|
|
27505
|
+
});
|
|
27506
|
+
try {
|
|
27507
|
+
return await Promise.race([promise3, timeout]);
|
|
27508
|
+
} finally {
|
|
27509
|
+
clearTimeout(timer);
|
|
27510
|
+
}
|
|
27511
|
+
}
|
|
27436
27512
|
async function runAgent(ctx, task, toolCtx, internalSessions, deps, progress) {
|
|
27437
27513
|
const { agent, prompt, description } = task;
|
|
27438
27514
|
const t0 = Date.now();
|
|
27515
|
+
const timeoutMs = deps.agentTimeoutMs ?? DEFAULT_AGENT_TIMEOUT_MS;
|
|
27439
27516
|
const elapsed = () => ((Date.now() - (progress?.startTime ?? t0)) / 1000).toFixed(0);
|
|
27440
27517
|
const updateTitle = (status) => {
|
|
27441
27518
|
toolCtx.metadata({ title: status });
|
|
@@ -27461,26 +27538,27 @@ async function runAgent(ctx, task, toolCtx, internalSessions, deps, progress) {
|
|
|
27461
27538
|
**Error**: Failed to create session`;
|
|
27462
27539
|
}
|
|
27463
27540
|
internalSessions.add(sessionID);
|
|
27464
|
-
await ctx.client.session.prompt({
|
|
27541
|
+
await withTimeout(ctx.client.session.prompt({
|
|
27465
27542
|
path: { id: sessionID },
|
|
27466
27543
|
body: {
|
|
27467
27544
|
parts: [{ type: "text", text: prompt }],
|
|
27468
27545
|
agent
|
|
27469
27546
|
},
|
|
27470
27547
|
query: { directory: ctx.directory }
|
|
27471
|
-
});
|
|
27548
|
+
}), timeoutMs, `${agent} (${description})`);
|
|
27472
27549
|
const messagesResp = await ctx.client.session.messages({
|
|
27473
27550
|
path: { id: sessionID },
|
|
27474
27551
|
query: { directory: ctx.directory }
|
|
27475
27552
|
});
|
|
27476
27553
|
const messages = messagesResp.data ?? [];
|
|
27477
27554
|
const lastAssistant = messages.filter((m) => m.info?.role === "assistant").pop();
|
|
27478
|
-
const
|
|
27555
|
+
const rawResult = lastAssistant?.parts?.filter((p) => p.type === "text" && p.text).map((p) => p.text).join(`
|
|
27479
27556
|
`) ?? "(No response from agent)";
|
|
27480
27557
|
internalSessions.delete(sessionID);
|
|
27481
27558
|
await ctx.client.session.delete({ path: { id: sessionID }, query: { directory: ctx.directory } }).catch(() => {});
|
|
27482
27559
|
const agentTime = ((Date.now() - t0) / 1000).toFixed(1);
|
|
27483
27560
|
log(`spawn_agent: ${agent} done`, { seconds: agentTime, description });
|
|
27561
|
+
const result = sanitizeSpawnResult(rawResult);
|
|
27484
27562
|
return `## ${description} (${agentTime}s)
|
|
27485
27563
|
|
|
27486
27564
|
**Agent**: ${agent}
|
|
@@ -27494,7 +27572,14 @@ ${result}`;
|
|
|
27494
27572
|
await ctx.client.session.delete({ path: { id: sessionID }, query: { directory: ctx.directory } }).catch(() => {});
|
|
27495
27573
|
}
|
|
27496
27574
|
const msg = error92 instanceof Error ? error92.message : String(error92);
|
|
27497
|
-
|
|
27575
|
+
const isTimeout = msg.startsWith("Timeout:");
|
|
27576
|
+
log(`spawn_agent: ${agent} ${isTimeout ? "timeout" : "error"}`, { error: msg });
|
|
27577
|
+
if (isTimeout) {
|
|
27578
|
+
return `## ${description}
|
|
27579
|
+
|
|
27580
|
+
**Agent**: ${agent}
|
|
27581
|
+
**Timeout**: Agent did not complete within ${(timeoutMs / 1000).toFixed(0)}s`;
|
|
27582
|
+
}
|
|
27498
27583
|
return `## ${description}
|
|
27499
27584
|
|
|
27500
27585
|
**Agent**: ${agent}
|
|
@@ -27502,6 +27587,7 @@ ${result}`;
|
|
|
27502
27587
|
}
|
|
27503
27588
|
}
|
|
27504
27589
|
function createSpawnAgentTool(ctx, internalSessions, deps = {}) {
|
|
27590
|
+
const maxTotalSpawned = deps.maxTotalSpawned ?? DEFAULT_MAX_TOTAL_SPAWNED;
|
|
27505
27591
|
return tool({
|
|
27506
27592
|
description: `Spawn subagents to execute tasks in PARALLEL.
|
|
27507
27593
|
All agents in the array run concurrently (respecting concurrency limits if configured).
|
|
@@ -27546,6 +27632,11 @@ spawn_agent({
|
|
|
27546
27632
|
return `Error: agents[${i}].description is required`;
|
|
27547
27633
|
}
|
|
27548
27634
|
}
|
|
27635
|
+
const currentActive = internalSessions.size;
|
|
27636
|
+
if (currentActive + agents.length > maxTotalSpawned) {
|
|
27637
|
+
log("spawn_agent: BLOCKED by spawn limit", { currentActive, requested: agents.length, max: maxTotalSpawned });
|
|
27638
|
+
return `Error: Too many concurrent spawned agents. Active: ${currentActive}, requested: ${agents.length}, max: ${maxTotalSpawned}. Wait for active agents to complete or increase safety.maxTotalSpawned.`;
|
|
27639
|
+
}
|
|
27549
27640
|
const agentNames = agents.map((a) => a.agent);
|
|
27550
27641
|
log("spawn_agent", { count: agents.length, agents: agentNames });
|
|
27551
27642
|
showToast(ctx, "spawn_agent", `${agents.length} agents: ${agentNames.join(", ")}`);
|
|
@@ -27622,8 +27713,21 @@ function resolveTaskModel(task, deps) {
|
|
|
27622
27713
|
// src/tools/ralph-loop.ts
|
|
27623
27714
|
var COMPLETION_MARKER = "<promise>DONE</promise>";
|
|
27624
27715
|
var DEFAULT_MAX_ITERATIONS = 10;
|
|
27716
|
+
var DEFAULT_ITERATION_TIMEOUT_MS = 180000;
|
|
27625
27717
|
var activeLoops = new Map;
|
|
27626
|
-
function
|
|
27718
|
+
async function withTimeout2(promise3, ms, label) {
|
|
27719
|
+
let timer;
|
|
27720
|
+
const timeout = new Promise((_, reject) => {
|
|
27721
|
+
timer = setTimeout(() => reject(new Error(`Timeout: ${label} exceeded ${ms}ms`)), ms);
|
|
27722
|
+
});
|
|
27723
|
+
try {
|
|
27724
|
+
return await Promise.race([promise3, timeout]);
|
|
27725
|
+
} finally {
|
|
27726
|
+
clearTimeout(timer);
|
|
27727
|
+
}
|
|
27728
|
+
}
|
|
27729
|
+
function createRalphLoopTools(ctx, internalSessions, deps = {}) {
|
|
27730
|
+
const iterationTimeoutMs = deps.iterationTimeoutMs ?? DEFAULT_ITERATION_TIMEOUT_MS;
|
|
27627
27731
|
const ralph_loop = tool({
|
|
27628
27732
|
description: `Start autonomous loop. Agent keeps working until it outputs ${COMPLETION_MARKER} or max iterations reached.
|
|
27629
27733
|
Use this for tasks that require multiple rounds of autonomous work (implementation, refactoring, migration).
|
|
@@ -27651,29 +27755,43 @@ The agent will be prompted to continue from where it left off each iteration.`,
|
|
|
27651
27755
|
sessionID
|
|
27652
27756
|
};
|
|
27653
27757
|
activeLoops.set(sessionID, state);
|
|
27654
|
-
log(`Ralph Loop started: ${agentName}, max ${maxIter} iterations`);
|
|
27758
|
+
log(`Ralph Loop started: ${agentName}, max ${maxIter} iterations, timeout ${iterationTimeoutMs}ms/iter`);
|
|
27655
27759
|
try {
|
|
27656
27760
|
for (let i = 0;i < maxIter && state.active; i++) {
|
|
27657
27761
|
state.iteration = i + 1;
|
|
27658
27762
|
toolCtx.metadata({ title: `Ralph Loop [${i + 1}/${maxIter}] \u2014 ${agentName}` });
|
|
27659
27763
|
const prompt = i === 0 ? args.prompt : buildContinuationPrompt(args.prompt, i + 1);
|
|
27660
|
-
|
|
27661
|
-
|
|
27662
|
-
|
|
27663
|
-
|
|
27664
|
-
|
|
27665
|
-
|
|
27666
|
-
|
|
27667
|
-
|
|
27764
|
+
try {
|
|
27765
|
+
await withTimeout2(ctx.client.session.prompt({
|
|
27766
|
+
path: { id: sessionID },
|
|
27767
|
+
body: {
|
|
27768
|
+
parts: [{ type: "text", text: prompt }],
|
|
27769
|
+
agent: agentName
|
|
27770
|
+
},
|
|
27771
|
+
query: { directory: ctx.directory }
|
|
27772
|
+
}), iterationTimeoutMs, `Ralph Loop iteration ${i + 1}`);
|
|
27773
|
+
} catch (iterError) {
|
|
27774
|
+
const msg = iterError instanceof Error ? iterError.message : String(iterError);
|
|
27775
|
+
if (msg.startsWith("Timeout:")) {
|
|
27776
|
+
log(`Ralph Loop iteration ${i + 1} timed out`);
|
|
27777
|
+
toolCtx.metadata({ title: `Ralph Loop TIMEOUT [${i + 1}/${maxIter}]` });
|
|
27778
|
+
return `## Ralph Loop Timeout (iteration ${i + 1}/${maxIter})
|
|
27779
|
+
|
|
27780
|
+
**Agent**: ${agentName}
|
|
27781
|
+
**Timeout**: Iteration did not complete within ${(iterationTimeoutMs / 1000).toFixed(0)}s`;
|
|
27782
|
+
}
|
|
27783
|
+
throw iterError;
|
|
27784
|
+
}
|
|
27668
27785
|
const messagesResp = await ctx.client.session.messages({
|
|
27669
27786
|
path: { id: sessionID },
|
|
27670
27787
|
query: { directory: ctx.directory }
|
|
27671
27788
|
});
|
|
27672
27789
|
const messages = messagesResp.data ?? [];
|
|
27673
27790
|
const lastAssistant = messages.filter((m) => m.info?.role === "assistant").pop();
|
|
27674
|
-
const
|
|
27791
|
+
const rawResult = lastAssistant?.parts?.filter((p) => p.type === "text" && p.text).map((p) => p.text).join(`
|
|
27675
27792
|
`) ?? "";
|
|
27676
|
-
|
|
27793
|
+
const resultText = sanitizeSpawnResult(rawResult);
|
|
27794
|
+
if (rawResult.includes(COMPLETION_MARKER)) {
|
|
27677
27795
|
toolCtx.metadata({ title: `Ralph Loop DONE [${i + 1}/${maxIter}]` });
|
|
27678
27796
|
log(`Ralph Loop completed at iteration ${i + 1}`);
|
|
27679
27797
|
const cleaned = resultText.replace(COMPLETION_MARKER, "").trim();
|
|
@@ -28421,12 +28539,17 @@ var OpenCodeUltra = async (ctx) => {
|
|
|
28421
28539
|
const resolveAgentModel = (agent) => {
|
|
28422
28540
|
return agents[agent]?.model;
|
|
28423
28541
|
};
|
|
28542
|
+
const safetyConfig = pluginConfig.safety ?? {};
|
|
28424
28543
|
const spawnAgent = createSpawnAgentTool(ctx, internalSessions, {
|
|
28425
28544
|
pool,
|
|
28426
28545
|
categories: pluginConfig.categories,
|
|
28427
|
-
resolveAgentModel
|
|
28546
|
+
resolveAgentModel,
|
|
28547
|
+
maxTotalSpawned: safetyConfig.maxTotalSpawned,
|
|
28548
|
+
agentTimeoutMs: safetyConfig.agentTimeoutMs
|
|
28549
|
+
});
|
|
28550
|
+
const ralphTools = createRalphLoopTools(ctx, internalSessions, {
|
|
28551
|
+
iterationTimeoutMs: safetyConfig.agentTimeoutMs
|
|
28428
28552
|
});
|
|
28429
|
-
const ralphTools = createRalphLoopTools(ctx, internalSessions);
|
|
28430
28553
|
const batchRead = createBatchReadTool(ctx);
|
|
28431
28554
|
const ledgerSave = createLedgerSaveTool(ctx);
|
|
28432
28555
|
const ledgerLoad = createLedgerLoadTool(ctx);
|
package/package.json
CHANGED