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 +1 -0
- package/dist/cli.cjs +482 -17
- package/dist/types/index.d.ts +8 -8
- package/package.json +1 -1
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.
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
74857
|
-
|
|
74858
|
-
|
|
74859
|
-
|
|
74860
|
-
|
|
74861
|
-
|
|
74862
|
-
|
|
74863
|
-
|
|
74864
|
-
|
|
74865
|
-
|
|
74866
|
-
|
|
74867
|
-
|
|
74868
|
-
|
|
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 {
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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.
|
|
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>",
|