viber-channel 0.8.0 → 0.8.2
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/lib/agent_tools.ts +21 -7
- package/lib/bridge_tool_host.ts +8 -4
- package/lib/capabilities.ts +11 -1
- package/lib/control_stream.ts +14 -1
- package/lib/peers.ts +46 -4
- package/package.json +1 -1
- package/viber-channel.ts +7 -3
- package/viber-codex-bridge.ts +33 -10
package/lib/agent_tools.ts
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* MCP tool-result shape. It does NOT hold state.
|
|
18
18
|
*/
|
|
19
19
|
import { postMessage, parseArtifact, type Artifact } from "./messages.js";
|
|
20
|
-
import {
|
|
20
|
+
import { listPeersAuto, openDm, type OpenDmResult } from "./peers.js";
|
|
21
21
|
import { ConversationTokenExpiredError } from "./messages.js";
|
|
22
22
|
|
|
23
23
|
/** MCP tool-result shape (text content + optional error flag). */
|
|
@@ -66,22 +66,36 @@ function errorText(s: string): AgentToolResult {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
/**
|
|
69
|
-
* list_agents — return
|
|
70
|
-
*
|
|
71
|
-
*
|
|
69
|
+
* list_agents — return the other agents so the model can pick one to message.
|
|
70
|
+
* Guards the startup window: an empty instance token means "channel not ready"
|
|
71
|
+
* rather than hitting the peers endpoint with no credential.
|
|
72
|
+
*
|
|
73
|
+
* #307: the listing runs at the widest allowed scope (listPeersAuto) — an
|
|
74
|
+
* ORCHESTRATOR instance sees the agents of ALL its owner's projects (each
|
|
75
|
+
* entry then carries a `project` field); a regular instance transparently
|
|
76
|
+
* falls back to its own project (unchanged behaviour, no `project` field).
|
|
72
77
|
*/
|
|
73
78
|
export async function listAgents(ctx: AgentToolsContext): Promise<AgentToolResult> {
|
|
74
79
|
if (!ctx.instanceToken()) {
|
|
75
80
|
return errorText("Channel not ready: no instance identity yet.");
|
|
76
81
|
}
|
|
77
82
|
try {
|
|
78
|
-
const peers = await
|
|
83
|
+
const { peers, scope } = await listPeersAuto(ctx.baseUrl(), ctx.instanceToken());
|
|
79
84
|
// Project only what the model needs to pick a peer (drop last_seen /
|
|
80
85
|
// active_conversation_id — available over the wire if a future tool needs them).
|
|
81
|
-
const summary = peers.map((p) => ({
|
|
86
|
+
const summary = peers.map((p) => ({
|
|
87
|
+
id: p.id,
|
|
88
|
+
label: p.label,
|
|
89
|
+
kind: p.kind,
|
|
90
|
+
online: p.online,
|
|
91
|
+
// Present only in the user-scoped (orchestrator) listing.
|
|
92
|
+
...(p.project_name !== undefined ? { project: p.project_name } : {}),
|
|
93
|
+
}));
|
|
82
94
|
const body =
|
|
83
95
|
summary.length === 0
|
|
84
|
-
?
|
|
96
|
+
? scope === "user"
|
|
97
|
+
? "No other agents are currently registered in any of your projects."
|
|
98
|
+
: "No other agents are currently registered in this project."
|
|
85
99
|
: JSON.stringify(summary, null, 2);
|
|
86
100
|
return text(body);
|
|
87
101
|
} catch (err) {
|
package/lib/bridge_tool_host.ts
CHANGED
|
@@ -68,16 +68,20 @@ const TOOL_DEFS = [
|
|
|
68
68
|
{
|
|
69
69
|
name: "list_agents",
|
|
70
70
|
description:
|
|
71
|
-
"List the OTHER agents (instances)
|
|
72
|
-
"label, runtime kind, and whether it is online. Use it to find an id before message_agent."
|
|
71
|
+
"List the OTHER agents (instances) you can DM. Returns each agent's id, " +
|
|
72
|
+
"label, runtime kind, and whether it is online. Use it to find an id before message_agent. " +
|
|
73
|
+
"Normally project-scoped; an ORCHESTRATOR instance (#307) sees all the owner's projects, " +
|
|
74
|
+
"each entry tagged with a `project` field.",
|
|
73
75
|
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
74
76
|
},
|
|
75
77
|
{
|
|
76
78
|
name: "message_agent",
|
|
77
79
|
description:
|
|
78
|
-
"Send a direct message to another agent
|
|
80
|
+
"Send a direct message to another agent. Pass the target agent's instance id " +
|
|
79
81
|
"(from list_agents) and the text. Opens or reuses a private 1:1 DM and posts your message; " +
|
|
80
|
-
"the agent's reply arrives back on this channel.
|
|
82
|
+
"the agent's reply arrives back on this channel. Targets are normally same-project; an " +
|
|
83
|
+
"ORCHESTRATOR instance (#307) can also message agents of the owner's other projects. " +
|
|
84
|
+
"Fails if the target agent is offline.",
|
|
81
85
|
inputSchema: {
|
|
82
86
|
type: "object",
|
|
83
87
|
properties: {
|
package/lib/capabilities.ts
CHANGED
|
@@ -26,15 +26,25 @@ export function capabilitiesText(): string {
|
|
|
26
26
|
"",
|
|
27
27
|
"Spawn one agent (CLI, non-interactive):",
|
|
28
28
|
" vibe-master spawn --permission <read-only|read-write> \\",
|
|
29
|
-
" [--runtime codex|claude|gemma] [--name <id>]",
|
|
29
|
+
" [--runtime codex|claude|gemma] [--name <id>] [--scope <name>]",
|
|
30
30
|
" → only --permission is required; --runtime and --name are optional",
|
|
31
31
|
" (--name auto-generated if omitted). Prints the agent id; it registers",
|
|
32
32
|
" in Viber and comes online.",
|
|
33
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
|
+
"",
|
|
34
39
|
"Interactive: `vibe-master tui` (menu: + Add agent → runtime → permission → name).",
|
|
35
40
|
"Inspect / stop: `vibe-master list` · `vibe-master kill <id>`.",
|
|
36
41
|
"",
|
|
37
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.",
|
|
38
48
|
"Requires vibe-master installed on the machine (see the Install page on the web).",
|
|
39
49
|
"Run `vibe-master --help` for the full command reference.",
|
|
40
50
|
].join("\n");
|
package/lib/control_stream.ts
CHANGED
|
@@ -297,6 +297,12 @@ export function runPersistentControlStream(
|
|
|
297
297
|
};
|
|
298
298
|
|
|
299
299
|
const done = (async () => {
|
|
300
|
+
// #389 step-02: announce ONCE that the instance control stream actually
|
|
301
|
+
// connected server-side (the SSE `connected` open-marker arrived) — proof
|
|
302
|
+
// the agent is ONLINE/reachable, not merely registered. vibe-master's spawn
|
|
303
|
+
// readiness gate polls the bridge log for this line, so a launch that
|
|
304
|
+
// registers then fails to connect (CF/401/network) is NOT reported as ready.
|
|
305
|
+
let announcedOnline = false;
|
|
300
306
|
try {
|
|
301
307
|
while (true) {
|
|
302
308
|
if (signal?.aborted) {
|
|
@@ -305,6 +311,13 @@ export function runPersistentControlStream(
|
|
|
305
311
|
}
|
|
306
312
|
try {
|
|
307
313
|
for await (const ev of subscribeControlStream(opts)) {
|
|
314
|
+
if (ev.event === "connected") {
|
|
315
|
+
if (!announcedOnline) {
|
|
316
|
+
announcedOnline = true;
|
|
317
|
+
log("[control-stream] online — instance control stream connected\n");
|
|
318
|
+
}
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
308
321
|
if (ev.event === "stop") {
|
|
309
322
|
settleFirstReject(new ControlStreamStopped());
|
|
310
323
|
handlers.onStop?.("stopped");
|
|
@@ -314,7 +327,7 @@ export function runPersistentControlStream(
|
|
|
314
327
|
handlers.onBeatNow?.();
|
|
315
328
|
continue;
|
|
316
329
|
}
|
|
317
|
-
if (ev.event !== "join") continue; //
|
|
330
|
+
if (ev.event !== "join") continue; // ping / unknown (connected handled above)
|
|
318
331
|
const minted = parseJoinPayload(ev.data, log);
|
|
319
332
|
if (!minted) continue;
|
|
320
333
|
handlers.onJoin?.(minted);
|
package/lib/peers.ts
CHANGED
|
@@ -11,7 +11,10 @@
|
|
|
11
11
|
|
|
12
12
|
import { cfAccessHeaders } from "./cfAccess.js";
|
|
13
13
|
|
|
14
|
-
/** A peer agent as returned by GET /api/instances/peers.
|
|
14
|
+
/** A peer agent as returned by GET /api/instances/peers.
|
|
15
|
+
* project_id/project_name are present only in the user-scoped listing (#307):
|
|
16
|
+
* an orchestrator sees peers across all its owner's projects, each tagged
|
|
17
|
+
* with its project. */
|
|
15
18
|
export interface PeerView {
|
|
16
19
|
id: string;
|
|
17
20
|
label: string | null;
|
|
@@ -19,17 +22,35 @@ export interface PeerView {
|
|
|
19
22
|
online: boolean;
|
|
20
23
|
last_seen: number | null;
|
|
21
24
|
active_conversation_id: string | null;
|
|
25
|
+
project_id?: number;
|
|
26
|
+
project_name?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Error carrying the HTTP status so callers can branch on 403 (#307). */
|
|
30
|
+
export class PeersHttpError extends Error {
|
|
31
|
+
constructor(
|
|
32
|
+
message: string,
|
|
33
|
+
public readonly status: number,
|
|
34
|
+
) {
|
|
35
|
+
super(message);
|
|
36
|
+
this.name = "PeersHttpError";
|
|
37
|
+
}
|
|
22
38
|
}
|
|
23
39
|
|
|
24
40
|
/**
|
|
25
41
|
* List the OTHER active instances of this agent's project (self excluded).
|
|
26
|
-
*
|
|
42
|
+
* With scope "user" (#307), an ORCHESTRATOR instance lists the peers of ALL
|
|
43
|
+
* its owner's projects (the server returns 403 for a non-orchestrator).
|
|
44
|
+
* Throws PeersHttpError on a non-2xx response so the caller can surface a
|
|
45
|
+
* clear tool error — or fall back from user scope on a 403.
|
|
27
46
|
*/
|
|
28
47
|
export async function listPeers(
|
|
29
48
|
baseUrl: string,
|
|
30
49
|
instanceToken: string,
|
|
50
|
+
scope?: "user",
|
|
31
51
|
): Promise<PeerView[]> {
|
|
32
|
-
const
|
|
52
|
+
const qs = scope === "user" ? "?scope=user" : "";
|
|
53
|
+
const resp = await fetch(`${baseUrl}/api/instances/peers${qs}`, {
|
|
33
54
|
method: "GET",
|
|
34
55
|
headers: { Authorization: `Bearer ${instanceToken}`, ...cfAccessHeaders() },
|
|
35
56
|
});
|
|
@@ -41,12 +62,33 @@ export async function listPeers(
|
|
|
41
62
|
} catch {
|
|
42
63
|
/* non-JSON body */
|
|
43
64
|
}
|
|
44
|
-
throw new
|
|
65
|
+
throw new PeersHttpError(`listPeers failed: ${detail}`, resp.status);
|
|
45
66
|
}
|
|
46
67
|
const body = (await resp.json()) as { instances?: PeerView[] };
|
|
47
68
|
return Array.isArray(body.instances) ? body.instances : [];
|
|
48
69
|
}
|
|
49
70
|
|
|
71
|
+
/**
|
|
72
|
+
* List peers at the WIDEST scope this instance is allowed (#307): try the
|
|
73
|
+
* user-scoped listing first; a 403 (non-orchestrator) transparently falls
|
|
74
|
+
* back to the project-scoped listing. Any other failure propagates. Single
|
|
75
|
+
* tool surface, zero client-side configuration: orchestrators see all their
|
|
76
|
+
* owner's projects, workers keep seeing their own project only.
|
|
77
|
+
*/
|
|
78
|
+
export async function listPeersAuto(
|
|
79
|
+
baseUrl: string,
|
|
80
|
+
instanceToken: string,
|
|
81
|
+
): Promise<{ peers: PeerView[]; scope: "user" | "project" }> {
|
|
82
|
+
try {
|
|
83
|
+
return { peers: await listPeers(baseUrl, instanceToken, "user"), scope: "user" };
|
|
84
|
+
} catch (err) {
|
|
85
|
+
if (err instanceof PeersHttpError && err.status === 403) {
|
|
86
|
+
return { peers: await listPeers(baseUrl, instanceToken), scope: "project" };
|
|
87
|
+
}
|
|
88
|
+
throw err;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
50
92
|
/** Result of opening (or reusing) a DM with a peer — carries the caller's own
|
|
51
93
|
* membership conversation_token so it can post into the DM. */
|
|
52
94
|
export interface OpenDmResult {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "viber-channel",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2",
|
|
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": {
|
package/viber-channel.ts
CHANGED
|
@@ -428,10 +428,12 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
428
428
|
{
|
|
429
429
|
name: "list_agents",
|
|
430
430
|
description:
|
|
431
|
-
"List the OTHER agents (instances)
|
|
431
|
+
"List the OTHER agents (instances) you can talk to directly (#288). " +
|
|
432
432
|
"Returns each agent's id, label, runtime kind, and whether it is currently online. " +
|
|
433
433
|
"Use this to find the id of an agent (e.g. 'Codex Review') before calling message_agent. " +
|
|
434
|
-
"
|
|
434
|
+
"Normally scoped to this project; if the owner designated this instance an ORCHESTRATOR " +
|
|
435
|
+
"(#307), the list covers ALL the owner's projects and each entry carries a `project` field. " +
|
|
436
|
+
"You are never in the list.",
|
|
435
437
|
inputSchema: {
|
|
436
438
|
type: "object" as const,
|
|
437
439
|
properties: {},
|
|
@@ -442,10 +444,12 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
442
444
|
{
|
|
443
445
|
name: "message_agent",
|
|
444
446
|
description:
|
|
445
|
-
"Send a direct message to another agent
|
|
447
|
+
"Send a direct message to another agent (#288). " +
|
|
446
448
|
"Pass the target agent's instance id (from list_agents, or the from_instance_id of a DM you received) " +
|
|
447
449
|
"and the message text. Opens or reuses a private 1:1 DM with that agent and posts your message; " +
|
|
448
450
|
"the agent's reply arrives back on this channel tagged source=agent-dm. " +
|
|
451
|
+
"Targets are normally same-project; an ORCHESTRATOR instance (#307) can also message agents " +
|
|
452
|
+
"of the owner's other projects (ids from its cross-project list_agents). " +
|
|
449
453
|
"Fails if the target agent is offline.",
|
|
450
454
|
inputSchema: {
|
|
451
455
|
type: "object" as const,
|
package/viber-codex-bridge.ts
CHANGED
|
@@ -356,7 +356,7 @@ function instructionsForTier(tier: AgentTier): string {
|
|
|
356
356
|
return [
|
|
357
357
|
"You are a bridge-owned Codex agent connected to Viber, in READ-WRITE mode.",
|
|
358
358
|
"You MAY read files, create/modify files within the workspace, and run commands (including state-changing ones) to carry out the user's requests.",
|
|
359
|
-
"The sandbox is workspace-write: writes are confined to the workspace and network access is
|
|
359
|
+
"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`).",
|
|
360
360
|
"There is NO human approval step, so be deliberate: make only the changes the user asked for, and avoid destructive commands unless explicitly requested.",
|
|
361
361
|
VOICE_CONCISE_LINE,
|
|
362
362
|
CHANNEL_TOOLS_LINE,
|
|
@@ -385,17 +385,43 @@ function instructionsForTier(tier: AgentTier): string {
|
|
|
385
385
|
// Approval policy per tier. `read`/`write` use "never" so Codex runs its commands
|
|
386
386
|
// WITHOUT a human approver (there is none on the bridge) — the SANDBOX is the real
|
|
387
387
|
// guardrail (read-only blocks writes/network; workspace-write confines writes to
|
|
388
|
-
// the workspace
|
|
389
|
-
function approvalForTier(tier: AgentTier): "never" | "on-request" {
|
|
388
|
+
// the workspace). `chat` keeps "on-request" (no exec anyway).
|
|
389
|
+
export function approvalForTier(tier: AgentTier): "never" | "on-request" {
|
|
390
390
|
return tier === "chat" ? "on-request" : "never";
|
|
391
391
|
}
|
|
392
392
|
|
|
393
393
|
// Codex OS sandbox per tier. `write` widens to workspace-write (edits confined to
|
|
394
|
-
// the workspace
|
|
395
|
-
function sandboxForTier(tier: AgentTier): "read-only" | "workspace-write" {
|
|
394
|
+
// the workspace); chat/read stay read-only.
|
|
395
|
+
export function sandboxForTier(tier: AgentTier): "read-only" | "workspace-write" {
|
|
396
396
|
return tier === "write" ? "workspace-write" : "read-only";
|
|
397
397
|
}
|
|
398
398
|
|
|
399
|
+
// Network access per tier (#389 step-03). ONLY the trusted `write` tier gets
|
|
400
|
+
// network — so a codex coder can `git push` and open its PR (git-over-https +
|
|
401
|
+
// `gh`). read/chat stay offline. SECURITY: codex's workspace-write sandbox opens
|
|
402
|
+
// ALL network, not git-only (the app-server sandbox has no per-domain allowlist),
|
|
403
|
+
// so this is a TRUST decision scoped to the write tier, which is already
|
|
404
|
+
// operator-authorized-at-launch for unattended command execution. If codex ever
|
|
405
|
+
// exposes an outbound-domain allowlist, tighten here (follow-up issue).
|
|
406
|
+
export function networkForTier(tier: AgentTier): boolean {
|
|
407
|
+
return tier === "write";
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// The EXACT `sandboxPolicy` object sent on every `turn/start` (#389 step-03).
|
|
411
|
+
// Per the codex app-server protocol (generate-ts), `networkAccess` lives ONLY on
|
|
412
|
+
// this per-turn policy object (workspaceWrite/readOnly variants) — NOT on
|
|
413
|
+
// thread/start|resume, which take a bare `sandbox` MODE string. So the turn-level
|
|
414
|
+
// policy is the single lever for network. Exported to unit-test the payload/tier.
|
|
415
|
+
export function buildTurnSandboxPolicy(
|
|
416
|
+
tier: AgentTier,
|
|
417
|
+
):
|
|
418
|
+
| { type: "workspaceWrite"; networkAccess: boolean }
|
|
419
|
+
| { type: "readOnly"; networkAccess: boolean } {
|
|
420
|
+
return sandboxForTier(tier) === "workspace-write"
|
|
421
|
+
? { type: "workspaceWrite", networkAccess: networkForTier(tier) }
|
|
422
|
+
: { type: "readOnly", networkAccess: false };
|
|
423
|
+
}
|
|
424
|
+
|
|
399
425
|
const AGENT_TIER: AgentTier = resolveAgentTier();
|
|
400
426
|
// Identity line (#309 step-17) is appended AFTER the tier instructions so the
|
|
401
427
|
// permission rules stay first + strongest; the role is framed as descriptive and
|
|
@@ -635,10 +661,7 @@ class CodexAppServer {
|
|
|
635
661
|
cwd: process.cwd(),
|
|
636
662
|
runtimeWorkspaceRoots: [process.cwd()],
|
|
637
663
|
approvalPolicy: AGENT_APPROVAL_POLICY,
|
|
638
|
-
sandboxPolicy:
|
|
639
|
-
AGENT_SANDBOX === "workspace-write"
|
|
640
|
-
? { type: "workspaceWrite", networkAccess: false }
|
|
641
|
-
: { type: "readOnly", networkAccess: false },
|
|
664
|
+
sandboxPolicy: buildTurnSandboxPolicy(AGENT_TIER),
|
|
642
665
|
developerInstructions: AGENT_INSTRUCTIONS,
|
|
643
666
|
responsesapiClientMetadata: { source: "viber-codex-bridge" },
|
|
644
667
|
});
|
|
@@ -1115,7 +1138,7 @@ function setupCodexRuntime(deps: {
|
|
|
1115
1138
|
|
|
1116
1139
|
function logCodexSafety(): void {
|
|
1117
1140
|
process.stderr.write(
|
|
1118
|
-
`${LOG_PREFIX} codex safety: tier=${AGENT_TIER}, sandbox=${AGENT_SANDBOX}, approvalPolicy=${AGENT_APPROVAL_POLICY}, network
|
|
1141
|
+
`${LOG_PREFIX} codex safety: tier=${AGENT_TIER}, sandbox=${AGENT_SANDBOX}, approvalPolicy=${AGENT_APPROVAL_POLICY}, network=${networkForTier(AGENT_TIER)}, ` +
|
|
1119
1142
|
`instructions=${
|
|
1120
1143
|
AGENT_TIER === "write"
|
|
1121
1144
|
? "read+write/run commands (workspace)"
|