@spinabot/brigade 1.13.0 → 1.15.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.
- package/convex/logs.d.ts +9 -9
- package/convex/memory.d.ts +21 -21
- package/convex/schema.d.ts +11 -11
- package/convex/skills.d.ts +3 -3
- package/dist/buildstamp.json +1 -1
- package/dist/cli/commands/config-cmd.d.ts +12 -19
- package/dist/cli/commands/config-cmd.d.ts.map +1 -1
- package/dist/cli/commands/config-cmd.js +14 -197
- package/dist/cli/commands/config-cmd.js.map +1 -1
- package/dist/cli/commands/connect-transcript.d.ts +39 -0
- package/dist/cli/commands/connect-transcript.d.ts.map +1 -0
- package/dist/cli/commands/connect-transcript.js +60 -0
- package/dist/cli/commands/connect-transcript.js.map +1 -0
- package/dist/cli/commands/connect.d.ts.map +1 -1
- package/dist/cli/commands/connect.js +297 -169
- package/dist/cli/commands/connect.js.map +1 -1
- package/dist/core/agents-crud-ops.d.ts +15 -0
- package/dist/core/agents-crud-ops.d.ts.map +1 -0
- package/dist/core/agents-crud-ops.js +27 -0
- package/dist/core/agents-crud-ops.js.map +1 -0
- package/dist/core/agents-ops.d.ts +43 -0
- package/dist/core/agents-ops.d.ts.map +1 -0
- package/dist/core/agents-ops.js +117 -0
- package/dist/core/agents-ops.js.map +1 -0
- package/dist/core/channels-ops.d.ts +30 -0
- package/dist/core/channels-ops.d.ts.map +1 -0
- package/dist/core/channels-ops.js +52 -0
- package/dist/core/channels-ops.js.map +1 -0
- package/dist/core/config-ops.d.ts +77 -0
- package/dist/core/config-ops.d.ts.map +1 -0
- package/dist/core/config-ops.js +241 -0
- package/dist/core/config-ops.js.map +1 -0
- package/dist/core/exec-ops.d.ts +48 -0
- package/dist/core/exec-ops.d.ts.map +1 -0
- package/dist/core/exec-ops.js +101 -0
- package/dist/core/exec-ops.js.map +1 -0
- package/dist/core/integrations-ops.d.ts +25 -0
- package/dist/core/integrations-ops.d.ts.map +1 -0
- package/dist/core/integrations-ops.js +40 -0
- package/dist/core/integrations-ops.js.map +1 -0
- package/dist/core/memory-ops.d.ts +20 -0
- package/dist/core/memory-ops.d.ts.map +1 -0
- package/dist/core/memory-ops.js +40 -0
- package/dist/core/memory-ops.js.map +1 -0
- package/dist/core/pairing-ops.d.ts +33 -0
- package/dist/core/pairing-ops.d.ts.map +1 -0
- package/dist/core/pairing-ops.js +78 -0
- package/dist/core/pairing-ops.js.map +1 -0
- package/dist/core/provider-ops.d.ts +17 -0
- package/dist/core/provider-ops.d.ts.map +1 -0
- package/dist/core/provider-ops.js +29 -0
- package/dist/core/provider-ops.js.map +1 -0
- package/dist/core/server.d.ts.map +1 -1
- package/dist/core/server.js +261 -14
- package/dist/core/server.js.map +1 -1
- package/dist/core/sessions-ops.d.ts +25 -0
- package/dist/core/sessions-ops.d.ts.map +1 -0
- package/dist/core/sessions-ops.js +77 -0
- package/dist/core/sessions-ops.js.map +1 -0
- package/dist/core/skills-ops.d.ts +14 -0
- package/dist/core/skills-ops.d.ts.map +1 -0
- package/dist/core/skills-ops.js +28 -0
- package/dist/core/skills-ops.js.map +1 -0
- package/dist/protocol/errors.d.ts +14 -0
- package/dist/protocol/errors.d.ts.map +1 -1
- package/dist/protocol/errors.js +14 -0
- package/dist/protocol/errors.js.map +1 -1
- package/dist/protocol/handshake.d.ts +10 -0
- package/dist/protocol/handshake.d.ts.map +1 -1
- package/dist/protocol/methods.d.ts +478 -0
- package/dist/protocol/methods.d.ts.map +1 -1
- package/dist/protocol/stream-seq.d.ts +30 -0
- package/dist/protocol/stream-seq.d.ts.map +1 -0
- package/dist/protocol/stream-seq.js +38 -0
- package/dist/protocol/stream-seq.js.map +1 -0
- package/dist/protocol.d.ts +265 -6
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +95 -5
- package/dist/protocol.js.map +1 -1
- package/dist/tui/client.d.ts +67 -2
- package/dist/tui/client.d.ts.map +1 -1
- package/dist/tui/client.js +106 -2
- package/dist/tui/client.js.map +1 -1
- package/package.json +1 -1
package/dist/core/server.js
CHANGED
|
@@ -28,7 +28,9 @@ import { createServer as createTcpServer } from "node:net";
|
|
|
28
28
|
import { pathToFileURL } from "node:url";
|
|
29
29
|
import { ModelRegistry, } from "@earendil-works/pi-coding-agent";
|
|
30
30
|
import { WebSocketServer } from "ws";
|
|
31
|
-
import { DEFAULT_PORT, isFrame, modelToSummary, TICK_INTERVAL_MS, } from "../protocol.js";
|
|
31
|
+
import { DEFAULT_PORT, EVENT_NAMES, isFrame, modelToSummary, REQUEST_METHODS, TICK_INTERVAL_MS, } from "../protocol.js";
|
|
32
|
+
import { PROTOCOL_VERSION } from "../protocol/handshake.js";
|
|
33
|
+
import { nextSeq } from "../protocol/stream-seq.js";
|
|
32
34
|
// Per-turn execution path (the single canonical runtime). The gateway no
|
|
33
35
|
// longer holds a long-lived Pi session: every inbound `prompt` builds a
|
|
34
36
|
// fresh session via `runResilientTurn`, resumes the JSONL transcript by
|
|
@@ -135,6 +137,17 @@ import { mutateConfigAtomic } from "../config/io.js";
|
|
|
135
137
|
import { acquireGatewayLock } from "./gateway-lock.js";
|
|
136
138
|
import { clearHeartbeatFile, clearPidFile, writeHeartbeatFile, writePidFile } from "./gateway-probe.js";
|
|
137
139
|
import { extractToken, matchesAnyToken, resolveGatewayAuth } from "./gateway-auth.js";
|
|
140
|
+
import { handleConfigGet, handleConfigList, handleConfigSchema, handleConfigSet, handleConfigUnset, handleConfigValidate, } from "./config-ops.js";
|
|
141
|
+
import { handleExecAllow, handleExecAllowPattern, handleExecDenyTest, handleExecList, handleExecRemove, } from "./exec-ops.js";
|
|
142
|
+
import { handleAgentsBind, handleAgentsBindings, handleAgentsUnbind } from "./agents-ops.js";
|
|
143
|
+
import { handlePairingApprove, handlePairingList, handlePairingRevoke } from "./pairing-ops.js";
|
|
144
|
+
import { handleSessionsCleanup } from "./sessions-ops.js";
|
|
145
|
+
import { handleMemoryManage, handleMemoryWrite } from "./memory-ops.js";
|
|
146
|
+
import { handleAgentsAdd, handleAgentsDelete, handleAgentsSetIdentity } from "./agents-crud-ops.js";
|
|
147
|
+
import { handleSkillsCreate, handleSkillsDelete, handleSkillsWriteFile } from "./skills-ops.js";
|
|
148
|
+
import { handleChannelsAllowAdd, handleChannelsAllowList, handleChannelsAllowRemove, handleChannelsConnect, handleChannelsDisconnect, } from "./channels-ops.js";
|
|
149
|
+
import { handleProviderRemove } from "./provider-ops.js";
|
|
150
|
+
import { handleComposio, handleOauth } from "./integrations-ops.js";
|
|
138
151
|
// Persist a model selection to brigade.json's new wizard-shape (the lifted
|
|
139
152
|
// code expected the older flat `defaultProvider`/`defaultModelId` fields).
|
|
140
153
|
// Writes through the same `agents.defaults.{provider, model.primary}` path
|
|
@@ -1491,6 +1504,32 @@ async function continueBoot(args) {
|
|
|
1491
1504
|
const clientConnIds = new WeakMap();
|
|
1492
1505
|
const clientAgentSubs = new Map();
|
|
1493
1506
|
const clientSessionSubs = new Map();
|
|
1507
|
+
/**
|
|
1508
|
+
* Per-session monotonic sequence for the ordered, recoverable stream.
|
|
1509
|
+
* `broadcast` stamps the next value onto every ORDERED frame tagged with a
|
|
1510
|
+
* sessionId (= sessionKey): top-level `pi`, `approval-request`, and
|
|
1511
|
+
* `system-event` (they SHARE one counter per session, so a client detects a
|
|
1512
|
+
* gap in any of them and `resume`s). A client tracks the last seq it saw per
|
|
1513
|
+
* session; a jump means it missed a frame. `resume` returns the current value
|
|
1514
|
+
* as `headSeq`. One int per session — negligible; never pruned so a session's
|
|
1515
|
+
* seq stays monotonic across turns. A gateway restart resets these to 0 — the
|
|
1516
|
+
* client detects that via the `epoch` change on its next `HelloOk` and
|
|
1517
|
+
* invalidates its cursor.
|
|
1518
|
+
*/
|
|
1519
|
+
const seqCounters = new Map();
|
|
1520
|
+
/**
|
|
1521
|
+
* Bounded per-session tail of recent `system-event` notices (cron announces /
|
|
1522
|
+
* channel-health), so a client that was disconnected when one fired can still
|
|
1523
|
+
* recover it via `resume`. Oldest-first, capped at RECENT_SYSTEM_EVENTS_MAX.
|
|
1524
|
+
*/
|
|
1525
|
+
const recentSystemEvents = new Map();
|
|
1526
|
+
const RECENT_SYSTEM_EVENTS_MAX = 30;
|
|
1527
|
+
/**
|
|
1528
|
+
* Process boot id (session generation / "epoch"). Constant for this gateway
|
|
1529
|
+
* process; a restart yields a new value. Advertised in `HelloOk` so a client
|
|
1530
|
+
* can tell a restart (→ invalidate seq cursors) from a normal reconnect.
|
|
1531
|
+
*/
|
|
1532
|
+
const gatewayEpoch = crypto.randomUUID();
|
|
1494
1533
|
const subscribeAgent = (connId, agentIdValue) => {
|
|
1495
1534
|
let set = clientAgentSubs.get(connId);
|
|
1496
1535
|
if (!set) {
|
|
@@ -1522,13 +1561,42 @@ async function continueBoot(args) {
|
|
|
1522
1561
|
const connWantsFrame = (connId, frameAgentId, frameSessionId) => shouldDeliverFrame(clientAgentSubs.get(connId), clientSessionSubs.get(connId), { agentId: frameAgentId, sessionId: frameSessionId });
|
|
1523
1562
|
/** Send one event to all connected clients (or a filtered subset). */
|
|
1524
1563
|
const broadcast = (event, payload) => {
|
|
1525
|
-
const frame = { type: "event", event, payload };
|
|
1526
|
-
const json = JSON.stringify(frame);
|
|
1527
1564
|
// Untagged payloads broadcast to everyone (state, error, basic log).
|
|
1528
1565
|
// Tagged payloads (pi, log with agent/session, approval-request,
|
|
1529
1566
|
// system-event with target) consult the subscription filter so the
|
|
1530
1567
|
// approval prompt for agent A doesn't pop on operator B's TUI.
|
|
1531
1568
|
const { agentId: frameAgentId, sessionId: frameSessionId } = extractFrameTags(payload);
|
|
1569
|
+
// Stamp a per-session monotonic seq on the ordered transcript stream
|
|
1570
|
+
// (`pi`). This is the gap detector: a client that sees seq jump knows it
|
|
1571
|
+
// missed a frame and issues `resume`. Only `pi` frames carry seq — they
|
|
1572
|
+
// are the transcript; state/error/log are unordered side-channels a
|
|
1573
|
+
// client never gap-checks. Same `json` goes to every subscriber, so the
|
|
1574
|
+
// seq is shared across all clients watching this session.
|
|
1575
|
+
//
|
|
1576
|
+
// The ordered, recoverable stream = top-level `pi` + `approval-request` +
|
|
1577
|
+
// `system-event`, sharing one per-session counter so a client detects a
|
|
1578
|
+
// gap in ANY of them and `resume`s. EXCLUDED (no seq):
|
|
1579
|
+
// - sub-agent `pi` frames (subagentDepth>0): they carry the child's own
|
|
1580
|
+
// session id (a UUID) and live in a separate child transcript the
|
|
1581
|
+
// parent's `resume` can't backfill — ephemeral nested decoration.
|
|
1582
|
+
// - `state` (self-healing cumulative snapshot), `error`, `log` (on disk).
|
|
1583
|
+
const subDepth = event === "pi" ? Number(payload.subagentDepth) || 0 : 0;
|
|
1584
|
+
const isOrderedFrame = (event === "pi" && subDepth === 0) ||
|
|
1585
|
+
event === "approval-request" ||
|
|
1586
|
+
event === "system-event";
|
|
1587
|
+
const seq = isOrderedFrame ? nextSeq(seqCounters, frameSessionId) : undefined;
|
|
1588
|
+
// Retain a bounded per-session tail of system-events for `resume` recovery.
|
|
1589
|
+
if (event === "system-event" && frameSessionId) {
|
|
1590
|
+
const ring = recentSystemEvents.get(frameSessionId) ?? [];
|
|
1591
|
+
ring.push(payload);
|
|
1592
|
+
while (ring.length > RECENT_SYSTEM_EVENTS_MAX)
|
|
1593
|
+
ring.shift();
|
|
1594
|
+
recentSystemEvents.set(frameSessionId, ring);
|
|
1595
|
+
}
|
|
1596
|
+
const frame = seq !== undefined
|
|
1597
|
+
? { type: "event", event, payload, seq }
|
|
1598
|
+
: { type: "event", event, payload };
|
|
1599
|
+
const json = JSON.stringify(frame);
|
|
1532
1600
|
for (const ws of clients) {
|
|
1533
1601
|
if (ws.readyState !== ws.OPEN)
|
|
1534
1602
|
continue;
|
|
@@ -2902,6 +2970,57 @@ async function continueBoot(args) {
|
|
|
2902
2970
|
case "get-state": {
|
|
2903
2971
|
return buildSnapshot();
|
|
2904
2972
|
}
|
|
2973
|
+
case "resume": {
|
|
2974
|
+
// Reliable-streaming recovery. Return the session's committed
|
|
2975
|
+
// transcript (the single source of truth — works in BOTH
|
|
2976
|
+
// filesystem + Convex mode via `readSessionTranscriptMessages`)
|
|
2977
|
+
// plus the session's current head seq and the header snapshot.
|
|
2978
|
+
// The client re-materialises from this on (re)connect or a
|
|
2979
|
+
// detected seq gap, then keeps applying live `pi` frames keyed by
|
|
2980
|
+
// identity — so a dropped/reordered frame self-heals. Any
|
|
2981
|
+
// in-flight (not-yet-committed) message is NOT in the transcript
|
|
2982
|
+
// yet; the live `message_update` stream paints it after resume and
|
|
2983
|
+
// the identity-keyed renderer dedupes it on commit. Read-only;
|
|
2984
|
+
// default-pass session guard (the local WS client is the operator).
|
|
2985
|
+
const guardErr = defaultPassSessionGuard(rawParams, "list");
|
|
2986
|
+
if (guardErr)
|
|
2987
|
+
throw guardErr;
|
|
2988
|
+
const p = (params ?? {});
|
|
2989
|
+
const targetAgentId = p.agentId?.trim() || agentId;
|
|
2990
|
+
const targetSessionKey = p.sessionKey?.trim() || defaultSessionKey(targetAgentId);
|
|
2991
|
+
const messages = await readSessionTranscriptMessages({ sessionKey: targetSessionKey });
|
|
2992
|
+
const headSeq = seqCounters.get(targetSessionKey) ?? 0;
|
|
2993
|
+
// Recovery for the two non-transcript event types so a (re)connecting
|
|
2994
|
+
// client loses NOTHING: tool-approval prompts still pending on this
|
|
2995
|
+
// session (else the turn hangs to auto-deny), and the recent
|
|
2996
|
+
// system-event tail. Pending approvals are filtered to this session.
|
|
2997
|
+
const pendingApprovals = approvalBridge
|
|
2998
|
+
.listPending()
|
|
2999
|
+
.filter((a) => a.sessionId === targetSessionKey)
|
|
3000
|
+
.map((a) => ({
|
|
3001
|
+
id: a.id,
|
|
3002
|
+
command: a.command,
|
|
3003
|
+
toolName: a.toolName,
|
|
3004
|
+
timeoutMs: a.timeoutMs,
|
|
3005
|
+
decisions: a.decisions,
|
|
3006
|
+
...(a.cwd !== undefined ? { cwd: a.cwd } : {}),
|
|
3007
|
+
...(a.subagentLabel !== undefined ? { subagentLabel: a.subagentLabel } : {}),
|
|
3008
|
+
...(a.subagentDepth !== undefined ? { subagentDepth: a.subagentDepth } : {}),
|
|
3009
|
+
...(a.parentRunId !== undefined ? { parentRunId: a.parentRunId } : {}),
|
|
3010
|
+
...(a.agentId !== undefined ? { agentId: a.agentId } : {}),
|
|
3011
|
+
...(a.sessionId !== undefined ? { sessionId: a.sessionId } : {}),
|
|
3012
|
+
}));
|
|
3013
|
+
return {
|
|
3014
|
+
sessionKey: targetSessionKey,
|
|
3015
|
+
agentId: targetAgentId,
|
|
3016
|
+
messages: messages,
|
|
3017
|
+
headSeq,
|
|
3018
|
+
pendingApprovals,
|
|
3019
|
+
recentSystemEvents: recentSystemEvents.get(targetSessionKey) ?? [],
|
|
3020
|
+
epoch: gatewayEpoch,
|
|
3021
|
+
snapshot: buildSnapshot(targetAgentId),
|
|
3022
|
+
};
|
|
3023
|
+
}
|
|
2905
3024
|
case "memory-graph": {
|
|
2906
3025
|
// Memory Graph dashboard data — nodes + typed edges + topic clusters
|
|
2907
3026
|
// + stats, for an agent's workspace. Read; default-pass access guard
|
|
@@ -3106,8 +3225,32 @@ async function continueBoot(args) {
|
|
|
3106
3225
|
// already enforced regardless, so plugins that declare a scope
|
|
3107
3226
|
// today won't need a code change when multi-user lands.
|
|
3108
3227
|
const caller = { id: "local", scopes: ["operator.admin", "operator.write", "operator.read"] };
|
|
3109
|
-
//
|
|
3110
|
-
//
|
|
3228
|
+
// Champion-tier handshake: the FIRST frame is `hello-ok`, handing the
|
|
3229
|
+
// client everything it needs to subscribe without hardcoding — the
|
|
3230
|
+
// protocol version, its connId, the gateway's build version + epoch
|
|
3231
|
+
// (session generation, for restart detection), the full list of callable
|
|
3232
|
+
// methods (core wire methods + registered control-plane RPCs) and
|
|
3233
|
+
// subscribable events, and the policy limits (payload/buffer caps + tick
|
|
3234
|
+
// interval). A client that ignores it still works (the `state` frame
|
|
3235
|
+
// below preserves the legacy boot path).
|
|
3236
|
+
const helloOk = {
|
|
3237
|
+
type: "hello-ok",
|
|
3238
|
+
protocol: PROTOCOL_VERSION,
|
|
3239
|
+
server: { version: getBuildInfo().version, connId, epoch: gatewayEpoch },
|
|
3240
|
+
features: {
|
|
3241
|
+
methods: [...REQUEST_METHODS, ...customMethods.keys()],
|
|
3242
|
+
events: [...EVENT_NAMES],
|
|
3243
|
+
},
|
|
3244
|
+
policy: {
|
|
3245
|
+
maxPayload: MAX_WS_PAYLOAD_BYTES,
|
|
3246
|
+
maxBufferedBytes: MAX_WS_BUFFERED_BYTES,
|
|
3247
|
+
tickIntervalMs: TICK_INTERVAL_MS,
|
|
3248
|
+
},
|
|
3249
|
+
auth: { role: "operator" },
|
|
3250
|
+
};
|
|
3251
|
+
ws.send(JSON.stringify(helloOk));
|
|
3252
|
+
// Then the initial snapshot so the client can render its header before
|
|
3253
|
+
// any user action (also the back-compat boot frame for older clients).
|
|
3111
3254
|
ws.send(JSON.stringify({ type: "event", event: "state", payload: buildSnapshot() }));
|
|
3112
3255
|
// Per-connection ring of RPC timestamps powering the sliding-window check.
|
|
3113
3256
|
const rateRing = [];
|
|
@@ -3247,16 +3390,31 @@ async function continueBoot(args) {
|
|
|
3247
3390
|
});
|
|
3248
3391
|
});
|
|
3249
3392
|
/* ──────────────── tick heartbeat ──────────────── */
|
|
3250
|
-
//
|
|
3251
|
-
//
|
|
3252
|
-
//
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3393
|
+
// Send a raw (non-`event`) frame to every open client. Used for the cheap
|
|
3394
|
+
// `tick` keepalive + the graceful `shutdown` notice — each a single tiny
|
|
3395
|
+
// frame, so no backpressure gate (the ping reaper handles a truly dead one).
|
|
3396
|
+
const sendRawToAll = (frame) => {
|
|
3397
|
+
const json = JSON.stringify(frame);
|
|
3398
|
+
for (const ws of clients) {
|
|
3399
|
+
if (ws.readyState !== ws.OPEN)
|
|
3400
|
+
continue;
|
|
3401
|
+
try {
|
|
3402
|
+
ws.send(json);
|
|
3403
|
+
}
|
|
3404
|
+
catch {
|
|
3405
|
+
/* best-effort */
|
|
3406
|
+
}
|
|
3407
|
+
}
|
|
3408
|
+
};
|
|
3409
|
+
// Emit a cheap `tick` frame every TICK_INTERVAL_MS so clients detect a dead
|
|
3410
|
+
// server (no frames in 2× this interval = close + reconnect). Was a full
|
|
3411
|
+
// `state` snapshot to every binding; a tick is far lighter (battery/bandwidth
|
|
3412
|
+
// on mobile) and `state` is still pushed on every real mutation + on connect,
|
|
3413
|
+
// so idle clients stay consistent. The tick also doubles as the heartbeat-file
|
|
3414
|
+
// beat: refreshing it from inside the event-loop tick proves the loop is
|
|
3415
|
+
// healthy — a starved loop misses it and the supervisor restarts the process.
|
|
3258
3416
|
const tickTimer = setInterval(() => {
|
|
3259
|
-
|
|
3417
|
+
sendRawToAll({ type: "tick", ts: Date.now() });
|
|
3260
3418
|
void writeHeartbeatFile().catch(() => {
|
|
3261
3419
|
/* best-effort */
|
|
3262
3420
|
});
|
|
@@ -4093,6 +4251,86 @@ async function continueBoot(args) {
|
|
|
4093
4251
|
disposeHandlers.push(registerGatewayHandler("org.snapshot", (_params) => handleOrgSnapshot(undefined, {
|
|
4094
4252
|
loadConfig: () => loadConfig(),
|
|
4095
4253
|
})));
|
|
4254
|
+
// `config.*` — operator-level config CRUD over the wire (the `brigade
|
|
4255
|
+
// config` CLI, reachable from a remote client). Path/value/redact shape:
|
|
4256
|
+
// never session-targeted, so the guard-sweep correctly needs no per-session
|
|
4257
|
+
// access check. Reads/writes go through the mode-aware loadConfig/saveConfig,
|
|
4258
|
+
// so this works in filesystem AND Convex mode.
|
|
4259
|
+
disposeHandlers.push(registerGatewayHandler("config.get", handleConfigGet));
|
|
4260
|
+
disposeHandlers.push(registerGatewayHandler("config.set", handleConfigSet));
|
|
4261
|
+
disposeHandlers.push(registerGatewayHandler("config.unset", handleConfigUnset));
|
|
4262
|
+
disposeHandlers.push(registerGatewayHandler("config.list", handleConfigList));
|
|
4263
|
+
disposeHandlers.push(registerGatewayHandler("config.schema", handleConfigSchema));
|
|
4264
|
+
disposeHandlers.push(registerGatewayHandler("config.validate", handleConfigValidate));
|
|
4265
|
+
// `exec.*` — operator-level exec-approval allowlist CRUD (the `brigade exec`
|
|
4266
|
+
// CLI over the wire). Per-agent + operator-scoped (the operator manages
|
|
4267
|
+
// their OWN agents' bash-approval allowlist), the same posture as the
|
|
4268
|
+
// allowlisted exec-allow-all / exec-grant-skill RPCs — no per-session guard
|
|
4269
|
+
// (see ALLOWLIST_NO_GUARD_NEEDED in server.guard-sweep.test.ts). The
|
|
4270
|
+
// hard-deny safety net in exec-approvals.ts still applies on every allow.
|
|
4271
|
+
disposeHandlers.push(registerGatewayHandler("exec.list", handleExecList));
|
|
4272
|
+
disposeHandlers.push(registerGatewayHandler("exec.allow", handleExecAllow));
|
|
4273
|
+
disposeHandlers.push(registerGatewayHandler("exec.allow-pattern", handleExecAllowPattern));
|
|
4274
|
+
disposeHandlers.push(registerGatewayHandler("exec.remove", handleExecRemove));
|
|
4275
|
+
disposeHandlers.push(registerGatewayHandler("exec.deny-test", handleExecDenyTest));
|
|
4276
|
+
// `agents.*` — operator-level routing-binding management (which agent owns
|
|
4277
|
+
// which channel/account). The genuine no-other-path gap: agent add/delete/
|
|
4278
|
+
// set-identity are already reachable via the `manage_agent` tool, but
|
|
4279
|
+
// bindings had no remote path. Operator-scoped config mutation, no per-
|
|
4280
|
+
// session guard (allowlisted in server.guard-sweep.test.ts).
|
|
4281
|
+
disposeHandlers.push(registerGatewayHandler("agents.bindings", handleAgentsBindings));
|
|
4282
|
+
disposeHandlers.push(registerGatewayHandler("agents.bind", handleAgentsBind));
|
|
4283
|
+
disposeHandlers.push(registerGatewayHandler("agents.unbind", handleAgentsUnbind));
|
|
4284
|
+
// `pairing.*` — operator-level channel pairing (approve/revoke strangers who
|
|
4285
|
+
// DM the bot). Per-channel + operator-scoped, no per-session guard. The RPCs
|
|
4286
|
+
// require an explicit channel (a client gets the channel list from
|
|
4287
|
+
// system.capabilities), unlike the CLI's single-channel auto-pick.
|
|
4288
|
+
disposeHandlers.push(registerGatewayHandler("pairing.list", handlePairingList));
|
|
4289
|
+
disposeHandlers.push(registerGatewayHandler("pairing.approve", handlePairingApprove));
|
|
4290
|
+
disposeHandlers.push(registerGatewayHandler("pairing.revoke", handlePairingRevoke));
|
|
4291
|
+
// `sessions.cleanup` — operator maintenance: delete an agent's stale idle
|
|
4292
|
+
// transcript files (the gateway regenerates the store entry on next access).
|
|
4293
|
+
// NOT session-content access (unlike sessions.list/history), so no per-
|
|
4294
|
+
// session guard (allowlisted in server.guard-sweep.test.ts).
|
|
4295
|
+
disposeHandlers.push(registerGatewayHandler("sessions.cleanup", handleSessionsCleanup));
|
|
4296
|
+
// `memory.*` — Tideline write + governance (write_memory / manage_memory).
|
|
4297
|
+
// Memory lives in facts.jsonl (NOT config), so config.set can't reach it;
|
|
4298
|
+
// these are the only typed remote path to MUTATE memory (read is covered by
|
|
4299
|
+
// memory-query / memory-graph). Operator-scoped owner origin, no per-session
|
|
4300
|
+
// guard (allowlisted in server.guard-sweep.test.ts).
|
|
4301
|
+
disposeHandlers.push(registerGatewayHandler("memory.write", handleMemoryWrite));
|
|
4302
|
+
disposeHandlers.push(registerGatewayHandler("memory.manage", handleMemoryManage));
|
|
4303
|
+
// agents.add/delete/set-identity — agent CRUD (reuses the manage_agent tool,
|
|
4304
|
+
// which wraps `brigade agents add/delete/set-identity`). Seeds/soft-deletes a
|
|
4305
|
+
// workspace, so config.set alone can't do it. Operator-scoped (allowlisted).
|
|
4306
|
+
disposeHandlers.push(registerGatewayHandler("agents.add", handleAgentsAdd));
|
|
4307
|
+
disposeHandlers.push(registerGatewayHandler("agents.delete", handleAgentsDelete));
|
|
4308
|
+
disposeHandlers.push(registerGatewayHandler("agents.set-identity", handleAgentsSetIdentity));
|
|
4309
|
+
// skills.create/delete/write-file — skill authoring (reuses the manage_skill
|
|
4310
|
+
// tool). SKILL.md files on disk, not config. (status/install/update already
|
|
4311
|
+
// cover read/install/enable.) Operator-scoped (allowlisted).
|
|
4312
|
+
disposeHandlers.push(registerGatewayHandler("skills.create", handleSkillsCreate));
|
|
4313
|
+
disposeHandlers.push(registerGatewayHandler("skills.delete", handleSkillsDelete));
|
|
4314
|
+
disposeHandlers.push(registerGatewayHandler("skills.write-file", handleSkillsWriteFile));
|
|
4315
|
+
// channels.* — LIVE connect/disconnect (runtime adapter via the global
|
|
4316
|
+
// channel manager) + DM allow-from (a per-channel file store, not config).
|
|
4317
|
+
// Channel enable/disable/policy are already config.set-reachable. Operator-
|
|
4318
|
+
// scoped (allowlisted). connect reuses the owner-scoped connect_channel tool.
|
|
4319
|
+
disposeHandlers.push(registerGatewayHandler("channels.connect", handleChannelsConnect));
|
|
4320
|
+
disposeHandlers.push(registerGatewayHandler("channels.disconnect", handleChannelsDisconnect));
|
|
4321
|
+
disposeHandlers.push(registerGatewayHandler("channels.allow-add", handleChannelsAllowAdd));
|
|
4322
|
+
disposeHandlers.push(registerGatewayHandler("channels.allow-remove", handleChannelsAllowRemove));
|
|
4323
|
+
disposeHandlers.push(registerGatewayHandler("channels.allow-list", handleChannelsAllowList));
|
|
4324
|
+
// provider.remove — delete a provider key (auth-profiles.json, not config;
|
|
4325
|
+
// add-provider exists, removal had no gateway path). Operator-scoped.
|
|
4326
|
+
disposeHandlers.push(registerGatewayHandler("provider.remove", handleProviderRemove));
|
|
4327
|
+
// composio + oauth — integrations. `composio` is remote-clean (Composio
|
|
4328
|
+
// hosts the OAuth callback; the gateway hands over a click-link + polls).
|
|
4329
|
+
// `oauth` is the DIY loopback flow (callback on the gateway host — completes
|
|
4330
|
+
// only for a local/tunneled operator; status/token work remotely). Both
|
|
4331
|
+
// reuse the owner-scoped tools. Operator-scoped (allowlisted).
|
|
4332
|
+
disposeHandlers.push(registerGatewayHandler("composio", handleComposio));
|
|
4333
|
+
disposeHandlers.push(registerGatewayHandler("oauth", handleOauth));
|
|
4096
4334
|
// Wave O0.8 GAP 11 — opt the session inbox into JSONL persistence at
|
|
4097
4335
|
// gateway boot. The disk write surface defaults off so the existing
|
|
4098
4336
|
// unit-test fleet (which doesn't tempdir-isolate ~/.brigade) keeps
|
|
@@ -4955,6 +5193,15 @@ async function continueBoot(args) {
|
|
|
4955
5193
|
catch {
|
|
4956
5194
|
/* best-effort */
|
|
4957
5195
|
}
|
|
5196
|
+
// Tell connected clients we're going down gracefully BEFORE the
|
|
5197
|
+
// sockets close, so a web/mobile UI shows "reconnecting…" and
|
|
5198
|
+
// pre-empts the resume instead of treating the drop as an error.
|
|
5199
|
+
try {
|
|
5200
|
+
sendRawToAll({ type: "shutdown", reason: "gateway shutting down" });
|
|
5201
|
+
}
|
|
5202
|
+
catch {
|
|
5203
|
+
/* best-effort */
|
|
5204
|
+
}
|
|
4958
5205
|
// Stop the heartbeat runner first so its wake handler unregisters
|
|
4959
5206
|
// before the rest of the wires unwire — prevents a late wake
|
|
4960
5207
|
// from firing through a half-torn-down dispatcher.
|