@tokenfactory/acc-runner 0.39.0 → 0.40.1
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/dist/cli.js +60 -0
- package/dist/cli.js.map +1 -1
- package/dist/companion-autostart.d.ts +71 -0
- package/dist/companion-autostart.d.ts.map +1 -0
- package/dist/companion-autostart.js +191 -0
- package/dist/companion-autostart.js.map +1 -0
- package/dist/companion-mirror-socket.d.ts +126 -0
- package/dist/companion-mirror-socket.d.ts.map +1 -0
- package/dist/companion-mirror-socket.js +196 -0
- package/dist/companion-mirror-socket.js.map +1 -0
- package/dist/companion-run.d.ts +19 -0
- package/dist/companion-run.d.ts.map +1 -0
- package/dist/companion-run.js +132 -0
- package/dist/companion-run.js.map +1 -0
- package/dist/companion.d.ts +181 -0
- package/dist/companion.d.ts.map +1 -0
- package/dist/companion.js +343 -0
- package/dist/companion.js.map +1 -0
- package/dist/config.d.ts +38 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +39 -0
- package/dist/config.js.map +1 -1
- package/dist/doctor.d.ts +1 -0
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +92 -1
- package/dist/doctor.js.map +1 -1
- package/dist/login.d.ts +3 -2
- package/dist/login.d.ts.map +1 -1
- package/dist/login.js +8 -2
- package/dist/login.js.map +1 -1
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/watch.d.ts +27 -1
- package/dist/watch.d.ts.map +1 -1
- package/dist/watch.js +101 -1
- package/dist/watch.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CC-3C (COMPANION): the localhost mirror socket.
|
|
3
|
+
*
|
|
4
|
+
* A companion runner already streams its chat-turn output to the control plane
|
|
5
|
+
* over the `chat:<conversation_id>` Realtime topic (chat-transport.ts) — that
|
|
6
|
+
* path is authoritative: the server-side consumer persists deltas into
|
|
7
|
+
* acc.chat_turn_deltas and the DB stays the SINGLE source of truth. This module
|
|
8
|
+
* adds a SECOND, purely-local delivery of the same frames over a loopback-only
|
|
9
|
+
* WebSocket so the operator's browser — running on the same machine as the
|
|
10
|
+
* companion — can paint the reply with sub-round-trip latency instead of waiting
|
|
11
|
+
* on the Realtime fan-out.
|
|
12
|
+
*
|
|
13
|
+
* Two invariants make the fast path safe, and both are enforced structurally so
|
|
14
|
+
* a conformance test can assert them:
|
|
15
|
+
*
|
|
16
|
+
* 1. AUTHENTICATED — the server binds to 127.0.0.1 ONLY (never a routable
|
|
17
|
+
* interface) and every upgrade must present the session-bound handshake
|
|
18
|
+
* token. A missing/blank/wrong token is closed with 1008 before a single
|
|
19
|
+
* frame is delivered. The expected token is injected (bound to the runner's
|
|
20
|
+
* current keychain session, rotated on logout), so the socket can never
|
|
21
|
+
* outlive the session it mirrors.
|
|
22
|
+
*
|
|
23
|
+
* 2. MIRROR-ONLY — the server is constructed with an OUTBOUND broadcast source
|
|
24
|
+
* and nothing else. It holds no Supabase client, no `ChatLaneTransport`, no
|
|
25
|
+
* RPC handle: there is literally no method by which an inbound frame could
|
|
26
|
+
* mutate DB state. Inbound frames are accepted (so the browser may send
|
|
27
|
+
* liveness pings) but routed to a no-op sink; the module exposes an
|
|
28
|
+
* `inboundCount()` so the conformance test can prove traffic arrived AND
|
|
29
|
+
* that arrival changed nothing. The `mirror_only: true` flag stamped on
|
|
30
|
+
* every frame tells the browser the same thing on the wire: treat the DB
|
|
31
|
+
* read as truth; this is a latency shim.
|
|
32
|
+
*
|
|
33
|
+
* The whole surface (server factory, clock, log sink) is injectable so the
|
|
34
|
+
* conformance suite exercises it against a real loopback socket without any
|
|
35
|
+
* network or `claude` binary.
|
|
36
|
+
*/
|
|
37
|
+
import { createServer } from "node:http";
|
|
38
|
+
import { timingSafeEqual } from "node:crypto";
|
|
39
|
+
import { WebSocketServer } from "ws";
|
|
40
|
+
import { CHAT_LANE_PROTOCOL_VERSION } from "./chat-lane.js";
|
|
41
|
+
/** The loopback address the mirror socket binds to — never a routable interface. */
|
|
42
|
+
export const MIRROR_SOCKET_HOST = "127.0.0.1";
|
|
43
|
+
/** Query-string key the browser passes the session-bound handshake token under. */
|
|
44
|
+
export const MIRROR_TOKEN_QUERY_KEY = "token";
|
|
45
|
+
/** WebSocket close code for an unauthenticated / unauthorised upgrade (policy violation). */
|
|
46
|
+
export const MIRROR_CLOSE_UNAUTHORIZED = 1008;
|
|
47
|
+
/** Constant-time token compare that tolerates length mismatch without throwing. */
|
|
48
|
+
export function tokensMatch(a, b) {
|
|
49
|
+
if (a.length === 0 || b.length === 0)
|
|
50
|
+
return false;
|
|
51
|
+
const ba = Buffer.from(a);
|
|
52
|
+
const bb = Buffer.from(b);
|
|
53
|
+
if (ba.length !== bb.length)
|
|
54
|
+
return false;
|
|
55
|
+
return timingSafeEqual(ba, bb);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Extract the handshake token from an upgrade request URL's query string.
|
|
59
|
+
* Returns "" when absent so the caller's compare fails closed.
|
|
60
|
+
*/
|
|
61
|
+
export function extractMirrorToken(url) {
|
|
62
|
+
if (!url)
|
|
63
|
+
return "";
|
|
64
|
+
const q = url.indexOf("?");
|
|
65
|
+
if (q === -1)
|
|
66
|
+
return "";
|
|
67
|
+
const params = new URLSearchParams(url.slice(q + 1));
|
|
68
|
+
return params.get(MIRROR_TOKEN_QUERY_KEY) ?? "";
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The loopback mirror server. Construct it with an OUTBOUND broadcast source
|
|
72
|
+
* only: `mirror()` fans a frame to every authed browser client; there is no
|
|
73
|
+
* inbound → mutation path by construction (see module header, invariant 2).
|
|
74
|
+
*/
|
|
75
|
+
export class CompanionMirrorSocket {
|
|
76
|
+
opts;
|
|
77
|
+
http = null;
|
|
78
|
+
wss = null;
|
|
79
|
+
clients = new Set();
|
|
80
|
+
inbound = 0;
|
|
81
|
+
boundPort = 0;
|
|
82
|
+
now;
|
|
83
|
+
constructor(opts) {
|
|
84
|
+
this.opts = opts;
|
|
85
|
+
this.now = opts.now ?? Date.now;
|
|
86
|
+
}
|
|
87
|
+
/** Bind the loopback server and begin accepting authed upgrades. */
|
|
88
|
+
async start() {
|
|
89
|
+
if (this.wss)
|
|
90
|
+
return;
|
|
91
|
+
const http = (this.opts.createHttpServer ?? createServer)();
|
|
92
|
+
// `noServer` + manual upgrade so we authorise BEFORE completing the handshake
|
|
93
|
+
// (an unauthorised client is closed without ever joining the client set).
|
|
94
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
95
|
+
http.on("upgrade", (req, socket, head) => {
|
|
96
|
+
const token = extractMirrorToken(req.url);
|
|
97
|
+
if (!tokensMatch(token, this.opts.expectedToken)) {
|
|
98
|
+
this.opts.onLog?.("[acc-runner] mirror socket: rejected unauthorised upgrade");
|
|
99
|
+
// Refuse the handshake outright — the browser never becomes a client.
|
|
100
|
+
socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
|
|
101
|
+
socket.destroy();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
wss.handleUpgrade(req, socket, head, (ws) => this.accept(ws));
|
|
105
|
+
});
|
|
106
|
+
await new Promise((resolve, reject) => {
|
|
107
|
+
http.once("error", reject);
|
|
108
|
+
http.listen(this.opts.port, MIRROR_SOCKET_HOST, () => {
|
|
109
|
+
http.off("error", reject);
|
|
110
|
+
const addr = http.address();
|
|
111
|
+
this.boundPort = typeof addr === "object" && addr ? addr.port : this.opts.port;
|
|
112
|
+
resolve();
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
this.http = http;
|
|
116
|
+
this.wss = wss;
|
|
117
|
+
}
|
|
118
|
+
/** The bound loopback port (meaningful only after `start`; resolves an ephemeral 0). */
|
|
119
|
+
port() {
|
|
120
|
+
return this.boundPort;
|
|
121
|
+
}
|
|
122
|
+
/** Live authed browser client count. */
|
|
123
|
+
clientCount() {
|
|
124
|
+
return this.clients.size;
|
|
125
|
+
}
|
|
126
|
+
/** Total inbound frames received (all routed to the no-op sink — proof of mirror-only). */
|
|
127
|
+
inboundCount() {
|
|
128
|
+
return this.inbound;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Fan one chat-lane frame to every connected browser. Best-effort: a send
|
|
132
|
+
* failure to one stale client never blocks the others or the turn. Returns
|
|
133
|
+
* the number of clients the frame was written to.
|
|
134
|
+
*/
|
|
135
|
+
mirror(event, conversationId, turnId, payload) {
|
|
136
|
+
if (!this.wss || this.clients.size === 0)
|
|
137
|
+
return 0;
|
|
138
|
+
const frame = {
|
|
139
|
+
protocol_version: CHAT_LANE_PROTOCOL_VERSION,
|
|
140
|
+
source: "companion-mirror",
|
|
141
|
+
mirror_only: true,
|
|
142
|
+
conversation_id: conversationId,
|
|
143
|
+
turn_id: turnId,
|
|
144
|
+
event,
|
|
145
|
+
payload,
|
|
146
|
+
ts: new Date(this.now()).toISOString(),
|
|
147
|
+
};
|
|
148
|
+
const text = JSON.stringify(frame);
|
|
149
|
+
let sent = 0;
|
|
150
|
+
for (const ws of this.clients) {
|
|
151
|
+
if (ws.readyState !== ws.OPEN)
|
|
152
|
+
continue;
|
|
153
|
+
try {
|
|
154
|
+
ws.send(text);
|
|
155
|
+
sent += 1;
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
this.opts.onLog?.(`[acc-runner] mirror send failed: ${err.message}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return sent;
|
|
162
|
+
}
|
|
163
|
+
/** Tear the server down: close every client, then the WS + HTTP servers. */
|
|
164
|
+
async close() {
|
|
165
|
+
for (const ws of this.clients) {
|
|
166
|
+
try {
|
|
167
|
+
ws.close(1001, "runner shutdown");
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
/* best-effort */
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
this.clients.clear();
|
|
174
|
+
const wss = this.wss;
|
|
175
|
+
const http = this.http;
|
|
176
|
+
this.wss = null;
|
|
177
|
+
this.http = null;
|
|
178
|
+
if (wss)
|
|
179
|
+
await new Promise((resolve) => wss.close(() => resolve()));
|
|
180
|
+
if (http)
|
|
181
|
+
await new Promise((resolve) => http.close(() => resolve()));
|
|
182
|
+
}
|
|
183
|
+
accept(ws) {
|
|
184
|
+
this.clients.add(ws);
|
|
185
|
+
// INBOUND is a dead end: count it, hand it to the pure sink, and drop it.
|
|
186
|
+
// No branch here reaches a transport / RPC / DB write — that is the
|
|
187
|
+
// mirror-only invariant, enforced by construction (this class holds none).
|
|
188
|
+
ws.on("message", (data) => {
|
|
189
|
+
this.inbound += 1;
|
|
190
|
+
this.opts.onInbound?.(typeof data === "string" ? data : data.toString());
|
|
191
|
+
});
|
|
192
|
+
ws.on("close", () => this.clients.delete(ws));
|
|
193
|
+
ws.on("error", () => this.clients.delete(ws));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
//# sourceMappingURL=companion-mirror-socket.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"companion-mirror-socket.js","sourceRoot":"","sources":["../src/companion-mirror-socket.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** A running companion; `stop()` requeues in-flight turns and flips offline. */
|
|
2
|
+
export interface CompanionHandle {
|
|
3
|
+
stop(): Promise<void>;
|
|
4
|
+
}
|
|
5
|
+
/** Env var seeding the owner's conversations the companion should serve at boot. */
|
|
6
|
+
export declare const COMPANION_CONVERSATIONS_ENV = "ACC_RUNNER_COMPANION_CONVERSATIONS";
|
|
7
|
+
/**
|
|
8
|
+
* Start companion mode. Registers the owner-bound identity, serves the owner's
|
|
9
|
+
* chat turns (chat lane only), and returns a handle whose `stop()` requeues
|
|
10
|
+
* in-flight turns to the fleet and flips the identity offline.
|
|
11
|
+
*/
|
|
12
|
+
export declare function companionCommand(): Promise<CompanionHandle>;
|
|
13
|
+
/** Print the one-line companion status (menu-bar friendly). */
|
|
14
|
+
export declare function companionStatusCommand(): Promise<void>;
|
|
15
|
+
/** Install the companion login agent (autostart at login). */
|
|
16
|
+
export declare function companionInstallCommand(): Promise<void>;
|
|
17
|
+
/** Remove the companion login agent. */
|
|
18
|
+
export declare function companionUninstallCommand(): Promise<void>;
|
|
19
|
+
//# sourceMappingURL=companion-run.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"companion-run.d.ts","sourceRoot":"","sources":["../src/companion-run.ts"],"names":[],"mappings":"AAgCA,gFAAgF;AAChF,MAAM,WAAW,eAAe;IAC9B,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAED,oFAAoF;AACpF,eAAO,MAAM,2BAA2B,uCAAuC,CAAC;AAEhF;;;;GAIG;AACH,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,eAAe,CAAC,CAgFjE;AAED,+DAA+D;AAC/D,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,IAAI,CAAC,CAmB5D;AAED,8DAA8D;AAC9D,wBAAsB,uBAAuB,IAAI,OAAO,CAAC,IAAI,CAAC,CAK7D;AAED,wCAAwC;AACxC,wBAAsB,yBAAyB,IAAI,OAAO,CAAC,IAAI,CAAC,CAK/D"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CC-3C (COMPANION): production wiring for the `acc-runner companion` command.
|
|
3
|
+
*
|
|
4
|
+
* Thin glue that builds the live dependencies (session, Supabase client, chat
|
|
5
|
+
* engine, transport, circuit breaker, mirror socket, identity broadcaster) and
|
|
6
|
+
* drives `CompanionRunner`. Kept out of cli.ts so cli.ts stays a pure command
|
|
7
|
+
* table, and out of companion.ts so the orchestrator stays free of I/O and
|
|
8
|
+
* unit-testable with injected fakes.
|
|
9
|
+
*/
|
|
10
|
+
import chalk from "chalk";
|
|
11
|
+
import { applyBootstrapConfig, loadConfig, resolveRunnerConcurrency } from "./config.js";
|
|
12
|
+
import { getTokenProvider } from "./token-provider.js";
|
|
13
|
+
import { createRunnerClient } from "./supabase.js";
|
|
14
|
+
import { createSupabaseChatTransport } from "./chat-transport.js";
|
|
15
|
+
import { getChatEngine } from "./engines/registry.js";
|
|
16
|
+
import { ChatCircuitBreaker, getChatCircuit } from "./runtime/chat-circuit-breaker.js";
|
|
17
|
+
import { CompanionMirrorSocket } from "./companion-mirror-socket.js";
|
|
18
|
+
import { CompanionRunner, OwnerConversationSource, createIdentityBroadcaster, deriveMirrorToken, resolveCompanionConfig, } from "./companion.js";
|
|
19
|
+
import { companionStatusLine, defaultAutostartHost, installAutostart, uninstallAutostart, } from "./companion-autostart.js";
|
|
20
|
+
import { PACKAGE_VERSION } from "./pkg-version.js";
|
|
21
|
+
/** Env var seeding the owner's conversations the companion should serve at boot. */
|
|
22
|
+
export const COMPANION_CONVERSATIONS_ENV = "ACC_RUNNER_COMPANION_CONVERSATIONS";
|
|
23
|
+
/**
|
|
24
|
+
* Start companion mode. Registers the owner-bound identity, serves the owner's
|
|
25
|
+
* chat turns (chat lane only), and returns a handle whose `stop()` requeues
|
|
26
|
+
* in-flight turns to the fleet and flips the identity offline.
|
|
27
|
+
*/
|
|
28
|
+
export async function companionCommand() {
|
|
29
|
+
let cfg = loadConfig();
|
|
30
|
+
const session = await getTokenProvider().load();
|
|
31
|
+
if (!session) {
|
|
32
|
+
throw new Error("Not logged in. Run `acc-runner login` first.");
|
|
33
|
+
}
|
|
34
|
+
cfg = applyBootstrapConfig(cfg, session);
|
|
35
|
+
const supabase = createRunnerClient(cfg, session.access_token, session.realtime_config);
|
|
36
|
+
const breaker = new ChatCircuitBreaker();
|
|
37
|
+
await breaker.load();
|
|
38
|
+
const engine = getChatEngine();
|
|
39
|
+
const transport = createSupabaseChatTransport(supabase);
|
|
40
|
+
const companionCfg = resolveCompanionConfig();
|
|
41
|
+
const conc = resolveRunnerConcurrency();
|
|
42
|
+
const mirror = companionCfg.mirrorEnabled
|
|
43
|
+
? new CompanionMirrorSocket({
|
|
44
|
+
expectedToken: companionCfg.mirrorToken ?? deriveMirrorToken(session),
|
|
45
|
+
port: companionCfg.mirrorPort,
|
|
46
|
+
onLog: (line) => console.log(chalk.gray(line)),
|
|
47
|
+
})
|
|
48
|
+
: null;
|
|
49
|
+
// Owner-affinity: only conversations owned by this user are servable. Seeded
|
|
50
|
+
// from the env allowlist (owner == this session's user); the control plane
|
|
51
|
+
// adds/removes more over the authenticated identity topic.
|
|
52
|
+
const seedIds = (process.env[COMPANION_CONVERSATIONS_ENV] ?? "")
|
|
53
|
+
.split(",")
|
|
54
|
+
.map((s) => s.trim())
|
|
55
|
+
.filter(Boolean)
|
|
56
|
+
.map((conversationId) => ({ conversationId, ownerUserId: session.user_id }));
|
|
57
|
+
const conversations = new OwnerConversationSource(session.user_id, seedIds);
|
|
58
|
+
const broadcaster = createIdentityBroadcaster(supabase, session.user_id, {
|
|
59
|
+
onLog: (line) => console.log(chalk.gray(line)),
|
|
60
|
+
});
|
|
61
|
+
const runner = new CompanionRunner({
|
|
62
|
+
runnerId: session.runner_id,
|
|
63
|
+
boundUserId: session.user_id,
|
|
64
|
+
engine,
|
|
65
|
+
transport,
|
|
66
|
+
breaker,
|
|
67
|
+
conversations,
|
|
68
|
+
concurrencyLimit: conc.concurrency,
|
|
69
|
+
// A dedicated companion has no code lane, so the whole runner is chat's
|
|
70
|
+
// (the budget still reserves ≥1 slot internally).
|
|
71
|
+
maxShare: 1,
|
|
72
|
+
leaseMs: 60_000,
|
|
73
|
+
mirror,
|
|
74
|
+
announce: (event, payload) => broadcaster.announce(event, payload),
|
|
75
|
+
heartbeat: async () => {
|
|
76
|
+
const { error } = await supabase.rpc("heartbeat_runner", {
|
|
77
|
+
p_id: session.runner_id,
|
|
78
|
+
p_version: PACKAGE_VERSION,
|
|
79
|
+
});
|
|
80
|
+
if (error)
|
|
81
|
+
throw new Error(error.message);
|
|
82
|
+
},
|
|
83
|
+
cwd: cfg.repoPath,
|
|
84
|
+
onLog: (line) => console.log(chalk.gray(line)),
|
|
85
|
+
});
|
|
86
|
+
await runner.start();
|
|
87
|
+
console.log(chalk.green(`✓ Companion online for ${session.email ?? session.user_id}`));
|
|
88
|
+
console.log(chalk.gray(` serving ${conversations.size()} conversation(s)` +
|
|
89
|
+
(mirror ? `; mirror on 127.0.0.1:${mirror.port()}` : "; mirror disabled")));
|
|
90
|
+
return {
|
|
91
|
+
async stop() {
|
|
92
|
+
await runner.stop();
|
|
93
|
+
await broadcaster.close();
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/** Print the one-line companion status (menu-bar friendly). */
|
|
98
|
+
export async function companionStatusCommand() {
|
|
99
|
+
const session = await getTokenProvider().load();
|
|
100
|
+
const circuit = await getChatCircuit();
|
|
101
|
+
const cfg = resolveCompanionConfig();
|
|
102
|
+
// A `status` invocation is a separate process from the running companion, so
|
|
103
|
+
// it reports what it can see locally: login state, the chat-circuit sentinel,
|
|
104
|
+
// and the mirror config. Live turn/conversation counts belong to the running
|
|
105
|
+
// process and are shown as 0 here.
|
|
106
|
+
console.log(companionStatusLine({
|
|
107
|
+
online: session !== null,
|
|
108
|
+
ownerEmail: session?.email ?? null,
|
|
109
|
+
conversations: 0,
|
|
110
|
+
activeTurns: 0,
|
|
111
|
+
capacity: null,
|
|
112
|
+
circuitOpen: circuit !== null,
|
|
113
|
+
mirror: cfg.mirrorEnabled,
|
|
114
|
+
}));
|
|
115
|
+
}
|
|
116
|
+
/** Install the companion login agent (autostart at login). */
|
|
117
|
+
export async function companionInstallCommand() {
|
|
118
|
+
const binPath = process.argv[1] ?? "acc-runner";
|
|
119
|
+
const res = await installAutostart(defaultAutostartHost(binPath));
|
|
120
|
+
console.log(res.ok ? chalk.green(`✓ ${res.detail}`) : chalk.red(`✗ ${res.detail}`));
|
|
121
|
+
if (!res.ok)
|
|
122
|
+
process.exitCode = 1;
|
|
123
|
+
}
|
|
124
|
+
/** Remove the companion login agent. */
|
|
125
|
+
export async function companionUninstallCommand() {
|
|
126
|
+
const binPath = process.argv[1] ?? "acc-runner";
|
|
127
|
+
const res = await uninstallAutostart(defaultAutostartHost(binPath));
|
|
128
|
+
console.log(res.ok ? chalk.green(`✓ ${res.detail}`) : chalk.red(`✗ ${res.detail}`));
|
|
129
|
+
if (!res.ok)
|
|
130
|
+
process.exitCode = 1;
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=companion-run.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"companion-run.js","sourceRoot":"","sources":["../src/companion-run.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { resolveChatLaneConfig, type ChatLaneTransport, type ClaimedChatTurn } from "./chat-lane.js";
|
|
2
|
+
import type { CompanionMirrorSocket } from "./companion-mirror-socket.js";
|
|
3
|
+
import type { ChatCircuitBreaker } from "./runtime/chat-circuit-breaker.js";
|
|
4
|
+
import type { ConformantAgentEngine, EngineCapabilityMatrix, RunnerCapacityClass } from "./engines/types.js";
|
|
5
|
+
import type { StoredSession } from "./types.js";
|
|
6
|
+
/** Identity-topic events (mirror of the web-app `IdentityTopicEvent`). */
|
|
7
|
+
export type IdentityTopicEvent = "capacity_changed" | "companion_registered" | "companion_lost";
|
|
8
|
+
/** runner → server: register/refresh a companion bound to its owner. */
|
|
9
|
+
export interface CompanionRegisterPayload {
|
|
10
|
+
mode: "companion";
|
|
11
|
+
bound_user_id: string;
|
|
12
|
+
/** Engines this companion can drive, richest tier first. */
|
|
13
|
+
engine_ids: string[];
|
|
14
|
+
capability_matrix: EngineCapabilityMatrix;
|
|
15
|
+
capacity: RunnerCapacityClass;
|
|
16
|
+
}
|
|
17
|
+
/** Payload of an `identity:<user_id>` capacity broadcast. */
|
|
18
|
+
export interface IdentityCapacityEvent {
|
|
19
|
+
runner_id: string;
|
|
20
|
+
capacity: RunnerCapacityClass;
|
|
21
|
+
/** Engines still claimable on this runner at report time. */
|
|
22
|
+
available_engine_ids: string[];
|
|
23
|
+
ts: string;
|
|
24
|
+
}
|
|
25
|
+
/** Build the per-identity capacity Realtime topic (mirror of the web builder). */
|
|
26
|
+
export declare function identityTopic(userId: string): `identity:${string}`;
|
|
27
|
+
/** Opt the runner's chat lane into serving code tasks too (default: chat only). */
|
|
28
|
+
export declare const COMPANION_CODE_TASKS_ENV = "ACC_RUNNER_COMPANION_CODE_TASKS";
|
|
29
|
+
/** Disable the localhost mirror socket (default: enabled). */
|
|
30
|
+
export declare const COMPANION_MIRROR_ENV = "ACC_RUNNER_COMPANION_MIRROR";
|
|
31
|
+
/** Loopback port the mirror socket binds to. */
|
|
32
|
+
export declare const COMPANION_MIRROR_PORT_ENV = "ACC_RUNNER_COMPANION_MIRROR_PORT";
|
|
33
|
+
/** Explicit mirror handshake token override (else derived from the session). */
|
|
34
|
+
export declare const COMPANION_MIRROR_TOKEN_ENV = "ACC_RUNNER_COMPANION_MIRROR_TOKEN";
|
|
35
|
+
/** Default loopback port for the mirror socket. */
|
|
36
|
+
export declare const DEFAULT_COMPANION_MIRROR_PORT = 7717;
|
|
37
|
+
/** The resolved companion knobs. */
|
|
38
|
+
export interface CompanionConfig {
|
|
39
|
+
/** Whether the companion also claims code/review tasks (default false). */
|
|
40
|
+
codeTasksEnabled: boolean;
|
|
41
|
+
/** Whether the localhost mirror socket is served (default true). */
|
|
42
|
+
mirrorEnabled: boolean;
|
|
43
|
+
/** Loopback port the mirror socket binds to. */
|
|
44
|
+
mirrorPort: number;
|
|
45
|
+
/** Explicit mirror handshake token, or null to derive from the session. */
|
|
46
|
+
mirrorToken: string | null;
|
|
47
|
+
}
|
|
48
|
+
/** Resolve companion knobs from the environment in one place. */
|
|
49
|
+
export declare function resolveCompanionConfig(env?: NodeJS.ProcessEnv): CompanionConfig;
|
|
50
|
+
/**
|
|
51
|
+
* Derive the loopback mirror handshake token from the session. Bound to the
|
|
52
|
+
* refresh token (stable across hourly access-token refreshes, rotated on
|
|
53
|
+
* re-login / logout) + the runner id, one-way hashed so the token never leaks
|
|
54
|
+
* either. The same-machine browser obtains the matching token through the
|
|
55
|
+
* authenticated control plane; this keeps the mirror strictly session-bound —
|
|
56
|
+
* it cannot outlive the session it reflects.
|
|
57
|
+
*/
|
|
58
|
+
export declare function deriveMirrorToken(session: Pick<StoredSession, "refresh_token" | "runner_id">): string;
|
|
59
|
+
/**
|
|
60
|
+
* The set of conversations a companion is allowed to serve — the OWNER-AFFINITY
|
|
61
|
+
* guarantee. A conversation enters the set only when its owner is the bound
|
|
62
|
+
* user; `add` returns false and no-ops for any other owner, so the companion's
|
|
63
|
+
* claim loop can never present a foreign operator's conversation to the claim
|
|
64
|
+
* RPC. Fed by the control plane (the browser announces which conversation the
|
|
65
|
+
* operator is active in over the authenticated `identity:<user>` topic); an
|
|
66
|
+
* optional seed lists conversations known at boot.
|
|
67
|
+
*/
|
|
68
|
+
export declare class OwnerConversationSource {
|
|
69
|
+
private readonly boundUserId;
|
|
70
|
+
private readonly ids;
|
|
71
|
+
constructor(boundUserId: string, seed?: Array<{
|
|
72
|
+
conversationId: string;
|
|
73
|
+
ownerUserId: string;
|
|
74
|
+
}>);
|
|
75
|
+
/**
|
|
76
|
+
* Register a conversation IF and only if its owner is the bound user. Returns
|
|
77
|
+
* true when it was added (owner match), false when rejected (foreign owner or
|
|
78
|
+
* blank id). This is the single enforcement point for owner-affinity.
|
|
79
|
+
*/
|
|
80
|
+
add(conversationId: string, ownerUserId: string): boolean;
|
|
81
|
+
/** Drop a conversation (operator closed it / it moved to another owner). */
|
|
82
|
+
remove(conversationId: string): void;
|
|
83
|
+
/** True when the conversation is currently served by this companion. */
|
|
84
|
+
has(conversationId: string): boolean;
|
|
85
|
+
/** Stable snapshot of the served conversations (for the claim loop). */
|
|
86
|
+
snapshot(): string[];
|
|
87
|
+
/** Count of served conversations. */
|
|
88
|
+
size(): number;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Wrap a `ChatLaneTransport` so every OUTBOUND lifecycle message (stream /
|
|
92
|
+
* complete / requeue) is ALSO pushed to the loopback mirror socket after the
|
|
93
|
+
* authoritative transport call succeeds. This is a strict tee: the wrapper only
|
|
94
|
+
* reads the payloads the driver already sends to the DB path — it introduces no
|
|
95
|
+
* new mutation and no inbound path. With no mirror socket it is the identity
|
|
96
|
+
* wrapper. `claim` / `markStreaming` pass straight through (nothing to mirror).
|
|
97
|
+
*/
|
|
98
|
+
export declare function mirrorTee(transport: ChatLaneTransport, mirror: CompanionMirrorSocket | null | undefined): ChatLaneTransport;
|
|
99
|
+
export interface CompanionRunnerDeps {
|
|
100
|
+
/** This runner's id. */
|
|
101
|
+
runnerId: string;
|
|
102
|
+
/** The owner this companion is bound to (acc.users id). */
|
|
103
|
+
boundUserId: string;
|
|
104
|
+
/** The chat-conformant engine driving replies. */
|
|
105
|
+
engine: ConformantAgentEngine;
|
|
106
|
+
/** The chat-lane wire seam (claim/stream/complete/requeue). */
|
|
107
|
+
transport: ChatLaneTransport;
|
|
108
|
+
/** The chat-capability circuit breaker. */
|
|
109
|
+
breaker: ChatCircuitBreaker;
|
|
110
|
+
/** Owner-affinity conversation source (the served-conversation set). */
|
|
111
|
+
conversations: OwnerConversationSource;
|
|
112
|
+
/** Runner concurrency ceiling shared with the max-share budget. */
|
|
113
|
+
concurrencyLimit: number;
|
|
114
|
+
/** Chat-lane max share of that ceiling (0..1). */
|
|
115
|
+
maxShare: number;
|
|
116
|
+
/** Requested per-turn lease (ms). */
|
|
117
|
+
leaseMs: number;
|
|
118
|
+
/** The loopback mirror socket, or null when disabled. */
|
|
119
|
+
mirror?: CompanionMirrorSocket | null;
|
|
120
|
+
/** Broadcast an identity-topic event (companion_registered / capacity_changed / companion_lost). */
|
|
121
|
+
announce: (event: IdentityTopicEvent, payload: unknown) => Promise<void>;
|
|
122
|
+
/** Keep the runner identity online (typically acc.heartbeat_runner). */
|
|
123
|
+
heartbeat: () => Promise<void>;
|
|
124
|
+
/** Working directory forwarded to the engine per turn. */
|
|
125
|
+
cwd?: string;
|
|
126
|
+
/** Mint a per-turn delegation token (narrowly-scoped identity) — optional. */
|
|
127
|
+
mintDelegation?: (turn: ClaimedChatTurn) => Promise<string | undefined>;
|
|
128
|
+
/** Pump cadence (ms) between claim attempts. */
|
|
129
|
+
pumpMs?: number;
|
|
130
|
+
/** Heartbeat cadence (ms). */
|
|
131
|
+
heartbeatMs?: number;
|
|
132
|
+
/** Clock (test seam). */
|
|
133
|
+
now?: () => number;
|
|
134
|
+
/** Best-effort log sink. */
|
|
135
|
+
onLog?: (line: string) => void;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Drive companion mode: register the owner-bound identity, serve the owner's
|
|
139
|
+
* chat turns through a `ChatCompanion` (chat lane only — `activeCodeTasks` is
|
|
140
|
+
* always 0), tee the stream to the loopback mirror, heartbeat the identity
|
|
141
|
+
* online, and on stop requeue in-flight turns + flip the identity offline so an
|
|
142
|
+
* Auto turn fails over to the fleet.
|
|
143
|
+
*/
|
|
144
|
+
export declare class CompanionRunner {
|
|
145
|
+
private readonly deps;
|
|
146
|
+
private companion;
|
|
147
|
+
private heartbeatTimer;
|
|
148
|
+
private lastCapacity;
|
|
149
|
+
private stopped;
|
|
150
|
+
private readonly now;
|
|
151
|
+
constructor(deps: CompanionRunnerDeps);
|
|
152
|
+
/** Chat turns currently in flight on this companion. */
|
|
153
|
+
activeChatTurns(): number;
|
|
154
|
+
/** Register the owner-bound identity, start the pump + mirror + heartbeat. */
|
|
155
|
+
start(): Promise<void>;
|
|
156
|
+
/**
|
|
157
|
+
* Stop the companion: requeue in-flight turns (clean fail-over — the driver
|
|
158
|
+
* emits exactly one requeue, no duplicate completion), flip the identity
|
|
159
|
+
* offline so Auto routes to the fleet, and tear the mirror down.
|
|
160
|
+
*/
|
|
161
|
+
stop(): Promise<void>;
|
|
162
|
+
/** One heartbeat: keep the identity online + announce a capacity change. */
|
|
163
|
+
private beat;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Build the identity-topic broadcaster for a companion. Broadcasts on
|
|
167
|
+
* `identity:<userId>` via the runner's Supabase realtime client. Kept separate
|
|
168
|
+
* from the orchestrator so tests inject a recording fake instead.
|
|
169
|
+
*/
|
|
170
|
+
export declare function createIdentityBroadcaster(client: {
|
|
171
|
+
channel: (topic: string) => any;
|
|
172
|
+
removeChannel: (ch: any) => Promise<unknown>;
|
|
173
|
+
}, userId: string, opts?: {
|
|
174
|
+
onLog?: (line: string) => void;
|
|
175
|
+
}): {
|
|
176
|
+
announce: (event: IdentityTopicEvent, payload: unknown) => Promise<void>;
|
|
177
|
+
close: () => Promise<void>;
|
|
178
|
+
};
|
|
179
|
+
/** Re-export for callers that resolve chat-lane knobs alongside companion knobs. */
|
|
180
|
+
export { resolveChatLaneConfig };
|
|
181
|
+
//# sourceMappingURL=companion.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"companion.d.ts","sourceRoot":"","sources":["../src/companion.ts"],"names":[],"mappings":"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"}
|