baychat 0.36.0 → 0.38.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.
@@ -39,6 +39,9 @@ async function handleLaunch(launch, deps, io) {
39
39
  return refused("NAME_INVALID");
40
40
  if (launch.kind === "STOP")
41
41
  return stopSession(launch, deps);
42
+ const agentProblem = agentStartRefusal(launch, deps);
43
+ if (agentProblem)
44
+ return agentProblem;
42
45
  if ((0, acp_launch_1.isAcpLaunchRuntime)(launch.runtime))
43
46
  return startAcp(launch, deps);
44
47
  if (launch.runtime === "codex" && deps.codex && (0, codex_version_1.codexExecFallback)() && inDefaultBay(launch, deps)) {
@@ -58,6 +61,38 @@ async function handleLaunch(launch, deps, io) {
58
61
  return { status: "STARTED" };
59
62
  return watchForDevChannelWarning(launch, started.entry, deps, io);
60
63
  }
64
+ /**
65
+ * An AGENT's start (Phase 4, docs/features/AUTOMATIONS_AND_PACKAGES.md §4.3–4.4), before anything
66
+ * else about it: undefined when it may go on.
67
+ *
68
+ * TWO KEYS. One that went through the owner's standing permission with no card ("agent-grant"),
69
+ * and a schedule's run ("schedule" — a standing permission too), needs the computer's own yes as
70
+ * well — the spot's `agentStarts`, which only `baychat launch add … --agent-starts` at this
71
+ * computer writes — or it is refused AGENT_STARTS_NOT_ALLOWED. A spot that no longer exists is
72
+ * left to `spotFor` (SPOT_UNKNOWN). A card-allowed start needs no flag.
73
+ *
74
+ * NEVER UNATTENDED. Any agent's start runs with its normal approval cards on the owner's phone:
75
+ * Claude Code with its approve hook, Codex only as the relay runs it (app-server, approvals on the
76
+ * phone) — never the Codex TUI with `--ask-for-approval never`, which a start in another Bay, or a
77
+ * relay without the Codex runner, would otherwise open. Those are refused as not offered yet.
78
+ */
79
+ function agentStartRefusal(launch, deps) {
80
+ if (launch.startedBy === "person")
81
+ return undefined;
82
+ if (launch.startedBy === "agent-grant" || launch.startedBy === "schedule") {
83
+ const spot = deps.loadPolicy()?.spots.find((s) => s.id === launch.spotId);
84
+ if (spot && !spot.agentStarts) {
85
+ const who = launch.startedBy === "schedule" ? "schedules" : "agents";
86
+ deps.log(`launch ${launch.id}: refused, ${who} may not start sessions in "${spot.label}" without asking. Run \`baychat launch add <folder> --agent-starts\` here to allow it.`);
87
+ return refused("AGENT_STARTS_NOT_ALLOWED");
88
+ }
89
+ }
90
+ if (launch.runtime === "codex" && !relayRunsCodex(launch, deps)) {
91
+ deps.log(`launch ${launch.id}: refused, an agent's or a schedule's Codex start runs only as the relay runs it, with approvals on the phone`);
92
+ return refused(inDefaultBay(launch, deps) ? "RUNTIME_NOT_YET" : "OTHER_BAY_NOT_YET");
93
+ }
94
+ return undefined;
95
+ }
61
96
  /** The checks every START makes before anything runs: the spot, the runtime, the room, the folder. */
62
97
  function spotFor(launch, deps) {
63
98
  const spot = deps.loadPolicy()?.spots.find((s) => s.id === launch.spotId);
@@ -530,6 +565,10 @@ function parseLaunch(raw) {
530
565
  const listen = l.listen === undefined || l.listen === "channel" ? "channel" : l.listen === "monitor" ? "monitor" : null;
531
566
  if (!listen)
532
567
  return null;
568
+ // Missing = an older server = a person's start. Anything unknown is malformed, never a person.
569
+ const startedBy = l.startedBy === undefined ? "person" : STARTED_BY.find((s) => s === l.startedBy);
570
+ if (!startedBy)
571
+ return null;
533
572
  return {
534
573
  id: l.id,
535
574
  kind: l.kind,
@@ -545,8 +584,10 @@ function parseLaunch(raw) {
545
584
  commandAuthorized: l.commandAuthorized === true,
546
585
  // Only an explicit "any" on a STOP widens it; anything else is the launcher's own sessions.
547
586
  ...(l.kind === "STOP" && l.scope === "any" ? { scope: "any" } : {}),
587
+ startedBy,
548
588
  };
549
589
  }
590
+ const STARTED_BY = ["person", "agent-card", "agent-grant", "schedule"];
550
591
  /**
551
592
  * Which of this computer's Bay connections a launch joins on. `entry` is the MCP connection to
552
593
  * name in the `/baychat` line — undefined when there is only one, so there is nothing to choose
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.deviceRequest = void 0;
36
+ exports.deviceRequest = exports.LAUNCH_LEVEL = void 0;
37
37
  exports.startLauncher = startLauncher;
38
38
  const fs = __importStar(require("fs"));
39
39
  const api_1 = require("../../api");
@@ -241,6 +241,13 @@ function realLoopDeps(auth, log, signal, { forgetSession, registeredSession, reg
241
241
  : {}),
242
242
  };
243
243
  }
244
+ /**
245
+ * This relay's launch level (security sweep S4, 29 Oct): 1 = it honours `startedBy` and a folder's
246
+ * `--agent-starts`, so the server may hand it a standing permission's start (an agent's "Always",
247
+ * a schedule). Sent as a header on every device-API call — never in the capabilities body, whose
248
+ * strict schema on an older server would refuse the whole report.
249
+ */
250
+ exports.LAUNCH_LEVEL = 1;
244
251
  /**
245
252
  * The device API, with an abort signal and a deadline. Unlike `apiRequest` it accepts the empty
246
253
  * `204` the capabilities PUT and the report answer with.
@@ -248,7 +255,7 @@ function realLoopDeps(auth, log, signal, { forgetSession, registeredSession, reg
248
255
  const deviceRequest = async (auth, method, apiPath, body, signal) => {
249
256
  const res = await fetch(`${auth.baseUrl}${apiPath}`, {
250
257
  method,
251
- headers: { Authorization: `Bearer ${auth.token}`, "Content-Type": "application/json" },
258
+ headers: { Authorization: `Bearer ${auth.token}`, "Content-Type": "application/json", "X-BayChat-Launch-Level": String(exports.LAUNCH_LEVEL) },
252
259
  body: body === undefined ? undefined : JSON.stringify(body),
253
260
  signal: AbortSignal.any([signal, AbortSignal.timeout(WAIT_SECONDS * 1000 + REQUEST_GRACE_MS)]),
254
261
  });
package/hooks/hooks.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "description": "BayChat's denied-command card for every session, also ones started by hand: an auto-mode denial becomes a card in the session's BayChat chat once it has joined (docs/features/DENIED_COMMAND_CARD.md). No PostToolUse hook: it would start baychat on every tool call.",
2
+ "description": "BayChat's denied-command card for every session, also ones started by hand: an auto-mode denial becomes a card in the session's BayChat chat once it has joined (docs/features/DENIED_COMMAND_CARD.md). After a compact, a channel session is told when its BayChat feed is down (channel-check). A question left open in the terminal (a permission dialog, an MCP form) becomes one line on the owner's phone (notify-hook). No PostToolUse hook: it would start baychat on every tool call.",
3
3
  "hooks": {
4
4
  "PermissionDenied": [
5
5
  {
@@ -13,6 +13,30 @@
13
13
  }
14
14
  ]
15
15
  }
16
+ ],
17
+ "SessionStart": [
18
+ {
19
+ "matcher": "compact",
20
+ "hooks": [
21
+ {
22
+ "type": "command",
23
+ "command": "baychat channel-check",
24
+ "timeout": 10
25
+ }
26
+ ]
27
+ }
28
+ ],
29
+ "Notification": [
30
+ {
31
+ "matcher": "permission_prompt|elicitation_dialog|elicitation_url_dialog",
32
+ "hooks": [
33
+ {
34
+ "type": "command",
35
+ "command": "baychat notify-hook",
36
+ "timeout": 30
37
+ }
38
+ ]
39
+ }
16
40
  ]
17
41
  }
18
42
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "baychat",
3
- "version": "0.36.0",
3
+ "version": "0.38.0",
4
4
  "description": "BayChat connector CLI — pair an agent session (Claude Code, Codex, Cursor) with BayChat, or have the relay run DeepSeek or the Cursor CLI for you",
5
5
  "bin": {
6
6
  "baychat": "dist/index.js"