@cabane/companion 0.6.186 → 0.6.188

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/runtime.js CHANGED
@@ -230,7 +230,7 @@ var localAgentConfigSchema = z2.object({
230
230
  // yourself, `claudeCode: { autoMemory: true }` hands auto-memory back to your
231
231
  // own `~/.claude/settings.json` — Cabane then stops
232
232
  // injecting the off switch and your normal Claude Code memory workflow applies
233
- // (in coding mode, where the checkout's project settings are read).
233
+ // (the user and project settings load on every Claude Code turn).
234
234
  claudeCode: z2.object({ autoMemory: z2.boolean().optional() }).strict().optional()
235
235
  }).strict();
236
236
  var agentsConfigSchema = z2.record(z2.string(), localAgentConfigSchema);
@@ -847,7 +847,6 @@ var CompanionStateHub = class {
847
847
  agent_id: a.agentId,
848
848
  username: a.username,
849
849
  display_name: a.displayName,
850
- mode: a.mode,
851
850
  has_credential: a.hasCredential,
852
851
  missing_secrets: a.missingSecrets
853
852
  }))
@@ -2345,14 +2344,19 @@ var DeviceApi = class {
2345
2344
  return parsed;
2346
2345
  }
2347
2346
  // Pull the agents this device should run, across all the owner's workspaces.
2348
- getAssignments() {
2349
- return this.request("GET", "/api/companion/assignments");
2347
+ getAssignments(signal) {
2348
+ return this.request("GET", "/api/companion/assignments", void 0, requestSignal(signal));
2350
2349
  }
2351
2350
  // Ask again for the credential of an agent assigned to this device, when the saved copy is
2352
2351
  // gone. The previous credential stops working; any agent not assigned here is a 404, and a
2353
2352
  // second ask inside a minute a 429.
2354
- reissueAgentCredential(agentId) {
2355
- return this.request("POST", `/api/companion/agents/${encodeURIComponent(agentId)}/credential`);
2353
+ reissueAgentCredential(agentId, signal) {
2354
+ return this.request(
2355
+ "POST",
2356
+ `/api/companion/agents/${encodeURIComponent(agentId)}/credential`,
2357
+ void 0,
2358
+ requestSignal(signal, 3e4)
2359
+ );
2356
2360
  }
2357
2361
  getRuns(cursor) {
2358
2362
  const query = new URLSearchParams({ limit: "100", order: "asc" });
@@ -2392,6 +2396,10 @@ function errorMessage2(status, body) {
2392
2396
  if (typeof body === "string" && body.length > 0) return `${status} ${body.slice(0, 200)}`;
2393
2397
  return `${status} error`;
2394
2398
  }
2399
+ function requestSignal(signal, timeoutMs = 5e3) {
2400
+ const deadline = AbortSignal.timeout(timeoutMs);
2401
+ return signal ? AbortSignal.any([signal, deadline]) : deadline;
2402
+ }
2395
2403
 
2396
2404
  // src/credentials.ts
2397
2405
  import {
@@ -2620,35 +2628,9 @@ function noResume() {
2620
2628
  // packages/agent-runtime/src/version.ts
2621
2629
  var TURN_PROTOCOL_VERSION = 1;
2622
2630
 
2623
- // packages/agent-runtime/src/host-policy.ts
2624
- import { z as z6 } from "zod";
2625
- var hostPolicySchema = z6.object({
2626
- // Host filesystem and shell: `Bash`/`Read`/`Write`/`Edit`/`Glob`/`Grep`, the notebook pair,
2627
- // git-worktree tools and `REPL`. Off on the locked assistant surface; on in the coding posture.
2628
- hostFs: z6.boolean(),
2629
- // Public web (`WebSearch` / `WebFetch`). Not host reach; a grant like the rest.
2630
- web: z6.boolean(),
2631
- // Browser automation (the Playwright MCP surface). Varies by host.
2632
- browser: z6.boolean(),
2633
- // User-configured MCP servers permitted.
2634
- userMcp: z6.boolean(),
2635
- // Subagent dispatch (`Task` / `Agent`) — a grant: `false` on the locked surface, `true` in the
2636
- // coding posture, where the coding allow-hook reaches sub-agent tool calls. A subagent completes
2637
- // within the turn, so it's not an invariant like `scheduling`.
2638
- subagents: z6.boolean(),
2639
- // ── Hard platform invariants — always denied, never granted ────────────────
2640
- // Deferred re-invocation / scheduling (`ScheduleWakeup`, the `Cron*` / `Task*` families,
2641
- // `Monitor`, `Workflow`, …). A turn is one query that resolves on `result`; a scheduled callback
2642
- // fires after the reply window has closed and strands the agent.
2643
- scheduling: z6.literal("never"),
2644
- // Human-facing UI prompts (`AskUserQuestion`). Nothing in a Cabane conversation can answer a
2645
- // structured prompt, so the call hangs the turn. The agent asks in its reply instead.
2646
- uiPrompts: z6.literal("never")
2647
- });
2648
-
2649
2631
  // packages/agent-runtime/src/turn-event.ts
2650
- import { z as z7 } from "zod";
2651
- var turnEventSchema = z7.discriminatedUnion("type", [
2632
+ import { z as z6 } from "zod";
2633
+ var turnEventSchema = z6.discriminatedUnion("type", [
2652
2634
  // The runtime's opaque session state, emitted when the adapter learns it (e.g. the SDK
2653
2635
  // `system/init` frame). The platform stores `state` verbatim per (conversation, agent) and
2654
2636
  // hands it back next turn; only the adapter knows what it means.
@@ -2660,21 +2642,21 @@ var turnEventSchema = z7.discriminatedUnion("type", [
2660
2642
  // server rewinds the mark, so the next turn rebuilds a full catch-up (this turn is unavoidably
2661
2643
  // lossy — the degrade is known only after the manifest was committed). A plain boolean; the
2662
2644
  // runtime-specific reason stays in the adapter's `onWarn` log. Optional.
2663
- z7.object({
2664
- type: z7.literal("session"),
2665
- state: z7.string(),
2666
- degraded: z7.boolean().optional()
2645
+ z6.object({
2646
+ type: z6.literal("session"),
2647
+ state: z6.string(),
2648
+ degraded: z6.boolean().optional()
2667
2649
  }),
2668
2650
  // One readable thinking summary. Transient — surfaced live, never persisted as content.
2669
- z7.object({ type: z7.literal("thinking"), text: z7.string() }),
2651
+ z6.object({ type: z6.literal("thinking"), text: z6.string() }),
2670
2652
  // Assistant text. `terminal: false` is interim narration (commits as `progress`);
2671
2653
  // `terminal: true` is the turn's closing reply (commits as `final`).
2672
- z7.object({ type: z7.literal("text"), body: z7.string(), terminal: z7.boolean() }),
2654
+ z6.object({ type: z6.literal("text"), body: z6.string(), terminal: z6.boolean() }),
2673
2655
  // Runtime/provider-authored prose about the turn — a failure's explanation, or a line the
2674
2656
  // harness wrote on a turn it recovered. Not the agent's words: the pump persists it as
2675
2657
  // `runtime_notice`, drawn in the SystemNote voice. Adapters emit it only from positive runtime
2676
2658
  // evidence; the web never classifies English strings.
2677
- z7.object({ type: z7.literal("runtime_notice"), body: z7.string() }),
2659
+ z6.object({ type: z6.literal("runtime_notice"), body: z6.string() }),
2678
2660
  // A tool-activity transition. `name` is prefix-stripped (`cabane_read`, not
2679
2661
  // `mcp__cabane__cabane_read`); `summary` is the short card label; `phase` pairs `start` with a
2680
2662
  // later `done`/`error` on the same `id`.
@@ -2683,15 +2665,15 @@ var turnEventSchema = z7.discriminatedUnion("type", [
2683
2665
  // log are a consumer concern (docs/turn-protocol.md). `mcpServer` is the MCP server the call
2684
2666
  // came from (`cabane`, `playwright`, …), derived before `name` dropped the prefix; null for a
2685
2667
  // host tool. Optional, read as null when absent.
2686
- z7.object({
2687
- type: z7.literal("tool"),
2688
- id: z7.string(),
2689
- name: z7.string(),
2690
- phase: z7.enum(["start", "done", "error"]),
2691
- summary: z7.string(),
2692
- input: z7.unknown().optional(),
2693
- result: z7.unknown().optional(),
2694
- mcpServer: z7.string().nullable().optional()
2668
+ z6.object({
2669
+ type: z6.literal("tool"),
2670
+ id: z6.string(),
2671
+ name: z6.string(),
2672
+ phase: z6.enum(["start", "done", "error"]),
2673
+ summary: z6.string(),
2674
+ input: z6.unknown().optional(),
2675
+ result: z6.unknown().optional(),
2676
+ mcpServer: z6.string().nullable().optional()
2695
2677
  }),
2696
2678
  // The turn's terminal outcome. `ok:false` carries a machine reason (`no_session`, an error
2697
2679
  // code), surfaced by the host's clear-PATCH as `errorReason`.
@@ -2712,70 +2694,70 @@ var turnEventSchema = z7.discriminatedUnion("type", [
2712
2694
  // started with); `resolvedConfig` the knobs the harness reported (`{ reasoningEffort }` for
2713
2695
  // codex). Known only after the run streams, so they ride this event home and the host relays
2714
2696
  // them on settle. A cancel has no result event at all.
2715
- z7.object({
2716
- type: z7.literal("result"),
2717
- ok: z7.boolean(),
2718
- reason: z7.string().optional(),
2719
- usage: z7.object({
2720
- inputTokens: z7.number(),
2721
- outputTokens: z7.number(),
2722
- cacheReadTokens: z7.number().optional(),
2723
- cacheCreationTokens: z7.number().optional(),
2724
- contextTokens: z7.number().optional(),
2725
- contextWindow: z7.number().optional(),
2697
+ z6.object({
2698
+ type: z6.literal("result"),
2699
+ ok: z6.boolean(),
2700
+ reason: z6.string().optional(),
2701
+ usage: z6.object({
2702
+ inputTokens: z6.number(),
2703
+ outputTokens: z6.number(),
2704
+ cacheReadTokens: z6.number().optional(),
2705
+ cacheCreationTokens: z6.number().optional(),
2706
+ contextTokens: z6.number().optional(),
2707
+ contextWindow: z6.number().optional(),
2726
2708
  // The rest of the harness's context reading, when it gave one: where it will compact, its
2727
2709
  // `/context` breakdown, and every compaction it reported. Claude Code only.
2728
- contextCompactAt: z7.number().optional(),
2729
- contextCategories: z7.array(
2730
- z7.object({
2731
- name: z7.string(),
2732
- tokens: z7.number(),
2733
- kind: z7.enum(["used", "free", "buffer", "deferred"])
2710
+ contextCompactAt: z6.number().optional(),
2711
+ contextCategories: z6.array(
2712
+ z6.object({
2713
+ name: z6.string(),
2714
+ tokens: z6.number(),
2715
+ kind: z6.enum(["used", "free", "buffer", "deferred"])
2734
2716
  })
2735
2717
  ).optional(),
2736
- contextCompactions: z7.array(
2737
- z7.object({
2738
- trigger: z7.enum(["manual", "auto"]),
2739
- preTokens: z7.number(),
2740
- postTokens: z7.number().optional()
2718
+ contextCompactions: z6.array(
2719
+ z6.object({
2720
+ trigger: z6.enum(["manual", "auto"]),
2721
+ preTokens: z6.number(),
2722
+ postTokens: z6.number().optional()
2741
2723
  })
2742
2724
  ).optional()
2743
2725
  }).optional(),
2744
- resolvedModel: z7.string().optional(),
2745
- resolvedConfig: z7.object({
2746
- effort: z7.string().optional(),
2747
- thinking: z7.string().optional(),
2748
- reasoningEffort: z7.string().optional()
2726
+ resolvedModel: z6.string().optional(),
2727
+ resolvedConfig: z6.object({
2728
+ effort: z6.string().optional(),
2729
+ thinking: z6.string().optional(),
2730
+ reasoningEffort: z6.string().optional()
2749
2731
  }).optional(),
2750
2732
  // The harness-reported MCP inventory from this turn's init frame. Diagnostic only: names,
2751
2733
  // statuses and a count — never server definitions, credentials or session ids.
2752
2734
  // `initReceived:false` tells a missing init frame from a real empty inventory.
2753
- mcpInventory: z7.object({
2754
- initReceived: z7.boolean(),
2755
- servers: z7.array(z7.object({ name: z7.string(), status: z7.string() })),
2756
- toolCount: z7.number().int().nonnegative()
2735
+ mcpInventory: z6.object({
2736
+ initReceived: z6.boolean(),
2737
+ servers: z6.array(z6.object({ name: z6.string(), status: z6.string() })),
2738
+ toolCount: z6.number().int().nonnegative()
2757
2739
  }).optional()
2758
2740
  })
2759
2741
  ]);
2760
2742
 
2761
2743
  // packages/agent-runtime/src/turn-diagnostics.ts
2762
- import { z as z8 } from "zod";
2763
- var turnResultReasonSchema = z8.discriminatedUnion("kind", [
2764
- z8.object({ kind: z8.literal("usage_capped"), resetsAt: z8.string().optional() }),
2765
- z8.object({ kind: z8.literal("rate_limited") }),
2766
- z8.object({ kind: z8.literal("server_error") }),
2767
- z8.object({ kind: z8.literal("auth_expired") }),
2768
- z8.object({ kind: z8.literal("no_result") }),
2769
- z8.object({ kind: z8.literal("empty_result") }),
2770
- z8.object({ kind: z8.literal("empty_result_unverified") }),
2771
- z8.object({ kind: z8.literal("timeout_idle") }),
2772
- z8.object({ kind: z8.literal("timeout_total") }),
2773
- z8.object({ kind: z8.literal("cancelled") }),
2774
- z8.object({ kind: z8.literal("lease_lost") }),
2775
- z8.object({ kind: z8.literal("skipped") }),
2776
- z8.object({ kind: z8.literal("session_start_failed") }),
2777
- z8.object({ kind: z8.literal("workspace_tools_missing") }),
2778
- z8.object({ kind: z8.literal("runtime_error") })
2744
+ import { z as z7 } from "zod";
2745
+ var turnResultReasonSchema = z7.discriminatedUnion("kind", [
2746
+ z7.object({ kind: z7.literal("usage_capped"), resetsAt: z7.string().optional() }),
2747
+ z7.object({ kind: z7.literal("rate_limited") }),
2748
+ z7.object({ kind: z7.literal("server_error") }),
2749
+ z7.object({ kind: z7.literal("auth_expired") }),
2750
+ z7.object({ kind: z7.literal("no_result") }),
2751
+ z7.object({ kind: z7.literal("empty_result") }),
2752
+ z7.object({ kind: z7.literal("empty_result_unverified") }),
2753
+ z7.object({ kind: z7.literal("timeout_idle") }),
2754
+ z7.object({ kind: z7.literal("timeout_total") }),
2755
+ z7.object({ kind: z7.literal("cancelled") }),
2756
+ z7.object({ kind: z7.literal("lease_lost") }),
2757
+ z7.object({ kind: z7.literal("skipped") }),
2758
+ z7.object({ kind: z7.literal("session_start_failed") }),
2759
+ z7.object({ kind: z7.literal("workspace_tools_missing") }),
2760
+ z7.object({ kind: z7.literal("runtime_error") })
2779
2761
  ]);
2780
2762
  var turnOutcomes = ["success", "failure", "cancelled", "skipped"];
2781
2763
  var turnSessionModes = ["fresh", "resumed", "degraded"];
@@ -2787,123 +2769,125 @@ var turnFinalSources = [
2787
2769
  "marker",
2788
2770
  "none"
2789
2771
  ];
2790
- var turnDiagnosticsSchema = z8.object({
2791
- outcome: z8.enum(turnOutcomes),
2772
+ var turnDiagnosticsSchema = z7.object({
2773
+ outcome: z7.enum(turnOutcomes),
2792
2774
  resultReason: turnResultReasonSchema.nullable(),
2793
- sessionMode: z8.enum(turnSessionModes),
2794
- sessionFingerprint: z8.string().regex(/^[a-f0-9]{16}$/).nullable(),
2795
- eventCounts: z8.object({
2796
- session: z8.number().int().nonnegative(),
2797
- text: z8.number().int().nonnegative(),
2798
- runtime_notice: z8.number().int().nonnegative(),
2799
- thinking: z8.number().int().nonnegative(),
2800
- tool: z8.number().int().nonnegative(),
2801
- result: z8.number().int().nonnegative()
2775
+ sessionMode: z7.enum(turnSessionModes),
2776
+ sessionFingerprint: z7.string().regex(/^[a-f0-9]{16}$/).nullable(),
2777
+ eventCounts: z7.object({
2778
+ session: z7.number().int().nonnegative(),
2779
+ text: z7.number().int().nonnegative(),
2780
+ runtime_notice: z7.number().int().nonnegative(),
2781
+ thinking: z7.number().int().nonnegative(),
2782
+ tool: z7.number().int().nonnegative(),
2783
+ result: z7.number().int().nonnegative()
2802
2784
  }),
2803
- runtimeResultKind: z8.enum(turnRuntimeResultKinds).nullable(),
2804
- finalSource: z8.enum(turnFinalSources),
2805
- mcpInventory: z8.object({
2806
- initReceived: z8.boolean(),
2807
- servers: z8.array(z8.object({ name: z8.string(), status: z8.string() })),
2808
- toolCount: z8.number().int().nonnegative()
2785
+ runtimeResultKind: z7.enum(turnRuntimeResultKinds).nullable(),
2786
+ finalSource: z7.enum(turnFinalSources),
2787
+ mcpInventory: z7.object({
2788
+ initReceived: z7.boolean(),
2789
+ servers: z7.array(z7.object({ name: z7.string(), status: z7.string() })),
2790
+ toolCount: z7.number().int().nonnegative()
2809
2791
  }).optional()
2810
2792
  });
2811
2793
 
2812
2794
  // packages/agent-runtime/src/turn-request.ts
2813
- import { z as z9 } from "zod";
2814
- var contentBlockSchema = z9.discriminatedUnion("type", [
2815
- z9.object({ type: z9.literal("text"), text: z9.string() }),
2816
- z9.object({
2817
- type: z9.literal("image"),
2818
- source: z9.object({ type: z9.literal("url"), url: z9.string() })
2795
+ import { z as z8 } from "zod";
2796
+ var contentBlockSchema = z8.discriminatedUnion("type", [
2797
+ z8.object({ type: z8.literal("text"), text: z8.string() }),
2798
+ z8.object({
2799
+ type: z8.literal("image"),
2800
+ source: z8.object({ type: z8.literal("url"), url: z8.string() })
2819
2801
  }),
2820
- z9.object({
2821
- type: z9.literal("document"),
2822
- source: z9.object({ type: z9.literal("url"), url: z9.string() })
2802
+ z8.object({
2803
+ type: z8.literal("document"),
2804
+ source: z8.object({ type: z8.literal("url"), url: z8.string() })
2823
2805
  })
2824
2806
  ]);
2825
- var effortLevelSchema = z9.enum(["low", "medium", "high", "xhigh", "max"]);
2826
- var resolvedRunConfigSchema = z9.object({
2827
- model: z9.string().nullable(),
2807
+ var effortLevelSchema = z8.enum(["low", "medium", "high", "xhigh", "max"]);
2808
+ var resolvedRunConfigSchema = z8.object({
2809
+ model: z8.string().nullable(),
2828
2810
  effort: effortLevelSchema.optional(),
2829
- runtimeOptions: z9.record(z9.string(), z9.unknown()).optional()
2811
+ runtimeOptions: z8.record(z8.string(), z8.unknown()).optional()
2830
2812
  });
2831
- var resolvedMcpServerSchema = z9.union([
2832
- z9.object({
2833
- type: z9.literal("stdio").optional(),
2834
- command: z9.string(),
2835
- args: z9.array(z9.string()).optional(),
2836
- env: z9.record(z9.string(), z9.string()).optional(),
2837
- envFromEnv: z9.record(z9.string(), z9.string()).optional()
2813
+ var resolvedMcpServerSchema = z8.union([
2814
+ z8.object({
2815
+ type: z8.literal("stdio").optional(),
2816
+ command: z8.string(),
2817
+ args: z8.array(z8.string()).optional(),
2818
+ env: z8.record(z8.string(), z8.string()).optional(),
2819
+ envFromEnv: z8.record(z8.string(), z8.string()).optional()
2838
2820
  }),
2839
- z9.object({
2840
- type: z9.literal("http"),
2841
- url: z9.string(),
2842
- headers: z9.record(z9.string(), z9.string()).optional(),
2843
- headersFromEnv: z9.record(z9.string(), z9.string()).optional()
2821
+ z8.object({
2822
+ type: z8.literal("http"),
2823
+ url: z8.string(),
2824
+ headers: z8.record(z8.string(), z8.string()).optional(),
2825
+ headersFromEnv: z8.record(z8.string(), z8.string()).optional()
2844
2826
  }),
2845
- z9.object({
2846
- type: z9.literal("sse"),
2847
- url: z9.string(),
2848
- headers: z9.record(z9.string(), z9.string()).optional(),
2849
- headersFromEnv: z9.record(z9.string(), z9.string()).optional()
2827
+ z8.object({
2828
+ type: z8.literal("sse"),
2829
+ url: z8.string(),
2830
+ headers: z8.record(z8.string(), z8.string()).optional(),
2831
+ headersFromEnv: z8.record(z8.string(), z8.string()).optional()
2850
2832
  })
2851
2833
  ]);
2852
- var resolvedMcpServersSchema = z9.record(z9.string(), resolvedMcpServerSchema);
2853
- var hostInjectedServersSchema = z9.record(z9.string(), z9.unknown());
2854
- var turnRequestSchema = z9.object({
2834
+ var resolvedMcpServersSchema = z8.record(z8.string(), resolvedMcpServerSchema);
2835
+ var hostInjectedServersSchema = z8.record(z8.string(), z8.unknown());
2836
+ var turnRequestSchema = z8.object({
2855
2837
  // Server-composed standing instructions: the core segment (body, tool naming, identity, agent
2856
2838
  // charter) and, after it, the conversation segment (workspace and room charters, skill indexes,
2857
2839
  // roster). One string to the adapter, current as of this dispatch.
2858
- systemPrompt: z9.string(),
2840
+ systemPrompt: z8.string(),
2859
2841
  // The same text as its two ordered segments, so an adapter that writes standing text into the
2860
2842
  // conversation (Codex's input preamble) can tell which half changed. `systemPrompt` is
2861
2843
  // `joinStanding(standing)`. Optional: absent from an API older than the split, where
2862
2844
  // `systemPrompt` is the core alone and the conversation material rides `prompt`.
2863
- standing: z9.object({ core: z9.string(), conversation: z9.string() }).optional(),
2845
+ standing: z8.object({ core: z8.string(), conversation: z8.string() }).optional(),
2864
2846
  // Server-composed per-turn user text (anchor reminder + the triggering message).
2865
- prompt: z9.string(),
2847
+ prompt: z8.string(),
2866
2848
  // The multi-block user-message body (text + vision).
2867
- content: z9.array(contentBlockSchema),
2849
+ content: z8.array(contentBlockSchema),
2868
2850
  // Portable-or-dialect run-config (above).
2869
2851
  config: resolvedRunConfigSchema,
2870
- // Abstract capability grants; the adapter maps them to tool names.
2871
- policy: hostPolicySchema,
2872
2852
  // Prior opaque session state, or null for a fresh session.
2873
- session: z9.string().nullable(),
2853
+ session: z8.string().nullable(),
2874
2854
  // The cabane control-plane coordinates for this turn's MCP + post-back.
2875
- cabane: z9.object({
2876
- mcpUrl: z9.string(),
2877
- bearer: z9.string(),
2878
- activeConversationId: z9.string(),
2855
+ cabane: z8.object({
2856
+ mcpUrl: z8.string(),
2857
+ bearer: z8.string(),
2858
+ activeConversationId: z8.string(),
2879
2859
  // Scoped turn-control MCP URL. Codex and opencode mount it as `cabane_companion` with the bearer
2880
2860
  // and active-conversation header, for ask, withdraw_ask, wake_me, cancel_wake and skip_turn.
2881
2861
  // Claude Code mounts the companion's in-process instance instead. Optional; the companion
2882
2862
  // populates it.
2883
- turnControlUrl: z9.string().optional(),
2863
+ turnControlUrl: z8.string().optional(),
2884
2864
  // Workspace coordinate retained for envelope compatibility. Current
2885
2865
  // adapters use MCP, whose workspace operations take workspaceId explicitly.
2886
2866
  // The companion supplies this field; it is not required by those adapters.
2887
- workspaceId: z9.string().optional()
2867
+ workspaceId: z8.string().optional()
2888
2868
  }),
2889
2869
  // Machine-local resolution (host-filled): the checkout cwd, extra env from a
2890
2870
  // prepare hook, and the resolved user MCP servers.
2891
- local: z9.object({
2892
- cwd: z9.string().optional(),
2893
- env: z9.record(z9.string(), z9.string()).optional(),
2871
+ local: z8.object({
2872
+ cwd: z8.string().optional(),
2873
+ env: z8.record(z8.string(), z8.string()).optional(),
2894
2874
  mcpServers: resolvedMcpServersSchema.optional(),
2895
2875
  // The values `headersFromEnv` names, per turn (`CABANE_MCP_<n>` → complete header value). The
2896
2876
  // adapter merges them into the harness process's environment and nowhere else; the host never
2897
2877
  // merges them into its own. Absent when no server references one.
2898
- secretEnv: z9.record(z9.string(), z9.string()).optional(),
2878
+ secretEnv: z8.record(z8.string(), z8.string()).optional(),
2899
2879
  // Machine-local claude-code knobs for an operator running their own companion — the auto-memory
2900
2880
  // escape hatch. `autoMemory: true` opts back into Claude Code's auto-memory (governed by the
2901
2881
  // operator's `.claude/settings.json`); absent/false keeps the adapter's force-off default
2902
2882
  // (`buildClaudeCodeOptions`).
2903
- claudeCode: z9.object({ autoMemory: z9.boolean().optional() }).optional()
2883
+ claudeCode: z8.object({ autoMemory: z8.boolean().optional() }).optional(),
2884
+ // Machine-local Codex facts the host read before the turn. `configSetsSandboxMode`: the
2885
+ // person's own Codex `config.toml` names a `sandbox_mode`, so the adapter passes no sandbox
2886
+ // flag and the file governs (`codexSandboxMode`). Absent/false → `danger-full-access`.
2887
+ codex: z8.object({ configSetsSandboxMode: z8.boolean().optional() }).optional()
2904
2888
  }),
2905
2889
  // Host-owned injected servers (host-filled).
2906
- extra: z9.object({
2890
+ extra: z8.object({
2907
2891
  mcpServers: hostInjectedServersSchema
2908
2892
  })
2909
2893
  });
@@ -2919,12 +2903,12 @@ function standingSegments(req) {
2919
2903
  }
2920
2904
 
2921
2905
  // packages/agent-runtime/src/conformance.ts
2922
- import { z as z10 } from "zod";
2923
- var conformanceFixtureSchema = z10.object({
2924
- name: z10.string(),
2906
+ import { z as z9 } from "zod";
2907
+ var conformanceFixtureSchema = z9.object({
2908
+ name: z9.string(),
2925
2909
  request: turnRequestSchema,
2926
- nativeStream: z10.array(z10.unknown()),
2927
- expected: z10.array(turnEventSchema)
2910
+ nativeStream: z9.array(z9.unknown()),
2911
+ expected: z9.array(turnEventSchema)
2928
2912
  });
2929
2913
 
2930
2914
  // packages/agent-runtime/src/transcript.ts
@@ -3189,24 +3173,7 @@ function envResolvedFrom(server, secretEnv) {
3189
3173
  }
3190
3174
 
3191
3175
  // packages/agent-runtime/src/claude-code/policy.ts
3192
- import { z as z11 } from "zod";
3193
- var HOST_FS_TOOLS = [
3194
- // shell + local filesystem
3195
- "Bash",
3196
- "Read",
3197
- "Write",
3198
- "Edit",
3199
- "Glob",
3200
- "Grep",
3201
- // notebook tools (host-fs, Jupyter) — same host reach as Read/Write/Edit
3202
- "NotebookEdit",
3203
- "NotebookRead",
3204
- // git-worktree manipulation on the host checkout
3205
- "EnterWorktree",
3206
- "ExitWorktree",
3207
- // host code execution
3208
- "REPL"
3209
- ];
3176
+ import { z as z10 } from "zod";
3210
3177
  var SUBAGENT_TOOLS = ["Task", "Agent"];
3211
3178
  var UNSUPPORTED_TOOLS = ["AskUserQuestion"];
3212
3179
  var STRANDING_TOOLS = [
@@ -3228,29 +3195,23 @@ var STRANDING_TOOLS = [
3228
3195
  "Workflow"
3229
3196
  ];
3230
3197
  var DEFAULT_WEB_TOOLS = ["WebSearch", "WebFetch"];
3231
- function disallowedToolsFor(policy) {
3232
- const tools = [...UNSUPPORTED_TOOLS, ...STRANDING_TOOLS];
3233
- if (!policy.subagents) tools.unshift(...SUBAGENT_TOOLS);
3234
- if (!policy.hostFs) tools.unshift(...HOST_FS_TOOLS);
3235
- return tools;
3236
- }
3237
3198
  function withThinkingSummaries(thinking) {
3238
3199
  if (!thinking) return { type: "adaptive", display: "summarized" };
3239
3200
  if (thinking.type === "disabled") return thinking;
3240
3201
  return { display: "summarized", ...thinking };
3241
3202
  }
3242
- var claudeCodeDialectSchema = z11.object({
3243
- thinking: z11.discriminatedUnion("type", [
3244
- z11.object({
3245
- type: z11.literal("adaptive"),
3246
- display: z11.enum(["summarized", "omitted"]).optional()
3203
+ var claudeCodeDialectSchema = z10.object({
3204
+ thinking: z10.discriminatedUnion("type", [
3205
+ z10.object({
3206
+ type: z10.literal("adaptive"),
3207
+ display: z10.enum(["summarized", "omitted"]).optional()
3247
3208
  }),
3248
- z11.object({
3249
- type: z11.literal("enabled"),
3250
- budgetTokens: z11.number().int().positive().optional(),
3251
- display: z11.enum(["summarized", "omitted"]).optional()
3209
+ z10.object({
3210
+ type: z10.literal("enabled"),
3211
+ budgetTokens: z10.number().int().positive().optional(),
3212
+ display: z10.enum(["summarized", "omitted"]).optional()
3252
3213
  }),
3253
- z11.object({ type: z11.literal("disabled") })
3214
+ z10.object({ type: z10.literal("disabled") })
3254
3215
  ]).optional()
3255
3216
  }).loose();
3256
3217
  function readThinking(runtimeOptions) {
@@ -3313,13 +3274,13 @@ async function preToolUseHook(input) {
3313
3274
  hookSpecificOutput: {
3314
3275
  hookEventName: "PreToolUse",
3315
3276
  permissionDecision: "allow",
3316
- permissionDecisionReason: "coding mode: headless never-prompt (CT680)",
3277
+ permissionDecisionReason: "headless never-prompt (CT680)",
3317
3278
  updatedInput: toolInput
3318
3279
  }
3319
3280
  };
3320
3281
  }
3321
3282
  function buildClaudeCodeOptions(req, augment) {
3322
- const { policy, config } = req;
3283
+ const { config } = req;
3323
3284
  const cwd = req.local.cwd;
3324
3285
  const userServers = {};
3325
3286
  for (const [name, server] of Object.entries(req.local.mcpServers ?? {})) {
@@ -3352,15 +3313,10 @@ function buildClaudeCodeOptions(req, augment) {
3352
3313
  alwaysLoad: true
3353
3314
  };
3354
3315
  }
3355
- const useCodingPreset = policy.hostFs;
3356
3316
  const cabaneGlob = `mcp__${CABANE_MCP_SERVER}__*`;
3357
3317
  const extraServerGlobs = Object.keys(req.extra.mcpServers).map((name) => `mcp__${name}__*`);
3358
- const allowedTools = dedupe([
3359
- cabaneGlob,
3360
- ...extraServerGlobs,
3361
- ...policy.web ? DEFAULT_WEB_TOOLS : []
3362
- ]);
3363
- const disallowedTools = dedupe([...disallowedToolsFor(policy)]);
3318
+ const allowedTools = dedupe([cabaneGlob, ...extraServerGlobs, ...DEFAULT_WEB_TOOLS]);
3319
+ const disallowedTools = dedupe([...UNSUPPORTED_TOOLS, ...STRANDING_TOOLS]);
3364
3320
  const resumeDecision = decideResume(req.session, cwd);
3365
3321
  const resume = "resume" in resumeDecision ? resumeDecision.resume : null;
3366
3322
  const freshReason = "fresh" in resumeDecision ? resumeDecision.reason : void 0;
@@ -3378,9 +3334,7 @@ function buildClaudeCodeOptions(req, augment) {
3378
3334
  // (`runtimeOptions['claude-code']`).
3379
3335
  thinking: withThinkingSummaries(readThinking(config.runtimeOptions)),
3380
3336
  ...config.effort ? { effort: config.effort } : {},
3381
- // Never merge arbitrary user `~/.claude` config; without `strictMcpConfig` the
3382
- // user's existing `cabane` MCP registration could shadow ours.
3383
- settingSources: [],
3337
+ // Without `strictMcpConfig` the user's existing `cabane` MCP registration could shadow ours.
3384
3338
  strictMcpConfig: true,
3385
3339
  // Auto-memory off unless the operator opted back in (see above).
3386
3340
  ...devControlsAutoMemory ? {} : { settings: { autoMemoryEnabled: false } },
@@ -3416,31 +3370,19 @@ function buildClaudeCodeOptions(req, augment) {
3416
3370
  },
3417
3371
  ...resume ? { resume } : {}
3418
3372
  };
3419
- let options;
3420
- if (useCodingPreset) {
3421
- options = {
3422
- ...base,
3423
- systemPrompt: {
3424
- type: "preset",
3425
- preset: "claude_code",
3426
- append: req.systemPrompt,
3427
- snapshot: false
3428
- },
3429
- settingSources: ["user", "project"],
3430
- allowedTools,
3431
- disallowedTools,
3432
- hooks: { PreToolUse: [{ hooks: [preToolUseHook] }] }
3433
- };
3434
- } else {
3435
- options = {
3436
- ...base,
3437
- systemPrompt: { type: "custom", prompt: req.systemPrompt, snapshot: false },
3438
- allowedTools,
3439
- disallowedTools,
3440
- permissionMode: "bypassPermissions",
3441
- allowDangerouslySkipPermissions: true
3442
- };
3443
- }
3373
+ const options = {
3374
+ ...base,
3375
+ systemPrompt: {
3376
+ type: "preset",
3377
+ preset: "claude_code",
3378
+ append: req.systemPrompt,
3379
+ snapshot: false
3380
+ },
3381
+ settingSources: ["user", "project"],
3382
+ allowedTools,
3383
+ disallowedTools,
3384
+ hooks: { PreToolUse: [{ hooks: [preToolUseHook] }] }
3385
+ };
3444
3386
  return { options: augment ? augment(options, req) : options, resume, freshReason };
3445
3387
  }
3446
3388
  function dedupe(xs) {
@@ -3920,15 +3862,6 @@ var claudeCodeAdapter = createClaudeCodeAdapter();
3920
3862
 
3921
3863
  // packages/agent-runtime/src/claude-code/conformance.ts
3922
3864
  var ABORT_SENTINEL = { __abortHere: true };
3923
- var CABANE_POLICY = {
3924
- hostFs: false,
3925
- web: true,
3926
- browser: false,
3927
- userMcp: false,
3928
- subagents: false,
3929
- scheduling: "never",
3930
- uiPrompts: "never"
3931
- };
3932
3865
  var CWD = "/env/here";
3933
3866
  var HEALTHY_MCP_INVENTORY = {
3934
3867
  initReceived: true,
@@ -3942,7 +3875,6 @@ function makeRequest(overrides2 = {}) {
3942
3875
  content: [{ type: "text", text: "hi there" }],
3943
3876
  // The adapter rejects a null model, so the fixture carries a real bare SDK id.
3944
3877
  config: { model: "claude-opus-4-8" },
3945
- policy: CABANE_POLICY,
3946
3878
  session: null,
3947
3879
  cabane: { mcpUrl: "https://cabane.test/api/mcp", bearer: "tok", activeConversationId: "conv" },
3948
3880
  local: { cwd: CWD },
@@ -4689,35 +4621,17 @@ function sealHeld(held, terminal) {
4689
4621
  }
4690
4622
 
4691
4623
  // packages/agent-runtime/src/opencode/policy.ts
4692
- import { z as z12 } from "zod";
4693
- var OPENCODE_HOST_TOOLS = [
4694
- "bash",
4695
- "edit",
4696
- "write",
4697
- "read",
4698
- "grep",
4699
- "glob",
4700
- "list",
4701
- "patch"
4702
- ];
4703
- var OPENCODE_WEB_TOOLS = ["webfetch"];
4704
- var OPENCODE_SUBAGENT_TOOLS = ["task"];
4624
+ import { z as z11 } from "zod";
4705
4625
  var OPENCODE_SCHEDULING_TOOLS = [];
4706
4626
  var OPENCODE_UI_PROMPT_TOOLS = ["question"];
4707
- function opencodeToolPolicy(policy) {
4627
+ function opencodeTools() {
4708
4628
  const tools = {};
4709
- const deny = (names) => {
4710
- for (const n of names) tools[n] = false;
4711
- };
4712
- if (!policy.hostFs) deny(OPENCODE_HOST_TOOLS);
4713
- if (!policy.subagents) deny(OPENCODE_SUBAGENT_TOOLS);
4714
- if (!policy.web) deny(OPENCODE_WEB_TOOLS);
4715
- deny(OPENCODE_SCHEDULING_TOOLS);
4716
- deny(OPENCODE_UI_PROMPT_TOOLS);
4717
- return { tools, allowAllHostTools: policy.hostFs };
4718
- }
4719
- var opencodeDialectSchema = z12.object({
4720
- agent: z12.string().min(1).optional()
4629
+ for (const name of [...OPENCODE_SCHEDULING_TOOLS, ...OPENCODE_UI_PROMPT_TOOLS])
4630
+ tools[name] = false;
4631
+ return tools;
4632
+ }
4633
+ var opencodeDialectSchema = z11.object({
4634
+ agent: z11.string().min(1).optional()
4721
4635
  }).loose();
4722
4636
  function readOpencodeDialect(runtimeOptions) {
4723
4637
  const parsed = opencodeDialectSchema.safeParse(runtimeOptions?.["opencode"] ?? {});
@@ -4736,18 +4650,16 @@ var CABANE_MCP_SERVER2 = "cabane";
4736
4650
  var TURN_CONTROL_MCP_SERVER = "cabane_companion";
4737
4651
  var ACTIVE_CONVERSATION_HEADER2 = "x-cabane-active-conversation";
4738
4652
  function buildRunSpec(req, resumeSessionId) {
4739
- const { policy, config } = req;
4653
+ const { config } = req;
4740
4654
  const directory = req.local.cwd ?? "";
4741
4655
  const dialect = readOpencodeDialect(config.runtimeOptions);
4742
- const { tools, allowAllHostTools } = opencodeToolPolicy(policy);
4743
4656
  return {
4744
4657
  resumeSessionId,
4745
4658
  directory,
4746
4659
  model: config.model ? parseOpencodeModel(config.model) : null,
4747
4660
  systemPrompt: req.systemPrompt,
4748
4661
  parts: buildParts(req),
4749
- tools,
4750
- allowAllHostTools,
4662
+ tools: opencodeTools(),
4751
4663
  ...dialect.agent ? { agent: dialect.agent } : {},
4752
4664
  mcp: buildMcp(req),
4753
4665
  // Every server this turn installs with a credential resolved into it: a remote one's
@@ -4879,11 +4791,12 @@ function createHttpOpencodeTransport(opts) {
4879
4791
  const doFetch = opts.fetchImpl ?? fetch;
4880
4792
  return {
4881
4793
  async run(spec, signal) {
4794
+ const url = scopedUrl(base, spec.directory);
4882
4795
  const release = await acquireServerTurnLock(base);
4883
4796
  if (signal.aborted) release();
4884
4797
  else signal.addEventListener("abort", release, { once: true });
4885
4798
  try {
4886
- const eventRes = await doFetch(`${base}/event`, {
4799
+ const eventRes = await doFetch(url("/event"), {
4887
4800
  headers: { accept: "text/event-stream" },
4888
4801
  signal
4889
4802
  });
@@ -4893,7 +4806,7 @@ function createHttpOpencodeTransport(opts) {
4893
4806
  let sessionId = spec.resumeSessionId;
4894
4807
  let created = false;
4895
4808
  if (!sessionId) {
4896
- const res = await doFetch(`${base}/session`, {
4809
+ const res = await doFetch(url("/session"), {
4897
4810
  method: "POST",
4898
4811
  headers: { "content-type": "application/json" },
4899
4812
  body: JSON.stringify({ body: {} }),
@@ -4905,9 +4818,9 @@ function createHttpOpencodeTransport(opts) {
4905
4818
  sessionId = json.id;
4906
4819
  created = true;
4907
4820
  }
4908
- await installMcp(doFetch, base, spec, signal);
4821
+ await installMcp(doFetch, url, spec, signal);
4909
4822
  const promptRes = await doFetch(
4910
- `${base}/session/${encodeURIComponent(sessionId)}/prompt_async`,
4823
+ url(`/session/${encodeURIComponent(sessionId)}/prompt_async`),
4911
4824
  {
4912
4825
  method: "POST",
4913
4826
  headers: { "content-type": "application/json" },
@@ -4926,7 +4839,7 @@ function createHttpOpencodeTransport(opts) {
4926
4839
  }
4927
4840
  const sid = sessionId;
4928
4841
  const onAbort = () => {
4929
- void doFetch(`${base}/session/${encodeURIComponent(sid)}/abort`, {
4842
+ void doFetch(url(`/session/${encodeURIComponent(sid)}/abort`), {
4930
4843
  method: "POST"
4931
4844
  }).catch(() => {
4932
4845
  });
@@ -4938,7 +4851,13 @@ function createHttpOpencodeTransport(opts) {
4938
4851
  created,
4939
4852
  directory: spec.directory,
4940
4853
  // The lock is released when this stream finishes draining.
4941
- events: releaseAfter(parseSseStream(eventRes.body, sessionId, signal), release)
4854
+ events: releaseAfter(
4855
+ approvePermissions(
4856
+ parseSseStream(eventRes.body, sessionId, signal),
4857
+ (ev) => replyToPermission(doFetch, url, ev, signal)
4858
+ ),
4859
+ release
4860
+ )
4942
4861
  };
4943
4862
  } catch (err) {
4944
4863
  release();
@@ -4959,16 +4878,16 @@ var SCRUBBED_MCP_CONFIG = {
4959
4878
  url: "http://127.0.0.1:9/cabane-removed",
4960
4879
  enabled: false
4961
4880
  };
4962
- async function installMcp(doFetch, base, spec, signal) {
4881
+ async function installMcp(doFetch, url, spec, signal) {
4963
4882
  const [live, configured] = await Promise.all([
4964
- getJson(doFetch, `${base}/mcp`, signal),
4965
- getJson(doFetch, `${base}/config`, signal)
4883
+ getJson(doFetch, url("/mcp"), signal),
4884
+ getJson(doFetch, url("/config"), signal)
4966
4885
  ]);
4967
4886
  const operatorOwned = new Set(Object.keys(asRecord2(asRecord2(configured).mcp)));
4968
4887
  const stale = Object.keys(asRecord2(live)).filter(
4969
4888
  (name) => !(name in spec.mcp) && !operatorOwned.has(name)
4970
4889
  );
4971
- await Promise.all(stale.map((name) => postMcp(doFetch, base, name, SCRUBBED_MCP_CONFIG, signal)));
4890
+ await Promise.all(stale.map((name) => postMcp(doFetch, url, name, SCRUBBED_MCP_CONFIG, signal)));
4972
4891
  const shadowing = (spec.credentialServers ?? []).filter((name) => operatorOwned.has(name));
4973
4892
  if (shadowing.length > 0) {
4974
4893
  throw new Error(
@@ -4976,11 +4895,11 @@ async function installMcp(doFetch, base, spec, signal) {
4976
4895
  );
4977
4896
  }
4978
4897
  await Promise.all(
4979
- Object.entries(spec.mcp).map(([name, config]) => postMcp(doFetch, base, name, config, signal))
4898
+ Object.entries(spec.mcp).map(([name, config]) => postMcp(doFetch, url, name, config, signal))
4980
4899
  );
4981
4900
  }
4982
- async function postMcp(doFetch, base, name, config, signal) {
4983
- const res = await doFetch(`${base}/mcp`, {
4901
+ async function postMcp(doFetch, url, name, config, signal) {
4902
+ const res = await doFetch(url("/mcp"), {
4984
4903
  method: "POST",
4985
4904
  headers: { "content-type": "application/json" },
4986
4905
  body: JSON.stringify({ name, config }),
@@ -5044,6 +4963,33 @@ function belongsToSession(ev, sessionId) {
5044
4963
  if (typeof infoSid === "string") return infoSid === sessionId;
5045
4964
  return true;
5046
4965
  }
4966
+ async function* approvePermissions(events, reply2) {
4967
+ for await (const ev of events) {
4968
+ if (ev.type === "permission.asked" || ev.type === "permission.v2.asked") await reply2(ev);
4969
+ yield ev;
4970
+ }
4971
+ }
4972
+ async function replyToPermission(doFetch, url, ev, signal) {
4973
+ const props = asRecord2(ev.properties);
4974
+ const id = typeof props.id === "string" ? props.id : null;
4975
+ const sessionId = typeof props.sessionID === "string" ? props.sessionID : null;
4976
+ if (!id) throw new Error(`opencode: ${ev.type} carried no request id`);
4977
+ const target = ev.type === "permission.v2.asked" && sessionId ? url(
4978
+ `/api/session/${encodeURIComponent(sessionId)}/permission/${encodeURIComponent(id)}/reply`,
4979
+ { unscoped: true }
4980
+ ) : url(`/permission/${encodeURIComponent(id)}/reply`);
4981
+ const res = await doFetch(target, {
4982
+ method: "POST",
4983
+ headers: { "content-type": "application/json" },
4984
+ body: JSON.stringify({ reply: "once" }),
4985
+ signal
4986
+ });
4987
+ if (!res.ok) throw new Error(`opencode: permission reply failed (HTTP ${res.status})`);
4988
+ }
4989
+ function scopedUrl(base, directory) {
4990
+ const scope = directory ? `directory=${encodeURIComponent(directory)}` : "";
4991
+ return (path, opts) => scope && !opts?.unscoped ? `${base}${path}${path.includes("?") ? "&" : "?"}${scope}` : base + path;
4992
+ }
5047
4993
  function trimSlash(s) {
5048
4994
  return s.endsWith("/") ? s.slice(0, -1) : s;
5049
4995
  }
@@ -5106,15 +5052,6 @@ var opencodeAdapter = createOpencodeAdapter();
5106
5052
  var ABORT_SENTINEL2 = { __abortHere: true };
5107
5053
  var NEW_SESSION_ID = "sess_new";
5108
5054
  var BOUNDED_NOTICE = "You've hit your usage limit \xB7 ".padEnd(200, "x");
5109
- var COMPANION_POLICY = {
5110
- hostFs: false,
5111
- web: true,
5112
- browser: true,
5113
- userMcp: true,
5114
- subagents: false,
5115
- scheduling: "never",
5116
- uiPrompts: "never"
5117
- };
5118
5055
  var DIR = "/env/here";
5119
5056
  function makeRequest2(overrides2 = {}) {
5120
5057
  return {
@@ -5122,7 +5059,6 @@ function makeRequest2(overrides2 = {}) {
5122
5059
  prompt: "hi there",
5123
5060
  content: [{ type: "text", text: "hi there" }],
5124
5061
  config: { model: "deepseek/deepseek-chat" },
5125
- policy: COMPANION_POLICY,
5126
5062
  session: null,
5127
5063
  cabane: { mcpUrl: "https://cabane.test/api/mcp", bearer: "tok", activeConversationId: "conv" },
5128
5064
  local: { cwd: DIR },
@@ -5928,27 +5864,13 @@ function sealHeld2(held, terminal) {
5928
5864
  }
5929
5865
 
5930
5866
  // packages/agent-runtime/src/codex/policy.ts
5931
- import { z as z13 } from "zod";
5932
- function codexToolPolicy(policy) {
5933
- return policy.hostFs ? {
5934
- sandboxMode: "danger-full-access",
5935
- // Headless: nothing to approve, because nothing is denied.
5936
- approvalPolicy: "never",
5937
- // Full access means the command network is open regardless of the grant —
5938
- // recorded as `true` so the value never claims a restriction that isn't
5939
- // enforced.
5940
- networkAccessEnabled: true
5941
- } : {
5942
- sandboxMode: "read-only",
5943
- // Headless: the sandbox is the boundary; never pause for a human.
5944
- approvalPolicy: "never",
5945
- // Retained in the policy value for symmetry; read-only ignores it.
5946
- networkAccessEnabled: policy.web
5947
- };
5867
+ import { z as z12 } from "zod";
5868
+ function codexSandboxMode(local) {
5869
+ return local.codex?.configSetsSandboxMode ? null : "danger-full-access";
5948
5870
  }
5949
5871
  var CODEX_REASONING_EFFORTS = ["low", "medium", "high", "xhigh", "max", "ultra"];
5950
- var codexDialectSchema = z13.object({
5951
- modelReasoningEffort: z13.enum(CODEX_REASONING_EFFORTS).optional()
5872
+ var codexDialectSchema = z12.object({
5873
+ modelReasoningEffort: z12.enum(CODEX_REASONING_EFFORTS).optional()
5952
5874
  }).loose();
5953
5875
  function readCodexDialect(runtimeOptions) {
5954
5876
  const parsed = codexDialectSchema.safeParse(runtimeOptions?.["codex"] ?? {});
@@ -5972,14 +5894,12 @@ var ENV_ENVELOPE_KEYS = [
5972
5894
  "CABANE_PLAYGROUND_BIN",
5973
5895
  "CABANE_CONVERSATION_ID",
5974
5896
  "CABANE_AGENT_ID",
5975
- "CABANE_CONVERSATION_TITLE",
5976
- "CABANE_HOST_ACCESS"
5897
+ "CABANE_CONVERSATION_TITLE"
5977
5898
  ];
5978
- function buildRunSpec2(req, resumeThreadId, instructionsFile = null, threadStanding = null, imagePaths = []) {
5979
- const { policy, config } = req;
5899
+ function buildRunSpec2(req, resumeThreadId, threadStanding = null, imagePaths = []) {
5900
+ const { config } = req;
5980
5901
  const directory = req.local.cwd ?? "";
5981
5902
  const dialect = readCodexDialect(config.runtimeOptions);
5982
- const baseInstructionsFile = !policy.hostFs && instructionsFile ? instructionsFile : null;
5983
5903
  const model = config.model ? parseCodexModel(config.model) : null;
5984
5904
  const segments = standingSegments(req);
5985
5905
  const conversationText = segments.conversation.trim();
@@ -5989,25 +5909,24 @@ function buildRunSpec2(req, resumeThreadId, instructionsFile = null, threadStand
5989
5909
  };
5990
5910
  const resumed = resumeThreadId !== null;
5991
5911
  const held = resumed ? threadStanding : null;
5992
- const viaInput = baseInstructionsFile === null;
5993
5912
  const preamble = {
5994
- core: viaInput && (held?.core ?? null) !== fingerprints.core,
5913
+ core: (held?.core ?? null) !== fingerprints.core,
5995
5914
  // Compared both ways: a held segment that is now empty differs from it, and nothing held
5996
5915
  // equals nothing to send.
5997
- conversation: viaInput && (held?.conversation ?? null) !== fingerprints.conversation
5916
+ conversation: (held?.conversation ?? null) !== fingerprints.conversation
5998
5917
  };
5999
5918
  return {
6000
5919
  resumeThreadId,
6001
5920
  directory,
6002
5921
  model,
6003
- policy: codexToolPolicy(policy),
5922
+ sandboxMode: codexSandboxMode(req.local),
5923
+ approvalPolicy: "never",
6004
5924
  skipGitRepoCheck: true,
6005
5925
  ...dialect.modelReasoningEffort ? { modelReasoningEffort: dialect.modelReasoningEffort } : {},
6006
- baseInstructionsFile,
6007
5926
  input: withImages(buildInput(req, segments, preamble, resumed), imagePaths),
6008
5927
  preamble,
6009
5928
  fingerprints,
6010
- config: buildConfig(req, baseInstructionsFile),
5929
+ config: buildConfig(req),
6011
5930
  secretEnv: codexSecretEnv(req)
6012
5931
  };
6013
5932
  }
@@ -6052,7 +5971,7 @@ function withImages(text, imagePaths) {
6052
5971
  ...imagePaths.map((path) => ({ type: "local_image", path }))
6053
5972
  ];
6054
5973
  }
6055
- function buildConfig(req, baseInstructionsFile) {
5974
+ function buildConfig(req) {
6056
5975
  const mcp_servers = {};
6057
5976
  for (const [name, server] of Object.entries(req.local.mcpServers ?? {})) {
6058
5977
  if ("url" in server) {
@@ -6118,9 +6037,6 @@ function buildConfig(req, baseInstructionsFile) {
6118
6037
  return {
6119
6038
  mcp_servers,
6120
6039
  experimental_use_rmcp_client: true,
6121
- // The no-host-access prompt replacement — Cabane's composed prompt becomes Codex's base
6122
- // instructions, and Codex's environment preamble goes with the harness prompt it belonged to.
6123
- ...baseInstructionsFile ? { model_instructions_file: baseInstructionsFile, include_environment_context: false } : {},
6124
6040
  ...Object.keys(shellEnv).length ? { shell_environment_policy: { set: shellEnv } } : {}
6125
6041
  };
6126
6042
  }
@@ -6196,10 +6112,10 @@ function buildConfigOverrides(config) {
6196
6112
  function buildSdkThreadOptions(spec) {
6197
6113
  return {
6198
6114
  ...spec.model ? { model: spec.model } : {},
6199
- sandboxMode: spec.policy.sandboxMode,
6115
+ ...spec.sandboxMode ? { sandboxMode: spec.sandboxMode } : {},
6200
6116
  workingDirectory: spec.directory,
6201
6117
  skipGitRepoCheck: spec.skipGitRepoCheck,
6202
- approvalPolicy: spec.policy.approvalPolicy,
6118
+ approvalPolicy: spec.approvalPolicy,
6203
6119
  ...spec.modelReasoningEffort ? { modelReasoningEffort: spec.modelReasoningEffort } : {}
6204
6120
  };
6205
6121
  }
@@ -6295,23 +6211,6 @@ function createCodexAdapter(deps = {}) {
6295
6211
  );
6296
6212
  }
6297
6213
  }
6298
- let instructions = null;
6299
- if (!req.policy.hostFs) {
6300
- if (!deps.writeInstructionsFile) {
6301
- deps.onWarn?.(
6302
- "codex adapter: no instructions-file writer wired on this host \u2014 the composed prompt rides as the input preamble, so Codex keeps its own base instructions (CT871)"
6303
- );
6304
- } else {
6305
- try {
6306
- instructions = await deps.writeInstructionsFile(req.systemPrompt);
6307
- } catch (err) {
6308
- deps.onWarn?.(
6309
- "codex adapter: failed to write the per-turn instructions file \u2014 falling back to the input preamble (CT871)",
6310
- { err: err instanceof Error ? err.message : String(err) }
6311
- );
6312
- }
6313
- }
6314
- }
6315
6214
  let images = null;
6316
6215
  try {
6317
6216
  const imageUrls = req.content.flatMap((b) => b.type === "image" ? [b.source.url] : []);
@@ -6335,13 +6234,7 @@ function createCodexAdapter(deps = {}) {
6335
6234
  }
6336
6235
  if (signal.aborted) return;
6337
6236
  }
6338
- const spec = buildRunSpec2(
6339
- req,
6340
- resumeThreadId,
6341
- instructions?.path ?? null,
6342
- threadStanding,
6343
- images?.paths ?? []
6344
- );
6237
+ const spec = buildRunSpec2(req, resumeThreadId, threadStanding, images?.paths ?? []);
6345
6238
  if (spec.model === null) {
6346
6239
  deps.onWarn?.("Codex is using its default model because no model was resolved", {
6347
6240
  model: req.config.model
@@ -6383,15 +6276,6 @@ function createCodexAdapter(deps = {}) {
6383
6276
  });
6384
6277
  }
6385
6278
  }
6386
- if (instructions) {
6387
- try {
6388
- await instructions.cleanup();
6389
- } catch (err) {
6390
- deps.onWarn?.("codex adapter: instructions-file cleanup failed (CT871)", {
6391
- err: err instanceof Error ? err.message : String(err)
6392
- });
6393
- }
6394
- }
6395
6279
  }
6396
6280
  }
6397
6281
  };
@@ -6402,15 +6286,6 @@ var codexAdapter = createCodexAdapter();
6402
6286
  var ABORT_SENTINEL3 = { __abortHere: true };
6403
6287
  var NEW_THREAD_ID = "th_new";
6404
6288
  var BOUNDED_NOTICE2 = "You've hit your usage limit \xB7 ".padEnd(200, "x");
6405
- var COMPANION_POLICY2 = {
6406
- hostFs: false,
6407
- web: true,
6408
- browser: true,
6409
- userMcp: true,
6410
- subagents: false,
6411
- scheduling: "never",
6412
- uiPrompts: "never"
6413
- };
6414
6289
  var DIR2 = "/env/here";
6415
6290
  var PROMPT = "system";
6416
6291
  function makeRequest3(overrides2 = {}) {
@@ -6422,7 +6297,6 @@ function makeRequest3(overrides2 = {}) {
6422
6297
  // Behavioural fixtures use the model-less token (no `resolvedModel` emitted); a dedicated capture
6423
6298
  // fixture sets a real model and effort.
6424
6299
  config: { model: null },
6425
- policy: COMPANION_POLICY2,
6426
6300
  session: null,
6427
6301
  cabane: { mcpUrl: "https://cabane.test/api/mcp", bearer: "tok", activeConversationId: "conv" },
6428
6302
  local: { cwd: DIR2 },
@@ -7454,52 +7328,47 @@ function classifyStopError(message) {
7454
7328
  }
7455
7329
 
7456
7330
  // src/runner/protocol.ts
7457
- import { z as z15 } from "zod";
7331
+ import { z as z14 } from "zod";
7458
7332
 
7459
7333
  // src/run-config.ts
7460
- import { z as z14 } from "zod";
7461
- var mcpStdioServerSchema = z14.object({
7462
- type: z14.literal("stdio").optional(),
7463
- command: z14.string().min(1),
7464
- args: z14.array(z14.string()).optional(),
7465
- env: z14.record(z14.string(), z14.string()).optional(),
7334
+ import { z as z13 } from "zod";
7335
+ var mcpStdioServerSchema = z13.object({
7336
+ type: z13.literal("stdio").optional(),
7337
+ command: z13.string().min(1),
7338
+ args: z13.array(z13.string()).optional(),
7339
+ env: z13.record(z13.string(), z13.string()).optional(),
7466
7340
  // Variable → the name of a `secretEnv` entry holding its value. Set by the server on a
7467
7341
  // room's shared browser, whose connect URL is a credential.
7468
- envFromEnv: z14.record(z14.string(), z14.string()).optional()
7342
+ envFromEnv: z13.record(z13.string(), z13.string()).optional()
7469
7343
  });
7470
- var mcpHttpServerSchema = z14.object({
7471
- type: z14.literal("http"),
7472
- url: z14.string().url(),
7473
- headers: z14.record(z14.string(), z14.string()).optional(),
7344
+ var mcpHttpServerSchema = z13.object({
7345
+ type: z13.literal("http"),
7346
+ url: z13.string().url(),
7347
+ headers: z13.record(z13.string(), z13.string()).optional(),
7474
7348
  // Header → the name of a `secretEnv` entry holding its value. Set by
7475
7349
  // the server on a room connection's entry, and by `resolveMcpSecrets` on a
7476
7350
  // header whose `${VAR}` it resolved.
7477
- headersFromEnv: z14.record(z14.string(), z14.string()).optional()
7351
+ headersFromEnv: z13.record(z13.string(), z13.string()).optional()
7478
7352
  });
7479
- var mcpSseServerSchema = z14.object({
7480
- type: z14.literal("sse"),
7481
- url: z14.string().url(),
7482
- headers: z14.record(z14.string(), z14.string()).optional(),
7353
+ var mcpSseServerSchema = z13.object({
7354
+ type: z13.literal("sse"),
7355
+ url: z13.string().url(),
7356
+ headers: z13.record(z13.string(), z13.string()).optional(),
7483
7357
  // Header → the name of a `secretEnv` entry holding its value. Set by
7484
7358
  // the server on a room connection's entry, and by `resolveMcpSecrets` on a
7485
7359
  // header whose `${VAR}` it resolved.
7486
- headersFromEnv: z14.record(z14.string(), z14.string()).optional()
7360
+ headersFromEnv: z13.record(z13.string(), z13.string()).optional()
7487
7361
  });
7488
- var mcpServerDefSchema = z14.union([mcpHttpServerSchema, mcpSseServerSchema, mcpStdioServerSchema]);
7489
- var thinkingConfigSchema = z14.discriminatedUnion("type", [
7490
- z14.object({ type: z14.literal("adaptive") }),
7491
- z14.object({ type: z14.literal("enabled"), budgetTokens: z14.number().int().positive().optional() }),
7492
- z14.object({ type: z14.literal("disabled") })
7362
+ var mcpServerDefSchema = z13.union([mcpHttpServerSchema, mcpSseServerSchema, mcpStdioServerSchema]);
7363
+ var thinkingConfigSchema = z13.discriminatedUnion("type", [
7364
+ z13.object({ type: z13.literal("adaptive") }),
7365
+ z13.object({ type: z13.literal("enabled"), budgetTokens: z13.number().int().positive().optional() }),
7366
+ z13.object({ type: z13.literal("disabled") })
7493
7367
  ]);
7494
- var effortSchema = z14.enum(["low", "medium", "high", "xhigh", "max"]);
7495
- var runConfigSchema = z14.object({
7496
- // `true` grants the host filesystem/shell; absent is the locked surface. Kept
7497
- // in lockstep with `@cabane/shared`'s
7498
- // `agentRunConfigSchema` (independent zod, forward-compatible: unknown keys are
7499
- // stripped, so an older companion riding a newer server never rejects the config).
7500
- hostAccess: z14.boolean().optional(),
7501
- mcpServers: z14.record(z14.string(), mcpServerDefSchema).optional(),
7502
- model: z14.string().min(1).optional(),
7368
+ var effortSchema = z13.enum(["low", "medium", "high", "xhigh", "max"]);
7369
+ var runConfigSchema = z13.object({
7370
+ mcpServers: z13.record(z13.string(), mcpServerDefSchema).optional(),
7371
+ model: z13.string().min(1).optional(),
7503
7372
  thinking: thinkingConfigSchema.optional(),
7504
7373
  effort: effortSchema.optional()
7505
7374
  });
@@ -7527,56 +7396,56 @@ var RUNNER_ENV = {
7527
7396
  // Everything else the turn is constructed from, as JSON: see `RunnerSpec`.
7528
7397
  spec: "CABANE_RUNNER_SPEC"
7529
7398
  };
7530
- var runtimeHealthSchema = z15.object({
7531
- quotaState: z15.enum(["ok", "limited", "auth_expired", "unknown"]),
7532
- limitedUntil: z15.string().nullable(),
7533
- lastError: z15.string().nullable()
7399
+ var runtimeHealthSchema = z14.object({
7400
+ quotaState: z14.enum(["ok", "limited", "auth_expired", "unknown"]),
7401
+ limitedUntil: z14.string().nullable(),
7402
+ lastError: z14.string().nullable()
7534
7403
  });
7535
- var runnerSpecSchema = z15.object({
7536
- deviceId: z15.string().optional(),
7404
+ var runnerSpecSchema = z14.object({
7405
+ deviceId: z14.string().optional(),
7537
7406
  // The launcher's boot id: the one its heartbeat carries, so a runner's lease
7538
7407
  // matches the device that holds it.
7539
- bootId: z15.string(),
7540
- workspaceId: z15.string(),
7541
- workspaceSlug: z15.string(),
7542
- agentId: z15.string(),
7543
- agentUsername: z15.string(),
7544
- agentName: z15.string(),
7408
+ bootId: z14.string(),
7409
+ workspaceId: z14.string(),
7410
+ workspaceSlug: z14.string(),
7411
+ agentId: z14.string(),
7412
+ agentUsername: z14.string(),
7413
+ agentName: z14.string(),
7545
7414
  local: localAgentConfigSchema,
7546
7415
  runConfig: runConfigSchema,
7547
7416
  // The device's `root`, resolved by the launcher: the pair directory the turn
7548
7417
  // stands in when nothing else names one lives under it (`work-dir.ts`).
7549
- workRoot: z15.string(),
7550
- opencodeServerUrl: z15.string().optional(),
7551
- claudeCodeAvailable: z15.boolean(),
7552
- codexAvailable: z15.boolean(),
7553
- idleTimeoutMs: z15.number().int().positive().optional(),
7554
- totalTimeoutMs: z15.number().int().positive().optional(),
7555
- leaseRenewalMs: z15.number().int().positive().optional(),
7556
- preparingRowDelayMs: z15.number().int().nonnegative().optional(),
7418
+ workRoot: z14.string(),
7419
+ opencodeServerUrl: z14.string().optional(),
7420
+ claudeCodeAvailable: z14.boolean(),
7421
+ codexAvailable: z14.boolean(),
7422
+ idleTimeoutMs: z14.number().int().positive().optional(),
7423
+ totalTimeoutMs: z14.number().int().positive().optional(),
7424
+ leaseRenewalMs: z14.number().int().positive().optional(),
7425
+ preparingRowDelayMs: z14.number().int().nonnegative().optional(),
7557
7426
  // Whether the turn writes a transcript, into the device's own directory
7558
7427
  // (`transcriptsDir()`).
7559
- transcripts: z15.boolean(),
7560
- logLevel: z15.string(),
7428
+ transcripts: z14.boolean(),
7429
+ logLevel: z14.string(),
7561
7430
  // The launcher's connector-health observations, by runtime, as of the start.
7562
- connectorHealth: z15.record(z15.string(), runtimeHealthSchema),
7431
+ connectorHealth: z14.record(z14.string(), runtimeHealthSchema),
7563
7432
  // True when this (conversation, agent) pair was already told its session
7564
7433
  // could not be saved: the notice is once per pair, not once per process.
7565
- sessionWriteNoticeClaimed: z15.boolean(),
7434
+ sessionWriteNoticeClaimed: z14.boolean(),
7566
7435
  // The runner's identity, minted with its credential. The start
7567
7436
  // PATCH carries it so the lease knows which runner holds the span.
7568
- runnerId: z15.string()
7437
+ runnerId: z14.string()
7569
7438
  });
7570
- var dispatchSchema = z15.object({
7571
- type: z15.literal("device:dispatch_requested"),
7572
- agentId: z15.string(),
7573
- deviceId: z15.string(),
7574
- conversationId: z15.string(),
7575
- messageId: z15.string(),
7576
- episode: z15.number().int().optional()
7439
+ var dispatchSchema = z14.object({
7440
+ type: z14.literal("device:dispatch_requested"),
7441
+ agentId: z14.string(),
7442
+ deviceId: z14.string(),
7443
+ conversationId: z14.string(),
7444
+ messageId: z14.string(),
7445
+ episode: z14.number().int().optional()
7577
7446
  });
7578
- var credentialsSchema = z15.object({
7579
- credential: z15.string().startsWith("cabrn_")
7447
+ var credentialsSchema = z14.object({
7448
+ credential: z14.string().startsWith("cabrn_")
7580
7449
  });
7581
7450
  function runnerEnv(input) {
7582
7451
  return {
@@ -7965,7 +7834,7 @@ import { join as join13 } from "path";
7965
7834
 
7966
7835
  // src/turn-control-tools.ts
7967
7836
  import { randomUUID as randomUUID2 } from "crypto";
7968
- import { z as z16 } from "zod";
7837
+ import { z as z15 } from "zod";
7969
7838
  var COMPANION_LOCAL_MCP_SERVER = "cabane_companion";
7970
7839
  var COMPANION_LOCAL_TOOL_GLOB = `mcp__${COMPANION_LOCAL_MCP_SERVER}__*`;
7971
7840
  var ASK_TOOL = "ask";
@@ -7977,20 +7846,21 @@ var WAKE_ME_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${WAKE_ME_TOOL}`;
7977
7846
  var CANCEL_WAKE_TOOL = "cancel_wake";
7978
7847
  var CANCEL_WAKE_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${CANCEL_WAKE_TOOL}`;
7979
7848
 
7980
- // src/codex-instructions.ts
7981
- import { mkdtemp, rm, writeFile } from "fs/promises";
7982
- import { tmpdir } from "os";
7849
+ // src/codex-config.ts
7850
+ import { readFile as readFile2 } from "fs/promises";
7851
+ import { homedir as homedir6 } from "os";
7983
7852
  import { join as join14 } from "path";
7853
+ import { parse } from "smol-toml";
7984
7854
 
7985
7855
  // src/codex-images.ts
7986
- import { mkdtemp as mkdtemp2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
7987
- import { tmpdir as tmpdir2 } from "os";
7856
+ import { mkdtemp, rm, writeFile } from "fs/promises";
7857
+ import { tmpdir } from "os";
7988
7858
  import { join as join15 } from "path";
7989
7859
 
7990
7860
  // src/codex-compactions.ts
7991
7861
  import { createReadStream } from "fs";
7992
7862
  import { readdir } from "fs/promises";
7993
- import { homedir as homedir6 } from "os";
7863
+ import { homedir as homedir7 } from "os";
7994
7864
  import { join as join16 } from "path";
7995
7865
  import { createInterface as createInterface2 } from "readline";
7996
7866
 
@@ -8005,12 +7875,12 @@ import { join as join17 } from "path";
8005
7875
  // src/secrets.ts
8006
7876
  import { existsSync as existsSync10, readFileSync as readFileSync8 } from "fs";
8007
7877
  import { join as join18 } from "path";
8008
- import { z as z17 } from "zod";
7878
+ import { z as z16 } from "zod";
8009
7879
  var PLACEHOLDER_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
8010
7880
  function secretsPath() {
8011
7881
  return join18(cabaneDir(), "secrets.json");
8012
7882
  }
8013
- var secretStoreSchema = z17.record(z17.string(), z17.string());
7883
+ var secretStoreSchema = z16.record(z16.string(), z16.string());
8014
7884
  function loadSecretStore() {
8015
7885
  const path = secretsPath();
8016
7886
  if (!existsSync10(path)) return makeStore({});
@@ -8361,7 +8231,7 @@ var DeviceLocks = class {
8361
8231
  // src/manifest.ts
8362
8232
  var DEVICE_MANIFEST = {
8363
8233
  runtimes: [{ name: "claude-code", version: null }],
8364
- capabilities: { hostFs: true, browser: true, userMcp: true }
8234
+ capabilities: { browser: true, userMcp: true }
8365
8235
  };
8366
8236
  function buildCompanionManifest(opts) {
8367
8237
  const v = opts.versions ?? {};
@@ -8465,7 +8335,7 @@ function emptyHarnessVersions() {
8465
8335
  }
8466
8336
 
8467
8337
  // src/harness-copy.ts
8468
- import { homedir as homedir7 } from "os";
8338
+ import { homedir as homedir8 } from "os";
8469
8339
  import { join as join21 } from "path";
8470
8340
  var HARNESS_LABELS = {
8471
8341
  "claude-code": "Claude Code",
@@ -8480,7 +8350,7 @@ function failureText(runtime, reason) {
8480
8350
  case "spawn":
8481
8351
  return `Couldn't check whether ${label} is signed in: Cabane's bundled ${label} wouldn't start. Repair it with ${repairSteps()} and try again.${spaced(repairCost())}`;
8482
8352
  case "config":
8483
- return `Couldn't check whether ${label} is signed in: its configuration file didn't load${runtime === "codex" ? ` (${tildePath(join21(homedir7(), ".codex", "config.toml"))})` : ""}. Fix it, then try again.`;
8353
+ return `Couldn't check whether ${label} is signed in: its configuration file didn't load${runtime === "codex" ? ` (${tildePath(join21(homedir8(), ".codex", "config.toml"))})` : ""}. Fix it, then try again.`;
8484
8354
  case "unreadable":
8485
8355
  return `Couldn't check whether ${label} is signed in: it answered in a way Cabane couldn't read. Try again, and if it keeps happening, update Cabane.`;
8486
8356
  }
@@ -9227,27 +9097,27 @@ function compareVersions(a, b) {
9227
9097
  // src/update-state.ts
9228
9098
  import { mkdirSync as mkdirSync13, readFileSync as readFileSync13, renameSync as renameSync8, writeFileSync as writeFileSync8 } from "fs";
9229
9099
  import { dirname as dirname10, join as join25 } from "path";
9230
- import { z as z18 } from "zod";
9231
- var resultSchema = z18.object({
9232
- kind: z18.enum(["updated", "rolled_back", "failed"]),
9233
- from: z18.string(),
9234
- to: z18.string(),
9235
- at: z18.string(),
9236
- reason: z18.string().optional()
9100
+ import { z as z17 } from "zod";
9101
+ var resultSchema = z17.object({
9102
+ kind: z17.enum(["updated", "rolled_back", "failed"]),
9103
+ from: z17.string(),
9104
+ to: z17.string(),
9105
+ at: z17.string(),
9106
+ reason: z17.string().optional()
9237
9107
  });
9238
- var pendingSchema = z18.object({
9239
- from: z18.string(),
9108
+ var pendingSchema = z17.object({
9109
+ from: z17.string(),
9240
9110
  // The package root `current` pointed at before the swap, so a rollback can point it back.
9241
- fromRoot: z18.string(),
9242
- to: z18.string(),
9243
- at: z18.string(),
9244
- boots: z18.number().int().nonnegative()
9111
+ fromRoot: z17.string(),
9112
+ to: z17.string(),
9113
+ at: z17.string(),
9114
+ boots: z17.number().int().nonnegative()
9245
9115
  });
9246
- var stateSchema = z18.object({
9247
- server: z18.object({ latest: z18.string(), minimum: z18.string(), at: z18.string() }).optional(),
9116
+ var stateSchema = z17.object({
9117
+ server: z17.object({ latest: z17.string(), minimum: z17.string(), at: z17.string() }).optional(),
9248
9118
  pending: pendingSchema.optional(),
9249
9119
  lastResult: resultSchema.optional(),
9250
- failed: z18.object({ version: z18.string(), attempts: z18.number().int(), at: z18.string() }).optional()
9120
+ failed: z17.object({ version: z17.string(), attempts: z17.number().int(), at: z17.string() }).optional()
9251
9121
  });
9252
9122
  function updateStatePath(dir2 = cabaneDir()) {
9253
9123
  return join25(dir2, "update.json");
@@ -9762,6 +9632,7 @@ var CompanionSupervisor = class {
9762
9632
  pollTimer = null;
9763
9633
  refreshing = false;
9764
9634
  stopped = false;
9635
+ reconciliationAbort = new AbortController();
9765
9636
  // The version-skew warning is logged once, not every heartbeat.
9766
9637
  versionSkewWarned = false;
9767
9638
  // This device's id, captured from the heartbeat / assignments response. The SSE
@@ -9807,6 +9678,7 @@ var CompanionSupervisor = class {
9807
9678
  // heartbeat + poll loops. A companion with no device token (logged out) does
9808
9679
  // nothing but say so.
9809
9680
  async start() {
9681
+ if (this.stopped) return;
9810
9682
  this.log.info(
9811
9683
  { protocolVersion: TURN_PROTOCOL_VERSION, version: COMPANION_VERSION },
9812
9684
  `Companion ${COMPANION_VERSION} starting on ${this.config.deviceLabel ?? hostname2()}`
@@ -9891,6 +9763,7 @@ var CompanionSupervisor = class {
9891
9763
  });
9892
9764
  this.updater.start();
9893
9765
  await this.refreshAssignments();
9766
+ if (this.stopped) return;
9894
9767
  this.deviceSub.start();
9895
9768
  this.heartbeatTimer = setInterval(() => this.kickHeartbeat(), HEARTBEAT_INTERVAL_MS);
9896
9769
  this.heartbeatTimer.unref?.();
@@ -10003,10 +9876,12 @@ var CompanionSupervisor = class {
10003
9876
  let items;
10004
9877
  let device;
10005
9878
  try {
10006
- const resp = await this.deviceApi.getAssignments();
9879
+ const resp = await this.deviceApi.getAssignments(this.reconciliationAbort.signal);
9880
+ if (this.stopped) return;
10007
9881
  items = resp.assignments;
10008
9882
  device = resp.device;
10009
9883
  } catch (err) {
9884
+ if (this.stopped) return;
10010
9885
  this.log.error(
10011
9886
  { ...apiErrorLogFields(err) },
10012
9887
  "Couldn't load the assigned agents. Check that this device is active in Cabane; trying again shortly."
@@ -10025,6 +9900,7 @@ var CompanionSupervisor = class {
10025
9900
  if (it.credential) setCredential(device.id, it.agentId, it.credential);
10026
9901
  }
10027
9902
  items = await this.restoreMissingCredentials(device.id, items);
9903
+ if (this.stopped) return;
10028
9904
  this.uncredentialed = new Set(
10029
9905
  items.filter((it) => !it.credential && !getCredential(device.id, it.agentId)).map((it) => `${it.workspaceId}|${it.agentId}`)
10030
9906
  );
@@ -10037,6 +9913,7 @@ var CompanionSupervisor = class {
10037
9913
  for (const wsId of [...this.workspaces.keys()]) {
10038
9914
  if (!byWorkspace.has(wsId)) await this.removeWorkspace(wsId);
10039
9915
  }
9916
+ if (this.stopped) return;
10040
9917
  for (const [wsId, list] of byWorkspace) this.reconcileWorkspace(device.id, wsId, list);
10041
9918
  const pruned = pruneCredentials(
10042
9919
  device.id,
@@ -10068,6 +9945,7 @@ var CompanionSupervisor = class {
10068
9945
  async restoreMissingCredentials(deviceId, items) {
10069
9946
  const restored = [];
10070
9947
  for (const it of items) {
9948
+ if (this.stopped) return restored;
10071
9949
  if (it.credential || getCredential(deviceId, it.agentId)) {
10072
9950
  restored.push(it);
10073
9951
  continue;
@@ -10079,7 +9957,11 @@ var CompanionSupervisor = class {
10079
9957
  continue;
10080
9958
  }
10081
9959
  try {
10082
- const { credential } = await this.deviceApi.reissueAgentCredential(it.agentId);
9960
+ const { credential } = await this.deviceApi.reissueAgentCredential(
9961
+ it.agentId,
9962
+ this.reconciliationAbort.signal
9963
+ );
9964
+ if (this.stopped) return restored;
10083
9965
  setCredential(deviceId, it.agentId, credential);
10084
9966
  this.log.info(
10085
9967
  { agentId: it.agentId, agentName: it.agentDisplayName },
@@ -10143,7 +10025,6 @@ var CompanionSupervisor = class {
10143
10025
  agentId: it.agentId,
10144
10026
  username: it.agentUsername,
10145
10027
  displayName: it.agentDisplayName,
10146
- mode: runConfig.hostAccess ? "full" : "none",
10147
10028
  hasCredential: false,
10148
10029
  missingSecrets: missing
10149
10030
  });
@@ -10163,7 +10044,6 @@ var CompanionSupervisor = class {
10163
10044
  agentId: it.agentId,
10164
10045
  username: it.agentUsername,
10165
10046
  displayName: it.agentDisplayName,
10166
- mode: runConfig.hostAccess ? "full" : "none",
10167
10047
  hasCredential: true,
10168
10048
  missingSecrets: missing
10169
10049
  });
@@ -10956,6 +10836,7 @@ var CompanionSupervisor = class {
10956
10836
  // ---- control actions ----
10957
10837
  async shutdown() {
10958
10838
  this.stopped = true;
10839
+ this.reconciliationAbort.abort();
10959
10840
  this.updater?.stop();
10960
10841
  if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
10961
10842
  if (this.detectTimer) clearInterval(this.detectTimer);
@@ -11231,33 +11112,46 @@ async function createCompanionRuntime(opts = {}) {
11231
11112
  });
11232
11113
  let restartForUpdate = async () => {
11233
11114
  };
11234
- const supervisor = new CompanionSupervisor({
11235
- config: cfg,
11236
- log,
11237
- hub,
11238
- ...opts.checkHarness ? { checkHarness: opts.checkHarness } : {},
11239
- ...embeddedRunner({
11240
- electron: Boolean(process.versions.electron),
11241
- execPath: process.execPath,
11242
- moduleUrl: import.meta.url,
11243
- env: process.env
11244
- }) ?? {},
11245
- restartForUpdate: () => restartForUpdate()
11246
- });
11247
- await supervisor.start();
11115
+ let supervisor;
11248
11116
  let control = null;
11249
11117
  let dashboard = null;
11118
+ let stopped = false;
11119
+ const stop = async () => {
11120
+ if (stopped) return;
11121
+ stopped = true;
11122
+ clearRuntimeStateIfOurs(instanceId);
11123
+ process.removeListener("exit", clearRuntimeOnExit);
11124
+ await Promise.allSettled([
11125
+ ...supervisor ? [supervisor.shutdown()] : [],
11126
+ ...control ? [control.close()] : [],
11127
+ ...dashboard ? [dashboard.close()] : []
11128
+ ]);
11129
+ };
11250
11130
  try {
11131
+ supervisor = new CompanionSupervisor({
11132
+ config: cfg,
11133
+ log,
11134
+ hub,
11135
+ ...opts.checkHarness ? { checkHarness: opts.checkHarness } : {},
11136
+ ...embeddedRunner({
11137
+ electron: Boolean(process.versions.electron),
11138
+ execPath: process.execPath,
11139
+ moduleUrl: import.meta.url,
11140
+ env: process.env
11141
+ }) ?? {},
11142
+ restartForUpdate: () => restartForUpdate()
11143
+ });
11144
+ const active2 = supervisor;
11251
11145
  control = await startControlServer({
11252
11146
  status: () => ({
11253
11147
  ...hub.statusJson(),
11254
- ...supervisor.lastHeartbeat(),
11255
- detection: supervisor.currentDetection()
11148
+ ...active2.lastHeartbeat(),
11149
+ detection: active2.currentDetection()
11256
11150
  }),
11257
- detect: () => supervisor.redetect(),
11258
- allow: (runtime) => supervisor.allowHarness(runtime),
11259
- update: (auto) => supervisor.update(auto),
11260
- stop: () => void supervisor.requestStop()
11151
+ detect: () => active2.redetect(),
11152
+ allow: (runtime) => active2.allowHarness(runtime),
11153
+ update: (auto) => active2.update(auto),
11154
+ stop: () => void stop().then(() => active2.requestStop())
11261
11155
  });
11262
11156
  if (opts.dashboard) {
11263
11157
  const preferredPort = opts.port ?? cfg.dashboardPort;
@@ -11268,6 +11162,7 @@ async function createCompanionRuntime(opts = {}) {
11268
11162
  });
11269
11163
  hub.setDashboardUrl(dashboard.url);
11270
11164
  }
11165
+ if (stopped) throw new Error("The companion was stopped during startup.");
11271
11166
  writeRuntimeState({
11272
11167
  pid: process.pid,
11273
11168
  socket: control.path,
@@ -11285,7 +11180,7 @@ async function createCompanionRuntime(opts = {}) {
11285
11180
  clearRuntimeStateIfOurs(instanceId);
11286
11181
  process.removeListener("exit", clearRuntimeOnExit);
11287
11182
  await Promise.allSettled([
11288
- supervisor.shutdown(),
11183
+ ...supervisor ? [supervisor.shutdown()] : [],
11289
11184
  ...control ? [control.close()] : [],
11290
11185
  ...dashboard ? [dashboard.close()] : []
11291
11186
  ]);
@@ -11297,22 +11192,17 @@ async function createCompanionRuntime(opts = {}) {
11297
11192
  });
11298
11193
  throw err;
11299
11194
  }
11300
- let stopped = false;
11301
- const stop = async () => {
11302
- if (stopped) return;
11303
- stopped = true;
11304
- clearRuntimeStateIfOurs(instanceId);
11305
- process.removeListener("exit", clearRuntimeOnExit);
11306
- try {
11307
- await supervisor.shutdown();
11308
- await closeSurfaces(control, dashboard);
11309
- } catch {
11310
- }
11311
- };
11195
+ const active = supervisor;
11312
11196
  restartForUpdate = async () => {
11313
11197
  await stop();
11314
11198
  process.exit(0);
11315
11199
  };
11200
+ void active.start().catch(async (err) => {
11201
+ log.error({ err }, "Couldn't initialize the companion.");
11202
+ recordCrash({ reason: String(err), origin: "startup", at: (/* @__PURE__ */ new Date()).toISOString() });
11203
+ await stop();
11204
+ process.exitCode = 1;
11205
+ });
11316
11206
  return {
11317
11207
  ok: true,
11318
11208
  runtime: {
@@ -11320,16 +11210,12 @@ async function createCompanionRuntime(opts = {}) {
11320
11210
  socketPath: control.path,
11321
11211
  config: cfg,
11322
11212
  stop,
11323
- heartbeatNow: () => supervisor.heartbeatNow(),
11213
+ heartbeatNow: () => active.heartbeatNow(),
11324
11214
  harnesses: () => hub.statusJson().harnesses ?? [],
11325
- detect: () => supervisor.redetect()
11215
+ detect: () => active.redetect()
11326
11216
  }
11327
11217
  };
11328
11218
  }
11329
- async function closeSurfaces(control, dashboard) {
11330
- await control.close();
11331
- if (dashboard) await dashboard.close();
11332
- }
11333
11219
  export {
11334
11220
  createCompanionRuntime,
11335
11221
  embeddedRunner,