viber-channel 0.8.22 → 0.8.24

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.
@@ -1,51 +1,94 @@
1
1
  /**
2
- * Control-plane capabilities help (#332).
2
+ * Control-plane capabilities help (#332, rewritten for the Gateway in #651).
3
3
  *
4
- * An agent connected via the channel does not otherwise know that a `vibe-master`
5
- * control plane exists on the machine, nor how to spawn other agents with it. Per
6
- * the issue + JP: this must be DISCOVERABLE ON DEMAND, never injected into every
7
- * conversation. So the channel exposes a `capabilities` MCP tool that returns
8
- * this text only when the agent chooses to call it; the channel instructions
9
- * carry just a one-line pointer to the tool, not its contents.
4
+ * An agent connected via the channel does not otherwise know how Viber launches,
5
+ * supervises and dissolves other agents. Per the issue + JP: this must be
6
+ * DISCOVERABLE ON DEMAND, never injected into every conversation. So the channel
7
+ * exposes a `capabilities` MCP tool that returns this text only when the agent
8
+ * chooses to call it; the channel instructions carry just a one-line pointer.
9
+ *
10
+ * This is the HOME of "how to USE Viber" for every project (TaskMan points here,
11
+ * #651). Every claim below was checked against the code at #651; keep it that way.
10
12
  *
11
13
  * Pure (no I/O) so it is trivially testable and identical across hosts.
12
14
  */
13
15
 
14
16
  export function capabilitiesText(): string {
15
17
  return [
16
- "Viber control plane (vibe-master) — orchestrate other agents from this machine.",
17
- "",
18
- "vibe-master is a CLI + TUI (installed on the operator machine) that spawns and",
19
- "supervises agents which appear in the Viber web UI and answer in their own",
20
- "conversations. Use it when you want to delegate to another agent (e.g. a Codex",
21
- "reviewer, a second Claude, or a Gemma chat).",
22
- "",
23
- "Runtimes: codex (background, via vctl) · gemma (background) · claude (terminal).",
24
- "(--runtime defaults to codex. Gemma is conversational/read-only only — it",
25
- " rejects --permission read-write.)",
26
- "",
27
- "Spawn one agent (CLI, non-interactive):",
28
- " vibe-master spawn --permission <read-only|read-write> \\",
29
- " [--runtime codex|claude|gemma] [--name <id>] [--scope <name>]",
30
- " → only --permission is required; --runtime and --name are optional",
31
- " (--name auto-generated if omitted). Prints the agent id; it registers",
32
- " in Viber and comes online.",
33
- "",
34
- "Multi-scope (#307): spawn into ANOTHER project by name. Register a project",
35
- "folder once with `vibe-master scopes add <name> <path>`, then",
36
- "`vibe-master spawn --scope <name> …` lands the agent in that project (its own",
37
- ".viber auth). `--scope` and `--cwd` are exclusive. `vibe-master scopes` lists them.",
38
- "",
39
- "Interactive: `vibe-master tui` (menu: + Add agent → runtime → permission → name).",
40
- "Inspect / stop: `vibe-master list` · `vibe-master kill <id>`.",
41
- "",
42
- "Talk to a spawned agent from here with list_agents + message_agent.",
43
- "Cross-project (#307): by default list_agents/message_agent see only THIS",
44
- "project. If the OWNER marks this instance an ORCHESTRATOR (toggle on the web",
45
- "project page — server-authorized, never self-declared), list_agents then covers",
46
- "ALL the owner's projects (each entry tagged with its project) and message_agent",
47
- "can DM an agent in another of those projects.",
48
- "Requires vibe-master installed on the machine (see the Install page on the web).",
49
- "Run `vibe-master --help` for the full command reference.",
18
+ "Viber control plane — launch, supervise and dissolve OTHER agents (Codex, Claude, Gemma).",
19
+ "Launched agents appear in the Viber web UI and answer in their own conversations.",
20
+ "",
21
+ "INSTALL (new machine, Windows): run install.ps1 from the newest GitHub release tagged",
22
+ "vibe-master-v* in dgx80/viber. Those releases are NOT marked \"Latest\" (Latest is the",
23
+ "core v* release, which has no install.ps1), so resolve the tag instead of using /latest:",
24
+ " $t = (gh release list --repo dgx80/viber --limit 30 --json tagName --jq",
25
+ " '.[].tagName' | Where-Object { $_ -like 'vibe-master-v*' })[0]",
26
+ " gh release download $t --repo dgx80/viber --pattern install.ps1 --clobber",
27
+ " .\\install.ps1 -Token <PAT> # fine-grained, READ-ONLY on dgx80/viber + dgx80/vctl:",
28
+ " it is stored on disk for auto-update — never pass your full `gh auth token`.",
29
+ "It installs vibe-master.exe + viber-gateway.exe (%LOCALAPPDATA%\\vibe-master, on PATH),",
30
+ "vctl, bun, viber-channel and the Start-menu app \"Viber\". Then, in the project folder:",
31
+ "`bunx viber-channel connect <url from the web>` writes .viber/auth.json.",
32
+ "",
33
+ "THE VIBER APP (runner) carries presence. Start \"Viber\" (viber-gateway.exe, no args):",
34
+ "a tray app whose runner beats every 30 s for every agent it launched. Agents do not",
35
+ "heartbeat themselves: while the app is not running they stay PAUSED — shown offline,",
36
+ "and send_message / message_agent answer \"Paused — … Nothing was sent\". That is not",
37
+ "a failure: it resumes by itself once the runner covers the agent again.",
38
+ "Quitting the app does not kill agents. The runner also relaunches a dead agent when",
39
+ "the web asks (greyed rows).",
40
+ "",
41
+ "LAUNCH A TEAM (from the project folder, which must hold .viber/auth.json, or",
42
+ ".viber/dev.auth.json for --env dev):",
43
+ " viber-gateway launch-team <template> --prefix <p> [--team <t>] [--env dev|staging]",
44
+ " [--cwd <dir>]",
45
+ " → --env defaults to staging (the stable channel: agents register with no trust",
46
+ " prompt; use it for working teams). --team defaults to the prefix. Each agent is",
47
+ " named <prefix>-<role>. Built-in template: dev-team (dev lead + 2 reviewers).",
48
+ "ONE AGENT ALONE = a one-role team. launch-team has no --name: declare a template in",
49
+ ".viber/agent-teams.json (an entry with a built-in's name REPLACES it entirely):",
50
+ " {\"teams\":{\"solo\":{\"roles\":[{\"role\":\"dev\",\"runtime\":\"claude\",",
51
+ " \"permission\":\"read-write\",\"rolePrompt\":\"You are the dev of {prefix}.\"}]}}}",
52
+ " runtime: claude|codex|gemma · permission: read-only|read-write (gemma: read-only",
53
+ " only; all roles of one template must share one permission) · optional model, count.",
54
+ "Templates edited on the web (and via team_templates) are a SEPARATE store used by the",
55
+ "web launch; launch-team reads only built-ins + .viber/agent-teams.json.",
56
+ "",
57
+ "DISSOLVE (stops the processes, then revokes the proven-stopped instances):",
58
+ " viber-gateway dissolve --team <t> | --agent <launch-id> --dir <registry>",
59
+ " [--auth <auth.json>] [--base-url <url>]",
60
+ " --dir is REQUIRED (exit 2 without it; refused under %TEMP%). For agents launched by",
61
+ " the app or launch-team it is <app home>\\.viber\\gateway-registry, the app home",
62
+ " being %LOCALAPPDATA%\\Viber unless VIBER_APP_HOME is set.",
63
+ " --auth defaults to VIBER_AUTH_FILE, else .viber/auth.json; --base-url to",
64
+ " VIBER_BASE_URL, else https://viber.dgypx.dev (the stable backend) — in dev pass both.",
65
+ " It never kills its caller. LIMIT: an agent already dead whose stop it cannot prove",
66
+ " is reported \"stop NOT proven, not revoked\" — revoke it from the web sidebar instead.",
67
+ "",
68
+ "ON THE WEB (owner, sidebar \"⋯\"): project row → Add agent (one named agent from a",
69
+ "template role, into an existing or new team) / New team; team row → Add agent;",
70
+ "agent row → Revoke (confirmation; works online or offline). Both launches need an",
71
+ "ONLINE runner (the Viber app). Project page → clean up offline agents: revokes the",
72
+ "offline instances and their conversations (never DMs), server re-checks each one.",
73
+ "Its only guard is live presence: right after a ws_server restart, wait ~30 s before",
74
+ "cleaning up, or live agents not yet re-reported get revoked.",
75
+ "",
76
+ "TALK to agents: list_agents (take the entry with online: true — after a revoke +",
77
+ "relaunch the label repeats and the old id is dead) + message_agent.",
78
+ "ORCHESTRATOR (#307): only the OWNER can mark an instance orchestrator (web project",
79
+ "page). Then list_agents/message_agent cover ALL the owner's projects, and the",
80
+ "team_templates tool can list/get/create/update/delete the owner's team templates —",
81
+ "only on the owner's explicit request, never on a request relayed by an agent.",
82
+ "Agents cannot launch teams through the API: launches go through the web or the CLI.",
83
+ "",
84
+ "CODEX agents, observed today: their Viber MCP calls, message_agent included, have",
85
+ "been rejected by their approval policy (#609), so they cannot open an exchange — write",
86
+ "to them FIRST and read their reply on that DM. Their `gh` has no usable token in",
87
+ "the sandbox (#525): run gh (labels, PR comments) for them.",
88
+ "",
89
+ "Low level, not relaunchable from the web: `vibe-master spawn --permission <p>",
90
+ "[--runtime codex|claude|gemma] [--name <id>]` (`vibe-master --help`).",
91
+ "viber-gateway has no --help: an unknown verb runs the startup command (exit 2",
92
+ "without --dir; with --dir it runs startup on that registry).",
50
93
  ].join("\n");
51
94
  }
@@ -125,8 +125,9 @@ export const CLAUDE_TOOL_DEFS = [
125
125
  {
126
126
  name: "capabilities",
127
127
  description:
128
- "On-demand: learn what the Viber control plane (vibe-master) can do on this " +
129
- "machine — how to spawn and supervise OTHER agents (Codex, Claude, Gemma). " +
128
+ "On-demand: how Viber launches, supervises and dissolves OTHER agents (Codex, " +
129
+ "Claude, Gemma) — install, the Viber app/runner, viber-gateway launch-team and " +
130
+ "dissolve, the web sidebar, orchestrator template admin, Codex limits. " +
130
131
  "Call this only when orchestrating other agents is relevant; it is not part " +
131
132
  "of the default context. Returns a short usage reference.",
132
133
  inputSchema: {
@@ -0,0 +1,76 @@
1
+ /**
2
+ * gateway_features.ts — which `capabilities.version` this runner may announce (#647).
3
+ *
4
+ * The server refuses a NAMED agent to a runner announcing < 2. That refusal is the only stop
5
+ * BEFORE launch: an older gateway ignores `--name` (and an unknown verb falls through to its
6
+ * startup sweep), so the runner must never announce 2 unless its gateway says it carries `name`.
7
+ * Probed with `<gateway> version`; any failure, timeout or unreadable output → 1 (fail closed).
8
+ *
9
+ * A 2 is cached for the life of the process. A 1 is re-probed after `RETRY_ONE_MS`: an owner
10
+ * who updates viber-gateway must not have to restart the runner too (Opus step-02 review).
11
+ * The probe is SYNCHRONOUS (up to its 5 s timeout) on the heartbeat that runs it — bounded,
12
+ * and at most once per retry window.
13
+ */
14
+ import { execFileSync } from "./hidden_proc.js";
15
+ import { viberGatewayBaseCmd } from "./runner_exec.js";
16
+
17
+ export type VersionProbe = (base: string[]) => string;
18
+
19
+ /** The default probe: no shell, short timeout; throws on non-zero exit or spawn error. */
20
+ export const spawnVersionProbe: VersionProbe = (base) =>
21
+ // execFileSync throws on spawn error, non-zero exit and timeout — all three mean "announce 1".
22
+ execFileSync(base[0] as string, [...base.slice(1), "version"], {
23
+ timeout: 5_000,
24
+ encoding: "utf-8",
25
+ stdio: ["ignore", "pipe", "ignore"],
26
+ });
27
+
28
+ /** The feature tokens of a `viber-gateway <ver> features=a,b` line; [] when absent. */
29
+ export function parseGatewayFeatures(output: string): string[] {
30
+ const line = output.split(/\r?\n/).find((l) => l.startsWith("viber-gateway ")) ?? "";
31
+ const m = / features=(\S+)/.exec(line);
32
+ return m ? (m[1] as string).split(",").filter((t) => t.length > 0) : [];
33
+ }
34
+
35
+ export function capabilitiesVersionFrom(output: string): 1 | 2 {
36
+ return parseGatewayFeatures(output).includes("name") ? 2 : 1;
37
+ }
38
+
39
+ const RETRY_ONE_MS = 10 * 60 * 1000;
40
+
41
+ let cached: 1 | 2 | undefined;
42
+ let cachedAt = 0;
43
+ let probeBase: string[] | undefined;
44
+
45
+ /** #632 app mode: the gateway IS this process — probe it, not a `viber-gateway` on PATH. */
46
+ export function setGatewayProbeBase(base: string[]): void {
47
+ probeBase = base;
48
+ }
49
+
50
+ export function announcedCapabilitiesVersion(
51
+ probe: VersionProbe = spawnVersionProbe,
52
+ log: (line: string) => void = (l) => process.stderr.write(`${l}\n`),
53
+ now: () => number = Date.now,
54
+ ): 1 | 2 {
55
+ if (cached === 2) return cached;
56
+ if (cached === 1 && now() - cachedAt < RETRY_ONE_MS) return cached;
57
+ cachedAt = now();
58
+ let out: string;
59
+ try {
60
+ out = probe(probeBase ?? viberGatewayBaseCmd());
61
+ } catch (err) {
62
+ log(`[runner] gateway version probe failed (${String(err)}): announcing capabilities.version 1 — named agents refused`);
63
+ cached = 1;
64
+ return cached;
65
+ }
66
+ cached = capabilitiesVersionFrom(out);
67
+ if (cached === 1) log(`[runner] gateway has no 'name' feature (${out.trim().slice(0, 80)}): announcing capabilities.version 1`);
68
+ return cached;
69
+ }
70
+
71
+ /** Tests only. */
72
+ export function resetGatewayFeatureProbe(): void {
73
+ cached = undefined;
74
+ cachedAt = 0;
75
+ probeBase = undefined;
76
+ }
@@ -65,6 +65,8 @@ export interface ParsedGatewayResult {
65
65
  * caller has a proven FAILURE with no cause to render — which is honest, and is not a success. */
66
66
  reason?: SpawnReason;
67
67
  detail?: string;
68
+ /** #647: the agent name the gateway echoes for a `--name` spawn. Absent from an older gateway. */
69
+ agentName?: string;
68
70
  }
69
71
 
70
72
  /**
@@ -115,8 +117,10 @@ export function parseGatewayResult(
115
117
  reason,
116
118
  detail,
117
119
  noVerdictTreeMayStand,
120
+ agentName,
118
121
  nonce: echoed,
119
122
  } = parsed as {
123
+ agentName?: unknown;
120
124
  verdict?: unknown;
121
125
  reason?: unknown;
122
126
  detail?: unknown;
@@ -141,6 +145,7 @@ export function parseGatewayResult(
141
145
  ...(typeof detail === "string" && detail.length > 0
142
146
  ? { detail: detail.slice(0, DETAIL_MAX) }
143
147
  : {}),
148
+ ...(typeof agentName === "string" ? { agentName } : {}),
144
149
  };
145
150
  }
146
151
  return null;
@@ -48,6 +48,9 @@ export interface ClaimedCommand {
48
48
  /** #627: `relaunch` brings back ONE agent from the runner's registry; absent = team. */
49
49
  kind?: "team" | "relaunch";
50
50
  relaunch_instance_id?: string | null;
51
+ /** #647: a ONE-agent spawn — its role and chosen label (both or neither, server-side). */
52
+ agent_role?: string | null;
53
+ agent_label?: string | null;
51
54
  }
52
55
 
53
56
  export interface RunnerAuthLite {
@@ -208,6 +211,11 @@ export function validateAgainstPolicy(cmd: ClaimedCommand, policy: RunnerPolicy)
208
211
  // never something that could be read as a flag by vibe-master.
209
212
  if (!ARGV_NAME_RE.test(cmd.prefix)) return `invalid prefix '${cmd.prefix}'`;
210
213
  if (cmd.team_name !== null && !ARGV_NAME_RE.test(cmd.team_name)) return `invalid team_name '${cmd.team_name}'`;
214
+ // #647: the chosen label becomes argv AND the agent id — same charset, and one agent only.
215
+ if (cmd.agent_label != null) {
216
+ if (!ARGV_NAME_RE.test(cmd.agent_label)) return `invalid agent_label '${cmd.agent_label}'`;
217
+ if (totalAgents !== 1) return `agent_label '${cmd.agent_label}' names one agent, the spec has ${totalAgents}`;
218
+ }
211
219
  const cap = PERMISSION_RANK[policy.maxPermission] ?? 1;
212
220
  for (const r of roles) {
213
221
  if (!policy.allowedRuntimes.includes(r.runtime)) return `runtime '${r.runtime}' not allowed locally`;
@@ -231,6 +239,8 @@ export type TeamSpawnRunner = (
231
239
  recap: string;
232
240
  reason?: SpawnReason;
233
241
  detail?: string;
242
+ /** #647: the agent name the launcher confirms it launched (gateway marker echo). */
243
+ agentName?: string;
234
244
  /**
235
245
  * ⚠⚠ #590: **the LAUNCHER states whether the desired state was already in place** — the reader
236
246
  * no longer infers it from a bare `reason` string.
@@ -451,6 +461,8 @@ export function buildGatewayTeamArgs(cmd: {
451
461
  env?: string;
452
462
  nonce: string;
453
463
  vibeMasterCmd?: string;
464
+ /** #647: the chosen label of a one-agent spawn. Absent = argv unchanged. */
465
+ agentName?: string;
454
466
  }): string[] {
455
467
  const args = [
456
468
  "team",
@@ -495,6 +507,7 @@ export function buildGatewayTeamArgs(cmd: {
495
507
  cmd.nonce,
496
508
  ];
497
509
  if (cmd.env) args.push("--env", cmd.env);
510
+ if (cmd.agentName !== undefined) args.push("--name", cmd.agentName);
498
511
  // #627: the gateway writes the team into the runner registry, keyed by project.
499
512
  if (cmd.projectId !== undefined) args.push("--project", String(cmd.projectId));
500
513
  // ▶ Only when set: an empty `--vibe-master-cmd` would make the gateway resolve an
@@ -555,6 +568,7 @@ export const gatewayTeamSpawnWith = (base: string[]): TeamSpawnRunner => (args,
555
568
  recap,
556
569
  ...(parsed?.reason ? { reason: parsed.reason } : {}),
557
570
  ...(parsed?.detail ? { detail: parsed.detail } : {}),
571
+ ...(parsed?.agentName !== undefined ? { agentName: parsed.agentName } : {}),
558
572
  });
559
573
  },
560
574
  );
@@ -892,6 +906,7 @@ export async function executeClaim(
892
906
  permission: cmd.template_spec.roles[0]?.permission ?? "read-only",
893
907
  ...(launchEnv.env ? { env: launchEnv.env } : {}),
894
908
  nonce,
909
+ ...(cmd.agent_label ? { agentName: cmd.agent_label } : {}),
895
910
  // ⚠ See (3) on `buildGatewayTeamArgs`: the runner no longer launches
896
911
  // `vibe-master`, so this must TRAVEL or dev silently uses the installed
897
912
  // binary. Read from the env here, where the variable is already the
@@ -904,8 +919,17 @@ export async function executeClaim(
904
919
  });
905
920
 
906
921
  inflight.mark(cmd.id); // durable "started here" BEFORE launch (C4)
907
- const { ok, recap, reason, detail, nothingToDo: launcherSaysNothingToDo } =
908
- await runTeamSpawn(args, nonce);
922
+ const launched = await runTeamSpawn(args, nonce);
923
+ // #647 FAIL CLOSED: a chosen name the launcher does not echo back (an older gateway ignores
924
+ // `--name`) is a failure, never a success under another name.
925
+ // A launcher that FAILED keeps its own reason; only a claimed success is re-checked.
926
+ const nameUnconfirmed =
927
+ !!cmd.agent_label &&
928
+ (launched.ok || launched.nothingToDo === true) &&
929
+ launched.agentName !== cmd.agent_label;
930
+ const { recap, reason, detail } = launched;
931
+ const ok = launched.ok && !nameUnconfirmed;
932
+ const launcherSaysNothingToDo = nameUnconfirmed ? false : launched.nothingToDo;
909
933
  // ⚠⚠ JP's arbitration of 2026-08-27: a replayed spawn that is CORRECTLY de-duplicated is a
910
934
  // NORMAL outcome, not a failure. ▶ Measured live (TIR D): second emission of the same command
911
935
  // -> `member: existing`, ONE record, ONE manifest, same jobName, same host anchor. **Nothing is
@@ -947,6 +971,7 @@ export async function executeClaim(
947
971
  project_id: cmd.project_id ?? null,
948
972
  workspace,
949
973
  roles: cmd.template_spec.roles,
974
+ ...(cmd.agent_label ? { agent_name: cmd.agent_label } : {}),
950
975
  });
951
976
  }
952
977
 
@@ -991,17 +1016,21 @@ export async function executeClaim(
991
1016
  : {
992
1017
  fencing_token: cmd.fencing_token,
993
1018
  status: "failed",
994
- error: reason
1019
+ error: nameUnconfirmed
1020
+ ? `the launcher on that machine did not confirm the agent name '${cmd.agent_label}' — ` +
1021
+ "update viber-gateway; it may have launched under another name, see " +
1022
+ (logRef ?? "the machine's log")
1023
+ : reason
995
1024
  ? messageForReason(
996
1025
  reason,
997
1026
  detail,
998
- rosterNames(cmd.prefix, cmd.template_spec.roles),
1027
+ cmd.agent_label ? [cmd.agent_label] : rosterNames(cmd.prefix, cmd.template_spec.roles),
999
1028
  logRef,
1000
1029
  )
1001
1030
  : logRef
1002
1031
  ? `team spawn failed — see ${logRef} on that machine`
1003
1032
  : "team spawn failed, and the local log could not be written on that machine",
1004
- ...(reason ? { reason } : {}),
1033
+ ...(reason && !nameUnconfirmed ? { reason } : {}),
1005
1034
  },
1006
1035
  fetchImpl,
1007
1036
  );
@@ -250,6 +250,8 @@ export interface SpawnForRegistry {
250
250
  project_id: number | null;
251
251
  workspace: string;
252
252
  roles: readonly (RegistryRole & { count?: number })[];
253
+ /** #647: the chosen label of a ONE-agent spawn; replaces `{prefix}-{role}`. */
254
+ agent_name?: string;
253
255
  }
254
256
 
255
257
  /**
@@ -259,8 +261,17 @@ export interface SpawnForRegistry {
259
261
  export function rosterMembers(
260
262
  prefix: string,
261
263
  roles: readonly (RegistryRole & { count?: number })[],
264
+ agentName?: string,
262
265
  ): { name: string; role: RegistryRole }[] {
263
266
  const members: { name: string; role: RegistryRole }[] = [];
267
+ // #647: a chosen name is ONE agent. On a wider roster nothing is recorded rather than
268
+ // one member mislabelled — the launch path refuses that roster anyway.
269
+ if (agentName !== undefined) {
270
+ const only = roles.length === 1 ? roles[0] : undefined;
271
+ if (!only || (only.count ?? 1) !== 1) return [];
272
+ const { count: _count, ...role } = only;
273
+ return [{ name: agentName, role }];
274
+ }
264
275
  for (const r of roles) {
265
276
  const { count, ...role } = r;
266
277
  const n = count && count > 1 ? count : 1;
@@ -280,7 +291,7 @@ export function rosterMembers(
280
291
  */
281
292
  export function recordSpawn(registry: Registry, spawn: SpawnForRegistry, now: number): void {
282
293
  pruneClosed(registry, now);
283
- for (const { name, role } of rosterMembers(spawn.prefix, spawn.roles)) {
294
+ for (const { name, role } of rosterMembers(spawn.prefix, spawn.roles, spawn.agent_name)) {
284
295
  registry.entries[registryKey(spawn.project_id, name)] = {
285
296
  name,
286
297
  command_id: spawn.command_id,
@@ -30,6 +30,7 @@ import {
30
30
  readPresenceRecord,
31
31
  } from "./presence_record.js";
32
32
  import { enumerateStartTimes } from "./process_start.js";
33
+ import { announcedCapabilitiesVersion } from "./gateway_features.js";
33
34
  import { runnerIdentity } from "./runner_exec.js";
34
35
  import * as reg from "./runner_registry.js";
35
36
  import type { SeenAgent } from "./runner_registry.js";
@@ -337,6 +338,8 @@ export interface RosterBeatDeps {
337
338
  now?: () => number;
338
339
  log?: (line: string) => void;
339
340
  headers?: () => Record<string, string>;
341
+ /** #647: the announced capabilities version (default: the cached gateway probe). */
342
+ capabilitiesVersion?: () => 1 | 2;
340
343
  }
341
344
 
342
345
  /**
@@ -398,7 +401,7 @@ async function rosterBeatOnce(auth: RosterBeatAuth, deps: RosterBeatDeps): Promi
398
401
  ...(deps.headers?.() ?? {}),
399
402
  },
400
403
  body: JSON.stringify({
401
- capabilities: { runtimes: ["claude-code", "codex", "gemma"], platform: process.platform, version: 1 },
404
+ capabilities: { runtimes: ["claude-code", "codex", "gemma"], platform: process.platform, version: (deps.capabilitiesVersion ?? announcedCapabilitiesVersion)() },
402
405
  // #627 (JP, decision A): the new instance of each pair takes the old one's
403
406
  // conversations — the server re-invites it once it is online.
404
407
  roster: replaced.length > 0 ? { ...collected.roster, replaced } : collected.roster,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "viber-channel",
3
- "version": "0.8.22",
3
+ "version": "0.8.24",
4
4
  "description": "Voice + text MCP channel between a Claude Code session and the Viber UI (https://viber.dgypx.dev). Push transcripts to Claude; send_message tool delivers text back to the UI.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -492,22 +492,23 @@ export const VOICE_CONCISE_LINE = `Your replies are returned to a voice/text con
492
492
  // outbound tool was used — but once an outbound tool is called, the bridge does
493
493
  // NOT also auto-post (anti-double-post), so the tool call must carry everything.
494
494
  const CHANNEL_TOOLS_LINE =
495
- "You have three Viber channel tools: send_message (post your reply to the current conversation — " +
495
+ "You have four Viber channel tools: send_message (post your reply to the current conversation — " +
496
496
  "`text` is the spoken reply, optional `artifact` {content, format: markdown|code|json|html} carries " +
497
497
  "technical content shown in a side panel), list_agents (list the other agents in this project), and " +
498
- "message_agent (open or reuse a direct message to another agent by its instance id). " +
498
+ "message_agent (open or reuse a direct message to another agent by its instance id), and " +
499
+ "team_templates (orchestrator only: administer the owner's team templates, on the owner's request). " +
499
500
  "To reply you may EITHER call send_message OR simply write your answer as this turn's text: if you did " +
500
501
  "NOT call send_message or message_agent, the bridge auto-delivers your turn text as the reply. If you DID " +
501
502
  "call an outbound tool, the bridge does not also auto-post — so put everything you want delivered into the " +
502
503
  "tool call(s). Keep the spoken text short and natural; put code, lists, JSON, or long technical detail in " +
503
504
  "send_message's artifact, not the spoken text.";
504
505
 
505
- function instructionsForTier(tier: AgentTier): string {
506
+ export function instructionsForTier(tier: AgentTier): string {
506
507
  if (tier === "write") {
507
508
  return [
508
509
  "You are a bridge-owned Codex agent connected to Viber, in READ-WRITE mode.",
509
510
  "You MAY read files, create/modify files within the workspace, and run commands (including state-changing ones) to carry out the user's requests.",
510
- "The sandbox is workspace-write: writes are confined to the workspace, and network access is ENABLED — so you can run git (including `git push`) and open pull requests (e.g. with `gh`).",
511
+ "The sandbox is workspace-write: writes are confined to the workspace, and network access is ENABLED — so you can run git (including `git push`). `gh` has no usable token in this sandbox (#525): ask the agent who briefed you to run gh for you.",
511
512
  "There is NO human approval step, so be deliberate: make only the changes the user asked for, and avoid destructive commands unless explicitly requested.",
512
513
  VOICE_CONCISE_LINE,
513
514
  CHANNEL_TOOLS_LINE,
@@ -608,9 +609,10 @@ export function decideServerRequestResponse(
608
609
  return { decision: "decline" };
609
610
  }
610
611
  // NB (codex review): `serverName === "viber"` is sufficient today because OUR
611
- // host advertises only the 3 channel tools (ListTools), and the "viber" name is
612
- // bound to our own loopback URL by config WE set — not attacker-controllable.
613
- // If the host ever exposed more than the 3 tools, tighten here by tool name.
612
+ // host advertises only its 4 channel tools (ListTools; #642 added team_templates,
613
+ // server-gated by the orchestrator flag), and the "viber" name is bound to our own
614
+ // loopback URL by config WE set — not attacker-controllable. If the host ever
615
+ // exposes a tool that is not a channel tool, tighten here by tool name.
614
616
 
615
617
 
616
618
  /**