agent-shim 8.16.0 → 8.17.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 CHANGED
@@ -248,6 +248,7 @@ Issues and pull requests are welcome. Please keep the tool itself free of assump
248
248
  - [docs/development.md](docs/development.md): test isolation, commit hooks, and the secret-redaction filter rationale.
249
249
  - [docs/headroom-measurement.md](docs/headroom-measurement.md): measuring what headroom's transforms are worth on real conversations, offline: the replay method, the cache-weighted cost model and its limits.
250
250
  - [docs/rc-interception-rig.md](docs/rc-interception-rig.md): the Remote Control interception rig: bring-up, per-host TLS proof, teardown, and the manual login step.
251
+ - [docs/rc-web-client.md](docs/rc-web-client.md): the self-hosted Remote Control web client: the page the door serves behind its control token for browser control of its sessions, how it authenticates and speaks the typed API, and how it is tested.
251
252
 
252
253
  ## License
253
254
 
package/dist/cli.cjs CHANGED
@@ -61749,7 +61749,7 @@ function registerIdentityCommand(program2, deps) {
61749
61749
  // package.json
61750
61750
  var package_default = {
61751
61751
  name: "agent-shim",
61752
- version: "8.16.0",
61752
+ version: "8.17.1",
61753
61753
  description: "A profile manager and launcher for Claude Code that lets one person run multiple logins from one machine while controlling what gets shared between them.",
61754
61754
  license: "MIT",
61755
61755
  author: "Joseph Mearman <joseph@mearman.co.uk>",
@@ -67093,7 +67093,7 @@ function frontDoorRcControl(transport, token) {
67093
67093
  var HEALTH_TIMEOUT_MS = 2e3;
67094
67094
  var HEALTH_START_BUDGET_MS = 5e3;
67095
67095
  var HEALTH_RETRY_MS = 50;
67096
- function createFrontDoorServer(pipeline, log, tls2, control, api) {
67096
+ function createFrontDoorServer(pipeline, log, tls2, control, apis) {
67097
67097
  const handler = (request7, response) => {
67098
67098
  if (request7.method === "GET" && request7.url === "/healthz") {
67099
67099
  response.writeHead(HTTP_STATUS.ok, { "Content-Type": "text/plain" });
@@ -67104,7 +67104,8 @@ function createFrontDoorServer(pipeline, log, tls2, control, api) {
67104
67104
  control(request7, response);
67105
67105
  return;
67106
67106
  }
67107
- if (api !== void 0 && (request7.url ?? "/").startsWith(api.pathPrefix)) {
67107
+ const api = apis?.find((candidate) => (request7.url ?? "/").startsWith(candidate.pathPrefix));
67108
+ if (api !== void 0) {
67108
67109
  api.handle(request7, response).then((result) => {
67109
67110
  if (!result.matched && !response.headersSent) {
67110
67111
  response.writeHead(HTTP_STATUS.notFound, { "Content-Type": "application/json" });
@@ -73408,6 +73409,466 @@ function createRcCredentialStore(fs19, dir) {
73408
73409
  };
73409
73410
  }
73410
73411
 
73412
+ // src/frontdoor/rcClientPage.ts
73413
+ var RC_CLIENT_PATH = "/__agent-shim/client";
73414
+ var RC_CLIENT_PAGE_SCRIPT = `'use strict';
73415
+ // The door's typed API as this page speaks it: the oRPC RPC protocol over plain fetch, which is one POST per procedure with a {"json": input} body and a {"json": output} answer, so no client library is needed. These helpers are plain top-level functions (not boot-scoped) so the page's tests can evaluate this script outside a browser and drive them against a mounted listener.
73416
+ var API_PREFIX = '/__agent-shim/orpc';
73417
+ var TOKEN_KEY = 'agent-shim-rc-token';
73418
+ var token = null;
73419
+
73420
+ function apiBase() { return window.location.origin + API_PREFIX; }
73421
+
73422
+ // Calls one procedure: resolves the output's own value, rejects with the door's error message when it answered one.
73423
+ async function rpc(path, input) {
73424
+ var response = await fetch(apiBase() + '/' + path, {
73425
+ method: 'POST',
73426
+ headers: { 'content-type': 'application/json', authorization: 'Bearer ' + token },
73427
+ body: JSON.stringify({ json: input === undefined ? {} : input }),
73428
+ });
73429
+ var envelope = await response.json().catch(function () { return {}; });
73430
+ if (!response.ok) {
73431
+ var message = envelope && envelope.json && envelope.json.message ? envelope.json.message : ('HTTP ' + response.status);
73432
+ var error = new Error(message);
73433
+ error.status = response.status;
73434
+ throw error;
73435
+ }
73436
+ return envelope.json;
73437
+ }
73438
+
73439
+ // Parses every complete SSE frame out of the buffered text, returning the frames and the unparsed remainder. Comment frames (the door's keepalive opens with one) carry no event name and are dropped.
73440
+ function parseSseFrames(buffer) {
73441
+ var frames = [];
73442
+ var rest = buffer;
73443
+ for (;;) {
73444
+ var boundary = rest.indexOf('\\n\\n');
73445
+ if (boundary === -1) { break; }
73446
+ var raw = rest.slice(0, boundary);
73447
+ rest = rest.slice(boundary + 2);
73448
+ var frame = { event: 'message', id: '', data: '' };
73449
+ var lines = raw.split('\\n');
73450
+ for (var i = 0; i < lines.length; i++) {
73451
+ var line = lines[i];
73452
+ if (line === '' || line.charAt(0) === ':') { continue; }
73453
+ var colon = line.indexOf(':');
73454
+ var field = colon === -1 ? line : line.slice(0, colon);
73455
+ var value = colon === -1 ? '' : line.slice(colon + 1);
73456
+ if (value.charAt(0) === ' ') { value = value.slice(1); }
73457
+ if (field === 'event') { frame.event = value; }
73458
+ if (field === 'id') { frame.id = value; }
73459
+ if (field === 'data') { frame.data += (frame.data === '' ? '' : '\\n') + value; }
73460
+ }
73461
+ if (frame.data !== '') { frames.push(frame); }
73462
+ }
73463
+ return { frames: frames, rest: rest };
73464
+ }
73465
+
73466
+ // Whether one source-tagged event continues the per-source sequence the tracker holds: the count of events the stream dropped when it does not, zero when it does. The tracker keeps the high-water mark, so a number the door re-sends after a reconnect is a duplicate, not a step back, and never reports a negative gap. The backbone holds no replay, so a gap is reported, never hidden.
73467
+ function sequenceGap(tracker, event) {
73468
+ var previous = tracker[event.source];
73469
+ if (event.sequence > (previous === undefined ? 0 : previous)) { tracker[event.source] = event.sequence; }
73470
+ if (previous === undefined || event.sequence <= previous) { return 0; }
73471
+ return event.sequence - previous - 1;
73472
+ }
73473
+
73474
+ function shortId(id) { return id.length > 14 ? id.slice(0, 8) + '..' + id.slice(-4) : id; }
73475
+
73476
+ // A bounded sketch of any payload, the feed's fallback rendering: one JSON line, cut with a marker rather than silently.
73477
+ function sketch(value, cap) {
73478
+ var text;
73479
+ try { text = JSON.stringify(value); } catch (e) { text = String(value); }
73480
+ if (text === undefined) { text = 'undefined'; }
73481
+ return text.length > cap ? text.slice(0, cap) + '...' : text;
73482
+ }
73483
+
73484
+ // The headline text of one Remote Control stream event: the assistant and user payloads that carry a content array of text blocks render as their prose, everything else as its type plus a sketch.
73485
+ function rcLine(event) {
73486
+ var envelope = event.envelope;
73487
+ var payload = envelope.payload;
73488
+ var headline = '';
73489
+ if (payload !== undefined && payload !== null && typeof payload === 'object' && Array.isArray(payload.content)) {
73490
+ var parts = [];
73491
+ for (var i = 0; i < payload.content.length; i++) {
73492
+ var block = payload.content[i];
73493
+ if (block !== null && typeof block === 'object' && block.type === 'text' && typeof block.text === 'string') { parts.push(block.text); }
73494
+ }
73495
+ if (parts.length > 0) { headline = parts.join(' '); }
73496
+ }
73497
+ if (headline === '') {
73498
+ headline = envelope.event_type;
73499
+ if (payload !== undefined) { headline += ' ' + sketch(payload, 240); }
73500
+ }
73501
+ return new Date().toLocaleTimeString() + ' ' + shortId(event.session) + ' ' + envelope.event_type + ' ' + headline;
73502
+ }
73503
+
73504
+ function el(tag, className, text) {
73505
+ var node = document.createElement(tag);
73506
+ if (className) { node.className = className; }
73507
+ if (text !== undefined) { node.textContent = text; }
73508
+ return node;
73509
+ }
73510
+
73511
+ var ui = null;
73512
+
73513
+ function renderEvent(event, gap) {
73514
+ if (ui === null) { return; }
73515
+ var list = ui.eventList;
73516
+ if (gap > 0) {
73517
+ list.appendChild(el('div', 'event gap', 'dropped events: ' + String(gap) + ' on source ' + event.source + ' (the stream holds no replay)'));
73518
+ }
73519
+ var line;
73520
+ if (event.source === 'rc') {
73521
+ line = rcLine(event.payload);
73522
+ } else {
73523
+ line = new Date().toLocaleTimeString() + ' [' + event.source + '] ' + sketch(event.payload, 240);
73524
+ }
73525
+ list.appendChild(el('div', 'event', line));
73526
+ while (list.childElementCount > 500) { list.removeChild(list.firstChild); }
73527
+ list.scrollTop = list.scrollHeight;
73528
+ }
73529
+
73530
+ function renderSessions(statuses) {
73531
+ if (ui === null) { return; }
73532
+ var list = ui.sessionList;
73533
+ list.textContent = '';
73534
+ if (statuses.length === 0) {
73535
+ list.appendChild(el('div', 'event dim', 'no Remote Control sessions observed'));
73536
+ return;
73537
+ }
73538
+ for (var i = 0; i < statuses.length; i++) {
73539
+ var status = statuses[i];
73540
+ var worker = status.workerState === undefined ? 'worker unknown' : 'worker ' + status.workerState.value;
73541
+ var row = el('div', 'session' + (status.id === ui.selected ? ' selected' : ''));
73542
+ row.appendChild(el('div', null, shortId(status.id)));
73543
+ row.appendChild(el('div', 'dim', worker + (status.pending.length === 0 ? '' : ', ' + String(status.pending.length) + ' pending') + ', seen ' + new Date(status.lastSeenAt).toLocaleTimeString()));
73544
+ row.addEventListener('click', selectSession(status.id));
73545
+ list.appendChild(row);
73546
+ }
73547
+ }
73548
+
73549
+ function selectSession(id) {
73550
+ return function () {
73551
+ if (ui === null) { return; }
73552
+ ui.selected = ui.selected === id ? null : id;
73553
+ ui.promptTarget.textContent = ui.selected === null ? 'no session selected' : 'to ' + ui.selected;
73554
+ refreshSessions();
73555
+ refreshPending();
73556
+ };
73557
+ }
73558
+
73559
+ function renderPending(pending) {
73560
+ if (ui === null) { return; }
73561
+ var list = ui.pendingList;
73562
+ list.textContent = '';
73563
+ if (pending.length === 0) {
73564
+ list.appendChild(el('div', 'event dim', 'no control requests awaiting an answer'));
73565
+ return;
73566
+ }
73567
+ for (var i = 0; i < pending.length; i++) {
73568
+ var request = pending[i];
73569
+ var card = el('div', 'card');
73570
+ card.appendChild(el('div', null, request.type + ' ' + (request.summary === '' ? shortId(request.requestId) : request.summary)));
73571
+ card.appendChild(el('div', 'dim', shortId(request.sessionId) + ' ' + shortId(request.requestId) + ' observed ' + new Date(request.observedAt).toLocaleTimeString()));
73572
+ var row = el('div', 'row');
73573
+ var denial = el('input');
73574
+ denial.type = 'text';
73575
+ denial.placeholder = 'denial message (optional)';
73576
+ var approve = el('button', null, 'Approve');
73577
+ var deny = el('button', 'secondary', 'Deny');
73578
+ approve.addEventListener('click', answerRequest(request, true, denial));
73579
+ deny.addEventListener('click', answerRequest(request, false, denial));
73580
+ row.appendChild(denial);
73581
+ row.appendChild(approve);
73582
+ row.appendChild(deny);
73583
+ card.appendChild(row);
73584
+ list.appendChild(card);
73585
+ }
73586
+ }
73587
+
73588
+ function answerRequest(request, approve, denialInput) {
73589
+ return function () {
73590
+ var input = { session: request.sessionId, request: request.requestId, approve: approve };
73591
+ if (!approve && denialInput.value !== '') { input.text = denialInput.value; }
73592
+ rpc('rc/answer', input).then(function () {
73593
+ renderEvent({ source: 'page', sequence: 0, payload: { note: (approve ? 'approved' : 'denied') + ' ' + request.requestId } }, 0);
73594
+ refreshPending();
73595
+ }, function (error) { window.alert('answer failed: ' + error.message); });
73596
+ };
73597
+ }
73598
+
73599
+ function sendPrompt() {
73600
+ if (ui === null || ui.selected === null) { window.alert('select a session first'); return; }
73601
+ var text = ui.prompt.value;
73602
+ if (text === '') { return; }
73603
+ ui.send.disabled = true;
73604
+ rpc('rc/send', { session: ui.selected, text: text }).then(function (result) {
73605
+ ui.prompt.value = '';
73606
+ renderEvent({ source: 'page', sequence: 0, payload: { note: 'sent, sequence_num ' + result.sequenceNums.join(', ') } }, 0);
73607
+ }, function (error) { window.alert('send failed: ' + error.message); }).then(function () { ui.send.disabled = false; });
73608
+ }
73609
+
73610
+ function refreshSessions() {
73611
+ rpc('rc/status', {}).then(function (result) { renderSessions(result.statuses); }, authOrReport('rc/status'));
73612
+ }
73613
+
73614
+ function refreshPending() {
73615
+ rpc('rc/pending', ui !== null && ui.selected !== null ? { session: ui.selected } : {}).then(function (result) { renderPending(result.pending); }, authOrReport('rc/pending'));
73616
+ }
73617
+
73618
+ // The one error path every call shares: a 401 means this token is not this generation's, so the page says so and asks for it again; anything else is reported in the feed.
73619
+ function authOrReport(what) {
73620
+ return function (error) {
73621
+ if (error.status === 401) {
73622
+ handleUnauthorized();
73623
+ return;
73624
+ }
73625
+ renderEvent({ source: 'page', sequence: 0, payload: { note: what + ' failed: ' + error.message } }, 0);
73626
+ };
73627
+ }
73628
+
73629
+ function handleUnauthorized() {
73630
+ window.sessionStorage.removeItem(TOKEN_KEY);
73631
+ if (feedAbort !== null) { feedAbort.abort(); }
73632
+ streamState('');
73633
+ showTokenForm('this door refused the token: it names a different door generation, paste the current one');
73634
+ }
73635
+
73636
+ function streamState(text) { if (ui !== null) { ui.streamState.textContent = text; } }
73637
+
73638
+ var feedAbort = null;
73639
+
73640
+ // Subscribes to the door's backbone and renders what it yields, reconnecting on any drop. The stream holds no replay, so a reconnect starts at the live head and the per-source sequence numbers surface whatever the gap swallowed.
73641
+ async function runFeed() {
73642
+ var tracker = {};
73643
+ for (;;) {
73644
+ if (feedAbort !== null) { feedAbort.abort(); }
73645
+ feedAbort = new AbortController();
73646
+ var filter = document.getElementById('source-filter').value;
73647
+ var input = filter === '' ? {} : { sources: [filter] };
73648
+ try {
73649
+ streamState('stream: connecting');
73650
+ var response = await fetch(apiBase() + '/events/subscribe', {
73651
+ method: 'POST',
73652
+ headers: { 'content-type': 'application/json', authorization: 'Bearer ' + token },
73653
+ body: JSON.stringify({ json: input }),
73654
+ signal: feedAbort.signal,
73655
+ });
73656
+ if (response.status === 401) { handleUnauthorized(); return; }
73657
+ if (!response.ok || response.body === null) { throw new Error('HTTP ' + String(response.status)); }
73658
+ streamState('stream: live');
73659
+ var reader = response.body.getReader();
73660
+ var decoder = new TextDecoder();
73661
+ var buffer = '';
73662
+ for (;;) {
73663
+ var chunk = await reader.read();
73664
+ if (chunk.done) { break; }
73665
+ buffer += decoder.decode(chunk.value, { stream: true });
73666
+ var parsed = parseSseFrames(buffer);
73667
+ buffer = parsed.rest;
73668
+ for (var i = 0; i < parsed.frames.length; i++) {
73669
+ var frame = parsed.frames[i];
73670
+ if (frame.event !== 'message') { continue; }
73671
+ var event;
73672
+ try { event = JSON.parse(frame.data).json; } catch (e) { continue; }
73673
+ if (event === undefined || event.source === undefined) { continue; }
73674
+ renderEvent(event, sequenceGap(tracker, event));
73675
+ }
73676
+ }
73677
+ } catch (error) {
73678
+ if (feedAbort !== null && feedAbort.signal.aborted) { return; }
73679
+ }
73680
+ streamState('stream: dropped, reconnecting');
73681
+ await new Promise(function (resolve) { window.setTimeout(resolve, 1000); });
73682
+ }
73683
+ }
73684
+
73685
+ function showTokenForm(message) {
73686
+ if (feedAbort !== null) { feedAbort.abort(); }
73687
+ if (ui !== null) { ui.main.classList.add('hidden'); }
73688
+ var form = document.getElementById('token-form');
73689
+ form.classList.remove('hidden');
73690
+ document.getElementById('token-error').textContent = message === undefined ? '' : message;
73691
+ document.getElementById('token-input').focus();
73692
+ }
73693
+
73694
+ function startPage(heldToken) {
73695
+ token = heldToken;
73696
+ ui = {
73697
+ main: document.getElementById('main'),
73698
+ eventList: document.getElementById('event-list'),
73699
+ sessionList: document.getElementById('session-list'),
73700
+ pendingList: document.getElementById('pending-list'),
73701
+ prompt: document.getElementById('prompt'),
73702
+ promptTarget: document.getElementById('prompt-target'),
73703
+ send: document.getElementById('send'),
73704
+ streamState: document.getElementById('stream-state'),
73705
+ selected: null,
73706
+ };
73707
+ document.getElementById('token-form').classList.add('hidden');
73708
+ ui.main.classList.remove('hidden');
73709
+ document.getElementById('refresh').addEventListener('click', function () { refreshSessions(); refreshPending(); });
73710
+ document.getElementById('send').addEventListener('click', sendPrompt);
73711
+ document.getElementById('source-filter').addEventListener('change', function () { runFeed(); });
73712
+ ui.prompt.addEventListener('keydown', function (event) {
73713
+ if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) { sendPrompt(); }
73714
+ });
73715
+ refreshSessions();
73716
+ refreshPending();
73717
+ window.setInterval(function () { refreshSessions(); refreshPending(); }, 4000);
73718
+ runFeed();
73719
+ document.getElementById('door-state').textContent = 'connected';
73720
+ }
73721
+
73722
+ function boot() {
73723
+ var held = null;
73724
+ var query = new URLSearchParams(window.location.search);
73725
+ var fromQuery = query.get('token');
73726
+ if (fromQuery !== null && fromQuery !== '') {
73727
+ window.sessionStorage.setItem(TOKEN_KEY, fromQuery);
73728
+ // The token leaves the address bar the moment the page holds it, so a refresh or a shared screen shows the clean URL; sessionStorage keeps the tab working.
73729
+ window.history.replaceState(null, '', window.location.pathname);
73730
+ held = fromQuery;
73731
+ } else {
73732
+ held = window.sessionStorage.getItem(TOKEN_KEY);
73733
+ }
73734
+ document.getElementById('token-save').addEventListener('click', function () {
73735
+ var value = document.getElementById('token-input').value.trim();
73736
+ if (value === '') { return; }
73737
+ window.sessionStorage.setItem(TOKEN_KEY, value);
73738
+ startPage(value);
73739
+ });
73740
+ document.getElementById('token-input').addEventListener('keydown', function (event) {
73741
+ if (event.key === 'Enter') { document.getElementById('token-save').click(); }
73742
+ });
73743
+ if (held !== null && held !== '') { startPage(held); }
73744
+ }
73745
+
73746
+ if (typeof document !== 'undefined' && document.getElementById('event-list') !== null) {
73747
+ window.addEventListener('DOMContentLoaded', boot);
73748
+ }
73749
+ `;
73750
+ var RC_CLIENT_PAGE_HTML = `<!doctype html>
73751
+ <html lang="en">
73752
+ <head>
73753
+ <meta charset="utf-8">
73754
+ <meta name="viewport" content="width=device-width, initial-scale=1">
73755
+ <title>agent-shim Remote Control</title>
73756
+ <style>
73757
+ :root { color-scheme: dark; }
73758
+ * { box-sizing: border-box; }
73759
+ body { margin: 0; background: #101418; color: #d7dde3; font: 14px/1.45 ui-monospace, Menlo, Consolas, monospace; }
73760
+ header { display: flex; gap: 12px; align-items: baseline; padding: 10px 16px; background: #171d24; border-bottom: 1px solid #2a333d; }
73761
+ header h1 { font-size: 15px; margin: 0; font-weight: 600; }
73762
+ header .state { color: #8b98a5; }
73763
+ main { display: flex; gap: 1px; background: #2a333d; min-height: calc(100vh - 39px); }
73764
+ section { background: #101418; padding: 12px 16px; overflow-y: auto; }
73765
+ #sessions { flex: 0 0 320px; border-right: 1px solid #2a333d; }
73766
+ #right { flex: 1; display: flex; flex-direction: column; min-width: 0; }
73767
+ #feed { flex: 1; }
73768
+ #controls { border-top: 1px solid #2a333d; }
73769
+ h2 { font-size: 12px; text-transform: uppercase; letter-spacing: 0.08em; color: #8b98a5; margin: 8px 0 8px; font-weight: 600; }
73770
+ .session { padding: 6px 8px; border: 1px solid transparent; border-radius: 4px; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
73771
+ .session:hover { background: #171d24; }
73772
+ .session.selected { border-color: #3d6ea5; background: #17222e; }
73773
+ .session .dim { color: #8b98a5; }
73774
+ .event { padding: 2px 0; white-space: pre-wrap; word-break: break-word; }
73775
+ .event .dim { color: #8b98a5; }
73776
+ .event.gap { color: #d29922; }
73777
+ .event.error { color: #e5534b; }
73778
+ textarea, input[type=text], select { background: #171d24; color: #d7dde3; border: 1px solid #2a333d; border-radius: 4px; padding: 6px 8px; font: inherit; width: 100%; }
73779
+ textarea { resize: vertical; min-height: 44px; }
73780
+ button { background: #1f4a7a; color: #d7dde3; border: 1px solid #3d6ea5; border-radius: 4px; padding: 5px 12px; font: inherit; cursor: pointer; }
73781
+ button.secondary { background: #2a333d; border-color: #3a444f; }
73782
+ button:disabled { opacity: 0.5; cursor: default; }
73783
+ .row { display: flex; gap: 8px; margin: 6px 0; align-items: center; }
73784
+ .card { border: 1px solid #3d6ea5; border-radius: 4px; padding: 8px; margin: 6px 0; background: #17222e; }
73785
+ .card .dim { color: #8b98a5; }
73786
+ #token-form { max-width: 480px; margin: 48px auto; }
73787
+ #token-form p { color: #8b98a5; }
73788
+ .hidden { display: none !important; }
73789
+ </style>
73790
+ </head>
73791
+ <body data-agent-shim-rc-client>
73792
+ <header>
73793
+ <h1>agent-shim Remote Control</h1>
73794
+ <span class="state" id="door-state">connecting</span>
73795
+ <span class="state" id="stream-state"></span>
73796
+ </header>
73797
+ <div id="token-form">
73798
+ <h2>This door's control token</h2>
73799
+ <p>The page is served behind the door's per-generation control token. Paste this door generation's token (the owner-only value the door writes beside its state) to use the page; it stays in this tab only.</p>
73800
+ <div class="row"><input type="text" id="token-input" placeholder="control token" autocomplete="off"><button id="token-save">Use</button></div>
73801
+ <p id="token-error" class="event error"></p>
73802
+ </div>
73803
+ <main class="hidden" id="main">
73804
+ <section id="sessions">
73805
+ <h2>Sessions <button class="secondary" id="refresh">Refresh</button></h2>
73806
+ <div id="session-list"></div>
73807
+ </section>
73808
+ <section id="right">
73809
+ <div id="feed">
73810
+ <h2>Feed <select id="source-filter"><option value="">all sources</option><option value="rc">Remote Control only</option></select></h2>
73811
+ <div id="event-list"></div>
73812
+ </div>
73813
+ <div id="controls">
73814
+ <h2>Pending approvals</h2>
73815
+ <div id="pending-list"></div>
73816
+ <h2>Send a prompt</h2>
73817
+ <div class="row"><textarea id="prompt" placeholder="Prompt for the selected session"></textarea></div>
73818
+ <div class="row"><span class="dim" id="prompt-target">no session selected</span><span style="flex:1"></span><button id="send">Send</button></div>
73819
+ </div>
73820
+ </section>
73821
+ </main>
73822
+ <script>
73823
+ ${RC_CLIENT_PAGE_SCRIPT}
73824
+ </script>
73825
+ </body>
73826
+ </html>
73827
+ `;
73828
+ function pageTokenOf(request7, url2) {
73829
+ const presented = request7.headers.authorization;
73830
+ const bearerPrefix = "bearer ".length;
73831
+ const header = typeof presented === "string" && presented.slice(0, bearerPrefix).toLowerCase() === "bearer " ? presented.slice(bearerPrefix) : void 0;
73832
+ const query = url2.searchParams.get("token") ?? void 0;
73833
+ return header ?? query;
73834
+ }
73835
+ function serveRcClientPage(expectedToken, request7, response) {
73836
+ const url2 = new URL(request7.url ?? "/", "https://door.invalid");
73837
+ if (url2.pathname !== RC_CLIENT_PATH && url2.pathname !== `${RC_CLIENT_PATH}/`) {
73838
+ response.writeHead(HTTP_STATUS.notFound, { "Content-Type": "text/plain" });
73839
+ response.end("no such page under the door's client prefix");
73840
+ return;
73841
+ }
73842
+ if (request7.method !== "GET" && request7.method !== "HEAD") {
73843
+ response.writeHead(HTTP_STATUS.methodNotAllowed, { Allow: "GET, HEAD", "Content-Type": "text/plain" });
73844
+ response.end("the door's web client page is read-only");
73845
+ return;
73846
+ }
73847
+ const token = pageTokenOf(request7, url2);
73848
+ if (token === void 0 || !isLiveCapability(token, [expectedToken])) {
73849
+ response.writeHead(HTTP_STATUS.unauthorized, { "Content-Type": "application/json" });
73850
+ response.end(JSON.stringify({ error: "the door's web client demands this generation's control token, as a Bearer credential or the token query parameter" }));
73851
+ return;
73852
+ }
73853
+ response.writeHead(HTTP_STATUS.ok, {
73854
+ "Content-Type": "text/html; charset=utf-8",
73855
+ "Cache-Control": "no-store",
73856
+ "Referrer-Policy": "no-referrer",
73857
+ "Content-Security-Policy": "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'self'"
73858
+ });
73859
+ response.end(request7.method === "HEAD" ? void 0 : RC_CLIENT_PAGE_HTML);
73860
+ }
73861
+ function createRcClientPage(expectedToken) {
73862
+ return {
73863
+ pathPrefix: RC_CLIENT_PATH,
73864
+ handle: async (request7, response) => {
73865
+ serveRcClientPage(expectedToken, request7, response);
73866
+ await Promise.resolve();
73867
+ return { matched: true };
73868
+ }
73869
+ };
73870
+ }
73871
+
73411
73872
  // src/frontdoor/rcSelfHost.ts
73412
73873
  var RC_SELF_HOST_ENV = "AGENT_SHIM_FRONTDOOR_RC_SELF_HOST";
73413
73874
  function rcSelfHostFromEnv(env) {
@@ -74853,20 +75314,24 @@ function realFrontDoorSupervisorPorts(paths) {
74853
75314
  log,
74854
75315
  mintLeaf(authority, LOOPBACK_LEAF_NAMES, /* @__PURE__ */ new Date()),
74855
75316
  createRcControlHandler({ expectedToken: rcControlToken, list: rcTracker.list, statusOf: rcTracker.statusOf, pendingOf: rcTracker.pendingOf, inject: rcInject, answer: rcAnswer, interrupt: rcInterrupt, setModel: rcSetModel, setPermissionMode: rcSetPermissionMode }),
74856
- createDoorApiNodeHandler({
74857
- expectedToken: rcControlToken,
74858
- list: rcTracker.list,
74859
- statusOf: rcTracker.statusOf,
74860
- pendingOf: rcTracker.pendingOf,
74861
- inject: rcInject,
74862
- answer: rcAnswer,
74863
- interrupt: rcInterrupt,
74864
- setModel: rcSetModel,
74865
- setPermissionMode: rcSetPermissionMode,
74866
- fanout: rcFanout,
74867
- ...controlDeps,
74868
- events: doorEvents
74869
- })
75317
+ [
75318
+ createDoorApiNodeHandler({
75319
+ expectedToken: rcControlToken,
75320
+ list: rcTracker.list,
75321
+ statusOf: rcTracker.statusOf,
75322
+ pendingOf: rcTracker.pendingOf,
75323
+ inject: rcInject,
75324
+ answer: rcAnswer,
75325
+ interrupt: rcInterrupt,
75326
+ setModel: rcSetModel,
75327
+ setPermissionMode: rcSetPermissionMode,
75328
+ fanout: rcFanout,
75329
+ ...controlDeps,
75330
+ events: doorEvents
75331
+ }),
75332
+ // The self-hosted web client rides the same listener under the same per-generation token as the typed API it speaks: no extra process, no new trust surface.
75333
+ createRcClientPage(rcControlToken)
75334
+ ]
74870
75335
  );
74871
75336
  const handle = await listenFrontDoor(server, { ...preferredPort === void 0 ? {} : { preferredPort }, ca: authority.certPem, onError: onListenerError("provider") });
74872
75337
  return {
@@ -3976,13 +3976,13 @@ declare function createControlApiRouter(deps: ControlApiDeps): {
3976
3976
  "config-profile": "config-profile";
3977
3977
  identity: "identity";
3978
3978
  provider: "provider";
3979
+ pool: "pool";
3980
+ keychain: "keychain";
3979
3981
  "ambient-credential": "ambient-credential";
3980
3982
  "binary-discovery": "binary-discovery";
3981
3983
  "claude-shim": "claude-shim";
3982
3984
  "path-resolution": "path-resolution";
3983
3985
  "legacy-name": "legacy-name";
3984
- pool: "pool";
3985
- keychain: "keychain";
3986
3986
  "directory-rules": "directory-rules";
3987
3987
  "categories-local": "categories-local";
3988
3988
  "active-identity": "active-identity";
@@ -4453,13 +4453,13 @@ declare function createDoorApiRouter(deps: DoorApiDeps): {
4453
4453
  "config-profile": "config-profile";
4454
4454
  identity: "identity";
4455
4455
  provider: "provider";
4456
+ pool: "pool";
4457
+ keychain: "keychain";
4456
4458
  "ambient-credential": "ambient-credential";
4457
4459
  "binary-discovery": "binary-discovery";
4458
4460
  "claude-shim": "claude-shim";
4459
4461
  "path-resolution": "path-resolution";
4460
4462
  "legacy-name": "legacy-name";
4461
- pool: "pool";
4462
- keychain: "keychain";
4463
4463
  "directory-rules": "directory-rules";
4464
4464
  "categories-local": "categories-local";
4465
4465
  "active-identity": "active-identity";
@@ -4747,13 +4747,13 @@ declare const DoctorFindingSchema: z.ZodObject<{
4747
4747
  "config-profile": "config-profile";
4748
4748
  identity: "identity";
4749
4749
  provider: "provider";
4750
+ pool: "pool";
4751
+ keychain: "keychain";
4750
4752
  "ambient-credential": "ambient-credential";
4751
4753
  "binary-discovery": "binary-discovery";
4752
4754
  "claude-shim": "claude-shim";
4753
4755
  "path-resolution": "path-resolution";
4754
4756
  "legacy-name": "legacy-name";
4755
- pool: "pool";
4756
- keychain: "keychain";
4757
4757
  "directory-rules": "directory-rules";
4758
4758
  "categories-local": "categories-local";
4759
4759
  "active-identity": "active-identity";
@@ -4776,13 +4776,13 @@ declare const DoctorRunOutputSchema: z.ZodObject<{
4776
4776
  "config-profile": "config-profile";
4777
4777
  identity: "identity";
4778
4778
  provider: "provider";
4779
+ pool: "pool";
4780
+ keychain: "keychain";
4779
4781
  "ambient-credential": "ambient-credential";
4780
4782
  "binary-discovery": "binary-discovery";
4781
4783
  "claude-shim": "claude-shim";
4782
4784
  "path-resolution": "path-resolution";
4783
4785
  "legacy-name": "legacy-name";
4784
- pool: "pool";
4785
- keychain: "keychain";
4786
4786
  "directory-rules": "directory-rules";
4787
4787
  "categories-local": "categories-local";
4788
4788
  "active-identity": "active-identity";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-shim",
3
- "version": "8.16.0",
3
+ "version": "8.17.1",
4
4
  "description": "A profile manager and launcher for Claude Code that lets one person run multiple logins from one machine while controlling what gets shared between them.",
5
5
  "license": "MIT",
6
6
  "author": "Joseph Mearman <joseph@mearman.co.uk>",