agent-shim 8.23.0 → 8.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/dist/cli.cjs +2171 -1810
- package/dist/index.cjs +423 -100
- package/dist/index.mjs +398 -100
- package/dist/types/index.d.ts +371 -13
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -61796,7 +61796,7 @@ function registerIdentityCommand(program2, deps) {
|
|
|
61796
61796
|
// package.json
|
|
61797
61797
|
var package_default = {
|
|
61798
61798
|
name: "agent-shim",
|
|
61799
|
-
version: "8.
|
|
61799
|
+
version: "8.24.0",
|
|
61800
61800
|
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.",
|
|
61801
61801
|
license: "MIT",
|
|
61802
61802
|
author: "Joseph Mearman <joseph@mearman.co.uk>",
|
|
@@ -64385,12 +64385,12 @@ var SiwcInvalidGrantError = class extends SiwcError {
|
|
|
64385
64385
|
function base64Url(bytes) {
|
|
64386
64386
|
return Buffer.from(bytes).toString("base64url");
|
|
64387
64387
|
}
|
|
64388
|
-
function newPkce(
|
|
64389
|
-
const verifier = base64Url(
|
|
64388
|
+
function newPkce(randomBytes5) {
|
|
64389
|
+
const verifier = base64Url(randomBytes5(SIWC_RANDOM_BYTES));
|
|
64390
64390
|
return { verifier, challenge: (0, import_node_crypto6.createHash)("sha256").update(verifier).digest("base64url") };
|
|
64391
64391
|
}
|
|
64392
|
-
function newOpaqueValue(
|
|
64393
|
-
return base64Url(
|
|
64392
|
+
function newOpaqueValue(randomBytes5) {
|
|
64393
|
+
return base64Url(randomBytes5(SIWC_RANDOM_BYTES));
|
|
64394
64394
|
}
|
|
64395
64395
|
function buildAuthorizeUrl(request7) {
|
|
64396
64396
|
const url2 = new URL(SIWC_AUTHORIZE_URL);
|
|
@@ -65051,7 +65051,7 @@ function registerCodexCommand(program2, deps) {
|
|
|
65051
65051
|
}
|
|
65052
65052
|
|
|
65053
65053
|
// src/frontdoor/commands.ts
|
|
65054
|
-
var
|
|
65054
|
+
var import_node_crypto15 = require("node:crypto");
|
|
65055
65055
|
var import_node_path36 = __toESM(require("node:path"), 1);
|
|
65056
65056
|
var import_node_fs13 = __toESM(require("node:fs"), 1);
|
|
65057
65057
|
|
|
@@ -66548,6 +66548,8 @@ function teeRoutedResponse(response, observer, onSettled) {
|
|
|
66548
66548
|
}
|
|
66549
66549
|
};
|
|
66550
66550
|
}
|
|
66551
|
+
|
|
66552
|
+
// src/frontdoor/rcWrites.ts
|
|
66551
66553
|
function buildRcUserMessagePayload(uuid3, sessionId, text2) {
|
|
66552
66554
|
return {
|
|
66553
66555
|
type: "user",
|
|
@@ -66648,14 +66650,55 @@ var RC_PERMISSION_MODES = ["default", "acceptEdits", "bypassPermissions", "plan"
|
|
|
66648
66650
|
function isRcPermissionMode(value2) {
|
|
66649
66651
|
return RC_PERMISSION_MODES.some((mode) => mode === value2);
|
|
66650
66652
|
}
|
|
66653
|
+
function buildRcControlRequestPayload(requestId, request7) {
|
|
66654
|
+
return { type: "control_request", request_id: requestId, request: request7 };
|
|
66655
|
+
}
|
|
66651
66656
|
function buildRcInterruptPayload(requestId) {
|
|
66652
|
-
return
|
|
66657
|
+
return buildRcControlRequestPayload(requestId, { subtype: "interrupt" });
|
|
66653
66658
|
}
|
|
66654
66659
|
function buildRcSetModelPayload(requestId, model) {
|
|
66655
|
-
return
|
|
66660
|
+
return buildRcControlRequestPayload(requestId, { subtype: "set_model", model });
|
|
66656
66661
|
}
|
|
66657
66662
|
function buildRcSetPermissionModePayload(requestId, mode) {
|
|
66658
|
-
return
|
|
66663
|
+
return buildRcControlRequestPayload(requestId, { subtype: "set_permission_mode", mode });
|
|
66664
|
+
}
|
|
66665
|
+
function buildRcEndSessionPayload(requestId, reason) {
|
|
66666
|
+
return buildRcControlRequestPayload(requestId, { subtype: "end_session", ...reason === void 0 ? {} : { reason } });
|
|
66667
|
+
}
|
|
66668
|
+
function buildRcGetUsagePayload(requestId, skipBehaviors) {
|
|
66669
|
+
return buildRcControlRequestPayload(requestId, { subtype: "get_usage", ...skipBehaviors === true ? { skip_behaviors: true } : {} });
|
|
66670
|
+
}
|
|
66671
|
+
var RC_CONTEXT_USAGE_DETAILS = ["summary", "full"];
|
|
66672
|
+
function isRcContextUsageDetail(value2) {
|
|
66673
|
+
return RC_CONTEXT_USAGE_DETAILS.some((detail) => detail === value2);
|
|
66674
|
+
}
|
|
66675
|
+
function buildRcGetContextUsagePayload(requestId, detail) {
|
|
66676
|
+
return buildRcControlRequestPayload(requestId, { subtype: "get_context_usage", ...detail === void 0 ? {} : { detail } });
|
|
66677
|
+
}
|
|
66678
|
+
var RC_READ_FILE_ENCODINGS = ["utf-8", "base64"];
|
|
66679
|
+
function isRcReadFileEncoding(value2) {
|
|
66680
|
+
return RC_READ_FILE_ENCODINGS.some((encoding) => encoding === value2);
|
|
66681
|
+
}
|
|
66682
|
+
function buildRcReadFilePayload(requestId, path46, options) {
|
|
66683
|
+
return buildRcControlRequestPayload(requestId, { subtype: "read_file", path: path46, ...options?.maxBytes === void 0 ? {} : { max_bytes: options.maxBytes }, ...options?.encoding === void 0 ? {} : { encoding: options.encoding } });
|
|
66684
|
+
}
|
|
66685
|
+
function buildRcFileSuggestionsPayload(requestId, query) {
|
|
66686
|
+
return buildRcControlRequestPayload(requestId, { subtype: "file_suggestions", query });
|
|
66687
|
+
}
|
|
66688
|
+
function buildRcMcpStatusPayload(requestId) {
|
|
66689
|
+
return buildRcControlRequestPayload(requestId, { subtype: "mcp_status" });
|
|
66690
|
+
}
|
|
66691
|
+
function buildRcMcpReconnectPayload(requestId, serverName) {
|
|
66692
|
+
return buildRcControlRequestPayload(requestId, { subtype: "mcp_reconnect", serverName });
|
|
66693
|
+
}
|
|
66694
|
+
function buildRcMcpAuthenticatePayload(requestId, serverName, redirectUri) {
|
|
66695
|
+
return buildRcControlRequestPayload(requestId, { subtype: "mcp_authenticate", serverName, redirectUri });
|
|
66696
|
+
}
|
|
66697
|
+
function buildRcMcpOAuthCallbackUrlPayload(requestId, serverName, callbackUrl) {
|
|
66698
|
+
return buildRcControlRequestPayload(requestId, { subtype: "mcp_oauth_callback_url", serverName, callbackUrl });
|
|
66699
|
+
}
|
|
66700
|
+
function buildRcKeepAlivePayload() {
|
|
66701
|
+
return { type: "keep_alive" };
|
|
66659
66702
|
}
|
|
66660
66703
|
async function answerRcControlRequest(deps, sessionId, requestId, decision) {
|
|
66661
66704
|
const credential = deps.credentialOf(sessionId);
|
|
@@ -66682,7 +66725,7 @@ async function answerRcControlRequest(deps, sessionId, requestId, decision) {
|
|
|
66682
66725
|
}
|
|
66683
66726
|
return result;
|
|
66684
66727
|
}
|
|
66685
|
-
async function
|
|
66728
|
+
async function deliverRcClientPayload(deps, sessionId, payload, action) {
|
|
66686
66729
|
const credential = deps.credentialOf(sessionId);
|
|
66687
66730
|
if (credential === void 0) {
|
|
66688
66731
|
return { ok: false, message: rcSessionNotObservedMessage(sessionId) };
|
|
@@ -66690,12 +66733,12 @@ async function interruptRcSession(deps, sessionId) {
|
|
|
66690
66733
|
if (credential.authorization === void 0) {
|
|
66691
66734
|
return noOAuthCredentialResult(sessionId);
|
|
66692
66735
|
}
|
|
66693
|
-
const body = JSON.stringify(buildRcEventWriteBody(
|
|
66736
|
+
const body = JSON.stringify(buildRcEventWriteBody(payload));
|
|
66694
66737
|
let answer;
|
|
66695
66738
|
try {
|
|
66696
66739
|
answer = await deps.dial.writeEvents(sessionId, rcWriteHeaders(credential), body);
|
|
66697
66740
|
} catch (error62) {
|
|
66698
|
-
return { ok: false, message: `the door could not reach the API host to
|
|
66741
|
+
return { ok: false, message: `the door could not reach the API host to ${action}: ${error62 instanceof Error ? error62.message : String(error62)}` };
|
|
66699
66742
|
}
|
|
66700
66743
|
const result = rcEventWriteResultFromAnswer(answer);
|
|
66701
66744
|
if (result.ok) {
|
|
@@ -66703,50 +66746,89 @@ async function interruptRcSession(deps, sessionId) {
|
|
|
66703
66746
|
}
|
|
66704
66747
|
return result;
|
|
66705
66748
|
}
|
|
66749
|
+
async function interruptRcSession(deps, sessionId) {
|
|
66750
|
+
const requestId = deps.newUuid();
|
|
66751
|
+
const result = await deliverRcClientPayload(deps, sessionId, buildRcInterruptPayload(requestId), `interrupt session ${sessionId}`);
|
|
66752
|
+
return result.ok ? { ...result, requestId } : result;
|
|
66753
|
+
}
|
|
66706
66754
|
async function setRcSessionModel(deps, sessionId, model) {
|
|
66707
66755
|
if (model === "") {
|
|
66708
66756
|
return { ok: false, message: "a set-model write needs a non-empty model id: the SDK's own field treats an omitted or null value as a reset to the session default, which this operation never sends, and an empty string is neither" };
|
|
66709
66757
|
}
|
|
66710
|
-
const
|
|
66711
|
-
|
|
66712
|
-
|
|
66713
|
-
|
|
66714
|
-
|
|
66715
|
-
|
|
66758
|
+
const requestId = deps.newUuid();
|
|
66759
|
+
const result = await deliverRcClientPayload(deps, sessionId, buildRcSetModelPayload(requestId, model), `set the model on session ${sessionId}`);
|
|
66760
|
+
return result.ok ? { ...result, requestId } : result;
|
|
66761
|
+
}
|
|
66762
|
+
async function setRcSessionPermissionMode(deps, sessionId, mode) {
|
|
66763
|
+
const requestId = deps.newUuid();
|
|
66764
|
+
const result = await deliverRcClientPayload(deps, sessionId, buildRcSetPermissionModePayload(requestId, mode), `set the permission mode on session ${sessionId}`);
|
|
66765
|
+
return result.ok ? { ...result, requestId } : result;
|
|
66766
|
+
}
|
|
66767
|
+
async function endRcSession(deps, sessionId, reason) {
|
|
66768
|
+
if (reason === "") {
|
|
66769
|
+
return { ok: false, message: "an end-session write needs either no reason or a non-empty one: the worker reads an absent reason as its own unspecified form, and an empty string is neither" };
|
|
66716
66770
|
}
|
|
66717
|
-
const
|
|
66718
|
-
|
|
66719
|
-
|
|
66720
|
-
|
|
66721
|
-
|
|
66722
|
-
|
|
66771
|
+
const requestId = deps.newUuid();
|
|
66772
|
+
const result = await deliverRcClientPayload(deps, sessionId, buildRcEndSessionPayload(requestId, reason), `end session ${sessionId}`);
|
|
66773
|
+
return result.ok ? { ...result, requestId } : result;
|
|
66774
|
+
}
|
|
66775
|
+
async function getRcSessionUsage(deps, sessionId, skipBehaviors) {
|
|
66776
|
+
const requestId = deps.newUuid();
|
|
66777
|
+
const result = await deliverRcClientPayload(deps, sessionId, buildRcGetUsagePayload(requestId, skipBehaviors), `request the usage of session ${sessionId}`);
|
|
66778
|
+
return result.ok ? { ...result, requestId } : result;
|
|
66779
|
+
}
|
|
66780
|
+
async function getRcSessionContextUsage(deps, sessionId, detail) {
|
|
66781
|
+
const requestId = deps.newUuid();
|
|
66782
|
+
const result = await deliverRcClientPayload(deps, sessionId, buildRcGetContextUsagePayload(requestId, detail), `request the context usage of session ${sessionId}`);
|
|
66783
|
+
return result.ok ? { ...result, requestId } : result;
|
|
66784
|
+
}
|
|
66785
|
+
async function readRcSessionFile(deps, sessionId, path46, options) {
|
|
66786
|
+
if (path46 === "") {
|
|
66787
|
+
return { ok: false, message: "a read-file write needs a non-empty path: the SDK's own field names the file the worker resolves against the session's cwd, and an empty string names none" };
|
|
66723
66788
|
}
|
|
66724
|
-
|
|
66725
|
-
|
|
66726
|
-
deps.noteSequenceNums(sessionId, result.sequenceNums);
|
|
66789
|
+
if (options?.maxBytes !== void 0 && (!Number.isInteger(options.maxBytes) || options.maxBytes <= 0)) {
|
|
66790
|
+
return { ok: false, message: "a read-file write's max-bytes is a positive whole number of bytes: the SDK's own field caps the read, and a zero or negative cap reads nothing" };
|
|
66727
66791
|
}
|
|
66728
|
-
|
|
66792
|
+
const requestId = deps.newUuid();
|
|
66793
|
+
const result = await deliverRcClientPayload(deps, sessionId, buildRcReadFilePayload(requestId, path46, options), `read ${path46} from session ${sessionId}`);
|
|
66794
|
+
return result.ok ? { ...result, requestId } : result;
|
|
66729
66795
|
}
|
|
66730
|
-
async function
|
|
66731
|
-
const
|
|
66732
|
-
|
|
66733
|
-
|
|
66734
|
-
|
|
66735
|
-
|
|
66736
|
-
|
|
66796
|
+
async function suggestRcSessionFiles(deps, sessionId, query) {
|
|
66797
|
+
const requestId = deps.newUuid();
|
|
66798
|
+
const result = await deliverRcClientPayload(deps, sessionId, buildRcFileSuggestionsPayload(requestId, query), `request file suggestions from session ${sessionId}`);
|
|
66799
|
+
return result.ok ? { ...result, requestId } : result;
|
|
66800
|
+
}
|
|
66801
|
+
async function getRcSessionMcpStatus(deps, sessionId) {
|
|
66802
|
+
const requestId = deps.newUuid();
|
|
66803
|
+
const result = await deliverRcClientPayload(deps, sessionId, buildRcMcpStatusPayload(requestId), `request the MCP status of session ${sessionId}`);
|
|
66804
|
+
return result.ok ? { ...result, requestId } : result;
|
|
66805
|
+
}
|
|
66806
|
+
async function reconnectRcSessionMcpServer(deps, sessionId, serverName) {
|
|
66807
|
+
if (serverName === "") {
|
|
66808
|
+
return { ok: false, message: "an mcp-reconnect write needs a non-empty server name: the SDK's own field names the server as mcp_status reports it, and an empty string names none" };
|
|
66737
66809
|
}
|
|
66738
|
-
const
|
|
66739
|
-
|
|
66740
|
-
|
|
66741
|
-
|
|
66742
|
-
|
|
66743
|
-
|
|
66810
|
+
const requestId = deps.newUuid();
|
|
66811
|
+
const result = await deliverRcClientPayload(deps, sessionId, buildRcMcpReconnectPayload(requestId, serverName), `reconnect MCP server ${serverName} on session ${sessionId}`);
|
|
66812
|
+
return result.ok ? { ...result, requestId } : result;
|
|
66813
|
+
}
|
|
66814
|
+
async function authenticateRcSessionMcpServer(deps, sessionId, serverName, redirectUri) {
|
|
66815
|
+
if (serverName === "" || redirectUri === "") {
|
|
66816
|
+
return { ok: false, message: "an mcp-authenticate write needs a non-empty server name and redirect URI: the CLI's own request names the server and where its OAuth flow redirects back to, and an empty string names neither" };
|
|
66744
66817
|
}
|
|
66745
|
-
const
|
|
66746
|
-
|
|
66747
|
-
|
|
66818
|
+
const requestId = deps.newUuid();
|
|
66819
|
+
const result = await deliverRcClientPayload(deps, sessionId, buildRcMcpAuthenticatePayload(requestId, serverName, redirectUri), `authenticate MCP server ${serverName} on session ${sessionId}`);
|
|
66820
|
+
return result.ok ? { ...result, requestId } : result;
|
|
66821
|
+
}
|
|
66822
|
+
async function submitRcSessionMcpOAuthCallbackUrl(deps, sessionId, serverName, callbackUrl) {
|
|
66823
|
+
if (serverName === "" || callbackUrl === "") {
|
|
66824
|
+
return { ok: false, message: "an mcp-oauth-callback-url write needs a non-empty server name and callback URL: the CLI's own request names the server and the URL its OAuth flow landed on, and an empty string names neither" };
|
|
66748
66825
|
}
|
|
66749
|
-
|
|
66826
|
+
const requestId = deps.newUuid();
|
|
66827
|
+
const result = await deliverRcClientPayload(deps, sessionId, buildRcMcpOAuthCallbackUrlPayload(requestId, serverName, callbackUrl), `hand MCP server ${serverName}'s OAuth callback to session ${sessionId}`);
|
|
66828
|
+
return result.ok ? { ...result, requestId } : result;
|
|
66829
|
+
}
|
|
66830
|
+
async function sendRcKeepAlive(deps, sessionId) {
|
|
66831
|
+
return await deliverRcClientPayload(deps, sessionId, buildRcKeepAlivePayload(), `keep session ${sessionId} alive`);
|
|
66750
66832
|
}
|
|
66751
66833
|
|
|
66752
66834
|
// src/frontdoor/rcControl.ts
|
|
@@ -66758,6 +66840,25 @@ function answerJson(response, status, body) {
|
|
|
66758
66840
|
response.writeHead(status, { "content-type": "application/json", "content-length": String(Buffer.byteLength(text2, "utf8")) });
|
|
66759
66841
|
response.end(text2);
|
|
66760
66842
|
}
|
|
66843
|
+
function isJsonObject(value2) {
|
|
66844
|
+
return typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
|
|
66845
|
+
}
|
|
66846
|
+
function bodyString(body, field) {
|
|
66847
|
+
const value2 = body[field];
|
|
66848
|
+
return typeof value2 === "string" && value2 !== "" ? value2 : void 0;
|
|
66849
|
+
}
|
|
66850
|
+
function bodyStringAllowingEmpty(body, field) {
|
|
66851
|
+
const value2 = body[field];
|
|
66852
|
+
return typeof value2 === "string" ? value2 : void 0;
|
|
66853
|
+
}
|
|
66854
|
+
function bodyBoolean(body, field) {
|
|
66855
|
+
const value2 = body[field];
|
|
66856
|
+
return typeof value2 === "boolean" ? value2 : void 0;
|
|
66857
|
+
}
|
|
66858
|
+
function bodyPositiveInteger(body, field) {
|
|
66859
|
+
const value2 = body[field];
|
|
66860
|
+
return typeof value2 === "number" && Number.isInteger(value2) && value2 > 0 ? value2 : void 0;
|
|
66861
|
+
}
|
|
66761
66862
|
async function readBody(request7) {
|
|
66762
66863
|
return await new Promise((resolve, reject) => {
|
|
66763
66864
|
const chunks = [];
|
|
@@ -66785,9 +66886,127 @@ function createRcControlHandler(deps) {
|
|
|
66785
66886
|
const pendingPath = `${CONTROL_PATH_PREFIX}/pending`;
|
|
66786
66887
|
const injectPath = `${CONTROL_PATH_PREFIX}/inject`;
|
|
66787
66888
|
const answerPath = `${CONTROL_PATH_PREFIX}/answer`;
|
|
66788
|
-
const
|
|
66789
|
-
|
|
66790
|
-
|
|
66889
|
+
const controlWrites = [
|
|
66890
|
+
{
|
|
66891
|
+
name: "interrupt",
|
|
66892
|
+
path: "interrupt",
|
|
66893
|
+
read: (body) => {
|
|
66894
|
+
const session = bodyString(body, "session");
|
|
66895
|
+
return session === void 0 ? { error: "the interrupt body must be JSON naming a non-empty session id" } : { session, run: async () => await deps.interrupt(session) };
|
|
66896
|
+
}
|
|
66897
|
+
},
|
|
66898
|
+
{
|
|
66899
|
+
name: "set-model",
|
|
66900
|
+
path: "set-model",
|
|
66901
|
+
read: (body) => {
|
|
66902
|
+
const session = bodyString(body, "session");
|
|
66903
|
+
const model = bodyString(body, "model");
|
|
66904
|
+
return session === void 0 || model === void 0 ? { error: "the set-model body must be JSON naming a non-empty session id and model id" } : { session, run: async () => await deps.setModel(session, model) };
|
|
66905
|
+
}
|
|
66906
|
+
},
|
|
66907
|
+
{
|
|
66908
|
+
name: "set-permission-mode",
|
|
66909
|
+
path: "set-permission-mode",
|
|
66910
|
+
read: (body) => {
|
|
66911
|
+
const session = bodyString(body, "session");
|
|
66912
|
+
const mode = body.mode;
|
|
66913
|
+
return session === void 0 || !isRcPermissionMode(mode) ? { error: `the set-permission-mode body must be JSON naming a non-empty session id and a mode the SDK's own type permits (${RC_PERMISSION_MODE_LIST})` } : { session, run: async () => await deps.setPermissionMode(session, mode) };
|
|
66914
|
+
}
|
|
66915
|
+
},
|
|
66916
|
+
{
|
|
66917
|
+
name: "end-session",
|
|
66918
|
+
path: "end-session",
|
|
66919
|
+
read: (body) => {
|
|
66920
|
+
const session = bodyString(body, "session");
|
|
66921
|
+
const reason = "reason" in body ? bodyString(body, "reason") : void 0;
|
|
66922
|
+
return session === void 0 || "reason" in body && reason === void 0 ? { error: "the end-session body must be JSON naming a non-empty session id, and a reason only as a non-empty string or absent" } : { session, run: async () => await deps.endSession(session, reason) };
|
|
66923
|
+
}
|
|
66924
|
+
},
|
|
66925
|
+
{
|
|
66926
|
+
name: "get-usage",
|
|
66927
|
+
path: "get-usage",
|
|
66928
|
+
read: (body) => {
|
|
66929
|
+
const session = bodyString(body, "session");
|
|
66930
|
+
const skipBehaviors = "skipBehaviors" in body ? bodyBoolean(body, "skipBehaviors") : void 0;
|
|
66931
|
+
return session === void 0 || "skipBehaviors" in body && skipBehaviors === void 0 ? { error: "the get-usage body must be JSON naming a non-empty session id, and skipBehaviors only as a boolean or absent" } : { session, run: async () => await deps.getUsage(session, skipBehaviors) };
|
|
66932
|
+
}
|
|
66933
|
+
},
|
|
66934
|
+
{
|
|
66935
|
+
name: "get-context-usage",
|
|
66936
|
+
path: "get-context-usage",
|
|
66937
|
+
read: (body) => {
|
|
66938
|
+
const session = bodyString(body, "session");
|
|
66939
|
+
const detail = "detail" in body && isRcContextUsageDetail(body.detail) ? body.detail : void 0;
|
|
66940
|
+
return session === void 0 || "detail" in body && detail === void 0 ? { error: `the get-context-usage body must be JSON naming a non-empty session id, and a detail level the SDK's own type permits (${RC_CONTEXT_USAGE_DETAILS.join(", ")}) or none` } : { session, run: async () => await deps.getContextUsage(session, detail) };
|
|
66941
|
+
}
|
|
66942
|
+
},
|
|
66943
|
+
{
|
|
66944
|
+
name: "read-file",
|
|
66945
|
+
path: "read-file",
|
|
66946
|
+
read: (body) => {
|
|
66947
|
+
const session = bodyString(body, "session");
|
|
66948
|
+
const path46 = bodyString(body, "path");
|
|
66949
|
+
const maxBytes = "maxBytes" in body ? bodyPositiveInteger(body, "maxBytes") : void 0;
|
|
66950
|
+
const encoding = "encoding" in body && isRcReadFileEncoding(body.encoding) ? body.encoding : void 0;
|
|
66951
|
+
const refused2 = session === void 0 || path46 === void 0 || "maxBytes" in body && maxBytes === void 0 || "encoding" in body && encoding === void 0;
|
|
66952
|
+
return refused2 ? { error: `the read-file body must be JSON naming a non-empty session id and path, maxBytes only as a positive whole number or absent, and an encoding the SDK's own type permits (${RC_READ_FILE_ENCODINGS.join(", ")}) or none` } : { session, run: async () => await deps.readFile(session, path46, { ...maxBytes === void 0 ? {} : { maxBytes }, ...encoding === void 0 ? {} : { encoding } }) };
|
|
66953
|
+
}
|
|
66954
|
+
},
|
|
66955
|
+
{
|
|
66956
|
+
name: "file-suggestions",
|
|
66957
|
+
path: "file-suggestions",
|
|
66958
|
+
read: (body) => {
|
|
66959
|
+
const session = bodyString(body, "session");
|
|
66960
|
+
const query = bodyStringAllowingEmpty(body, "query");
|
|
66961
|
+
return session === void 0 || query === void 0 ? { error: "the file-suggestions body must be JSON naming a non-empty session id and a query string, which may be empty" } : { session, run: async () => await deps.fileSuggestions(session, query) };
|
|
66962
|
+
}
|
|
66963
|
+
},
|
|
66964
|
+
{
|
|
66965
|
+
name: "keep-alive",
|
|
66966
|
+
path: "keep-alive",
|
|
66967
|
+
read: (body) => {
|
|
66968
|
+
const session = bodyString(body, "session");
|
|
66969
|
+
return session === void 0 ? { error: "the keep-alive body must be JSON naming a non-empty session id" } : { session, run: async () => await deps.keepAlive(session) };
|
|
66970
|
+
}
|
|
66971
|
+
},
|
|
66972
|
+
{
|
|
66973
|
+
name: "mcp-status",
|
|
66974
|
+
path: "mcp-status",
|
|
66975
|
+
read: (body) => {
|
|
66976
|
+
const session = bodyString(body, "session");
|
|
66977
|
+
return session === void 0 ? { error: "the mcp-status body must be JSON naming a non-empty session id" } : { session, run: async () => await deps.mcpStatus(session) };
|
|
66978
|
+
}
|
|
66979
|
+
},
|
|
66980
|
+
{
|
|
66981
|
+
name: "mcp-reconnect",
|
|
66982
|
+
path: "mcp-reconnect",
|
|
66983
|
+
read: (body) => {
|
|
66984
|
+
const session = bodyString(body, "session");
|
|
66985
|
+
const serverName = bodyString(body, "serverName");
|
|
66986
|
+
return session === void 0 || serverName === void 0 ? { error: "the mcp-reconnect body must be JSON naming a non-empty session id and server name" } : { session, run: async () => await deps.mcpReconnect(session, serverName) };
|
|
66987
|
+
}
|
|
66988
|
+
},
|
|
66989
|
+
{
|
|
66990
|
+
name: "mcp-authenticate",
|
|
66991
|
+
path: "mcp-authenticate",
|
|
66992
|
+
read: (body) => {
|
|
66993
|
+
const session = bodyString(body, "session");
|
|
66994
|
+
const serverName = bodyString(body, "serverName");
|
|
66995
|
+
const redirectUri = bodyString(body, "redirectUri");
|
|
66996
|
+
return session === void 0 || serverName === void 0 || redirectUri === void 0 ? { error: "the mcp-authenticate body must be JSON naming a non-empty session id, server name, and redirect URI" } : { session, run: async () => await deps.mcpAuthenticate(session, serverName, redirectUri) };
|
|
66997
|
+
}
|
|
66998
|
+
},
|
|
66999
|
+
{
|
|
67000
|
+
name: "mcp-oauth-callback-url",
|
|
67001
|
+
path: "mcp-oauth-callback-url",
|
|
67002
|
+
read: (body) => {
|
|
67003
|
+
const session = bodyString(body, "session");
|
|
67004
|
+
const serverName = bodyString(body, "serverName");
|
|
67005
|
+
const callbackUrl = bodyString(body, "callbackUrl");
|
|
67006
|
+
return session === void 0 || serverName === void 0 || callbackUrl === void 0 ? { error: "the mcp-oauth-callback-url body must be JSON naming a non-empty session id, server name, and callback URL" } : { session, run: async () => await deps.mcpOAuthCallbackUrl(session, serverName, callbackUrl) };
|
|
67007
|
+
}
|
|
67008
|
+
}
|
|
67009
|
+
];
|
|
66791
67010
|
return (request7, response) => {
|
|
66792
67011
|
const presented = request7.headers.authorization;
|
|
66793
67012
|
const bearerPrefix = "bearer ".length;
|
|
@@ -66872,73 +67091,33 @@ function createRcControlHandler(deps) {
|
|
|
66872
67091
|
answerJson(response, HTTP_STATUS.badGateway, { error: result.message });
|
|
66873
67092
|
return;
|
|
66874
67093
|
}
|
|
66875
|
-
|
|
66876
|
-
if (
|
|
66877
|
-
|
|
66878
|
-
return;
|
|
66879
|
-
}
|
|
66880
|
-
let parsed;
|
|
66881
|
-
try {
|
|
66882
|
-
parsed = JSON.parse(await readBody(request7));
|
|
66883
|
-
} catch {
|
|
66884
|
-
answerJson(response, HTTP_STATUS.badRequest, { error: "the interrupt body is not readable JSON within the control body cap" });
|
|
66885
|
-
return;
|
|
66886
|
-
}
|
|
66887
|
-
if (typeof parsed !== "object" || parsed === null || !("session" in parsed) || typeof parsed.session !== "string" || parsed.session === "") {
|
|
66888
|
-
answerJson(response, HTTP_STATUS.badRequest, { error: "the interrupt body must be JSON naming a non-empty session id" });
|
|
66889
|
-
return;
|
|
66890
|
-
}
|
|
66891
|
-
const result = await deps.interrupt(parsed.session);
|
|
66892
|
-
if (result.ok) {
|
|
66893
|
-
answerJson(response, HTTP_STATUS.ok, { session: parsed.session, sequenceNums: result.sequenceNums });
|
|
66894
|
-
return;
|
|
67094
|
+
for (const route of controlWrites) {
|
|
67095
|
+
if (url2.pathname !== `${CONTROL_PATH_PREFIX}/${route.path}`) {
|
|
67096
|
+
continue;
|
|
66895
67097
|
}
|
|
66896
|
-
answerJson(response, HTTP_STATUS.badGateway, { error: result.message });
|
|
66897
|
-
return;
|
|
66898
|
-
}
|
|
66899
|
-
if (url2.pathname === setModelPath) {
|
|
66900
67098
|
if (request7.method !== "POST") {
|
|
66901
|
-
answerJson(response, HTTP_STATUS.methodNotAllowed, { error:
|
|
67099
|
+
answerJson(response, HTTP_STATUS.methodNotAllowed, { error: `the ${route.name} operation is a POST` });
|
|
66902
67100
|
return;
|
|
66903
67101
|
}
|
|
66904
67102
|
let parsed;
|
|
66905
67103
|
try {
|
|
66906
67104
|
parsed = JSON.parse(await readBody(request7));
|
|
66907
67105
|
} catch {
|
|
66908
|
-
answerJson(response, HTTP_STATUS.badRequest, { error:
|
|
66909
|
-
return;
|
|
66910
|
-
}
|
|
66911
|
-
if (typeof parsed !== "object" || parsed === null || !("session" in parsed) || !("model" in parsed) || typeof parsed.session !== "string" || typeof parsed.model !== "string" || parsed.session === "" || parsed.model === "") {
|
|
66912
|
-
answerJson(response, HTTP_STATUS.badRequest, { error: "the set-model body must be JSON naming a non-empty session id and model id" });
|
|
66913
|
-
return;
|
|
66914
|
-
}
|
|
66915
|
-
const result = await deps.setModel(parsed.session, parsed.model);
|
|
66916
|
-
if (result.ok) {
|
|
66917
|
-
answerJson(response, HTTP_STATUS.ok, { session: parsed.session, model: parsed.model, sequenceNums: result.sequenceNums });
|
|
66918
|
-
return;
|
|
66919
|
-
}
|
|
66920
|
-
answerJson(response, HTTP_STATUS.badGateway, { error: result.message });
|
|
66921
|
-
return;
|
|
66922
|
-
}
|
|
66923
|
-
if (url2.pathname === setPermissionModePath) {
|
|
66924
|
-
if (request7.method !== "POST") {
|
|
66925
|
-
answerJson(response, HTTP_STATUS.methodNotAllowed, { error: "the set-permission-mode operation is a POST" });
|
|
67106
|
+
answerJson(response, HTTP_STATUS.badRequest, { error: `the ${route.name} body is not readable JSON within the control body cap` });
|
|
66926
67107
|
return;
|
|
66927
67108
|
}
|
|
66928
|
-
|
|
66929
|
-
|
|
66930
|
-
parsed = JSON.parse(await readBody(request7));
|
|
66931
|
-
} catch {
|
|
66932
|
-
answerJson(response, HTTP_STATUS.badRequest, { error: "the set-permission-mode body is not readable JSON within the control body cap" });
|
|
67109
|
+
if (!isJsonObject(parsed)) {
|
|
67110
|
+
answerJson(response, HTTP_STATUS.badRequest, { error: `the ${route.name} body must be a JSON object` });
|
|
66933
67111
|
return;
|
|
66934
67112
|
}
|
|
66935
|
-
|
|
66936
|
-
|
|
67113
|
+
const read = route.read(parsed);
|
|
67114
|
+
if ("error" in read) {
|
|
67115
|
+
answerJson(response, HTTP_STATUS.badRequest, { error: read.error });
|
|
66937
67116
|
return;
|
|
66938
67117
|
}
|
|
66939
|
-
const result = await
|
|
67118
|
+
const result = await read.run();
|
|
66940
67119
|
if (result.ok) {
|
|
66941
|
-
answerJson(response, HTTP_STATUS.ok, { session:
|
|
67120
|
+
answerJson(response, HTTP_STATUS.ok, { session: read.session, ...result.requestId === void 0 ? {} : { request: result.requestId }, sequenceNums: result.sequenceNums });
|
|
66942
67121
|
return;
|
|
66943
67122
|
}
|
|
66944
67123
|
answerJson(response, HTTP_STATUS.badGateway, { error: result.message });
|
|
@@ -67056,6 +67235,19 @@ function sequenceNumsOfBody(body) {
|
|
|
67056
67235
|
}
|
|
67057
67236
|
return sequenceNums;
|
|
67058
67237
|
}
|
|
67238
|
+
function requestIdOfBody(body) {
|
|
67239
|
+
let parsed;
|
|
67240
|
+
try {
|
|
67241
|
+
parsed = JSON.parse(body);
|
|
67242
|
+
} catch {
|
|
67243
|
+
return void 0;
|
|
67244
|
+
}
|
|
67245
|
+
if (typeof parsed !== "object" || parsed === null || !("request" in parsed)) {
|
|
67246
|
+
return void 0;
|
|
67247
|
+
}
|
|
67248
|
+
const value2 = parsed.request;
|
|
67249
|
+
return typeof value2 === "string" && value2 !== "" ? value2 : void 0;
|
|
67250
|
+
}
|
|
67059
67251
|
function frontDoorRcControl(transport, token) {
|
|
67060
67252
|
const headers = { authorization: `Bearer ${token}` };
|
|
67061
67253
|
const listAnswerOf = async (path46, what) => {
|
|
@@ -67144,7 +67336,8 @@ function frontDoorRcControl(transport, token) {
|
|
|
67144
67336
|
if (sequenceNums === void 0) {
|
|
67145
67337
|
return { ok: false, message: `the door accepted the event but its answer named no sequence numbers: ${answer.body}` };
|
|
67146
67338
|
}
|
|
67147
|
-
|
|
67339
|
+
const requestId = requestIdOfBody(answer.body);
|
|
67340
|
+
return requestId === void 0 ? { ok: true, sequenceNums } : { ok: true, sequenceNums, requestId };
|
|
67148
67341
|
}
|
|
67149
67342
|
return { ok: false, message: describeFailure(answer.status, controlErrorOf(answer.body) ?? answer.body) };
|
|
67150
67343
|
};
|
|
@@ -67161,7 +67354,21 @@ function frontDoorRcControl(transport, token) {
|
|
|
67161
67354
|
),
|
|
67162
67355
|
interruptSession: async (sessionId) => await writeOf(`${CONTROL_PATH_PREFIX}/interrupt`, JSON.stringify({ session: sessionId }), (status, message) => `interrupting ${sessionId} failed (HTTP ${String(status)}): ${message}`),
|
|
67163
67356
|
setModel: async (sessionId, model) => await writeOf(`${CONTROL_PATH_PREFIX}/set-model`, JSON.stringify({ session: sessionId, model }), (status, message) => `setting the model on ${sessionId} failed (HTTP ${String(status)}): ${message}`),
|
|
67164
|
-
setPermissionMode: async (sessionId, mode) => await writeOf(`${CONTROL_PATH_PREFIX}/set-permission-mode`, JSON.stringify({ session: sessionId, mode }), (status, message) => `setting the permission mode on ${sessionId} failed (HTTP ${String(status)}): ${message}`)
|
|
67357
|
+
setPermissionMode: async (sessionId, mode) => await writeOf(`${CONTROL_PATH_PREFIX}/set-permission-mode`, JSON.stringify({ session: sessionId, mode }), (status, message) => `setting the permission mode on ${sessionId} failed (HTTP ${String(status)}): ${message}`),
|
|
67358
|
+
endSession: async (sessionId, reason) => await writeOf(`${CONTROL_PATH_PREFIX}/end-session`, JSON.stringify({ session: sessionId, ...reason === void 0 ? {} : { reason } }), (status, message) => `ending ${sessionId} failed (HTTP ${String(status)}): ${message}`),
|
|
67359
|
+
getUsage: async (sessionId, skipBehaviors) => await writeOf(`${CONTROL_PATH_PREFIX}/get-usage`, JSON.stringify({ session: sessionId, ...skipBehaviors === void 0 ? {} : { skipBehaviors } }), (status, message) => `requesting the usage of ${sessionId} failed (HTTP ${String(status)}): ${message}`),
|
|
67360
|
+
getContextUsage: async (sessionId, detail) => await writeOf(`${CONTROL_PATH_PREFIX}/get-context-usage`, JSON.stringify({ session: sessionId, ...detail === void 0 ? {} : { detail } }), (status, message) => `requesting the context usage of ${sessionId} failed (HTTP ${String(status)}): ${message}`),
|
|
67361
|
+
readFile: async (sessionId, path46, options) => await writeOf(
|
|
67362
|
+
`${CONTROL_PATH_PREFIX}/read-file`,
|
|
67363
|
+
JSON.stringify({ session: sessionId, path: path46, ...options?.maxBytes === void 0 ? {} : { maxBytes: options.maxBytes }, ...options?.encoding === void 0 ? {} : { encoding: options.encoding } }),
|
|
67364
|
+
(status, message) => `reading ${path46} from ${sessionId} failed (HTTP ${String(status)}): ${message}`
|
|
67365
|
+
),
|
|
67366
|
+
fileSuggestions: async (sessionId, query) => await writeOf(`${CONTROL_PATH_PREFIX}/file-suggestions`, JSON.stringify({ session: sessionId, query }), (status, message) => `requesting file suggestions from ${sessionId} failed (HTTP ${String(status)}): ${message}`),
|
|
67367
|
+
keepAlive: async (sessionId) => await writeOf(`${CONTROL_PATH_PREFIX}/keep-alive`, JSON.stringify({ session: sessionId }), (status, message) => `keeping ${sessionId} alive failed (HTTP ${String(status)}): ${message}`),
|
|
67368
|
+
mcpStatus: async (sessionId) => await writeOf(`${CONTROL_PATH_PREFIX}/mcp-status`, JSON.stringify({ session: sessionId }), (status, message) => `requesting the MCP status of ${sessionId} failed (HTTP ${String(status)}): ${message}`),
|
|
67369
|
+
mcpReconnect: async (sessionId, serverName) => await writeOf(`${CONTROL_PATH_PREFIX}/mcp-reconnect`, JSON.stringify({ session: sessionId, serverName }), (status, message) => `reconnecting MCP server ${serverName} on ${sessionId} failed (HTTP ${String(status)}): ${message}`),
|
|
67370
|
+
mcpAuthenticate: async (sessionId, serverName, redirectUri) => await writeOf(`${CONTROL_PATH_PREFIX}/mcp-authenticate`, JSON.stringify({ session: sessionId, serverName, redirectUri }), (status, message) => `authenticating MCP server ${serverName} on ${sessionId} failed (HTTP ${String(status)}): ${message}`),
|
|
67371
|
+
mcpOAuthCallbackUrl: async (sessionId, serverName, callbackUrl) => await writeOf(`${CONTROL_PATH_PREFIX}/mcp-oauth-callback-url`, JSON.stringify({ session: sessionId, serverName, callbackUrl }), (status, message) => `handing MCP server ${serverName}'s OAuth callback to ${sessionId} failed (HTTP ${String(status)}): ${message}`)
|
|
67165
67372
|
};
|
|
67166
67373
|
}
|
|
67167
67374
|
|
|
@@ -71825,6 +72032,48 @@ var RcSetPermissionModeInputSchema = external_exports.strictObject({
|
|
|
71825
72032
|
session: external_exports.string().min(1),
|
|
71826
72033
|
mode: RcPermissionModeSchema
|
|
71827
72034
|
});
|
|
72035
|
+
var RcEndSessionInputSchema = external_exports.strictObject({
|
|
72036
|
+
session: external_exports.string().min(1),
|
|
72037
|
+
reason: external_exports.string().min(1).optional()
|
|
72038
|
+
});
|
|
72039
|
+
var RcGetUsageInputSchema = external_exports.strictObject({
|
|
72040
|
+
session: external_exports.string().min(1),
|
|
72041
|
+
skipBehaviors: external_exports.boolean().optional()
|
|
72042
|
+
});
|
|
72043
|
+
var RcGetContextUsageInputSchema = external_exports.strictObject({
|
|
72044
|
+
session: external_exports.string().min(1),
|
|
72045
|
+
detail: external_exports.enum(RC_CONTEXT_USAGE_DETAILS).optional()
|
|
72046
|
+
});
|
|
72047
|
+
var RcReadFileInputSchema = external_exports.strictObject({
|
|
72048
|
+
session: external_exports.string().min(1),
|
|
72049
|
+
path: external_exports.string().min(1),
|
|
72050
|
+
maxBytes: external_exports.number().int().positive().optional(),
|
|
72051
|
+
encoding: external_exports.enum(RC_READ_FILE_ENCODINGS).optional()
|
|
72052
|
+
});
|
|
72053
|
+
var RcFileSuggestionsInputSchema = external_exports.strictObject({
|
|
72054
|
+
session: external_exports.string().min(1),
|
|
72055
|
+
query: external_exports.string()
|
|
72056
|
+
});
|
|
72057
|
+
var RcKeepAliveInputSchema = external_exports.strictObject({
|
|
72058
|
+
session: external_exports.string().min(1)
|
|
72059
|
+
});
|
|
72060
|
+
var RcMcpStatusInputSchema = external_exports.strictObject({
|
|
72061
|
+
session: external_exports.string().min(1)
|
|
72062
|
+
});
|
|
72063
|
+
var RcMcpReconnectInputSchema = external_exports.strictObject({
|
|
72064
|
+
session: external_exports.string().min(1),
|
|
72065
|
+
serverName: external_exports.string().min(1)
|
|
72066
|
+
});
|
|
72067
|
+
var RcMcpAuthenticateInputSchema = external_exports.strictObject({
|
|
72068
|
+
session: external_exports.string().min(1),
|
|
72069
|
+
serverName: external_exports.string().min(1),
|
|
72070
|
+
redirectUri: external_exports.string().min(1)
|
|
72071
|
+
});
|
|
72072
|
+
var RcMcpOAuthCallbackUrlInputSchema = external_exports.strictObject({
|
|
72073
|
+
session: external_exports.string().min(1),
|
|
72074
|
+
serverName: external_exports.string().min(1),
|
|
72075
|
+
callbackUrl: external_exports.string().min(1)
|
|
72076
|
+
});
|
|
71828
72077
|
var RcListOutputSchema = external_exports.strictObject({ sessions: external_exports.readonly(external_exports.array(RcSessionSummarySchema)) });
|
|
71829
72078
|
var RcStatusOutputSchema = external_exports.strictObject({ statuses: external_exports.readonly(external_exports.array(RcSessionStatusSchema)) });
|
|
71830
72079
|
var RcPendingOutputSchema = external_exports.strictObject({ pending: external_exports.readonly(external_exports.array(RcPendingRequestSummarySchema)) });
|
|
@@ -71832,6 +72081,11 @@ var RcWriteOutputSchema = external_exports.strictObject({
|
|
|
71832
72081
|
session: external_exports.string(),
|
|
71833
72082
|
sequenceNums: external_exports.readonly(external_exports.array(external_exports.number()))
|
|
71834
72083
|
});
|
|
72084
|
+
var RcControlWriteOutputSchema = external_exports.strictObject({
|
|
72085
|
+
session: external_exports.string(),
|
|
72086
|
+
request: external_exports.string(),
|
|
72087
|
+
sequenceNums: external_exports.readonly(external_exports.array(external_exports.number()))
|
|
72088
|
+
});
|
|
71835
72089
|
var RcAnswerOutputSchema = external_exports.strictObject({
|
|
71836
72090
|
session: external_exports.string(),
|
|
71837
72091
|
request: external_exports.string(),
|
|
@@ -71860,6 +72114,15 @@ function createRcApiRouter(deps) {
|
|
|
71860
72114
|
}
|
|
71861
72115
|
return result.sequenceNums;
|
|
71862
72116
|
};
|
|
72117
|
+
const deliveredControl = (sessionId, result) => {
|
|
72118
|
+
if (!result.ok) {
|
|
72119
|
+
throw new ORPCError("BAD_GATEWAY", { message: result.message });
|
|
72120
|
+
}
|
|
72121
|
+
if (result.requestId === void 0) {
|
|
72122
|
+
throw new ORPCError("INTERNAL_SERVER_ERROR", { message: "the control request was delivered but the door cannot name the request id it minted, so the worker's answer cannot be matched" });
|
|
72123
|
+
}
|
|
72124
|
+
return { request: result.requestId, sequenceNums: result.sequenceNums };
|
|
72125
|
+
};
|
|
71863
72126
|
return {
|
|
71864
72127
|
rc: {
|
|
71865
72128
|
list: authed.output(RcListOutputSchema).handler(() => ({ sessions: deps.list() })),
|
|
@@ -71882,9 +72145,19 @@ function createRcApiRouter(deps) {
|
|
|
71882
72145
|
const decision = { approve: input2.approve, message: input2.approve || input2.text === void 0 ? void 0 : input2.text };
|
|
71883
72146
|
return { session: input2.session, request: input2.request, sequenceNums: delivered(input2.session, await deps.answer(input2.session, input2.request, decision)) };
|
|
71884
72147
|
}),
|
|
71885
|
-
interrupt: authed.input(RcInterruptInputSchema).output(
|
|
71886
|
-
setModel: authed.input(RcSetModelInputSchema).output(
|
|
71887
|
-
setPermissionMode: authed.input(RcSetPermissionModeInputSchema).output(
|
|
72148
|
+
interrupt: authed.input(RcInterruptInputSchema).output(RcControlWriteOutputSchema).handler(async ({ input: input2 }) => ({ session: input2.session, ...deliveredControl(input2.session, await deps.interrupt(input2.session)) })),
|
|
72149
|
+
setModel: authed.input(RcSetModelInputSchema).output(RcControlWriteOutputSchema).handler(async ({ input: input2 }) => ({ session: input2.session, ...deliveredControl(input2.session, await deps.setModel(input2.session, input2.model)) })),
|
|
72150
|
+
setPermissionMode: authed.input(RcSetPermissionModeInputSchema).output(RcControlWriteOutputSchema).handler(async ({ input: input2 }) => ({ session: input2.session, ...deliveredControl(input2.session, await deps.setPermissionMode(input2.session, input2.mode)) })),
|
|
72151
|
+
endSession: authed.input(RcEndSessionInputSchema).output(RcControlWriteOutputSchema).handler(async ({ input: input2 }) => ({ session: input2.session, ...deliveredControl(input2.session, await deps.endSession(input2.session, input2.reason)) })),
|
|
72152
|
+
getUsage: authed.input(RcGetUsageInputSchema).output(RcControlWriteOutputSchema).handler(async ({ input: input2 }) => ({ session: input2.session, ...deliveredControl(input2.session, await deps.getUsage(input2.session, input2.skipBehaviors)) })),
|
|
72153
|
+
getContextUsage: authed.input(RcGetContextUsageInputSchema).output(RcControlWriteOutputSchema).handler(async ({ input: input2 }) => ({ session: input2.session, ...deliveredControl(input2.session, await deps.getContextUsage(input2.session, input2.detail)) })),
|
|
72154
|
+
readFile: authed.input(RcReadFileInputSchema).output(RcControlWriteOutputSchema).handler(async ({ input: input2 }) => ({ session: input2.session, ...deliveredControl(input2.session, await deps.readFile(input2.session, input2.path, { ...input2.maxBytes === void 0 ? {} : { maxBytes: input2.maxBytes }, ...input2.encoding === void 0 ? {} : { encoding: input2.encoding } })) })),
|
|
72155
|
+
fileSuggestions: authed.input(RcFileSuggestionsInputSchema).output(RcControlWriteOutputSchema).handler(async ({ input: input2 }) => ({ session: input2.session, ...deliveredControl(input2.session, await deps.fileSuggestions(input2.session, input2.query)) })),
|
|
72156
|
+
keepAlive: authed.input(RcKeepAliveInputSchema).output(RcWriteOutputSchema).handler(async ({ input: input2 }) => ({ session: input2.session, sequenceNums: delivered(input2.session, await deps.keepAlive(input2.session)) })),
|
|
72157
|
+
mcpStatus: authed.input(RcMcpStatusInputSchema).output(RcControlWriteOutputSchema).handler(async ({ input: input2 }) => ({ session: input2.session, ...deliveredControl(input2.session, await deps.mcpStatus(input2.session)) })),
|
|
72158
|
+
mcpReconnect: authed.input(RcMcpReconnectInputSchema).output(RcControlWriteOutputSchema).handler(async ({ input: input2 }) => ({ session: input2.session, ...deliveredControl(input2.session, await deps.mcpReconnect(input2.session, input2.serverName)) })),
|
|
72159
|
+
mcpAuthenticate: authed.input(RcMcpAuthenticateInputSchema).output(RcControlWriteOutputSchema).handler(async ({ input: input2 }) => ({ session: input2.session, ...deliveredControl(input2.session, await deps.mcpAuthenticate(input2.session, input2.serverName, input2.redirectUri)) })),
|
|
72160
|
+
mcpOAuthCallbackUrl: authed.input(RcMcpOAuthCallbackUrlInputSchema).output(RcControlWriteOutputSchema).handler(async ({ input: input2 }) => ({ session: input2.session, ...deliveredControl(input2.session, await deps.mcpOAuthCallbackUrl(input2.session, input2.serverName, input2.callbackUrl)) })),
|
|
71888
72161
|
subscribe: authed.input(RcSessionQuerySchema).output(eventIterator(RcStreamEventSchema)).handler(async function* ({ input: input2, signal }) {
|
|
71889
72162
|
const queue = [];
|
|
71890
72163
|
let wake;
|
|
@@ -73459,1419 +73732,884 @@ function attachingProviderCredential(route, deps) {
|
|
|
73459
73732
|
};
|
|
73460
73733
|
}
|
|
73461
73734
|
|
|
73462
|
-
// src/frontdoor/
|
|
73735
|
+
// src/frontdoor/rcCommands.ts
|
|
73736
|
+
var import_node_crypto14 = require("node:crypto");
|
|
73737
|
+
|
|
73738
|
+
// src/frontdoor/rcSelfHostMint.ts
|
|
73463
73739
|
var import_node_path34 = __toESM(require("node:path"), 1);
|
|
73464
|
-
|
|
73465
|
-
|
|
73740
|
+
|
|
73741
|
+
// src/frontdoor/rcWebFetch.ts
|
|
73742
|
+
var dns2 = __toESM(require("node:dns"), 1);
|
|
73743
|
+
var http6 = __toESM(require("node:http"), 1);
|
|
73744
|
+
var https5 = __toESM(require("node:https"), 1);
|
|
73745
|
+
var net3 = __toESM(require("node:net"), 1);
|
|
73746
|
+
var RC_WEB_FETCH_MAX_BYTES = 12582912;
|
|
73747
|
+
var RC_WEB_FETCH_DEADLINE_MS = 4e4;
|
|
73748
|
+
var RC_WEB_FETCH_MAX_REDIRECTS = 20;
|
|
73749
|
+
var RC_WEB_FETCH_ALLOW_PRIVATE_ENV = "AGENT_SHIM_FRONTDOOR_RC_SELF_HOST_WEB_FETCH_ALLOW_PRIVATE";
|
|
73750
|
+
function rcWebFetchAllowsPrivateFromEnv(env) {
|
|
73751
|
+
return env[RC_WEB_FETCH_ALLOW_PRIVATE_ENV] === "1";
|
|
73466
73752
|
}
|
|
73467
|
-
|
|
73468
|
-
|
|
73753
|
+
var WEB_FETCH_USER_AGENT = "agent-shim-frontdoor";
|
|
73754
|
+
var ERROR_PREFIX = "web_fetch_";
|
|
73755
|
+
var V4_THIS_NETWORK_OCTET = 0;
|
|
73756
|
+
var V4_PRIVATE_A_OCTET = 10;
|
|
73757
|
+
var V4_LOOPBACK_OCTET = 127;
|
|
73758
|
+
var V4_CGNAT_OCTET = 100;
|
|
73759
|
+
var V4_CGNAT_SECOND_FROM = 64;
|
|
73760
|
+
var V4_CGNAT_SECOND_TO = 127;
|
|
73761
|
+
var V4_LINK_LOCAL_OCTET = 169;
|
|
73762
|
+
var V4_LINK_LOCAL_SECOND = 254;
|
|
73763
|
+
var V4_PRIVATE_B_OCTET = 172;
|
|
73764
|
+
var V4_PRIVATE_B_SECOND_FROM = 16;
|
|
73765
|
+
var V4_PRIVATE_B_SECOND_TO = 31;
|
|
73766
|
+
var V4_PRIVATE_C_OCTET = 192;
|
|
73767
|
+
var V4_PRIVATE_C_SECOND = 168;
|
|
73768
|
+
var V4_MULTICAST_FROM_OCTET = 224;
|
|
73769
|
+
var V6_UNIQUE_LOCAL_FROM = 64512;
|
|
73770
|
+
var V6_UNIQUE_LOCAL_TO = 65023;
|
|
73771
|
+
var V6_LINK_LOCAL_FROM = 65152;
|
|
73772
|
+
var V6_LINK_LOCAL_TO = 65215;
|
|
73773
|
+
var V6_MULTICAST_FROM = 65280;
|
|
73774
|
+
var IP_VERSION_4 = 4;
|
|
73775
|
+
var IP_VERSION_6 = 6;
|
|
73776
|
+
var HTTP_MOVED_PERMANENTLY = 301;
|
|
73777
|
+
var HTTP_FOUND = 302;
|
|
73778
|
+
var HTTP_SEE_OTHER = 303;
|
|
73779
|
+
var HTTP_TEMPORARY_REDIRECT = 307;
|
|
73780
|
+
var HTTP_PERMANENT_REDIRECT = 308;
|
|
73781
|
+
var SUCCESS_STATUS_MAX_EXCLUSIVE3 = 300;
|
|
73782
|
+
function refused(errorType, errorMessage2) {
|
|
73783
|
+
return { kind: "refused", errorType: `${ERROR_PREFIX}${errorType}`, errorMessage: errorMessage2 };
|
|
73469
73784
|
}
|
|
73470
|
-
function
|
|
73471
|
-
|
|
73472
|
-
|
|
73785
|
+
function isPrivateAddress(address) {
|
|
73786
|
+
const version2 = net3.isIP(address);
|
|
73787
|
+
if (version2 === IP_VERSION_4) {
|
|
73788
|
+
const octets = address.split(".").map((part) => Number(part));
|
|
73789
|
+
const first = octets[0] ?? -1;
|
|
73790
|
+
const second = octets[1] ?? -1;
|
|
73791
|
+
if (first === V4_THIS_NETWORK_OCTET || first === V4_PRIVATE_A_OCTET || first === V4_LOOPBACK_OCTET) {
|
|
73792
|
+
return true;
|
|
73793
|
+
}
|
|
73794
|
+
if (first === V4_CGNAT_OCTET && second >= V4_CGNAT_SECOND_FROM && second <= V4_CGNAT_SECOND_TO) {
|
|
73795
|
+
return true;
|
|
73796
|
+
}
|
|
73797
|
+
if (first === V4_LINK_LOCAL_OCTET && second === V4_LINK_LOCAL_SECOND) {
|
|
73798
|
+
return true;
|
|
73799
|
+
}
|
|
73800
|
+
if (first === V4_PRIVATE_B_OCTET && second >= V4_PRIVATE_B_SECOND_FROM && second <= V4_PRIVATE_B_SECOND_TO) {
|
|
73801
|
+
return true;
|
|
73802
|
+
}
|
|
73803
|
+
if (first === V4_PRIVATE_C_OCTET && second === V4_PRIVATE_C_SECOND) {
|
|
73804
|
+
return true;
|
|
73805
|
+
}
|
|
73806
|
+
return first >= V4_MULTICAST_FROM_OCTET;
|
|
73473
73807
|
}
|
|
73474
|
-
|
|
73475
|
-
|
|
73476
|
-
|
|
73477
|
-
|
|
73478
|
-
|
|
73479
|
-
authorization,
|
|
73480
|
-
anthropicVersion: typeof anthropicVersion === "string" ? anthropicVersion : void 0,
|
|
73481
|
-
anthropicClientPlatform: typeof anthropicClientPlatform === "string" ? anthropicClientPlatform : void 0
|
|
73482
|
-
};
|
|
73483
|
-
}
|
|
73484
|
-
function sequenceNumOfJson(value2) {
|
|
73485
|
-
return typeof value2 === "number" && Number.isInteger(value2) && value2 >= 0 ? value2 : void 0;
|
|
73486
|
-
}
|
|
73487
|
-
function createRcCredentialStore(fs20, dir) {
|
|
73488
|
-
fs20.mkdirPrivate(dir);
|
|
73489
|
-
const fileFor = (sessionId) => import_node_path34.default.join(dir, `${sessionId}.json`);
|
|
73490
|
-
const readParsed = (sessionId) => {
|
|
73491
|
-
const raw = fs20.readFileUtf8(fileFor(sessionId));
|
|
73492
|
-
if (raw === void 0) {
|
|
73493
|
-
return void 0;
|
|
73808
|
+
if (version2 === IP_VERSION_6) {
|
|
73809
|
+
const lower = address.toLowerCase();
|
|
73810
|
+
const mapped = /^::ffff:(\d{1,3}(?:\.\d{1,3}){3})$/.exec(lower);
|
|
73811
|
+
if (mapped !== null) {
|
|
73812
|
+
return isPrivateAddress(mapped[1] ?? "");
|
|
73494
73813
|
}
|
|
73495
|
-
|
|
73496
|
-
|
|
73497
|
-
parsed = JSON.parse(raw);
|
|
73498
|
-
} catch {
|
|
73499
|
-
return void 0;
|
|
73814
|
+
if (lower === "::" || lower === "::1") {
|
|
73815
|
+
return true;
|
|
73500
73816
|
}
|
|
73501
|
-
|
|
73502
|
-
|
|
73503
|
-
|
|
73504
|
-
read: (sessionId) => {
|
|
73505
|
-
if (!isSafeSessionId(sessionId)) {
|
|
73506
|
-
return void 0;
|
|
73507
|
-
}
|
|
73508
|
-
return credentialOfJson(readParsed(sessionId));
|
|
73509
|
-
},
|
|
73510
|
-
write: (sessionId, credential) => {
|
|
73511
|
-
if (!isSafeSessionId(sessionId)) {
|
|
73512
|
-
return;
|
|
73513
|
-
}
|
|
73514
|
-
const cursor3 = sequenceNumOfJson(readParsed(sessionId)?.highestSequenceNum);
|
|
73515
|
-
const record2 = { ...credential, ...cursor3 === void 0 ? {} : { highestSequenceNum: cursor3 } };
|
|
73516
|
-
fs20.writeFilePrivate(fileFor(sessionId), `${JSON.stringify(record2, null, 2)}
|
|
73517
|
-
`);
|
|
73518
|
-
},
|
|
73519
|
-
readCursor: (sessionId) => {
|
|
73520
|
-
if (!isSafeSessionId(sessionId)) {
|
|
73521
|
-
return void 0;
|
|
73522
|
-
}
|
|
73523
|
-
return sequenceNumOfJson(readParsed(sessionId)?.highestSequenceNum);
|
|
73524
|
-
},
|
|
73525
|
-
writeCursor: (sessionId, highestSequenceNum) => {
|
|
73526
|
-
if (!isSafeSessionId(sessionId)) {
|
|
73527
|
-
return;
|
|
73528
|
-
}
|
|
73529
|
-
const credential = credentialOfJson(readParsed(sessionId));
|
|
73530
|
-
const record2 = credential === void 0 ? { highestSequenceNum } : { ...credential, highestSequenceNum };
|
|
73531
|
-
fs20.writeFilePrivate(fileFor(sessionId), `${JSON.stringify(record2, null, 2)}
|
|
73532
|
-
`);
|
|
73533
|
-
},
|
|
73534
|
-
remove: (sessionId) => {
|
|
73535
|
-
if (!isSafeSessionId(sessionId)) {
|
|
73536
|
-
return;
|
|
73537
|
-
}
|
|
73538
|
-
fs20.removeRecursive(fileFor(sessionId));
|
|
73817
|
+
const firstGroup = Number.parseInt(/^([0-9a-f]+):/.exec(lower)?.[1] ?? "0", 16);
|
|
73818
|
+
if (Number.isNaN(firstGroup)) {
|
|
73819
|
+
return true;
|
|
73539
73820
|
}
|
|
73540
|
-
|
|
73541
|
-
|
|
73542
|
-
|
|
73543
|
-
// src/frontdoor/rcClientPage.ts
|
|
73544
|
-
var RC_CLIENT_PATH = "/__agent-shim/client";
|
|
73545
|
-
var RC_CLIENT_PAGE_SCRIPT = `'use strict';
|
|
73546
|
-
// 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.
|
|
73547
|
-
var API_PREFIX = '/__agent-shim/orpc';
|
|
73548
|
-
var TOKEN_KEY = 'agent-shim-rc-token';
|
|
73549
|
-
var token = null;
|
|
73550
|
-
|
|
73551
|
-
function apiBase() { return window.location.origin + API_PREFIX; }
|
|
73552
|
-
|
|
73553
|
-
// Calls one procedure: resolves the output's own value, rejects with the door's error message when it answered one.
|
|
73554
|
-
async function rpc(path, input) {
|
|
73555
|
-
var response = await fetch(apiBase() + '/' + path, {
|
|
73556
|
-
method: 'POST',
|
|
73557
|
-
headers: { 'content-type': 'application/json', authorization: 'Bearer ' + token },
|
|
73558
|
-
body: JSON.stringify({ json: input === undefined ? {} : input }),
|
|
73559
|
-
});
|
|
73560
|
-
var envelope = await response.json().catch(function () { return {}; });
|
|
73561
|
-
if (!response.ok) {
|
|
73562
|
-
var message = envelope && envelope.json && envelope.json.message ? envelope.json.message : ('HTTP ' + response.status);
|
|
73563
|
-
var error = new Error(message);
|
|
73564
|
-
error.status = response.status;
|
|
73565
|
-
throw error;
|
|
73566
|
-
}
|
|
73567
|
-
return envelope.json;
|
|
73568
|
-
}
|
|
73569
|
-
|
|
73570
|
-
// 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.
|
|
73571
|
-
function parseSseFrames(buffer) {
|
|
73572
|
-
var frames = [];
|
|
73573
|
-
var rest = buffer;
|
|
73574
|
-
for (;;) {
|
|
73575
|
-
var boundary = rest.indexOf('\\n\\n');
|
|
73576
|
-
if (boundary === -1) { break; }
|
|
73577
|
-
var raw = rest.slice(0, boundary);
|
|
73578
|
-
rest = rest.slice(boundary + 2);
|
|
73579
|
-
var frame = { event: 'message', id: '', data: '' };
|
|
73580
|
-
var lines = raw.split('\\n');
|
|
73581
|
-
for (var i = 0; i < lines.length; i++) {
|
|
73582
|
-
var line = lines[i];
|
|
73583
|
-
if (line === '' || line.charAt(0) === ':') { continue; }
|
|
73584
|
-
var colon = line.indexOf(':');
|
|
73585
|
-
var field = colon === -1 ? line : line.slice(0, colon);
|
|
73586
|
-
var value = colon === -1 ? '' : line.slice(colon + 1);
|
|
73587
|
-
if (value.charAt(0) === ' ') { value = value.slice(1); }
|
|
73588
|
-
if (field === 'event') { frame.event = value; }
|
|
73589
|
-
if (field === 'id') { frame.id = value; }
|
|
73590
|
-
if (field === 'data') { frame.data += (frame.data === '' ? '' : '\\n') + value; }
|
|
73821
|
+
if (firstGroup >= V6_UNIQUE_LOCAL_FROM && firstGroup <= V6_UNIQUE_LOCAL_TO) {
|
|
73822
|
+
return true;
|
|
73591
73823
|
}
|
|
73592
|
-
if (
|
|
73824
|
+
if (firstGroup >= V6_LINK_LOCAL_FROM && firstGroup <= V6_LINK_LOCAL_TO) {
|
|
73825
|
+
return true;
|
|
73826
|
+
}
|
|
73827
|
+
return firstGroup >= V6_MULTICAST_FROM;
|
|
73593
73828
|
}
|
|
73594
|
-
return
|
|
73829
|
+
return true;
|
|
73595
73830
|
}
|
|
73596
|
-
|
|
73597
|
-
|
|
73598
|
-
function sequenceGap(tracker, event) {
|
|
73599
|
-
var previous = tracker[event.source];
|
|
73600
|
-
if (event.sequence > (previous === undefined ? 0 : previous)) { tracker[event.source] = event.sequence; }
|
|
73601
|
-
if (previous === undefined || event.sequence <= previous) { return 0; }
|
|
73602
|
-
return event.sequence - previous - 1;
|
|
73831
|
+
function isSuccessful3(status) {
|
|
73832
|
+
return status >= HTTP_STATUS.ok && status < SUCCESS_STATUS_MAX_EXCLUSIVE3;
|
|
73603
73833
|
}
|
|
73604
|
-
|
|
73605
|
-
|
|
73606
|
-
|
|
73607
|
-
// A bounded sketch of any payload, the feed's fallback rendering: one JSON line, cut with a marker rather than silently.
|
|
73608
|
-
function sketch(value, cap) {
|
|
73609
|
-
var text;
|
|
73610
|
-
try { text = JSON.stringify(value); } catch (e) { text = String(value); }
|
|
73611
|
-
if (text === undefined) { text = 'undefined'; }
|
|
73612
|
-
return text.length > cap ? text.slice(0, cap) + '...' : text;
|
|
73834
|
+
function isRedirect(status) {
|
|
73835
|
+
return status === HTTP_MOVED_PERMANENTLY || status === HTTP_FOUND || status === HTTP_SEE_OTHER || status === HTTP_TEMPORARY_REDIRECT || status === HTTP_PERMANENT_REDIRECT;
|
|
73613
73836
|
}
|
|
73614
|
-
|
|
73615
|
-
|
|
73616
|
-
|
|
73617
|
-
var envelope = event.envelope;
|
|
73618
|
-
var payload = envelope.payload;
|
|
73619
|
-
var headline = '';
|
|
73620
|
-
if (payload !== undefined && payload !== null && typeof payload === 'object' && Array.isArray(payload.content)) {
|
|
73621
|
-
var parts = [];
|
|
73622
|
-
for (var i = 0; i < payload.content.length; i++) {
|
|
73623
|
-
var block = payload.content[i];
|
|
73624
|
-
if (block !== null && typeof block === 'object' && block.type === 'text' && typeof block.text === 'string') { parts.push(block.text); }
|
|
73625
|
-
}
|
|
73626
|
-
if (parts.length > 0) { headline = parts.join(' '); }
|
|
73837
|
+
function readableContentType(contentType) {
|
|
73838
|
+
if (contentType === void 0 || contentType.trim() === "") {
|
|
73839
|
+
return true;
|
|
73627
73840
|
}
|
|
73628
|
-
|
|
73629
|
-
|
|
73630
|
-
|
|
73841
|
+
const normalised = contentType.split(";")[0]?.trim().toLowerCase() ?? "";
|
|
73842
|
+
const slash = normalised.indexOf("/");
|
|
73843
|
+
const main2 = slash === -1 ? normalised : normalised.slice(0, slash);
|
|
73844
|
+
const sub = slash === -1 ? "" : normalised.slice(slash + 1);
|
|
73845
|
+
if (main2 === "text") {
|
|
73846
|
+
return true;
|
|
73631
73847
|
}
|
|
73632
|
-
return
|
|
73848
|
+
return /json|xml|javascript|ecmascript|yaml|csv/.test(sub);
|
|
73633
73849
|
}
|
|
73634
|
-
|
|
73635
|
-
|
|
73636
|
-
|
|
73637
|
-
|
|
73638
|
-
|
|
73639
|
-
|
|
73850
|
+
async function readCapped(chunks) {
|
|
73851
|
+
const collected = [];
|
|
73852
|
+
let total = 0;
|
|
73853
|
+
for await (const chunk of chunks) {
|
|
73854
|
+
total += chunk.byteLength;
|
|
73855
|
+
if (total > RC_WEB_FETCH_MAX_BYTES) {
|
|
73856
|
+
return { tooLarge: true, text: "" };
|
|
73857
|
+
}
|
|
73858
|
+
collected.push(Buffer.from(chunk));
|
|
73859
|
+
}
|
|
73860
|
+
return { tooLarge: false, text: Buffer.concat(collected).toString("utf8") };
|
|
73640
73861
|
}
|
|
73641
|
-
|
|
73642
|
-
|
|
73643
|
-
|
|
73644
|
-
function renderEvent(event, gap) {
|
|
73645
|
-
if (ui === null) { return; }
|
|
73646
|
-
var list = ui.eventList;
|
|
73647
|
-
if (gap > 0) {
|
|
73648
|
-
list.appendChild(el('div', 'event gap', 'dropped events: ' + String(gap) + ' on source ' + event.source + ' (the stream holds no replay)'));
|
|
73862
|
+
async function checkTarget(url2, deps) {
|
|
73863
|
+
if (url2.protocol !== "http:" && url2.protocol !== "https:") {
|
|
73864
|
+
return refused("scheme", `the web-fetch proxy dials http and https only, not ${url2.protocol}`);
|
|
73649
73865
|
}
|
|
73650
|
-
|
|
73651
|
-
|
|
73652
|
-
line = rcLine(event.payload);
|
|
73653
|
-
} else {
|
|
73654
|
-
line = new Date().toLocaleTimeString() + ' [' + event.source + '] ' + sketch(event.payload, 240);
|
|
73866
|
+
if (deps.allowPrivate) {
|
|
73867
|
+
return void 0;
|
|
73655
73868
|
}
|
|
73656
|
-
|
|
73657
|
-
|
|
73658
|
-
|
|
73659
|
-
|
|
73660
|
-
|
|
73661
|
-
|
|
73662
|
-
if (ui === null) { return; }
|
|
73663
|
-
var list = ui.sessionList;
|
|
73664
|
-
list.textContent = '';
|
|
73665
|
-
if (statuses.length === 0) {
|
|
73666
|
-
list.appendChild(el('div', 'event dim', 'no Remote Control sessions observed'));
|
|
73667
|
-
return;
|
|
73869
|
+
const hostname3 = url2.hostname.replace(/^\[|\]$/g, "");
|
|
73870
|
+
let addresses;
|
|
73871
|
+
try {
|
|
73872
|
+
addresses = net3.isIP(hostname3) === 0 ? await deps.lookup(hostname3) : [hostname3];
|
|
73873
|
+
} catch {
|
|
73874
|
+
return refused("resolve", `${hostname3} did not resolve`);
|
|
73668
73875
|
}
|
|
73669
|
-
for (
|
|
73670
|
-
|
|
73671
|
-
|
|
73672
|
-
|
|
73673
|
-
row.appendChild(el('div', null, shortId(status.id)));
|
|
73674
|
-
row.appendChild(el('div', 'dim', worker + (status.pending.length === 0 ? '' : ', ' + String(status.pending.length) + ' pending') + ', seen ' + new Date(status.lastSeenAt).toLocaleTimeString()));
|
|
73675
|
-
row.addEventListener('click', selectSession(status.id));
|
|
73676
|
-
list.appendChild(row);
|
|
73876
|
+
for (const address of addresses) {
|
|
73877
|
+
if (isPrivateAddress(address)) {
|
|
73878
|
+
return refused("private_address", `${hostname3} resolves to ${address}, a private address this web-fetch proxy refuses; set ${RC_WEB_FETCH_ALLOW_PRIVATE_ENV}=1 on the door if serving internal targets is intended`);
|
|
73879
|
+
}
|
|
73677
73880
|
}
|
|
73881
|
+
return void 0;
|
|
73678
73882
|
}
|
|
73679
|
-
|
|
73680
|
-
|
|
73681
|
-
|
|
73682
|
-
|
|
73683
|
-
|
|
73684
|
-
|
|
73685
|
-
refreshSessions();
|
|
73686
|
-
refreshPending();
|
|
73687
|
-
};
|
|
73688
|
-
}
|
|
73689
|
-
|
|
73690
|
-
function renderPending(pending) {
|
|
73691
|
-
if (ui === null) { return; }
|
|
73692
|
-
var list = ui.pendingList;
|
|
73693
|
-
list.textContent = '';
|
|
73694
|
-
if (pending.length === 0) {
|
|
73695
|
-
list.appendChild(el('div', 'event dim', 'no control requests awaiting an answer'));
|
|
73696
|
-
return;
|
|
73883
|
+
async function fetchThroughWebProxy(url2, deps) {
|
|
73884
|
+
let current;
|
|
73885
|
+
try {
|
|
73886
|
+
current = new URL(url2);
|
|
73887
|
+
} catch {
|
|
73888
|
+
return refused("malformed_url", `${url2} is not a URL this proxy can dial`);
|
|
73697
73889
|
}
|
|
73698
|
-
|
|
73699
|
-
|
|
73700
|
-
|
|
73701
|
-
|
|
73702
|
-
|
|
73703
|
-
|
|
73704
|
-
|
|
73705
|
-
|
|
73706
|
-
|
|
73707
|
-
|
|
73708
|
-
|
|
73709
|
-
|
|
73710
|
-
|
|
73711
|
-
|
|
73712
|
-
|
|
73713
|
-
|
|
73714
|
-
|
|
73715
|
-
|
|
73890
|
+
const controller = new AbortController();
|
|
73891
|
+
const timer = setTimeout(() => {
|
|
73892
|
+
controller.abort();
|
|
73893
|
+
}, deps.deadlineMs);
|
|
73894
|
+
timer.unref();
|
|
73895
|
+
try {
|
|
73896
|
+
let redirects = 0;
|
|
73897
|
+
for (; ; ) {
|
|
73898
|
+
const refusal = await checkTarget(current, deps);
|
|
73899
|
+
if (refusal !== void 0) {
|
|
73900
|
+
return refusal;
|
|
73901
|
+
}
|
|
73902
|
+
let hop;
|
|
73903
|
+
try {
|
|
73904
|
+
hop = await deps.dial(current, controller.signal);
|
|
73905
|
+
} catch (error62) {
|
|
73906
|
+
if (controller.signal.aborted) {
|
|
73907
|
+
return refused("deadline", `the fetch of ${url2} did not settle within its deadline`);
|
|
73908
|
+
}
|
|
73909
|
+
return refused("transport", `the dial of ${current.toString()} failed: ${error62 instanceof Error ? error62.message : "unknown error"}`);
|
|
73910
|
+
}
|
|
73911
|
+
if (isRedirect(hop.status) && hop.location !== void 0) {
|
|
73912
|
+
if (redirects >= RC_WEB_FETCH_MAX_REDIRECTS) {
|
|
73913
|
+
controller.abort();
|
|
73914
|
+
return refused("too_many_redirects", `${url2} followed more than ${String(RC_WEB_FETCH_MAX_REDIRECTS)} redirects without settling`);
|
|
73915
|
+
}
|
|
73916
|
+
let next;
|
|
73917
|
+
try {
|
|
73918
|
+
next = new URL(hop.location, current);
|
|
73919
|
+
} catch {
|
|
73920
|
+
controller.abort();
|
|
73921
|
+
return refused("malformed_url", `a redirect of ${current.toString()} named ${hop.location}, which is not a URL this proxy can dial`);
|
|
73922
|
+
}
|
|
73923
|
+
const drained = await readCapped(hop.chunks);
|
|
73924
|
+
if (drained.tooLarge) {
|
|
73925
|
+
controller.abort();
|
|
73926
|
+
return refused("too_large", `the body of a redirect from ${url2} exceeds the ${String(RC_WEB_FETCH_MAX_BYTES)} byte cap the CLI's proxy client reads`);
|
|
73927
|
+
}
|
|
73928
|
+
redirects += 1;
|
|
73929
|
+
current = next;
|
|
73930
|
+
continue;
|
|
73931
|
+
}
|
|
73932
|
+
if (!isSuccessful3(hop.status)) {
|
|
73933
|
+
controller.abort();
|
|
73934
|
+
return refused("status", `the target answered HTTP ${String(hop.status)} for ${current.toString()}`);
|
|
73935
|
+
}
|
|
73936
|
+
const contentType = hop.contentType ?? "";
|
|
73937
|
+
if (!readableContentType(contentType)) {
|
|
73938
|
+
controller.abort();
|
|
73939
|
+
return refused("content_type", `the target at ${current.toString()} served a ${contentType} answer, which the proxy's text field cannot carry`);
|
|
73940
|
+
}
|
|
73941
|
+
const read = await readCapped(hop.chunks);
|
|
73942
|
+
if (read.tooLarge) {
|
|
73943
|
+
controller.abort();
|
|
73944
|
+
return refused("too_large", `the body of ${current.toString()} exceeds the ${String(RC_WEB_FETCH_MAX_BYTES)} byte cap the CLI's proxy client reads`);
|
|
73945
|
+
}
|
|
73946
|
+
return { kind: "fetched", url: url2, destinationUrl: current.toString(), contentType: hop.contentType, text: read.text };
|
|
73947
|
+
}
|
|
73948
|
+
} finally {
|
|
73949
|
+
clearTimeout(timer);
|
|
73716
73950
|
}
|
|
73717
73951
|
}
|
|
73718
|
-
|
|
73719
|
-
|
|
73720
|
-
|
|
73721
|
-
|
|
73722
|
-
|
|
73723
|
-
|
|
73724
|
-
|
|
73725
|
-
|
|
73726
|
-
|
|
73952
|
+
function realRcWebFetch(options) {
|
|
73953
|
+
const plainAgent = new ExemptHttpAgent();
|
|
73954
|
+
const tlsAgent = new ExemptTlsAgent();
|
|
73955
|
+
const deps = {
|
|
73956
|
+
dial: async (url2, signal) => await new Promise((resolve, reject) => {
|
|
73957
|
+
const request7 = (url2.protocol === "https:" ? https5 : http6).request(
|
|
73958
|
+
{
|
|
73959
|
+
hostname: url2.hostname,
|
|
73960
|
+
port: url2.port === "" ? void 0 : Number(url2.port),
|
|
73961
|
+
method: "GET",
|
|
73962
|
+
path: `${url2.pathname}${url2.search}`,
|
|
73963
|
+
headers: { accept: "*/*", "accept-encoding": "identity", "user-agent": WEB_FETCH_USER_AGENT },
|
|
73964
|
+
agent: url2.protocol === "https:" ? tlsAgent : plainAgent,
|
|
73965
|
+
signal
|
|
73966
|
+
},
|
|
73967
|
+
(response) => {
|
|
73968
|
+
const location = response.headers.location;
|
|
73969
|
+
resolve({
|
|
73970
|
+
status: response.statusCode ?? 0,
|
|
73971
|
+
location: typeof location === "string" ? location : void 0,
|
|
73972
|
+
contentType: typeof response.headers["content-type"] === "string" ? response.headers["content-type"] : void 0,
|
|
73973
|
+
chunks: response
|
|
73974
|
+
});
|
|
73975
|
+
}
|
|
73976
|
+
);
|
|
73977
|
+
request7.once("error", reject);
|
|
73978
|
+
request7.end();
|
|
73979
|
+
}),
|
|
73980
|
+
lookup: async (hostname3) => (await dns2.promises.lookup(hostname3, { all: true })).map((entry) => entry.address),
|
|
73981
|
+
allowPrivate: options.allowPrivate,
|
|
73982
|
+
deadlineMs: RC_WEB_FETCH_DEADLINE_MS
|
|
73727
73983
|
};
|
|
73984
|
+
return async (url2) => await fetchThroughWebProxy(url2, deps);
|
|
73728
73985
|
}
|
|
73729
73986
|
|
|
73730
|
-
|
|
73731
|
-
|
|
73732
|
-
|
|
73733
|
-
|
|
73734
|
-
ui.send.disabled = true;
|
|
73735
|
-
rpc('rc/send', { session: ui.selected, text: text }).then(function (result) {
|
|
73736
|
-
ui.prompt.value = '';
|
|
73737
|
-
renderEvent({ source: 'page', sequence: 0, payload: { note: 'sent, sequence_num ' + result.sequenceNums.join(', ') } }, 0);
|
|
73738
|
-
}, function (error) { window.alert('send failed: ' + error.message); }).then(function () { ui.send.disabled = false; });
|
|
73739
|
-
}
|
|
73740
|
-
|
|
73741
|
-
function refreshSessions() {
|
|
73742
|
-
rpc('rc/status', {}).then(function (result) { renderSessions(result.statuses); }, authOrReport('rc/status'));
|
|
73987
|
+
// src/frontdoor/rcSelfHost.ts
|
|
73988
|
+
var RC_SELF_HOST_ENV = "AGENT_SHIM_FRONTDOOR_RC_SELF_HOST";
|
|
73989
|
+
function rcSelfHostFromEnv(env) {
|
|
73990
|
+
return env[RC_SELF_HOST_ENV] === "1";
|
|
73743
73991
|
}
|
|
73744
|
-
|
|
73745
|
-
|
|
73746
|
-
|
|
73992
|
+
var RC_SELF_HOST_SCOPE_LIST = ["user:profile", "user:inference", "user:sessions:claude_code", "user:mcp_servers", "user:file_upload"];
|
|
73993
|
+
var RC_SESSIONS_PATH_PREFIX2 = "/v1/code/sessions";
|
|
73994
|
+
var RC_COMPAT_SESSIONS_PATH = "/v1/sessions";
|
|
73995
|
+
var RC_SESSION_ID_PREFIX2 = "cse_";
|
|
73996
|
+
var RC_SELF_HOST_API_BASE_URL = "https://api.anthropic.com";
|
|
73997
|
+
var RC_SELF_HOST_OAUTH_HOST = "platform.claude.com";
|
|
73998
|
+
var HTTP_CONFLICT = 409;
|
|
73999
|
+
var MS_PER_SECOND7 = 1e3;
|
|
74000
|
+
var HTTP_SERVICE_UNAVAILABLE = 503;
|
|
74001
|
+
var RC_SELF_HOST_WORKER_JWT_TTL_SECONDS = 46800;
|
|
74002
|
+
var RC_SELF_HOST_OAUTH_TOKEN_TTL_SECONDS = 3600;
|
|
74003
|
+
var RC_SELF_HOST_KEEPALIVE_MS = 15e3;
|
|
74004
|
+
var RC_SELF_HOST_RETENTION_MS = 6e5;
|
|
74005
|
+
var RC_PRESENCE_REFRESH_SECONDS = 60;
|
|
74006
|
+
var RC_SELF_HOST_MAX_BATCH_EVENTS = 100;
|
|
74007
|
+
var RC_SELF_HOST_WORKER_EPOCH = 1;
|
|
74008
|
+
var RC_STREAM_EVENT_NAME = "client_event";
|
|
74009
|
+
var RC_SOURCE_WORKER = "worker";
|
|
74010
|
+
var RC_SOURCE_CLIENT = "client";
|
|
74011
|
+
var CONFLICT_REASON_HEADER2 = "x-ccr-conflict-reason";
|
|
74012
|
+
var CONFLICT_REASON_EPOCH_STALE = "epoch_stale";
|
|
74013
|
+
function isRecord4(value2) {
|
|
74014
|
+
return typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
|
|
73747
74015
|
}
|
|
73748
|
-
|
|
73749
|
-
|
|
73750
|
-
function authOrReport(what) {
|
|
73751
|
-
return function (error) {
|
|
73752
|
-
if (error.status === 401) {
|
|
73753
|
-
handleUnauthorized();
|
|
73754
|
-
return;
|
|
73755
|
-
}
|
|
73756
|
-
renderEvent({ source: 'page', sequence: 0, payload: { note: what + ' failed: ' + error.message } }, 0);
|
|
73757
|
-
};
|
|
74016
|
+
function isWebSearchRefusal(value2) {
|
|
74017
|
+
return !Array.isArray(value2);
|
|
73758
74018
|
}
|
|
73759
|
-
|
|
73760
|
-
|
|
73761
|
-
|
|
73762
|
-
if (feedAbort !== null) { feedAbort.abort(); }
|
|
73763
|
-
streamState('');
|
|
73764
|
-
showTokenForm('this door refused the token: it names a different door generation, paste the current one');
|
|
74019
|
+
function nonEmptyString2(record2, field) {
|
|
74020
|
+
const value2 = record2 === void 0 ? void 0 : record2[field];
|
|
74021
|
+
return typeof value2 === "string" && value2 !== "" ? value2 : void 0;
|
|
73765
74022
|
}
|
|
73766
|
-
|
|
73767
|
-
|
|
73768
|
-
|
|
73769
|
-
|
|
73770
|
-
|
|
73771
|
-
|
|
73772
|
-
|
|
73773
|
-
var tracker = {};
|
|
73774
|
-
for (;;) {
|
|
73775
|
-
if (feedAbort !== null) { feedAbort.abort(); }
|
|
73776
|
-
feedAbort = new AbortController();
|
|
73777
|
-
var filter = document.getElementById('source-filter').value;
|
|
73778
|
-
var input = filter === '' ? {} : { sources: [filter] };
|
|
73779
|
-
try {
|
|
73780
|
-
streamState('stream: connecting');
|
|
73781
|
-
var response = await fetch(apiBase() + '/events/subscribe', {
|
|
73782
|
-
method: 'POST',
|
|
73783
|
-
headers: { 'content-type': 'application/json', authorization: 'Bearer ' + token },
|
|
73784
|
-
body: JSON.stringify({ json: input }),
|
|
73785
|
-
signal: feedAbort.signal,
|
|
73786
|
-
});
|
|
73787
|
-
if (response.status === 401) { handleUnauthorized(); return; }
|
|
73788
|
-
if (!response.ok || response.body === null) { throw new Error('HTTP ' + String(response.status)); }
|
|
73789
|
-
streamState('stream: live');
|
|
73790
|
-
var reader = response.body.getReader();
|
|
73791
|
-
var decoder = new TextDecoder();
|
|
73792
|
-
var buffer = '';
|
|
73793
|
-
for (;;) {
|
|
73794
|
-
var chunk = await reader.read();
|
|
73795
|
-
if (chunk.done) { break; }
|
|
73796
|
-
buffer += decoder.decode(chunk.value, { stream: true });
|
|
73797
|
-
var parsed = parseSseFrames(buffer);
|
|
73798
|
-
buffer = parsed.rest;
|
|
73799
|
-
for (var i = 0; i < parsed.frames.length; i++) {
|
|
73800
|
-
var frame = parsed.frames[i];
|
|
73801
|
-
if (frame.event !== 'message') { continue; }
|
|
73802
|
-
var event;
|
|
73803
|
-
try { event = JSON.parse(frame.data).json; } catch (e) { continue; }
|
|
73804
|
-
if (event === undefined || event.source === undefined) { continue; }
|
|
73805
|
-
renderEvent(event, sequenceGap(tracker, event));
|
|
73806
|
-
}
|
|
73807
|
-
}
|
|
73808
|
-
} catch (error) {
|
|
73809
|
-
if (feedAbort !== null && feedAbort.signal.aborted) { return; }
|
|
73810
|
-
}
|
|
73811
|
-
streamState('stream: dropped, reconnecting');
|
|
73812
|
-
await new Promise(function (resolve) { window.setTimeout(resolve, 1000); });
|
|
74023
|
+
function numberOf(record2, field) {
|
|
74024
|
+
const value2 = record2 === void 0 ? void 0 : record2[field];
|
|
74025
|
+
if (typeof value2 === "number" && Number.isFinite(value2)) {
|
|
74026
|
+
return value2;
|
|
74027
|
+
}
|
|
74028
|
+
if (typeof value2 === "string" && /^[0-9]+$/.test(value2)) {
|
|
74029
|
+
return Number(value2);
|
|
73813
74030
|
}
|
|
74031
|
+
return void 0;
|
|
73814
74032
|
}
|
|
73815
|
-
|
|
73816
|
-
|
|
73817
|
-
if (
|
|
73818
|
-
|
|
73819
|
-
var form = document.getElementById('token-form');
|
|
73820
|
-
form.classList.remove('hidden');
|
|
73821
|
-
document.getElementById('token-error').textContent = message === undefined ? '' : message;
|
|
73822
|
-
document.getElementById('token-input').focus();
|
|
73823
|
-
}
|
|
73824
|
-
|
|
73825
|
-
function startPage(heldToken) {
|
|
73826
|
-
token = heldToken;
|
|
73827
|
-
ui = {
|
|
73828
|
-
main: document.getElementById('main'),
|
|
73829
|
-
eventList: document.getElementById('event-list'),
|
|
73830
|
-
sessionList: document.getElementById('session-list'),
|
|
73831
|
-
pendingList: document.getElementById('pending-list'),
|
|
73832
|
-
prompt: document.getElementById('prompt'),
|
|
73833
|
-
promptTarget: document.getElementById('prompt-target'),
|
|
73834
|
-
send: document.getElementById('send'),
|
|
73835
|
-
streamState: document.getElementById('stream-state'),
|
|
73836
|
-
selected: null,
|
|
73837
|
-
};
|
|
73838
|
-
document.getElementById('token-form').classList.add('hidden');
|
|
73839
|
-
ui.main.classList.remove('hidden');
|
|
73840
|
-
document.getElementById('refresh').addEventListener('click', function () { refreshSessions(); refreshPending(); });
|
|
73841
|
-
document.getElementById('send').addEventListener('click', sendPrompt);
|
|
73842
|
-
document.getElementById('source-filter').addEventListener('change', function () { runFeed(); });
|
|
73843
|
-
ui.prompt.addEventListener('keydown', function (event) {
|
|
73844
|
-
if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) { sendPrompt(); }
|
|
73845
|
-
});
|
|
73846
|
-
refreshSessions();
|
|
73847
|
-
refreshPending();
|
|
73848
|
-
window.setInterval(function () { refreshSessions(); refreshPending(); }, 4000);
|
|
73849
|
-
runFeed();
|
|
73850
|
-
document.getElementById('door-state').textContent = 'connected';
|
|
73851
|
-
}
|
|
73852
|
-
|
|
73853
|
-
function boot() {
|
|
73854
|
-
var held = null;
|
|
73855
|
-
var query = new URLSearchParams(window.location.search);
|
|
73856
|
-
var fromQuery = query.get('token');
|
|
73857
|
-
if (fromQuery !== null && fromQuery !== '') {
|
|
73858
|
-
window.sessionStorage.setItem(TOKEN_KEY, fromQuery);
|
|
73859
|
-
// 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.
|
|
73860
|
-
window.history.replaceState(null, '', window.location.pathname);
|
|
73861
|
-
held = fromQuery;
|
|
73862
|
-
} else {
|
|
73863
|
-
held = window.sessionStorage.getItem(TOKEN_KEY);
|
|
74033
|
+
function stringArray(record2, field) {
|
|
74034
|
+
const value2 = record2 === void 0 ? void 0 : record2[field];
|
|
74035
|
+
if (!Array.isArray(value2)) {
|
|
74036
|
+
return void 0;
|
|
73864
74037
|
}
|
|
73865
|
-
|
|
73866
|
-
|
|
73867
|
-
if (
|
|
73868
|
-
|
|
73869
|
-
|
|
73870
|
-
|
|
73871
|
-
|
|
73872
|
-
|
|
73873
|
-
});
|
|
73874
|
-
if (held !== null && held !== '') { startPage(held); }
|
|
73875
|
-
}
|
|
73876
|
-
|
|
73877
|
-
if (typeof document !== 'undefined' && document.getElementById('event-list') !== null) {
|
|
73878
|
-
window.addEventListener('DOMContentLoaded', boot);
|
|
74038
|
+
const members2 = [];
|
|
74039
|
+
for (const member of value2) {
|
|
74040
|
+
if (typeof member !== "string") {
|
|
74041
|
+
return void 0;
|
|
74042
|
+
}
|
|
74043
|
+
members2.push(member);
|
|
74044
|
+
}
|
|
74045
|
+
return members2;
|
|
73879
74046
|
}
|
|
73880
|
-
|
|
73881
|
-
|
|
73882
|
-
|
|
73883
|
-
<head>
|
|
73884
|
-
<meta charset="utf-8">
|
|
73885
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
73886
|
-
<title>agent-shim Remote Control</title>
|
|
73887
|
-
<style>
|
|
73888
|
-
:root { color-scheme: dark; }
|
|
73889
|
-
* { box-sizing: border-box; }
|
|
73890
|
-
body { margin: 0; background: #101418; color: #d7dde3; font: 14px/1.45 ui-monospace, Menlo, Consolas, monospace; }
|
|
73891
|
-
header { display: flex; gap: 12px; align-items: baseline; padding: 10px 16px; background: #171d24; border-bottom: 1px solid #2a333d; }
|
|
73892
|
-
header h1 { font-size: 15px; margin: 0; font-weight: 600; }
|
|
73893
|
-
header .state { color: #8b98a5; }
|
|
73894
|
-
main { display: flex; gap: 1px; background: #2a333d; min-height: calc(100vh - 39px); }
|
|
73895
|
-
section { background: #101418; padding: 12px 16px; overflow-y: auto; }
|
|
73896
|
-
#sessions { flex: 0 0 320px; border-right: 1px solid #2a333d; }
|
|
73897
|
-
#right { flex: 1; display: flex; flex-direction: column; min-width: 0; }
|
|
73898
|
-
#feed { flex: 1; }
|
|
73899
|
-
#controls { border-top: 1px solid #2a333d; }
|
|
73900
|
-
h2 { font-size: 12px; text-transform: uppercase; letter-spacing: 0.08em; color: #8b98a5; margin: 8px 0 8px; font-weight: 600; }
|
|
73901
|
-
.session { padding: 6px 8px; border: 1px solid transparent; border-radius: 4px; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
73902
|
-
.session:hover { background: #171d24; }
|
|
73903
|
-
.session.selected { border-color: #3d6ea5; background: #17222e; }
|
|
73904
|
-
.session .dim { color: #8b98a5; }
|
|
73905
|
-
.event { padding: 2px 0; white-space: pre-wrap; word-break: break-word; }
|
|
73906
|
-
.event .dim { color: #8b98a5; }
|
|
73907
|
-
.event.gap { color: #d29922; }
|
|
73908
|
-
.event.error { color: #e5534b; }
|
|
73909
|
-
textarea, input[type=text], select { background: #171d24; color: #d7dde3; border: 1px solid #2a333d; border-radius: 4px; padding: 6px 8px; font: inherit; width: 100%; }
|
|
73910
|
-
textarea { resize: vertical; min-height: 44px; }
|
|
73911
|
-
button { background: #1f4a7a; color: #d7dde3; border: 1px solid #3d6ea5; border-radius: 4px; padding: 5px 12px; font: inherit; cursor: pointer; }
|
|
73912
|
-
button.secondary { background: #2a333d; border-color: #3a444f; }
|
|
73913
|
-
button:disabled { opacity: 0.5; cursor: default; }
|
|
73914
|
-
.row { display: flex; gap: 8px; margin: 6px 0; align-items: center; }
|
|
73915
|
-
.card { border: 1px solid #3d6ea5; border-radius: 4px; padding: 8px; margin: 6px 0; background: #17222e; }
|
|
73916
|
-
.card .dim { color: #8b98a5; }
|
|
73917
|
-
#token-form { max-width: 480px; margin: 48px auto; }
|
|
73918
|
-
#token-form p { color: #8b98a5; }
|
|
73919
|
-
.hidden { display: none !important; }
|
|
73920
|
-
</style>
|
|
73921
|
-
</head>
|
|
73922
|
-
<body data-agent-shim-rc-client>
|
|
73923
|
-
<header>
|
|
73924
|
-
<h1>agent-shim Remote Control</h1>
|
|
73925
|
-
<span class="state" id="door-state">connecting</span>
|
|
73926
|
-
<span class="state" id="stream-state"></span>
|
|
73927
|
-
</header>
|
|
73928
|
-
<div id="token-form">
|
|
73929
|
-
<h2>This door's control token</h2>
|
|
73930
|
-
<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>
|
|
73931
|
-
<div class="row"><input type="text" id="token-input" placeholder="control token" autocomplete="off"><button id="token-save">Use</button></div>
|
|
73932
|
-
<p id="token-error" class="event error"></p>
|
|
73933
|
-
</div>
|
|
73934
|
-
<main class="hidden" id="main">
|
|
73935
|
-
<section id="sessions">
|
|
73936
|
-
<h2>Sessions <button class="secondary" id="refresh">Refresh</button></h2>
|
|
73937
|
-
<div id="session-list"></div>
|
|
73938
|
-
</section>
|
|
73939
|
-
<section id="right">
|
|
73940
|
-
<div id="feed">
|
|
73941
|
-
<h2>Feed <select id="source-filter"><option value="">all sources</option><option value="rc">Remote Control only</option></select></h2>
|
|
73942
|
-
<div id="event-list"></div>
|
|
73943
|
-
</div>
|
|
73944
|
-
<div id="controls">
|
|
73945
|
-
<h2>Pending approvals</h2>
|
|
73946
|
-
<div id="pending-list"></div>
|
|
73947
|
-
<h2>Send a prompt</h2>
|
|
73948
|
-
<div class="row"><textarea id="prompt" placeholder="Prompt for the selected session"></textarea></div>
|
|
73949
|
-
<div class="row"><span class="dim" id="prompt-target">no session selected</span><span style="flex:1"></span><button id="send">Send</button></div>
|
|
73950
|
-
</div>
|
|
73951
|
-
</section>
|
|
73952
|
-
</main>
|
|
73953
|
-
<script>
|
|
73954
|
-
${RC_CLIENT_PAGE_SCRIPT}
|
|
73955
|
-
</script>
|
|
73956
|
-
</body>
|
|
73957
|
-
</html>
|
|
73958
|
-
`;
|
|
73959
|
-
function pageTokenOf(request7, url2) {
|
|
73960
|
-
const presented = request7.headers.authorization;
|
|
73961
|
-
const bearerPrefix = "bearer ".length;
|
|
73962
|
-
const header = typeof presented === "string" && presented.slice(0, bearerPrefix).toLowerCase() === "bearer " ? presented.slice(bearerPrefix) : void 0;
|
|
73963
|
-
const query = url2.searchParams.get("token") ?? void 0;
|
|
73964
|
-
return header ?? query;
|
|
74047
|
+
function singleHeader2(headers, name) {
|
|
74048
|
+
const value2 = headers[name];
|
|
74049
|
+
return typeof value2 === "string" ? value2 : void 0;
|
|
73965
74050
|
}
|
|
73966
|
-
function
|
|
73967
|
-
const
|
|
73968
|
-
if (
|
|
73969
|
-
|
|
73970
|
-
response.end("no such page under the door's client prefix");
|
|
73971
|
-
return;
|
|
74051
|
+
function bearerOf(headers) {
|
|
74052
|
+
const presented = singleHeader2(headers, "authorization");
|
|
74053
|
+
if (presented === void 0) {
|
|
74054
|
+
return void 0;
|
|
73972
74055
|
}
|
|
73973
|
-
|
|
73974
|
-
|
|
73975
|
-
|
|
73976
|
-
|
|
74056
|
+
return presented.slice(0, "bearer ".length).toLowerCase() === "bearer " ? presented.slice("bearer ".length) : void 0;
|
|
74057
|
+
}
|
|
74058
|
+
function presentationRefusal(headers) {
|
|
74059
|
+
const presented = singleHeader2(headers, "authorization");
|
|
74060
|
+
if (presented === void 0) {
|
|
74061
|
+
return "absent";
|
|
73977
74062
|
}
|
|
73978
|
-
|
|
73979
|
-
|
|
73980
|
-
response.writeHead(HTTP_STATUS.unauthorized, { "Content-Type": "application/json" });
|
|
73981
|
-
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" }));
|
|
73982
|
-
return;
|
|
74063
|
+
if (presented.slice(0, "bearer ".length).toLowerCase() !== "bearer ") {
|
|
74064
|
+
return "not-a-bearer";
|
|
73983
74065
|
}
|
|
73984
|
-
|
|
73985
|
-
|
|
73986
|
-
|
|
73987
|
-
|
|
73988
|
-
|
|
74066
|
+
const bearer = presented.slice("bearer ".length);
|
|
74067
|
+
return bearer.slice(0, "bearer ".length).toLowerCase() === "bearer " ? "doubled-scheme-word" : "unrecognised";
|
|
74068
|
+
}
|
|
74069
|
+
async function readBody3(request7, capBytes) {
|
|
74070
|
+
return await new Promise((resolve, reject) => {
|
|
74071
|
+
const chunks = [];
|
|
74072
|
+
let total = 0;
|
|
74073
|
+
request7.on("data", (chunk) => {
|
|
74074
|
+
total += chunk.length;
|
|
74075
|
+
if (total > capBytes) {
|
|
74076
|
+
request7.destroy();
|
|
74077
|
+
reject(new Error("body too large"));
|
|
74078
|
+
return;
|
|
74079
|
+
}
|
|
74080
|
+
chunks.push(chunk);
|
|
74081
|
+
});
|
|
74082
|
+
request7.on("end", () => {
|
|
74083
|
+
resolve(Buffer.concat(chunks).toString("utf8"));
|
|
74084
|
+
});
|
|
74085
|
+
request7.on("error", () => {
|
|
74086
|
+
reject(new Error("the request body ended early"));
|
|
74087
|
+
});
|
|
73989
74088
|
});
|
|
73990
|
-
response.end(request7.method === "HEAD" ? void 0 : RC_CLIENT_PAGE_HTML);
|
|
73991
74089
|
}
|
|
73992
|
-
function
|
|
74090
|
+
function parseJsonObject2(body) {
|
|
74091
|
+
let parsed;
|
|
74092
|
+
try {
|
|
74093
|
+
parsed = JSON.parse(body);
|
|
74094
|
+
} catch {
|
|
74095
|
+
return void 0;
|
|
74096
|
+
}
|
|
74097
|
+
return isRecord4(parsed) ? parsed : void 0;
|
|
74098
|
+
}
|
|
74099
|
+
function mintWorkerJwt(randomToken, issuedAt) {
|
|
74100
|
+
const encode3 = (value2) => Buffer.from(JSON.stringify(value2), "utf8").toString("base64url");
|
|
74101
|
+
const header = encode3({ alg: "HS256", typ: "JWT" });
|
|
74102
|
+
const payload = encode3({ exp: Math.floor((issuedAt + RC_SELF_HOST_WORKER_JWT_TTL_SECONDS * MS_PER_SECOND7) / MS_PER_SECOND7), iat: Math.floor(issuedAt / MS_PER_SECOND7) });
|
|
74103
|
+
return `${header}.${payload}.${Buffer.from(randomToken(), "utf8").toString("base64url")}`;
|
|
74104
|
+
}
|
|
74105
|
+
function growthBookFeatures() {
|
|
73993
74106
|
return {
|
|
73994
|
-
|
|
73995
|
-
|
|
73996
|
-
|
|
73997
|
-
|
|
73998
|
-
return { matched: true };
|
|
73999
|
-
}
|
|
74107
|
+
tengu_ccr_bridge: { defaultValue: true },
|
|
74108
|
+
tengu_bridge_repl_v2: { defaultValue: true },
|
|
74109
|
+
tengu_bridge_repl_v2_cse_shim_enabled: { defaultValue: true },
|
|
74110
|
+
tengu_bridge_min_version: { defaultValue: { minVersion: "0.0.0" } }
|
|
74000
74111
|
};
|
|
74001
74112
|
}
|
|
74002
|
-
|
|
74003
|
-
|
|
74004
|
-
|
|
74005
|
-
|
|
74006
|
-
|
|
74007
|
-
|
|
74008
|
-
|
|
74009
|
-
|
|
74010
|
-
|
|
74011
|
-
|
|
74012
|
-
|
|
74013
|
-
|
|
74014
|
-
|
|
74015
|
-
|
|
74016
|
-
|
|
74017
|
-
var V4_THIS_NETWORK_OCTET = 0;
|
|
74018
|
-
var V4_PRIVATE_A_OCTET = 10;
|
|
74019
|
-
var V4_LOOPBACK_OCTET = 127;
|
|
74020
|
-
var V4_CGNAT_OCTET = 100;
|
|
74021
|
-
var V4_CGNAT_SECOND_FROM = 64;
|
|
74022
|
-
var V4_CGNAT_SECOND_TO = 127;
|
|
74023
|
-
var V4_LINK_LOCAL_OCTET = 169;
|
|
74024
|
-
var V4_LINK_LOCAL_SECOND = 254;
|
|
74025
|
-
var V4_PRIVATE_B_OCTET = 172;
|
|
74026
|
-
var V4_PRIVATE_B_SECOND_FROM = 16;
|
|
74027
|
-
var V4_PRIVATE_B_SECOND_TO = 31;
|
|
74028
|
-
var V4_PRIVATE_C_OCTET = 192;
|
|
74029
|
-
var V4_PRIVATE_C_SECOND = 168;
|
|
74030
|
-
var V4_MULTICAST_FROM_OCTET = 224;
|
|
74031
|
-
var V6_UNIQUE_LOCAL_FROM = 64512;
|
|
74032
|
-
var V6_UNIQUE_LOCAL_TO = 65023;
|
|
74033
|
-
var V6_LINK_LOCAL_FROM = 65152;
|
|
74034
|
-
var V6_LINK_LOCAL_TO = 65215;
|
|
74035
|
-
var V6_MULTICAST_FROM = 65280;
|
|
74036
|
-
var IP_VERSION_4 = 4;
|
|
74037
|
-
var IP_VERSION_6 = 6;
|
|
74038
|
-
var HTTP_MOVED_PERMANENTLY = 301;
|
|
74039
|
-
var HTTP_FOUND = 302;
|
|
74040
|
-
var HTTP_SEE_OTHER = 303;
|
|
74041
|
-
var HTTP_TEMPORARY_REDIRECT = 307;
|
|
74042
|
-
var HTTP_PERMANENT_REDIRECT = 308;
|
|
74043
|
-
var SUCCESS_STATUS_MAX_EXCLUSIVE3 = 300;
|
|
74044
|
-
function refused(errorType, errorMessage2) {
|
|
74045
|
-
return { kind: "refused", errorType: `${ERROR_PREFIX}${errorType}`, errorMessage: errorMessage2 };
|
|
74046
|
-
}
|
|
74047
|
-
function isPrivateAddress(address) {
|
|
74048
|
-
const version2 = net3.isIP(address);
|
|
74049
|
-
if (version2 === IP_VERSION_4) {
|
|
74050
|
-
const octets = address.split(".").map((part) => Number(part));
|
|
74051
|
-
const first = octets[0] ?? -1;
|
|
74052
|
-
const second = octets[1] ?? -1;
|
|
74053
|
-
if (first === V4_THIS_NETWORK_OCTET || first === V4_PRIVATE_A_OCTET || first === V4_LOOPBACK_OCTET) {
|
|
74054
|
-
return true;
|
|
74113
|
+
function createRcSelfHostSurface(deps) {
|
|
74114
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
74115
|
+
const sweep = () => {
|
|
74116
|
+
const now = deps.now();
|
|
74117
|
+
for (const session of sessions.values()) {
|
|
74118
|
+
while (session.events[0] !== void 0 && now - session.events[0].storedAt > RC_SELF_HOST_RETENTION_MS) {
|
|
74119
|
+
const dropped = session.events.shift();
|
|
74120
|
+
const uuid3 = dropped === void 0 ? void 0 : nonEmptyString2(dropped.payload, "uuid");
|
|
74121
|
+
if (uuid3 !== void 0) {
|
|
74122
|
+
session.seenPayloadUuids.delete(uuid3);
|
|
74123
|
+
}
|
|
74124
|
+
}
|
|
74125
|
+
if (session.workerStreams.size === 0 && session.clientStreams.size === 0 && now - session.lastTrafficAt > RC_SELF_HOST_RETENTION_MS) {
|
|
74126
|
+
sessions.delete(session.id);
|
|
74127
|
+
}
|
|
74055
74128
|
}
|
|
74056
|
-
|
|
74057
|
-
|
|
74129
|
+
};
|
|
74130
|
+
const timer = setInterval(sweep, RC_SELF_HOST_RETENTION_MS);
|
|
74131
|
+
timer.unref();
|
|
74132
|
+
const keepalive = setInterval(() => {
|
|
74133
|
+
for (const session of sessions.values()) {
|
|
74134
|
+
for (const sink of [...session.workerStreams, ...session.clientStreams]) {
|
|
74135
|
+
sink.write(": keep-alive\n\n");
|
|
74136
|
+
}
|
|
74058
74137
|
}
|
|
74059
|
-
|
|
74060
|
-
|
|
74138
|
+
}, RC_SELF_HOST_KEEPALIVE_MS);
|
|
74139
|
+
keepalive.unref();
|
|
74140
|
+
const presentsCredential = (headers) => {
|
|
74141
|
+
const expected = deps.credentialRecord()?.accessToken;
|
|
74142
|
+
const presented = bearerOf(headers);
|
|
74143
|
+
return expected !== void 0 && presented !== void 0 && isLiveCapability(presented, [expected]);
|
|
74144
|
+
};
|
|
74145
|
+
const presentsWorkerJwt = (headers, session) => {
|
|
74146
|
+
const presented = bearerOf(headers);
|
|
74147
|
+
if (presented === void 0) {
|
|
74148
|
+
return false;
|
|
74061
74149
|
}
|
|
74062
|
-
|
|
74063
|
-
|
|
74150
|
+
for (const minted of session.workerJwts) {
|
|
74151
|
+
if (isLiveCapability(presented, [minted])) {
|
|
74152
|
+
return true;
|
|
74153
|
+
}
|
|
74064
74154
|
}
|
|
74065
|
-
|
|
74066
|
-
|
|
74155
|
+
return false;
|
|
74156
|
+
};
|
|
74157
|
+
const credentialRefusal = (headers) => presentsCredential(headers) ? void 0 : presentationRefusal(headers);
|
|
74158
|
+
const workerRefusal = (headers, session) => presentsWorkerJwt(headers, session) ? void 0 : presentationRefusal(headers);
|
|
74159
|
+
const workerOrCredentialRefusal = (headers, session) => presentsWorkerJwt(headers, session) || presentsCredential(headers) ? void 0 : presentationRefusal(headers);
|
|
74160
|
+
const frameOf = (event) => {
|
|
74161
|
+
const envelope = {
|
|
74162
|
+
event_id: event.eventId,
|
|
74163
|
+
event_type: nonEmptyString2(event.payload, "type") ?? "event",
|
|
74164
|
+
sequence_num: event.sequenceNum,
|
|
74165
|
+
source: event.source,
|
|
74166
|
+
payload: event.payload,
|
|
74167
|
+
created_at: event.createdAt
|
|
74168
|
+
};
|
|
74169
|
+
return `event: ${RC_STREAM_EVENT_NAME}
|
|
74170
|
+
id: ${String(event.sequenceNum)}
|
|
74171
|
+
data: ${JSON.stringify(envelope)}
|
|
74172
|
+
|
|
74173
|
+
`;
|
|
74174
|
+
};
|
|
74175
|
+
const holdStream = async (session, which, wantsReplay, request7, response) => {
|
|
74176
|
+
const url2 = new URL(request7.url, "http://127.0.0.1");
|
|
74177
|
+
const cursor3 = numberOf({ value: url2.searchParams.get("from_sequence_num") }, "value") ?? numberOf({ value: singleHeader2(request7.headers, "last-event-id") }, "value");
|
|
74178
|
+
const replay = cursor3 === void 0 ? [] : session.events.filter((event) => wantsReplay(event) && event.sequenceNum > cursor3);
|
|
74179
|
+
response.start(HTTP_STATUS.ok, { "content-type": "text/event-stream", "cache-control": "no-cache" });
|
|
74180
|
+
response.flush();
|
|
74181
|
+
let pending = Promise.resolve();
|
|
74182
|
+
const sink = {
|
|
74183
|
+
write: (frame) => {
|
|
74184
|
+
pending = pending.then(async () => {
|
|
74185
|
+
await response.write(frame);
|
|
74186
|
+
}).catch(() => {
|
|
74187
|
+
});
|
|
74188
|
+
},
|
|
74189
|
+
retire: () => {
|
|
74190
|
+
session[which].delete(sink);
|
|
74191
|
+
}
|
|
74192
|
+
};
|
|
74193
|
+
session[which].add(sink);
|
|
74194
|
+
session.lastTrafficAt = deps.now();
|
|
74195
|
+
for (const event of replay) {
|
|
74196
|
+
sink.write(frameOf(event));
|
|
74067
74197
|
}
|
|
74068
|
-
|
|
74069
|
-
|
|
74070
|
-
|
|
74071
|
-
|
|
74072
|
-
|
|
74073
|
-
|
|
74074
|
-
|
|
74198
|
+
await new Promise((resolve) => {
|
|
74199
|
+
request7.signal.addEventListener("abort", () => {
|
|
74200
|
+
resolve(void 0);
|
|
74201
|
+
}, { once: true });
|
|
74202
|
+
});
|
|
74203
|
+
sink.retire();
|
|
74204
|
+
response.end();
|
|
74205
|
+
};
|
|
74206
|
+
const publish = (session, source, payload) => {
|
|
74207
|
+
const uuid3 = nonEmptyString2(payload, "uuid");
|
|
74208
|
+
const duplicate = uuid3 !== void 0 && session.seenPayloadUuids.has(uuid3);
|
|
74209
|
+
session.sequenceNum += 1;
|
|
74210
|
+
const event = {
|
|
74211
|
+
sequenceNum: session.sequenceNum,
|
|
74212
|
+
eventId: uuid3 ?? deps.newUuid(),
|
|
74213
|
+
source,
|
|
74214
|
+
// The one enrichment the real service was observed making on the live rig's stream: the wall-clock instant it took the write. The worker's handler ignores it; the door's own parser forwards it verbatim.
|
|
74215
|
+
payload: source === RC_SOURCE_CLIENT ? { ...payload, server_received_wall_ms: deps.now() } : payload,
|
|
74216
|
+
createdAt: new Date(deps.now()).toISOString(),
|
|
74217
|
+
toWorker: source === RC_SOURCE_CLIENT,
|
|
74218
|
+
storedAt: deps.now()
|
|
74219
|
+
};
|
|
74220
|
+
session.events.push(event);
|
|
74221
|
+
if (uuid3 !== void 0) {
|
|
74222
|
+
session.seenPayloadUuids.add(uuid3);
|
|
74075
74223
|
}
|
|
74076
|
-
|
|
74077
|
-
|
|
74224
|
+
session.lastTrafficAt = event.storedAt;
|
|
74225
|
+
const frame = frameOf(event);
|
|
74226
|
+
if (event.toWorker) {
|
|
74227
|
+
for (const sink of [...session.workerStreams]) {
|
|
74228
|
+
sink.write(frame);
|
|
74229
|
+
}
|
|
74078
74230
|
}
|
|
74079
|
-
const
|
|
74080
|
-
|
|
74081
|
-
return true;
|
|
74231
|
+
for (const sink of [...session.clientStreams]) {
|
|
74232
|
+
sink.write(frame);
|
|
74082
74233
|
}
|
|
74083
|
-
|
|
74084
|
-
|
|
74234
|
+
return { sequenceNum: event.sequenceNum, eventId: event.eventId, duplicate };
|
|
74235
|
+
};
|
|
74236
|
+
const answerJson2 = async (response, status, body, extraHeaders = {}) => {
|
|
74237
|
+
const text2 = JSON.stringify(body);
|
|
74238
|
+
response.start(status, { "content-type": "application/json", "content-length": String(Buffer.byteLength(text2, "utf8")), ...extraHeaders });
|
|
74239
|
+
await response.write(text2);
|
|
74240
|
+
response.end();
|
|
74241
|
+
};
|
|
74242
|
+
const noCredential = async (response) => {
|
|
74243
|
+
await answerJson2(response, HTTP_SERVICE_UNAVAILABLE, { type: "error", error: { type: "api_error", message: "the front door's self-hosted Remote Control surface is on but no credential has been minted for it; run agent-shim frontdoor rc selfhost mint first" } });
|
|
74244
|
+
};
|
|
74245
|
+
const epochStale = async (response) => {
|
|
74246
|
+
await answerJson2(response, HTTP_CONFLICT, { type: "error", error: { type: "api_error", message: "worker_epoch does not match the session's live epoch" } }, { [CONFLICT_REASON_HEADER2]: CONFLICT_REASON_EPOCH_STALE });
|
|
74247
|
+
};
|
|
74248
|
+
const refusalMessages = {
|
|
74249
|
+
absent: "the self-hosted Remote Control surface authenticates by the Authorization header, and this call carried none",
|
|
74250
|
+
"not-a-bearer": "the self-hosted Remote Control surface reads a Bearer authorization, and this call's Authorization header did not carry the Bearer scheme word",
|
|
74251
|
+
"doubled-scheme-word": "the self-hosted Remote Control surface reads one Bearer scheme word, and this call's Authorization header carried it twice: a persisted session record holds the whole header value with the scheme word included, so prefixing Bearer onto it presents a different credential",
|
|
74252
|
+
unrecognised: "the self-hosted Remote Control surface did not recognise the presented credential"
|
|
74253
|
+
};
|
|
74254
|
+
const unauthorized = async (response, refusal) => {
|
|
74255
|
+
await answerJson2(response, HTTP_STATUS.unauthorized, { type: "error", error: { type: "authentication_error", message: refusalMessages[refusal] } });
|
|
74256
|
+
};
|
|
74257
|
+
const serveSessionPath = async (session, tail, request7, response) => {
|
|
74258
|
+
const method = request7.method.toUpperCase();
|
|
74259
|
+
if (session === void 0) {
|
|
74260
|
+
await answerJson2(response, HTTP_STATUS.notFound, { type: "error", error: { type: "api_error", message: "no such Remote Control session on this door" } });
|
|
74261
|
+
return;
|
|
74085
74262
|
}
|
|
74086
|
-
|
|
74087
|
-
|
|
74263
|
+
session.lastTrafficAt = deps.now();
|
|
74264
|
+
if (tail === "") {
|
|
74265
|
+
if (method !== "GET") {
|
|
74266
|
+
await answerJson2(response, HTTP_STATUS.methodNotAllowed, { type: "error", error: { type: "api_error", message: "the session read is a GET" } });
|
|
74267
|
+
return;
|
|
74268
|
+
}
|
|
74269
|
+
await answerJson2(response, HTTP_STATUS.ok, { session: { id: session.id, title: session.title, created_at: new Date(session.createdAt).toISOString(), status: "active" } });
|
|
74270
|
+
return;
|
|
74088
74271
|
}
|
|
74089
|
-
|
|
74090
|
-
|
|
74091
|
-
|
|
74092
|
-
|
|
74093
|
-
|
|
74094
|
-
|
|
74095
|
-
|
|
74096
|
-
|
|
74097
|
-
|
|
74098
|
-
}
|
|
74099
|
-
|
|
74100
|
-
if (contentType === void 0 || contentType.trim() === "") {
|
|
74101
|
-
return true;
|
|
74102
|
-
}
|
|
74103
|
-
const normalised = contentType.split(";")[0]?.trim().toLowerCase() ?? "";
|
|
74104
|
-
const slash = normalised.indexOf("/");
|
|
74105
|
-
const main2 = slash === -1 ? normalised : normalised.slice(0, slash);
|
|
74106
|
-
const sub = slash === -1 ? "" : normalised.slice(slash + 1);
|
|
74107
|
-
if (main2 === "text") {
|
|
74108
|
-
return true;
|
|
74109
|
-
}
|
|
74110
|
-
return /json|xml|javascript|ecmascript|yaml|csv/.test(sub);
|
|
74111
|
-
}
|
|
74112
|
-
async function readCapped(chunks) {
|
|
74113
|
-
const collected = [];
|
|
74114
|
-
let total = 0;
|
|
74115
|
-
for await (const chunk of chunks) {
|
|
74116
|
-
total += chunk.byteLength;
|
|
74117
|
-
if (total > RC_WEB_FETCH_MAX_BYTES) {
|
|
74118
|
-
return { tooLarge: true, text: "" };
|
|
74272
|
+
if (tail === "bridge" && method === "POST") {
|
|
74273
|
+
const refusal = credentialRefusal(request7.headers);
|
|
74274
|
+
if (refusal !== void 0) {
|
|
74275
|
+
await unauthorized(response, refusal);
|
|
74276
|
+
return;
|
|
74277
|
+
}
|
|
74278
|
+
const workerJwt = mintWorkerJwt(deps.randomToken, deps.now());
|
|
74279
|
+
session.workerJwts.add(workerJwt);
|
|
74280
|
+
deps.log?.(`rc selfhost ${session.id}: bridge registration minted a worker credential`);
|
|
74281
|
+
await answerJson2(response, HTTP_STATUS.ok, { worker_jwt: workerJwt, expires_in: RC_SELF_HOST_WORKER_JWT_TTL_SECONDS, api_base_url: RC_SELF_HOST_API_BASE_URL, worker_epoch: RC_SELF_HOST_WORKER_EPOCH });
|
|
74282
|
+
return;
|
|
74119
74283
|
}
|
|
74120
|
-
|
|
74121
|
-
|
|
74122
|
-
|
|
74123
|
-
|
|
74124
|
-
|
|
74125
|
-
|
|
74126
|
-
|
|
74127
|
-
|
|
74128
|
-
|
|
74129
|
-
|
|
74130
|
-
|
|
74131
|
-
|
|
74132
|
-
|
|
74133
|
-
try {
|
|
74134
|
-
addresses = net3.isIP(hostname3) === 0 ? await deps.lookup(hostname3) : [hostname3];
|
|
74135
|
-
} catch {
|
|
74136
|
-
return refused("resolve", `${hostname3} did not resolve`);
|
|
74137
|
-
}
|
|
74138
|
-
for (const address of addresses) {
|
|
74139
|
-
if (isPrivateAddress(address)) {
|
|
74140
|
-
return refused("private_address", `${hostname3} resolves to ${address}, a private address this web-fetch proxy refuses; set ${RC_WEB_FETCH_ALLOW_PRIVATE_ENV}=1 on the door if serving internal targets is intended`);
|
|
74284
|
+
if (tail === "archive" && method === "POST") {
|
|
74285
|
+
const refusal = credentialRefusal(request7.headers);
|
|
74286
|
+
if (refusal !== void 0) {
|
|
74287
|
+
await unauthorized(response, refusal);
|
|
74288
|
+
return;
|
|
74289
|
+
}
|
|
74290
|
+
for (const sink of [...session.workerStreams, ...session.clientStreams]) {
|
|
74291
|
+
sink.retire();
|
|
74292
|
+
}
|
|
74293
|
+
sessions.delete(session.id);
|
|
74294
|
+
deps.log?.(`rc selfhost ${session.id}: archived at its client's request`);
|
|
74295
|
+
await answerJson2(response, HTTP_STATUS.ok, {});
|
|
74296
|
+
return;
|
|
74141
74297
|
}
|
|
74142
|
-
|
|
74143
|
-
|
|
74144
|
-
|
|
74145
|
-
|
|
74146
|
-
let current;
|
|
74147
|
-
try {
|
|
74148
|
-
current = new URL(url2);
|
|
74149
|
-
} catch {
|
|
74150
|
-
return refused("malformed_url", `${url2} is not a URL this proxy can dial`);
|
|
74151
|
-
}
|
|
74152
|
-
const controller = new AbortController();
|
|
74153
|
-
const timer = setTimeout(() => {
|
|
74154
|
-
controller.abort();
|
|
74155
|
-
}, deps.deadlineMs);
|
|
74156
|
-
timer.unref();
|
|
74157
|
-
try {
|
|
74158
|
-
let redirects = 0;
|
|
74159
|
-
for (; ; ) {
|
|
74160
|
-
const refusal = await checkTarget(current, deps);
|
|
74298
|
+
if (tail === "worker" || tail.startsWith("worker/")) {
|
|
74299
|
+
const workerTail = tail === "worker" ? "" : tail.slice("worker/".length);
|
|
74300
|
+
const webProxy = workerTail === "web-fetch" || workerTail === "web-search";
|
|
74301
|
+
const refusal = webProxy ? workerOrCredentialRefusal(request7.headers, session) : workerRefusal(request7.headers, session);
|
|
74161
74302
|
if (refusal !== void 0) {
|
|
74162
|
-
|
|
74303
|
+
await unauthorized(response, refusal);
|
|
74304
|
+
return;
|
|
74163
74305
|
}
|
|
74164
|
-
|
|
74165
|
-
|
|
74166
|
-
|
|
74167
|
-
}
|
|
74168
|
-
|
|
74169
|
-
|
|
74306
|
+
if (workerTail === "" && method === "GET") {
|
|
74307
|
+
await answerJson2(response, HTTP_STATUS.ok, { worker: { external_metadata: session.externalMetadata ?? null } });
|
|
74308
|
+
return;
|
|
74309
|
+
}
|
|
74310
|
+
if (workerTail === "" && method === "PUT") {
|
|
74311
|
+
const body = parseJsonObject2(await readBody3(request7.body, CONTROL_BODY_CAP_BYTES));
|
|
74312
|
+
const claimedEpoch = numberOf(body, "worker_epoch");
|
|
74313
|
+
if (claimedEpoch !== void 0 && claimedEpoch !== RC_SELF_HOST_WORKER_EPOCH) {
|
|
74314
|
+
await epochStale(response);
|
|
74315
|
+
return;
|
|
74170
74316
|
}
|
|
74171
|
-
|
|
74317
|
+
const status = nonEmptyString2(body, "worker_status");
|
|
74318
|
+
if (status !== void 0) {
|
|
74319
|
+
session.workerStatus = status;
|
|
74320
|
+
}
|
|
74321
|
+
if (isRecord4(body?.external_metadata)) {
|
|
74322
|
+
session.externalMetadata = body.external_metadata;
|
|
74323
|
+
}
|
|
74324
|
+
await answerJson2(response, HTTP_STATUS.ok, {});
|
|
74325
|
+
return;
|
|
74172
74326
|
}
|
|
74173
|
-
if (
|
|
74174
|
-
|
|
74175
|
-
|
|
74176
|
-
|
|
74327
|
+
if (workerTail === "register" && method === "POST") {
|
|
74328
|
+
await readBody3(request7.body, CONTROL_BODY_CAP_BYTES);
|
|
74329
|
+
await answerJson2(response, HTTP_STATUS.ok, { worker_epoch: RC_SELF_HOST_WORKER_EPOCH });
|
|
74330
|
+
return;
|
|
74331
|
+
}
|
|
74332
|
+
if (workerTail === "heartbeat" && method === "POST") {
|
|
74333
|
+
await readBody3(request7.body, CONTROL_BODY_CAP_BYTES);
|
|
74334
|
+
await answerJson2(response, HTTP_STATUS.ok, {});
|
|
74335
|
+
return;
|
|
74336
|
+
}
|
|
74337
|
+
if (workerTail === "events" && method === "POST") {
|
|
74338
|
+
const body = parseJsonObject2(await readBody3(request7.body, CONTROL_BODY_CAP_BYTES));
|
|
74339
|
+
const claimedEpoch = numberOf(body, "worker_epoch");
|
|
74340
|
+
if (claimedEpoch !== void 0 && claimedEpoch !== RC_SELF_HOST_WORKER_EPOCH) {
|
|
74341
|
+
await epochStale(response);
|
|
74342
|
+
return;
|
|
74177
74343
|
}
|
|
74178
|
-
|
|
74179
|
-
|
|
74180
|
-
|
|
74181
|
-
|
|
74182
|
-
controller.abort();
|
|
74183
|
-
return refused("malformed_url", `a redirect of ${current.toString()} named ${hop.location}, which is not a URL this proxy can dial`);
|
|
74344
|
+
const events = body?.events;
|
|
74345
|
+
if (!Array.isArray(events) || events.length === 0 || events.length > RC_SELF_HOST_MAX_BATCH_EVENTS) {
|
|
74346
|
+
await answerJson2(response, HTTP_STATUS.badRequest, { type: "error", error: { type: "invalid_request_error", message: `a worker event batch is a non-empty array of at most ${String(RC_SELF_HOST_MAX_BATCH_EVENTS)} events` } });
|
|
74347
|
+
return;
|
|
74184
74348
|
}
|
|
74185
|
-
const
|
|
74186
|
-
|
|
74187
|
-
|
|
74188
|
-
|
|
74349
|
+
const results = [];
|
|
74350
|
+
for (const event of events) {
|
|
74351
|
+
if (!isRecord4(event) || !isRecord4(event.payload)) {
|
|
74352
|
+
await answerJson2(response, HTTP_STATUS.badRequest, { type: "error", error: { type: "invalid_request_error", message: "every event in a batch is an object carrying one payload object" } });
|
|
74353
|
+
return;
|
|
74354
|
+
}
|
|
74355
|
+
const published = publish(session, RC_SOURCE_WORKER, event.payload);
|
|
74356
|
+
results.push({ event_id: published.eventId, sequence_num: String(published.sequenceNum) });
|
|
74189
74357
|
}
|
|
74190
|
-
|
|
74191
|
-
|
|
74192
|
-
|
|
74358
|
+
sweep();
|
|
74359
|
+
await answerJson2(response, HTTP_STATUS.ok, { results });
|
|
74360
|
+
return;
|
|
74193
74361
|
}
|
|
74194
|
-
if (
|
|
74195
|
-
|
|
74196
|
-
return
|
|
74362
|
+
if (workerTail === "events/stream" && method === "GET") {
|
|
74363
|
+
await holdStream(session, "workerStreams", (event) => event.toWorker, request7, response);
|
|
74364
|
+
return;
|
|
74197
74365
|
}
|
|
74198
|
-
|
|
74199
|
-
|
|
74200
|
-
|
|
74201
|
-
return
|
|
74366
|
+
if (workerTail === "events/delivery" && method === "POST") {
|
|
74367
|
+
await readBody3(request7.body, CONTROL_BODY_CAP_BYTES);
|
|
74368
|
+
await answerJson2(response, HTTP_STATUS.ok, {});
|
|
74369
|
+
return;
|
|
74202
74370
|
}
|
|
74203
|
-
|
|
74204
|
-
|
|
74205
|
-
|
|
74206
|
-
|
|
74371
|
+
if (workerTail === "internal-events" && method === "POST") {
|
|
74372
|
+
const body = parseJsonObject2(await readBody3(request7.body, CONTROL_BODY_CAP_BYTES));
|
|
74373
|
+
const claimedEpoch = numberOf(body, "worker_epoch");
|
|
74374
|
+
if (claimedEpoch !== void 0 && claimedEpoch !== RC_SELF_HOST_WORKER_EPOCH) {
|
|
74375
|
+
await epochStale(response);
|
|
74376
|
+
return;
|
|
74377
|
+
}
|
|
74378
|
+
const events = body?.events;
|
|
74379
|
+
if (!Array.isArray(events)) {
|
|
74380
|
+
await answerJson2(response, HTTP_STATUS.badRequest, { type: "error", error: { type: "invalid_request_error", message: "an internal event batch is an array" } });
|
|
74381
|
+
return;
|
|
74382
|
+
}
|
|
74383
|
+
for (const event of events) {
|
|
74384
|
+
if (isRecord4(event)) {
|
|
74385
|
+
session.internalEvents.push({ cursor: deps.newUuid(), body: event });
|
|
74386
|
+
}
|
|
74387
|
+
}
|
|
74388
|
+
await answerJson2(response, HTTP_STATUS.ok, {});
|
|
74389
|
+
return;
|
|
74207
74390
|
}
|
|
74208
|
-
|
|
74209
|
-
|
|
74210
|
-
|
|
74211
|
-
|
|
74212
|
-
|
|
74213
|
-
}
|
|
74214
|
-
|
|
74215
|
-
|
|
74216
|
-
|
|
74217
|
-
|
|
74218
|
-
|
|
74219
|
-
|
|
74220
|
-
|
|
74221
|
-
|
|
74222
|
-
|
|
74223
|
-
|
|
74224
|
-
|
|
74225
|
-
|
|
74226
|
-
|
|
74227
|
-
signal
|
|
74228
|
-
},
|
|
74229
|
-
(response) => {
|
|
74230
|
-
const location = response.headers.location;
|
|
74231
|
-
resolve({
|
|
74232
|
-
status: response.statusCode ?? 0,
|
|
74233
|
-
location: typeof location === "string" ? location : void 0,
|
|
74234
|
-
contentType: typeof response.headers["content-type"] === "string" ? response.headers["content-type"] : void 0,
|
|
74235
|
-
chunks: response
|
|
74236
|
-
});
|
|
74391
|
+
if (workerTail === "internal-events" && method === "GET") {
|
|
74392
|
+
const url2 = new URL(request7.url, "http://127.0.0.1");
|
|
74393
|
+
const cursor3 = url2.searchParams.get("cursor");
|
|
74394
|
+
const from = cursor3 === null ? 0 : session.internalEvents.findIndex((event) => event.cursor === cursor3) + 1;
|
|
74395
|
+
const page = session.internalEvents.slice(from);
|
|
74396
|
+
await answerJson2(response, HTTP_STATUS.ok, { data: page.map((event) => event.body), next_cursor: page.length === 0 ? void 0 : page[page.length - 1]?.cursor });
|
|
74397
|
+
return;
|
|
74398
|
+
}
|
|
74399
|
+
if (workerTail === "diagnostics" && method === "POST") {
|
|
74400
|
+
await readBody3(request7.body, CONTROL_BODY_CAP_BYTES);
|
|
74401
|
+
await answerJson2(response, HTTP_STATUS.ok, {});
|
|
74402
|
+
return;
|
|
74403
|
+
}
|
|
74404
|
+
if (workerTail === "web-fetch" && method === "POST") {
|
|
74405
|
+
const body = parseJsonObject2(await readBody3(request7.body, CONTROL_BODY_CAP_BYTES));
|
|
74406
|
+
const target = nonEmptyString2(body, "url");
|
|
74407
|
+
if (target === void 0) {
|
|
74408
|
+
await answerJson2(response, HTTP_STATUS.badRequest, { type: "error", error: { type: "invalid_request_error", message: "a worker web-fetch carries one url" } });
|
|
74409
|
+
return;
|
|
74237
74410
|
}
|
|
74238
|
-
|
|
74239
|
-
|
|
74240
|
-
|
|
74241
|
-
|
|
74242
|
-
|
|
74243
|
-
|
|
74244
|
-
|
|
74245
|
-
|
|
74246
|
-
|
|
74247
|
-
|
|
74248
|
-
|
|
74249
|
-
|
|
74250
|
-
|
|
74251
|
-
|
|
74252
|
-
return env[RC_SELF_HOST_ENV] === "1";
|
|
74253
|
-
}
|
|
74254
|
-
var RC_SELF_HOST_SCOPE_LIST = ["user:profile", "user:inference", "user:sessions:claude_code", "user:mcp_servers", "user:file_upload"];
|
|
74255
|
-
var RC_SESSIONS_PATH_PREFIX2 = "/v1/code/sessions";
|
|
74256
|
-
var RC_COMPAT_SESSIONS_PATH = "/v1/sessions";
|
|
74257
|
-
var RC_SESSION_ID_PREFIX2 = "cse_";
|
|
74258
|
-
var RC_SELF_HOST_API_BASE_URL = "https://api.anthropic.com";
|
|
74259
|
-
var RC_SELF_HOST_OAUTH_HOST = "platform.claude.com";
|
|
74260
|
-
var HTTP_CONFLICT = 409;
|
|
74261
|
-
var MS_PER_SECOND7 = 1e3;
|
|
74262
|
-
var HTTP_SERVICE_UNAVAILABLE = 503;
|
|
74263
|
-
var RC_SELF_HOST_WORKER_JWT_TTL_SECONDS = 46800;
|
|
74264
|
-
var RC_SELF_HOST_OAUTH_TOKEN_TTL_SECONDS = 3600;
|
|
74265
|
-
var RC_SELF_HOST_KEEPALIVE_MS = 15e3;
|
|
74266
|
-
var RC_SELF_HOST_RETENTION_MS = 6e5;
|
|
74267
|
-
var RC_PRESENCE_REFRESH_SECONDS = 60;
|
|
74268
|
-
var RC_SELF_HOST_MAX_BATCH_EVENTS = 100;
|
|
74269
|
-
var RC_SELF_HOST_WORKER_EPOCH = 1;
|
|
74270
|
-
var RC_STREAM_EVENT_NAME = "client_event";
|
|
74271
|
-
var RC_SOURCE_WORKER = "worker";
|
|
74272
|
-
var RC_SOURCE_CLIENT = "client";
|
|
74273
|
-
var CONFLICT_REASON_HEADER2 = "x-ccr-conflict-reason";
|
|
74274
|
-
var CONFLICT_REASON_EPOCH_STALE = "epoch_stale";
|
|
74275
|
-
function isRecord5(value2) {
|
|
74276
|
-
return typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
|
|
74277
|
-
}
|
|
74278
|
-
function isWebSearchRefusal(value2) {
|
|
74279
|
-
return !Array.isArray(value2);
|
|
74280
|
-
}
|
|
74281
|
-
function nonEmptyString2(record2, field) {
|
|
74282
|
-
const value2 = record2 === void 0 ? void 0 : record2[field];
|
|
74283
|
-
return typeof value2 === "string" && value2 !== "" ? value2 : void 0;
|
|
74284
|
-
}
|
|
74285
|
-
function numberOf(record2, field) {
|
|
74286
|
-
const value2 = record2 === void 0 ? void 0 : record2[field];
|
|
74287
|
-
if (typeof value2 === "number" && Number.isFinite(value2)) {
|
|
74288
|
-
return value2;
|
|
74289
|
-
}
|
|
74290
|
-
if (typeof value2 === "string" && /^[0-9]+$/.test(value2)) {
|
|
74291
|
-
return Number(value2);
|
|
74292
|
-
}
|
|
74293
|
-
return void 0;
|
|
74294
|
-
}
|
|
74295
|
-
function stringArray(record2, field) {
|
|
74296
|
-
const value2 = record2 === void 0 ? void 0 : record2[field];
|
|
74297
|
-
if (!Array.isArray(value2)) {
|
|
74298
|
-
return void 0;
|
|
74299
|
-
}
|
|
74300
|
-
const members2 = [];
|
|
74301
|
-
for (const member of value2) {
|
|
74302
|
-
if (typeof member !== "string") {
|
|
74303
|
-
return void 0;
|
|
74304
|
-
}
|
|
74305
|
-
members2.push(member);
|
|
74306
|
-
}
|
|
74307
|
-
return members2;
|
|
74308
|
-
}
|
|
74309
|
-
function singleHeader2(headers, name) {
|
|
74310
|
-
const value2 = headers[name];
|
|
74311
|
-
return typeof value2 === "string" ? value2 : void 0;
|
|
74312
|
-
}
|
|
74313
|
-
function bearerOf(headers) {
|
|
74314
|
-
const presented = singleHeader2(headers, "authorization");
|
|
74315
|
-
if (presented === void 0) {
|
|
74316
|
-
return void 0;
|
|
74317
|
-
}
|
|
74318
|
-
return presented.slice(0, "bearer ".length).toLowerCase() === "bearer " ? presented.slice("bearer ".length) : void 0;
|
|
74319
|
-
}
|
|
74320
|
-
function presentationRefusal(headers) {
|
|
74321
|
-
const presented = singleHeader2(headers, "authorization");
|
|
74322
|
-
if (presented === void 0) {
|
|
74323
|
-
return "absent";
|
|
74324
|
-
}
|
|
74325
|
-
if (presented.slice(0, "bearer ".length).toLowerCase() !== "bearer ") {
|
|
74326
|
-
return "not-a-bearer";
|
|
74327
|
-
}
|
|
74328
|
-
const bearer = presented.slice("bearer ".length);
|
|
74329
|
-
return bearer.slice(0, "bearer ".length).toLowerCase() === "bearer " ? "doubled-scheme-word" : "unrecognised";
|
|
74330
|
-
}
|
|
74331
|
-
async function readBody3(request7, capBytes) {
|
|
74332
|
-
return await new Promise((resolve, reject) => {
|
|
74333
|
-
const chunks = [];
|
|
74334
|
-
let total = 0;
|
|
74335
|
-
request7.on("data", (chunk) => {
|
|
74336
|
-
total += chunk.length;
|
|
74337
|
-
if (total > capBytes) {
|
|
74338
|
-
request7.destroy();
|
|
74339
|
-
reject(new Error("body too large"));
|
|
74411
|
+
const outcome = await deps.webFetch(target);
|
|
74412
|
+
if (outcome.kind === "fetched") {
|
|
74413
|
+
const answer = { url: target, destination_url: outcome.destinationUrl, text: outcome.text, ...outcome.contentType === void 0 ? {} : { content_type: outcome.contentType } };
|
|
74414
|
+
if (Buffer.byteLength(JSON.stringify(answer), "utf8") > RC_WEB_FETCH_MAX_BYTES) {
|
|
74415
|
+
deps.log?.(`rc selfhost ${session.id}: worker web-fetch of ${target} escaped past the reader cap when framed`);
|
|
74416
|
+
await answerJson2(response, HTTP_STATUS.ok, { error: { error_type: "web_fetch_too_large", error_message: `the answer for ${target} exceeds the ${String(RC_WEB_FETCH_MAX_BYTES)} byte cap the CLI's proxy client reads` } });
|
|
74417
|
+
return;
|
|
74418
|
+
}
|
|
74419
|
+
deps.log?.(`rc selfhost ${session.id}: worker web-fetch served ${target} as ${outcome.destinationUrl}`);
|
|
74420
|
+
await answerJson2(response, HTTP_STATUS.ok, answer);
|
|
74421
|
+
return;
|
|
74422
|
+
}
|
|
74423
|
+
deps.log?.(`rc selfhost ${session.id}: worker web-fetch of ${target} refused: ${outcome.errorType}`);
|
|
74424
|
+
await answerJson2(response, HTTP_STATUS.ok, { error: { error_type: outcome.errorType, error_message: outcome.errorMessage } });
|
|
74340
74425
|
return;
|
|
74341
74426
|
}
|
|
74342
|
-
|
|
74343
|
-
|
|
74344
|
-
|
|
74345
|
-
|
|
74346
|
-
|
|
74347
|
-
|
|
74348
|
-
reject(new Error("the request body ended early"));
|
|
74349
|
-
});
|
|
74350
|
-
});
|
|
74351
|
-
}
|
|
74352
|
-
function parseJsonObject2(body) {
|
|
74353
|
-
let parsed;
|
|
74354
|
-
try {
|
|
74355
|
-
parsed = JSON.parse(body);
|
|
74356
|
-
} catch {
|
|
74357
|
-
return void 0;
|
|
74358
|
-
}
|
|
74359
|
-
return isRecord5(parsed) ? parsed : void 0;
|
|
74360
|
-
}
|
|
74361
|
-
function mintWorkerJwt(randomToken, issuedAt) {
|
|
74362
|
-
const encode3 = (value2) => Buffer.from(JSON.stringify(value2), "utf8").toString("base64url");
|
|
74363
|
-
const header = encode3({ alg: "HS256", typ: "JWT" });
|
|
74364
|
-
const payload = encode3({ exp: Math.floor((issuedAt + RC_SELF_HOST_WORKER_JWT_TTL_SECONDS * MS_PER_SECOND7) / MS_PER_SECOND7), iat: Math.floor(issuedAt / MS_PER_SECOND7) });
|
|
74365
|
-
return `${header}.${payload}.${Buffer.from(randomToken(), "utf8").toString("base64url")}`;
|
|
74366
|
-
}
|
|
74367
|
-
function growthBookFeatures() {
|
|
74368
|
-
return {
|
|
74369
|
-
tengu_ccr_bridge: { defaultValue: true },
|
|
74370
|
-
tengu_bridge_repl_v2: { defaultValue: true },
|
|
74371
|
-
tengu_bridge_repl_v2_cse_shim_enabled: { defaultValue: true },
|
|
74372
|
-
tengu_bridge_min_version: { defaultValue: { minVersion: "0.0.0" } }
|
|
74373
|
-
};
|
|
74374
|
-
}
|
|
74375
|
-
function createRcSelfHostSurface(deps) {
|
|
74376
|
-
const sessions = /* @__PURE__ */ new Map();
|
|
74377
|
-
const sweep = () => {
|
|
74378
|
-
const now = deps.now();
|
|
74379
|
-
for (const session of sessions.values()) {
|
|
74380
|
-
while (session.events[0] !== void 0 && now - session.events[0].storedAt > RC_SELF_HOST_RETENTION_MS) {
|
|
74381
|
-
const dropped = session.events.shift();
|
|
74382
|
-
const uuid3 = dropped === void 0 ? void 0 : nonEmptyString2(dropped.payload, "uuid");
|
|
74383
|
-
if (uuid3 !== void 0) {
|
|
74384
|
-
session.seenPayloadUuids.delete(uuid3);
|
|
74427
|
+
if (workerTail === "web-search" && method === "POST") {
|
|
74428
|
+
const body = parseJsonObject2(await readBody3(request7.body, CONTROL_BODY_CAP_BYTES));
|
|
74429
|
+
const query = nonEmptyString2(body, "query");
|
|
74430
|
+
if (query === void 0) {
|
|
74431
|
+
await answerJson2(response, HTTP_STATUS.badRequest, { type: "error", error: { type: "invalid_request_error", message: "a worker web-search carries one query" } });
|
|
74432
|
+
return;
|
|
74385
74433
|
}
|
|
74434
|
+
const allowedDomains = stringArray(body, "allowed_domains");
|
|
74435
|
+
const blockedDomains = stringArray(body, "blocked_domains");
|
|
74436
|
+
if (body?.allowed_domains !== void 0 && allowedDomains === void 0 || body?.blocked_domains !== void 0 && blockedDomains === void 0) {
|
|
74437
|
+
await answerJson2(response, HTTP_STATUS.badRequest, { type: "error", error: { type: "invalid_request_error", message: "the domain filters of a worker web-search are string arrays" } });
|
|
74438
|
+
return;
|
|
74439
|
+
}
|
|
74440
|
+
const searchProfile = nonEmptyString2(body, "search_profile");
|
|
74441
|
+
const backend = deps.webSearch;
|
|
74442
|
+
if (backend === void 0) {
|
|
74443
|
+
deps.log?.(`rc selfhost ${session.id}: worker web-search of "${query}" refused: no search backend serves this door`);
|
|
74444
|
+
await answerJson2(response, HTTP_STATUS.ok, { results: [], error: { error_type: "web_search_unavailable", error_message: "this front door serves the Remote Control web-search proxy without a search backend, so no query can be served here" } });
|
|
74445
|
+
return;
|
|
74446
|
+
}
|
|
74447
|
+
const answered = await backend({ query, ...allowedDomains === void 0 ? {} : { allowedDomains }, ...blockedDomains === void 0 ? {} : { blockedDomains }, ...searchProfile === void 0 ? {} : { searchProfile } });
|
|
74448
|
+
deps.log?.(`rc selfhost ${session.id}: worker web-search of "${query}" answered by its backend`);
|
|
74449
|
+
if (isWebSearchRefusal(answered)) {
|
|
74450
|
+
await answerJson2(response, HTTP_STATUS.ok, { results: [], error: { error_type: answered.errorType, error_message: answered.errorMessage } });
|
|
74451
|
+
return;
|
|
74452
|
+
}
|
|
74453
|
+
await answerJson2(response, HTTP_STATUS.ok, { results: answered.map((result) => ({ title: result.title ?? "", url: result.url, snippet: result.snippet ?? "" })) });
|
|
74454
|
+
return;
|
|
74386
74455
|
}
|
|
74387
|
-
|
|
74388
|
-
|
|
74389
|
-
}
|
|
74456
|
+
await answerJson2(response, HTTP_STATUS.notFound, { type: "error", error: { type: "api_error", message: `the self-hosted Remote Control surface serves no worker path "${workerTail}"` } });
|
|
74457
|
+
return;
|
|
74390
74458
|
}
|
|
74391
|
-
|
|
74392
|
-
|
|
74393
|
-
|
|
74394
|
-
|
|
74395
|
-
|
|
74396
|
-
|
|
74397
|
-
|
|
74459
|
+
const clientRefusal = credentialRefusal(request7.headers);
|
|
74460
|
+
if (clientRefusal !== void 0) {
|
|
74461
|
+
await unauthorized(response, clientRefusal);
|
|
74462
|
+
return;
|
|
74463
|
+
}
|
|
74464
|
+
if (tail === "events" && method === "POST") {
|
|
74465
|
+
const body = parseJsonObject2(await readBody3(request7.body, CONTROL_BODY_CAP_BYTES));
|
|
74466
|
+
const events = body?.events;
|
|
74467
|
+
if (!Array.isArray(events) || events.length === 0 || events.length > RC_SELF_HOST_MAX_BATCH_EVENTS) {
|
|
74468
|
+
await answerJson2(response, HTTP_STATUS.badRequest, { type: "error", error: { type: "invalid_request_error", message: `a client event write is a non-empty array of at most ${String(RC_SELF_HOST_MAX_BATCH_EVENTS)} events` } });
|
|
74469
|
+
return;
|
|
74470
|
+
}
|
|
74471
|
+
const results = [];
|
|
74472
|
+
for (const event of events) {
|
|
74473
|
+
if (!isRecord4(event) || !isRecord4(event.payload)) {
|
|
74474
|
+
await answerJson2(response, HTTP_STATUS.badRequest, { type: "error", error: { type: "invalid_request_error", message: "every event in a write is an object carrying one payload object" } });
|
|
74475
|
+
return;
|
|
74476
|
+
}
|
|
74477
|
+
const published = publish(session, RC_SOURCE_CLIENT, event.payload);
|
|
74478
|
+
results.push({ sequence_num: String(published.sequenceNum), duplicate: published.duplicate });
|
|
74398
74479
|
}
|
|
74480
|
+
sweep();
|
|
74481
|
+
await answerJson2(response, HTTP_STATUS.ok, { results });
|
|
74482
|
+
return;
|
|
74399
74483
|
}
|
|
74400
|
-
|
|
74401
|
-
|
|
74402
|
-
|
|
74403
|
-
const expected = deps.credentialRecord()?.accessToken;
|
|
74404
|
-
const presented = bearerOf(headers);
|
|
74405
|
-
return expected !== void 0 && presented !== void 0 && isLiveCapability(presented, [expected]);
|
|
74406
|
-
};
|
|
74407
|
-
const presentsWorkerJwt = (headers, session) => {
|
|
74408
|
-
const presented = bearerOf(headers);
|
|
74409
|
-
if (presented === void 0) {
|
|
74410
|
-
return false;
|
|
74484
|
+
if (tail === "events" && method === "GET") {
|
|
74485
|
+
await answerJson2(response, HTTP_STATUS.ok, { data: session.events.map((event) => ({ event_id: event.eventId, event_type: nonEmptyString2(event.payload, "type") ?? "event", sequence_num: event.sequenceNum, source: event.source, payload: event.payload, created_at: event.createdAt })) });
|
|
74486
|
+
return;
|
|
74411
74487
|
}
|
|
74412
|
-
|
|
74413
|
-
|
|
74414
|
-
|
|
74415
|
-
}
|
|
74488
|
+
if (tail === "events/stream" && method === "GET") {
|
|
74489
|
+
await holdStream(session, "clientStreams", () => true, request7, response);
|
|
74490
|
+
return;
|
|
74416
74491
|
}
|
|
74417
|
-
|
|
74418
|
-
|
|
74419
|
-
|
|
74420
|
-
|
|
74421
|
-
|
|
74422
|
-
|
|
74423
|
-
|
|
74424
|
-
|
|
74425
|
-
|
|
74426
|
-
|
|
74427
|
-
|
|
74428
|
-
|
|
74429
|
-
|
|
74430
|
-
|
|
74431
|
-
|
|
74432
|
-
|
|
74433
|
-
data: ${JSON.stringify(envelope)}
|
|
74434
|
-
|
|
74435
|
-
`;
|
|
74492
|
+
if (tail === "client/presence" && method === "POST") {
|
|
74493
|
+
await readBody3(request7.body, CONTROL_BODY_CAP_BYTES);
|
|
74494
|
+
await answerJson2(response, HTTP_STATUS.ok, { refresh_after_seconds: RC_PRESENCE_REFRESH_SECONDS });
|
|
74495
|
+
return;
|
|
74496
|
+
}
|
|
74497
|
+
if (tail === "mark_read" && method === "POST") {
|
|
74498
|
+
await readBody3(request7.body, CONTROL_BODY_CAP_BYTES);
|
|
74499
|
+
await answerJson2(response, HTTP_STATUS.ok, {});
|
|
74500
|
+
return;
|
|
74501
|
+
}
|
|
74502
|
+
if (tail === "teleport-events" && method === "POST") {
|
|
74503
|
+
await readBody3(request7.body, CONTROL_BODY_CAP_BYTES);
|
|
74504
|
+
await answerJson2(response, HTTP_STATUS.ok, {});
|
|
74505
|
+
return;
|
|
74506
|
+
}
|
|
74507
|
+
await answerJson2(response, HTTP_STATUS.notFound, { type: "error", error: { type: "api_error", message: `the self-hosted Remote Control surface serves no path "${tail}"` } });
|
|
74436
74508
|
};
|
|
74437
|
-
const
|
|
74438
|
-
|
|
74439
|
-
|
|
74440
|
-
|
|
74441
|
-
|
|
74442
|
-
response
|
|
74443
|
-
|
|
74444
|
-
|
|
74445
|
-
|
|
74446
|
-
|
|
74447
|
-
|
|
74448
|
-
}).catch(() => {
|
|
74449
|
-
});
|
|
74450
|
-
},
|
|
74451
|
-
retire: () => {
|
|
74452
|
-
session[which].delete(sink);
|
|
74509
|
+
const recordMissing = () => deps.credentialRecord() === void 0;
|
|
74510
|
+
const route = {
|
|
74511
|
+
name: "rc-selfhost",
|
|
74512
|
+
headroomEligible: false,
|
|
74513
|
+
headroomUpstream: void 0,
|
|
74514
|
+
serve: async (request7, response) => {
|
|
74515
|
+
const method = request7.method.toUpperCase();
|
|
74516
|
+
const pathname = new URL(request7.url, "http://127.0.0.1").pathname;
|
|
74517
|
+
if (recordMissing()) {
|
|
74518
|
+
await noCredential(response);
|
|
74519
|
+
return;
|
|
74453
74520
|
}
|
|
74454
|
-
|
|
74455
|
-
|
|
74456
|
-
|
|
74457
|
-
for (const event of replay) {
|
|
74458
|
-
sink.write(frameOf(event));
|
|
74459
|
-
}
|
|
74460
|
-
await new Promise((resolve) => {
|
|
74461
|
-
request7.signal.addEventListener("abort", () => {
|
|
74462
|
-
resolve(void 0);
|
|
74463
|
-
}, { once: true });
|
|
74464
|
-
});
|
|
74465
|
-
sink.retire();
|
|
74466
|
-
response.end();
|
|
74467
|
-
};
|
|
74468
|
-
const publish = (session, source, payload) => {
|
|
74469
|
-
const uuid3 = nonEmptyString2(payload, "uuid");
|
|
74470
|
-
const duplicate = uuid3 !== void 0 && session.seenPayloadUuids.has(uuid3);
|
|
74471
|
-
session.sequenceNum += 1;
|
|
74472
|
-
const event = {
|
|
74473
|
-
sequenceNum: session.sequenceNum,
|
|
74474
|
-
eventId: uuid3 ?? deps.newUuid(),
|
|
74475
|
-
source,
|
|
74476
|
-
// The one enrichment the real service was observed making on the live rig's stream: the wall-clock instant it took the write. The worker's handler ignores it; the door's own parser forwards it verbatim.
|
|
74477
|
-
payload: source === RC_SOURCE_CLIENT ? { ...payload, server_received_wall_ms: deps.now() } : payload,
|
|
74478
|
-
createdAt: new Date(deps.now()).toISOString(),
|
|
74479
|
-
toWorker: source === RC_SOURCE_CLIENT,
|
|
74480
|
-
storedAt: deps.now()
|
|
74481
|
-
};
|
|
74482
|
-
session.events.push(event);
|
|
74483
|
-
if (uuid3 !== void 0) {
|
|
74484
|
-
session.seenPayloadUuids.add(uuid3);
|
|
74485
|
-
}
|
|
74486
|
-
session.lastTrafficAt = event.storedAt;
|
|
74487
|
-
const frame = frameOf(event);
|
|
74488
|
-
if (event.toWorker) {
|
|
74489
|
-
for (const sink of [...session.workerStreams]) {
|
|
74490
|
-
sink.write(frame);
|
|
74491
|
-
}
|
|
74492
|
-
}
|
|
74493
|
-
for (const sink of [...session.clientStreams]) {
|
|
74494
|
-
sink.write(frame);
|
|
74495
|
-
}
|
|
74496
|
-
return { sequenceNum: event.sequenceNum, eventId: event.eventId, duplicate };
|
|
74497
|
-
};
|
|
74498
|
-
const answerJson2 = async (response, status, body, extraHeaders = {}) => {
|
|
74499
|
-
const text2 = JSON.stringify(body);
|
|
74500
|
-
response.start(status, { "content-type": "application/json", "content-length": String(Buffer.byteLength(text2, "utf8")), ...extraHeaders });
|
|
74501
|
-
await response.write(text2);
|
|
74502
|
-
response.end();
|
|
74503
|
-
};
|
|
74504
|
-
const noCredential = async (response) => {
|
|
74505
|
-
await answerJson2(response, HTTP_SERVICE_UNAVAILABLE, { type: "error", error: { type: "api_error", message: "the front door's self-hosted Remote Control surface is on but no credential has been minted for it; run agent-shim frontdoor rc selfhost mint first" } });
|
|
74506
|
-
};
|
|
74507
|
-
const epochStale = async (response) => {
|
|
74508
|
-
await answerJson2(response, HTTP_CONFLICT, { type: "error", error: { type: "api_error", message: "worker_epoch does not match the session's live epoch" } }, { [CONFLICT_REASON_HEADER2]: CONFLICT_REASON_EPOCH_STALE });
|
|
74509
|
-
};
|
|
74510
|
-
const refusalMessages = {
|
|
74511
|
-
absent: "the self-hosted Remote Control surface authenticates by the Authorization header, and this call carried none",
|
|
74512
|
-
"not-a-bearer": "the self-hosted Remote Control surface reads a Bearer authorization, and this call's Authorization header did not carry the Bearer scheme word",
|
|
74513
|
-
"doubled-scheme-word": "the self-hosted Remote Control surface reads one Bearer scheme word, and this call's Authorization header carried it twice: a persisted session record holds the whole header value with the scheme word included, so prefixing Bearer onto it presents a different credential",
|
|
74514
|
-
unrecognised: "the self-hosted Remote Control surface did not recognise the presented credential"
|
|
74515
|
-
};
|
|
74516
|
-
const unauthorized = async (response, refusal) => {
|
|
74517
|
-
await answerJson2(response, HTTP_STATUS.unauthorized, { type: "error", error: { type: "authentication_error", message: refusalMessages[refusal] } });
|
|
74518
|
-
};
|
|
74519
|
-
const serveSessionPath = async (session, tail, request7, response) => {
|
|
74520
|
-
const method = request7.method.toUpperCase();
|
|
74521
|
-
if (session === void 0) {
|
|
74522
|
-
await answerJson2(response, HTTP_STATUS.notFound, { type: "error", error: { type: "api_error", message: "no such Remote Control session on this door" } });
|
|
74523
|
-
return;
|
|
74524
|
-
}
|
|
74525
|
-
session.lastTrafficAt = deps.now();
|
|
74526
|
-
if (tail === "") {
|
|
74527
|
-
if (method !== "GET") {
|
|
74528
|
-
await answerJson2(response, HTTP_STATUS.methodNotAllowed, { type: "error", error: { type: "api_error", message: "the session read is a GET" } });
|
|
74529
|
-
return;
|
|
74530
|
-
}
|
|
74531
|
-
await answerJson2(response, HTTP_STATUS.ok, { session: { id: session.id, title: session.title, created_at: new Date(session.createdAt).toISOString(), status: "active" } });
|
|
74532
|
-
return;
|
|
74533
|
-
}
|
|
74534
|
-
if (tail === "bridge" && method === "POST") {
|
|
74535
|
-
const refusal = credentialRefusal(request7.headers);
|
|
74536
|
-
if (refusal !== void 0) {
|
|
74537
|
-
await unauthorized(response, refusal);
|
|
74538
|
-
return;
|
|
74539
|
-
}
|
|
74540
|
-
const workerJwt = mintWorkerJwt(deps.randomToken, deps.now());
|
|
74541
|
-
session.workerJwts.add(workerJwt);
|
|
74542
|
-
deps.log?.(`rc selfhost ${session.id}: bridge registration minted a worker credential`);
|
|
74543
|
-
await answerJson2(response, HTTP_STATUS.ok, { worker_jwt: workerJwt, expires_in: RC_SELF_HOST_WORKER_JWT_TTL_SECONDS, api_base_url: RC_SELF_HOST_API_BASE_URL, worker_epoch: RC_SELF_HOST_WORKER_EPOCH });
|
|
74544
|
-
return;
|
|
74545
|
-
}
|
|
74546
|
-
if (tail === "archive" && method === "POST") {
|
|
74547
|
-
const refusal = credentialRefusal(request7.headers);
|
|
74548
|
-
if (refusal !== void 0) {
|
|
74549
|
-
await unauthorized(response, refusal);
|
|
74550
|
-
return;
|
|
74551
|
-
}
|
|
74552
|
-
for (const sink of [...session.workerStreams, ...session.clientStreams]) {
|
|
74553
|
-
sink.retire();
|
|
74554
|
-
}
|
|
74555
|
-
sessions.delete(session.id);
|
|
74556
|
-
deps.log?.(`rc selfhost ${session.id}: archived at its client's request`);
|
|
74557
|
-
await answerJson2(response, HTTP_STATUS.ok, {});
|
|
74558
|
-
return;
|
|
74559
|
-
}
|
|
74560
|
-
if (tail === "worker" || tail.startsWith("worker/")) {
|
|
74561
|
-
const workerTail = tail === "worker" ? "" : tail.slice("worker/".length);
|
|
74562
|
-
const webProxy = workerTail === "web-fetch" || workerTail === "web-search";
|
|
74563
|
-
const refusal = webProxy ? workerOrCredentialRefusal(request7.headers, session) : workerRefusal(request7.headers, session);
|
|
74564
|
-
if (refusal !== void 0) {
|
|
74565
|
-
await unauthorized(response, refusal);
|
|
74566
|
-
return;
|
|
74567
|
-
}
|
|
74568
|
-
if (workerTail === "" && method === "GET") {
|
|
74569
|
-
await answerJson2(response, HTTP_STATUS.ok, { worker: { external_metadata: session.externalMetadata ?? null } });
|
|
74570
|
-
return;
|
|
74571
|
-
}
|
|
74572
|
-
if (workerTail === "" && method === "PUT") {
|
|
74573
|
-
const body = parseJsonObject2(await readBody3(request7.body, CONTROL_BODY_CAP_BYTES));
|
|
74574
|
-
const claimedEpoch = numberOf(body, "worker_epoch");
|
|
74575
|
-
if (claimedEpoch !== void 0 && claimedEpoch !== RC_SELF_HOST_WORKER_EPOCH) {
|
|
74576
|
-
await epochStale(response);
|
|
74521
|
+
if (pathname === RC_COMPAT_SESSIONS_PATH) {
|
|
74522
|
+
if (method !== "GET") {
|
|
74523
|
+
await answerJson2(response, HTTP_STATUS.methodNotAllowed, { type: "error", error: { type: "api_error", message: "the compatibility session list is a GET" } });
|
|
74577
74524
|
return;
|
|
74578
74525
|
}
|
|
74579
|
-
const
|
|
74580
|
-
if (
|
|
74581
|
-
|
|
74582
|
-
|
|
74583
|
-
if (isRecord5(body?.external_metadata)) {
|
|
74584
|
-
session.externalMetadata = body.external_metadata;
|
|
74526
|
+
const listRefusal = credentialRefusal(request7.headers);
|
|
74527
|
+
if (listRefusal !== void 0) {
|
|
74528
|
+
await unauthorized(response, listRefusal);
|
|
74529
|
+
return;
|
|
74585
74530
|
}
|
|
74586
|
-
await answerJson2(response, HTTP_STATUS.ok, {});
|
|
74587
|
-
return;
|
|
74588
|
-
}
|
|
74589
|
-
if (workerTail === "register" && method === "POST") {
|
|
74590
|
-
await readBody3(request7.body, CONTROL_BODY_CAP_BYTES);
|
|
74591
|
-
await answerJson2(response, HTTP_STATUS.ok, { worker_epoch: RC_SELF_HOST_WORKER_EPOCH });
|
|
74531
|
+
await answerJson2(response, HTTP_STATUS.ok, { data: [] });
|
|
74592
74532
|
return;
|
|
74593
74533
|
}
|
|
74594
|
-
if (
|
|
74595
|
-
await
|
|
74596
|
-
await answerJson2(response, HTTP_STATUS.ok, {});
|
|
74534
|
+
if (pathname !== RC_SESSIONS_PATH_PREFIX2 && !pathname.startsWith(`${RC_SESSIONS_PATH_PREFIX2}/`)) {
|
|
74535
|
+
await answerJson2(response, HTTP_STATUS.notFound, { type: "error", error: { type: "api_error", message: `the self-hosted Remote Control surface serves no path "${pathname}"` } });
|
|
74597
74536
|
return;
|
|
74598
74537
|
}
|
|
74599
|
-
if (
|
|
74600
|
-
|
|
74601
|
-
|
|
74602
|
-
|
|
74603
|
-
|
|
74604
|
-
|
|
74605
|
-
|
|
74606
|
-
|
|
74607
|
-
|
|
74608
|
-
|
|
74538
|
+
if (pathname === RC_SESSIONS_PATH_PREFIX2) {
|
|
74539
|
+
if (method === "POST") {
|
|
74540
|
+
const createRefusal = credentialRefusal(request7.headers);
|
|
74541
|
+
if (createRefusal !== void 0) {
|
|
74542
|
+
await unauthorized(response, createRefusal);
|
|
74543
|
+
return;
|
|
74544
|
+
}
|
|
74545
|
+
const body = parseJsonObject2(await readBody3(request7.body, CONTROL_BODY_CAP_BYTES));
|
|
74546
|
+
const session = {
|
|
74547
|
+
id: `${RC_SESSION_ID_PREFIX2}${deps.newUuid()}`,
|
|
74548
|
+
createdAt: deps.now(),
|
|
74549
|
+
title: nonEmptyString2(body, "title"),
|
|
74550
|
+
workerJwts: /* @__PURE__ */ new Set(),
|
|
74551
|
+
sequenceNum: 0,
|
|
74552
|
+
events: [],
|
|
74553
|
+
seenPayloadUuids: /* @__PURE__ */ new Set(),
|
|
74554
|
+
workerStreams: /* @__PURE__ */ new Set(),
|
|
74555
|
+
clientStreams: /* @__PURE__ */ new Set(),
|
|
74556
|
+
internalEvents: [],
|
|
74557
|
+
workerStatus: void 0,
|
|
74558
|
+
externalMetadata: void 0,
|
|
74559
|
+
lastTrafficAt: deps.now()
|
|
74560
|
+
};
|
|
74561
|
+
sessions.set(session.id, session);
|
|
74562
|
+
deps.log?.(`rc selfhost ${session.id}: session created on the door's own surface`);
|
|
74563
|
+
await answerJson2(response, HTTP_STATUS.ok, { session: { id: session.id, title: session.title, created_at: new Date(session.createdAt).toISOString(), status: "active" } });
|
|
74609
74564
|
return;
|
|
74610
74565
|
}
|
|
74611
|
-
|
|
74612
|
-
|
|
74613
|
-
if (
|
|
74614
|
-
await
|
|
74566
|
+
if (method === "GET") {
|
|
74567
|
+
const readRefusal = credentialRefusal(request7.headers);
|
|
74568
|
+
if (readRefusal !== void 0) {
|
|
74569
|
+
await unauthorized(response, readRefusal);
|
|
74615
74570
|
return;
|
|
74616
74571
|
}
|
|
74617
|
-
|
|
74618
|
-
|
|
74572
|
+
await answerJson2(response, HTTP_STATUS.ok, { data: [...sessions.values()].map((session) => ({ id: session.id, title: session.title, created_at: new Date(session.createdAt).toISOString(), updated_at: new Date(session.lastTrafficAt).toISOString(), session_status: "active" })) });
|
|
74573
|
+
return;
|
|
74619
74574
|
}
|
|
74620
|
-
|
|
74621
|
-
await answerJson2(response, HTTP_STATUS.ok, { results });
|
|
74622
|
-
return;
|
|
74623
|
-
}
|
|
74624
|
-
if (workerTail === "events/stream" && method === "GET") {
|
|
74625
|
-
await holdStream(session, "workerStreams", (event) => event.toWorker, request7, response);
|
|
74575
|
+
await answerJson2(response, HTTP_STATUS.methodNotAllowed, { type: "error", error: { type: "api_error", message: "the session collection is a POST (create) and a GET (list)" } });
|
|
74626
74576
|
return;
|
|
74627
74577
|
}
|
|
74628
|
-
|
|
74629
|
-
|
|
74630
|
-
|
|
74578
|
+
const rest = pathname.slice(`${RC_SESSIONS_PATH_PREFIX2}/`.length);
|
|
74579
|
+
const slash = rest.indexOf("/");
|
|
74580
|
+
const id = slash === -1 ? rest : rest.slice(0, slash);
|
|
74581
|
+
const tail = slash === -1 ? "" : rest.slice(slash + 1);
|
|
74582
|
+
if (!id.startsWith(RC_SESSION_ID_PREFIX2)) {
|
|
74583
|
+
await answerJson2(response, HTTP_STATUS.notFound, { type: "error", error: { type: "api_error", message: "the session family names one cse_-prefixed session id" } });
|
|
74631
74584
|
return;
|
|
74632
74585
|
}
|
|
74633
|
-
|
|
74634
|
-
|
|
74635
|
-
|
|
74636
|
-
|
|
74637
|
-
|
|
74638
|
-
|
|
74639
|
-
|
|
74640
|
-
|
|
74641
|
-
|
|
74642
|
-
|
|
74643
|
-
|
|
74644
|
-
|
|
74645
|
-
|
|
74646
|
-
|
|
74647
|
-
|
|
74586
|
+
await serveSessionPath(sessions.get(id), tail, request7, response);
|
|
74587
|
+
}
|
|
74588
|
+
};
|
|
74589
|
+
const answerServerJson = (response, status, body) => {
|
|
74590
|
+
const text2 = JSON.stringify(body);
|
|
74591
|
+
response.writeHead(status, { "content-type": "application/json", "content-length": String(Buffer.byteLength(text2, "utf8")) });
|
|
74592
|
+
response.end(text2);
|
|
74593
|
+
};
|
|
74594
|
+
const local = {
|
|
74595
|
+
parsesHost: (host) => host === RC_SELF_HOST_OAUTH_HOST,
|
|
74596
|
+
serve: async (host, request7, response) => {
|
|
74597
|
+
const method = (request7.method ?? "").toUpperCase();
|
|
74598
|
+
const pathname = new URL(request7.url ?? "/", "http://127.0.0.1").pathname;
|
|
74599
|
+
if (host === RC_SELF_HOST_OAUTH_HOST) {
|
|
74600
|
+
if (method === "POST" && pathname === "/v1/oauth/token") {
|
|
74601
|
+
const body = parseJsonObject2(await readBody3(request7, CONTROL_BODY_CAP_BYTES));
|
|
74602
|
+
const record2 = deps.credentialRecord();
|
|
74603
|
+
if (record2 === void 0) {
|
|
74604
|
+
answerServerJson(response, HTTP_SERVICE_UNAVAILABLE, { error: "the front door's self-hosted Remote Control surface is on but no credential has been minted for it" });
|
|
74605
|
+
return true;
|
|
74606
|
+
}
|
|
74607
|
+
if (body?.grant_type !== void 0 && body.grant_type !== "refresh_token") {
|
|
74608
|
+
answerServerJson(response, HTTP_STATUS.badRequest, { error: "unsupported_grant_type" });
|
|
74609
|
+
return true;
|
|
74648
74610
|
}
|
|
74649
|
-
|
|
74650
|
-
|
|
74651
|
-
return;
|
|
74652
|
-
}
|
|
74653
|
-
if (workerTail === "internal-events" && method === "GET") {
|
|
74654
|
-
const url2 = new URL(request7.url, "http://127.0.0.1");
|
|
74655
|
-
const cursor3 = url2.searchParams.get("cursor");
|
|
74656
|
-
const from = cursor3 === null ? 0 : session.internalEvents.findIndex((event) => event.cursor === cursor3) + 1;
|
|
74657
|
-
const page = session.internalEvents.slice(from);
|
|
74658
|
-
await answerJson2(response, HTTP_STATUS.ok, { data: page.map((event) => event.body), next_cursor: page.length === 0 ? void 0 : page[page.length - 1]?.cursor });
|
|
74659
|
-
return;
|
|
74660
|
-
}
|
|
74661
|
-
if (workerTail === "diagnostics" && method === "POST") {
|
|
74662
|
-
await readBody3(request7.body, CONTROL_BODY_CAP_BYTES);
|
|
74663
|
-
await answerJson2(response, HTTP_STATUS.ok, {});
|
|
74664
|
-
return;
|
|
74665
|
-
}
|
|
74666
|
-
if (workerTail === "web-fetch" && method === "POST") {
|
|
74667
|
-
const body = parseJsonObject2(await readBody3(request7.body, CONTROL_BODY_CAP_BYTES));
|
|
74668
|
-
const target = nonEmptyString2(body, "url");
|
|
74669
|
-
if (target === void 0) {
|
|
74670
|
-
await answerJson2(response, HTTP_STATUS.badRequest, { type: "error", error: { type: "invalid_request_error", message: "a worker web-fetch carries one url" } });
|
|
74671
|
-
return;
|
|
74672
|
-
}
|
|
74673
|
-
const outcome = await deps.webFetch(target);
|
|
74674
|
-
if (outcome.kind === "fetched") {
|
|
74675
|
-
const answer = { url: target, destination_url: outcome.destinationUrl, text: outcome.text, ...outcome.contentType === void 0 ? {} : { content_type: outcome.contentType } };
|
|
74676
|
-
if (Buffer.byteLength(JSON.stringify(answer), "utf8") > RC_WEB_FETCH_MAX_BYTES) {
|
|
74677
|
-
deps.log?.(`rc selfhost ${session.id}: worker web-fetch of ${target} escaped past the reader cap when framed`);
|
|
74678
|
-
await answerJson2(response, HTTP_STATUS.ok, { error: { error_type: "web_fetch_too_large", error_message: `the answer for ${target} exceeds the ${String(RC_WEB_FETCH_MAX_BYTES)} byte cap the CLI's proxy client reads` } });
|
|
74679
|
-
return;
|
|
74680
|
-
}
|
|
74681
|
-
deps.log?.(`rc selfhost ${session.id}: worker web-fetch served ${target} as ${outcome.destinationUrl}`);
|
|
74682
|
-
await answerJson2(response, HTTP_STATUS.ok, answer);
|
|
74683
|
-
return;
|
|
74684
|
-
}
|
|
74685
|
-
deps.log?.(`rc selfhost ${session.id}: worker web-fetch of ${target} refused: ${outcome.errorType}`);
|
|
74686
|
-
await answerJson2(response, HTTP_STATUS.ok, { error: { error_type: outcome.errorType, error_message: outcome.errorMessage } });
|
|
74687
|
-
return;
|
|
74688
|
-
}
|
|
74689
|
-
if (workerTail === "web-search" && method === "POST") {
|
|
74690
|
-
const body = parseJsonObject2(await readBody3(request7.body, CONTROL_BODY_CAP_BYTES));
|
|
74691
|
-
const query = nonEmptyString2(body, "query");
|
|
74692
|
-
if (query === void 0) {
|
|
74693
|
-
await answerJson2(response, HTTP_STATUS.badRequest, { type: "error", error: { type: "invalid_request_error", message: "a worker web-search carries one query" } });
|
|
74694
|
-
return;
|
|
74695
|
-
}
|
|
74696
|
-
const allowedDomains = stringArray(body, "allowed_domains");
|
|
74697
|
-
const blockedDomains = stringArray(body, "blocked_domains");
|
|
74698
|
-
if (body?.allowed_domains !== void 0 && allowedDomains === void 0 || body?.blocked_domains !== void 0 && blockedDomains === void 0) {
|
|
74699
|
-
await answerJson2(response, HTTP_STATUS.badRequest, { type: "error", error: { type: "invalid_request_error", message: "the domain filters of a worker web-search are string arrays" } });
|
|
74700
|
-
return;
|
|
74701
|
-
}
|
|
74702
|
-
const searchProfile = nonEmptyString2(body, "search_profile");
|
|
74703
|
-
const backend = deps.webSearch;
|
|
74704
|
-
if (backend === void 0) {
|
|
74705
|
-
deps.log?.(`rc selfhost ${session.id}: worker web-search of "${query}" refused: no search backend serves this door`);
|
|
74706
|
-
await answerJson2(response, HTTP_STATUS.ok, { results: [], error: { error_type: "web_search_unavailable", error_message: "this front door serves the Remote Control web-search proxy without a search backend, so no query can be served here" } });
|
|
74707
|
-
return;
|
|
74708
|
-
}
|
|
74709
|
-
const answered = await backend({ query, ...allowedDomains === void 0 ? {} : { allowedDomains }, ...blockedDomains === void 0 ? {} : { blockedDomains }, ...searchProfile === void 0 ? {} : { searchProfile } });
|
|
74710
|
-
deps.log?.(`rc selfhost ${session.id}: worker web-search of "${query}" answered by its backend`);
|
|
74711
|
-
if (isWebSearchRefusal(answered)) {
|
|
74712
|
-
await answerJson2(response, HTTP_STATUS.ok, { results: [], error: { error_type: answered.errorType, error_message: answered.errorMessage } });
|
|
74713
|
-
return;
|
|
74714
|
-
}
|
|
74715
|
-
await answerJson2(response, HTTP_STATUS.ok, { results: answered.map((result) => ({ title: result.title ?? "", url: result.url, snippet: result.snippet ?? "" })) });
|
|
74716
|
-
return;
|
|
74717
|
-
}
|
|
74718
|
-
await answerJson2(response, HTTP_STATUS.notFound, { type: "error", error: { type: "api_error", message: `the self-hosted Remote Control surface serves no worker path "${workerTail}"` } });
|
|
74719
|
-
return;
|
|
74720
|
-
}
|
|
74721
|
-
const clientRefusal = credentialRefusal(request7.headers);
|
|
74722
|
-
if (clientRefusal !== void 0) {
|
|
74723
|
-
await unauthorized(response, clientRefusal);
|
|
74724
|
-
return;
|
|
74725
|
-
}
|
|
74726
|
-
if (tail === "events" && method === "POST") {
|
|
74727
|
-
const body = parseJsonObject2(await readBody3(request7.body, CONTROL_BODY_CAP_BYTES));
|
|
74728
|
-
const events = body?.events;
|
|
74729
|
-
if (!Array.isArray(events) || events.length === 0 || events.length > RC_SELF_HOST_MAX_BATCH_EVENTS) {
|
|
74730
|
-
await answerJson2(response, HTTP_STATUS.badRequest, { type: "error", error: { type: "invalid_request_error", message: `a client event write is a non-empty array of at most ${String(RC_SELF_HOST_MAX_BATCH_EVENTS)} events` } });
|
|
74731
|
-
return;
|
|
74732
|
-
}
|
|
74733
|
-
const results = [];
|
|
74734
|
-
for (const event of events) {
|
|
74735
|
-
if (!isRecord5(event) || !isRecord5(event.payload)) {
|
|
74736
|
-
await answerJson2(response, HTTP_STATUS.badRequest, { type: "error", error: { type: "invalid_request_error", message: "every event in a write is an object carrying one payload object" } });
|
|
74737
|
-
return;
|
|
74738
|
-
}
|
|
74739
|
-
const published = publish(session, RC_SOURCE_CLIENT, event.payload);
|
|
74740
|
-
results.push({ sequence_num: String(published.sequenceNum), duplicate: published.duplicate });
|
|
74741
|
-
}
|
|
74742
|
-
sweep();
|
|
74743
|
-
await answerJson2(response, HTTP_STATUS.ok, { results });
|
|
74744
|
-
return;
|
|
74745
|
-
}
|
|
74746
|
-
if (tail === "events" && method === "GET") {
|
|
74747
|
-
await answerJson2(response, HTTP_STATUS.ok, { data: session.events.map((event) => ({ event_id: event.eventId, event_type: nonEmptyString2(event.payload, "type") ?? "event", sequence_num: event.sequenceNum, source: event.source, payload: event.payload, created_at: event.createdAt })) });
|
|
74748
|
-
return;
|
|
74749
|
-
}
|
|
74750
|
-
if (tail === "events/stream" && method === "GET") {
|
|
74751
|
-
await holdStream(session, "clientStreams", () => true, request7, response);
|
|
74752
|
-
return;
|
|
74753
|
-
}
|
|
74754
|
-
if (tail === "client/presence" && method === "POST") {
|
|
74755
|
-
await readBody3(request7.body, CONTROL_BODY_CAP_BYTES);
|
|
74756
|
-
await answerJson2(response, HTTP_STATUS.ok, { refresh_after_seconds: RC_PRESENCE_REFRESH_SECONDS });
|
|
74757
|
-
return;
|
|
74758
|
-
}
|
|
74759
|
-
if (tail === "mark_read" && method === "POST") {
|
|
74760
|
-
await readBody3(request7.body, CONTROL_BODY_CAP_BYTES);
|
|
74761
|
-
await answerJson2(response, HTTP_STATUS.ok, {});
|
|
74762
|
-
return;
|
|
74763
|
-
}
|
|
74764
|
-
if (tail === "teleport-events" && method === "POST") {
|
|
74765
|
-
await readBody3(request7.body, CONTROL_BODY_CAP_BYTES);
|
|
74766
|
-
await answerJson2(response, HTTP_STATUS.ok, {});
|
|
74767
|
-
return;
|
|
74768
|
-
}
|
|
74769
|
-
await answerJson2(response, HTTP_STATUS.notFound, { type: "error", error: { type: "api_error", message: `the self-hosted Remote Control surface serves no path "${tail}"` } });
|
|
74770
|
-
};
|
|
74771
|
-
const recordMissing = () => deps.credentialRecord() === void 0;
|
|
74772
|
-
const route = {
|
|
74773
|
-
name: "rc-selfhost",
|
|
74774
|
-
headroomEligible: false,
|
|
74775
|
-
headroomUpstream: void 0,
|
|
74776
|
-
serve: async (request7, response) => {
|
|
74777
|
-
const method = request7.method.toUpperCase();
|
|
74778
|
-
const pathname = new URL(request7.url, "http://127.0.0.1").pathname;
|
|
74779
|
-
if (recordMissing()) {
|
|
74780
|
-
await noCredential(response);
|
|
74781
|
-
return;
|
|
74782
|
-
}
|
|
74783
|
-
if (pathname === RC_COMPAT_SESSIONS_PATH) {
|
|
74784
|
-
if (method !== "GET") {
|
|
74785
|
-
await answerJson2(response, HTTP_STATUS.methodNotAllowed, { type: "error", error: { type: "api_error", message: "the compatibility session list is a GET" } });
|
|
74786
|
-
return;
|
|
74787
|
-
}
|
|
74788
|
-
const listRefusal = credentialRefusal(request7.headers);
|
|
74789
|
-
if (listRefusal !== void 0) {
|
|
74790
|
-
await unauthorized(response, listRefusal);
|
|
74791
|
-
return;
|
|
74792
|
-
}
|
|
74793
|
-
await answerJson2(response, HTTP_STATUS.ok, { data: [] });
|
|
74794
|
-
return;
|
|
74795
|
-
}
|
|
74796
|
-
if (pathname !== RC_SESSIONS_PATH_PREFIX2 && !pathname.startsWith(`${RC_SESSIONS_PATH_PREFIX2}/`)) {
|
|
74797
|
-
await answerJson2(response, HTTP_STATUS.notFound, { type: "error", error: { type: "api_error", message: `the self-hosted Remote Control surface serves no path "${pathname}"` } });
|
|
74798
|
-
return;
|
|
74799
|
-
}
|
|
74800
|
-
if (pathname === RC_SESSIONS_PATH_PREFIX2) {
|
|
74801
|
-
if (method === "POST") {
|
|
74802
|
-
const createRefusal = credentialRefusal(request7.headers);
|
|
74803
|
-
if (createRefusal !== void 0) {
|
|
74804
|
-
await unauthorized(response, createRefusal);
|
|
74805
|
-
return;
|
|
74806
|
-
}
|
|
74807
|
-
const body = parseJsonObject2(await readBody3(request7.body, CONTROL_BODY_CAP_BYTES));
|
|
74808
|
-
const session = {
|
|
74809
|
-
id: `${RC_SESSION_ID_PREFIX2}${deps.newUuid()}`,
|
|
74810
|
-
createdAt: deps.now(),
|
|
74811
|
-
title: nonEmptyString2(body, "title"),
|
|
74812
|
-
workerJwts: /* @__PURE__ */ new Set(),
|
|
74813
|
-
sequenceNum: 0,
|
|
74814
|
-
events: [],
|
|
74815
|
-
seenPayloadUuids: /* @__PURE__ */ new Set(),
|
|
74816
|
-
workerStreams: /* @__PURE__ */ new Set(),
|
|
74817
|
-
clientStreams: /* @__PURE__ */ new Set(),
|
|
74818
|
-
internalEvents: [],
|
|
74819
|
-
workerStatus: void 0,
|
|
74820
|
-
externalMetadata: void 0,
|
|
74821
|
-
lastTrafficAt: deps.now()
|
|
74822
|
-
};
|
|
74823
|
-
sessions.set(session.id, session);
|
|
74824
|
-
deps.log?.(`rc selfhost ${session.id}: session created on the door's own surface`);
|
|
74825
|
-
await answerJson2(response, HTTP_STATUS.ok, { session: { id: session.id, title: session.title, created_at: new Date(session.createdAt).toISOString(), status: "active" } });
|
|
74826
|
-
return;
|
|
74827
|
-
}
|
|
74828
|
-
if (method === "GET") {
|
|
74829
|
-
const readRefusal = credentialRefusal(request7.headers);
|
|
74830
|
-
if (readRefusal !== void 0) {
|
|
74831
|
-
await unauthorized(response, readRefusal);
|
|
74832
|
-
return;
|
|
74833
|
-
}
|
|
74834
|
-
await answerJson2(response, HTTP_STATUS.ok, { data: [...sessions.values()].map((session) => ({ id: session.id, title: session.title, created_at: new Date(session.createdAt).toISOString(), updated_at: new Date(session.lastTrafficAt).toISOString(), session_status: "active" })) });
|
|
74835
|
-
return;
|
|
74836
|
-
}
|
|
74837
|
-
await answerJson2(response, HTTP_STATUS.methodNotAllowed, { type: "error", error: { type: "api_error", message: "the session collection is a POST (create) and a GET (list)" } });
|
|
74838
|
-
return;
|
|
74839
|
-
}
|
|
74840
|
-
const rest = pathname.slice(`${RC_SESSIONS_PATH_PREFIX2}/`.length);
|
|
74841
|
-
const slash = rest.indexOf("/");
|
|
74842
|
-
const id = slash === -1 ? rest : rest.slice(0, slash);
|
|
74843
|
-
const tail = slash === -1 ? "" : rest.slice(slash + 1);
|
|
74844
|
-
if (!id.startsWith(RC_SESSION_ID_PREFIX2)) {
|
|
74845
|
-
await answerJson2(response, HTTP_STATUS.notFound, { type: "error", error: { type: "api_error", message: "the session family names one cse_-prefixed session id" } });
|
|
74846
|
-
return;
|
|
74847
|
-
}
|
|
74848
|
-
await serveSessionPath(sessions.get(id), tail, request7, response);
|
|
74849
|
-
}
|
|
74850
|
-
};
|
|
74851
|
-
const answerServerJson = (response, status, body) => {
|
|
74852
|
-
const text2 = JSON.stringify(body);
|
|
74853
|
-
response.writeHead(status, { "content-type": "application/json", "content-length": String(Buffer.byteLength(text2, "utf8")) });
|
|
74854
|
-
response.end(text2);
|
|
74855
|
-
};
|
|
74856
|
-
const local = {
|
|
74857
|
-
parsesHost: (host) => host === RC_SELF_HOST_OAUTH_HOST,
|
|
74858
|
-
serve: async (host, request7, response) => {
|
|
74859
|
-
const method = (request7.method ?? "").toUpperCase();
|
|
74860
|
-
const pathname = new URL(request7.url ?? "/", "http://127.0.0.1").pathname;
|
|
74861
|
-
if (host === RC_SELF_HOST_OAUTH_HOST) {
|
|
74862
|
-
if (method === "POST" && pathname === "/v1/oauth/token") {
|
|
74863
|
-
const body = parseJsonObject2(await readBody3(request7, CONTROL_BODY_CAP_BYTES));
|
|
74864
|
-
const record2 = deps.credentialRecord();
|
|
74865
|
-
if (record2 === void 0) {
|
|
74866
|
-
answerServerJson(response, HTTP_SERVICE_UNAVAILABLE, { error: "the front door's self-hosted Remote Control surface is on but no credential has been minted for it" });
|
|
74867
|
-
return true;
|
|
74868
|
-
}
|
|
74869
|
-
if (body?.grant_type !== void 0 && body.grant_type !== "refresh_token") {
|
|
74870
|
-
answerServerJson(response, HTTP_STATUS.badRequest, { error: "unsupported_grant_type" });
|
|
74871
|
-
return true;
|
|
74872
|
-
}
|
|
74873
|
-
answerServerJson(response, HTTP_STATUS.ok, { access_token: record2.accessToken, refresh_token: record2.refreshToken, expires_in: RC_SELF_HOST_OAUTH_TOKEN_TTL_SECONDS, scope: RC_SELF_HOST_SCOPE_LIST.join(" ") });
|
|
74874
|
-
return true;
|
|
74611
|
+
answerServerJson(response, HTTP_STATUS.ok, { access_token: record2.accessToken, refresh_token: record2.refreshToken, expires_in: RC_SELF_HOST_OAUTH_TOKEN_TTL_SECONDS, scope: RC_SELF_HOST_SCOPE_LIST.join(" ") });
|
|
74612
|
+
return true;
|
|
74875
74613
|
}
|
|
74876
74614
|
if (method === "GET" && pathname === "/v1/oauth/hello") {
|
|
74877
74615
|
answerServerJson(response, HTTP_STATUS.ok, {});
|
|
@@ -74938,95 +74676,960 @@ data: ${JSON.stringify(envelope)}
|
|
|
74938
74676
|
};
|
|
74939
74677
|
}
|
|
74940
74678
|
|
|
74941
|
-
// src/frontdoor/rcSelfHostMint.ts
|
|
74942
|
-
var
|
|
74943
|
-
var
|
|
74944
|
-
var
|
|
74945
|
-
var
|
|
74946
|
-
var
|
|
74947
|
-
var
|
|
74948
|
-
|
|
74949
|
-
|
|
74950
|
-
|
|
74679
|
+
// src/frontdoor/rcSelfHostMint.ts
|
|
74680
|
+
var CREDENTIALS_FILE = ".credentials.json";
|
|
74681
|
+
var CLAUDE_JSON2 = ".claude.json";
|
|
74682
|
+
var RC_SELF_HOST_RECORD_DIR = "rc-selfhost";
|
|
74683
|
+
var RC_SELF_HOST_RECORD_FILE = "credential.json";
|
|
74684
|
+
var SELF_HOST_EMAIL = "selfhost@agent-shim.invalid";
|
|
74685
|
+
var SELF_HOST_SUBSCRIPTION_TYPE = "max";
|
|
74686
|
+
function isRecord5(value2) {
|
|
74687
|
+
return typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
|
|
74688
|
+
}
|
|
74689
|
+
function readJsonObject(fs20, file2) {
|
|
74690
|
+
const raw = fs20.readFileUtf8(file2);
|
|
74691
|
+
if (raw === void 0) {
|
|
74692
|
+
return void 0;
|
|
74693
|
+
}
|
|
74694
|
+
let parsed;
|
|
74695
|
+
try {
|
|
74696
|
+
parsed = JSON.parse(raw);
|
|
74697
|
+
} catch {
|
|
74698
|
+
return void 0;
|
|
74699
|
+
}
|
|
74700
|
+
return isRecord5(parsed) ? parsed : void 0;
|
|
74701
|
+
}
|
|
74702
|
+
function readRcSelfHostRecord(fs20, frontdoorDir) {
|
|
74703
|
+
const parsed = readJsonObject(fs20, import_node_path34.default.join(frontdoorDir, RC_SELF_HOST_RECORD_DIR, RC_SELF_HOST_RECORD_FILE));
|
|
74704
|
+
if (parsed === void 0) {
|
|
74705
|
+
return void 0;
|
|
74706
|
+
}
|
|
74707
|
+
const { accessToken, refreshToken, organizationUuid, accountUuid } = parsed;
|
|
74708
|
+
if (typeof accessToken !== "string" || accessToken === "" || typeof refreshToken !== "string" || refreshToken === "" || typeof organizationUuid !== "string" || organizationUuid === "" || typeof accountUuid !== "string" || accountUuid === "") {
|
|
74709
|
+
return void 0;
|
|
74710
|
+
}
|
|
74711
|
+
return { accessToken, refreshToken, organizationUuid, accountUuid };
|
|
74712
|
+
}
|
|
74713
|
+
function mintRcSelfHostCredential(deps) {
|
|
74714
|
+
if (!isIdentityName(deps.identity)) {
|
|
74715
|
+
throw new Error(`"${deps.identity}" is not a valid identity name.`);
|
|
74716
|
+
}
|
|
74717
|
+
const identityDir = import_node_path34.default.join(deps.identitiesDir, deps.identity);
|
|
74718
|
+
const credentialsFile = import_node_path34.default.join(identityDir, CREDENTIALS_FILE);
|
|
74719
|
+
const claudeJsonFile = import_node_path34.default.join(identityDir, CLAUDE_JSON2);
|
|
74720
|
+
const recordDir = import_node_path34.default.join(deps.frontdoorDir, RC_SELF_HOST_RECORD_DIR);
|
|
74721
|
+
const recordFile = import_node_path34.default.join(recordDir, RC_SELF_HOST_RECORD_FILE);
|
|
74722
|
+
const previousRecord = readRcSelfHostRecord(deps.fs, deps.frontdoorDir);
|
|
74723
|
+
const existingCredentials = readJsonObject(deps.fs, credentialsFile);
|
|
74724
|
+
const existingOauth = isRecord5(existingCredentials?.claudeAiOauth) ? existingCredentials.claudeAiOauth : void 0;
|
|
74725
|
+
const existingToken = typeof existingOauth?.accessToken === "string" && existingOauth.accessToken !== "" ? existingOauth.accessToken : void 0;
|
|
74726
|
+
const existingIsOwn = existingToken !== void 0 && previousRecord?.accessToken === existingToken;
|
|
74727
|
+
if (existingToken !== void 0 && !existingIsOwn && !deps.force) {
|
|
74728
|
+
throw new Error(
|
|
74729
|
+
`identity ${deps.identity} already holds an OAuth credential this door did not mint (a real claude.ai login): pass --force to replace it, or mint into an identity with no login. The self-hosted mode expects no Anthropic credential to be in play.`
|
|
74730
|
+
);
|
|
74731
|
+
}
|
|
74732
|
+
const accessToken = `sk-ant-oat${deps.randomToken()}`;
|
|
74733
|
+
const refreshToken = deps.randomToken();
|
|
74734
|
+
const organizationUuid = deps.newUuid();
|
|
74735
|
+
const accountUuid = deps.newUuid();
|
|
74736
|
+
const mintedAt = new Date(deps.now()).toISOString();
|
|
74737
|
+
deps.fs.mkdirPrivate(import_node_path34.default.dirname(credentialsFile));
|
|
74738
|
+
deps.fs.writeFilePrivate(credentialsFile, `${JSON.stringify({ ...existingCredentials, claudeAiOauth: { accessToken, refreshToken, expiresAt: null, scopes: [...RC_SELF_HOST_SCOPE_LIST], subscriptionType: SELF_HOST_SUBSCRIPTION_TYPE, rateLimitTier: null } }, null, 2)}
|
|
74739
|
+
`);
|
|
74740
|
+
const claudeJson = readJsonObject(deps.fs, claudeJsonFile) ?? {};
|
|
74741
|
+
const existingBlock = (value2) => isRecord5(value2) ? value2 : {};
|
|
74742
|
+
deps.fs.writeFilePrivate(claudeJsonFile, `${JSON.stringify({
|
|
74743
|
+
...claudeJson,
|
|
74744
|
+
oauthAccount: {
|
|
74745
|
+
...existingBlock(claudeJson.oauthAccount),
|
|
74746
|
+
accountUuid,
|
|
74747
|
+
emailAddress: SELF_HOST_EMAIL,
|
|
74748
|
+
organizationUuid,
|
|
74749
|
+
organizationName: "agent-shim self-hosted",
|
|
74750
|
+
// The billing and creation fields exist so the CLI's "populate the account block if it is missing" step finds a complete block and skips its profile fetch; the door answers that fetch locally anyway, so these are the quiet path, not a load-bearing claim.
|
|
74751
|
+
billingType: "stripe_subscription",
|
|
74752
|
+
accountCreatedAt: mintedAt,
|
|
74753
|
+
subscriptionCreatedAt: mintedAt
|
|
74754
|
+
},
|
|
74755
|
+
cachedGrowthBookFeatures: {
|
|
74756
|
+
...existingBlock(claudeJson.cachedGrowthBookFeatures),
|
|
74757
|
+
tengu_ccr_bridge: true,
|
|
74758
|
+
tengu_bridge_repl_v2: true,
|
|
74759
|
+
tengu_bridge_repl_v2_cse_shim_enabled: true
|
|
74760
|
+
}
|
|
74761
|
+
}, null, 2)}
|
|
74762
|
+
`);
|
|
74763
|
+
deps.fs.mkdirPrivate(recordDir);
|
|
74764
|
+
deps.fs.writeFilePrivate(recordFile, `${JSON.stringify({ accessToken, refreshToken, organizationUuid, accountUuid }, null, 2)}
|
|
74765
|
+
`);
|
|
74766
|
+
return { identity: deps.identity, organizationUuid, credentialsFile, claudeJsonFile, recordFile, replaced: existingIsOwn };
|
|
74767
|
+
}
|
|
74768
|
+
|
|
74769
|
+
// src/frontdoor/rcCommands.ts
|
|
74770
|
+
var MINTED_TOKEN_RANDOM_BYTES = 32;
|
|
74771
|
+
function reportControlWrite(what, session, result, json2, extra = {}) {
|
|
74772
|
+
if (!result.ok) {
|
|
74773
|
+
throw new Error(result.message);
|
|
74774
|
+
}
|
|
74775
|
+
if (json2 === true) {
|
|
74776
|
+
printJson({ session, ...result.requestId === void 0 ? {} : { request: result.requestId }, sequenceNums: result.sequenceNums, ...extra });
|
|
74777
|
+
return;
|
|
74778
|
+
}
|
|
74779
|
+
console.log(`${what} on ${session}${result.requestId === void 0 ? "" : `: request ${result.requestId}`} sequence_num ${result.sequenceNums.join(", ")}`);
|
|
74780
|
+
}
|
|
74781
|
+
function formatRcSessionList(sessions) {
|
|
74782
|
+
if (sessions.length === 0) {
|
|
74783
|
+
return ["no Remote Control sessions observed"];
|
|
74784
|
+
}
|
|
74785
|
+
return sessions.map((session) => `${session.id} created ${new Date(session.createdAt).toISOString()} last seen ${new Date(session.lastSeenAt).toISOString()}`);
|
|
74786
|
+
}
|
|
74787
|
+
function formatRcSessionStatus(statuses) {
|
|
74788
|
+
if (statuses.length === 0) {
|
|
74789
|
+
return ["no Remote Control sessions observed"];
|
|
74790
|
+
}
|
|
74791
|
+
const lines = [];
|
|
74792
|
+
for (const status of statuses) {
|
|
74793
|
+
lines.push(`${status.id} created ${new Date(status.createdAt).toISOString()} last seen ${new Date(status.lastSeenAt).toISOString()}`);
|
|
74794
|
+
const worker = status.workerState === void 0 && status.workerIdleSeconds === void 0 ? "not observed" : [
|
|
74795
|
+
status.workerState === void 0 ? "state unknown" : `state ${status.workerState.value} since ${new Date(status.workerState.observedAt).toISOString()}`,
|
|
74796
|
+
status.workerIdleSeconds === void 0 ? "idle unknown" : `idle ${String(status.workerIdleSeconds.value)} s as of ${new Date(status.workerIdleSeconds.observedAt).toISOString()}`
|
|
74797
|
+
].join(", ");
|
|
74798
|
+
lines.push(` worker: ${worker}`);
|
|
74799
|
+
if (status.pending.length === 0) {
|
|
74800
|
+
lines.push(" pending: none");
|
|
74801
|
+
} else {
|
|
74802
|
+
lines.push(" pending:");
|
|
74803
|
+
for (const pending of status.pending) {
|
|
74804
|
+
lines.push(...formatRcPendingList([pending]).map((line) => ` ${line}`));
|
|
74805
|
+
}
|
|
74806
|
+
}
|
|
74807
|
+
}
|
|
74808
|
+
return lines;
|
|
74809
|
+
}
|
|
74810
|
+
function formatRcPendingList(pending) {
|
|
74811
|
+
if (pending.length === 0) {
|
|
74812
|
+
return ["no pending control requests observed"];
|
|
74813
|
+
}
|
|
74814
|
+
return pending.map((request7) => `${request7.sessionId} ${request7.requestId} ${request7.type}${request7.summary === "" ? "" : ` ${request7.summary}`} observed ${new Date(request7.observedAt).toISOString()}`);
|
|
74815
|
+
}
|
|
74816
|
+
function eventExcerpt(text2) {
|
|
74817
|
+
return text2.length <= RC_PENDING_SUMMARY_EXCERPT_CHARS ? text2 : `${text2.slice(0, RC_PENDING_SUMMARY_EXCERPT_CHARS)}...`;
|
|
74818
|
+
}
|
|
74819
|
+
function formatRcStreamEvent(event) {
|
|
74820
|
+
const { envelope } = event;
|
|
74821
|
+
const sketch = envelope.payload === void 0 ? "" : ` ${eventExcerpt(JSON.stringify(envelope.payload))}`;
|
|
74822
|
+
return `${event.session} ${envelope.event_type} sequence_num ${String(envelope.sequence_num)} source ${envelope.source}${sketch}`;
|
|
74823
|
+
}
|
|
74824
|
+
function formatRcSelfHostMint(result) {
|
|
74825
|
+
return [
|
|
74826
|
+
`identity: ${result.identity} (organisation ${result.organizationUuid})`,
|
|
74827
|
+
`credential: ${result.credentialsFile}${result.replaced ? " (replacing this door's previous mint)" : ""}`,
|
|
74828
|
+
`account block: ${result.claudeJsonFile} (oauthAccount and the feature-cache seed merged in)`,
|
|
74829
|
+
`door record: ${result.recordFile} (the copy the door's served surface authenticates against)`,
|
|
74830
|
+
"next: start the door with AGENT_SHIM_FRONTDOOR_RC_SELF_HOST=1 and its transparent surface pointed at the interception port, then launch this identity and switch Remote Control on; no claude.ai login is involved"
|
|
74831
|
+
];
|
|
74832
|
+
}
|
|
74833
|
+
function frontDoorRcControlFromState(fsPort, paths) {
|
|
74834
|
+
const state = readFrontDoorState(fsPort, paths.frontdoorStateFile);
|
|
74835
|
+
if (state?.port === void 0) {
|
|
74836
|
+
throw new Error("the front door is not serving: Remote Control sessions are observed only while it runs, so start a session through the door first");
|
|
74837
|
+
}
|
|
74838
|
+
const ca = fsPort.readFileUtf8(paths.frontdoorCaCertFile);
|
|
74839
|
+
if (ca === void 0) {
|
|
74840
|
+
throw new Error(`the front door's CA certificate is missing at ${paths.frontdoorCaCertFile}, so its control listener cannot be authenticated`);
|
|
74841
|
+
}
|
|
74842
|
+
const token = fsPort.readFileUtf8(paths.frontdoorControlTokenFile)?.trim();
|
|
74843
|
+
if (token === void 0 || token === "") {
|
|
74844
|
+
throw new Error(`the serving front door's control token is missing at ${paths.frontdoorControlTokenFile}`);
|
|
74845
|
+
}
|
|
74846
|
+
return frontDoorRcControl(realRcControlTransport(state.port, ca), token);
|
|
74847
|
+
}
|
|
74848
|
+
function frontDoorRcApiFromState(fsPort, paths) {
|
|
74849
|
+
const state = readFrontDoorState(fsPort, paths.frontdoorStateFile);
|
|
74850
|
+
if (state?.port === void 0) {
|
|
74851
|
+
throw new Error("the front door is not serving: Remote Control sessions are observed only while it runs, so start a session through the door first");
|
|
74852
|
+
}
|
|
74853
|
+
const ca = fsPort.readFileUtf8(paths.frontdoorCaCertFile);
|
|
74854
|
+
if (ca === void 0) {
|
|
74855
|
+
throw new Error(`the front door's CA certificate is missing at ${paths.frontdoorCaCertFile}, so its typed API cannot be authenticated`);
|
|
74856
|
+
}
|
|
74857
|
+
const token = fsPort.readFileUtf8(paths.frontdoorControlTokenFile)?.trim();
|
|
74858
|
+
if (token === void 0 || token === "") {
|
|
74859
|
+
throw new Error(`the serving front door's control token is missing at ${paths.frontdoorControlTokenFile}`);
|
|
74860
|
+
}
|
|
74861
|
+
return frontDoorRcApiClient(state.port, ca, token);
|
|
74862
|
+
}
|
|
74863
|
+
function registerRcCommand(frontdoor, paths) {
|
|
74864
|
+
const rc = withExamples(
|
|
74865
|
+
frontdoor.command("rc").description("Observe the Remote Control sessions passing through the front door, watch the stream their attached client receives, send a prompt into one, answer its pending control requests, and steer one with the client half's own control requests (interrupt, model, permission mode, end-session, the usage and context queries, read-file, file-suggestions, keep-alive, and the mcp_* family)."),
|
|
74866
|
+
["agent-shim frontdoor rc list", "agent-shim frontdoor rc pending"]
|
|
74867
|
+
);
|
|
74868
|
+
withExamples(
|
|
74869
|
+
rc.command("list").description("List the Remote Control sessions the front door has observed, with when each was created and last seen. Read-only.").option("--json", "Print the sessions as JSON.").action(async (options) => {
|
|
74870
|
+
const sessions = await frontDoorRcControlFromState(realFarmFs, paths).listSessions();
|
|
74871
|
+
if (options.json === true) {
|
|
74872
|
+
printJson({ sessions });
|
|
74873
|
+
return;
|
|
74874
|
+
}
|
|
74875
|
+
for (const line of formatRcSessionList(sessions)) {
|
|
74876
|
+
console.log(line);
|
|
74877
|
+
}
|
|
74878
|
+
}),
|
|
74879
|
+
["agent-shim frontdoor rc list", "agent-shim frontdoor rc list --json"]
|
|
74880
|
+
);
|
|
74881
|
+
withExamples(
|
|
74882
|
+
rc.command("status").description("Report each observed Remote Control session's worker state and idle from its latest observed heartbeat and registration, with its pending control requests. Read-only.").argument("[session]", "One cse_ session id, as `frontdoor rc list` shows it; every observed session when omitted.").option("--json", "Print the statuses as JSON.").action(async (session, options) => {
|
|
74883
|
+
const statuses = await frontDoorRcControlFromState(realFarmFs, paths).statusOf(session);
|
|
74884
|
+
if (options.json === true) {
|
|
74885
|
+
printJson({ statuses });
|
|
74886
|
+
return;
|
|
74887
|
+
}
|
|
74888
|
+
for (const line of formatRcSessionStatus(statuses)) {
|
|
74889
|
+
console.log(line);
|
|
74890
|
+
}
|
|
74891
|
+
}),
|
|
74892
|
+
["agent-shim frontdoor rc status", "agent-shim frontdoor rc status cse_00000000-0000-4000-8000-000000000000 --json"]
|
|
74893
|
+
);
|
|
74894
|
+
withExamples(
|
|
74895
|
+
rc.command("pending").description("List the control requests (tool and plan approvals above all) the front door has observed and that are still awaiting an answer. Read-only.").argument("[session]", "One cse_ session id, as `frontdoor rc list` shows it; every observed session when omitted.").option("--json", "Print the pending requests as JSON.").action(async (session, options) => {
|
|
74896
|
+
const pending = await frontDoorRcControlFromState(realFarmFs, paths).pendingOf(session);
|
|
74897
|
+
if (options.json === true) {
|
|
74898
|
+
printJson({ pending });
|
|
74899
|
+
return;
|
|
74900
|
+
}
|
|
74901
|
+
for (const line of formatRcPendingList(pending)) {
|
|
74902
|
+
console.log(line);
|
|
74903
|
+
}
|
|
74904
|
+
}),
|
|
74905
|
+
["agent-shim frontdoor rc pending", "agent-shim frontdoor rc pending --json"]
|
|
74906
|
+
);
|
|
74907
|
+
withExamples(
|
|
74908
|
+
rc.command("send").description("Send a text prompt into one observed Remote Control session, arriving as a message from an attached client would.").requiredOption("--session <id>", "The cse_ session id, as `frontdoor rc list` shows it.").requiredOption("--text <text>", "The prompt text to deliver.").option("--json", "Print the delivery result as JSON.").action(async (options) => {
|
|
74909
|
+
const result = await frontDoorRcControlFromState(realFarmFs, paths).sendPrompt(options.session, options.text);
|
|
74910
|
+
if (!result.ok) {
|
|
74911
|
+
throw new Error(result.message);
|
|
74912
|
+
}
|
|
74913
|
+
if (options.json === true) {
|
|
74914
|
+
printJson({ session: options.session, sequenceNums: result.sequenceNums });
|
|
74915
|
+
return;
|
|
74916
|
+
}
|
|
74917
|
+
console.log(`sent to ${options.session}: sequence_num ${result.sequenceNums.join(", ")}`);
|
|
74918
|
+
}),
|
|
74919
|
+
['agent-shim frontdoor rc send --session cse_00000000-0000-4000-8000-000000000000 --text "run the tests"']
|
|
74920
|
+
);
|
|
74921
|
+
withExamples(
|
|
74922
|
+
rc.command("answer").description("Answer one pending control request on an observed session, approving or denying it as an attached client's approval would.").requiredOption("--session <id>", "The cse_ session id, as `frontdoor rc list` shows it.").requiredOption("--request <id>", "The control request's id, as `frontdoor rc pending` shows it.").option("--approve", "Approve the request.").option("--deny", "Deny the request.").option("--text <text>", "The denial message the worker sees; applies to --deny only.").option("--json", "Print the delivery result as JSON.").action(async (options) => {
|
|
74923
|
+
if (options.approve === options.deny) {
|
|
74924
|
+
throw new UsageError("answer exactly one way: pass --approve or --deny, not both and not neither");
|
|
74925
|
+
}
|
|
74926
|
+
if (options.approve === true && options.text !== void 0) {
|
|
74927
|
+
throw new UsageError("--text is the denial message; an approval carries no text (the protocol's allow result has no message field)");
|
|
74928
|
+
}
|
|
74929
|
+
const result = await frontDoorRcControlFromState(realFarmFs, paths).answerRequest(options.session, options.request, { approve: options.approve === true, message: options.deny === true ? options.text : void 0 });
|
|
74930
|
+
if (!result.ok) {
|
|
74931
|
+
throw new Error(result.message);
|
|
74932
|
+
}
|
|
74933
|
+
if (options.json === true) {
|
|
74934
|
+
printJson({ session: options.session, request: options.request, sequenceNums: result.sequenceNums });
|
|
74935
|
+
return;
|
|
74936
|
+
}
|
|
74937
|
+
console.log(`${options.approve === true ? "approved" : "denied"} ${options.request} on ${options.session}: sequence_num ${result.sequenceNums.join(", ")}`);
|
|
74938
|
+
}),
|
|
74939
|
+
[
|
|
74940
|
+
"agent-shim frontdoor rc answer --session cse_00000000-0000-4000-8000-000000000000 --request req_00000000-0000-4000-8000-000000000000 --approve",
|
|
74941
|
+
'agent-shim frontdoor rc answer --session cse_00000000-0000-4000-8000-000000000000 --request req_00000000-0000-4000-8000-000000000000 --deny --text "not today"'
|
|
74942
|
+
]
|
|
74943
|
+
);
|
|
74944
|
+
withExamples(
|
|
74945
|
+
rc.command("interrupt").description("Interrupt one observed Remote Control session's running turn, as an attached client's stop button would. Queued commands survive the interrupt.").requiredOption("--session <id>", "The cse_ session id, as `frontdoor rc list` shows it.").option("--json", "Print the delivery result as JSON.").action(async (options) => {
|
|
74946
|
+
reportControlWrite("interrupted", options.session, await frontDoorRcControlFromState(realFarmFs, paths).interruptSession(options.session), options.json);
|
|
74947
|
+
}),
|
|
74948
|
+
["agent-shim frontdoor rc interrupt --session cse_00000000-0000-4000-8000-000000000000"]
|
|
74949
|
+
);
|
|
74950
|
+
withExamples(
|
|
74951
|
+
rc.command("set-model").description("Set the model one observed Remote Control session's subsequent turns use, as an attached client's model picker would.").requiredOption("--session <id>", "The cse_ session id, as `frontdoor rc list` shows it.").requiredOption("--model <model>", "The model id to switch the session to.").option("--json", "Print the delivery result as JSON.").action(async (options) => {
|
|
74952
|
+
reportControlWrite(`set model ${options.model}`, options.session, await frontDoorRcControlFromState(realFarmFs, paths).setModel(options.session, options.model), options.json, { model: options.model });
|
|
74953
|
+
}),
|
|
74954
|
+
["agent-shim frontdoor rc set-model --session cse_00000000-0000-4000-8000-000000000000 --model claude-opus-5-5"]
|
|
74955
|
+
);
|
|
74956
|
+
withExamples(
|
|
74957
|
+
rc.command("set-permission-mode").description("Set one observed Remote Control session's permission mode, as an attached client's mode switcher would.").requiredOption("--session <id>", "The cse_ session id, as `frontdoor rc list` shows it.").requiredOption("--mode <mode>", `The permission mode to set; one of the SDK's own modes (${RC_PERMISSION_MODES.join(", ")}).`).option("--json", "Print the delivery result as JSON.").action(async (options) => {
|
|
74958
|
+
if (!isRcPermissionMode(options.mode)) {
|
|
74959
|
+
throw new UsageError(`--mode must be one of the SDK's own permission modes (${RC_PERMISSION_MODES.join(", ")}), not "${options.mode}"`);
|
|
74960
|
+
}
|
|
74961
|
+
reportControlWrite(`set permission mode ${options.mode}`, options.session, await frontDoorRcControlFromState(realFarmFs, paths).setPermissionMode(options.session, options.mode), options.json, { mode: options.mode });
|
|
74962
|
+
}),
|
|
74963
|
+
[
|
|
74964
|
+
"agent-shim frontdoor rc set-permission-mode --session cse_00000000-0000-4000-8000-000000000000 --mode plan",
|
|
74965
|
+
"agent-shim frontdoor rc set-permission-mode --session cse_00000000-0000-4000-8000-000000000000 --mode acceptEdits"
|
|
74966
|
+
]
|
|
74967
|
+
);
|
|
74968
|
+
withExamples(
|
|
74969
|
+
rc.command("end-session").description("End one observed Remote Control session, as an attached client closing it would: the worker aborts its turn and shuts down on receipt of the request.").requiredOption("--session <id>", "The cse_ session id, as `frontdoor rc list` shows it.").option("--reason <text>", "The reason the worker's own log names; omitted means the protocol's unspecified form.").option("--json", "Print the delivery result as JSON.").action(async (options) => {
|
|
74970
|
+
reportControlWrite("ended", options.session, await frontDoorRcControlFromState(realFarmFs, paths).endSession(options.session, options.reason), options.json);
|
|
74971
|
+
}),
|
|
74972
|
+
["agent-shim frontdoor rc end-session --session cse_00000000-0000-4000-8000-000000000000 --reason done for today"]
|
|
74973
|
+
);
|
|
74974
|
+
withExamples(
|
|
74975
|
+
rc.command("get-usage").description("Ask one observed session's worker for its structured usage (session totals plus the plan's rate limits). The worker's answer arrives on `frontdoor rc watch` as the control_response echoing the printed request id.").requiredOption("--session <id>", "The cse_ session id, as `frontdoor rc list` shows it.").option("--skip-behaviors", "Skip the worker's local-transcript scan that fills the answer's behaviours section.").option("--json", "Print the delivery result as JSON.").action(async (options) => {
|
|
74976
|
+
reportControlWrite("requested usage", options.session, await frontDoorRcControlFromState(realFarmFs, paths).getUsage(options.session, options.skipBehaviors === true ? true : void 0), options.json);
|
|
74977
|
+
}),
|
|
74978
|
+
["agent-shim frontdoor rc get-usage --session cse_00000000-0000-4000-8000-000000000000 --skip-behaviors"]
|
|
74979
|
+
);
|
|
74980
|
+
withExamples(
|
|
74981
|
+
rc.command("get-context-usage").description("Ask one observed session's worker for its context-window breakdown by category. The worker's answer arrives on `frontdoor rc watch` as the control_response echoing the printed request id.").requiredOption("--session <id>", "The cse_ session id, as `frontdoor rc list` shows it.").option("--detail <level>", `The detail level; one of the SDK's own levels (${RC_CONTEXT_USAGE_DETAILS.join(", ")}); the worker defaults to full.`).option("--json", "Print the delivery result as JSON.").action(async (options) => {
|
|
74982
|
+
if (options.detail !== void 0 && !isRcContextUsageDetail(options.detail)) {
|
|
74983
|
+
throw new UsageError(`--detail must be one of the SDK's own context-usage levels (${RC_CONTEXT_USAGE_DETAILS.join(", ")}), not "${options.detail}"`);
|
|
74984
|
+
}
|
|
74985
|
+
reportControlWrite("requested context usage", options.session, await frontDoorRcControlFromState(realFarmFs, paths).getContextUsage(options.session, options.detail), options.json);
|
|
74986
|
+
}),
|
|
74987
|
+
["agent-shim frontdoor rc get-context-usage --session cse_00000000-0000-4000-8000-000000000000 --detail summary"]
|
|
74988
|
+
);
|
|
74989
|
+
withExamples(
|
|
74990
|
+
rc.command("read-file").description("Ask one observed session's worker to read one file from the session filesystem, gated by its own read-permission rules. The worker's answer arrives on `frontdoor rc watch` as the control_response echoing the printed request id.").requiredOption("--session <id>", "The cse_ session id, as `frontdoor rc list` shows it.").requiredOption("--path <path>", "The file's path, as the worker resolves it against the session's cwd.").option("--max-bytes <n>", "The byte cap on the read, a positive whole number.").option("--encoding <encoding>", `How the answer encodes the contents; one of the SDK's own encodings (${RC_READ_FILE_ENCODINGS.join(", ")}).`).option("--json", "Print the delivery result as JSON.").action(async (options) => {
|
|
74991
|
+
if (options.maxBytes !== void 0 && !/^[1-9][0-9]*$/.test(options.maxBytes)) {
|
|
74992
|
+
throw new UsageError(`--max-bytes must be a positive whole number of bytes, not "${options.maxBytes}"`);
|
|
74993
|
+
}
|
|
74994
|
+
if (options.encoding !== void 0 && !isRcReadFileEncoding(options.encoding)) {
|
|
74995
|
+
throw new UsageError(`--encoding must be one of the SDK's own read-file encodings (${RC_READ_FILE_ENCODINGS.join(", ")}), not "${options.encoding}"`);
|
|
74996
|
+
}
|
|
74997
|
+
reportControlWrite(`requested a read of ${options.path}`, options.session, await frontDoorRcControlFromState(realFarmFs, paths).readFile(options.session, options.path, { ...options.maxBytes === void 0 ? {} : { maxBytes: Number(options.maxBytes) }, ...options.encoding === void 0 ? {} : { encoding: options.encoding } }), options.json, { path: options.path });
|
|
74998
|
+
}),
|
|
74999
|
+
[
|
|
75000
|
+
"agent-shim frontdoor rc read-file --session cse_00000000-0000-4000-8000-000000000000 --path src/index.ts",
|
|
75001
|
+
"agent-shim frontdoor rc read-file --session cse_00000000-0000-4000-8000-000000000000 --path screenshot.png --encoding base64 --max-bytes 65536"
|
|
75002
|
+
]
|
|
75003
|
+
);
|
|
75004
|
+
withExamples(
|
|
75005
|
+
rc.command("file-suggestions").description("Ask one observed session's worker for its at-mention file suggestions for a partial path prefix, the same fuzzy-matched results the TUI shows. The worker's answer arrives on `frontdoor rc watch` as the control_response echoing the printed request id.").requiredOption("--session <id>", "The cse_ session id, as `frontdoor rc list` shows it.").requiredOption("--query <prefix>", "The partial path prefix to fuzzy-match; the empty string is the root listing.").option("--json", "Print the delivery result as JSON.").action(async (options) => {
|
|
75006
|
+
reportControlWrite(`requested file suggestions for "${options.query}"`, options.session, await frontDoorRcControlFromState(realFarmFs, paths).fileSuggestions(options.session, options.query), options.json);
|
|
75007
|
+
}),
|
|
75008
|
+
['agent-shim frontdoor rc file-suggestions --session cse_00000000-0000-4000-8000-000000000000 --query "src/frontdoor/rc"']
|
|
75009
|
+
);
|
|
75010
|
+
withExamples(
|
|
75011
|
+
rc.command("keep-alive").description("Send one liveness heartbeat into an observed session: the payload every receiver ignores by its own contract, useful to keep the client half's stream warm without saying anything.").requiredOption("--session <id>", "The cse_ session id, as `frontdoor rc list` shows it.").option("--json", "Print the delivery result as JSON.").action(async (options) => {
|
|
75012
|
+
reportControlWrite("sent a keep-alive", options.session, await frontDoorRcControlFromState(realFarmFs, paths).keepAlive(options.session), options.json);
|
|
75013
|
+
}),
|
|
75014
|
+
["agent-shim frontdoor rc keep-alive --session cse_00000000-0000-4000-8000-000000000000"]
|
|
75015
|
+
);
|
|
75016
|
+
withExamples(
|
|
75017
|
+
rc.command("mcp-status").description("Ask one observed session's worker for the status of its MCP server connections. The worker's answer arrives on `frontdoor rc watch` as the control_response echoing the printed request id.").requiredOption("--session <id>", "The cse_ session id, as `frontdoor rc list` shows it.").option("--json", "Print the delivery result as JSON.").action(async (options) => {
|
|
75018
|
+
reportControlWrite("requested MCP status", options.session, await frontDoorRcControlFromState(realFarmFs, paths).mcpStatus(options.session), options.json);
|
|
75019
|
+
}),
|
|
75020
|
+
["agent-shim frontdoor rc mcp-status --session cse_00000000-0000-4000-8000-000000000000"]
|
|
75021
|
+
);
|
|
75022
|
+
withExamples(
|
|
75023
|
+
rc.command("mcp-reconnect").description("Ask one observed session's worker to reconnect one MCP server, named exactly as `mcp-status` reports it.").requiredOption("--session <id>", "The cse_ session id, as `frontdoor rc list` shows it.").requiredOption("--server <name>", "The MCP server's name, as `mcp-status` reports it.").option("--json", "Print the delivery result as JSON.").action(async (options) => {
|
|
75024
|
+
reportControlWrite(`requested a reconnect of MCP server ${options.server}`, options.session, await frontDoorRcControlFromState(realFarmFs, paths).mcpReconnect(options.session, options.server), options.json);
|
|
75025
|
+
}),
|
|
75026
|
+
["agent-shim frontdoor rc mcp-reconnect --session cse_00000000-0000-4000-8000-000000000000 --server github"]
|
|
75027
|
+
);
|
|
75028
|
+
withExamples(
|
|
75029
|
+
rc.command("mcp-authenticate").description("Start one MCP server's OAuth handshake on an observed session, naming the redirect URI the flow redirects back to. The worker's answer arrives on `frontdoor rc watch` as the control_response echoing the printed request id.").requiredOption("--session <id>", "The cse_ session id, as `frontdoor rc list` shows it.").requiredOption("--server <name>", "The MCP server's name, as `mcp-status` reports it.").requiredOption("--redirect-uri <uri>", "The URI the server's OAuth flow redirects back to.").option("--json", "Print the delivery result as JSON.").action(async (options) => {
|
|
75030
|
+
reportControlWrite(`requested authentication of MCP server ${options.server}`, options.session, await frontDoorRcControlFromState(realFarmFs, paths).mcpAuthenticate(options.session, options.server, options.redirectUri), options.json);
|
|
75031
|
+
}),
|
|
75032
|
+
["agent-shim frontdoor rc mcp-authenticate --session cse_00000000-0000-4000-8000-000000000000 --server github --redirect-uri https://example.com/oauth/callback"]
|
|
75033
|
+
);
|
|
75034
|
+
withExamples(
|
|
75035
|
+
rc.command("mcp-oauth-callback-url").description("Hand one MCP server's OAuth callback URL to an observed session's worker, completing the handshake `mcp-authenticate` started. The worker's answer arrives on `frontdoor rc watch` as the control_response echoing the printed request id.").requiredOption("--session <id>", "The cse_ session id, as `frontdoor rc list` shows it.").requiredOption("--server <name>", "The MCP server's name, as `mcp-status` reports it.").requiredOption("--callback-url <url>", "The callback URL the browser landed on.").option("--json", "Print the delivery result as JSON.").action(async (options) => {
|
|
75036
|
+
reportControlWrite(`handed MCP server ${options.server} its OAuth callback`, options.session, await frontDoorRcControlFromState(realFarmFs, paths).mcpOAuthCallbackUrl(options.session, options.server, options.callbackUrl), options.json);
|
|
75037
|
+
}),
|
|
75038
|
+
["agent-shim frontdoor rc mcp-oauth-callback-url --session cse_00000000-0000-4000-8000-000000000000 --server github --callback-url https://example.com/oauth/callback?code=x"]
|
|
75039
|
+
);
|
|
75040
|
+
withExamples(
|
|
75041
|
+
rc.command("watch").description("Print the observed sessions' client read stream events as they arrive, the approvals the door's held stream receives above all, until Ctrl-C leaves. Read-only.").argument("[session]", "One cse_ session id, as `frontdoor rc list` shows it; every observed session when omitted.").option("--json", "Print each event as one JSON line as it arrives.").action(async (session, options) => {
|
|
75042
|
+
const client = frontDoorRcApiFromState(realFarmFs, paths);
|
|
75043
|
+
const leave = new AbortController();
|
|
75044
|
+
const onInterrupt = () => {
|
|
75045
|
+
leave.abort();
|
|
75046
|
+
};
|
|
75047
|
+
process.once("SIGINT", onInterrupt);
|
|
75048
|
+
try {
|
|
75049
|
+
for await (const event of await client.rc.subscribe(session === void 0 ? {} : { session }, { signal: leave.signal })) {
|
|
75050
|
+
if (options.json === true) {
|
|
75051
|
+
console.log(JSON.stringify(event));
|
|
75052
|
+
continue;
|
|
75053
|
+
}
|
|
75054
|
+
console.log(formatRcStreamEvent(event));
|
|
75055
|
+
}
|
|
75056
|
+
} catch (error62) {
|
|
75057
|
+
if (!leave.signal.aborted) {
|
|
75058
|
+
throw error62;
|
|
75059
|
+
}
|
|
75060
|
+
} finally {
|
|
75061
|
+
process.off("SIGINT", onInterrupt);
|
|
75062
|
+
}
|
|
75063
|
+
}),
|
|
75064
|
+
["agent-shim frontdoor rc watch", "agent-shim frontdoor rc watch cse_00000000-0000-4000-8000-000000000000 --json"]
|
|
75065
|
+
);
|
|
75066
|
+
const selfhost = withExamples(
|
|
75067
|
+
rc.command("selfhost").description("Mint the local credential behind the door's self-hosted Remote Control surface: the opt-in mode (AGENT_SHIM_FRONTDOOR_RC_SELF_HOST=1 in the door's environment) where the door serves the CCR surface itself and a session activates Remote Control with no Anthropic credential anywhere."),
|
|
75068
|
+
["agent-shim frontdoor rc selfhost mint rig"]
|
|
75069
|
+
);
|
|
75070
|
+
withExamples(
|
|
75071
|
+
selfhost.command("mint").description("Mint the local OAuth credential, account block and feature-cache seed one identity needs to activate Remote Control against the door's own served surface, and record the door-side copy the surface authenticates against. Refuses to replace a real claude.ai login unless --force. Never prints a token.").argument("<identity>", "The identity to mint into, as `agent-shim identity list` names it.").option("--force", "Replace an existing OAuth credential this door did not mint.").option("--json", "Print the mint result as JSON.").action((identity, options) => {
|
|
75072
|
+
const result = mintRcSelfHostCredential({
|
|
75073
|
+
fs: realFarmFs,
|
|
75074
|
+
identitiesDir: paths.identitiesDir,
|
|
75075
|
+
frontdoorDir: paths.frontdoorDir,
|
|
75076
|
+
identity,
|
|
75077
|
+
newUuid: import_node_crypto14.randomUUID,
|
|
75078
|
+
randomToken: () => (0, import_node_crypto14.randomBytes)(MINTED_TOKEN_RANDOM_BYTES).toString("base64url"),
|
|
75079
|
+
force: options.force === true,
|
|
75080
|
+
now: () => Date.now()
|
|
75081
|
+
});
|
|
75082
|
+
if (options.json === true) {
|
|
75083
|
+
printJson(result);
|
|
75084
|
+
return;
|
|
75085
|
+
}
|
|
75086
|
+
for (const line of formatRcSelfHostMint(result)) {
|
|
75087
|
+
console.log(line);
|
|
75088
|
+
}
|
|
75089
|
+
}),
|
|
75090
|
+
["agent-shim frontdoor rc selfhost mint rig", "agent-shim frontdoor rc selfhost mint rig --force"]
|
|
75091
|
+
);
|
|
75092
|
+
}
|
|
75093
|
+
|
|
75094
|
+
// src/frontdoor/rcCredentialStore.ts
|
|
75095
|
+
var import_node_path35 = __toESM(require("node:path"), 1);
|
|
75096
|
+
function isSafeSessionId(sessionId) {
|
|
75097
|
+
return /^[A-Za-z0-9_-]+$/.test(sessionId);
|
|
75098
|
+
}
|
|
75099
|
+
function isRecord6(value2) {
|
|
75100
|
+
return typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
|
|
75101
|
+
}
|
|
75102
|
+
function credentialOfJson(parsed) {
|
|
75103
|
+
if (!isRecord6(parsed)) {
|
|
75104
|
+
return void 0;
|
|
75105
|
+
}
|
|
75106
|
+
const { authorization, anthropicVersion, anthropicClientPlatform } = parsed;
|
|
75107
|
+
if (typeof authorization !== "string" || authorization === "") {
|
|
75108
|
+
return void 0;
|
|
75109
|
+
}
|
|
75110
|
+
return {
|
|
75111
|
+
authorization,
|
|
75112
|
+
anthropicVersion: typeof anthropicVersion === "string" ? anthropicVersion : void 0,
|
|
75113
|
+
anthropicClientPlatform: typeof anthropicClientPlatform === "string" ? anthropicClientPlatform : void 0
|
|
75114
|
+
};
|
|
75115
|
+
}
|
|
75116
|
+
function sequenceNumOfJson(value2) {
|
|
75117
|
+
return typeof value2 === "number" && Number.isInteger(value2) && value2 >= 0 ? value2 : void 0;
|
|
75118
|
+
}
|
|
75119
|
+
function createRcCredentialStore(fs20, dir) {
|
|
75120
|
+
fs20.mkdirPrivate(dir);
|
|
75121
|
+
const fileFor = (sessionId) => import_node_path35.default.join(dir, `${sessionId}.json`);
|
|
75122
|
+
const readParsed = (sessionId) => {
|
|
75123
|
+
const raw = fs20.readFileUtf8(fileFor(sessionId));
|
|
75124
|
+
if (raw === void 0) {
|
|
75125
|
+
return void 0;
|
|
75126
|
+
}
|
|
75127
|
+
let parsed;
|
|
75128
|
+
try {
|
|
75129
|
+
parsed = JSON.parse(raw);
|
|
75130
|
+
} catch {
|
|
75131
|
+
return void 0;
|
|
75132
|
+
}
|
|
75133
|
+
return isRecord6(parsed) ? parsed : void 0;
|
|
75134
|
+
};
|
|
75135
|
+
return {
|
|
75136
|
+
read: (sessionId) => {
|
|
75137
|
+
if (!isSafeSessionId(sessionId)) {
|
|
75138
|
+
return void 0;
|
|
75139
|
+
}
|
|
75140
|
+
return credentialOfJson(readParsed(sessionId));
|
|
75141
|
+
},
|
|
75142
|
+
write: (sessionId, credential) => {
|
|
75143
|
+
if (!isSafeSessionId(sessionId)) {
|
|
75144
|
+
return;
|
|
75145
|
+
}
|
|
75146
|
+
const cursor3 = sequenceNumOfJson(readParsed(sessionId)?.highestSequenceNum);
|
|
75147
|
+
const record2 = { ...credential, ...cursor3 === void 0 ? {} : { highestSequenceNum: cursor3 } };
|
|
75148
|
+
fs20.writeFilePrivate(fileFor(sessionId), `${JSON.stringify(record2, null, 2)}
|
|
75149
|
+
`);
|
|
75150
|
+
},
|
|
75151
|
+
readCursor: (sessionId) => {
|
|
75152
|
+
if (!isSafeSessionId(sessionId)) {
|
|
75153
|
+
return void 0;
|
|
75154
|
+
}
|
|
75155
|
+
return sequenceNumOfJson(readParsed(sessionId)?.highestSequenceNum);
|
|
75156
|
+
},
|
|
75157
|
+
writeCursor: (sessionId, highestSequenceNum) => {
|
|
75158
|
+
if (!isSafeSessionId(sessionId)) {
|
|
75159
|
+
return;
|
|
75160
|
+
}
|
|
75161
|
+
const credential = credentialOfJson(readParsed(sessionId));
|
|
75162
|
+
const record2 = credential === void 0 ? { highestSequenceNum } : { ...credential, highestSequenceNum };
|
|
75163
|
+
fs20.writeFilePrivate(fileFor(sessionId), `${JSON.stringify(record2, null, 2)}
|
|
75164
|
+
`);
|
|
75165
|
+
},
|
|
75166
|
+
remove: (sessionId) => {
|
|
75167
|
+
if (!isSafeSessionId(sessionId)) {
|
|
75168
|
+
return;
|
|
75169
|
+
}
|
|
75170
|
+
fs20.removeRecursive(fileFor(sessionId));
|
|
75171
|
+
}
|
|
75172
|
+
};
|
|
75173
|
+
}
|
|
75174
|
+
|
|
75175
|
+
// src/frontdoor/rcClientPage.ts
|
|
75176
|
+
var RC_CLIENT_PATH = "/__agent-shim/client";
|
|
75177
|
+
var RC_CLIENT_PAGE_SCRIPT = `'use strict';
|
|
75178
|
+
// 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.
|
|
75179
|
+
var API_PREFIX = '/__agent-shim/orpc';
|
|
75180
|
+
var TOKEN_KEY = 'agent-shim-rc-token';
|
|
75181
|
+
var token = null;
|
|
75182
|
+
|
|
75183
|
+
function apiBase() { return window.location.origin + API_PREFIX; }
|
|
75184
|
+
|
|
75185
|
+
// Calls one procedure: resolves the output's own value, rejects with the door's error message when it answered one.
|
|
75186
|
+
async function rpc(path, input) {
|
|
75187
|
+
var response = await fetch(apiBase() + '/' + path, {
|
|
75188
|
+
method: 'POST',
|
|
75189
|
+
headers: { 'content-type': 'application/json', authorization: 'Bearer ' + token },
|
|
75190
|
+
body: JSON.stringify({ json: input === undefined ? {} : input }),
|
|
75191
|
+
});
|
|
75192
|
+
var envelope = await response.json().catch(function () { return {}; });
|
|
75193
|
+
if (!response.ok) {
|
|
75194
|
+
var message = envelope && envelope.json && envelope.json.message ? envelope.json.message : ('HTTP ' + response.status);
|
|
75195
|
+
var error = new Error(message);
|
|
75196
|
+
error.status = response.status;
|
|
75197
|
+
throw error;
|
|
75198
|
+
}
|
|
75199
|
+
return envelope.json;
|
|
75200
|
+
}
|
|
75201
|
+
|
|
75202
|
+
// 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.
|
|
75203
|
+
function parseSseFrames(buffer) {
|
|
75204
|
+
var frames = [];
|
|
75205
|
+
var rest = buffer;
|
|
75206
|
+
for (;;) {
|
|
75207
|
+
var boundary = rest.indexOf('\\n\\n');
|
|
75208
|
+
if (boundary === -1) { break; }
|
|
75209
|
+
var raw = rest.slice(0, boundary);
|
|
75210
|
+
rest = rest.slice(boundary + 2);
|
|
75211
|
+
var frame = { event: 'message', id: '', data: '' };
|
|
75212
|
+
var lines = raw.split('\\n');
|
|
75213
|
+
for (var i = 0; i < lines.length; i++) {
|
|
75214
|
+
var line = lines[i];
|
|
75215
|
+
if (line === '' || line.charAt(0) === ':') { continue; }
|
|
75216
|
+
var colon = line.indexOf(':');
|
|
75217
|
+
var field = colon === -1 ? line : line.slice(0, colon);
|
|
75218
|
+
var value = colon === -1 ? '' : line.slice(colon + 1);
|
|
75219
|
+
if (value.charAt(0) === ' ') { value = value.slice(1); }
|
|
75220
|
+
if (field === 'event') { frame.event = value; }
|
|
75221
|
+
if (field === 'id') { frame.id = value; }
|
|
75222
|
+
if (field === 'data') { frame.data += (frame.data === '' ? '' : '\\n') + value; }
|
|
75223
|
+
}
|
|
75224
|
+
if (frame.data !== '') { frames.push(frame); }
|
|
75225
|
+
}
|
|
75226
|
+
return { frames: frames, rest: rest };
|
|
75227
|
+
}
|
|
75228
|
+
|
|
75229
|
+
// 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.
|
|
75230
|
+
function sequenceGap(tracker, event) {
|
|
75231
|
+
var previous = tracker[event.source];
|
|
75232
|
+
if (event.sequence > (previous === undefined ? 0 : previous)) { tracker[event.source] = event.sequence; }
|
|
75233
|
+
if (previous === undefined || event.sequence <= previous) { return 0; }
|
|
75234
|
+
return event.sequence - previous - 1;
|
|
75235
|
+
}
|
|
75236
|
+
|
|
75237
|
+
function shortId(id) { return id.length > 14 ? id.slice(0, 8) + '..' + id.slice(-4) : id; }
|
|
75238
|
+
|
|
75239
|
+
// A bounded sketch of any payload, the feed's fallback rendering: one JSON line, cut with a marker rather than silently.
|
|
75240
|
+
function sketch(value, cap) {
|
|
75241
|
+
var text;
|
|
75242
|
+
try { text = JSON.stringify(value); } catch (e) { text = String(value); }
|
|
75243
|
+
if (text === undefined) { text = 'undefined'; }
|
|
75244
|
+
return text.length > cap ? text.slice(0, cap) + '...' : text;
|
|
75245
|
+
}
|
|
75246
|
+
|
|
75247
|
+
// 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.
|
|
75248
|
+
function rcLine(event) {
|
|
75249
|
+
var envelope = event.envelope;
|
|
75250
|
+
var payload = envelope.payload;
|
|
75251
|
+
var headline = '';
|
|
75252
|
+
if (payload !== undefined && payload !== null && typeof payload === 'object' && Array.isArray(payload.content)) {
|
|
75253
|
+
var parts = [];
|
|
75254
|
+
for (var i = 0; i < payload.content.length; i++) {
|
|
75255
|
+
var block = payload.content[i];
|
|
75256
|
+
if (block !== null && typeof block === 'object' && block.type === 'text' && typeof block.text === 'string') { parts.push(block.text); }
|
|
75257
|
+
}
|
|
75258
|
+
if (parts.length > 0) { headline = parts.join(' '); }
|
|
75259
|
+
}
|
|
75260
|
+
if (headline === '') {
|
|
75261
|
+
headline = envelope.event_type;
|
|
75262
|
+
if (payload !== undefined) { headline += ' ' + sketch(payload, 240); }
|
|
75263
|
+
}
|
|
75264
|
+
return new Date().toLocaleTimeString() + ' ' + shortId(event.session) + ' ' + envelope.event_type + ' ' + headline;
|
|
75265
|
+
}
|
|
75266
|
+
|
|
75267
|
+
function el(tag, className, text) {
|
|
75268
|
+
var node = document.createElement(tag);
|
|
75269
|
+
if (className) { node.className = className; }
|
|
75270
|
+
if (text !== undefined) { node.textContent = text; }
|
|
75271
|
+
return node;
|
|
75272
|
+
}
|
|
75273
|
+
|
|
75274
|
+
var ui = null;
|
|
75275
|
+
|
|
75276
|
+
function renderEvent(event, gap) {
|
|
75277
|
+
if (ui === null) { return; }
|
|
75278
|
+
var list = ui.eventList;
|
|
75279
|
+
if (gap > 0) {
|
|
75280
|
+
list.appendChild(el('div', 'event gap', 'dropped events: ' + String(gap) + ' on source ' + event.source + ' (the stream holds no replay)'));
|
|
75281
|
+
}
|
|
75282
|
+
var line;
|
|
75283
|
+
if (event.source === 'rc') {
|
|
75284
|
+
line = rcLine(event.payload);
|
|
75285
|
+
} else {
|
|
75286
|
+
line = new Date().toLocaleTimeString() + ' [' + event.source + '] ' + sketch(event.payload, 240);
|
|
75287
|
+
}
|
|
75288
|
+
list.appendChild(el('div', 'event', line));
|
|
75289
|
+
while (list.childElementCount > 500) { list.removeChild(list.firstChild); }
|
|
75290
|
+
list.scrollTop = list.scrollHeight;
|
|
75291
|
+
}
|
|
75292
|
+
|
|
75293
|
+
function renderSessions(statuses) {
|
|
75294
|
+
if (ui === null) { return; }
|
|
75295
|
+
var list = ui.sessionList;
|
|
75296
|
+
list.textContent = '';
|
|
75297
|
+
if (statuses.length === 0) {
|
|
75298
|
+
list.appendChild(el('div', 'event dim', 'no Remote Control sessions observed'));
|
|
75299
|
+
return;
|
|
75300
|
+
}
|
|
75301
|
+
for (var i = 0; i < statuses.length; i++) {
|
|
75302
|
+
var status = statuses[i];
|
|
75303
|
+
var worker = status.workerState === undefined ? 'worker unknown' : 'worker ' + status.workerState.value;
|
|
75304
|
+
var row = el('div', 'session' + (status.id === ui.selected ? ' selected' : ''));
|
|
75305
|
+
row.appendChild(el('div', null, shortId(status.id)));
|
|
75306
|
+
row.appendChild(el('div', 'dim', worker + (status.pending.length === 0 ? '' : ', ' + String(status.pending.length) + ' pending') + ', seen ' + new Date(status.lastSeenAt).toLocaleTimeString()));
|
|
75307
|
+
row.addEventListener('click', selectSession(status.id));
|
|
75308
|
+
list.appendChild(row);
|
|
75309
|
+
}
|
|
75310
|
+
}
|
|
75311
|
+
|
|
75312
|
+
function selectSession(id) {
|
|
75313
|
+
return function () {
|
|
75314
|
+
if (ui === null) { return; }
|
|
75315
|
+
ui.selected = ui.selected === id ? null : id;
|
|
75316
|
+
ui.promptTarget.textContent = ui.selected === null ? 'no session selected' : 'to ' + ui.selected;
|
|
75317
|
+
refreshSessions();
|
|
75318
|
+
refreshPending();
|
|
75319
|
+
};
|
|
75320
|
+
}
|
|
75321
|
+
|
|
75322
|
+
function renderPending(pending) {
|
|
75323
|
+
if (ui === null) { return; }
|
|
75324
|
+
var list = ui.pendingList;
|
|
75325
|
+
list.textContent = '';
|
|
75326
|
+
if (pending.length === 0) {
|
|
75327
|
+
list.appendChild(el('div', 'event dim', 'no control requests awaiting an answer'));
|
|
75328
|
+
return;
|
|
75329
|
+
}
|
|
75330
|
+
for (var i = 0; i < pending.length; i++) {
|
|
75331
|
+
var request = pending[i];
|
|
75332
|
+
var card = el('div', 'card');
|
|
75333
|
+
card.appendChild(el('div', null, request.type + ' ' + (request.summary === '' ? shortId(request.requestId) : request.summary)));
|
|
75334
|
+
card.appendChild(el('div', 'dim', shortId(request.sessionId) + ' ' + shortId(request.requestId) + ' observed ' + new Date(request.observedAt).toLocaleTimeString()));
|
|
75335
|
+
var row = el('div', 'row');
|
|
75336
|
+
var denial = el('input');
|
|
75337
|
+
denial.type = 'text';
|
|
75338
|
+
denial.placeholder = 'denial message (optional)';
|
|
75339
|
+
var approve = el('button', null, 'Approve');
|
|
75340
|
+
var deny = el('button', 'secondary', 'Deny');
|
|
75341
|
+
approve.addEventListener('click', answerRequest(request, true, denial));
|
|
75342
|
+
deny.addEventListener('click', answerRequest(request, false, denial));
|
|
75343
|
+
row.appendChild(denial);
|
|
75344
|
+
row.appendChild(approve);
|
|
75345
|
+
row.appendChild(deny);
|
|
75346
|
+
card.appendChild(row);
|
|
75347
|
+
list.appendChild(card);
|
|
75348
|
+
}
|
|
75349
|
+
}
|
|
75350
|
+
|
|
75351
|
+
function answerRequest(request, approve, denialInput) {
|
|
75352
|
+
return function () {
|
|
75353
|
+
var input = { session: request.sessionId, request: request.requestId, approve: approve };
|
|
75354
|
+
if (!approve && denialInput.value !== '') { input.text = denialInput.value; }
|
|
75355
|
+
rpc('rc/answer', input).then(function () {
|
|
75356
|
+
renderEvent({ source: 'page', sequence: 0, payload: { note: (approve ? 'approved' : 'denied') + ' ' + request.requestId } }, 0);
|
|
75357
|
+
refreshPending();
|
|
75358
|
+
}, function (error) { window.alert('answer failed: ' + error.message); });
|
|
75359
|
+
};
|
|
75360
|
+
}
|
|
75361
|
+
|
|
75362
|
+
function sendPrompt() {
|
|
75363
|
+
if (ui === null || ui.selected === null) { window.alert('select a session first'); return; }
|
|
75364
|
+
var text = ui.prompt.value;
|
|
75365
|
+
if (text === '') { return; }
|
|
75366
|
+
ui.send.disabled = true;
|
|
75367
|
+
rpc('rc/send', { session: ui.selected, text: text }).then(function (result) {
|
|
75368
|
+
ui.prompt.value = '';
|
|
75369
|
+
renderEvent({ source: 'page', sequence: 0, payload: { note: 'sent, sequence_num ' + result.sequenceNums.join(', ') } }, 0);
|
|
75370
|
+
}, function (error) { window.alert('send failed: ' + error.message); }).then(function () { ui.send.disabled = false; });
|
|
75371
|
+
}
|
|
75372
|
+
|
|
75373
|
+
function refreshSessions() {
|
|
75374
|
+
rpc('rc/status', {}).then(function (result) { renderSessions(result.statuses); }, authOrReport('rc/status'));
|
|
75375
|
+
}
|
|
75376
|
+
|
|
75377
|
+
function refreshPending() {
|
|
75378
|
+
rpc('rc/pending', ui !== null && ui.selected !== null ? { session: ui.selected } : {}).then(function (result) { renderPending(result.pending); }, authOrReport('rc/pending'));
|
|
75379
|
+
}
|
|
75380
|
+
|
|
75381
|
+
// 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.
|
|
75382
|
+
function authOrReport(what) {
|
|
75383
|
+
return function (error) {
|
|
75384
|
+
if (error.status === 401) {
|
|
75385
|
+
handleUnauthorized();
|
|
75386
|
+
return;
|
|
75387
|
+
}
|
|
75388
|
+
renderEvent({ source: 'page', sequence: 0, payload: { note: what + ' failed: ' + error.message } }, 0);
|
|
75389
|
+
};
|
|
75390
|
+
}
|
|
75391
|
+
|
|
75392
|
+
function handleUnauthorized() {
|
|
75393
|
+
window.sessionStorage.removeItem(TOKEN_KEY);
|
|
75394
|
+
if (feedAbort !== null) { feedAbort.abort(); }
|
|
75395
|
+
streamState('');
|
|
75396
|
+
showTokenForm('this door refused the token: it names a different door generation, paste the current one');
|
|
75397
|
+
}
|
|
75398
|
+
|
|
75399
|
+
function streamState(text) { if (ui !== null) { ui.streamState.textContent = text; } }
|
|
75400
|
+
|
|
75401
|
+
var feedAbort = null;
|
|
75402
|
+
|
|
75403
|
+
// 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.
|
|
75404
|
+
async function runFeed() {
|
|
75405
|
+
var tracker = {};
|
|
75406
|
+
for (;;) {
|
|
75407
|
+
if (feedAbort !== null) { feedAbort.abort(); }
|
|
75408
|
+
feedAbort = new AbortController();
|
|
75409
|
+
var filter = document.getElementById('source-filter').value;
|
|
75410
|
+
var input = filter === '' ? {} : { sources: [filter] };
|
|
75411
|
+
try {
|
|
75412
|
+
streamState('stream: connecting');
|
|
75413
|
+
var response = await fetch(apiBase() + '/events/subscribe', {
|
|
75414
|
+
method: 'POST',
|
|
75415
|
+
headers: { 'content-type': 'application/json', authorization: 'Bearer ' + token },
|
|
75416
|
+
body: JSON.stringify({ json: input }),
|
|
75417
|
+
signal: feedAbort.signal,
|
|
75418
|
+
});
|
|
75419
|
+
if (response.status === 401) { handleUnauthorized(); return; }
|
|
75420
|
+
if (!response.ok || response.body === null) { throw new Error('HTTP ' + String(response.status)); }
|
|
75421
|
+
streamState('stream: live');
|
|
75422
|
+
var reader = response.body.getReader();
|
|
75423
|
+
var decoder = new TextDecoder();
|
|
75424
|
+
var buffer = '';
|
|
75425
|
+
for (;;) {
|
|
75426
|
+
var chunk = await reader.read();
|
|
75427
|
+
if (chunk.done) { break; }
|
|
75428
|
+
buffer += decoder.decode(chunk.value, { stream: true });
|
|
75429
|
+
var parsed = parseSseFrames(buffer);
|
|
75430
|
+
buffer = parsed.rest;
|
|
75431
|
+
for (var i = 0; i < parsed.frames.length; i++) {
|
|
75432
|
+
var frame = parsed.frames[i];
|
|
75433
|
+
if (frame.event !== 'message') { continue; }
|
|
75434
|
+
var event;
|
|
75435
|
+
try { event = JSON.parse(frame.data).json; } catch (e) { continue; }
|
|
75436
|
+
if (event === undefined || event.source === undefined) { continue; }
|
|
75437
|
+
renderEvent(event, sequenceGap(tracker, event));
|
|
75438
|
+
}
|
|
75439
|
+
}
|
|
75440
|
+
} catch (error) {
|
|
75441
|
+
if (feedAbort !== null && feedAbort.signal.aborted) { return; }
|
|
75442
|
+
}
|
|
75443
|
+
streamState('stream: dropped, reconnecting');
|
|
75444
|
+
await new Promise(function (resolve) { window.setTimeout(resolve, 1000); });
|
|
75445
|
+
}
|
|
75446
|
+
}
|
|
75447
|
+
|
|
75448
|
+
function showTokenForm(message) {
|
|
75449
|
+
if (feedAbort !== null) { feedAbort.abort(); }
|
|
75450
|
+
if (ui !== null) { ui.main.classList.add('hidden'); }
|
|
75451
|
+
var form = document.getElementById('token-form');
|
|
75452
|
+
form.classList.remove('hidden');
|
|
75453
|
+
document.getElementById('token-error').textContent = message === undefined ? '' : message;
|
|
75454
|
+
document.getElementById('token-input').focus();
|
|
75455
|
+
}
|
|
75456
|
+
|
|
75457
|
+
function startPage(heldToken) {
|
|
75458
|
+
token = heldToken;
|
|
75459
|
+
ui = {
|
|
75460
|
+
main: document.getElementById('main'),
|
|
75461
|
+
eventList: document.getElementById('event-list'),
|
|
75462
|
+
sessionList: document.getElementById('session-list'),
|
|
75463
|
+
pendingList: document.getElementById('pending-list'),
|
|
75464
|
+
prompt: document.getElementById('prompt'),
|
|
75465
|
+
promptTarget: document.getElementById('prompt-target'),
|
|
75466
|
+
send: document.getElementById('send'),
|
|
75467
|
+
streamState: document.getElementById('stream-state'),
|
|
75468
|
+
selected: null,
|
|
75469
|
+
};
|
|
75470
|
+
document.getElementById('token-form').classList.add('hidden');
|
|
75471
|
+
ui.main.classList.remove('hidden');
|
|
75472
|
+
document.getElementById('refresh').addEventListener('click', function () { refreshSessions(); refreshPending(); });
|
|
75473
|
+
document.getElementById('send').addEventListener('click', sendPrompt);
|
|
75474
|
+
document.getElementById('source-filter').addEventListener('change', function () { runFeed(); });
|
|
75475
|
+
ui.prompt.addEventListener('keydown', function (event) {
|
|
75476
|
+
if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) { sendPrompt(); }
|
|
75477
|
+
});
|
|
75478
|
+
refreshSessions();
|
|
75479
|
+
refreshPending();
|
|
75480
|
+
window.setInterval(function () { refreshSessions(); refreshPending(); }, 4000);
|
|
75481
|
+
runFeed();
|
|
75482
|
+
document.getElementById('door-state').textContent = 'connected';
|
|
75483
|
+
}
|
|
75484
|
+
|
|
75485
|
+
function boot() {
|
|
75486
|
+
var held = null;
|
|
75487
|
+
var query = new URLSearchParams(window.location.search);
|
|
75488
|
+
var fromQuery = query.get('token');
|
|
75489
|
+
if (fromQuery !== null && fromQuery !== '') {
|
|
75490
|
+
window.sessionStorage.setItem(TOKEN_KEY, fromQuery);
|
|
75491
|
+
// 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.
|
|
75492
|
+
window.history.replaceState(null, '', window.location.pathname);
|
|
75493
|
+
held = fromQuery;
|
|
75494
|
+
} else {
|
|
75495
|
+
held = window.sessionStorage.getItem(TOKEN_KEY);
|
|
75496
|
+
}
|
|
75497
|
+
document.getElementById('token-save').addEventListener('click', function () {
|
|
75498
|
+
var value = document.getElementById('token-input').value.trim();
|
|
75499
|
+
if (value === '') { return; }
|
|
75500
|
+
window.sessionStorage.setItem(TOKEN_KEY, value);
|
|
75501
|
+
startPage(value);
|
|
75502
|
+
});
|
|
75503
|
+
document.getElementById('token-input').addEventListener('keydown', function (event) {
|
|
75504
|
+
if (event.key === 'Enter') { document.getElementById('token-save').click(); }
|
|
75505
|
+
});
|
|
75506
|
+
if (held !== null && held !== '') { startPage(held); }
|
|
75507
|
+
}
|
|
75508
|
+
|
|
75509
|
+
if (typeof document !== 'undefined' && document.getElementById('event-list') !== null) {
|
|
75510
|
+
window.addEventListener('DOMContentLoaded', boot);
|
|
74951
75511
|
}
|
|
74952
|
-
|
|
74953
|
-
|
|
74954
|
-
|
|
74955
|
-
|
|
74956
|
-
|
|
74957
|
-
|
|
74958
|
-
|
|
74959
|
-
|
|
74960
|
-
|
|
74961
|
-
|
|
74962
|
-
}
|
|
74963
|
-
|
|
75512
|
+
`;
|
|
75513
|
+
var RC_CLIENT_PAGE_HTML = `<!doctype html>
|
|
75514
|
+
<html lang="en">
|
|
75515
|
+
<head>
|
|
75516
|
+
<meta charset="utf-8">
|
|
75517
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
75518
|
+
<title>agent-shim Remote Control</title>
|
|
75519
|
+
<style>
|
|
75520
|
+
:root { color-scheme: dark; }
|
|
75521
|
+
* { box-sizing: border-box; }
|
|
75522
|
+
body { margin: 0; background: #101418; color: #d7dde3; font: 14px/1.45 ui-monospace, Menlo, Consolas, monospace; }
|
|
75523
|
+
header { display: flex; gap: 12px; align-items: baseline; padding: 10px 16px; background: #171d24; border-bottom: 1px solid #2a333d; }
|
|
75524
|
+
header h1 { font-size: 15px; margin: 0; font-weight: 600; }
|
|
75525
|
+
header .state { color: #8b98a5; }
|
|
75526
|
+
main { display: flex; gap: 1px; background: #2a333d; min-height: calc(100vh - 39px); }
|
|
75527
|
+
section { background: #101418; padding: 12px 16px; overflow-y: auto; }
|
|
75528
|
+
#sessions { flex: 0 0 320px; border-right: 1px solid #2a333d; }
|
|
75529
|
+
#right { flex: 1; display: flex; flex-direction: column; min-width: 0; }
|
|
75530
|
+
#feed { flex: 1; }
|
|
75531
|
+
#controls { border-top: 1px solid #2a333d; }
|
|
75532
|
+
h2 { font-size: 12px; text-transform: uppercase; letter-spacing: 0.08em; color: #8b98a5; margin: 8px 0 8px; font-weight: 600; }
|
|
75533
|
+
.session { padding: 6px 8px; border: 1px solid transparent; border-radius: 4px; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
75534
|
+
.session:hover { background: #171d24; }
|
|
75535
|
+
.session.selected { border-color: #3d6ea5; background: #17222e; }
|
|
75536
|
+
.session .dim { color: #8b98a5; }
|
|
75537
|
+
.event { padding: 2px 0; white-space: pre-wrap; word-break: break-word; }
|
|
75538
|
+
.event .dim { color: #8b98a5; }
|
|
75539
|
+
.event.gap { color: #d29922; }
|
|
75540
|
+
.event.error { color: #e5534b; }
|
|
75541
|
+
textarea, input[type=text], select { background: #171d24; color: #d7dde3; border: 1px solid #2a333d; border-radius: 4px; padding: 6px 8px; font: inherit; width: 100%; }
|
|
75542
|
+
textarea { resize: vertical; min-height: 44px; }
|
|
75543
|
+
button { background: #1f4a7a; color: #d7dde3; border: 1px solid #3d6ea5; border-radius: 4px; padding: 5px 12px; font: inherit; cursor: pointer; }
|
|
75544
|
+
button.secondary { background: #2a333d; border-color: #3a444f; }
|
|
75545
|
+
button:disabled { opacity: 0.5; cursor: default; }
|
|
75546
|
+
.row { display: flex; gap: 8px; margin: 6px 0; align-items: center; }
|
|
75547
|
+
.card { border: 1px solid #3d6ea5; border-radius: 4px; padding: 8px; margin: 6px 0; background: #17222e; }
|
|
75548
|
+
.card .dim { color: #8b98a5; }
|
|
75549
|
+
#token-form { max-width: 480px; margin: 48px auto; }
|
|
75550
|
+
#token-form p { color: #8b98a5; }
|
|
75551
|
+
.hidden { display: none !important; }
|
|
75552
|
+
</style>
|
|
75553
|
+
</head>
|
|
75554
|
+
<body data-agent-shim-rc-client>
|
|
75555
|
+
<header>
|
|
75556
|
+
<h1>agent-shim Remote Control</h1>
|
|
75557
|
+
<span class="state" id="door-state">connecting</span>
|
|
75558
|
+
<span class="state" id="stream-state"></span>
|
|
75559
|
+
</header>
|
|
75560
|
+
<div id="token-form">
|
|
75561
|
+
<h2>This door's control token</h2>
|
|
75562
|
+
<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>
|
|
75563
|
+
<div class="row"><input type="text" id="token-input" placeholder="control token" autocomplete="off"><button id="token-save">Use</button></div>
|
|
75564
|
+
<p id="token-error" class="event error"></p>
|
|
75565
|
+
</div>
|
|
75566
|
+
<main class="hidden" id="main">
|
|
75567
|
+
<section id="sessions">
|
|
75568
|
+
<h2>Sessions <button class="secondary" id="refresh">Refresh</button></h2>
|
|
75569
|
+
<div id="session-list"></div>
|
|
75570
|
+
</section>
|
|
75571
|
+
<section id="right">
|
|
75572
|
+
<div id="feed">
|
|
75573
|
+
<h2>Feed <select id="source-filter"><option value="">all sources</option><option value="rc">Remote Control only</option></select></h2>
|
|
75574
|
+
<div id="event-list"></div>
|
|
75575
|
+
</div>
|
|
75576
|
+
<div id="controls">
|
|
75577
|
+
<h2>Pending approvals</h2>
|
|
75578
|
+
<div id="pending-list"></div>
|
|
75579
|
+
<h2>Send a prompt</h2>
|
|
75580
|
+
<div class="row"><textarea id="prompt" placeholder="Prompt for the selected session"></textarea></div>
|
|
75581
|
+
<div class="row"><span class="dim" id="prompt-target">no session selected</span><span style="flex:1"></span><button id="send">Send</button></div>
|
|
75582
|
+
</div>
|
|
75583
|
+
</section>
|
|
75584
|
+
</main>
|
|
75585
|
+
<script>
|
|
75586
|
+
${RC_CLIENT_PAGE_SCRIPT}
|
|
75587
|
+
</script>
|
|
75588
|
+
</body>
|
|
75589
|
+
</html>
|
|
75590
|
+
`;
|
|
75591
|
+
function pageTokenOf(request7, url2) {
|
|
75592
|
+
const presented = request7.headers.authorization;
|
|
75593
|
+
const bearerPrefix = "bearer ".length;
|
|
75594
|
+
const header = typeof presented === "string" && presented.slice(0, bearerPrefix).toLowerCase() === "bearer " ? presented.slice(bearerPrefix) : void 0;
|
|
75595
|
+
const query = url2.searchParams.get("token") ?? void 0;
|
|
75596
|
+
return header ?? query;
|
|
74964
75597
|
}
|
|
74965
|
-
function
|
|
74966
|
-
const
|
|
74967
|
-
if (
|
|
74968
|
-
|
|
74969
|
-
|
|
74970
|
-
|
|
74971
|
-
if (typeof accessToken !== "string" || accessToken === "" || typeof refreshToken !== "string" || refreshToken === "" || typeof organizationUuid !== "string" || organizationUuid === "" || typeof accountUuid !== "string" || accountUuid === "") {
|
|
74972
|
-
return void 0;
|
|
75598
|
+
function serveRcClientPage(expectedToken, request7, response) {
|
|
75599
|
+
const url2 = new URL(request7.url ?? "/", "https://door.invalid");
|
|
75600
|
+
if (url2.pathname !== RC_CLIENT_PATH && url2.pathname !== `${RC_CLIENT_PATH}/`) {
|
|
75601
|
+
response.writeHead(HTTP_STATUS.notFound, { "Content-Type": "text/plain" });
|
|
75602
|
+
response.end("no such page under the door's client prefix");
|
|
75603
|
+
return;
|
|
74973
75604
|
}
|
|
74974
|
-
|
|
74975
|
-
}
|
|
74976
|
-
|
|
74977
|
-
|
|
74978
|
-
throw new Error(`"${deps.identity}" is not a valid identity name.`);
|
|
75605
|
+
if (request7.method !== "GET" && request7.method !== "HEAD") {
|
|
75606
|
+
response.writeHead(HTTP_STATUS.methodNotAllowed, { Allow: "GET, HEAD", "Content-Type": "text/plain" });
|
|
75607
|
+
response.end("the door's web client page is read-only");
|
|
75608
|
+
return;
|
|
74979
75609
|
}
|
|
74980
|
-
const
|
|
74981
|
-
|
|
74982
|
-
|
|
74983
|
-
|
|
74984
|
-
|
|
74985
|
-
const previousRecord = readRcSelfHostRecord(deps.fs, deps.frontdoorDir);
|
|
74986
|
-
const existingCredentials = readJsonObject(deps.fs, credentialsFile);
|
|
74987
|
-
const existingOauth = isRecord6(existingCredentials?.claudeAiOauth) ? existingCredentials.claudeAiOauth : void 0;
|
|
74988
|
-
const existingToken = typeof existingOauth?.accessToken === "string" && existingOauth.accessToken !== "" ? existingOauth.accessToken : void 0;
|
|
74989
|
-
const existingIsOwn = existingToken !== void 0 && previousRecord?.accessToken === existingToken;
|
|
74990
|
-
if (existingToken !== void 0 && !existingIsOwn && !deps.force) {
|
|
74991
|
-
throw new Error(
|
|
74992
|
-
`identity ${deps.identity} already holds an OAuth credential this door did not mint (a real claude.ai login): pass --force to replace it, or mint into an identity with no login. The self-hosted mode expects no Anthropic credential to be in play.`
|
|
74993
|
-
);
|
|
75610
|
+
const token = pageTokenOf(request7, url2);
|
|
75611
|
+
if (token === void 0 || !isLiveCapability(token, [expectedToken])) {
|
|
75612
|
+
response.writeHead(HTTP_STATUS.unauthorized, { "Content-Type": "application/json" });
|
|
75613
|
+
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" }));
|
|
75614
|
+
return;
|
|
74994
75615
|
}
|
|
74995
|
-
|
|
74996
|
-
|
|
74997
|
-
|
|
74998
|
-
|
|
74999
|
-
|
|
75000
|
-
|
|
75001
|
-
|
|
75002
|
-
|
|
75003
|
-
|
|
75004
|
-
|
|
75005
|
-
|
|
75006
|
-
|
|
75007
|
-
|
|
75008
|
-
|
|
75009
|
-
|
|
75010
|
-
emailAddress: SELF_HOST_EMAIL,
|
|
75011
|
-
organizationUuid,
|
|
75012
|
-
organizationName: "agent-shim self-hosted",
|
|
75013
|
-
// The billing and creation fields exist so the CLI's "populate the account block if it is missing" step finds a complete block and skips its profile fetch; the door answers that fetch locally anyway, so these are the quiet path, not a load-bearing claim.
|
|
75014
|
-
billingType: "stripe_subscription",
|
|
75015
|
-
accountCreatedAt: mintedAt,
|
|
75016
|
-
subscriptionCreatedAt: mintedAt
|
|
75017
|
-
},
|
|
75018
|
-
cachedGrowthBookFeatures: {
|
|
75019
|
-
...existingBlock(claudeJson.cachedGrowthBookFeatures),
|
|
75020
|
-
tengu_ccr_bridge: true,
|
|
75021
|
-
tengu_bridge_repl_v2: true,
|
|
75022
|
-
tengu_bridge_repl_v2_cse_shim_enabled: true
|
|
75616
|
+
response.writeHead(HTTP_STATUS.ok, {
|
|
75617
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
75618
|
+
"Cache-Control": "no-store",
|
|
75619
|
+
"Referrer-Policy": "no-referrer",
|
|
75620
|
+
"Content-Security-Policy": "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'self'"
|
|
75621
|
+
});
|
|
75622
|
+
response.end(request7.method === "HEAD" ? void 0 : RC_CLIENT_PAGE_HTML);
|
|
75623
|
+
}
|
|
75624
|
+
function createRcClientPage(expectedToken) {
|
|
75625
|
+
return {
|
|
75626
|
+
pathPrefix: RC_CLIENT_PATH,
|
|
75627
|
+
handle: async (request7, response) => {
|
|
75628
|
+
serveRcClientPage(expectedToken, request7, response);
|
|
75629
|
+
await Promise.resolve();
|
|
75630
|
+
return { matched: true };
|
|
75023
75631
|
}
|
|
75024
|
-
}
|
|
75025
|
-
`);
|
|
75026
|
-
deps.fs.mkdirPrivate(recordDir);
|
|
75027
|
-
deps.fs.writeFilePrivate(recordFile, `${JSON.stringify({ accessToken, refreshToken, organizationUuid, accountUuid }, null, 2)}
|
|
75028
|
-
`);
|
|
75029
|
-
return { identity: deps.identity, organizationUuid, credentialsFile, claudeJsonFile, recordFile, replaced: existingIsOwn };
|
|
75632
|
+
};
|
|
75030
75633
|
}
|
|
75031
75634
|
|
|
75032
75635
|
// src/frontdoor/rcStream.ts
|
|
@@ -75649,7 +76252,6 @@ function appendLog(paths, line) {
|
|
|
75649
76252
|
import_node_fs13.default.appendFileSync(paths.frontdoorLogPath, `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
|
|
75650
76253
|
`);
|
|
75651
76254
|
}
|
|
75652
|
-
var MINTED_TOKEN_RANDOM_BYTES = 32;
|
|
75653
76255
|
function rcObservingResolver(resolve, tracker, hub) {
|
|
75654
76256
|
return async (request7) => {
|
|
75655
76257
|
const resolution = await resolve(request7);
|
|
@@ -75681,8 +76283,8 @@ function realFrontDoorSupervisorPorts(paths) {
|
|
|
75681
76283
|
};
|
|
75682
76284
|
const rcSelfHostSurface = rcSelfHost ? createRcSelfHostSurface({
|
|
75683
76285
|
now: () => Date.now(),
|
|
75684
|
-
newUuid:
|
|
75685
|
-
randomToken: () => (0,
|
|
76286
|
+
newUuid: import_node_crypto15.randomUUID,
|
|
76287
|
+
randomToken: () => (0, import_node_crypto15.randomBytes)(MINTED_TOKEN_RANDOM_BYTES).toString("base64url"),
|
|
75686
76288
|
credentialRecord: () => readRcSelfHostRecord(realFarmFs, paths.frontdoorDir),
|
|
75687
76289
|
webFetch: realRcWebFetch({ allowPrivate: rcWebFetchAllowsPrivateFromEnv(process.env) }),
|
|
75688
76290
|
log
|
|
@@ -75701,7 +76303,7 @@ function realFrontDoorSupervisorPorts(paths) {
|
|
|
75701
76303
|
saveSequenceNum: rcCredentialStore.writeCursor,
|
|
75702
76304
|
dial: rcSelfHost ? lateRcStreamDial(rcSelfHostDialTarget) : realRcStreamDial(),
|
|
75703
76305
|
fanout: rcFanout,
|
|
75704
|
-
newClientId:
|
|
76306
|
+
newClientId: import_node_crypto15.randomUUID,
|
|
75705
76307
|
backoffMs: RC_STREAM_BACKOFF_MS,
|
|
75706
76308
|
sleep: async (ms) => {
|
|
75707
76309
|
await new Promise((resolve) => {
|
|
@@ -75712,7 +76314,7 @@ function realFrontDoorSupervisorPorts(paths) {
|
|
|
75712
76314
|
appendLog(paths, `frontdoor ${String(process.pid)}: ${line}`);
|
|
75713
76315
|
}
|
|
75714
76316
|
});
|
|
75715
|
-
const rcControlToken = (0,
|
|
76317
|
+
const rcControlToken = (0, import_node_crypto15.randomUUID)();
|
|
75716
76318
|
const resolveRoute = rcObservingResolver(
|
|
75717
76319
|
createProviderRouteResolver({
|
|
75718
76320
|
fs: realFsPort,
|
|
@@ -75752,8 +76354,8 @@ function realFrontDoorSupervisorPorts(paths) {
|
|
|
75752
76354
|
now: () => Date.now(),
|
|
75753
76355
|
headroomSocket: () => headroomSocketTarget(realFarmFs, realHeadroomSocketTrust, paths),
|
|
75754
76356
|
// The per-generation capability the direct listener demands: held only in this process's memory, so a loopback process that discovers the direct port still cannot use it.
|
|
75755
|
-
hopSecret: (0,
|
|
75756
|
-
custody: createCredentialCustody(() => (0,
|
|
76357
|
+
hopSecret: (0, import_node_crypto15.randomUUID)(),
|
|
76358
|
+
custody: createCredentialCustody(() => (0, import_node_crypto15.randomUUID)()),
|
|
75757
76359
|
log
|
|
75758
76360
|
});
|
|
75759
76361
|
const onListenerError = (name) => (error62) => {
|
|
@@ -75774,12 +76376,22 @@ function realFrontDoorSupervisorPorts(paths) {
|
|
|
75774
76376
|
startProviderListener: async (preferredPort) => {
|
|
75775
76377
|
const authority = loadCa();
|
|
75776
76378
|
const rcDial = rcSelfHost ? lateRcEventDial(rcSelfHostDialTarget) : realRcEventDial();
|
|
75777
|
-
const rcInject = async (sessionId, text2) => await injectRcUserMessage({ credentialOf: rcTracker.credentialOf, noteSequenceNums: rcTracker.noteSequenceNums, dial: rcDial, newUuid:
|
|
76379
|
+
const rcInject = async (sessionId, text2) => await injectRcUserMessage({ credentialOf: rcTracker.credentialOf, noteSequenceNums: rcTracker.noteSequenceNums, dial: rcDial, newUuid: import_node_crypto15.randomUUID }, sessionId, text2);
|
|
75778
76380
|
const rcAnswer = async (sessionId, requestId, decision) => await answerRcControlRequest({ credentialOf: rcTracker.credentialOf, pendingOf: (id) => rcTracker.pendingOf(id), completePending: rcTracker.completePending, noteSequenceNums: rcTracker.noteSequenceNums, dial: rcDial }, sessionId, requestId, decision);
|
|
75779
|
-
const rcControlRequestDeps = { credentialOf: rcTracker.credentialOf, noteSequenceNums: rcTracker.noteSequenceNums, dial: rcDial, newUuid:
|
|
76381
|
+
const rcControlRequestDeps = { credentialOf: rcTracker.credentialOf, noteSequenceNums: rcTracker.noteSequenceNums, dial: rcDial, newUuid: import_node_crypto15.randomUUID };
|
|
75780
76382
|
const rcInterrupt = async (sessionId) => await interruptRcSession(rcControlRequestDeps, sessionId);
|
|
75781
76383
|
const rcSetModel = async (sessionId, model) => await setRcSessionModel(rcControlRequestDeps, sessionId, model);
|
|
75782
76384
|
const rcSetPermissionMode = async (sessionId, mode) => await setRcSessionPermissionMode(rcControlRequestDeps, sessionId, mode);
|
|
76385
|
+
const rcEndSession = async (sessionId, reason) => await endRcSession(rcControlRequestDeps, sessionId, reason);
|
|
76386
|
+
const rcGetUsage = async (sessionId, skipBehaviors) => await getRcSessionUsage(rcControlRequestDeps, sessionId, skipBehaviors);
|
|
76387
|
+
const rcGetContextUsage = async (sessionId, detail) => await getRcSessionContextUsage(rcControlRequestDeps, sessionId, detail);
|
|
76388
|
+
const rcReadFile = async (sessionId, filePath, options) => await readRcSessionFile(rcControlRequestDeps, sessionId, filePath, options);
|
|
76389
|
+
const rcFileSuggestions = async (sessionId, query) => await suggestRcSessionFiles(rcControlRequestDeps, sessionId, query);
|
|
76390
|
+
const rcKeepAlive = async (sessionId) => await sendRcKeepAlive(rcControlRequestDeps, sessionId);
|
|
76391
|
+
const rcMcpStatus = async (sessionId) => await getRcSessionMcpStatus(rcControlRequestDeps, sessionId);
|
|
76392
|
+
const rcMcpReconnect = async (sessionId, serverName) => await reconnectRcSessionMcpServer(rcControlRequestDeps, sessionId, serverName);
|
|
76393
|
+
const rcMcpAuthenticate = async (sessionId, serverName, redirectUri) => await authenticateRcSessionMcpServer(rcControlRequestDeps, sessionId, serverName, redirectUri);
|
|
76394
|
+
const rcMcpOAuthCallbackUrl = async (sessionId, serverName, callbackUrl) => await submitRcSessionMcpOAuthCallbackUrl(rcControlRequestDeps, sessionId, serverName, callbackUrl);
|
|
75783
76395
|
const controlDeps = {
|
|
75784
76396
|
usageSnapshots: () => listUsageSnapshots(realFarmFs, paths.usageSnapshotsDir),
|
|
75785
76397
|
usageSnapshotOf: (identity) => readUsageSnapshot(realFarmFs, paths.usageSnapshotsDir, identity),
|
|
@@ -75797,7 +76409,7 @@ function realFrontDoorSupervisorPorts(paths) {
|
|
|
75797
76409
|
},
|
|
75798
76410
|
log,
|
|
75799
76411
|
mintLeaf(authority, LOOPBACK_LEAF_NAMES, /* @__PURE__ */ new Date()),
|
|
75800
|
-
createRcControlHandler({ expectedToken: rcControlToken, list: rcTracker.list, statusOf: rcTracker.statusOf, pendingOf: rcTracker.pendingOf, inject: rcInject, answer: rcAnswer, interrupt: rcInterrupt, setModel: rcSetModel, setPermissionMode: rcSetPermissionMode }),
|
|
76412
|
+
createRcControlHandler({ expectedToken: rcControlToken, list: rcTracker.list, statusOf: rcTracker.statusOf, pendingOf: rcTracker.pendingOf, inject: rcInject, answer: rcAnswer, interrupt: rcInterrupt, setModel: rcSetModel, setPermissionMode: rcSetPermissionMode, endSession: rcEndSession, getUsage: rcGetUsage, getContextUsage: rcGetContextUsage, readFile: rcReadFile, fileSuggestions: rcFileSuggestions, keepAlive: rcKeepAlive, mcpStatus: rcMcpStatus, mcpReconnect: rcMcpReconnect, mcpAuthenticate: rcMcpAuthenticate, mcpOAuthCallbackUrl: rcMcpOAuthCallbackUrl }),
|
|
75801
76413
|
[
|
|
75802
76414
|
createDoorApiNodeHandler({
|
|
75803
76415
|
expectedToken: rcControlToken,
|
|
@@ -75809,6 +76421,16 @@ function realFrontDoorSupervisorPorts(paths) {
|
|
|
75809
76421
|
interrupt: rcInterrupt,
|
|
75810
76422
|
setModel: rcSetModel,
|
|
75811
76423
|
setPermissionMode: rcSetPermissionMode,
|
|
76424
|
+
endSession: rcEndSession,
|
|
76425
|
+
getUsage: rcGetUsage,
|
|
76426
|
+
getContextUsage: rcGetContextUsage,
|
|
76427
|
+
readFile: rcReadFile,
|
|
76428
|
+
fileSuggestions: rcFileSuggestions,
|
|
76429
|
+
keepAlive: rcKeepAlive,
|
|
76430
|
+
mcpStatus: rcMcpStatus,
|
|
76431
|
+
mcpReconnect: rcMcpReconnect,
|
|
76432
|
+
mcpAuthenticate: rcMcpAuthenticate,
|
|
76433
|
+
mcpOAuthCallbackUrl: rcMcpOAuthCallbackUrl,
|
|
75812
76434
|
fanout: rcFanout,
|
|
75813
76435
|
...controlDeps,
|
|
75814
76436
|
events: doorEvents
|
|
@@ -75828,7 +76450,7 @@ function realFrontDoorSupervisorPorts(paths) {
|
|
|
75828
76450
|
},
|
|
75829
76451
|
startConnectListener: async (preferredPort) => {
|
|
75830
76452
|
const wantedTransparentPort = process.env.AGENT_SHIM_TRANSPARENT_SURFACE === void 0 ? void 0 : Number(process.env.AGENT_SHIM_TRANSPARENT_SURFACE);
|
|
75831
|
-
const transparentCapability = wantedTransparentPort === void 0 || Number.isNaN(wantedTransparentPort) ? void 0 : (0,
|
|
76453
|
+
const transparentCapability = wantedTransparentPort === void 0 || Number.isNaN(wantedTransparentPort) ? void 0 : (0, import_node_crypto15.randomUUID)();
|
|
75832
76454
|
if (transparentCapability !== void 0) {
|
|
75833
76455
|
writeFrontDoorSession(realFarmFs, paths.frontdoorSessionsDir, { pid: process.pid, startedAt: Date.now(), token: transparentCapability });
|
|
75834
76456
|
}
|
|
@@ -75885,110 +76507,28 @@ function realFrontDoorSupervisorPorts(paths) {
|
|
|
75885
76507
|
rcSelfHostSurface?.close();
|
|
75886
76508
|
await server.close();
|
|
75887
76509
|
}
|
|
75888
|
-
};
|
|
75889
|
-
},
|
|
75890
|
-
startDirectListener: async (preferredPort) => {
|
|
75891
|
-
const server = createFrontDoorServer(async (request7) => {
|
|
75892
|
-
await serveRouted(request7, pipelines.direct);
|
|
75893
|
-
}, log);
|
|
75894
|
-
const handle = await listenFrontDoor(server, {
|
|
75895
|
-
...preferredPort === void 0 ? {} : { preferredPort },
|
|
75896
|
-
onBound: (port) => {
|
|
75897
|
-
directPort = port;
|
|
75898
|
-
},
|
|
75899
|
-
onError: onListenerError("direct")
|
|
75900
|
-
});
|
|
75901
|
-
return { port: handle.port, close: handle.close };
|
|
75902
|
-
},
|
|
75903
|
-
closeRcStreamHub: rcHub.close,
|
|
75904
|
-
// The launch lifecycle publisher over the same backbone the typed API serves: the tick's registry facts go in here, and the door's own register, prune and end moments come out as source-tagged events.
|
|
75905
|
-
observeLaunchRegistry: createLaunchEventPublisher(doorEvents, () => Date.now()).observe,
|
|
75906
|
-
log: (line) => {
|
|
75907
|
-
appendLog(paths, line);
|
|
75908
|
-
}
|
|
75909
|
-
};
|
|
75910
|
-
}
|
|
75911
|
-
function formatRcSessionList(sessions) {
|
|
75912
|
-
if (sessions.length === 0) {
|
|
75913
|
-
return ["no Remote Control sessions observed"];
|
|
75914
|
-
}
|
|
75915
|
-
return sessions.map((session) => `${session.id} created ${new Date(session.createdAt).toISOString()} last seen ${new Date(session.lastSeenAt).toISOString()}`);
|
|
75916
|
-
}
|
|
75917
|
-
function formatRcSessionStatus(statuses) {
|
|
75918
|
-
if (statuses.length === 0) {
|
|
75919
|
-
return ["no Remote Control sessions observed"];
|
|
75920
|
-
}
|
|
75921
|
-
const lines = [];
|
|
75922
|
-
for (const status of statuses) {
|
|
75923
|
-
lines.push(`${status.id} created ${new Date(status.createdAt).toISOString()} last seen ${new Date(status.lastSeenAt).toISOString()}`);
|
|
75924
|
-
const worker = status.workerState === void 0 && status.workerIdleSeconds === void 0 ? "not observed" : [
|
|
75925
|
-
status.workerState === void 0 ? "state unknown" : `state ${status.workerState.value} since ${new Date(status.workerState.observedAt).toISOString()}`,
|
|
75926
|
-
status.workerIdleSeconds === void 0 ? "idle unknown" : `idle ${String(status.workerIdleSeconds.value)} s as of ${new Date(status.workerIdleSeconds.observedAt).toISOString()}`
|
|
75927
|
-
].join(", ");
|
|
75928
|
-
lines.push(` worker: ${worker}`);
|
|
75929
|
-
if (status.pending.length === 0) {
|
|
75930
|
-
lines.push(" pending: none");
|
|
75931
|
-
} else {
|
|
75932
|
-
lines.push(" pending:");
|
|
75933
|
-
for (const pending of status.pending) {
|
|
75934
|
-
lines.push(...formatRcPendingList([pending]).map((line) => ` ${line}`));
|
|
75935
|
-
}
|
|
76510
|
+
};
|
|
76511
|
+
},
|
|
76512
|
+
startDirectListener: async (preferredPort) => {
|
|
76513
|
+
const server = createFrontDoorServer(async (request7) => {
|
|
76514
|
+
await serveRouted(request7, pipelines.direct);
|
|
76515
|
+
}, log);
|
|
76516
|
+
const handle = await listenFrontDoor(server, {
|
|
76517
|
+
...preferredPort === void 0 ? {} : { preferredPort },
|
|
76518
|
+
onBound: (port) => {
|
|
76519
|
+
directPort = port;
|
|
76520
|
+
},
|
|
76521
|
+
onError: onListenerError("direct")
|
|
76522
|
+
});
|
|
76523
|
+
return { port: handle.port, close: handle.close };
|
|
76524
|
+
},
|
|
76525
|
+
closeRcStreamHub: rcHub.close,
|
|
76526
|
+
// The launch lifecycle publisher over the same backbone the typed API serves: the tick's registry facts go in here, and the door's own register, prune and end moments come out as source-tagged events.
|
|
76527
|
+
observeLaunchRegistry: createLaunchEventPublisher(doorEvents, () => Date.now()).observe,
|
|
76528
|
+
log: (line) => {
|
|
76529
|
+
appendLog(paths, line);
|
|
75936
76530
|
}
|
|
75937
|
-
}
|
|
75938
|
-
return lines;
|
|
75939
|
-
}
|
|
75940
|
-
function formatRcPendingList(pending) {
|
|
75941
|
-
if (pending.length === 0) {
|
|
75942
|
-
return ["no pending control requests observed"];
|
|
75943
|
-
}
|
|
75944
|
-
return pending.map((request7) => `${request7.sessionId} ${request7.requestId} ${request7.type}${request7.summary === "" ? "" : ` ${request7.summary}`} observed ${new Date(request7.observedAt).toISOString()}`);
|
|
75945
|
-
}
|
|
75946
|
-
function eventExcerpt(text2) {
|
|
75947
|
-
return text2.length <= RC_PENDING_SUMMARY_EXCERPT_CHARS ? text2 : `${text2.slice(0, RC_PENDING_SUMMARY_EXCERPT_CHARS)}...`;
|
|
75948
|
-
}
|
|
75949
|
-
function formatRcStreamEvent(event) {
|
|
75950
|
-
const { envelope } = event;
|
|
75951
|
-
const sketch = envelope.payload === void 0 ? "" : ` ${eventExcerpt(JSON.stringify(envelope.payload))}`;
|
|
75952
|
-
return `${event.session} ${envelope.event_type} sequence_num ${String(envelope.sequence_num)} source ${envelope.source}${sketch}`;
|
|
75953
|
-
}
|
|
75954
|
-
function formatRcSelfHostMint(result) {
|
|
75955
|
-
return [
|
|
75956
|
-
`identity: ${result.identity} (organisation ${result.organizationUuid})`,
|
|
75957
|
-
`credential: ${result.credentialsFile}${result.replaced ? " (replacing this door's previous mint)" : ""}`,
|
|
75958
|
-
`account block: ${result.claudeJsonFile} (oauthAccount and the feature-cache seed merged in)`,
|
|
75959
|
-
`door record: ${result.recordFile} (the copy the door's served surface authenticates against)`,
|
|
75960
|
-
"next: start the door with AGENT_SHIM_FRONTDOOR_RC_SELF_HOST=1 and its transparent surface pointed at the interception port, then launch this identity and switch Remote Control on; no claude.ai login is involved"
|
|
75961
|
-
];
|
|
75962
|
-
}
|
|
75963
|
-
function frontDoorRcControlFromState(fsPort, paths) {
|
|
75964
|
-
const state = readFrontDoorState(fsPort, paths.frontdoorStateFile);
|
|
75965
|
-
if (state?.port === void 0) {
|
|
75966
|
-
throw new Error("the front door is not serving: Remote Control sessions are observed only while it runs, so start a session through the door first");
|
|
75967
|
-
}
|
|
75968
|
-
const ca = fsPort.readFileUtf8(paths.frontdoorCaCertFile);
|
|
75969
|
-
if (ca === void 0) {
|
|
75970
|
-
throw new Error(`the front door's CA certificate is missing at ${paths.frontdoorCaCertFile}, so its control listener cannot be authenticated`);
|
|
75971
|
-
}
|
|
75972
|
-
const token = fsPort.readFileUtf8(paths.frontdoorControlTokenFile)?.trim();
|
|
75973
|
-
if (token === void 0 || token === "") {
|
|
75974
|
-
throw new Error(`the serving front door's control token is missing at ${paths.frontdoorControlTokenFile}`);
|
|
75975
|
-
}
|
|
75976
|
-
return frontDoorRcControl(realRcControlTransport(state.port, ca), token);
|
|
75977
|
-
}
|
|
75978
|
-
function frontDoorRcApiFromState(fsPort, paths) {
|
|
75979
|
-
const state = readFrontDoorState(fsPort, paths.frontdoorStateFile);
|
|
75980
|
-
if (state?.port === void 0) {
|
|
75981
|
-
throw new Error("the front door is not serving: Remote Control sessions are observed only while it runs, so start a session through the door first");
|
|
75982
|
-
}
|
|
75983
|
-
const ca = fsPort.readFileUtf8(paths.frontdoorCaCertFile);
|
|
75984
|
-
if (ca === void 0) {
|
|
75985
|
-
throw new Error(`the front door's CA certificate is missing at ${paths.frontdoorCaCertFile}, so its typed API cannot be authenticated`);
|
|
75986
|
-
}
|
|
75987
|
-
const token = fsPort.readFileUtf8(paths.frontdoorControlTokenFile)?.trim();
|
|
75988
|
-
if (token === void 0 || token === "") {
|
|
75989
|
-
throw new Error(`the serving front door's control token is missing at ${paths.frontdoorControlTokenFile}`);
|
|
75990
|
-
}
|
|
75991
|
-
return frontDoorRcApiClient(state.port, ca, token);
|
|
76531
|
+
};
|
|
75992
76532
|
}
|
|
75993
76533
|
function registerFrontDoorCommand(program2, deps) {
|
|
75994
76534
|
const { paths } = deps;
|
|
@@ -76008,186 +76548,7 @@ function registerFrontDoorCommand(program2, deps) {
|
|
|
76008
76548
|
}),
|
|
76009
76549
|
["agent-shim frontdoor status", "agent-shim frontdoor status --json"]
|
|
76010
76550
|
);
|
|
76011
|
-
|
|
76012
|
-
frontdoor.command("rc").description("Observe the Remote Control sessions passing through the front door, watch the stream their attached client receives, send a prompt into one, answer its pending control requests, and steer one with the client half's own control requests (interrupt, model, permission mode)."),
|
|
76013
|
-
["agent-shim frontdoor rc list", "agent-shim frontdoor rc pending"]
|
|
76014
|
-
);
|
|
76015
|
-
withExamples(
|
|
76016
|
-
rc.command("list").description("List the Remote Control sessions the front door has observed, with when each was created and last seen. Read-only.").option("--json", "Print the sessions as JSON.").action(async (options) => {
|
|
76017
|
-
const sessions = await frontDoorRcControlFromState(realFarmFs, paths).listSessions();
|
|
76018
|
-
if (options.json === true) {
|
|
76019
|
-
printJson({ sessions });
|
|
76020
|
-
return;
|
|
76021
|
-
}
|
|
76022
|
-
for (const line of formatRcSessionList(sessions)) {
|
|
76023
|
-
console.log(line);
|
|
76024
|
-
}
|
|
76025
|
-
}),
|
|
76026
|
-
["agent-shim frontdoor rc list", "agent-shim frontdoor rc list --json"]
|
|
76027
|
-
);
|
|
76028
|
-
withExamples(
|
|
76029
|
-
rc.command("status").description("Report each observed Remote Control session's worker state and idle from its latest observed heartbeat and registration, with its pending control requests. Read-only.").argument("[session]", "One cse_ session id, as `frontdoor rc list` shows it; every observed session when omitted.").option("--json", "Print the statuses as JSON.").action(async (session, options) => {
|
|
76030
|
-
const statuses = await frontDoorRcControlFromState(realFarmFs, paths).statusOf(session);
|
|
76031
|
-
if (options.json === true) {
|
|
76032
|
-
printJson({ statuses });
|
|
76033
|
-
return;
|
|
76034
|
-
}
|
|
76035
|
-
for (const line of formatRcSessionStatus(statuses)) {
|
|
76036
|
-
console.log(line);
|
|
76037
|
-
}
|
|
76038
|
-
}),
|
|
76039
|
-
["agent-shim frontdoor rc status", "agent-shim frontdoor rc status cse_00000000-0000-4000-8000-000000000000 --json"]
|
|
76040
|
-
);
|
|
76041
|
-
withExamples(
|
|
76042
|
-
rc.command("pending").description("List the control requests (tool and plan approvals above all) the front door has observed and that are still awaiting an answer. Read-only.").argument("[session]", "One cse_ session id, as `frontdoor rc list` shows it; every observed session when omitted.").option("--json", "Print the pending requests as JSON.").action(async (session, options) => {
|
|
76043
|
-
const pending = await frontDoorRcControlFromState(realFarmFs, paths).pendingOf(session);
|
|
76044
|
-
if (options.json === true) {
|
|
76045
|
-
printJson({ pending });
|
|
76046
|
-
return;
|
|
76047
|
-
}
|
|
76048
|
-
for (const line of formatRcPendingList(pending)) {
|
|
76049
|
-
console.log(line);
|
|
76050
|
-
}
|
|
76051
|
-
}),
|
|
76052
|
-
["agent-shim frontdoor rc pending", "agent-shim frontdoor rc pending --json"]
|
|
76053
|
-
);
|
|
76054
|
-
withExamples(
|
|
76055
|
-
rc.command("send").description("Send a text prompt into one observed Remote Control session, arriving as a message from an attached client would.").requiredOption("--session <id>", "The cse_ session id, as `frontdoor rc list` shows it.").requiredOption("--text <text>", "The prompt text to deliver.").option("--json", "Print the delivery result as JSON.").action(async (options) => {
|
|
76056
|
-
const result = await frontDoorRcControlFromState(realFarmFs, paths).sendPrompt(options.session, options.text);
|
|
76057
|
-
if (!result.ok) {
|
|
76058
|
-
throw new Error(result.message);
|
|
76059
|
-
}
|
|
76060
|
-
if (options.json === true) {
|
|
76061
|
-
printJson({ session: options.session, sequenceNums: result.sequenceNums });
|
|
76062
|
-
return;
|
|
76063
|
-
}
|
|
76064
|
-
console.log(`sent to ${options.session}: sequence_num ${result.sequenceNums.join(", ")}`);
|
|
76065
|
-
}),
|
|
76066
|
-
['agent-shim frontdoor rc send --session cse_00000000-0000-4000-8000-000000000000 --text "run the tests"']
|
|
76067
|
-
);
|
|
76068
|
-
withExamples(
|
|
76069
|
-
rc.command("answer").description("Answer one pending control request on an observed session, approving or denying it as an attached client's approval would.").requiredOption("--session <id>", "The cse_ session id, as `frontdoor rc list` shows it.").requiredOption("--request <id>", "The control request's id, as `frontdoor rc pending` shows it.").option("--approve", "Approve the request.").option("--deny", "Deny the request.").option("--text <text>", "The denial message the worker sees; applies to --deny only.").option("--json", "Print the delivery result as JSON.").action(async (options) => {
|
|
76070
|
-
if (options.approve === options.deny) {
|
|
76071
|
-
throw new UsageError("answer exactly one way: pass --approve or --deny, not both and not neither");
|
|
76072
|
-
}
|
|
76073
|
-
if (options.approve === true && options.text !== void 0) {
|
|
76074
|
-
throw new UsageError("--text is the denial message; an approval carries no text (the protocol's allow result has no message field)");
|
|
76075
|
-
}
|
|
76076
|
-
const result = await frontDoorRcControlFromState(realFarmFs, paths).answerRequest(options.session, options.request, { approve: options.approve === true, message: options.deny === true ? options.text : void 0 });
|
|
76077
|
-
if (!result.ok) {
|
|
76078
|
-
throw new Error(result.message);
|
|
76079
|
-
}
|
|
76080
|
-
if (options.json === true) {
|
|
76081
|
-
printJson({ session: options.session, request: options.request, sequenceNums: result.sequenceNums });
|
|
76082
|
-
return;
|
|
76083
|
-
}
|
|
76084
|
-
console.log(`${options.approve === true ? "approved" : "denied"} ${options.request} on ${options.session}: sequence_num ${result.sequenceNums.join(", ")}`);
|
|
76085
|
-
}),
|
|
76086
|
-
[
|
|
76087
|
-
"agent-shim frontdoor rc answer --session cse_00000000-0000-4000-8000-000000000000 --request req_00000000-0000-4000-8000-000000000000 --approve",
|
|
76088
|
-
'agent-shim frontdoor rc answer --session cse_00000000-0000-4000-8000-000000000000 --request req_00000000-0000-4000-8000-000000000000 --deny --text "not today"'
|
|
76089
|
-
]
|
|
76090
|
-
);
|
|
76091
|
-
withExamples(
|
|
76092
|
-
rc.command("interrupt").description("Interrupt one observed Remote Control session's running turn, as an attached client's stop button would. Queued commands survive the interrupt.").requiredOption("--session <id>", "The cse_ session id, as `frontdoor rc list` shows it.").option("--json", "Print the delivery result as JSON.").action(async (options) => {
|
|
76093
|
-
const result = await frontDoorRcControlFromState(realFarmFs, paths).interruptSession(options.session);
|
|
76094
|
-
if (!result.ok) {
|
|
76095
|
-
throw new Error(result.message);
|
|
76096
|
-
}
|
|
76097
|
-
if (options.json === true) {
|
|
76098
|
-
printJson({ session: options.session, sequenceNums: result.sequenceNums });
|
|
76099
|
-
return;
|
|
76100
|
-
}
|
|
76101
|
-
console.log(`interrupted ${options.session}: sequence_num ${result.sequenceNums.join(", ")}`);
|
|
76102
|
-
}),
|
|
76103
|
-
["agent-shim frontdoor rc interrupt --session cse_00000000-0000-4000-8000-000000000000"]
|
|
76104
|
-
);
|
|
76105
|
-
withExamples(
|
|
76106
|
-
rc.command("set-model").description("Set the model one observed Remote Control session's subsequent turns use, as an attached client's model picker would.").requiredOption("--session <id>", "The cse_ session id, as `frontdoor rc list` shows it.").requiredOption("--model <model>", "The model id to switch the session to.").option("--json", "Print the delivery result as JSON.").action(async (options) => {
|
|
76107
|
-
const result = await frontDoorRcControlFromState(realFarmFs, paths).setModel(options.session, options.model);
|
|
76108
|
-
if (!result.ok) {
|
|
76109
|
-
throw new Error(result.message);
|
|
76110
|
-
}
|
|
76111
|
-
if (options.json === true) {
|
|
76112
|
-
printJson({ session: options.session, model: options.model, sequenceNums: result.sequenceNums });
|
|
76113
|
-
return;
|
|
76114
|
-
}
|
|
76115
|
-
console.log(`set ${options.session} to model ${options.model}: sequence_num ${result.sequenceNums.join(", ")}`);
|
|
76116
|
-
}),
|
|
76117
|
-
["agent-shim frontdoor rc set-model --session cse_00000000-0000-4000-8000-000000000000 --model claude-opus-5-5"]
|
|
76118
|
-
);
|
|
76119
|
-
withExamples(
|
|
76120
|
-
rc.command("set-permission-mode").description("Set one observed Remote Control session's permission mode, as an attached client's mode switcher would.").requiredOption("--session <id>", "The cse_ session id, as `frontdoor rc list` shows it.").requiredOption("--mode <mode>", `The permission mode to set; one of the SDK's own modes (${RC_PERMISSION_MODES.join(", ")}).`).option("--json", "Print the delivery result as JSON.").action(async (options) => {
|
|
76121
|
-
if (!isRcPermissionMode(options.mode)) {
|
|
76122
|
-
throw new UsageError(`--mode must be one of the SDK's own permission modes (${RC_PERMISSION_MODES.join(", ")}), not "${options.mode}"`);
|
|
76123
|
-
}
|
|
76124
|
-
const result = await frontDoorRcControlFromState(realFarmFs, paths).setPermissionMode(options.session, options.mode);
|
|
76125
|
-
if (!result.ok) {
|
|
76126
|
-
throw new Error(result.message);
|
|
76127
|
-
}
|
|
76128
|
-
if (options.json === true) {
|
|
76129
|
-
printJson({ session: options.session, mode: options.mode, sequenceNums: result.sequenceNums });
|
|
76130
|
-
return;
|
|
76131
|
-
}
|
|
76132
|
-
console.log(`set ${options.session} to permission mode ${options.mode}: sequence_num ${result.sequenceNums.join(", ")}`);
|
|
76133
|
-
}),
|
|
76134
|
-
[
|
|
76135
|
-
"agent-shim frontdoor rc set-permission-mode --session cse_00000000-0000-4000-8000-000000000000 --mode plan",
|
|
76136
|
-
"agent-shim frontdoor rc set-permission-mode --session cse_00000000-0000-4000-8000-000000000000 --mode acceptEdits"
|
|
76137
|
-
]
|
|
76138
|
-
);
|
|
76139
|
-
withExamples(
|
|
76140
|
-
rc.command("watch").description("Print the observed sessions' client read stream events as they arrive, the approvals the door's held stream receives above all, until Ctrl-C leaves. Read-only.").argument("[session]", "One cse_ session id, as `frontdoor rc list` shows it; every observed session when omitted.").option("--json", "Print each event as one JSON line as it arrives.").action(async (session, options) => {
|
|
76141
|
-
const client = frontDoorRcApiFromState(realFarmFs, paths);
|
|
76142
|
-
const leave = new AbortController();
|
|
76143
|
-
const onInterrupt = () => {
|
|
76144
|
-
leave.abort();
|
|
76145
|
-
};
|
|
76146
|
-
process.once("SIGINT", onInterrupt);
|
|
76147
|
-
try {
|
|
76148
|
-
for await (const event of await client.rc.subscribe(session === void 0 ? {} : { session }, { signal: leave.signal })) {
|
|
76149
|
-
if (options.json === true) {
|
|
76150
|
-
console.log(JSON.stringify(event));
|
|
76151
|
-
continue;
|
|
76152
|
-
}
|
|
76153
|
-
console.log(formatRcStreamEvent(event));
|
|
76154
|
-
}
|
|
76155
|
-
} catch (error62) {
|
|
76156
|
-
if (!leave.signal.aborted) {
|
|
76157
|
-
throw error62;
|
|
76158
|
-
}
|
|
76159
|
-
} finally {
|
|
76160
|
-
process.off("SIGINT", onInterrupt);
|
|
76161
|
-
}
|
|
76162
|
-
}),
|
|
76163
|
-
["agent-shim frontdoor rc watch", "agent-shim frontdoor rc watch cse_00000000-0000-4000-8000-000000000000 --json"]
|
|
76164
|
-
);
|
|
76165
|
-
const selfhost = withExamples(
|
|
76166
|
-
rc.command("selfhost").description("Mint the local credential behind the door's self-hosted Remote Control surface: the opt-in mode (AGENT_SHIM_FRONTDOOR_RC_SELF_HOST=1 in the door's environment) where the door serves the CCR surface itself and a session activates Remote Control with no Anthropic credential anywhere."),
|
|
76167
|
-
["agent-shim frontdoor rc selfhost mint rig"]
|
|
76168
|
-
);
|
|
76169
|
-
withExamples(
|
|
76170
|
-
selfhost.command("mint").description("Mint the local OAuth credential, account block and feature-cache seed one identity needs to activate Remote Control against the door's own served surface, and record the door-side copy the surface authenticates against. Refuses to replace a real claude.ai login unless --force. Never prints a token.").argument("<identity>", "The identity to mint into, as `agent-shim identity list` names it.").option("--force", "Replace an existing OAuth credential this door did not mint.").option("--json", "Print the mint result as JSON.").action((identity, options) => {
|
|
76171
|
-
const result = mintRcSelfHostCredential({
|
|
76172
|
-
fs: realFarmFs,
|
|
76173
|
-
identitiesDir: paths.identitiesDir,
|
|
76174
|
-
frontdoorDir: paths.frontdoorDir,
|
|
76175
|
-
identity,
|
|
76176
|
-
newUuid: import_node_crypto14.randomUUID,
|
|
76177
|
-
randomToken: () => (0, import_node_crypto14.randomBytes)(MINTED_TOKEN_RANDOM_BYTES).toString("base64url"),
|
|
76178
|
-
force: options.force === true,
|
|
76179
|
-
now: () => Date.now()
|
|
76180
|
-
});
|
|
76181
|
-
if (options.json === true) {
|
|
76182
|
-
printJson(result);
|
|
76183
|
-
return;
|
|
76184
|
-
}
|
|
76185
|
-
for (const line of formatRcSelfHostMint(result)) {
|
|
76186
|
-
console.log(line);
|
|
76187
|
-
}
|
|
76188
|
-
}),
|
|
76189
|
-
["agent-shim frontdoor rc selfhost mint rig", "agent-shim frontdoor rc selfhost mint rig --force"]
|
|
76190
|
-
);
|
|
76551
|
+
registerRcCommand(frontdoor, paths);
|
|
76191
76552
|
program2.command("__frontdoor-supervisor", { hidden: true }).description("Internal: serve the front-door routing listeners. Started by the launcher; never run by hand.").allowUnknownOption().action(async () => {
|
|
76192
76553
|
const idleShutdownMinutes = readGlobalConfig(paths)?.frontdoor?.idleShutdownMinutes ?? FRONTDOOR_DEFAULT_IDLE_SHUTDOWN_MINUTES;
|
|
76193
76554
|
process.on("SIGTERM", () => {
|
|
@@ -76679,7 +77040,7 @@ var import_node_child_process6 = require("node:child_process");
|
|
|
76679
77040
|
var import_node_fs15 = __toESM(require("node:fs"), 1);
|
|
76680
77041
|
|
|
76681
77042
|
// src/frontdoor/ensure.ts
|
|
76682
|
-
var
|
|
77043
|
+
var import_node_crypto16 = require("node:crypto");
|
|
76683
77044
|
var FRONTDOOR_START_TIMEOUT_MS = 3e4;
|
|
76684
77045
|
var FRONTDOOR_LAUNCHER_POLL_MS = 100;
|
|
76685
77046
|
var FrontDoorStartError = class extends CliError {
|
|
@@ -76713,7 +77074,7 @@ function ensureFrontDoor(params) {
|
|
|
76713
77074
|
continue;
|
|
76714
77075
|
}
|
|
76715
77076
|
if (verdict.ok) {
|
|
76716
|
-
const token = (0,
|
|
77077
|
+
const token = (0, import_node_crypto16.randomUUID)();
|
|
76717
77078
|
writeFrontDoorSession(ports.fs, paths.frontdoorSessionsDir, { pid: params.launcherPid, startedAt: ports.now(), token });
|
|
76718
77079
|
return { port: state.port, connectPort: state.connectPort, token };
|
|
76719
77080
|
}
|
|
@@ -76817,7 +77178,7 @@ function probeFrontDoorSync(port, caPem) {
|
|
|
76817
77178
|
}
|
|
76818
77179
|
|
|
76819
77180
|
// src/frontdoor/trust.ts
|
|
76820
|
-
var
|
|
77181
|
+
var import_node_crypto17 = require("node:crypto");
|
|
76821
77182
|
var import_node_path38 = __toESM(require("node:path"), 1);
|
|
76822
77183
|
var BUNDLE_NAME_HEX_DIGITS = 16;
|
|
76823
77184
|
function resolveTrustBundle(params) {
|
|
@@ -76840,7 +77201,7 @@ function resolveTrustBundle(params) {
|
|
|
76840
77201
|
}
|
|
76841
77202
|
const combined = `${inheritedPem.trimEnd()}
|
|
76842
77203
|
${caPem}`;
|
|
76843
|
-
const bundle = import_node_path38.default.join(bundlesDir, `${(0,
|
|
77204
|
+
const bundle = import_node_path38.default.join(bundlesDir, `${(0, import_node_crypto17.createHash)("sha256").update(combined).digest("hex").slice(0, BUNDLE_NAME_HEX_DIGITS)}.pem`);
|
|
76844
77205
|
if (!fs20.exists(bundle)) {
|
|
76845
77206
|
fs20.mkdirp(bundlesDir);
|
|
76846
77207
|
fs20.writeFileAtomic(bundle, combined);
|
|
@@ -77551,7 +77912,7 @@ var import_node_os7 = __toESM(require("node:os"), 1);
|
|
|
77551
77912
|
var import_node_path41 = __toESM(require("node:path"), 1);
|
|
77552
77913
|
|
|
77553
77914
|
// src/update/update.ts
|
|
77554
|
-
var
|
|
77915
|
+
var import_node_crypto18 = require("node:crypto");
|
|
77555
77916
|
var import_node_path40 = __toESM(require("node:path"), 1);
|
|
77556
77917
|
var LATEST_RELEASE_URL = "https://github.com/ExaDev/agent-shim/releases/latest";
|
|
77557
77918
|
var DOWNLOAD_BASE_URL = "https://github.com/ExaDev/agent-shim/releases/latest/download";
|
|
@@ -77706,11 +78067,11 @@ async function downloadAndInstall(ports, asset, target) {
|
|
|
77706
78067
|
throw new UpdateDownloadError(`the downloaded files for ${asset} are missing from ${tempDir}`);
|
|
77707
78068
|
}
|
|
77708
78069
|
const expected = expectedSha256(sidecar);
|
|
77709
|
-
const actual = (0,
|
|
78070
|
+
const actual = (0, import_node_crypto18.createHash)("sha256").update(downloaded).digest("hex");
|
|
77710
78071
|
if (expected === void 0 || expected !== actual) {
|
|
77711
78072
|
throw new UpdateDownloadError(`checksum mismatch for ${asset} (expected ${expected ?? "(none)"}, got ${actual})`);
|
|
77712
78073
|
}
|
|
77713
|
-
const tempBinary = import_node_path40.default.join(import_node_path40.default.dirname(target), `.${import_node_path40.default.basename(target)}.${String(process.pid)}.${(0,
|
|
78074
|
+
const tempBinary = import_node_path40.default.join(import_node_path40.default.dirname(target), `.${import_node_path40.default.basename(target)}.${String(process.pid)}.${(0, import_node_crypto18.randomUUID)()}.tmp`);
|
|
77714
78075
|
ports.fs.writeFileBytes(tempBinary, downloaded);
|
|
77715
78076
|
try {
|
|
77716
78077
|
ports.fs.chmod(tempBinary, EXECUTABLE_MODE);
|
|
@@ -78219,10 +78580,10 @@ function buildProgram(deps) {
|
|
|
78219
78580
|
|
|
78220
78581
|
// src/launchWiring.ts
|
|
78221
78582
|
var import_node_os9 = __toESM(require("node:os"), 1);
|
|
78222
|
-
var
|
|
78583
|
+
var import_node_crypto20 = require("node:crypto");
|
|
78223
78584
|
|
|
78224
78585
|
// src/launcher.ts
|
|
78225
|
-
var
|
|
78586
|
+
var import_node_crypto19 = require("node:crypto");
|
|
78226
78587
|
var import_node_path43 = __toESM(require("node:path"), 1);
|
|
78227
78588
|
|
|
78228
78589
|
// src/launcher/cliOverride.ts
|
|
@@ -78846,7 +79207,7 @@ function prepareLaunch(params) {
|
|
|
78846
79207
|
...identityCredential === void 0 ? {} : { identityCredential },
|
|
78847
79208
|
...frontDoor === void 0 ? {} : { frontdoor: frontDoor },
|
|
78848
79209
|
...headroom === void 0 ? {} : { headroom },
|
|
78849
|
-
sessionId: (0,
|
|
79210
|
+
sessionId: (0, import_node_crypto19.randomUUID)()
|
|
78850
79211
|
});
|
|
78851
79212
|
const releases = [...headroom === void 0 || headroomPort === void 0 ? [] : [headroomPort.release], ...frontDoorRelease === void 0 ? [] : [frontDoorRelease]];
|
|
78852
79213
|
const release = () => {
|
|
@@ -78893,7 +79254,7 @@ function buildFarmRuntime(paths, cwd) {
|
|
|
78893
79254
|
...cliOverride === void 0 ? {} : { cliOverride }
|
|
78894
79255
|
}).input,
|
|
78895
79256
|
now: () => Date.now(),
|
|
78896
|
-
uniqueSuffix: `${String(process.pid)}.${(0,
|
|
79257
|
+
uniqueSuffix: `${String(process.pid)}.${(0, import_node_crypto20.randomUUID)()}`,
|
|
78897
79258
|
// Zombie-aware on purpose: a previous launcher that crashed out of a resync without releasing the lock may sit unreaped, still answering signal 0 as alive, and must read as a dead holder so this launch takes the lock over instead of timing out.
|
|
78898
79259
|
lock: { pid: process.pid, isRunning: realIsProcessRunning, sleep: realSleepSync }
|
|
78899
79260
|
},
|