@openparachute/agent 0.2.2 → 0.2.3-rc.10
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/.parachute/module.json +3 -3
- package/package.json +4 -1
- package/src/agent-defs.ts +9 -0
- package/src/auth.ts +182 -14
- package/src/backends/registry.ts +65 -27
- package/src/daemon.ts +311 -12
- package/src/def-vault-triggers.ts +317 -0
- package/src/preflight.ts +139 -0
- package/src/spawn-agent.ts +16 -0
- package/src/step-up.ts +316 -0
- package/src/terminal-ui.ts +73 -0
- package/src/transports/http-ui.ts +10 -8
- package/src/transports/vault.ts +40 -22
- package/src/ui-kit.ts +6 -3
- package/src/ui-ticket.ts +121 -0
- package/web/ui/dist/assets/index-Dhr5Kl_d.css +1 -0
- package/web/ui/dist/assets/index-Di5MmFZR.js +60 -0
- package/web/ui/dist/index.html +2 -2
- package/src/_parked/interactive-spawn.test.ts +0 -324
- package/src/_parked/interactive-spawn.ts +0 -701
- package/src/agent-defs.test.ts +0 -1504
- package/src/agent-mcp-config.test.ts +0 -115
- package/src/agents.test.ts +0 -360
- package/src/auth.test.ts +0 -46
- package/src/backends/attached-queue.test.ts +0 -376
- package/src/backends/programmatic.test.ts +0 -1715
- package/src/backends/registry.test.ts +0 -1494
- package/src/backends/stream-json.test.ts +0 -570
- package/src/channel-backend-wiring.test.ts +0 -237
- package/src/credentials.test.ts +0 -274
- package/src/cron.test.ts +0 -342
- package/src/daemon-agent-def-api.test.ts +0 -166
- package/src/daemon-agent-defs-api.test.ts +0 -953
- package/src/daemon-agent-env-api.test.ts +0 -338
- package/src/daemon-attached-queue-store.test.ts +0 -65
- package/src/daemon-config-api.test.ts +0 -962
- package/src/daemon-jobs-api.test.ts +0 -271
- package/src/daemon-vault-chat.test.ts +0 -250
- package/src/daemon.test.ts +0 -746
- package/src/def-vaults.test.ts +0 -136
- package/src/delivery-state.test.ts +0 -110
- package/src/effective-env.test.ts +0 -114
- package/src/grants.test.ts +0 -638
- package/src/hub-jwt.test.ts +0 -161
- package/src/jobs.test.ts +0 -245
- package/src/mcp-http.test.ts +0 -265
- package/src/mint-token.test.ts +0 -152
- package/src/module-manifest.test.ts +0 -158
- package/src/programmatic-wiring.test.ts +0 -838
- package/src/registry.test.ts +0 -227
- package/src/resolve-port.test.ts +0 -64
- package/src/routing.test.ts +0 -184
- package/src/runner.test.ts +0 -506
- package/src/sandbox/config.test.ts +0 -150
- package/src/sandbox/egress.test.ts +0 -113
- package/src/sandbox/live-seatbelt.test.ts +0 -277
- package/src/sandbox/mounts.test.ts +0 -154
- package/src/sandbox/sandbox.test.ts +0 -168
- package/src/services-manifest.test.ts +0 -106
- package/src/spa-serve.test.ts +0 -116
- package/src/spawn-agent-cli.test.ts +0 -172
- package/src/spawn-agent.test.ts +0 -1218
- package/src/spawn-deps.test.ts +0 -54
- package/src/terminal-assets.test.ts +0 -50
- package/src/terminal.test.ts +0 -530
- package/src/transports/http-ui.test.ts +0 -455
- package/src/transports/telegram.test.ts +0 -174
- package/src/transports/vault.test.ts +0 -2011
- package/src/ui-kit.test.ts +0 -178
- package/web/ui/dist/assets/index-C-iWdFFV.css +0 -1
- package/web/ui/dist/assets/index-VFETBk0a.js +0 -60
- package/web/ui/tsconfig.json +0 -21
package/src/ui-ticket.ts
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-time SSE tickets for the browser EventSource auth path (Layer 2, human↔UI).
|
|
3
|
+
*
|
|
4
|
+
* THE LEAK THIS CLOSES. A browser `EventSource` can't set an `Authorization`
|
|
5
|
+
* header, so the agent SPA used to put the hub JWT directly in the SSE URL
|
|
6
|
+
* (`/ui/events?token=<JWT>`, `/api/channels/<ch>/turn-events?token=<JWT>`). A
|
|
7
|
+
* full bearer JWT in a URL lands in any access log, proxy log, browser history,
|
|
8
|
+
* or network trace — mitigated before only by the token's ~10min TTL. That's a
|
|
9
|
+
* credential-in-a-URL leak.
|
|
10
|
+
*
|
|
11
|
+
* THE FIX. Trade the JWT for an opaque, single-use, very-short-lived TICKET that
|
|
12
|
+
* goes in the URL instead. The SPA presents its bearer JWT to a normal
|
|
13
|
+
* authenticated endpoint (a Bearer header on a `fetch`, no leak), which mints a
|
|
14
|
+
* ticket: a crypto-random 256-bit nonce (base64url) stored ONLY server-side in
|
|
15
|
+
* this TTL'd map, carrying the validated scope(s)/audience of the presenting
|
|
16
|
+
* token. The SPA opens `/ui/events?ticket=<nonce>`; the SSE consume path looks
|
|
17
|
+
* the nonce up, CONSUMES it (deletes immediately — single-use), and establishes
|
|
18
|
+
* the stream with the ticket's scopes. The JWT never appears in a URL or log.
|
|
19
|
+
*
|
|
20
|
+
* SECURITY PROPERTIES (all load-bearing):
|
|
21
|
+
* - Unguessable: 32 random bytes (256 bits) from `crypto.getRandomValues`,
|
|
22
|
+
* base64url-encoded. Far above the issue's 128-bit floor.
|
|
23
|
+
* - Single-use: `consume` DELETES the entry before returning, so a replayed
|
|
24
|
+
* ticket (a second connect, or a stolen URL) finds nothing → 401.
|
|
25
|
+
* - Short TTL: default 60s — just long enough to open the connection. An
|
|
26
|
+
* expired entry is treated as absent (and lazily pruned).
|
|
27
|
+
* - No scope widening: the ticket stores EXACTLY the scopes the minting token
|
|
28
|
+
* presented (validated upstream by `requireScope` before `mint` is called).
|
|
29
|
+
* The consume path asserts the required scope against the stored set, so a
|
|
30
|
+
* ticket can never authorize more than the JWT that minted it.
|
|
31
|
+
*
|
|
32
|
+
* This is process-local in-memory state by design (mirrors the daemon's other
|
|
33
|
+
* in-process registries). The daemon is single-instance per machine; tickets
|
|
34
|
+
* live ≤60s and are cheap to lose on restart (the SPA just re-mints). A
|
|
35
|
+
* module-level singleton is used because the two consume paths live in different
|
|
36
|
+
* modules (`http-ui.ts`'s `ingestHttp` and the daemon's turn-events route) and
|
|
37
|
+
* both must hit the SAME store — `ingestHttp(req, url)` has no place to thread an
|
|
38
|
+
* instance through. `_resetTicketsForTest` isolates unit tests.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/** A minted ticket's server-side record. Never leaves the process. */
|
|
42
|
+
interface TicketRecord {
|
|
43
|
+
/** The validated scopes carried from the minting JWT (the ceiling — never widened). */
|
|
44
|
+
scopes: string[];
|
|
45
|
+
/** Epoch ms after which the ticket is expired (treated as absent). */
|
|
46
|
+
expiresAt: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Default ticket lifetime — long enough to open an EventSource, no longer. */
|
|
50
|
+
export const TICKET_TTL_MS = 60_000;
|
|
51
|
+
|
|
52
|
+
/** Nonce entropy: 32 bytes = 256 bits, well above the 128-bit floor. */
|
|
53
|
+
const TICKET_BYTES = 32;
|
|
54
|
+
|
|
55
|
+
/** The process-local ticket store. nonce → record. */
|
|
56
|
+
const tickets = new Map<string, TicketRecord>();
|
|
57
|
+
|
|
58
|
+
/** base64url-encode bytes (no padding) — URL-safe, no `+`/`/`/`=`. */
|
|
59
|
+
function base64url(bytes: Uint8Array): string {
|
|
60
|
+
let bin = "";
|
|
61
|
+
for (const b of bytes) bin += String.fromCharCode(b);
|
|
62
|
+
return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Mint a single-use ticket carrying `scopes` (a COPY of the validated scopes from
|
|
67
|
+
* the presenting JWT — the caller must have already authenticated + scope-checked
|
|
68
|
+
* the token, so this never widens authority). Returns the opaque nonce + its
|
|
69
|
+
* absolute expiry. TTL defaults to {@link TICKET_TTL_MS}.
|
|
70
|
+
*/
|
|
71
|
+
export function mintTicket(scopes: readonly string[], ttlMs = TICKET_TTL_MS): {
|
|
72
|
+
ticket: string;
|
|
73
|
+
expiresAt: number;
|
|
74
|
+
} {
|
|
75
|
+
pruneExpiredTickets();
|
|
76
|
+
const bytes = new Uint8Array(TICKET_BYTES);
|
|
77
|
+
crypto.getRandomValues(bytes);
|
|
78
|
+
const ticket = base64url(bytes);
|
|
79
|
+
const expiresAt = Date.now() + ttlMs;
|
|
80
|
+
tickets.set(ticket, { scopes: [...scopes], expiresAt });
|
|
81
|
+
return { ticket, expiresAt };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Consume a ticket: look it up, and if present + unexpired, DELETE it (single-use)
|
|
86
|
+
* and return its scopes. Returns `null` for an absent / expired / already-consumed
|
|
87
|
+
* ticket — the caller maps that to a 401. Deletion happens before return, so two
|
|
88
|
+
* concurrent consumes of the same nonce can't both succeed.
|
|
89
|
+
*/
|
|
90
|
+
export function consumeTicket(ticket: string | null | undefined): { scopes: string[] } | null {
|
|
91
|
+
if (!ticket) return null;
|
|
92
|
+
const rec = tickets.get(ticket);
|
|
93
|
+
if (!rec) return null;
|
|
94
|
+
// Single-use: remove FIRST, so even an expired hit can't be retried and a
|
|
95
|
+
// concurrent second consume finds nothing.
|
|
96
|
+
tickets.delete(ticket);
|
|
97
|
+
if (Date.now() >= rec.expiresAt) return null;
|
|
98
|
+
return { scopes: rec.scopes };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Drop every expired ticket. Called opportunistically on each mint so the map
|
|
103
|
+
* can't grow unbounded if some tickets are never consumed; not on a timer (no
|
|
104
|
+
* background work in a possibly-idle daemon). O(n) over a map that's tiny in
|
|
105
|
+
* practice (≤ a handful of live tickets at 60s TTL).
|
|
106
|
+
*/
|
|
107
|
+
export function pruneExpiredTickets(now = Date.now()): void {
|
|
108
|
+
for (const [k, rec] of tickets) {
|
|
109
|
+
if (now >= rec.expiresAt) tickets.delete(k);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Test seam: clear all tickets so unit tests start from a clean store. */
|
|
114
|
+
export function _resetTicketsForTest(): void {
|
|
115
|
+
tickets.clear();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Test seam: the current live ticket count (asserts single-use deletion). */
|
|
119
|
+
export function _ticketCountForTest(): number {
|
|
120
|
+
return tickets.size;
|
|
121
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{--bg: #faf8f4;--bg-soft: #f3f0ea;--fg: #2c2a26;--fg-muted: #6b6860;--fg-dim: #9a9690;--accent: #4a7c59;--accent-soft: rgba(74, 124, 89, .08);--accent-hover: #3d6849;--border: #e4e0d8;--border-light: #ece9e2;--card-bg: #ffffff;--error: #a3392b;--error-soft: rgba(163, 57, 43, .08);--warn: #b08023;--warn-soft: rgba(176, 128, 35, .08);--success: #3d6849;--success-soft: rgba(61, 104, 73, .08);--font-serif: Georgia, "Times New Roman", serif;--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;--font-mono: ui-monospace, "SF Mono", Menlo, Monaco, "Cascadia Mono", monospace;font-family:var(--font-sans)}*{box-sizing:border-box}html,body{margin:0;padding:0;background:var(--bg);color:var(--fg)}a{color:var(--accent);text-decoration:none}a:hover{text-decoration:underline}button{font:inherit;background:var(--accent);color:#fff;border:0;border-radius:6px;padding:.45rem .9rem;cursor:pointer;transition:background .15s ease}button:hover{background:var(--accent-hover)}button:disabled{opacity:.5;cursor:not-allowed}button.secondary{background:#fff;color:var(--fg);border:1px solid var(--border)}button.secondary:hover{background:var(--bg-soft)}code{font-family:var(--font-mono);font-size:.85em;background:var(--bg-soft);padding:.1em .3em;border-radius:3px}.page{max-width:960px;margin:0 auto;padding:1.5rem 1.5rem 6rem}.nav{display:flex;flex-wrap:wrap;gap:.6rem 1rem;align-items:center;padding-bottom:1rem;border-bottom:1px solid var(--border);margin-bottom:2rem}.nav .brand{font-weight:600;font-family:var(--font-serif);font-size:1.15rem;margin-right:auto;display:inline-flex;align-items:center;gap:.45rem;color:var(--accent);text-decoration:none}.nav .brand:hover{color:var(--accent-hover);text-decoration:none}.nav .brand-wordmark{color:var(--fg);letter-spacing:-.005em}.nav .brand .sub{color:var(--fg-dim);font-size:.78rem;font-weight:400;margin-left:.4rem;font-family:var(--font-sans)}.nav a{color:var(--fg-muted);font-size:.95rem}.nav a:hover{text-decoration:none;color:var(--fg)}.nav a.nav-link-active{color:var(--accent);font-weight:500;text-decoration:underline;text-underline-offset:.3em;text-decoration-thickness:2px}h1{margin:0 0 .5rem;font-family:var(--font-serif);font-size:1.85rem;font-weight:400;letter-spacing:-.01em;line-height:1.2;color:var(--fg)}h2{margin:0 0 1rem;font-size:1.4rem;font-weight:500}h3{margin:0 0 .5rem;font-size:1.05rem;font-weight:600}.muted{color:var(--fg-muted);font-size:.92rem}.dim{color:var(--fg-dim);font-size:.85rem}.lede{color:var(--fg-muted);font-size:.95rem;margin:0 0 1.5rem;max-width:60ch}.error-banner{background:var(--error-soft);border:1px solid var(--error);color:var(--error);padding:.75rem 1rem;border-radius:8px;margin-bottom:1rem;font-size:.9rem}.info-banner{background:var(--accent-soft);border:1px solid var(--accent);color:var(--fg);padding:.65rem 1rem;border-radius:8px;margin-bottom:1rem;font-size:.88rem}.empty{border:1px dashed var(--border);border-radius:10px;padding:2rem 1.5rem;text-align:center;color:var(--fg-muted);background:var(--card-bg)}.loading{color:var(--fg-muted);padding:1rem 0;font-size:.92rem}.card{background:var(--card-bg);border:1px solid var(--border);border-radius:12px;padding:1.1rem 1.25rem;box-shadow:0 1px 2px #2c2a260a,0 8px 24px #2c2a260d;margin-bottom:1.5rem}.section-head{display:flex;align-items:baseline;justify-content:space-between;gap:1rem;margin-bottom:.75rem}.section-head .count{color:var(--fg-dim);font-size:.85rem}table{width:100%;border-collapse:collapse;font-size:.88rem}th,td{text-align:left;padding:.6rem .7rem;border-bottom:1px solid var(--border);vertical-align:middle}th{color:var(--fg-muted);font-weight:500;font-size:.78rem;text-transform:uppercase;letter-spacing:.03em}tr.agent-row{cursor:pointer}tr.agent-row:hover{background:var(--bg-soft)}tr.agent-row.selected{background:var(--accent-soft)}.cell-name{font-weight:600;color:var(--fg)}.cell-dim{color:var(--fg-dim)}.pill{display:inline-block;padding:.1rem .5rem;border-radius:999px;font-size:.72rem;font-weight:600;letter-spacing:.02em;border:1px solid var(--border);color:var(--fg-muted);background:var(--bg-soft)}.pill.backend-programmatic{color:var(--accent);background:var(--accent-soft);border-color:transparent}.pill.backend-channel{color:var(--warn);background:var(--warn-soft);border-color:transparent}.pill.status-enabled,.pill.status-idle{color:var(--success);background:var(--success-soft);border-color:transparent}.pill.status-working{color:var(--accent);background:var(--accent-soft);border-color:transparent}.pill.status-pending,.pill.status-queued{color:var(--warn);background:var(--warn-soft);border-color:transparent}.pill.status-error{color:var(--error);background:var(--error-soft);border-color:transparent}.detail{background:var(--card-bg);border:1px solid var(--border);border-radius:12px;padding:1.25rem 1.4rem;margin-bottom:1.5rem;box-shadow:0 1px 2px #2c2a260a,0 8px 24px #2c2a260d}.detail-head{display:flex;align-items:center;gap:.6rem;flex-wrap:wrap;margin-bottom:.9rem}.detail-head h2{margin:0;font-size:1.25rem}.detail-grid{display:grid;grid-template-columns:max-content 1fr;gap:.45rem 1.2rem;font-size:.9rem;margin-bottom:1rem}.detail-grid dt{color:var(--fg-muted);font-weight:500}.detail-grid dd{margin:0;color:var(--fg);word-break:break-word}.detail-prompt{background:var(--bg-soft);border:1px solid var(--border-light);border-radius:8px;padding:.75rem .9rem;font-family:var(--font-mono);font-size:.82rem;white-space:pre-wrap;color:var(--fg);margin:.3rem 0 1rem}.detail-note{font-size:.82rem;color:var(--fg-dim);margin:.5rem 0 0}.detail-close{margin-left:auto;background:#fff;color:var(--fg-muted);border:1px solid var(--border);padding:.3rem .7rem;font-size:.82rem}.detail-close:hover{background:var(--bg-soft);color:var(--fg)}.tag-list{display:flex;flex-wrap:wrap;gap:.35rem}.tag{font-family:var(--font-mono);font-size:.76rem;background:var(--bg-soft);border:1px solid var(--border-light);border-radius:4px;padding:.05rem .35rem;color:var(--fg-muted)}.section-head-actions{display:inline-flex;align-items:center;gap:.9rem}.button-link{display:inline-block;background:var(--accent);color:#fff;border-radius:6px;padding:.4rem .85rem;font-size:.85rem;font-weight:500;transition:background .15s ease}.button-link:hover{background:var(--accent-hover);color:#fff;text-decoration:none}.field{display:block;border:0;margin:0 0 1.25rem;padding:0}.field>label,.field>legend{display:block;font-weight:500;font-size:.9rem;color:var(--fg);margin-bottom:.35rem;padding:0}.field input[type=text],.field select,.field textarea{width:100%;font:inherit;font-size:.9rem;color:var(--fg);background:var(--card-bg);border:1px solid var(--border);border-radius:6px;padding:.5rem .6rem}.field textarea{font-family:var(--font-mono);font-size:.82rem;resize:vertical}.field input[type=text]:focus,.field select:focus,.field textarea:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-soft)}.field-hint{color:var(--fg-dim);font-size:.8rem;margin:.3rem 0 0}.field-error{color:var(--error);font-size:.8rem;margin:.3rem 0 0}.radio-row{display:flex;align-items:flex-start;gap:.6rem;border:1px solid var(--border);border-radius:8px;padding:.65rem .8rem;margin-bottom:.5rem;cursor:pointer;transition:border-color .15s ease,background .15s ease}.radio-row:hover{background:var(--bg-soft)}.radio-row.selected{border-color:var(--accent);background:var(--accent-soft)}.radio-row input[type=radio]{margin-top:.2rem;accent-color:var(--accent)}.radio-body{display:flex;flex-direction:column;gap:.15rem}.radio-label{font-weight:500;font-size:.9rem;color:var(--fg)}.radio-help{font-size:.8rem;color:var(--fg-muted)}.advanced{margin:0 0 1.25rem}.advanced>summary{cursor:pointer;font-size:.88rem;font-weight:500;color:var(--fg-muted);margin-bottom:.75rem}.advanced>summary:hover{color:var(--fg)}.form-actions{display:flex;align-items:center;gap:1rem}.cancel-link{color:var(--fg-muted);font-size:.9rem}.success-banner{background:var(--success-soft);border:1px solid var(--success);color:var(--success);padding:.75rem 1rem;border-radius:8px;margin-bottom:1rem;font-size:.95rem}.snippet-row{display:flex;align-items:stretch;gap:.6rem;margin:.5rem 0}.snippet{flex:1;font-family:var(--font-mono);font-size:.8rem;background:var(--bg-soft);border:1px solid var(--border-light);border-radius:6px;padding:.6rem .75rem;color:var(--fg);white-space:pre-wrap;word-break:break-all}.detail-actions{display:flex;gap:.6rem;margin-top:1.25rem;padding-top:1rem;border-top:1px solid var(--border-light)}button.button-danger{background:#fff;color:var(--error);border:1px solid var(--error)}button.button-danger:hover{background:var(--error-soft)}button.button-danger:disabled{opacity:.5;cursor:not-allowed}.confirm-box{margin-top:1.25rem;padding:1rem;border:1px solid var(--error);border-radius:8px;background:var(--error-soft)}.confirm-prompt{margin:0 0 .75rem;font-size:.9rem;color:var(--fg)}.confirm-inline{display:inline-flex;align-items:center;gap:.6rem}.inline-form{margin:.75rem 0 1rem}.schedules,.detail-section{margin-top:1.25rem;padding-top:1rem;border-top:1px solid var(--border-light)}.schedules .section-head h3,.detail-section .section-head h3{margin:0;font-size:1.05rem}.schedule-presets{display:flex;flex-wrap:wrap;gap:.4rem;margin-top:.45rem}.schedule-presets button{font-size:.78rem;padding:.2rem .55rem}.schedule-row-actions{display:inline-flex;align-items:center;gap:.6rem}.schedule-status{font-size:.82rem;color:var(--fg-muted);margin:.5rem 0 0}.chat-head{display:flex;align-items:baseline;flex-wrap:wrap;gap:.75rem 1rem;margin-bottom:1rem}.chat-head h1{margin:0}.chat-picker{display:inline-flex;align-items:center;gap:.5rem}.chat-picker-label{font-size:.82rem;color:var(--fg-muted)}.chat-picker select{font:inherit;font-size:.88rem;color:var(--fg);background:var(--card-bg);border:1px solid var(--border);border-radius:6px;padding:.35rem .5rem}.chat-status{font-size:.8rem;color:var(--fg-muted);margin-left:auto}.chat-status.status-live{color:var(--success)}.chat-status.status-err{color:var(--error)}.transcript{display:flex;flex-direction:column;gap:.5rem;height:60vh;min-height:18rem;overflow-y:auto;padding:1rem;background:var(--card-bg);border:1px solid var(--border);border-radius:12px 12px 0 0}.transcript .msg{max-width:78%;padding:.5rem .75rem;border-radius:12px;font-size:.9rem;line-height:1.4;white-space:pre-wrap;word-wrap:break-word;overflow-wrap:anywhere}.transcript .msg.you{align-self:flex-end;background:var(--accent);color:#fff;border-bottom-right-radius:4px}.transcript .msg.them{align-self:flex-start;background:var(--bg-soft);color:var(--fg);border:1px solid var(--border);border-bottom-left-radius:4px}.transcript .msg.sys{align-self:center;background:transparent;color:var(--fg-muted);font-size:.8rem;font-style:italic;max-width:90%;text-align:center}.transcript .msg.live{border-style:dashed;animation:chatLivePulse 1.4s ease-in-out infinite}@keyframes chatLivePulse{0%,to{opacity:1}50%{opacity:.6}}.transcript .msg.live.errored{border-color:var(--error);color:var(--error);animation:none}.transcript .live-tools{display:flex;flex-wrap:wrap;gap:.25rem;margin-top:.4rem}.transcript .tool-chip{font-family:var(--font-mono);font-size:.72rem;padding:.05rem .45rem;border-radius:10px;background:var(--card-bg);color:var(--fg-muted);border:1px solid var(--border)}.transcript .live-working{margin-top:.4rem;font-size:.75rem;color:var(--fg-muted);font-style:italic}.composer{display:flex;gap:.6rem;padding:.75rem 1rem;background:var(--card-bg);border:1px solid var(--border);border-top:0;border-radius:0 0 12px 12px;margin-bottom:1.5rem}.composer .chat-input{flex:1;font:inherit;font-size:.9rem;color:var(--fg);background:var(--card-bg);border:1px solid var(--border);border-radius:8px;padding:.55rem .7rem;resize:none;max-height:7.5rem}.composer .chat-input:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-soft)}.composer button{flex:0 0 auto;align-self:flex-end}.nav .nav-link-button{background:none;border:none;padding:0;margin:0;cursor:pointer;color:var(--fg-muted);font-size:.95rem;font-family:inherit}.nav .nav-link-button:hover{color:var(--fg)}.step-up-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;background:#2c2a2673;display:flex;align-items:center;justify-content:center;padding:1rem;z-index:1000}.step-up-modal{background:var(--card-bg);border:1px solid var(--border);border-radius:10px;box-shadow:0 12px 40px #2c2a2633;padding:1.5rem;width:100%;max-width:26rem}.step-up-modal h2{margin:0 0 .5rem;font-family:var(--font-serif);font-size:1.25rem}.step-up-sub{color:var(--fg-muted);font-size:.9rem;margin:0 0 1rem}.step-up-field{display:block;margin-bottom:.85rem}.step-up-field>span{display:block;font-size:.85rem;color:var(--fg-muted);margin-bottom:.3rem}.step-up-field input{width:100%;padding:.5rem .6rem;border:1px solid var(--border);border-radius:6px;font-family:var(--font-mono);font-size:1.1rem;letter-spacing:.25em;background:var(--bg);color:var(--fg);box-sizing:border-box}.step-up-field input:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-soft)}.step-up-error{color:var(--error);background:var(--error-soft);border-radius:6px;padding:.5rem .6rem;font-size:.85rem;margin:0 0 .85rem}.step-up-actions{display:flex;justify-content:flex-end;gap:.6rem;margin-top:.5rem}
|