@ours.network/fleet 1.0.5 → 1.1.0-nightly.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/README.md +28 -41
- package/dist/application/capabilities.d.ts +0 -1
- package/dist/application/capabilities.js +3 -13
- package/dist/application/fleet-query-service.d.ts +0 -3
- package/dist/application/fleet-query-service.js +1 -24
- package/dist/application/role-creation-service.d.ts +5 -5
- package/dist/application/role-creation-service.js +5 -7
- package/dist/application/role-repository.d.ts +1 -2
- package/dist/application/role-repository.js +7 -7
- package/dist/application/session-control.d.ts +0 -12
- package/dist/application/session-control.js +0 -42
- package/dist/application/session-mutations.d.ts +5 -5
- package/dist/application/types.d.ts +0 -7
- package/dist/briefing.js +1 -4
- package/dist/build-info.json +5 -5
- package/dist/cli.js +9 -23
- package/dist/config.d.ts +1 -4
- package/dist/config.js +13 -17
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +20 -26
- package/dist/doctor.js +0 -7
- package/dist/fleet-proxy.d.ts +1 -1
- package/dist/harness/acp-session-transport.d.ts +8 -0
- package/dist/harness/acp-session-transport.js +3 -0
- package/dist/harness/agent-session.d.ts +33 -0
- package/dist/harness/agent-session.js +1 -0
- package/dist/harness/claude-code-session.d.ts +18 -0
- package/dist/harness/claude-code-session.js +28 -0
- package/dist/harness/claude-code.d.ts +2 -1
- package/dist/harness/claude-code.js +29 -81
- package/dist/harness/codex-session.d.ts +18 -0
- package/dist/harness/codex-session.js +28 -0
- package/dist/harness/codex.d.ts +2 -1
- package/dist/harness/codex.js +35 -45
- package/dist/harness/types.d.ts +6 -38
- package/dist/index.d.ts +3 -5
- package/dist/index.js +1 -3
- package/dist/isolation/resources.d.ts +3 -7
- package/dist/isolation/resources.js +3 -7
- package/dist/model-env.d.ts +2 -3
- package/dist/model-env.js +2 -3
- package/dist/monitor.d.ts +1 -24
- package/dist/monitor.js +5 -140
- package/dist/owner-channel/channel.d.ts +2 -2
- package/dist/runner.d.ts +5 -15
- package/dist/runner.js +26 -102
- package/dist/session/acp.d.ts +2 -2
- package/dist/session/acp.js +2 -3
- package/dist/session/activity.d.ts +2 -3
- package/dist/session/activity.js +2 -3
- package/dist/session/arbiter.d.ts +4 -4
- package/dist/session/control.d.ts +3 -3
- package/dist/session/types.d.ts +11 -5
- package/dist/spawn.js +8 -9
- package/dist/supervisor/none.d.ts +1 -1
- package/dist/supervisor/none.js +2 -2
- package/dist/tmux.d.ts +1 -14
- package/dist/tmux.js +1 -51
- package/dist/watchdog/config.js +6 -4
- package/dist/watchdog/run.js +8 -23
- package/dist/web/auth.d.ts +1 -1
- package/dist/web/runtime.js +4 -15
- package/dist/web/server.d.ts +1 -3
- package/dist/web/server.js +3 -14
- package/dist/web/topology-promote.js +2 -5
- package/dist/web-app/assets/index-59GF-gsZ.css +1 -0
- package/dist/web-app/assets/{index-BCBK78hw.js → index-DhMDVRU1.js} +6 -6
- package/dist/web-app/index.html +2 -2
- package/package.json +3 -8
- package/dist/session/tmux.d.ts +0 -28
- package/dist/session/tmux.js +0 -80
- package/dist/web/terminal/bridge.d.ts +0 -27
- package/dist/web/terminal/bridge.js +0 -317
- package/dist/web-app/assets/TerminalView-C_G1ID2P.js +0 -9
- package/dist/web-app/assets/index-DuC-xnX4.css +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Socket } from 'node:net';
|
|
2
|
-
import type { ControlFailureKind, SessionEvent,
|
|
2
|
+
import type { AgentSession, ControlFailureKind, SessionEvent, SessionSnapshot } from './types.js';
|
|
3
3
|
import type { OwnerChannelHandle, OwnerChannelManagementRequest } from '../owner-channel/channel.js';
|
|
4
4
|
import type { ScheduledLoopManagerHandle } from '../loops/manager.js';
|
|
5
5
|
import type { SpawnOpts } from '../spawn.js';
|
|
@@ -48,7 +48,7 @@ export interface RetainedEventPage {
|
|
|
48
48
|
truncated: boolean;
|
|
49
49
|
}
|
|
50
50
|
/** The one retained-range projection shared by polling and live-follow admission. */
|
|
51
|
-
export declare function retainedEventPage(session:
|
|
51
|
+
export declare function retainedEventPage(session: AgentSession, since: number): RetainedEventPage;
|
|
52
52
|
/**
|
|
53
53
|
* One line saying what a control failure does — and does not — prove about the
|
|
54
54
|
* agent. Only `offline` is evidence that it is gone; every other kind used to
|
|
@@ -105,7 +105,7 @@ export declare class RoleControlServer {
|
|
|
105
105
|
private loopManager?;
|
|
106
106
|
private reloadConfig?;
|
|
107
107
|
private fleetSpawner?;
|
|
108
|
-
constructor(stateDir: string, session:
|
|
108
|
+
constructor(stateDir: string, session: AgentSession, log: (line: string) => void);
|
|
109
109
|
start(): Promise<void>;
|
|
110
110
|
close(): Promise<void>;
|
|
111
111
|
/** Attach only the already-started supervisor-owned channel client. */
|
package/dist/session/types.d.ts
CHANGED
|
@@ -96,7 +96,7 @@ export interface InterruptOutcome {
|
|
|
96
96
|
reasonCode?: string;
|
|
97
97
|
}
|
|
98
98
|
/**
|
|
99
|
-
* What
|
|
99
|
+
* What an `AgentSession.interrupt` implementation may resolve. Before 0.17.1 the
|
|
100
100
|
* contract was `Promise<void>`, and resolving at all meant the cancellation had
|
|
101
101
|
* taken effect cooperatively — so an implementation written against that
|
|
102
102
|
* contract stays valid and keeps its exact meaning. Only in-tree consumers read
|
|
@@ -146,7 +146,7 @@ export interface ExitRecord {
|
|
|
146
146
|
code?: number;
|
|
147
147
|
/** Signal that killed it, when one did. */
|
|
148
148
|
signal?: string;
|
|
149
|
-
/** Raw wait status
|
|
149
|
+
/** Raw wait status retained when migrating a legacy shell-exit record. */
|
|
150
150
|
status?: number;
|
|
151
151
|
at?: string;
|
|
152
152
|
/** One line an operator can read. */
|
|
@@ -194,8 +194,7 @@ export interface SessionSnapshot {
|
|
|
194
194
|
};
|
|
195
195
|
/**
|
|
196
196
|
* Observed agent activity, independent of turn occupancy: the evidence a
|
|
197
|
-
* human-facing surface needs before calling a role idle.
|
|
198
|
-
* that cannot observe the agent at all (tmux), which is itself honest — no
|
|
197
|
+
* human-facing surface needs before calling a role idle. When absent, no
|
|
199
198
|
* evidence is not evidence of inactivity.
|
|
200
199
|
*/
|
|
201
200
|
activity?: SessionActivity;
|
|
@@ -259,7 +258,12 @@ export interface ConversationHandlePage {
|
|
|
259
258
|
hasMore: boolean;
|
|
260
259
|
snapshot: ConversationSnapshot;
|
|
261
260
|
}
|
|
262
|
-
|
|
261
|
+
/**
|
|
262
|
+
* The live, harness-neutral contract between Fleet and one agent session.
|
|
263
|
+
* Harness adapters construct this handle; orchestration code must not depend
|
|
264
|
+
* on the ACP transport which currently implements it.
|
|
265
|
+
*/
|
|
266
|
+
export interface AgentSession {
|
|
263
267
|
readonly backend: SessionBackendId;
|
|
264
268
|
readonly pid: number;
|
|
265
269
|
isAlive(): boolean;
|
|
@@ -301,3 +305,5 @@ export interface SessionHandle {
|
|
|
301
305
|
exitResult(): ExitRecord | null;
|
|
302
306
|
close(): Promise<void>;
|
|
303
307
|
}
|
|
308
|
+
/** @deprecated Public compatibility name; use AgentSession. */
|
|
309
|
+
export type SessionHandle = AgentSession;
|
package/dist/spawn.js
CHANGED
|
@@ -123,8 +123,10 @@ export function readIsolationFile(path) {
|
|
|
123
123
|
export function validateSpawnOpts(o) {
|
|
124
124
|
if (o.mission !== undefined && o.missionFile)
|
|
125
125
|
throw new Error('--mission and --mission-file are mutually exclusive');
|
|
126
|
-
if (o.session
|
|
127
|
-
throw new Error(
|
|
126
|
+
if (o.session === 'tmux')
|
|
127
|
+
throw new Error("invalid --session 'tmux'; tmux is no longer supported; use --session acp");
|
|
128
|
+
if (o.session && o.session !== 'acp')
|
|
129
|
+
throw new Error(`invalid --session '${o.session}'; allowed: acp`);
|
|
128
130
|
if (o.approval && !['ask', 'auto', 'allow', 'deny'].includes(o.approval))
|
|
129
131
|
throw new Error(`invalid --approval '${o.approval}'; allowed: ask, auto, allow (deprecated alias: deny)`);
|
|
130
132
|
if (o.filesystem && !['read-only', 'workspace', 'unrestricted'].includes(o.filesystem))
|
|
@@ -203,7 +205,7 @@ export function spawnDryRun(o) {
|
|
|
203
205
|
...(authProxy ? { authProxyBaseUrl: authProxy.base_url } : {}),
|
|
204
206
|
});
|
|
205
207
|
const model = modelEnv.model;
|
|
206
|
-
const session = raw.session ?? cfg.defaults.session ?? '
|
|
208
|
+
const session = raw.session ?? cfg.defaults.session ?? 'acp';
|
|
207
209
|
const resolvedRole = {
|
|
208
210
|
...raw,
|
|
209
211
|
name: o.name,
|
|
@@ -255,7 +257,7 @@ function provenanceSettings(o, defaults) {
|
|
|
255
257
|
const inheritedModel = resolveRoleModel(undefined, o.harness, defaults);
|
|
256
258
|
return {
|
|
257
259
|
harness: tagged('harness', provenanceOf(o.harness, defaults.harness, 'claude-code')),
|
|
258
|
-
session: tagged('session', provenanceOf(o.session, defaults.session, '
|
|
260
|
+
session: tagged('session', provenanceOf(o.session, defaults.session, 'acp')),
|
|
259
261
|
identity: o.identity
|
|
260
262
|
? { value: o.identity, source: 'cli' }
|
|
261
263
|
: { value: o.name, source: 'built-in' }, // defaults to the role name
|
|
@@ -417,7 +419,7 @@ async function spawnTempInner(o, binPath, launch, tx, onStage) {
|
|
|
417
419
|
...(tempAuthProxy ? { authProxyBaseUrl: tempAuthProxy.base_url } : {}),
|
|
418
420
|
});
|
|
419
421
|
const model = modelEnv.model;
|
|
420
|
-
const session = o.session ?? cfg.defaults.session ?? '
|
|
422
|
+
const session = o.session ?? cfg.defaults.session ?? 'acp';
|
|
421
423
|
const role = {
|
|
422
424
|
...fromOpts, // includes `isolation` when --isolation-file was given
|
|
423
425
|
name: o.name,
|
|
@@ -467,10 +469,7 @@ async function spawnTempInner(o, binPath, launch, tx, onStage) {
|
|
|
467
469
|
if (cfg.startStaggerMs > 0)
|
|
468
470
|
writeFileSync(join(dir, START_STAGGER_FILE), String(cfg.startStaggerMs));
|
|
469
471
|
prepareTempSupervisor(dir, o.name);
|
|
470
|
-
// Run the supervisor independently
|
|
471
|
-
// `_run-temp` -> runOnce() creates AND kills the tmux session <name> for the
|
|
472
|
-
// agent itself; a supervisor sharing that session name would SIGHUP its own
|
|
473
|
-
// process before the agent ever launches. On a service-managed host the temp
|
|
472
|
+
// Run the supervisor independently. On a service-managed host the temp
|
|
474
473
|
// runner gets its own transient unit/job, so stopping the coordinator's unit
|
|
475
474
|
// cannot kill a live worker in the coordinator's cgroup.
|
|
476
475
|
onStage?.('starting_temp');
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type Exec } from '../exec.js';
|
|
2
2
|
import type { SupervisorBackend } from './types.js';
|
|
3
3
|
/**
|
|
4
|
-
* No supervision:
|
|
4
|
+
* No service supervision: tmux contains the direct runner process, nothing survives a reboot and
|
|
5
5
|
* nothing restarts on crash. Used for temp agents and CI tests
|
|
6
6
|
* (OURS_FLEET_SUPERVISOR=none).
|
|
7
7
|
*/
|
package/dist/supervisor/none.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Tmux, tmuxArgs } from '../tmux.js';
|
|
2
2
|
import { realExec, shq } from '../exec.js';
|
|
3
3
|
/**
|
|
4
|
-
* No supervision:
|
|
4
|
+
* No service supervision: tmux contains the direct runner process, nothing survives a reboot and
|
|
5
5
|
* nothing restarts on crash. Used for temp agents and CI tests
|
|
6
6
|
* (OURS_FLEET_SUPERVISOR=none).
|
|
7
7
|
*/
|
|
@@ -9,7 +9,7 @@ export function makeNoneBackend(exec = realExec) {
|
|
|
9
9
|
const tmux = new Tmux(exec);
|
|
10
10
|
return {
|
|
11
11
|
id: 'none',
|
|
12
|
-
async init() { return ['no supervisor:
|
|
12
|
+
async init() { return ['no service supervisor: direct runner containment only (no reboot survival)']; },
|
|
13
13
|
async install(name, binPath) {
|
|
14
14
|
const existed = await tmux.kill(name); // true when a session was there
|
|
15
15
|
await tmux.newSession(name, process.cwd(), `${shq(binPath)} _run ${shq(name)}`);
|
package/dist/tmux.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ export declare const tmuxSocket: (session: string) => string;
|
|
|
20
20
|
* the shared default server and re-creates #32.
|
|
21
21
|
*/
|
|
22
22
|
export declare const tmuxArgs: (session: string, args: string[]) => string[];
|
|
23
|
-
/** Thin tmux wrapper
|
|
23
|
+
/** Thin tmux wrapper retained only for supervisor=none process containment. */
|
|
24
24
|
export declare class Tmux {
|
|
25
25
|
private exec;
|
|
26
26
|
constructor(exec?: Exec);
|
|
@@ -32,17 +32,4 @@ export declare class Tmux {
|
|
|
32
32
|
* program inside it exited on its own".
|
|
33
33
|
*/
|
|
34
34
|
kill(name: string): Promise<boolean>;
|
|
35
|
-
capture(name: string, lines?: number): Promise<string>;
|
|
36
|
-
/** Bounded ANSI/history seed for the browser terminal projection. */
|
|
37
|
-
captureHistory(name: string, lines?: number): Promise<string>;
|
|
38
|
-
panePid(name: string): Promise<number | null>;
|
|
39
|
-
/**
|
|
40
|
-
* List the live sessions among `names`, asking each server in turn.
|
|
41
|
-
* There is no fleet-wide `tmux ls` any more: a session per server means the
|
|
42
|
-
* caller says which sessions to ask about. A server that is not running
|
|
43
|
-
* answers non-zero and contributes nothing.
|
|
44
|
-
*/
|
|
45
|
-
list(names: readonly string[]): Promise<string>;
|
|
46
|
-
sendText(name: string, text: string): Promise<void>;
|
|
47
|
-
sendKey(name: string, key: string): Promise<void>;
|
|
48
35
|
}
|
package/dist/tmux.js
CHANGED
|
@@ -20,7 +20,7 @@ export const tmuxSocket = (session) => `${TMUX_SOCKET_PREFIX}${session}`;
|
|
|
20
20
|
* the shared default server and re-creates #32.
|
|
21
21
|
*/
|
|
22
22
|
export const tmuxArgs = (session, args) => ['-L', tmuxSocket(session), ...args];
|
|
23
|
-
/** Thin tmux wrapper
|
|
23
|
+
/** Thin tmux wrapper retained only for supervisor=none process containment. */
|
|
24
24
|
export class Tmux {
|
|
25
25
|
exec;
|
|
26
26
|
constructor(exec = realExec) {
|
|
@@ -42,54 +42,4 @@ export class Tmux {
|
|
|
42
42
|
async kill(name) {
|
|
43
43
|
return (await this.exec('tmux', tmuxArgs(name, ['kill-session', '-t', name]))).code === 0;
|
|
44
44
|
}
|
|
45
|
-
async capture(name, lines = 40) {
|
|
46
|
-
const r = await this.exec('tmux', tmuxArgs(name, ['capture-pane', '-t', name, '-p']));
|
|
47
|
-
if (r.code !== 0)
|
|
48
|
-
throw new Error(`tmux capture-pane '${name}' failed: ${r.stderr.trim()}`);
|
|
49
|
-
const all = r.stdout.replace(/\n+$/, '').split('\n');
|
|
50
|
-
return all.slice(-lines).join('\n');
|
|
51
|
-
}
|
|
52
|
-
/** Bounded ANSI/history seed for the browser terminal projection. */
|
|
53
|
-
async captureHistory(name, lines = 5_000) {
|
|
54
|
-
const bounded = Math.min(Math.max(Math.trunc(lines), 1), 20_000);
|
|
55
|
-
const r = await this.exec('tmux', tmuxArgs(name, ['capture-pane', '-t', name, '-p', '-e', '-J', '-S', `-${bounded}`]));
|
|
56
|
-
if (r.code !== 0)
|
|
57
|
-
throw new Error(`tmux capture-pane '${name}' failed: ${r.stderr.trim()}`);
|
|
58
|
-
return Buffer.from(r.stdout).subarray(0, 4 * 1024 * 1024).toString();
|
|
59
|
-
}
|
|
60
|
-
async panePid(name) {
|
|
61
|
-
const r = await this.exec('tmux', tmuxArgs(name, ['list-panes', '-t', name, '-F', '#{pane_pid}']));
|
|
62
|
-
if (r.code !== 0)
|
|
63
|
-
return null;
|
|
64
|
-
const pid = parseInt(r.stdout.trim().split('\n')[0], 10);
|
|
65
|
-
return Number.isFinite(pid) ? pid : null;
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* List the live sessions among `names`, asking each server in turn.
|
|
69
|
-
* There is no fleet-wide `tmux ls` any more: a session per server means the
|
|
70
|
-
* caller says which sessions to ask about. A server that is not running
|
|
71
|
-
* answers non-zero and contributes nothing.
|
|
72
|
-
*/
|
|
73
|
-
async list(names) {
|
|
74
|
-
const lines = [];
|
|
75
|
-
for (const name of names) {
|
|
76
|
-
const r = await this.exec('tmux', tmuxArgs(name, ['ls']));
|
|
77
|
-
if (r.code === 0 && r.stdout.trim())
|
|
78
|
-
lines.push(r.stdout.trimEnd());
|
|
79
|
-
}
|
|
80
|
-
return lines.join('\n');
|
|
81
|
-
}
|
|
82
|
-
async sendText(name, text) {
|
|
83
|
-
let r = await this.exec('tmux', tmuxArgs(name, ['send-keys', '-t', name, '-l', text]));
|
|
84
|
-
if (r.code !== 0)
|
|
85
|
-
throw new Error(`tmux send-keys '${name}' failed: ${r.stderr.trim()}`);
|
|
86
|
-
r = await this.exec('tmux', tmuxArgs(name, ['send-keys', '-t', name, 'Enter']));
|
|
87
|
-
if (r.code !== 0)
|
|
88
|
-
throw new Error(`tmux send-keys Enter '${name}' failed: ${r.stderr.trim()}`);
|
|
89
|
-
}
|
|
90
|
-
async sendKey(name, key) {
|
|
91
|
-
const r = await this.exec('tmux', tmuxArgs(name, ['send-keys', '-t', name, key]));
|
|
92
|
-
if (r.code !== 0)
|
|
93
|
-
throw new Error(`tmux send-keys '${name}' failed: ${r.stderr.trim()}`);
|
|
94
|
-
}
|
|
95
45
|
}
|
package/dist/watchdog/config.js
CHANGED
|
@@ -31,7 +31,7 @@ export function resolveWatchdogs(baseDoc, baseFile, roles, vars, defaults) {
|
|
|
31
31
|
const roleIdentities = new Set(roles.map(r => r.identity));
|
|
32
32
|
// Tracks identity -> owning watchdog name across entries:
|
|
33
33
|
// two watchdogs declaring the same identity would otherwise share a temp
|
|
34
|
-
// dir,
|
|
34
|
+
// dir, agent session, and run lock with no error until they collide at
|
|
35
35
|
// runtime.
|
|
36
36
|
const watchdogIdentities = new Map();
|
|
37
37
|
const out = [];
|
|
@@ -61,9 +61,11 @@ export function resolveWatchdogs(baseDoc, baseFile, roles, vars, defaults) {
|
|
|
61
61
|
throw new ConfigError(`${where}: identity '${identity}' collides with watchdog '${collidingWatchdog}'`);
|
|
62
62
|
watchdogIdentities.set(identity, name);
|
|
63
63
|
const harness = w.harness ?? defaults.harness ?? 'claude-code';
|
|
64
|
-
const sessionRaw = w.session ?? defaults.session ?? '
|
|
65
|
-
if (sessionRaw
|
|
66
|
-
throw new ConfigError(`${where}: session
|
|
64
|
+
const sessionRaw = w.session ?? defaults.session ?? 'acp';
|
|
65
|
+
if (sessionRaw === 'tmux')
|
|
66
|
+
throw new ConfigError(`${where}: session 'tmux' is no longer supported; use session: acp`);
|
|
67
|
+
if (sessionRaw !== 'acp')
|
|
68
|
+
throw new ConfigError(`${where}: session must be 'acp'`);
|
|
67
69
|
if (w.prompt_file !== undefined) {
|
|
68
70
|
if (typeof w.prompt_file !== 'string' || !isAbsolute(w.prompt_file))
|
|
69
71
|
throw new ConfigError(`${where}: prompt_file must be an absolute path`);
|
package/dist/watchdog/run.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { closeSync, existsSync, openSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync, } from 'node:fs';
|
|
2
|
-
import { spawn as spawnChild
|
|
2
|
+
import { spawn as spawnChild } from 'node:child_process';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
-
import { promisify } from 'node:util';
|
|
5
4
|
import { stringify } from 'yaml';
|
|
6
5
|
import { errorReport, normalizeWatchdogReport, validateWatchdogReport } from './report.js';
|
|
7
6
|
import { acquireRunLock, formatRunId, pruneReports, releaseRunLock, writeReport } from './store.js';
|
|
@@ -15,8 +14,6 @@ import { loadConfig, resolveMonitorConfig, resolveWorklogPolicy, ROLE_NAME_RE, }
|
|
|
15
14
|
import { getAdapter } from '../harness/registry.js';
|
|
16
15
|
import { redactLogLine } from '../application/log-service.js';
|
|
17
16
|
import { controlRequest, controlSocketPath } from '../session/control.js';
|
|
18
|
-
import { Tmux } from '../tmux.js';
|
|
19
|
-
const execFileAsync = promisify(execFile);
|
|
20
17
|
/** How often the deadline loop polls for a completed report or a dead child. */
|
|
21
18
|
const POLL_MS = 1000;
|
|
22
19
|
/** A `report.json` this old, with no further writes, is treated as finished. */
|
|
@@ -56,7 +53,6 @@ async function killIfNeeded(reason, child, roleName, sleep) {
|
|
|
56
53
|
if (reason === 'stable')
|
|
57
54
|
await sleep(HARVEST_GRACE_MS);
|
|
58
55
|
child.kill();
|
|
59
|
-
await killTmuxSession(roleName);
|
|
60
56
|
}
|
|
61
57
|
}
|
|
62
58
|
function defaultLaunchChild(binPath, roleName, runDir) {
|
|
@@ -77,13 +73,6 @@ function defaultLaunchChild(binPath, roleName, runDir) {
|
|
|
77
73
|
}),
|
|
78
74
|
};
|
|
79
75
|
}
|
|
80
|
-
/** Best-effort: the tmux session outlives the supervisor child (`runOnce` created it). */
|
|
81
|
-
async function killTmuxSession(roleName) {
|
|
82
|
-
try {
|
|
83
|
-
await execFileAsync('tmux', ['kill-session', '-t', roleName]);
|
|
84
|
-
}
|
|
85
|
-
catch { /* best effort */ }
|
|
86
|
-
}
|
|
87
76
|
/** Last 4096 chars of run.log, redacted — attached to error reports as diagnostic tail. */
|
|
88
77
|
function readTail(runDir) {
|
|
89
78
|
try {
|
|
@@ -121,19 +110,15 @@ async function discoverLiveTemporaryRoles() {
|
|
|
121
110
|
catch {
|
|
122
111
|
return [];
|
|
123
112
|
}
|
|
124
|
-
const tmux = new Tmux();
|
|
125
113
|
const live = await Promise.all(names.map(async (name) => {
|
|
126
114
|
const dir = agentDir(name, true);
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
tmux.has(name).catch(() => false),
|
|
135
|
-
]);
|
|
136
|
-
return acpAlive || tmuxAlive ? name : undefined;
|
|
115
|
+
const alive = existsSync(controlSocketPath(dir))
|
|
116
|
+
? await controlRequest(dir, { command: 'status' }, 2_000)
|
|
117
|
+
.then(response => response.ok
|
|
118
|
+
&& response.result?.alive === true)
|
|
119
|
+
.catch(() => false)
|
|
120
|
+
: false;
|
|
121
|
+
return alive ? name : undefined;
|
|
137
122
|
}));
|
|
138
123
|
return live.filter((name) => name !== undefined);
|
|
139
124
|
}
|
package/dist/web/auth.d.ts
CHANGED
package/dist/web/runtime.js
CHANGED
|
@@ -4,14 +4,13 @@ import { resolve } from 'node:path';
|
|
|
4
4
|
import { loadConfig } from '../config.js';
|
|
5
5
|
import { RoleRepository } from '../application/role-repository.js';
|
|
6
6
|
import { FleetQueryService } from '../application/fleet-query-service.js';
|
|
7
|
-
import { AcpRoleSessionAdapter
|
|
7
|
+
import { AcpRoleSessionAdapter } from '../application/session-control.js';
|
|
8
8
|
import { StructuredLogService } from '../application/log-service.js';
|
|
9
9
|
import { RoleCommandService } from '../application/role-command-service.js';
|
|
10
10
|
import { RoleCreationService } from '../application/role-creation-service.js';
|
|
11
11
|
import { RoleRemovalService } from '../application/role-removal-service.js';
|
|
12
12
|
import { FleetError } from '../application/errors.js';
|
|
13
13
|
import { controlRequest, controlSocketPath } from '../session/control.js';
|
|
14
|
-
import { Tmux } from '../tmux.js';
|
|
15
14
|
import { pickBackend } from '../supervisor/index.js';
|
|
16
15
|
import { realExec } from '../exec.js';
|
|
17
16
|
import { home, stateRoot } from '../paths.js';
|
|
@@ -23,7 +22,6 @@ import { mergeTopology } from './topology-model.js';
|
|
|
23
22
|
import { TopologyDraftStore } from './topology-draft-store.js';
|
|
24
23
|
import { TopologyPromoteService } from './topology-promote.js';
|
|
25
24
|
import { doctor } from '../doctor.js';
|
|
26
|
-
import { TerminalBridgeManager } from './terminal/bridge.js';
|
|
27
25
|
import { acquireWebServerLock } from './lock.js';
|
|
28
26
|
import { TrustedDeviceStore } from './device-store.js';
|
|
29
27
|
import { WebAuth } from './auth.js';
|
|
@@ -62,7 +60,6 @@ export async function startWebConsole(options) {
|
|
|
62
60
|
}
|
|
63
61
|
const access = options.access ?? new WebAccessStore(webDir).read();
|
|
64
62
|
const auth = new WebAuth(publicOrigin?.origin ?? `http://127.0.0.1:${requestedPort}`, publicOrigin?.host ?? `127.0.0.1:${requestedPort}`, Date.now, new TrustedDeviceStore(webDir), access);
|
|
65
|
-
const tmux = new Tmux();
|
|
66
63
|
const backend = pickBackend();
|
|
67
64
|
const repository = new RoleRepository({
|
|
68
65
|
configPath: options.configPath,
|
|
@@ -78,13 +75,11 @@ export async function startWebConsole(options) {
|
|
|
78
75
|
}
|
|
79
76
|
catch { /* stale socket is evidence, not reachability */ }
|
|
80
77
|
}
|
|
81
|
-
return { acp
|
|
78
|
+
return { acp };
|
|
82
79
|
},
|
|
83
80
|
});
|
|
84
81
|
const events = new FleetEventBus();
|
|
85
82
|
const audit = new AuditSink();
|
|
86
|
-
const terminals = new TerminalBridgeManager({ repository, audit, tmux });
|
|
87
|
-
const terminalAvailable = await terminals.available();
|
|
88
83
|
const watchdogConfigProvider = cachedConfigProvider(options.configPath);
|
|
89
84
|
const log = options.log ?? (() => { });
|
|
90
85
|
let loggedWatchdogFindingsError = false;
|
|
@@ -109,8 +104,8 @@ export async function startWebConsole(options) {
|
|
|
109
104
|
}
|
|
110
105
|
}, CONFIG_CACHE_TTL_MS);
|
|
111
106
|
const query = new FleetQueryService({
|
|
112
|
-
repository, supervisor: backend,
|
|
113
|
-
capabilityContext: {
|
|
107
|
+
repository, supervisor: backend,
|
|
108
|
+
capabilityContext: {},
|
|
114
109
|
watchdogFindings,
|
|
115
110
|
});
|
|
116
111
|
const ops = { backend, binPath: options.binPath, log };
|
|
@@ -162,9 +157,6 @@ export async function startWebConsole(options) {
|
|
|
162
157
|
query, repository, logs, commands, creation, removal, audit, events, watchdogs, configuration,
|
|
163
158
|
taskRooms: new TaskRoomApplicationService(options.configPath),
|
|
164
159
|
topology: readTopology, topologyDrafts, topologyPromote,
|
|
165
|
-
terminalUpgrade: terminalAvailable
|
|
166
|
-
? async (socket, _request, roleId, _ticket, hello) => terminals.connect(socket, roleId, hello)
|
|
167
|
-
: undefined,
|
|
168
160
|
async session(roleId) {
|
|
169
161
|
const role = await repository.get(roleId);
|
|
170
162
|
if (!role)
|
|
@@ -175,8 +167,6 @@ export async function startWebConsole(options) {
|
|
|
175
167
|
throw new FleetError('control_unavailable', 'role state directory is unavailable');
|
|
176
168
|
return new AcpRoleSessionAdapter(dir);
|
|
177
169
|
}
|
|
178
|
-
if (role.configuredBackend === 'tmux' || role.detectedBackend === 'tmux')
|
|
179
|
-
return new TmuxRoleSessionAdapter(roleId, tmux);
|
|
180
170
|
throw new FleetError('capability_unavailable', 'role session backend is unavailable');
|
|
181
171
|
},
|
|
182
172
|
}, { origin: `http://127.0.0.1:${requestedPort}`, host: `127.0.0.1:${requestedPort}` }, { auth });
|
|
@@ -231,7 +221,6 @@ export async function startWebConsole(options) {
|
|
|
231
221
|
async close() {
|
|
232
222
|
try {
|
|
233
223
|
await control.close();
|
|
234
|
-
await terminals.close();
|
|
235
224
|
await server.close();
|
|
236
225
|
}
|
|
237
226
|
finally {
|
package/dist/web/server.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { type FastifyInstance
|
|
2
|
-
import type { WebSocket } from 'ws';
|
|
1
|
+
import { type FastifyInstance } from 'fastify';
|
|
3
2
|
import type { FleetQueryService } from '../application/fleet-query-service.js';
|
|
4
3
|
import type { RoleRepository } from '../application/role-repository.js';
|
|
5
4
|
import type { RoleSessionControl } from '../application/session-control.js';
|
|
@@ -32,7 +31,6 @@ export interface WebServices {
|
|
|
32
31
|
topologyPromote?: TopologyPromoteService;
|
|
33
32
|
removal?: RoleRemovalService;
|
|
34
33
|
taskRooms?: TaskRoomApplicationService;
|
|
35
|
-
terminalUpgrade?: (socket: WebSocket, request: FastifyRequest, roleId: string, ticket: string, hello: Record<string, unknown>) => Promise<void>;
|
|
36
34
|
}
|
|
37
35
|
export interface WebServer {
|
|
38
36
|
app: FastifyInstance;
|
package/dist/web/server.js
CHANGED
|
@@ -101,7 +101,7 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
101
101
|
return {
|
|
102
102
|
version: VERSION, api: { major: 1, minor: 0 },
|
|
103
103
|
websocketProtocols: [
|
|
104
|
-
'ours-fleet-events.v1', 'ours-fleet-
|
|
104
|
+
'ours-fleet-events.v1', 'ours-fleet-conversation.v1',
|
|
105
105
|
],
|
|
106
106
|
auditDegraded: audit.degraded,
|
|
107
107
|
};
|
|
@@ -335,7 +335,7 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
335
335
|
const control = await services.session(request.params.id);
|
|
336
336
|
// The idempotent, durably admitted path — used whenever the caller sends a
|
|
337
337
|
// command id and the role has a conversation ledger. The legacy path stays
|
|
338
|
-
// for old clients
|
|
338
|
+
// for old clients.
|
|
339
339
|
if (control.submitPromptV2) {
|
|
340
340
|
const admittedCommandId = commandId ?? randomBytes(16).toString('hex');
|
|
341
341
|
const receipt = await control.submitPromptV2({
|
|
@@ -440,7 +440,7 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
440
440
|
});
|
|
441
441
|
app.post('/api/v1/ws-tickets', async (request) => {
|
|
442
442
|
const body = request.body;
|
|
443
|
-
if (!body?.purpose || !['events', '
|
|
443
|
+
if (!body?.purpose || !['events', 'conversation'].includes(body.purpose))
|
|
444
444
|
throw new FleetError('invalid_request', 'ticket purpose is required');
|
|
445
445
|
if (body.purpose === 'conversation' && !body.roleId)
|
|
446
446
|
throw new FleetError('invalid_request', 'a conversation ticket must be bound to a role');
|
|
@@ -534,17 +534,6 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
534
534
|
socket.on('close', () => { clearInterval(heartbeat); follow.close(); });
|
|
535
535
|
});
|
|
536
536
|
});
|
|
537
|
-
app.get('/api/v1/roles/:id/terminal', { websocket: true }, (socket, request) => {
|
|
538
|
-
requireSubprotocol(request, 'ours-fleet-terminal.v1');
|
|
539
|
-
authorizeSocket(socket, request, async (hello) => {
|
|
540
|
-
const ticket = String(hello.ticket ?? '');
|
|
541
|
-
const session = auth.consumeTicket(request, ticket, 'terminal', request.params.id);
|
|
542
|
-
auth.bindSocket(session.id, socket);
|
|
543
|
-
if (!services.terminalUpgrade)
|
|
544
|
-
throw new FleetError('capability_unavailable', 'terminal PTY support is unavailable');
|
|
545
|
-
await services.terminalUpgrade(socket, request, request.params.id, ticket, hello);
|
|
546
|
-
});
|
|
547
|
-
});
|
|
548
537
|
const staticRoot = join(dirname(fileURLToPath(import.meta.url)), '..', 'web-app');
|
|
549
538
|
if (existsSync(staticRoot)) {
|
|
550
539
|
await app.register(fastifyStatic, { root: staticRoot, prefix: '/' });
|
|
@@ -126,10 +126,7 @@ function loopEntry(node, merged, model) {
|
|
|
126
126
|
roles, ...pick(node, LOOP_FIELDS),
|
|
127
127
|
};
|
|
128
128
|
entry.interval ??= DEFAULT_LOOP_INTERVAL;
|
|
129
|
-
|
|
130
|
-
// tmux agent would make the whole fleet unloadable, so it arrives switched off
|
|
131
|
-
// and badged instead — the owner turns it on after switching the agent to ACP.
|
|
132
|
-
if (node.enabled === false || !roles.every(role => sessionOf(model, role) === 'acp'))
|
|
129
|
+
if (node.enabled === false)
|
|
133
130
|
entry.enabled = false;
|
|
134
131
|
return entry;
|
|
135
132
|
}
|
|
@@ -142,7 +139,7 @@ function linked(merged, id, kind) {
|
|
|
142
139
|
function sessionOf(model, role) {
|
|
143
140
|
const roles = model.roles;
|
|
144
141
|
const defaults = model.defaults;
|
|
145
|
-
return String(roles?.[role]?.session ?? defaults?.session ?? '
|
|
142
|
+
return String(roles?.[role]?.session ?? defaults?.session ?? 'acp');
|
|
146
143
|
}
|
|
147
144
|
function section(model, key) {
|
|
148
145
|
const existing = model[key];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;color:#e9eef0;background:#080b0c;font-synthesis:none;--panel: #111617;--line: #222a2c;--muted: #849093;--mint: #7ce7b0;--amber: #f0c66c;--red: #ff827c}*{box-sizing:border-box}body{margin:0;min-width:0;min-height:100vh;background:radial-gradient(circle at 70% -20%,#183027 0,transparent 35%),#080b0c}button,input,select,textarea{font:inherit}button{color:inherit}button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible{outline:2px solid var(--mint);outline-offset:2px}.auth-screen{display:grid;place-content:center;text-align:center;min-height:100vh;color:var(--muted)}.auth-screen h1{color:#fff;margin-bottom:0}.brand-wordmark{color:var(--mint);font-size:21px;font-weight:820;letter-spacing:-.02em}.auth-screen .brand-wordmark{justify-self:center;font-size:32px}.shell{display:grid;grid-template-columns:224px 1fr;min-height:100vh}aside{position:fixed;inset:0 auto 0 0;width:224px;display:flex;flex-direction:column;padding:24px 14px;border-right:1px solid #1d2425;background:#080b0ceb;-webkit-backdrop-filter:blur(18px);backdrop-filter:blur(18px)}.brand{display:flex;gap:12px;align-items:center;padding:0 8px 30px}.brand div{display:flex;flex-direction:column}.brand small{color:var(--muted);font-size:11px;text-transform:uppercase;letter-spacing:1.4px}nav{display:grid;gap:5px}nav button{display:grid;grid-template-columns:24px 1fr auto;gap:8px;align-items:center;width:100%;border:0;border-radius:8px;padding:10px 12px;text-align:left;color:#98a3a5;background:transparent;text-transform:capitalize;cursor:pointer}nav button:hover,nav button.active{color:#fff;background:#151b1c}nav button.active{box-shadow:inset 2px 0 var(--mint)}nav b{background:#422f22;color:var(--amber);border-radius:10px;font-size:11px;padding:2px 7px}.sidebar-foot{margin-top:auto;display:grid;grid-template-columns:12px 1fr;align-items:center;padding:12px;color:var(--muted);font-size:12px;text-transform:capitalize}.sidebar-foot i{width:7px;height:7px;border-radius:50%;background:var(--amber)}.sidebar-foot i.live{background:var(--mint);box-shadow:0 0 9px var(--mint)}.sidebar-foot small{grid-column:2;opacity:.6;margin-top:3px}.main{grid-column:2;min-width:0}header{display:flex;justify-content:space-between;align-items:flex-end;height:124px;padding:25px 36px 23px;border-bottom:1px solid var(--line);background:#080b0c8c}h1,h2,h3,p{margin-top:0}header h1{margin:4px 0 0;font-size:29px;letter-spacing:-.8px}.eyebrow{color:var(--mint);text-transform:uppercase;font-size:10px;letter-spacing:1.8px;font-weight:700}.header-actions,.actions{display:flex;gap:9px}button.primary,button.secondary,.actions button{border-radius:7px;padding:9px 13px;cursor:pointer}button.primary{border:1px solid #83e9b5;color:#07130d;background:var(--mint);font-weight:700}button.primary:disabled{opacity:.4;cursor:default}button.secondary,.actions button{border:1px solid #30393b;background:#141a1b}button.secondary:hover,.actions button:hover{border-color:#526064}.content,.workspace{padding:28px 36px 60px}.toolbar{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;color:var(--muted);font-size:12px}.toolbar input{width:340px;padding:10px 13px;color:#fff;background:#101516;border:1px solid var(--line);border-radius:8px}.toolbar>div{display:flex;align-items:center;gap:10px}.toolbar button{padding:7px 10px}.role-row.inactive{opacity:.72}.role-row.active{background:#141a1b;border-left:2px solid var(--mint)}.watchdog-detail-layout{display:grid;gap:18px}.watchdog-report-head{display:flex;align-items:center;gap:12px}.watchdog-report-head h2{flex:1}.watchdog-roles{display:grid;gap:10px;margin-top:12px}.watchdog-role{border:1px solid var(--line);border-radius:8px;padding:12px 14px;background:#0d1213}.watchdog-role-head{display:flex;align-items:center;gap:10px}.watchdog-role-head strong{text-transform:capitalize}.watchdog-role-detail,.watchdog-role-note{margin:8px 0 0;font-size:12px;color:var(--muted)}.watchdog-role-note{color:var(--amber)}.watchdog-role details{margin-top:8px;font-size:12px}.watchdog-role summary{cursor:pointer;color:var(--muted)}.watchdog-role details ul{margin:8px 0 0;padding-left:18px;color:#c9d1d3}.watchdog-role details li{margin-bottom:4px}.status-guide{display:flex;flex-wrap:wrap;gap:7px;align-items:center;margin-bottom:15px;color:var(--muted);font-size:11px}.status-guide>span,.status-chip{display:inline-flex;align-items:center;gap:5px;border:1px solid #30393b;border-radius:12px;padding:3px 7px;white-space:nowrap}.status-guide b{color:#fff;font-size:11px}.status-guide small{flex-basis:100%;color:var(--muted)}.status-guide .ready,.status-chip.ready{border-color:#315e4a}.status-guide .busy,.status-chip.busy{border-color:#315478}.status-guide .attention,.status-chip.attention,.status-chip.unknown{border-color:#66552f}.status-guide .offline,.status-chip.offline{border-color:#693b38}.role-table{border:1px solid var(--line);border-radius:10px;overflow:hidden;background:#0e1213cc}.role-row{display:grid;grid-template-columns:minmax(260px,1.55fr) repeat(4,minmax(125px,1fr));width:100%;align-items:center;gap:15px;padding:14px 18px;border:0;border-bottom:1px solid var(--line);background:transparent;text-align:left;cursor:pointer}button.role-row:hover{background:#141a1b}.role-row.heading{padding-block:10px;color:#6f7b7e;font-size:10px;letter-spacing:1px;text-transform:uppercase;cursor:default}.role-row:last-child{border-bottom:0}.role-row>span{min-width:0;display:flex;flex-direction:column;gap:3px}.role-row strong{font-size:13px;text-transform:capitalize;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.role-row small{color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.role-name{position:relative;display:grid!important;grid-template-columns:auto 1fr auto;align-items:center}.role-name .mission-summary{display:-webkit-box;max-height:2.5em;line-height:1.25;overflow:hidden;overflow-wrap:anywhere;white-space:normal;-webkit-box-orient:vertical;-webkit-line-clamp:2}.status-chip{color:#c9d1d3;font-size:10px;padding:3px 6px}.status-chip i{width:6px;height:6px;border-radius:50%;background:#667174}.status-chip.ready i{background:var(--mint)}.status-chip.busy i{background:#71b7ff}.status-chip.attention i,.status-chip.unknown i{background:var(--amber)}.status-chip.offline i{background:var(--red)}.state.ready,.hero-state.ready i{background:var(--mint);box-shadow:0 0 7px #69dba0}.state.busy,.hero-state.busy i{background:#71b7ff}.state.attention,.state.unknown,.hero-state.attention i,.hero-state.unknown i{background:var(--amber)}.state.offline,.hero-state.offline i{background:var(--red)}.role-name em{color:#7f8a8d;font-size:10px;font-style:normal;border:1px solid #2e3739;border-radius:10px;padding:3px 6px}.role-pills{display:flex;flex-direction:column;align-items:flex-end;gap:4px}.empty{padding:60px;text-align:center;color:var(--muted)}.banner{margin:16px 36px 0;border:1px solid;border-radius:8px;padding:10px 13px;font-size:13px}.banner.error,.error{border-color:#5c302f;background:#271616;color:#ffaaa5}.banner.warning,.warning{border-color:#5a4726;background:#241e12;color:#f1ce83}.banner.info{border-color:#28463b;background:#10231c;color:#9be4bf}.workspace>.banner{margin-inline:0}.back{border:0;background:none;color:var(--muted);padding:0 0 18px;cursor:pointer}.workspace-summary{display:flex;flex-wrap:wrap;align-items:center;gap:7px;color:var(--muted);font-size:12px}.workspace-summary>span{border:1px solid var(--line);border-radius:12px;padding:4px 8px}.workspace-summary>small{margin-left:auto}.hero-state{display:flex;gap:7px;align-items:center;color:#fff;text-transform:capitalize}.hero-state i{width:7px;height:7px;border-radius:50%;background:#788}.tabs{display:flex;gap:22px;margin:26px 0 22px;border-bottom:1px solid var(--line)}.tabs button{border:0;border-bottom:2px solid transparent;padding:10px 2px;background:none;color:var(--muted);text-transform:capitalize;cursor:pointer}.tabs button.active{color:#fff;border-bottom-color:var(--mint)}.tabs button:disabled{opacity:.35;cursor:not-allowed}.dashboard-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}.panel{border:1px solid var(--line);border-radius:10px;background:#101516db;padding:18px}.panel h2{font-size:15px;letter-spacing:-.2px}.panel.wide{grid-column:1 / -1}.evidence>div:first-child{display:flex;justify-content:space-between}.evidence>div:first-child strong{color:var(--mint);text-transform:capitalize;font-size:12px}dl{margin:0}dl div{display:grid;grid-template-columns:120px 1fr;gap:15px;padding:7px 0;border-top:1px solid #1d2426;font-size:12px}dt{color:var(--muted)}dd{margin:0;word-break:break-word}.danger{color:#ff9e99!important;border-color:#613432!important}.activity-layout{display:grid;grid-template-columns:1fr 290px;gap:14px;align-items:start}.activity{display:grid;gap:9px}.activity-filter{display:flex;align-items:center;justify-content:space-between;gap:12px;color:var(--muted);font-size:11px}.activity-filter button{padding:6px 9px}.event{border-left:2px solid #334044;padding:10px 13px;background:#0d1213}.event.agent_text{border-color:var(--mint)}.event.error,.event.permission{border-color:var(--amber)}.event p{margin:5px 0 0;white-space:pre-wrap}.event small,.muted{color:var(--muted)}.composer{position:sticky;top:16px;display:grid;gap:9px}textarea{min-height:84px;resize:vertical}.composer textarea,fieldset input,fieldset select,fieldset textarea{width:100%;color:#fff;border:1px solid #303a3c;border-radius:6px;background:#0d1112;padding:9px 10px}.composer small{color:var(--muted);line-height:1.45}pre{max-width:100%;overflow:auto;white-space:pre-wrap;word-break:break-word;font:12px/1.55 SFMono-Regular,Consolas,monospace;color:#cbd5d7}.log-panel>pre{margin:0;padding:5px 8px;border-bottom:1px solid #1b2224}.audit-list div{display:grid;grid-template-columns:180px 1fr 120px;padding:10px 0;border-top:1px solid var(--line);font-size:12px}.audit-list time{color:var(--muted)}.modal-backdrop{position:fixed;z-index:10;inset:0;display:grid;place-items:center;padding:30px;background:#000000b8;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}.modal{width:min(900px,95vw);max-height:92vh;overflow:auto;border:1px solid #354043;border-radius:13px;background:#0d1112;box-shadow:0 30px 100px #000}.modal-head{position:sticky;z-index:2;top:0;display:flex;justify-content:space-between;align-items:flex-start;padding:22px 26px 18px;border-bottom:1px solid var(--line);background:#0d1112f5}.modal-head h2{margin:4px 0 0}.icon{border:0;background:none;color:var(--muted);font-size:25px;cursor:pointer}.wizard,.progress{padding:22px 26px}fieldset{border:0;border-bottom:1px solid var(--line);padding:0 0 20px;margin:0 0 20px}legend{margin-bottom:13px;color:var(--mint);text-transform:uppercase;font-size:10px;letter-spacing:1.5px;font-weight:700}.form-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:13px}.form-grid.three{grid-template-columns:repeat(3,1fr)}.wake-sources{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:7px 14px}.wake-sources>small{grid-column:1 / -1;color:var(--muted)}.wake-sources label{display:flex;flex-direction:row;align-items:center;gap:7px;color:#c7d0d1;font-size:11px;text-transform:none;letter-spacing:0}.form-grid label{display:grid;gap:6px;color:#b7c0c2;font-size:12px}.form-grid label small{color:var(--muted)}.form-grid .wide{grid-column:1 / -1}.risk{display:flex;align-items:center;gap:8px;margin-top:14px;color:var(--amber);font-size:12px}.review{border:1px solid #2f493f;border-radius:8px;background:#101b17;padding:15px}.review dl{display:grid;grid-template-columns:1fr 1fr;gap:0 20px}.modal-actions{display:flex;justify-content:flex-end;gap:9px;margin-top:20px}.stage{display:flex;gap:12px;min-height:54px;position:relative}.stage:after{position:absolute;left:5px;top:14px;bottom:0;width:1px;background:#34413d;content:""}.stage:last-child:after{display:none}.stage i{z-index:1;width:11px;height:11px;margin-top:2px;border-radius:50%;background:var(--mint);box-shadow:0 0 0 4px #173026}.stage span{display:flex;flex-direction:column;text-transform:capitalize}.stage small{color:var(--muted);margin-top:3px}@media(max-width:1050px){.role-row{grid-template-columns:1.5fr repeat(3,1fr)}.role-row>span:nth-child(4){display:none}}.topology{overflow-x:auto;margin-bottom:16px;border:1px solid var(--line);border-radius:12px;background:radial-gradient(circle at 50% 50%,#13201b 0,#0b1011 58%)}.topology-canvas{position:relative;width:1000px;min-width:100%}.topology svg{position:absolute;inset:0;width:100%;height:100%}.edge line{stroke:#405052;stroke-width:1.5;vector-effect:non-scaling-stroke}.edge.spawned line{stroke:var(--mint);stroke-dasharray:5 4}.edge.watches line{stroke:#d7ad58;stroke-dasharray:2 3}.edge.targets line{stroke:#729ed6;stroke-dasharray:9 3 2 3}.topology-legend{display:grid;gap:8px;margin:0 0 12px;padding:12px 14px;border:1px solid var(--line);border-radius:10px;background:#0c1112}.topology-legend ul{display:grid;grid-template-columns:repeat(4,minmax(140px,1fr));gap:8px;margin:0;padding:0;list-style:none}.topology-legend li{display:flex;gap:9px;align-items:start;padding:7px;border-radius:7px}.topology-legend li:focus{outline:2px solid var(--mint);outline-offset:1px}.topology-legend li>span:last-child{display:grid;gap:2px}.topology-legend small{color:var(--muted);line-height:1.3}.legend-line{width:34px;margin-top:7px;border-top:2px solid #405052;flex:0 0 auto}.legend-line.spawned{border-color:var(--mint);border-top-style:dashed}.legend-line.watches{border-color:#d7ad58;border-top-style:dotted}.legend-line.targets{height:5px;border-color:#729ed6;border-bottom:1px dotted #729ed6}.topology-node{position:absolute;width:150px;min-height:62px;display:grid;gap:3px;z-index:1;padding:0;text-align:left;border:1px solid #344044;border-radius:10px;background:#111718;box-shadow:0 8px 24px #00000047;transform:translateY(-50%)}.topology-open{display:grid;gap:3px;min-width:0;padding:10px 28px 10px 12px;color:inherit;text-align:left;border:0;background:transparent;cursor:pointer}.topology-remove{position:absolute;top:5px;right:5px;width:22px;height:22px;padding:0;color:var(--muted);border:0;background:transparent}.topology-remove:hover,.topology-remove:focus{color:#ff8e85}.topology-node:hover{border-color:var(--mint);transform:translateY(-50%) translateY(-2px)}.topology-open:disabled{cursor:default;opacity:1}.topology-node small{color:var(--muted);text-transform:uppercase;font-size:9px;letter-spacing:1px}.topology-node strong{overflow:hidden;text-overflow:ellipsis}.topology-node span{color:var(--muted);overflow:hidden;white-space:nowrap;text-overflow:ellipsis;font-size:10px}.topology-node.watchdog{border-color:#6a5830}.topology-node.loop{border-color:#385778}.topology-node.temporary{border-style:dashed}.topology-node.attention,.topology-node.unknown{box-shadow:inset 3px 0 var(--amber)}.topology-node.offline{opacity:.65}.lineage-note{color:var(--muted);font-size:11px}.topology-editor{display:grid;gap:12px}.topology-toolbar{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.topology-toolbar .spacer{flex:1 1 auto}.draft-count{color:var(--amber);font-size:11px}.topology-empty{display:grid;gap:10px;justify-items:start;padding:26px;border:1px dashed var(--line);border-radius:12px;background:#0c1112}.topology-empty h3{margin:0}.topology-empty p{margin:0;color:var(--muted)}.topology-tutorial{padding:12px 14px;border:1px solid var(--line);border-radius:10px;background:#0c1112;font-size:12px}.topology-tutorial ol{display:grid;gap:6px;margin:0 0 8px;padding-left:18px;color:var(--muted)}.topology-tutorial li.current{color:#d5dddf}.banner{margin:0;padding:9px 12px;border:1px solid var(--line);border-radius:8px;background:#0c1112;color:var(--muted);font-size:12px}.banner.warning{border-color:#6a5830;color:var(--amber)}.node-badge{display:inline-block;padding:1px 6px;border-radius:999px;font-size:9px;letter-spacing:.3px}.node-badge.draft-incomplete{background:#2a2413;color:var(--amber)}.node-badge.draft-ready{background:#12261f;color:var(--mint)}.node-badge.configured{background:#16202a;color:#9fc0e0}.node-badge.running{background:#12261f;color:var(--mint)}.node-detail{display:block}.topology-node.draft-incomplete,.topology-node.draft-ready{border-style:dashed}.topology-node.selected{border-color:var(--mint);box-shadow:0 0 0 2px #58c7a059}.topology-node[data-node-id]{touch-action:none}.edge.implicit line{stroke-dasharray:1 5;opacity:.75}.edge.dangling line{stroke:var(--red);stroke-dasharray:3 3}.edge.drafted line{opacity:.7}.topology-list{width:100%;border-collapse:collapse;color:#d5dddf;text-align:left;font-size:12px}.topology-list th,.topology-list td{padding:8px;border-top:1px solid var(--line)}.topology-list tr.selected{background:#101718}.node-inspector{display:grid;gap:10px;padding:14px;border:1px solid var(--line);border-radius:12px;background:#0c1112}.node-inspector header{display:flex;align-items:center;justify-content:space-between}.node-inspector h3,.node-inspector h4{margin:0}.node-inspector label{display:grid;gap:4px;font-size:12px}.node-inspector small{color:var(--muted);font-size:10px}.missing-list{display:grid;gap:4px;margin:0;padding-left:18px;color:var(--amber);font-size:12px}.connection-list{display:grid;gap:4px;margin:0;padding-left:18px;color:var(--muted);font-size:11px}.inspector-actions{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.review-panel{display:grid;gap:10px;padding:14px;border:1px solid var(--mint);border-radius:12px;background:#0c1112}.review-panel h3{margin:0}.review-panel .row{display:flex;gap:8px}.topology-fallback{margin:0 0 18px;color:var(--muted);font-size:12px}.topology-fallback summary{cursor:pointer}.topology-fallback table{width:100%;border-collapse:collapse;margin-top:10px;color:#d5dddf;text-align:left}.topology-fallback th,.topology-fallback td{padding:8px;border-top:1px solid var(--line)}.text-button{border:0;padding:0;color:var(--mint);background:transparent;cursor:pointer;text-align:left}.setup-entry{max-width:1180px;margin-inline:auto}.setup-hero{display:grid;grid-template-columns:1fr 300px;gap:30px;align-items:center;padding:12px 0 26px}.setup-hero h2{margin:6px 0 8px;font-size:28px}.setup-hero p{max-width:680px;color:var(--muted)}.setup-safety{display:grid;gap:5px;padding:15px;border:1px solid #315e4a;border-radius:10px;background:#102019}.setup-safety span{color:var(--muted);font-size:12px;line-height:1.45}.setup-steps{display:grid;grid-template-columns:repeat(4,1fr);gap:0;margin:0 0 18px;padding:0;list-style:none}.setup-steps li{display:flex;gap:8px;align-items:center;color:#647174;border-bottom:2px solid #263033;padding:10px;font-size:12px}.setup-steps li.active{color:#fff;border-color:var(--mint)}.setup-steps li.done{color:var(--mint);border-color:#315e4a}.setup-panel{min-height:360px}.setup-panel h3{margin-bottom:6px}.mode-switch{display:flex;gap:8px;align-items:center;margin:0 0 14px;padding:8px;border:1px solid var(--line);border-radius:10px}.mode-switch button.active{color:#07110d;background:var(--mint);border-color:var(--mint)}.mode-switch span{margin-left:6px;color:var(--muted);font-size:12px}.decision-note{display:flex;gap:10px;margin:12px 0;padding:11px;border-left:3px solid var(--mint);background:#102019;color:var(--muted)}.decision-note strong{color:#dce6e2;white-space:nowrap}.setup-actions{display:grid;grid-template-columns:auto 1fr auto;gap:10px;margin-top:22px;padding-top:18px;border-top:1px solid var(--line)}.section-title{display:flex;justify-content:space-between;align-items:start;gap:20px}.entity-list{display:grid;gap:12px}.entity-card{position:relative;padding:15px;border:1px solid var(--line);border-radius:9px;background:#0c1112}.entity-card>.danger{position:absolute;right:14px;bottom:12px}.empty.compact{padding:34px}.preflight{display:grid;gap:4px;margin:12px 0 18px;padding:13px;border:1px solid #315e4a;border-radius:8px;background:#102019}.preflight.failed{border-color:#66552f;background:#211b10}.preflight span{color:var(--muted);font-size:12px}.setup-panel details{margin-top:14px}.setup-panel summary{cursor:pointer;color:#cbd4d6}.config-diff{max-height:360px;padding:14px;border-radius:8px;background:#080b0c}@media(max-width:760px){.topology-legend ul{grid-template-columns:1fr}.mode-switch{align-items:stretch;flex-wrap:wrap}.mode-switch span{flex-basis:100%;margin-left:0}}.check-list{display:grid;gap:7px;padding-left:20px;font-size:12px}.check-list .ok::marker{color:var(--mint)}.check-list .failed::marker{color:var(--red)}@media(max-width:760px){.shell{display:block;padding-bottom:68px}aside{z-index:9;inset:auto 0 0;width:auto;height:64px;padding:7px;border:1px solid var(--line);background:#080b0cf7}aside .brand,.sidebar-foot{display:none}aside nav{grid-template-columns:repeat(5,1fr);gap:2px}aside nav button{display:flex;min-width:0;flex-direction:column;gap:2px;justify-content:center;padding:6px 2px;text-align:center;font-size:9px}aside nav button span{font-size:15px}aside nav b{display:none}.main{width:100%}header{height:auto;min-height:104px;padding:18px;align-items:start;gap:12px}header h1{font-size:23px}.header-actions{flex-wrap:wrap;justify-content:flex-end}.header-actions button{padding:7px 9px;font-size:11px}.content,.workspace{padding:18px 14px 36px}.banner{margin-inline:14px}.toolbar{align-items:stretch;gap:10px;flex-direction:column}.toolbar input{width:100%}.role-table{overflow-x:auto}.role-row{min-width:720px}.setup-hero{grid-template-columns:1fr}.setup-steps li{flex-direction:column;font-size:10px}.form-grid,.form-grid.three{grid-template-columns:1fr}.form-grid .wide{grid-column:auto}.section-title{align-items:stretch;flex-direction:column}}@media(prefers-reduced-motion:reduce){*,*:before,*:after{scroll-behavior:auto!important;transition:none!important;animation:none!important}}.conversation-layout{display:grid;grid-template-rows:1fr auto;gap:14px;min-height:60vh}.conversation{display:flex;flex-direction:column;min-height:0;position:relative}.conversation-header{display:flex;flex-wrap:wrap;gap:12px;align-items:baseline;padding-bottom:10px;border-bottom:1px solid var(--line);font-size:12px}.stream-state{text-transform:uppercase;font-size:10px;letter-spacing:1.4px;font-weight:700;color:var(--amber)}.stream-state.live{color:var(--mint)}.stream-state.offline{color:var(--red)}.conversation-header .warning{color:var(--amber)}.transcript{flex:1;overflow-y:auto;display:grid;gap:14px;padding:16px 4px;max-height:62vh}.turn{display:grid;gap:8px}.bubble{border:1px solid var(--line);border-radius:10px;padding:10px 14px;background:#0d1213}.bubble small{color:var(--muted);display:block;margin-bottom:6px}.bubble.user{border-left:2px solid var(--mint);margin-left:8%;background:#10181a}.bubble.assistant{margin-right:8%}.prompt-text,.assistant-text,.thought-text{margin:0;white-space:pre-wrap;word-break:break-word;font-family:inherit;font-size:14px;line-height:1.5}.assistant-text+.assistant-text{margin-top:10px;padding-top:10px;border-top:1px dashed var(--line)}.thought-text{color:var(--muted);font-size:13px;margin-top:8px}.thoughts{margin-bottom:8px}.streaming-hint{animation:pulse 1.4s ease-in-out infinite}@media(prefers-reduced-motion:reduce){.streaming-hint{animation:none}}@keyframes pulse{50%{opacity:.45}}.tool-card{border:1px solid var(--line);border-radius:8px;padding:7px 11px;margin:6px 0;font-size:13px;background:#101516}.tool-card summary{display:flex;gap:10px;align-items:baseline;cursor:pointer}.tool-detail{display:grid;gap:9px;padding-top:10px}.tool-detail code{display:block;overflow-wrap:anywhere;color:var(--muted);margin-top:4px}.tool-content{border-top:1px dashed var(--line);padding-top:8px;min-width:0}.tool-content pre{max-height:260px;overflow:auto;white-space:pre-wrap;word-break:break-word;margin:5px 0 0;color:#d9e4e2}.diff-old{color:#d99b96!important}.diff-new{color:#9ed8c4!important}.plan-card{border:1px solid var(--line);border-radius:8px;padding:9px 12px;margin:6px 0;font-size:13px}.plan-card ol{list-style:none;margin:7px 0 0;padding:0;display:grid;gap:5px}.plan-entry{display:grid;grid-template-columns:18px 1fr auto;gap:7px;color:var(--muted)}.plan-entry.in_progress{color:var(--amber)}.plan-entry.completed{color:var(--mint)}.usage-badge{margin-top:10px;color:var(--muted)}.tool-status{text-transform:uppercase;font-size:10px;letter-spacing:1px;font-weight:700;color:var(--muted)}.tool-status.in_progress{color:var(--amber)}.tool-status.completed{color:var(--mint)}.tool-status.failed{color:var(--red)}.permission-card{border:1px solid var(--amber);border-radius:8px;padding:10px 12px;margin:8px 0;font-size:13px;display:grid;gap:8px}.permission-card.resolved{border-color:var(--line);color:var(--muted)}.permission-actions{display:flex;gap:8px;flex-wrap:wrap}.jump-latest{position:absolute;right:20px;bottom:16px}.conversation-layout .composer{display:grid;gap:8px}.conversation-layout .composer textarea{min-height:68px;max-height:200px;resize:vertical;color:#fff;background:#101516;border:1px solid var(--line);border-radius:8px;padding:10px 13px}.composer-actions{display:flex;gap:9px;justify-content:space-between}.composer-actions .danger{margin-left:auto}button.danger{border:1px solid #5b322f;color:var(--red);background:#1a1213;border-radius:7px;padding:9px 13px;cursor:pointer}button.danger:disabled{opacity:.4;cursor:default}
|