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.
Files changed (2) hide show
  1. package/dist/index.js +142 -19
  2. 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 result = lastAssistant?.parts?.filter((p) => p.type === "text" && p.text).map((p) => p.text).join(`
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
- log(`spawn_agent: ${agent} error`, { error: msg });
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 createRalphLoopTools(ctx, internalSessions) {
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
- await ctx.client.session.prompt({
27661
- path: { id: sessionID },
27662
- body: {
27663
- parts: [{ type: "text", text: prompt }],
27664
- agent: agentName
27665
- },
27666
- query: { directory: ctx.directory }
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 resultText = lastAssistant?.parts?.filter((p) => p.type === "text" && p.text).map((p) => p.text).join(`
27791
+ const rawResult = lastAssistant?.parts?.filter((p) => p.type === "text" && p.text).map((p) => p.text).join(`
27675
27792
  `) ?? "";
27676
- if (resultText.includes(COMPLETION_MARKER)) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-ultra",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Lightweight OpenCode 1.2.x plugin — ultrawork mode, multi-agent orchestration, rules injection",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",