@runuai/host 0.9.5 → 0.9.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runuai/host",
3
- "version": "0.9.5",
3
+ "version": "0.9.7",
4
4
  "description": "Uai host — runs ephemeral AI coding tasks in Docker on a machine you control.",
5
5
  "license": "MIT",
6
6
  "author": "Diogo Perillo <diogo.perillo@gmail.com>",
@@ -1244,6 +1244,21 @@ managed_clean_env=(
1244
1244
  "PATH=/usr/bin:/bin"
1245
1245
  )
1246
1246
 
1247
+ # OpenCode and code-server share ~/.local/share. OpenCode credentials are
1248
+ # copied as root below, so creating only its leaf with `mkdir -p` can leave the
1249
+ # shared parent root-owned (live 2026-08-04). Then node cannot create
1250
+ # code-server's User profile and the editor silently falls back to its light,
1251
+ # full-chrome defaults. Repair both explicitly on every start/resume; GNU
1252
+ # `install -d` also corrects an existing directory with the wrong owner.
1253
+ if ! docker exec -u root "${managed_exec_env[@]}" "$app_container" \
1254
+ /usr/bin/install -d -o node -g node -m 0755 \
1255
+ /home/node/.local/share \
1256
+ /home/node/.local/share/opencode >/dev/null; then
1257
+ emit_err "CONTAINER_INIT_FAILED" \
1258
+ "Uai could not prepare the task's node-owned application data directory. Retry the task; the editor cannot load its settings until this succeeds." \
1259
+ "prepare container application data"
1260
+ fi
1261
+
1247
1262
  # Git rejects a repository owned by another uid even when its shared group is
1248
1263
  # writable. Trust only the selected worktrees in this task container—never `*`
1249
1264
  # and never the host cache. The task-private ~/.gitconfig makes these entries
@@ -1404,8 +1419,7 @@ docker exec -u root "$app_container" \
1404
1419
  # binary is baked into the image; here we copy the arch-independent auth+config.
1405
1420
  # ADR-076 EXTRA accounts (isolated dirs) are copied later from the host-agent
1406
1421
  # (provisionEngineAccounts) since they live in the sealed DB, not on disk here.
1407
- docker exec -u root "$app_container" \
1408
- mkdir -p /home/node/.local/share/opencode >/dev/null 2>&1 || true
1422
+ # The shared parent and this leaf were created with node ownership above.
1409
1423
  for oc_item in auth.json config.json; do
1410
1424
  if [ -e "$UAI_OWNER_HOME/.local/share/opencode/$oc_item" ]; then
1411
1425
  docker cp "$UAI_OWNER_HOME/.local/share/opencode/$oc_item" \
package/src/index.ts CHANGED
@@ -356,7 +356,21 @@ export const hostCommands: HostCommands = {
356
356
  logCommand(ctx, "channelEnsure", input.taskId);
357
357
  try {
358
358
  getOrchestrator().registerChannelSpec(normalizeChannelSpec(input));
359
- await getOrchestrator().ensureStarted(input.taskId);
359
+ const ready = await getOrchestrator().ensureStarted(input.taskId);
360
+ // Open mode historically treats ensure as best-effort: a stopped task or
361
+ // transient spawn failure still acknowledges the idempotent command.
362
+ // Secretary mode is different because its CLI is the only delegation
363
+ // path, so starting without it would create a human-facing agent that
364
+ // cannot hand work to the crew.
365
+ if (!ready && input.mode === "secretary") {
366
+ return {
367
+ ok: false,
368
+ code: HostErrorCode.TaskNotRunning,
369
+ message:
370
+ "Secretary channel could not start; verify the task CLI and token were materialized",
371
+ retryable: false,
372
+ };
373
+ }
360
374
  return ok(undefined);
361
375
  } catch (err) {
362
376
  return failFromUnknown(err);
package/src/main.ts CHANGED
@@ -71,6 +71,7 @@ import {
71
71
  capabilities as agentKindCapabilities,
72
72
  onChange as onRegistryChange,
73
73
  } from "../lib/agents/registry";
74
+ import { canAdvertiseTypedSecretaryDispatch } from "../lib/agents/mode";
74
75
  // Importing the real factory triggers the built-in adapters' register()
75
76
  // calls (claude, codex), so the registry is populated before we advertise.
76
77
  import "../lib/agents/factory";
@@ -78,6 +79,7 @@ import { ensureStandardImage, standardRuntimes } from "../lib/standard-image";
78
79
  import { hostCommands, hostEvents } from "./index";
79
80
  import {
80
81
  HostErrorCode,
82
+ SECRETARY_TYPED_DISPATCH_PROTOCOL_FEATURE,
81
83
  TRANSCRIPT_TARGETS_PROTOCOL_FEATURE,
82
84
  type CloudToHost,
83
85
  type McpOp,
@@ -221,7 +223,15 @@ async function startLocalUi(): Promise<void> {
221
223
  function buildCapabilities(): HostCapabilities {
222
224
  return {
223
225
  version: packageVersion(),
224
- protocolFeatures: [TRANSCRIPT_TARGETS_PROTOCOL_FEATURE],
226
+ protocolFeatures: [
227
+ TRANSCRIPT_TARGETS_PROTOCOL_FEATURE,
228
+ // The echo adapter cannot execute the in-task CLI. Advertising typed
229
+ // dispatch in mock mode would let the composer create a Secretary that
230
+ // has no way to wake crew.
231
+ ...(canAdvertiseTypedSecretaryDispatch()
232
+ ? [SECRETARY_TYPED_DISPATCH_PROTOCOL_FEATURE]
233
+ : []),
234
+ ],
225
235
  agentKinds: agentKindCapabilities(),
226
236
  runtimes: standardRuntimes(),
227
237
  githubUsers: connectedUserIds(),
@@ -1483,6 +1493,13 @@ function expectChannelEnsureInput(
1483
1493
  if (typeof input.secretaryAgentId === "string") {
1484
1494
  out.secretaryAgentId = input.secretaryAgentId;
1485
1495
  }
1496
+ if (
1497
+ input.secretaryDispatchProtocol ===
1498
+ SECRETARY_TYPED_DISPATCH_PROTOCOL_FEATURE
1499
+ ) {
1500
+ out.secretaryDispatchProtocol =
1501
+ SECRETARY_TYPED_DISPATCH_PROTOCOL_FEATURE;
1502
+ }
1486
1503
  // ADR-053: browser testing flag (optional; tolerant of absence).
1487
1504
  if (input.browserTesting === true) out.browserTesting = true;
1488
1505
  // ADR-049: humans in the chat (optional; tolerant of absence for older
package/src/protocol.ts CHANGED
@@ -33,7 +33,13 @@ export type HostCommandResult<T> =
33
33
 
34
34
  /** Capability ids shared by host advertisement and fail-closed cloud gates. */
35
35
  export const TRANSCRIPT_TARGETS_PROTOCOL_FEATURE = "transcript-targets-v1";
36
+ export const SECRETARY_TYPED_DISPATCH_PROTOCOL_FEATURE =
37
+ "secretary-typed-dispatch-v1";
36
38
  export const COMMUNICATOR_EXECUTION_PROFILE = "communicator";
39
+ export const MAX_AGENT_ID_CHARS = 128;
40
+ export const MAX_SECRETARY_DISPATCH_RECIPIENTS = 16;
41
+ export const MAX_SECRETARY_DISPATCH_INSTRUCTION_CHARS = 8_000;
42
+ export const MAX_SECRETARY_DISPATCH_ID_CHARS = 256;
37
43
 
38
44
  export interface CommandContext {
39
45
  commandId: string;
@@ -250,6 +256,10 @@ export interface ChannelEnsureInput {
250
256
  mode?: "open" | "secretary";
251
257
  /** The one human-facing communicator in secretary mode. */
252
258
  secretaryAgentId?: string;
259
+ /** Cloud↔host negotiation for ADR-083's structured dispatch action. A new
260
+ * host refuses Secretary mode when an older cloud omits it, rather than
261
+ * starting a role whose only delegation path that cloud cannot consume. */
262
+ secretaryDispatchProtocol?: typeof SECRETARY_TYPED_DISPATCH_PROTOCOL_FEATURE;
253
263
  /** ADR-049: the humans in the chat. Optional for wire back-compat; absent
254
264
  * or single-entry behaves exactly like the pre-ADR-049 single-human task. */
255
265
  humans?: ChannelHuman[];
@@ -642,6 +652,37 @@ export type HostEvent =
642
652
  costUsd?: number;
643
653
  };
644
654
  }
655
+ /**
656
+ * ADR-083 §5: the secretary raised a STRUCTURED dispatch (`#165vkk`).
657
+ *
658
+ * This exists because a lane cannot be inferred from prose. Rows are created
659
+ * on the first content chunk, so "who was this for?" has to be answered
660
+ * before any text exists — and only a tool call carries that answer
661
+ * structurally. v0 derived it from @mentions in the finished turn, which put
662
+ * every crew-directed sentence into the human's lane and copied the human's
663
+ * answer into the crew's.
664
+ *
665
+ * `recipients` are roster agent ids the engine's dispatch tool was given.
666
+ * The cloud validates them against the roster; it does not trust them.
667
+ */
668
+ | {
669
+ kind: "agent.dispatch";
670
+ taskId: string;
671
+ agentId: string;
672
+ recipients: string[];
673
+ instruction: string;
674
+ /**
675
+ * The engine's own id for the tool call this came from (Claude's
676
+ * `tool_use` id). Optional, and the cloud does not currently dedupe on
677
+ * it: nothing replays host events today, which is exactly the gap
678
+ * ADR-083 §6 / `#dmmcbh` exists to close. Carried now because it is free
679
+ * while the emission point is being chosen and expensive to add to the
680
+ * wire afterwards — and because a dispatch row that cannot be traced
681
+ * back to the tool call that raised it is hard to debug precisely when
682
+ * something has gone wrong with waking the crew.
683
+ */
684
+ dispatchId?: string;
685
+ }
645
686
  | {
646
687
  kind: "agent.tool_call";
647
688
  taskId: string;