@egoistmachines/opencode-switchboard 0.1.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.
@@ -0,0 +1,256 @@
1
+ import { PassportAuthError } from "./credentials.js";
2
+
3
+ // The host-neutral authenticated-request engine both plane surfaces share (the
4
+ // /agent/prefetch client in src/client.js and the tool-policy reporter in
5
+ // src/policy.js). Everything transport-shaped lives here ONCE: bearer
6
+ // headers, redirect refusal, per-class backoff, the forbidden
7
+ // probe-then-latch, the client-side request budget, the single 401
8
+ // refresh-retry, and terminal credential verdicts with their recheck window.
9
+ // The first version of the reporter re-implemented all of it, which meant the
10
+ // next transport fix had to land twice or the surfaces silently diverged.
11
+ //
12
+ // Each surface keeps its OWN transport instance on purpose. The backend
13
+ // throttles per route (prefetch 60/min, policy check 600/min, ...), so budget
14
+ // state must not be pooled: a chatty policy reporter must never spend the
15
+ // prefetch budget, and a prefetch 429 must not black out the audit trail.
16
+ // The cost is that a plane-closed 403 is probed once per surface per window,
17
+ // which is two requests where one would do; that is cheaper than the shared
18
+ // state machine that could tell those cases apart.
19
+
20
+ const BACKOFF_MS = {
21
+ // 429 is the backend telling us our own cadence is wrong; pause for at
22
+ // least one cache window so the next call is served from cache anyway.
23
+ rate_limited: 60_000,
24
+ // 403/404 mean the plane is closed to this install: the agent backend is
25
+ // disabled on that deployment (it ships behind a flag and Express answers
26
+ // its default 404 while it is dark), or this client is not an agent-connect
27
+ // registration. Nothing the plugin does will change that soon, so a REPEAT
28
+ // latches this long window. A single 403 can also be one transient store
29
+ // blip during the backend's token verification (it deliberately leaves a
30
+ // failed client lookup uncached so a blip costs one read, not a lockout),
31
+ // so the first one only pauses for `forbidden_probe` and the next read
32
+ // re-probes instead of amplifying the blip into a five-minute blackout.
33
+ forbidden: 5 * 60 * 1000,
34
+ forbidden_probe: 60_000,
35
+ // 400 means we sent a shape the backend refuses: a version skew, not a
36
+ // transient fault. Back off hard and say so once.
37
+ invalid_request: 5 * 60 * 1000,
38
+ unavailable: 30_000,
39
+ auth: 60_000,
40
+ };
41
+
42
+ // A terminal verdict (not installed, refresh token dead) stops network
43
+ // traffic, but the documented recovery is the owner rewriting the credentials
44
+ // file, and a long-running gateway must pick that up without a restart. Probe
45
+ // again after this window; an unchanged file re-latches at the cost of at
46
+ // most one stat (not_installed) or one token POST (invalid_grant) per window.
47
+ const TERMINAL_RECHECK_MS = 5 * 60 * 1000;
48
+
49
+ export function createPlaneTransport({
50
+ config,
51
+ credentials,
52
+ fetchImpl = globalThis.fetch,
53
+ logger = null,
54
+ now = () => Date.now(),
55
+ // Names this surface in the once-per-window warnings, so an operator
56
+ // grepping a log can tell a prefetch outage from a policy one.
57
+ label,
58
+ // Client-side share of this surface's per-route backend budget.
59
+ budgetMax,
60
+ budgetWindowMs = 60_000,
61
+ }) {
62
+ let backoffUntil = 0;
63
+ let backoffReason = null;
64
+ // Consecutive plane-closed answers, so one transient 403 (a store blip on
65
+ // the backend's verification path) is probed past quickly while a genuinely
66
+ // closed plane still converges to the long backoff on its second answer.
67
+ let forbiddenStreak = 0;
68
+ let terminalReason = null;
69
+ let terminalRecheckAt = 0;
70
+ const requestLog = [];
71
+ const pendingReservations = new Set();
72
+
73
+ const pruneRequestLog = () => {
74
+ const cutoff = now() - budgetWindowMs;
75
+ while (requestLog.length && requestLog[0] <= cutoff) requestLog.shift();
76
+ };
77
+
78
+ // Reserve before credential loading yields. Pending reservations do not
79
+ // expire, because a slow credential provider must not let later callers
80
+ // take the same slots and then release a burst larger than the route budget.
81
+ const reserveRequest = () => {
82
+ pruneRequestLog();
83
+ if (requestLog.length + pendingReservations.size >= budgetMax) return null;
84
+ const reservation = {};
85
+ pendingReservations.add(reservation);
86
+ return reservation;
87
+ };
88
+
89
+ const startRequest = (reservation) => {
90
+ pendingReservations.delete(reservation);
91
+ requestLog.push(now());
92
+ };
93
+
94
+ const releaseRequest = (reservation) => {
95
+ pendingReservations.delete(reservation);
96
+ };
97
+
98
+ const backOff = (reason, detail, ms = BACKOFF_MS[reason] ?? BACKOFF_MS.unavailable) => {
99
+ const alreadyBackedOff = backoffUntil > now() && backoffReason === reason;
100
+ backoffUntil = now() + ms;
101
+ backoffReason = reason;
102
+ if (!alreadyBackedOff) logger?.warn?.(`ai-passport: ${label} ${reason}${detail ? ` (${detail})` : ""}`);
103
+ };
104
+
105
+ const latchTerminal = (err) => {
106
+ terminalReason = err.code;
107
+ terminalRecheckAt = now() + TERMINAL_RECHECK_MS;
108
+ // Log the closed reason code, not an error message that may have been
109
+ // created by a filesystem implementation or another credential provider.
110
+ logger?.warn?.(`ai-passport: credentials ${err.code}`);
111
+ };
112
+
113
+ const requestOnce = async ({ baseUrl, path, method, accessToken, body, timeoutMs }) => {
114
+ const response = await fetchImpl(`${baseUrl}${path}`, {
115
+ method,
116
+ headers: {
117
+ authorization: `Bearer ${accessToken}`,
118
+ accept: "application/json",
119
+ ...(body ? { "content-type": "application/json" } : {}),
120
+ },
121
+ ...(body ? { body: JSON.stringify(body) } : {}),
122
+ // Never follow a redirect while carrying a bearer: no Passport endpoint
123
+ // answers one, so a 3xx is a middlebox (captive portal, proxy)
124
+ // answering in the backend's place, and following it hands the
125
+ // credential to whatever host it points at. The 3xx status falls
126
+ // through to the generic branch below and degrades like any outage.
127
+ redirect: "manual",
128
+ signal: AbortSignal.timeout(timeoutMs),
129
+ });
130
+ const text = await response.text();
131
+ let payload = null;
132
+ let malformed = false;
133
+ try {
134
+ payload = text ? JSON.parse(text) : null;
135
+ } catch {
136
+ malformed = true;
137
+ }
138
+ return { status: response.status, payload, malformed };
139
+ };
140
+
141
+ return {
142
+ /**
143
+ * One authenticated request. Resolves to the 200's JSON object, or null
144
+ * when nothing was knowable (terminal, backoff, budget, outage, refusal);
145
+ * the failure class has already been recorded and warned about here, so
146
+ * callers only decide what "no answer" means for their surface. Never
147
+ * throws.
148
+ */
149
+ async request({ path, method = "POST", body = null, timeoutMs }) {
150
+ if (terminalReason) {
151
+ if (now() < terminalRecheckAt) return null;
152
+ // Clear the verdict and fall through: the credentials layer re-reads
153
+ // the file on mtime change, so a reinstall is picked up here.
154
+ terminalReason = null;
155
+ }
156
+ if (backoffUntil > now()) return null;
157
+ const initialReservation = reserveRequest();
158
+ if (!initialReservation) return null;
159
+
160
+ let baseUrl;
161
+ let accessToken;
162
+ try {
163
+ baseUrl = config.baseUrl ?? (await credentials.baseUrl());
164
+ accessToken = await credentials.accessToken();
165
+ } catch (err) {
166
+ releaseRequest(initialReservation);
167
+ if (err instanceof PassportAuthError && err.terminal) {
168
+ // Not installed, or the refresh token is genuinely dead. Say it
169
+ // once and stop touching the network until the recheck window
170
+ // probes for the owner's reinstall.
171
+ latchTerminal(err);
172
+ return null;
173
+ }
174
+ backOff("auth", err?.code ?? "error");
175
+ return null;
176
+ }
177
+
178
+ let result;
179
+ let retried = false;
180
+ let retryReservation = null;
181
+ let retryStarted = false;
182
+ try {
183
+ startRequest(initialReservation);
184
+ result = await requestOnce({ baseUrl, path, method, accessToken, body, timeoutMs });
185
+ if (result.status === 401) {
186
+ // The stored access token outlived its hour, or the server rotated
187
+ // out from under us. One forced refresh, one retry, then give up
188
+ // for this window; the refresh path itself owns the terminal cases.
189
+ retryReservation = reserveRequest();
190
+ if (!retryReservation) return null;
191
+ retried = true;
192
+ const refreshed = await credentials.accessToken({ force: true });
193
+ startRequest(retryReservation);
194
+ retryStarted = true;
195
+ result = await requestOnce({ baseUrl, path, method, accessToken: refreshed, body, timeoutMs });
196
+ }
197
+ } catch (err) {
198
+ if (retryReservation && !retryStarted) releaseRequest(retryReservation);
199
+ if (err instanceof PassportAuthError && err.terminal) {
200
+ latchTerminal(err);
201
+ return null;
202
+ }
203
+ // AbortSignal.timeout rejects with TimeoutError; treat every
204
+ // transport failure the same way, because the caller's answer is the
205
+ // same.
206
+ backOff("unavailable", err?.name ?? "error");
207
+ return null;
208
+ }
209
+
210
+ if (result.status === 200) {
211
+ if (result.malformed || !result.payload || typeof result.payload !== "object" || Array.isArray(result.payload)) {
212
+ // A 200 whose body is not the plane's JSON object is a middlebox
213
+ // answering in the backend's place (captive portal, proxy error
214
+ // page), not an answer. Treating it as one would have the surface
215
+ // cache a lie; degrade like any other transport fault instead.
216
+ backOff("unavailable", "malformed_200");
217
+ return null;
218
+ }
219
+ backoffUntil = 0;
220
+ backoffReason = null;
221
+ forbiddenStreak = 0;
222
+ return result.payload;
223
+ }
224
+ if (result.status === 429) {
225
+ backOff("rate_limited");
226
+ return null;
227
+ }
228
+ if (result.status === 403 || result.status === 404) {
229
+ // 403 is the scope guard refusing this install; 404 is a deployment
230
+ // that never mounted the plane. Both mean the same thing to the
231
+ // owner, and neither is an outage.
232
+ forbiddenStreak += 1;
233
+ backOff(
234
+ "forbidden",
235
+ `${result.status}: ask the owner to enable the AI Passport agent backend`,
236
+ forbiddenStreak > 1 ? BACKOFF_MS.forbidden : BACKOFF_MS.forbidden_probe
237
+ );
238
+ return null;
239
+ }
240
+ if (result.status === 401) {
241
+ backOff("auth", retried ? "401 after refresh" : "401");
242
+ return null;
243
+ }
244
+ if (result.status === 400) {
245
+ backOff("invalid_request", String(result.payload?.error ?? "bad_request"));
246
+ return null;
247
+ }
248
+ backOff("unavailable", String(result.status));
249
+ return null;
250
+ },
251
+
252
+ state() {
253
+ return { backoffUntil, backoffReason, terminalReason };
254
+ },
255
+ };
256
+ }
package/src/status.js ADDED
@@ -0,0 +1,186 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+
5
+ import { GOVERNED_CATEGORIES } from "./config.js";
6
+ import { PROPOSAL_STATUSES, READ_STATUSES } from "./outcomes.js";
7
+
8
+ const EMPTY_COUNTS = Object.freeze({ rows: 0, skippedCategories: 0 });
9
+ const EMPTY_HANDOFF_COUNTS = Object.freeze({ claimed: 0, none: 0, disabled: 0, deliveryFailed: 0 });
10
+ const HANDOFF_REASONS = new Set(["missing_session_id", "delivery_failed"]);
11
+
12
+ async function writeStatusFile(filePath, value) {
13
+ await mkdir(path.dirname(filePath), { recursive: true });
14
+ const tempPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
15
+ try {
16
+ await writeFile(tempPath, `${JSON.stringify(value)}\n`, { mode: 0o600 });
17
+ await chmod(tempPath, 0o600);
18
+ await rename(tempPath, filePath);
19
+ } finally {
20
+ await rm(tempPath, { force: true }).catch(() => {});
21
+ }
22
+ }
23
+
24
+ export async function readStatusFile(filePath) {
25
+ try {
26
+ const value = JSON.parse(await readFile(filePath, "utf8"));
27
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
28
+ } catch {
29
+ return null;
30
+ }
31
+ }
32
+
33
+ export function createStatusTracker({
34
+ filePath,
35
+ config,
36
+ ambientSupported = true,
37
+ transportKind = "local",
38
+ handoffEnabled = config.handoff.enabled && transportKind === "local",
39
+ writeStatus = writeStatusFile,
40
+ }) {
41
+ const activeMode = ["local", "hosted", "unavailable"].includes(transportKind) ? transportKind : "unavailable";
42
+ let state = {
43
+ ambient: { enabled: config.ambient.enabled, supported: ambientSupported },
44
+ handoff: {
45
+ enabled: handoffEnabled,
46
+ lastReason: null,
47
+ counts: { ...EMPTY_HANDOFF_COUNTS, disabled: handoffEnabled ? 0 : 1 },
48
+ },
49
+ categoriesRequested: [...config.categories],
50
+ lastOutcomeClass: null,
51
+ transportKind: activeMode,
52
+ counts: { ...EMPTY_COUNTS },
53
+ };
54
+ let pending = Promise.resolve();
55
+
56
+ const persist = () => {
57
+ pending = pending.then(() => writeStatus(filePath, state)).catch(() => {});
58
+ return pending;
59
+ };
60
+ persist();
61
+
62
+ return {
63
+ recordOutcome(outcome) {
64
+ if (!outcome || typeof outcome.status !== "string") return;
65
+ state = {
66
+ ...state,
67
+ lastOutcomeClass: outcome.status,
68
+ counts: {
69
+ rows: Array.isArray(outcome.rows) ? outcome.rows.length : 0,
70
+ skippedCategories: Array.isArray(outcome.skipped_categories) ? outcome.skipped_categories.length : 0,
71
+ },
72
+ };
73
+ return persist();
74
+ },
75
+ recordUnavailable() {
76
+ state = {
77
+ ...state,
78
+ lastOutcomeClass: "unavailable",
79
+ counts: { ...EMPTY_COUNTS },
80
+ };
81
+ return persist();
82
+ },
83
+ recordHandoffOutcome(outcome) {
84
+ if (outcome !== "claimed" && outcome !== "none") return;
85
+ state = {
86
+ ...state,
87
+ handoff: {
88
+ ...state.handoff,
89
+ counts: { ...state.handoff.counts, [outcome]: state.handoff.counts[outcome] + 1 },
90
+ },
91
+ };
92
+ return persist();
93
+ },
94
+ recordHandoffReason(reason) {
95
+ if (!HANDOFF_REASONS.has(reason)) return;
96
+ state = { ...state, handoff: { ...state.handoff, lastReason: reason } };
97
+ return persist();
98
+ },
99
+ recordHandoffDeliveryFailure() {
100
+ state = {
101
+ ...state,
102
+ handoff: {
103
+ ...state.handoff,
104
+ lastReason: "delivery_failed",
105
+ counts: { ...state.handoff.counts, deliveryFailed: state.handoff.counts.deliveryFailed + 1 },
106
+ },
107
+ };
108
+ return persist();
109
+ },
110
+ setAmbientSupported(supported) {
111
+ state = { ...state, ambient: { ...state.ambient, supported: Boolean(supported) } };
112
+ return persist();
113
+ },
114
+ snapshot() {
115
+ return structuredClone(state);
116
+ },
117
+ settled() {
118
+ return pending;
119
+ },
120
+ };
121
+ }
122
+
123
+ export function buildStatusReport({ config, transportStatus, persisted }) {
124
+ const knownOutcomes = new Set([...READ_STATUSES, ...PROPOSAL_STATUSES]);
125
+ const persistedOutcome = knownOutcomes.has(persisted?.lastOutcomeClass) ? persisted.lastOutcomeClass : null;
126
+ const transportOutcome = knownOutcomes.has(transportStatus?.lastOutcome) ? transportStatus.lastOutcome : null;
127
+ const activeMode = ["local", "hosted", "unavailable"].includes(persisted?.transportKind)
128
+ ? persisted.transportKind
129
+ : "unavailable";
130
+ const wouldSelectOnRestart = transportStatus?.discoveryFound
131
+ ? "local"
132
+ : config.hostedFallback.enabled
133
+ ? "hosted"
134
+ : "unavailable";
135
+ const handoffEnabled = persisted?.handoff?.enabled ?? (config.handoff.enabled && activeMode === "local");
136
+ const handoffCount = (name, fallback = 0) => {
137
+ const value = persisted?.handoff?.counts?.[name];
138
+ return Number.isInteger(value) && value >= 0 ? value : fallback;
139
+ };
140
+ return {
141
+ transportKind: activeMode,
142
+ wouldSelectOnRestart,
143
+ discoveryRecord: transportStatus?.discoveryFound ? "found" : "missing",
144
+ paired: Boolean(transportStatus?.paired),
145
+ ambient: {
146
+ enabled: persisted?.ambient?.enabled ?? config.ambient.enabled,
147
+ supported: persisted?.ambient?.supported ?? true,
148
+ },
149
+ handoff: {
150
+ enabled: handoffEnabled,
151
+ lastReason: HANDOFF_REASONS.has(persisted?.handoff?.lastReason) ? persisted.handoff.lastReason : null,
152
+ counts: {
153
+ claimed: handoffCount("claimed"),
154
+ none: handoffCount("none"),
155
+ disabled: handoffCount("disabled", handoffEnabled ? 0 : 1),
156
+ deliveryFailed: handoffCount("deliveryFailed"),
157
+ },
158
+ },
159
+ categoriesRequested: Array.isArray(persisted?.categoriesRequested)
160
+ ? persisted.categoriesRequested.filter((category) => GOVERNED_CATEGORIES.includes(category))
161
+ : [...config.categories],
162
+ lastOutcomeClass: persistedOutcome ?? transportOutcome,
163
+ counts: {
164
+ rows: Number.isInteger(persisted?.counts?.rows) && persisted.counts.rows >= 0 ? persisted.counts.rows : 0,
165
+ skippedCategories: Number.isInteger(persisted?.counts?.skippedCategories) && persisted.counts.skippedCategories >= 0
166
+ ? persisted.counts.skippedCategories
167
+ : 0,
168
+ },
169
+ };
170
+ }
171
+
172
+ export function formatStatusReport(report) {
173
+ return [
174
+ `active transport: ${report.transportKind}`,
175
+ `would select on restart: ${report.wouldSelectOnRestart}`,
176
+ `discovery record: ${report.discoveryRecord}`,
177
+ `paired: ${report.paired ? "yes" : "no"}`,
178
+ `ambient: ${report.ambient.enabled ? "on" : "off"}, ${report.ambient.supported ? "supported" : "unsupported"}`,
179
+ `hand-offs: ${report.handoff.enabled ? "on" : "off"}; claimed=${report.handoff.counts.claimed}, ` +
180
+ `none=${report.handoff.counts.none}, disabled=${report.handoff.counts.disabled}, ` +
181
+ `delivery_failed=${report.handoff.counts.deliveryFailed}, reason=${report.handoff.lastReason ?? "none"}`,
182
+ `categories requested: ${report.categoriesRequested.join(", ")}`,
183
+ `last outcome: ${report.lastOutcomeClass ?? "none"}`,
184
+ `counts: rows=${report.counts.rows}, skipped_categories=${report.counts.skippedCategories}`,
185
+ ].join("\n");
186
+ }