Ctrl K
ComunidadGuías…

scripts/verify-opencode-permissions.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// Real OpenCode, disposable home, deterministic loopback model. No user data,
// real credentials or external model requests are used.
import assert from "node:assert/strict";
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { createServer } from "node:http";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";

assert.ok(process.argv[2], "Pass the absolute OpenCode CLI path");
const cli = resolve(process.argv[2]);
const home = mkdtempSync(join(tmpdir(), "omb-opencode-permissions-"));
const workspace = join(home, "workspace");
const external = join(home, "outside", "receipt.txt");
mkdirSync(workspace);
mkdirSync(join(home, "outside"));
writeFileSync(external, "OMB_EXTERNAL_READ_RECEIPT");
process.env = {
  PATH: process.env.PATH, HOME: home, USERPROFILE: home,
  XDG_CONFIG_HOME: join(home, "config"), XDG_DATA_HOME: join(home, "data"),
  XDG_CACHE_HOME: join(home, "cache"), XDG_STATE_HOME: join(home, "state"),
  OMB_DATA_DIR: join(home, "omb"), OPENCODE_DISABLE_DEFAULT_PLUGINS: "true",
  OPENCODE_DISABLE_MODELS_FETCH: "true", OPENMAUSBOT_PROBE_LOCAL_INJECT: "0",
  ...(process.env.SystemRoot ? { SystemRoot: process.env.SystemRoot } : {}),
};
let calls = 0;
let readSucceeded = false;
let failNextRequest = false;
let failedRequests = 0;
const server = createServer(async (req, res) => {
  let body = "";
  for await (const chunk of req) body += chunk;
  if (req.method !== "POST" || !req.url?.endsWith("/chat/completions")) { res.writeHead(404).end(); return; }
  if (failNextRequest) {
    failNextRequest = false;
    failedRequests++;
    res.writeHead(400, { "Content-Type": "application/json" });
    res.end(JSON.stringify({ error: { message: "Synthetic provider failure", type: "invalid_request_error" } }));
    return;
  }
  const payload = JSON.parse(body);
  const toolResult = payload.messages.at(-1)?.role === "tool";
  readSucceeded ||= toolResult && JSON.stringify(payload.messages.at(-1)).includes("OMB_EXTERNAL_READ_RECEIPT");
  const tool = { index: 0, id: `read-${++calls}`, type: "function", function: { name: "read", arguments: JSON.stringify({ filePath: external }) } };
  res.writeHead(200, { "Content-Type": "text/event-stream" });
  const chunk = (delta: unknown, finish_reason: string | null) => `data: ${JSON.stringify({id:`fixture-${calls}`,object:"chat.completion.chunk",created:0,model:"fixture",choices:[{index:0,delta,finish_reason}]})}\n\n`;
  res.end(chunk(toolResult ? {role:"assistant",content:"Fixture complete."} : {role:"assistant",tool_calls:[tool]}, null)
    + chunk({}, toolResult ? "stop" : "tool_calls") + "data: [DONE]\n\n");
});
await new Promise<void>(done => server.listen(0, "127.0.0.1", done));
const port = (server.address() as {port:number}).port;
writeFileSync(join(workspace, "opencode.json"), JSON.stringify({
  enabled_providers: ["fixture"], autoupdate: false, share: "disabled",
  provider: { fixture: { npm: "@ai-sdk/openai-compatible", name: "Offline fixture", options: { baseURL: `http://127.0.0.1:${port}/v1`, apiKey: "fixture-only" }, models: { fixture: { name: "Fixture", limit: {context:32000,output:1000} } } } },
  model: "fixture/fixture", permission: { external_directory: "ask" },
}));
const { ensureDirs, NATIVE_DIR } = await import("../server/config.ts");
const { createOpenCodeDriver } = await import("../server/drivers/acp/opencode-go.ts");
const { recordEvents } = await import("../server/testing/events.ts");
ensureDirs();
const instance = await createOpenCodeDriver(async () => ({ default:"fixture/fixture", options:[{id:"fixture/fixture",label:"Fixture"}] })).create({
  instanceId:"permission-fixture", displayName:"Fixture", enabled:true,
  environment:{OPENCODE_API_KEY:"fixture-only"}, config:{cli,fullAuto:false},
});
const recorder = recordEvents(instance.adapter);
try {
  let resumeCursor: string | undefined;
  for (const approvalMode of ["full", "ask", "full"] as const) {
    readSucceeded = false;
    const {turnId} = await instance.adapter.sendTurn({threadId:"fixture",text:"Read the receipt outside the workspace.",cwd:workspace,model:"fixture/fixture",approvalMode,resumeCursor});
    if (approvalMode === "ask") {
      const opened = await recorder.until(e => e.turnId === turnId && e.type === "request.opened", 30000);
      await instance.adapter.respondToRequest("fixture", opened.requestId!, {behavior:"deny"});
    }
    const completed = await recorder.until(e => e.turnId === turnId && e.type === "turn.completed", 30000);
    assert.equal(completed.type, "turn.completed", JSON.stringify(completed));
    assert.ok(completed.type === "turn.completed" && completed.ok, JSON.stringify(completed));
    assert.equal(readSucceeded, approvalMode === "full", "Actual external read must follow the current mode");
    if (approvalMode === "full") assert.equal(recorder.events.some(e => e.turnId === turnId && e.type === "request.opened"), false);
    const started = recorder.events.find(e => e.turnId === turnId && e.type === "session.started");
    resumeCursor = started?.type === "session.started" ? started.sessionId ?? undefined : undefined;
    assert.ok(resumeCursor, "The next turn must exercise session resumption");
    console.log(JSON.stringify({approvalMode,readSucceeded,resumed:true}));
  }

  // Use the real provider's ACP error path, then retry explicitly in the same
  // conversation. No hidden replay: a failed turn must finish before recovery.
  failNextRequest = true;
  const failed = await instance.adapter.sendTurn({threadId:"fixture",text:"Read the receipt again.",cwd:workspace,model:"fixture/fixture",approvalMode:"full",resumeCursor});
  const failure = await recorder.until(e => e.turnId === failed.turnId && e.type === "turn.completed", 30000);
  assert.ok(failure.type === "turn.completed" && !failure.ok, JSON.stringify(failure));
  assert.equal(failedRequests, 1);
  assert.ok(recorder.events.some(e => e.turnId === failed.turnId && e.type === "runtime.error"));
  assert.equal(instance.adapter.hasSession("fixture"), false, "The failed turn must release the thread");
  const nativeEvents = readFileSync(join(NATIVE_DIR, "fixture.ndjson"), "utf8").trim().split("\n").map(line => JSON.parse(line));
  assert.equal(nativeEvents.filter(event => event.msg?.close === "rpc-failure").length, 1, "Retire the failed ACP process");

  readSucceeded = false;
  const retried = await instance.adapter.sendTurn({threadId:"fixture",text:"Read the receipt now.",cwd:workspace,model:"fixture/fixture",approvalMode:"full",resumeCursor});
  const recovered = await recorder.until(e => e.turnId === retried.turnId && e.type === "turn.completed", 30000);
  assert.ok(recovered.type === "turn.completed" && recovered.ok, JSON.stringify(recovered));
  assert.equal(readSucceeded, true);
  const resumed = recorder.events.find(e => e.turnId === retried.turnId && e.type === "session.started");
  assert.ok(resumed?.type === "session.started");
  assert.equal(resumed.sessionId, resumeCursor, "Recovery must retain the real provider conversation");
  assert.equal(recorder.events.filter(e => e.turnId === failed.turnId && e.type === "turn.completed").length, 1);
  console.log(JSON.stringify({providerFailureReported:true,explicitRetryRecovered:true,sameSession:true}));
} finally {
  recorder.stop();
  await instance.dispose();
  server.closeAllConnections();
  await new Promise<void>(done => server.close(() => done()));
  rmSync(home, { recursive:true, force:true });
}