@diologue/local-agent 0.3.0 → 0.5.0

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.mjs CHANGED
@@ -1266,7 +1266,10 @@ var agentMessageBodySchema = z2.object({
1266
1266
  /** Optional user-chosen routing for this turn. When absent the cloud
1267
1267
  * picks the default (CODING_AGENT_DEFAULT_PROVIDER/MODEL). */
1268
1268
  preferredProvider: z2.string().min(1).max(100).optional(),
1269
- preferredModel: z2.string().min(1).max(200).optional()
1269
+ preferredModel: z2.string().min(1).max(200).optional(),
1270
+ /** Tool-gating policy. Absent → "auto" (run everything) so older clients
1271
+ * keep their current behaviour. */
1272
+ permissionMode: z2.enum(["auto", "confirm"]).optional()
1270
1273
  });
1271
1274
  var writeEvent = (res, event) => {
1272
1275
  res.write(`data: ${JSON.stringify(event)}
@@ -1322,7 +1325,8 @@ var createAgentRouter = (deps) => {
1322
1325
  signal: controller.signal,
1323
1326
  broker: (payload, observer) => broker.request(payload, observer),
1324
1327
  preferredProvider: parsed.data.preferredProvider,
1325
- preferredModel: parsed.data.preferredModel
1328
+ preferredModel: parsed.data.preferredModel,
1329
+ permissionMode: parsed.data.permissionMode
1326
1330
  })) {
1327
1331
  if (controller.signal.aborted) {
1328
1332
  logAgentRoute(
@@ -1493,6 +1497,36 @@ var createLlmChunkRouter = (deps) => {
1493
1497
  });
1494
1498
  return router;
1495
1499
  };
1500
+ var permissionDecisionBodySchema = z2.object({
1501
+ sessionId: z2.string().min(1),
1502
+ permissionId: z2.string().min(1),
1503
+ response: z2.enum(["once", "always", "reject"])
1504
+ });
1505
+ var createPermissionRouter = (deps) => {
1506
+ const router = createRouter2();
1507
+ router.post("/", async (req, res) => {
1508
+ const parsed = permissionDecisionBodySchema.safeParse(req.body);
1509
+ if (!parsed.success) {
1510
+ res.status(400).json({ error: "invalid_body", issues: parsed.error.issues });
1511
+ return;
1512
+ }
1513
+ if (!deps.adapter.replyPermission) {
1514
+ res.status(501).json({ error: "permissions_unsupported" });
1515
+ return;
1516
+ }
1517
+ const matched = await deps.adapter.replyPermission(
1518
+ parsed.data.sessionId,
1519
+ parsed.data.permissionId,
1520
+ parsed.data.response
1521
+ );
1522
+ if (!matched) {
1523
+ res.status(404).json({ error: "no_active_permission" });
1524
+ return;
1525
+ }
1526
+ res.json({ ok: true });
1527
+ });
1528
+ return router;
1529
+ };
1496
1530
 
1497
1531
  // src/routes/llm-shim.ts
1498
1532
  import { Router as createRouter3 } from "express";
@@ -2359,13 +2393,30 @@ var MockOpenCodeAdapter = class {
2359
2393
  };
2360
2394
 
2361
2395
  // src/adapters/opencode-event-mapper.ts
2396
+ var MAX_TOOL_OUTPUT_CHARS = 4e3;
2397
+ var truncateOutput = (raw) => {
2398
+ if (!raw) return void 0;
2399
+ if (raw.length <= MAX_TOOL_OUTPUT_CHARS) return raw;
2400
+ const omitted = raw.length - MAX_TOOL_OUTPUT_CHARS;
2401
+ return `${raw.slice(0, MAX_TOOL_OUTPUT_CHARS)}
2402
+ \u2026 [${omitted} more characters truncated]`;
2403
+ };
2362
2404
  var createMapState = (ourSessionId, openCodeSessionId) => ({
2363
2405
  ourSessionId,
2364
2406
  openCodeSessionId,
2365
2407
  startedTools: /* @__PURE__ */ new Set(),
2366
2408
  endedTools: /* @__PURE__ */ new Set(),
2367
- announcedPatches: /* @__PURE__ */ new Set()
2409
+ announcedPatches: /* @__PURE__ */ new Set(),
2410
+ announcedPermissions: /* @__PURE__ */ new Set()
2368
2411
  });
2412
+ var extractPermissionCommand = (metadata) => {
2413
+ if (!metadata) return void 0;
2414
+ for (const key of ["command", "cmd", "url", "pattern", "filePath"]) {
2415
+ const v = metadata[key];
2416
+ if (typeof v === "string" && v) return v;
2417
+ }
2418
+ return void 0;
2419
+ };
2369
2420
  var EMPTY = { events: [], done: false };
2370
2421
  var mapEvent = (item, state) => {
2371
2422
  const sid = item.properties?.sessionID;
@@ -2375,6 +2426,25 @@ var mapEvent = (item, state) => {
2375
2426
  if (item.type === "session.idle" && sid === state.openCodeSessionId) {
2376
2427
  return { events: [{ type: "done" }], done: true };
2377
2428
  }
2429
+ if (item.type === "permission.updated") {
2430
+ const permissionId = item.properties?.id;
2431
+ if (!permissionId || state.announcedPermissions.has(permissionId)) {
2432
+ return EMPTY;
2433
+ }
2434
+ state.announcedPermissions.add(permissionId);
2435
+ return {
2436
+ events: [
2437
+ {
2438
+ type: "permission_request",
2439
+ permissionId,
2440
+ tool: item.properties?.type ?? "tool",
2441
+ title: item.properties?.title,
2442
+ command: extractPermissionCommand(item.properties?.metadata)
2443
+ }
2444
+ ],
2445
+ done: false
2446
+ };
2447
+ }
2378
2448
  if (item.type !== "message.part.updated") {
2379
2449
  return EMPTY;
2380
2450
  }
@@ -2413,7 +2483,10 @@ var mapEvent = (item, state) => {
2413
2483
  type: "tool_call_end",
2414
2484
  toolCallId: callId,
2415
2485
  ok: status === "completed",
2416
- summary: status === "error" ? part.state?.error ?? "Tool failed" : part.state?.title
2486
+ summary: status === "error" ? part.state?.error ?? "Tool failed" : part.state?.title,
2487
+ output: truncateOutput(
2488
+ status === "error" ? part.state?.error : part.state?.output
2489
+ )
2417
2490
  };
2418
2491
  return { events: [event], done: false };
2419
2492
  }
@@ -2472,6 +2545,7 @@ init_engine_locator_npm();
2472
2545
  var DIOLOGUE_PROVIDER_ID = "diologue";
2473
2546
  var DIOLOGUE_MODEL_ID = "diologue-routed";
2474
2547
  var END_OF_TURN_GRACE_MS = 600;
2548
+ var PERMISSION_DECISION_TIMEOUT_MS = 18e4;
2475
2549
  var EDIT_DIRECTIVE = "You are an autonomous coding agent operating on a real git repository. Carry out the request by editing files directly with your tools (write / edit / patch / bash) \u2014 do not just describe the change, outline steps, or print code for the user to copy. Read only what you need, make the edits on disk, and finish once the change has actually been written.";
2476
2550
  var logAdapter = (message) => {
2477
2551
  console.error(`[opencode-adapter] ${message}`);
@@ -2485,6 +2559,9 @@ var OpenCodeProcessAdapter = class {
2485
2559
  /** Maps our sessionId → opencode session id so successive turns within
2486
2560
  * one coding-agent session reuse the same opencode conversation. */
2487
2561
  sessionMap = /* @__PURE__ */ new Map();
2562
+ /** In-flight turns by our sessionId, so replyPermission() can reach the
2563
+ * engine client to resolve a paused tool call. */
2564
+ activeTurns = /* @__PURE__ */ new Map();
2488
2565
  async resolveLocator() {
2489
2566
  if (this.options.engineLocator) {
2490
2567
  return this.options.engineLocator;
@@ -2527,14 +2604,17 @@ var OpenCodeProcessAdapter = class {
2527
2604
  }
2528
2605
  },
2529
2606
  model: `${DIOLOGUE_PROVIDER_ID}/${DIOLOGUE_MODEL_ID}`,
2530
- // Auto-approve tool execution. opencode gates edit/bash/webfetch
2531
- // behind a permission prompt; this helper runs headless with no
2532
- // approver, so without this the agent's writes are never applied to
2533
- // the repo and a turn finishes having changed nothing.
2607
+ // Permission policy. The ADAPTER is the policy engine, not this config:
2608
+ // - edit stays "allow" edits land in the working tree and are
2609
+ // reversible via the chat's Keep/Revert, so we never prompt on them.
2610
+ // - bash + webfetch are "ask" so opencode pauses them. In "auto" mode
2611
+ // the adapter auto-approves instantly (behaves like allow); in
2612
+ // "confirm" mode it forwards an AgentPermissionRequest to the browser
2613
+ // and resumes only once the user replies. See streamMessage().
2534
2614
  permission: {
2535
2615
  edit: "allow",
2536
- bash: "allow",
2537
- webfetch: "allow"
2616
+ bash: "ask",
2617
+ webfetch: "ask"
2538
2618
  }
2539
2619
  };
2540
2620
  }
@@ -2579,6 +2659,44 @@ var OpenCodeProcessAdapter = class {
2579
2659
  );
2580
2660
  return id;
2581
2661
  }
2662
+ /** Send a decision for a paused permission to opencode. Best-effort: a
2663
+ * failure is logged but not thrown — the worst case is the turn hangs
2664
+ * until the grace/abort path tears it down. */
2665
+ async replyToOpencode(client, openCodeSessionId, repoPath, permissionId, response) {
2666
+ try {
2667
+ await client.postSessionIdPermissionsPermissionId({
2668
+ path: { id: openCodeSessionId, permissionID: permissionId },
2669
+ query: { directory: repoPath },
2670
+ body: { response }
2671
+ });
2672
+ logAdapter(
2673
+ `permission reply id=${permissionId.slice(0, 8)} response=${response}`
2674
+ );
2675
+ } catch (err) {
2676
+ logAdapter(
2677
+ `permission reply FAILED id=${permissionId.slice(0, 8)} response=${response} error=${err instanceof Error ? err.message : String(err)}`
2678
+ );
2679
+ }
2680
+ }
2681
+ /** Apply a browser decision to a paused permission. Invoked by the
2682
+ * /agent/permission route on a separate HTTP request from the SSE stream. */
2683
+ async replyPermission(sessionId, permissionId, response) {
2684
+ const turn = this.activeTurns.get(sessionId);
2685
+ if (!turn) return false;
2686
+ const timer = turn.pending.get(permissionId);
2687
+ if (timer) {
2688
+ clearTimeout(timer);
2689
+ turn.pending.delete(permissionId);
2690
+ }
2691
+ await this.replyToOpencode(
2692
+ turn.client,
2693
+ turn.openCodeSessionId,
2694
+ turn.repoPath,
2695
+ permissionId,
2696
+ response
2697
+ );
2698
+ return true;
2699
+ }
2582
2700
  async *streamMessage(request) {
2583
2701
  let handle;
2584
2702
  try {
@@ -2613,6 +2731,14 @@ var OpenCodeProcessAdapter = class {
2613
2731
  );
2614
2732
  let sawAnyPatch = false;
2615
2733
  let lastPatchHash = "";
2734
+ const permissionMode = request.permissionMode ?? "auto";
2735
+ const activeTurn = {
2736
+ client: handle.client,
2737
+ openCodeSessionId,
2738
+ repoPath: request.repoPath,
2739
+ pending: /* @__PURE__ */ new Map()
2740
+ };
2741
+ this.activeTurns.set(request.sessionId, activeTurn);
2616
2742
  let activeBrokerHandle = null;
2617
2743
  let streamScopedBroker = null;
2618
2744
  if (request.broker) {
@@ -2725,6 +2851,34 @@ ${request.prompt}` }
2725
2851
  );
2726
2852
  }
2727
2853
  for (const event of mapped.events) {
2854
+ if (event.type === "permission_request") {
2855
+ if (permissionMode === "confirm") {
2856
+ const timer = setTimeout(() => {
2857
+ activeTurn.pending.delete(event.permissionId);
2858
+ void this.replyToOpencode(
2859
+ handle.client,
2860
+ openCodeSessionId,
2861
+ request.repoPath,
2862
+ event.permissionId,
2863
+ "reject"
2864
+ );
2865
+ logAdapter(
2866
+ `permission auto-rejected (timeout) id=${event.permissionId.slice(0, 8)} session=${request.sessionId.slice(0, 8)}`
2867
+ );
2868
+ }, PERMISSION_DECISION_TIMEOUT_MS);
2869
+ activeTurn.pending.set(event.permissionId, timer);
2870
+ yield event;
2871
+ } else {
2872
+ void this.replyToOpencode(
2873
+ handle.client,
2874
+ openCodeSessionId,
2875
+ request.repoPath,
2876
+ event.permissionId,
2877
+ "once"
2878
+ );
2879
+ }
2880
+ continue;
2881
+ }
2728
2882
  if (event.type === "diff_proposed" && mapped.patchToFetch) {
2729
2883
  sawAnyPatch = true;
2730
2884
  lastPatchHash = mapped.patchToFetch.hash;
@@ -2743,6 +2897,18 @@ ${request.prompt}` }
2743
2897
  } catch {
2744
2898
  }
2745
2899
  request.signal?.removeEventListener("abort", stopOnAbort);
2900
+ for (const [permId, timer] of activeTurn.pending) {
2901
+ clearTimeout(timer);
2902
+ void this.replyToOpencode(
2903
+ handle.client,
2904
+ openCodeSessionId,
2905
+ request.repoPath,
2906
+ permId,
2907
+ "reject"
2908
+ );
2909
+ }
2910
+ activeTurn.pending.clear();
2911
+ this.activeTurns.delete(request.sessionId);
2746
2912
  activeBrokerHandle?.release();
2747
2913
  streamScopedBroker?.close("turn_ended");
2748
2914
  }
@@ -2850,6 +3016,7 @@ var createApp = (options) => {
2850
3016
  app.use("/agent", createAgentRouter({ state, adapter, brokerRegistry }));
2851
3017
  app.use("/agent/llm-response", createLlmResponseRouter({ brokerRegistry }));
2852
3018
  app.use("/agent/llm-chunk", createLlmChunkRouter({ brokerRegistry }));
3019
+ app.use("/agent/permission", createPermissionRouter({ adapter }));
2853
3020
  app.use("/llm-shim", createLlmShimRouter());
2854
3021
  app.use((req, res) => {
2855
3022
  res.status(404).json({ error: "not_found", method: req.method, path: req.path });