wicked-crew 0.4.0 → 0.6.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/dist/api/api-prefix.d.ts +13 -0
- package/dist/api/api-prefix.d.ts.map +1 -0
- package/dist/api/api-prefix.js +13 -0
- package/dist/api/api-prefix.js.map +1 -0
- package/dist/api/audit.d.ts +57 -0
- package/dist/api/audit.d.ts.map +1 -0
- package/dist/api/audit.js +115 -0
- package/dist/api/audit.js.map +1 -0
- package/dist/api/auth.d.ts +192 -0
- package/dist/api/auth.d.ts.map +1 -0
- package/dist/api/auth.js +515 -0
- package/dist/api/auth.js.map +1 -0
- package/dist/api/evidence.d.ts +11 -0
- package/dist/api/evidence.d.ts.map +1 -1
- package/dist/api/evidence.js +8 -3
- package/dist/api/evidence.js.map +1 -1
- package/dist/api/gate-cache.d.ts +8 -0
- package/dist/api/gate-cache.d.ts.map +1 -1
- package/dist/api/gate-cache.js +10 -0
- package/dist/api/gate-cache.js.map +1 -1
- package/dist/api/open-path.d.ts +18 -0
- package/dist/api/open-path.d.ts.map +1 -0
- package/dist/api/open-path.js +79 -0
- package/dist/api/open-path.js.map +1 -0
- package/dist/api/routes.d.ts +113 -1
- package/dist/api/routes.d.ts.map +1 -1
- package/dist/api/routes.js +516 -27
- package/dist/api/routes.js.map +1 -1
- package/dist/api/seat-health.d.ts +55 -0
- package/dist/api/seat-health.d.ts.map +1 -0
- package/dist/api/seat-health.js +273 -0
- package/dist/api/seat-health.js.map +1 -0
- package/dist/api/seat-signin.d.ts +27 -0
- package/dist/api/seat-signin.d.ts.map +1 -0
- package/dist/api/seat-signin.js +143 -0
- package/dist/api/seat-signin.js.map +1 -0
- package/dist/api/server.d.ts +111 -3
- package/dist/api/server.d.ts.map +1 -1
- package/dist/api/server.js +236 -11
- package/dist/api/server.js.map +1 -1
- package/dist/api/unit-output.d.ts +62 -0
- package/dist/api/unit-output.d.ts.map +1 -0
- package/dist/api/unit-output.js +108 -0
- package/dist/api/unit-output.js.map +1 -0
- package/dist/cli/index.js +97 -3
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/mcp.d.ts +14 -0
- package/dist/cli/mcp.d.ts.map +1 -0
- package/dist/cli/mcp.js +119 -0
- package/dist/cli/mcp.js.map +1 -0
- package/dist/core/adapter.d.ts +106 -1
- package/dist/core/adapter.d.ts.map +1 -1
- package/dist/core/adapter.js +293 -12
- package/dist/core/adapter.js.map +1 -1
- package/dist/core/bridge-path.d.ts +17 -6
- package/dist/core/bridge-path.d.ts.map +1 -1
- package/dist/core/bridge-path.js +17 -6
- package/dist/core/bridge-path.js.map +1 -1
- package/dist/core/types.d.ts +23 -352
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +11 -4
- package/dist/core/types.js.map +1 -1
- package/dist/interactive/bridge-pool.d.ts +71 -0
- package/dist/interactive/bridge-pool.d.ts.map +1 -0
- package/dist/interactive/bridge-pool.js +187 -0
- package/dist/interactive/bridge-pool.js.map +1 -0
- package/dist/interactive/bridge-root.d.ts +36 -0
- package/dist/interactive/bridge-root.d.ts.map +1 -0
- package/dist/interactive/bridge-root.js +48 -0
- package/dist/interactive/bridge-root.js.map +1 -0
- package/dist/interactive/draft-events.d.ts +144 -0
- package/dist/interactive/draft-events.d.ts.map +1 -0
- package/dist/interactive/draft-events.js +490 -0
- package/dist/interactive/draft-events.js.map +1 -0
- package/dist/interactive/edit-events.d.ts +172 -0
- package/dist/interactive/edit-events.d.ts.map +1 -0
- package/dist/interactive/edit-events.js +530 -0
- package/dist/interactive/edit-events.js.map +1 -0
- package/dist/interactive/ledger.d.ts +39 -0
- package/dist/interactive/ledger.d.ts.map +1 -0
- package/dist/interactive/ledger.js +93 -0
- package/dist/interactive/ledger.js.map +1 -0
- package/dist/interactive/proxy-routes.d.ts +39 -0
- package/dist/interactive/proxy-routes.d.ts.map +1 -0
- package/dist/interactive/proxy-routes.js +189 -0
- package/dist/interactive/proxy-routes.js.map +1 -0
- package/dist/interactive/ws-relay.d.ts +85 -0
- package/dist/interactive/ws-relay.d.ts.map +1 -0
- package/dist/interactive/ws-relay.js +191 -0
- package/dist/interactive/ws-relay.js.map +1 -0
- package/dist/projects/activity.d.ts +29 -0
- package/dist/projects/activity.d.ts.map +1 -0
- package/dist/projects/activity.js +172 -0
- package/dist/projects/activity.js.map +1 -0
- package/dist/projects/charter.d.ts +28 -0
- package/dist/projects/charter.d.ts.map +1 -0
- package/dist/projects/charter.js +53 -0
- package/dist/projects/charter.js.map +1 -0
- package/dist/projects/events.d.ts +55 -0
- package/dist/projects/events.d.ts.map +1 -0
- package/dist/projects/events.js +141 -0
- package/dist/projects/events.js.map +1 -0
- package/dist/projects/membership-index.d.ts +25 -0
- package/dist/projects/membership-index.d.ts.map +1 -0
- package/dist/projects/membership-index.js +46 -0
- package/dist/projects/membership-index.js.map +1 -0
- package/dist/projects/routes.d.ts +90 -0
- package/dist/projects/routes.d.ts.map +1 -0
- package/dist/projects/routes.js +388 -0
- package/dist/projects/routes.js.map +1 -0
- package/dist/projects/settings.d.ts +32 -0
- package/dist/projects/settings.d.ts.map +1 -0
- package/dist/projects/settings.js +64 -0
- package/dist/projects/settings.js.map +1 -0
- package/dist/qe/acceptance.d.ts +137 -0
- package/dist/qe/acceptance.d.ts.map +1 -0
- package/dist/qe/acceptance.js +249 -0
- package/dist/qe/acceptance.js.map +1 -0
- package/dist/qe/gate-events.d.ts +111 -0
- package/dist/qe/gate-events.d.ts.map +1 -0
- package/dist/qe/gate-events.js +168 -0
- package/dist/qe/gate-events.js.map +1 -0
- package/dist/qe/ledger.d.ts +100 -0
- package/dist/qe/ledger.d.ts.map +1 -0
- package/dist/qe/ledger.js +154 -0
- package/dist/qe/ledger.js.map +1 -0
- package/dist/studio/assets/index-CCwXa1cn.js +428 -0
- package/dist/studio/assets/index-HWxo0h41.css +32 -0
- package/dist/studio/index.html +2 -2
- package/package.json +10 -4
- package/dist/studio/assets/index-Ci_R6ARr.js +0 -423
- package/dist/studio/assets/index-Dio_c1Q3.css +0 -32
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Injectable IO for tests: `home` replaces `os.homedir()` and `env` replaces `process.env`,
|
|
3
|
+
* so fixture dirs built with `mkdtemp` exercise every branch without touching the developer's
|
|
4
|
+
* real dotfiles. Production callers omit it.
|
|
5
|
+
*/
|
|
6
|
+
export interface SigninProbeIo {
|
|
7
|
+
home?: string;
|
|
8
|
+
env?: Record<string, string | undefined>;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Cheap signed-in presence for one seat. `workerConfigRoot` is the settings-applied
|
|
12
|
+
* `WICKED_WORKER_HOME` value when set (the roster route passes the live env, which is exactly
|
|
13
|
+
* what the engine will read at the next spawn); omitted = the engine default `~/.wicked-worker`.
|
|
14
|
+
*
|
|
15
|
+
* Never throws: an unreadable file or dir is treated as the corresponding "not observable"
|
|
16
|
+
* answer for that seat's rule, never a 500 on the roster route.
|
|
17
|
+
*/
|
|
18
|
+
export declare function signedInHeuristic(seatKey: string, workerConfigRoot?: string, io?: SigninProbeIo): boolean | null;
|
|
19
|
+
/**
|
|
20
|
+
* Apply the persisted `worker_config_root` setting to THIS process's environment. The engine
|
|
21
|
+
* reads `WICKED_WORKER_HOME` per worker spawn (acp_runner.rs — never cached at engine start),
|
|
22
|
+
* so calling this at daemon boot and again on every settings change is sufficient: the next
|
|
23
|
+
* spawn sees the new root with no daemon or engine restart. `settings.json` is the source of
|
|
24
|
+
* truth — unset/empty DELETES the env, restoring the engine default `~/.wicked-worker`.
|
|
25
|
+
*/
|
|
26
|
+
export declare function applyWorkerConfigRoot(root: string | undefined): void;
|
|
27
|
+
//# sourceMappingURL=seat-signin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seat-signin.d.ts","sourceRoot":"","sources":["../../src/api/seat-signin.ts"],"names":[],"mappings":"AAkCA;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;CAC1C;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EACf,gBAAgB,CAAC,EAAE,MAAM,EACzB,EAAE,GAAE,aAAkB,GACrB,OAAO,GAAG,IAAI,CA2FhB;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAMpE"}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// Seat sign-in presence (seat sign-in): does each council seat LOOK signed in?
|
|
2
|
+
//
|
|
3
|
+
// The doctrine mirrors seat health (crew#274): auth state is RUNTIME state the platform observes
|
|
4
|
+
// and displays, never config an operator hand-edits. The studio surfaces every seat's auth
|
|
5
|
+
// uniformly — `signed_in` says what the daemon can OBSERVE, and the seat's `login_invocation`
|
|
6
|
+
// (engine roster passthrough, wicked-core PR#278) is what the studio runs in a PTY terminal
|
|
7
|
+
// (`POST /terminals`) to fix it.
|
|
8
|
+
//
|
|
9
|
+
// This module is a HEURISTIC, deliberately cheap:
|
|
10
|
+
//
|
|
11
|
+
// - File/env PRESENCE only — no process is ever spawned (the roster route calls this per seat
|
|
12
|
+
// per request; a spawn here would be a fork bomb with a UI attached).
|
|
13
|
+
// - Three-valued: `true`/`false` when the observable half of the seat's auth state answers,
|
|
14
|
+
// `null` when it is unknowable cheaply. Keychain-backed credentials are the canonical `null`:
|
|
15
|
+
// a file can prove a login HAPPENED, but its absence proves nothing when the secret lives in
|
|
16
|
+
// the OS keychain — and `false` would send an operator to re-login a working seat.
|
|
17
|
+
// - `true` is "a credential artifact exists", NOT "the credential still works". An expired
|
|
18
|
+
// OAuth token reads `true` here; seat HEALTH (crew#274) is what catches it failing live.
|
|
19
|
+
//
|
|
20
|
+
// Per-seat rules (each documented at its branch):
|
|
21
|
+
// claude — the WORKER config home's `.claude.json` (per-config-dir keychain entry means the
|
|
22
|
+
// file is the observable half; see below), under `WICKED_WORKER_HOME`/the default.
|
|
23
|
+
// codex — `~/.codex/auth.json` presence.
|
|
24
|
+
// copilot — env token (COPILOT_GITHUB_TOKEN/GH_TOKEN/GITHUB_TOKEN) → true; installed but no
|
|
25
|
+
// env → null (keychain state unknowable cheaply — NOT false); no trace → false.
|
|
26
|
+
// opencode — `~/.local/share/opencode/auth.json` presence.
|
|
27
|
+
// pi — `~/.pi/agent/auth.json` presence.
|
|
28
|
+
// agy — any `.json` under `~/.antigravitycli/` → true; else null (keyring unknowable).
|
|
29
|
+
// unknown — null (a seat this module has no rule for is exactly "unknown").
|
|
30
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
31
|
+
import { homedir } from 'node:os';
|
|
32
|
+
import { join } from 'node:path';
|
|
33
|
+
/**
|
|
34
|
+
* Cheap signed-in presence for one seat. `workerConfigRoot` is the settings-applied
|
|
35
|
+
* `WICKED_WORKER_HOME` value when set (the roster route passes the live env, which is exactly
|
|
36
|
+
* what the engine will read at the next spawn); omitted = the engine default `~/.wicked-worker`.
|
|
37
|
+
*
|
|
38
|
+
* Never throws: an unreadable file or dir is treated as the corresponding "not observable"
|
|
39
|
+
* answer for that seat's rule, never a 500 on the roster route.
|
|
40
|
+
*/
|
|
41
|
+
export function signedInHeuristic(seatKey, workerConfigRoot, io = {}) {
|
|
42
|
+
const home = io.home ?? homedir();
|
|
43
|
+
const env = io.env ?? process.env;
|
|
44
|
+
switch (seatKey) {
|
|
45
|
+
case 'claude': {
|
|
46
|
+
// The engine places the claude WORKER home at `<root>/claude` (acp_runner.rs
|
|
47
|
+
// claude_worker_home; root = WICKED_WORKER_HOME or ~/.wicked-worker). Claude keeps the
|
|
48
|
+
// OAuth token itself in a PER-CONFIG-DIR keychain entry, so the token is not observable —
|
|
49
|
+
// but `/login` also writes an `oauthAccount` block into that dir's `.claude.json`, which
|
|
50
|
+
// is: present-with-key means a login completed for the WORKER home (not the operator's
|
|
51
|
+
// own ~/.claude). Heuristic by construction — a revoked token still reads true.
|
|
52
|
+
const root = workerConfigRoot !== undefined && workerConfigRoot !== ''
|
|
53
|
+
? workerConfigRoot
|
|
54
|
+
: join(home, '.wicked-worker');
|
|
55
|
+
const file = join(root, 'claude', '.claude.json');
|
|
56
|
+
try {
|
|
57
|
+
const parsed = JSON.parse(readFileSync(file, 'utf8'));
|
|
58
|
+
return typeof parsed === 'object' && parsed !== null && 'oauthAccount' in parsed;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return false; // missing / unreadable / malformed: no completed login is observable
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
case 'codex':
|
|
65
|
+
// `codex login` writes ~/.codex/auth.json (tokens live IN the file — presence is the state).
|
|
66
|
+
return existsSync(join(home, '.codex', 'auth.json'));
|
|
67
|
+
case 'copilot': {
|
|
68
|
+
// Env token wins: copilot honors these directly, and a set token IS a signed-in seat.
|
|
69
|
+
for (const key of ['COPILOT_GITHUB_TOKEN', 'GH_TOKEN', 'GITHUB_TOKEN']) {
|
|
70
|
+
const v = env[key];
|
|
71
|
+
if (typeof v === 'string' && v !== '')
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
// `copilot login` stores the TOKEN in the OS keychain, but it also records the logged-in
|
|
75
|
+
// USER in config.json (`lastLoggedInUser` / `loggedInUsers`) — verified in the field: a
|
|
76
|
+
// successful sign-in left the chip on null-neutral and the Sign in button up, reading as
|
|
77
|
+
// "didn't work". The file is JSONC (comment header), so this parses key-presence
|
|
78
|
+
// leniently rather than JSON.parse-ing the whole document.
|
|
79
|
+
const cfg = join(home, '.copilot', 'config.json');
|
|
80
|
+
if (existsSync(cfg)) {
|
|
81
|
+
try {
|
|
82
|
+
const raw = readFileSync(cfg, 'utf8');
|
|
83
|
+
// A non-empty STRING user, or an array whose first entry is a non-empty string —
|
|
84
|
+
// `[""]` must not read as signed in (Copilot, PR#282).
|
|
85
|
+
// Both shapes seen in the field: a plain string user, OR an OBJECT user
|
|
86
|
+
// ({host, login} — the live config on macOS). A non-empty "login" string inside
|
|
87
|
+
// either container is the common observable.
|
|
88
|
+
if (/"lastLoggedInUser"\s*:\s*"[^"]+"/.test(raw) ||
|
|
89
|
+
/"loggedInUsers"\s*:\s*\[\s*"[^"]+"/.test(raw) ||
|
|
90
|
+
// Object shapes, SCOPED to the user containers (an unrelated "login" string
|
|
91
|
+
// elsewhere in the config must not read as signed in — Copilot, PR#283).
|
|
92
|
+
/"lastLoggedInUser"\s*:\s*\{[^}]*"login"\s*:\s*"[^"]+"/.test(raw) ||
|
|
93
|
+
/"loggedInUsers"\s*:\s*\[\s*\{[^}]*"login"\s*:\s*"[^"]+"/.test(raw)) {
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
/* unreadable config falls through to null */
|
|
99
|
+
}
|
|
100
|
+
// Installed but no recorded user and no env: the keychain half stays unknowable.
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
return false; // no env, no config dir: nothing suggests this seat ever signed in
|
|
104
|
+
}
|
|
105
|
+
case 'opencode':
|
|
106
|
+
// `opencode auth login` writes the credential file itself — presence is the state.
|
|
107
|
+
return existsSync(join(home, '.local', 'share', 'opencode', 'auth.json'));
|
|
108
|
+
case 'pi':
|
|
109
|
+
// pi's auth flow writes ~/.pi/agent/auth.json — presence is the state.
|
|
110
|
+
return existsSync(join(home, '.pi', 'agent', 'auth.json'));
|
|
111
|
+
case 'agy': {
|
|
112
|
+
// Antigravity keeps its credential in the OS keyring; a `.json` in ~/.antigravitycli/ is
|
|
113
|
+
// the observable artifact a completed login leaves behind. Missing dir OR dir-with-no-json
|
|
114
|
+
// both mean the keyring state is unknowable cheaply → null (only a json upgrades to true).
|
|
115
|
+
let entries;
|
|
116
|
+
try {
|
|
117
|
+
entries = readdirSync(join(home, '.antigravitycli'));
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
return entries.some((f) => f.endsWith('.json')) ? true : null;
|
|
123
|
+
}
|
|
124
|
+
default:
|
|
125
|
+
return null; // a seat with no rule here is exactly "unknown"
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Apply the persisted `worker_config_root` setting to THIS process's environment. The engine
|
|
130
|
+
* reads `WICKED_WORKER_HOME` per worker spawn (acp_runner.rs — never cached at engine start),
|
|
131
|
+
* so calling this at daemon boot and again on every settings change is sufficient: the next
|
|
132
|
+
* spawn sees the new root with no daemon or engine restart. `settings.json` is the source of
|
|
133
|
+
* truth — unset/empty DELETES the env, restoring the engine default `~/.wicked-worker`.
|
|
134
|
+
*/
|
|
135
|
+
export function applyWorkerConfigRoot(root) {
|
|
136
|
+
if (typeof root === 'string' && root !== '') {
|
|
137
|
+
process.env['WICKED_WORKER_HOME'] = root;
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
delete process.env['WICKED_WORKER_HOME'];
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=seat-signin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seat-signin.js","sourceRoot":"","sources":["../../src/api/seat-signin.ts"],"names":[],"mappings":"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"}
|
package/dist/api/server.d.ts
CHANGED
|
@@ -1,14 +1,122 @@
|
|
|
1
1
|
import Fastify from 'fastify';
|
|
2
|
-
import type
|
|
2
|
+
import { type AuthOptions } from './auth.js';
|
|
3
|
+
import { CoreAdapter } from '../core/adapter.js';
|
|
3
4
|
/**
|
|
4
5
|
* The bundled studio SPA lives at `dist/studio` next to the compiled server
|
|
5
6
|
* (`dist/api/server.js` → `../studio`). See DES-STUDIO-SERVING-001 §2.3/§3.1
|
|
6
|
-
* and `scripts/bundle-studio.mjs
|
|
7
|
+
* and `scripts/bundle-studio.mjs`, which copies the installed `wicked-studio`
|
|
8
|
+
* package's `dist/` there (the SPA is its own product since the #98 carve —
|
|
9
|
+
* github.com/mikeparcewski/wicked-studio — consumed as a dist artifact).
|
|
7
10
|
*/
|
|
8
11
|
export declare function defaultStudioRoot(): string;
|
|
9
12
|
export interface CreateServerOptions {
|
|
10
13
|
/** Override the studio asset root (tests point this at a temp fixture dir). */
|
|
11
14
|
studioRoot?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Opt-in QE gate-event consumption over wicked-bus (Phase 6a). When enabled,
|
|
17
|
+
* a durable subscriber folds `wicked.qe.gate.*` / `wicked.qe.deploy.completed`
|
|
18
|
+
* into the acceptance route's freshness cache; when absent (the default),
|
|
19
|
+
* the route's lazy ledger read stands alone — same answers, read on demand.
|
|
20
|
+
*/
|
|
21
|
+
qeGateEvents?: {
|
|
22
|
+
enabled: boolean;
|
|
23
|
+
/** Bus db path; omit for wicked-bus's own default resolution. */
|
|
24
|
+
dbPath?: string;
|
|
25
|
+
/** Poll cadence, ms (tests shorten it). */
|
|
26
|
+
pollIntervalMs?: number;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Opt-in governed answering of wicked-interactive first-draft generation (task #86 spike,
|
|
30
|
+
* Phase 7c). When enabled, a durable subscriber answers `wicked.interactive.doc.created`
|
|
31
|
+
* (kind:source) with a governed `interactive-draft` run that ends in
|
|
32
|
+
* `wicked.interactive.draft.completed`; when absent (the default), interactive's own assist
|
|
33
|
+
* loop remains the answerer and crew never touches that bus traffic.
|
|
34
|
+
*/
|
|
35
|
+
interactiveDraftEvents?: {
|
|
36
|
+
enabled: boolean;
|
|
37
|
+
/** Bus db path; omit for wicked-bus's own default resolution (honors WICKED_BUS_DATA_DIR). */
|
|
38
|
+
dbPath?: string;
|
|
39
|
+
/** Poll cadence, ms (tests shorten it). */
|
|
40
|
+
pollIntervalMs?: number;
|
|
41
|
+
/** Heartbeat narration cadence, ms (default 15000). */
|
|
42
|
+
heartbeatMs?: number;
|
|
43
|
+
/** Durable replay-dedup ledger path (default ~/.wicked-crew/interactive-draft-ledger.json). */
|
|
44
|
+
ledgerPath?: string;
|
|
45
|
+
/** Where governed workers write finished drafts (default ~/.wicked-crew/interactive-drafts). */
|
|
46
|
+
draftDir?: string;
|
|
47
|
+
/** Seat roster override (JSON array); omit for the production council roster. */
|
|
48
|
+
clisJson?: string;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Opt-in governed answering of wicked-interactive STRUCTURAL edits (task #86, Phase 7c final
|
|
52
|
+
* leg). When enabled, a durable subscriber answers `wicked.interactive.feedback.processed`
|
|
53
|
+
* (awaiting_structural > 0) with a governed `interactive-edit` run that ends in
|
|
54
|
+
* `wicked.interactive.edit.completed` — after a deterministic INV-2 pre-emit self-check;
|
|
55
|
+
* when absent (the default), interactive's own assist loop remains the answerer.
|
|
56
|
+
* Deterministic (content/style/remove) edits never reach this seam at all: the model-free
|
|
57
|
+
* service applies those instantly and hands off only the structural remainder.
|
|
58
|
+
*/
|
|
59
|
+
interactiveEditEvents?: {
|
|
60
|
+
enabled: boolean;
|
|
61
|
+
/** Bus db path; omit for wicked-bus's own default resolution (honors WICKED_BUS_DATA_DIR). */
|
|
62
|
+
dbPath?: string;
|
|
63
|
+
/** Poll cadence, ms (tests shorten it). */
|
|
64
|
+
pollIntervalMs?: number;
|
|
65
|
+
/** Heartbeat narration cadence, ms (default 15000). */
|
|
66
|
+
heartbeatMs?: number;
|
|
67
|
+
/** Durable replay-dedup ledger path (default ~/.wicked-crew/interactive-edit-ledger.json). */
|
|
68
|
+
ledgerPath?: string;
|
|
69
|
+
/** Where handoff files land and workers write edited fragments
|
|
70
|
+
* (default ~/.wicked-crew/interactive-edits). */
|
|
71
|
+
editDir?: string;
|
|
72
|
+
/** Seat roster override (JSON array); omit for the production council roster. */
|
|
73
|
+
clisJson?: string;
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* The project bus seam (DES-PROJECT-001 §4/§5.2). DEFAULT-ON, unlike the opt-in seams
|
|
77
|
+
* above: the ADR's event vocabulary and the live activity bridge are part of the surface, not
|
|
78
|
+
* an integration experiment — but the posture stays LOUD-non-fatal (no wicked-bus / broken db
|
|
79
|
+
* ⇒ project CRUD works, events don't ride). `disabled: true` turns the whole seam off (tests).
|
|
80
|
+
*/
|
|
81
|
+
projectEvents?: {
|
|
82
|
+
disabled?: boolean;
|
|
83
|
+
/** Bus db path; omit for wicked-bus's own default resolution (honors WICKED_BUS_DATA_DIR). */
|
|
84
|
+
dbPath?: string;
|
|
85
|
+
/** Poll cadence for the /ws activity bridge, ms (tests shorten it). */
|
|
86
|
+
pollIntervalMs?: number;
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* DES-MERGE-001 §5.4/§6.1 (slice 3) — the interactive /ws relay. DEFAULT-ON: every
|
|
90
|
+
* wicked.interactive.** bus event is bridged onto the /ws stream as an `interactiveEvent`
|
|
91
|
+
* frame so the studio needs exactly ONE socket. `disabled: true` turns it off (tests).
|
|
92
|
+
*/
|
|
93
|
+
interactiveWsRelay?: {
|
|
94
|
+
disabled?: boolean;
|
|
95
|
+
/** Bus db path; omit for wicked-bus's own default resolution. */
|
|
96
|
+
dbPath?: string;
|
|
97
|
+
/** Poll cadence, ms (tests shorten it). */
|
|
98
|
+
pollIntervalMs?: number;
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* The identity/actor seam (task #88). Omit for full env/file resolution:
|
|
102
|
+
* OFF by default (the local loopback deployment — nothing changes), REQUIRED
|
|
103
|
+
* under `WICKED_RUNTIME=team` or `WICKED_CREW_AUTH=required`. See
|
|
104
|
+
* `src/api/auth.ts` + docs/auth.md.
|
|
105
|
+
*/
|
|
106
|
+
auth?: AuthOptions;
|
|
107
|
+
/** Audit-trail path override (tests). Default `~/.wicked-crew/audit.log` / `WICKED_CREW_AUDIT_LOG`. */
|
|
108
|
+
auditPath?: string;
|
|
109
|
+
/**
|
|
110
|
+
* The seat-health recovery probe (crew#274): every `intervalMs` (default 10 min), INACTIVE
|
|
111
|
+
* seats only, run the seat's `version_probe`; exit 0 flips the seat active again. `enabled`
|
|
112
|
+
* defaults to ON in the daemon and OFF under a test runner (VITEST / NODE_ENV=test), so a
|
|
113
|
+
* test-built server never spawns CLI probes unless it opts in explicitly.
|
|
114
|
+
*/
|
|
115
|
+
seatHealthProbe?: {
|
|
116
|
+
enabled?: boolean;
|
|
117
|
+
intervalMs?: number;
|
|
118
|
+
timeoutMs?: number;
|
|
119
|
+
};
|
|
12
120
|
}
|
|
13
121
|
export declare function createServer(adapter: CoreAdapter, options?: CreateServerOptions): Promise<ReturnType<typeof Fastify>>;
|
|
14
122
|
export interface StartedServer {
|
|
@@ -16,5 +124,5 @@ export interface StartedServer {
|
|
|
16
124
|
port: number;
|
|
17
125
|
host: string;
|
|
18
126
|
}
|
|
19
|
-
export declare function startServer(adapter: CoreAdapter, port?: number, host?: string): Promise<StartedServer>;
|
|
127
|
+
export declare function startServer(adapter: CoreAdapter, port?: number, host?: string, options?: CreateServerOptions): Promise<StartedServer>;
|
|
20
128
|
//# sourceMappingURL=server.d.ts.map
|
package/dist/api/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/api/server.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/api/server.ts"],"names":[],"mappings":"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"}
|
package/dist/api/server.js
CHANGED
|
@@ -6,8 +6,20 @@ import { fileURLToPath } from 'node:url';
|
|
|
6
6
|
import { registerRoutes } from './routes.js';
|
|
7
7
|
import { GateCache } from './gate-cache.js';
|
|
8
8
|
import { ElicitationCache } from './elicitation-cache.js';
|
|
9
|
+
import { registerAuthHooks, resolveAuth } from './auth.js';
|
|
10
|
+
import { AuditLog } from './audit.js';
|
|
9
11
|
import { registerClient, broadcast } from '../events/bus.js';
|
|
10
12
|
import { TerminalHub, registerTerminalWs } from '../events/terminals.js';
|
|
13
|
+
import { QeGateCache, startQeGateSubscriber } from '../qe/gate-events.js';
|
|
14
|
+
import { startInteractiveDraftSubscriber } from '../interactive/draft-events.js';
|
|
15
|
+
import { startInteractiveEditSubscriber } from '../interactive/edit-events.js';
|
|
16
|
+
import { startProjectBus, MEMBERSHIP_ATTACHED, membershipAttachedKey } from '../projects/events.js';
|
|
17
|
+
import { startInteractiveWsRelay, registerInteractiveEventRoutes } from '../interactive/ws-relay.js';
|
|
18
|
+
import { MembershipIndex } from '../projects/membership-index.js';
|
|
19
|
+
import { writeRunEvidencePointer } from '../projects/charter.js';
|
|
20
|
+
import { CoreAdapter } from '../core/adapter.js';
|
|
21
|
+
import { SeatHealthTracker, startSeatHealthProbe } from './seat-health.js';
|
|
22
|
+
import { applyWorkerConfigRoot } from './seat-signin.js';
|
|
11
23
|
// Allow the studio (a separate localhost origin, e.g. :4200) to call the
|
|
12
24
|
// daemon's REST API. Restricted to loopback origins — the daemon only binds
|
|
13
25
|
// 127.0.0.1, so this never widens exposure beyond the local machine.
|
|
@@ -15,7 +27,9 @@ const LOOPBACK_ORIGIN = /^http:\/\/(127\.0\.0\.1|localhost):\d+$/;
|
|
|
15
27
|
/**
|
|
16
28
|
* The bundled studio SPA lives at `dist/studio` next to the compiled server
|
|
17
29
|
* (`dist/api/server.js` → `../studio`). See DES-STUDIO-SERVING-001 §2.3/§3.1
|
|
18
|
-
* and `scripts/bundle-studio.mjs
|
|
30
|
+
* and `scripts/bundle-studio.mjs`, which copies the installed `wicked-studio`
|
|
31
|
+
* package's `dist/` there (the SPA is its own product since the #98 carve —
|
|
32
|
+
* github.com/mikeparcewski/wicked-studio — consumed as a dist artifact).
|
|
19
33
|
*/
|
|
20
34
|
export function defaultStudioRoot() {
|
|
21
35
|
return fileURLToPath(new URL('../studio', import.meta.url));
|
|
@@ -25,23 +39,219 @@ export async function createServer(adapter, options) {
|
|
|
25
39
|
const gateCache = new GateCache();
|
|
26
40
|
const elicitationCache = new ElicitationCache();
|
|
27
41
|
const terminals = new TerminalHub();
|
|
42
|
+
const qeGateCache = new QeGateCache();
|
|
43
|
+
// Per-seat runtime health (crew#274): folded from the single CoreEvent subscription below,
|
|
44
|
+
// surfaced on GET /roster, recovered by the low-frequency probe armed further down.
|
|
45
|
+
const seatHealth = new SeatHealthTracker();
|
|
46
|
+
// The identity/actor seam (task #88). Resolved ONCE, before any hook exists:
|
|
47
|
+
// a malformed token file or a configured-but-unimplemented OIDC block must
|
|
48
|
+
// fail the boot here, never a request mid-flight. In the default local mode
|
|
49
|
+
// this reads no file and installs a hook that only pins the local actor.
|
|
50
|
+
const auth = resolveAuth(options?.auth, (m) => app.log.warn(m));
|
|
51
|
+
if (auth.mode === 'required') {
|
|
52
|
+
app.log.info('auth REQUIRED: bearer tokens enforced on /api/v1 and /ws');
|
|
53
|
+
}
|
|
54
|
+
const audit = new AuditLog(options?.auditPath, (m) => app.log.warn(m));
|
|
55
|
+
app.addHook('onClose', async () => {
|
|
56
|
+
await audit.flush(); // don't lose the trail's tail on shutdown
|
|
57
|
+
});
|
|
58
|
+
// Seat sign-in: export the persisted worker-config root as WICKED_WORKER_HOME at boot (the
|
|
59
|
+
// PUT /settings route re-applies it on every change). The engine reads the env PER SPAWN
|
|
60
|
+
// (acp_runner.rs claude_worker_home), so boot + on-change application is sufficient — no
|
|
61
|
+
// engine restart is ever needed. settings.json is the source of truth: unset/empty deletes
|
|
62
|
+
// the env, restoring the engine default ~/.wicked-worker.
|
|
63
|
+
applyWorkerConfigRoot((await adapter.getSettings()).worker_config_root);
|
|
64
|
+
// The project seam (DES-PROJECT-001): the bus handle for post-commit event emission + the
|
|
65
|
+
// /ws activity bridge, and the run→project index that tags outbound frames (§5.2). Hydrated
|
|
66
|
+
// from the engine so a restarted daemon tags correctly from the first frame. Created BEFORE
|
|
67
|
+
// the interactive seams below: their project-bound launches share the launch route's
|
|
68
|
+
// post-commit half (index tag + membership.attached emit) via `fileRun`.
|
|
69
|
+
const membershipIndex = new MembershipIndex();
|
|
70
|
+
await membershipIndex.hydrate(adapter, (m) => app.log.warn(m));
|
|
71
|
+
const projectBus = options?.projectEvents?.disabled === true
|
|
72
|
+
? null
|
|
73
|
+
: await startProjectBus({
|
|
74
|
+
...(options?.projectEvents?.dbPath !== undefined
|
|
75
|
+
? { dbPath: options.projectEvents.dbPath }
|
|
76
|
+
: {}),
|
|
77
|
+
...(options?.projectEvents?.pollIntervalMs !== undefined
|
|
78
|
+
? { pollIntervalMs: options.projectEvents.pollIntervalMs }
|
|
79
|
+
: {}),
|
|
80
|
+
log: (m) => app.log.warn(m),
|
|
81
|
+
});
|
|
82
|
+
if (projectBus !== null) {
|
|
83
|
+
app.log.info('project bus seam armed (wicked.crew.project.* + /ws activity bridge)');
|
|
84
|
+
app.addHook('onClose', async () => {
|
|
85
|
+
await projectBus.stop();
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
// The interactive relay seam (DES-MERGE-001 §5.4/§6.1, slice 3): every wicked.interactive.**
|
|
89
|
+
// bus event becomes an `interactiveEvent` frame on the SAME /ws socket the studio already
|
|
90
|
+
// holds, and POST /projects/:id/interactive-events puts a whitelisted UI event back on the bus.
|
|
91
|
+
// Default ON — the merged skin needs it to render a generating doc — with the seam's usual
|
|
92
|
+
// posture: a machine without wicked-bus gets a logged null and boots anyway.
|
|
93
|
+
const interactiveRelay = options?.interactiveWsRelay?.disabled === true
|
|
94
|
+
? null
|
|
95
|
+
: await startInteractiveWsRelay({
|
|
96
|
+
...(options?.interactiveWsRelay?.dbPath !== undefined
|
|
97
|
+
? { dbPath: options.interactiveWsRelay.dbPath }
|
|
98
|
+
: {}),
|
|
99
|
+
...(options?.interactiveWsRelay?.pollIntervalMs !== undefined
|
|
100
|
+
? { pollIntervalMs: options.interactiveWsRelay.pollIntervalMs }
|
|
101
|
+
: {}),
|
|
102
|
+
log: (m) => app.log.warn(m),
|
|
103
|
+
});
|
|
104
|
+
if (interactiveRelay !== null) {
|
|
105
|
+
app.log.info('interactive /ws relay armed (filter wicked.interactive.** → interactiveEvent)');
|
|
106
|
+
app.addHook('onClose', async () => {
|
|
107
|
+
await interactiveRelay.stop();
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
/** The post-commit half of a project-FILED launch, shared with the launch route (§2.2/§4):
|
|
111
|
+
* the engine already attached the crew.run membership atomically with the launch — here we
|
|
112
|
+
* tag future /ws frames and announce the attach on the project bus. */
|
|
113
|
+
const fileRun = (runId, projectId) => {
|
|
114
|
+
membershipIndex.set(runId, projectId);
|
|
115
|
+
projectBus?.emit(MEMBERSHIP_ATTACHED,
|
|
116
|
+
// A daemon-internal launcher (the interactive-edit/draft subscribers),
|
|
117
|
+
// not an HTTP caller — the id is set server-side, so it stays honest
|
|
118
|
+
// under the task #88 rule that event actors are never caller-supplied.
|
|
119
|
+
{ project_id: projectId, member: { kind: 'crew.run', ref: runId }, actor: 'interactive' }, membershipAttachedKey(projectId, 'crew.run', runId, Date.now()));
|
|
120
|
+
};
|
|
121
|
+
// Arm the opt-in QE gate-event subscription (crew's bus seam). Failure to
|
|
122
|
+
// arm is LOUD but non-fatal: the acceptance route never depends on the bus.
|
|
123
|
+
if (options?.qeGateEvents?.enabled === true) {
|
|
124
|
+
const { dbPath, pollIntervalMs } = options.qeGateEvents;
|
|
125
|
+
const sub = await startQeGateSubscriber(qeGateCache, {
|
|
126
|
+
...(dbPath !== undefined ? { dbPath } : {}),
|
|
127
|
+
...(pollIntervalMs !== undefined ? { pollIntervalMs } : {}),
|
|
128
|
+
log: (m) => app.log.warn(m),
|
|
129
|
+
});
|
|
130
|
+
if (sub !== null) {
|
|
131
|
+
app.log.info(`qe gate-event subscription armed (filter wicked.qe.**)`);
|
|
132
|
+
app.addHook('onClose', async () => {
|
|
133
|
+
await sub.stop();
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// Arm the opt-in interactive-draft answering seam (task #86 spike). Same posture as the QE
|
|
138
|
+
// seam: failure to arm is LOUD but non-fatal — interactive's assist loop is the fallback
|
|
139
|
+
// answerer, and this daemon must boot on a machine whose bus is broken.
|
|
140
|
+
if (options?.interactiveDraftEvents?.enabled === true) {
|
|
141
|
+
const o = options.interactiveDraftEvents;
|
|
142
|
+
const sub = await startInteractiveDraftSubscriber(adapter, {
|
|
143
|
+
...(o.dbPath !== undefined ? { dbPath: o.dbPath } : {}),
|
|
144
|
+
...(o.pollIntervalMs !== undefined ? { pollIntervalMs: o.pollIntervalMs } : {}),
|
|
145
|
+
...(o.heartbeatMs !== undefined ? { heartbeatMs: o.heartbeatMs } : {}),
|
|
146
|
+
...(o.ledgerPath !== undefined ? { ledgerPath: o.ledgerPath } : {}),
|
|
147
|
+
...(o.draftDir !== undefined ? { draftDir: o.draftDir } : {}),
|
|
148
|
+
...(o.clisJson !== undefined ? { clisJson: o.clisJson } : {}),
|
|
149
|
+
onRunFiled: fileRun,
|
|
150
|
+
log: (m) => app.log.warn(m),
|
|
151
|
+
});
|
|
152
|
+
if (sub !== null) {
|
|
153
|
+
app.log.info('interactive-draft subscription armed (filter wicked.interactive.doc.created)');
|
|
154
|
+
app.addHook('onClose', async () => {
|
|
155
|
+
await sub.stop();
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Arm the opt-in interactive STRUCTURAL-edit answering seam (task #86 final leg). Same
|
|
160
|
+
// posture as the draft seam: failure to arm is LOUD but non-fatal — interactive's assist
|
|
161
|
+
// loop is the fallback answerer, and this daemon must boot on a machine whose bus is broken.
|
|
162
|
+
if (options?.interactiveEditEvents?.enabled === true) {
|
|
163
|
+
const o = options.interactiveEditEvents;
|
|
164
|
+
const sub = await startInteractiveEditSubscriber(adapter, {
|
|
165
|
+
...(o.dbPath !== undefined ? { dbPath: o.dbPath } : {}),
|
|
166
|
+
...(o.pollIntervalMs !== undefined ? { pollIntervalMs: o.pollIntervalMs } : {}),
|
|
167
|
+
...(o.heartbeatMs !== undefined ? { heartbeatMs: o.heartbeatMs } : {}),
|
|
168
|
+
...(o.ledgerPath !== undefined ? { ledgerPath: o.ledgerPath } : {}),
|
|
169
|
+
...(o.editDir !== undefined ? { editDir: o.editDir } : {}),
|
|
170
|
+
...(o.clisJson !== undefined ? { clisJson: o.clisJson } : {}),
|
|
171
|
+
onRunFiled: fileRun,
|
|
172
|
+
log: (m) => app.log.warn(m),
|
|
173
|
+
});
|
|
174
|
+
if (sub !== null) {
|
|
175
|
+
app.log.info('interactive-edit subscription armed (filter wicked.interactive.feedback.processed)');
|
|
176
|
+
app.addHook('onClose', async () => {
|
|
177
|
+
await sub.stop();
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
28
181
|
// The daemon's single CoreEvent subscription fans out here: cache gate prompts
|
|
29
182
|
// (§3.3), cache elicitation prompts (DES-002), route terminal frames to their owning
|
|
30
|
-
// per-terminal socket (by id, DES-TERMINAL-001 §6), then forward every frame
|
|
31
|
-
//
|
|
32
|
-
|
|
183
|
+
// per-terminal socket (by id, DES-TERMINAL-001 §6), then forward every frame — tagged
|
|
184
|
+
// with its run's `project_id` when the membership table files it (DES-PROJECT-001
|
|
185
|
+
// §5.2; no new socket, additive field) — to all `/ws` clients (§2.1).
|
|
186
|
+
//
|
|
187
|
+
// Unregistered on close: a process can build more than one server over the same
|
|
188
|
+
// adapter (tests do), and a closed server's caches must stop consuming events —
|
|
189
|
+
// otherwise every discarded server keeps folding state forever (listener leak).
|
|
190
|
+
const offEvent = adapter.onEvent((event) => {
|
|
33
191
|
gateCache.ingest(event);
|
|
34
192
|
elicitationCache.ingest(event);
|
|
193
|
+
seatHealth.ingest(event);
|
|
35
194
|
terminals.route(event);
|
|
36
|
-
|
|
195
|
+
const session = typeof event.session === 'string' ? event.session : undefined;
|
|
196
|
+
const projectId = session !== undefined ? membershipIndex.projectOf(session) : undefined;
|
|
197
|
+
broadcast(projectId !== undefined ? { ...event, project_id: projectId } : event);
|
|
198
|
+
// The foundation record's evidence pointer (§3.2 row 3): a project-bound run that completes
|
|
199
|
+
// gets its run-scope pointer written, best-effort, off the hot path.
|
|
200
|
+
if (event.type === 'sessionCompleted' && session !== undefined && projectId !== undefined) {
|
|
201
|
+
void (async () => {
|
|
202
|
+
try {
|
|
203
|
+
const views = await adapter.sessionsDetail();
|
|
204
|
+
const view = views.find((v) => v.session.id === session);
|
|
205
|
+
const repoRef = view?.session.repo_ref ?? null;
|
|
206
|
+
const repoRoot = repoRef !== null
|
|
207
|
+
? ((await adapter.listRepos()).find((r) => r.id === repoRef)?.root_path ?? null)
|
|
208
|
+
: null;
|
|
209
|
+
// The STORED scope, not a derived spelling: `Project.scope` is the designed tenancy
|
|
210
|
+
// seam (ADR §3.1 — a future `org:<o>/project:<id>` prefix must not strand pointers).
|
|
211
|
+
const project = await adapter.projectGet(projectId);
|
|
212
|
+
await writeRunEvidencePointer(adapter, project?.scope ?? `project:${projectId}`, session, repoRoot, (m) => app.log.warn(m));
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
app.log.warn(`[projects] evidence-pointer lookup for ${session} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
216
|
+
}
|
|
217
|
+
})();
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
app.addHook('onClose', async () => {
|
|
221
|
+
offEvent();
|
|
37
222
|
});
|
|
223
|
+
// The seat-health recovery probe (crew#274 §3). A plain setInterval, unref'd, torn down on
|
|
224
|
+
// close. Default ON in the daemon, OFF under a test runner — a test suite building servers
|
|
225
|
+
// must never spawn `<cli> --version` children unless it opts in with `enabled: true`.
|
|
226
|
+
const probeCfg = options?.seatHealthProbe;
|
|
227
|
+
const underTestRunner = process.env['VITEST'] !== undefined || process.env['NODE_ENV'] === 'test';
|
|
228
|
+
if (probeCfg?.enabled ?? !underTestRunner) {
|
|
229
|
+
const probe = startSeatHealthProbe(seatHealth, () => CoreAdapter.roster(), {
|
|
230
|
+
...(probeCfg?.intervalMs !== undefined ? { intervalMs: probeCfg.intervalMs } : {}),
|
|
231
|
+
...(probeCfg?.timeoutMs !== undefined ? { timeoutMs: probeCfg.timeoutMs } : {}),
|
|
232
|
+
log: (m) => app.log.warn(m),
|
|
233
|
+
});
|
|
234
|
+
app.addHook('onClose', async () => {
|
|
235
|
+
probe.stop();
|
|
236
|
+
});
|
|
237
|
+
}
|
|
38
238
|
app.addHook('onRequest', async (req, reply) => {
|
|
39
239
|
const origin = req.headers.origin;
|
|
40
|
-
|
|
240
|
+
// Loopback origins are always allowed (the studio on another localhost
|
|
241
|
+
// port). NON-loopback origins are allowed ONLY when auth is required — the
|
|
242
|
+
// R2 pairing: a hosted skin needs CORS, and CORS beyond the machine is
|
|
243
|
+
// safe exactly when every request must carry a bearer token (no ambient
|
|
244
|
+
// credential exists for a foreign page to ride). An explicit allowlist
|
|
245
|
+
// (`allowedOrigins` / WICKED_CREW_ALLOWED_ORIGINS) narrows it further.
|
|
246
|
+
const allowed = origin !== undefined &&
|
|
247
|
+
(LOOPBACK_ORIGIN.test(origin) ||
|
|
248
|
+
(auth.mode === 'required' &&
|
|
249
|
+
(auth.allowedOrigins === null || auth.allowedOrigins.includes(origin))));
|
|
250
|
+
if (origin && allowed) {
|
|
41
251
|
reply.header('Access-Control-Allow-Origin', origin);
|
|
42
252
|
reply.header('Vary', 'Origin');
|
|
43
|
-
reply.header('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS');
|
|
44
|
-
reply.header('Access-Control-Allow-Headers', 'Content-Type');
|
|
253
|
+
reply.header('Access-Control-Allow-Methods', 'GET,POST,PUT,PATCH,DELETE,OPTIONS');
|
|
254
|
+
reply.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
45
255
|
}
|
|
46
256
|
if (req.method === 'OPTIONS') {
|
|
47
257
|
await reply.code(204).send();
|
|
@@ -60,13 +270,28 @@ export async function createServer(adapter, options) {
|
|
|
60
270
|
}
|
|
61
271
|
});
|
|
62
272
|
await app.register(fastifyWebsocket);
|
|
273
|
+
// Identity (401) + trust (403) hooks. Ordering is load-bearing twice over:
|
|
274
|
+
// AFTER the CORS hook, so an OPTIONS preflight (which carries no
|
|
275
|
+
// Authorization by design) is answered 204 above and never reaches the token
|
|
276
|
+
// check — and AFTER the websocket plugin, whose own onRequest hook flags
|
|
277
|
+
// upgrade requests (`request.ws`); if auth 401s first, that flag is never
|
|
278
|
+
// set and the plugin's onResponse cleanup skips destroying the raw upgrade
|
|
279
|
+
// socket, which then holds `app.close()` open forever.
|
|
280
|
+
registerAuthHooks(app, auth);
|
|
63
281
|
app.get('/ws', { websocket: true }, (socket) => {
|
|
64
282
|
// Late-join gets no replay; the studio reconciles with a one-shot GET /runs.
|
|
65
283
|
registerClient(socket);
|
|
66
284
|
});
|
|
67
285
|
// One dedicated WS channel per PTY: /ws/terminals/:id (DES-TERMINAL-001 §6).
|
|
68
286
|
registerTerminalWs(app, adapter, terminals);
|
|
69
|
-
registerRoutes(app, adapter, gateCache, elicitationCache
|
|
287
|
+
registerRoutes(app, adapter, gateCache, elicitationCache, qeGateCache, {
|
|
288
|
+
bus: projectBus,
|
|
289
|
+
index: membershipIndex,
|
|
290
|
+
log: (m) => app.log.warn(m),
|
|
291
|
+
}, { audit, authMode: auth.mode }, { seatHealth });
|
|
292
|
+
// The UI-emittable direction of the interactive seam. Registered unconditionally (a null relay
|
|
293
|
+
// answers 503, not 404) and BEFORE the static/SPA fallback below, like every other API route.
|
|
294
|
+
registerInteractiveEventRoutes(app, interactiveRelay);
|
|
70
295
|
// Serve the bundled studio SPA same-origin (DES-STUDIO-SERVING-001 §3). The
|
|
71
296
|
// API routes, `/ws`, and terminal WS are registered ABOVE and keep winning:
|
|
72
297
|
// static uses `wildcard: false` (only serves files that physically exist),
|
|
@@ -121,8 +346,8 @@ export async function createServer(adapter, options) {
|
|
|
121
346
|
}
|
|
122
347
|
return app;
|
|
123
348
|
}
|
|
124
|
-
export async function startServer(adapter, port = 7701, host = '127.0.0.1') {
|
|
125
|
-
const app = await createServer(adapter);
|
|
349
|
+
export async function startServer(adapter, port = 7701, host = '127.0.0.1', options) {
|
|
350
|
+
const app = await createServer(adapter, options);
|
|
126
351
|
await app.listen({ port, host });
|
|
127
352
|
const addr = app.server.address();
|
|
128
353
|
const boundPort = typeof addr === 'object' && addr ? addr.port : port;
|