agent-shim 8.22.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 +11 -1
- package/dist/cli.cjs +2293 -1821
- package/dist/index.cjs +622 -101
- package/dist/index.mjs +597 -101
- package/dist/types/index.d.ts +416 -22
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -55361,6 +55361,11 @@ var FrontDoorGlobalConfigSchema = external_exports.strictObject({
|
|
|
55361
55361
|
/** How long the front door may sit with no registered sessions before it closes and exits. Defaults to FRONTDOOR_DEFAULT_IDLE_SHUTDOWN_MINUTES. */
|
|
55362
55362
|
idleShutdownMinutes: external_exports.number().int().positive().optional()
|
|
55363
55363
|
});
|
|
55364
|
+
var UPDATE_MODES = ["off", "notify", "auto"];
|
|
55365
|
+
var UpdateGlobalConfigSchema = external_exports.strictObject({
|
|
55366
|
+
/** What a launch does about a newer release of agent-shim itself; see `UPDATE_MODES`. Absent means `off`. */
|
|
55367
|
+
mode: external_exports.enum(UPDATE_MODES).optional()
|
|
55368
|
+
});
|
|
55364
55369
|
var GlobalConfigSchema = external_exports.strictObject({
|
|
55365
55370
|
$schema: external_exports.string().optional(),
|
|
55366
55371
|
defaultConfigProfile: external_exports.string().min(1).optional(),
|
|
@@ -55370,6 +55375,7 @@ var GlobalConfigSchema = external_exports.strictObject({
|
|
|
55370
55375
|
launch: LaunchSchema.optional(),
|
|
55371
55376
|
headroom: HeadroomGlobalConfigSchema.optional(),
|
|
55372
55377
|
frontdoor: FrontDoorGlobalConfigSchema.optional(),
|
|
55378
|
+
update: UpdateGlobalConfigSchema.optional(),
|
|
55373
55379
|
pools: external_exports.record(PoolNameSchema, PoolSchema).optional()
|
|
55374
55380
|
});
|
|
55375
55381
|
var IdentitySchema = external_exports.strictObject({
|
|
@@ -61790,7 +61796,7 @@ function registerIdentityCommand(program2, deps) {
|
|
|
61790
61796
|
// package.json
|
|
61791
61797
|
var package_default = {
|
|
61792
61798
|
name: "agent-shim",
|
|
61793
|
-
version: "8.
|
|
61799
|
+
version: "8.24.0",
|
|
61794
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.",
|
|
61795
61801
|
license: "MIT",
|
|
61796
61802
|
author: "Joseph Mearman <joseph@mearman.co.uk>",
|
|
@@ -64379,12 +64385,12 @@ var SiwcInvalidGrantError = class extends SiwcError {
|
|
|
64379
64385
|
function base64Url(bytes) {
|
|
64380
64386
|
return Buffer.from(bytes).toString("base64url");
|
|
64381
64387
|
}
|
|
64382
|
-
function newPkce(
|
|
64383
|
-
const verifier = base64Url(
|
|
64388
|
+
function newPkce(randomBytes5) {
|
|
64389
|
+
const verifier = base64Url(randomBytes5(SIWC_RANDOM_BYTES));
|
|
64384
64390
|
return { verifier, challenge: (0, import_node_crypto6.createHash)("sha256").update(verifier).digest("base64url") };
|
|
64385
64391
|
}
|
|
64386
|
-
function newOpaqueValue(
|
|
64387
|
-
return base64Url(
|
|
64392
|
+
function newOpaqueValue(randomBytes5) {
|
|
64393
|
+
return base64Url(randomBytes5(SIWC_RANDOM_BYTES));
|
|
64388
64394
|
}
|
|
64389
64395
|
function buildAuthorizeUrl(request7) {
|
|
64390
64396
|
const url2 = new URL(SIWC_AUTHORIZE_URL);
|
|
@@ -65045,7 +65051,7 @@ function registerCodexCommand(program2, deps) {
|
|
|
65045
65051
|
}
|
|
65046
65052
|
|
|
65047
65053
|
// src/frontdoor/commands.ts
|
|
65048
|
-
var
|
|
65054
|
+
var import_node_crypto15 = require("node:crypto");
|
|
65049
65055
|
var import_node_path36 = __toESM(require("node:path"), 1);
|
|
65050
65056
|
var import_node_fs13 = __toESM(require("node:fs"), 1);
|
|
65051
65057
|
|
|
@@ -66542,6 +66548,8 @@ function teeRoutedResponse(response, observer, onSettled) {
|
|
|
66542
66548
|
}
|
|
66543
66549
|
};
|
|
66544
66550
|
}
|
|
66551
|
+
|
|
66552
|
+
// src/frontdoor/rcWrites.ts
|
|
66545
66553
|
function buildRcUserMessagePayload(uuid3, sessionId, text2) {
|
|
66546
66554
|
return {
|
|
66547
66555
|
type: "user",
|
|
@@ -66642,14 +66650,55 @@ var RC_PERMISSION_MODES = ["default", "acceptEdits", "bypassPermissions", "plan"
|
|
|
66642
66650
|
function isRcPermissionMode(value2) {
|
|
66643
66651
|
return RC_PERMISSION_MODES.some((mode) => mode === value2);
|
|
66644
66652
|
}
|
|
66653
|
+
function buildRcControlRequestPayload(requestId, request7) {
|
|
66654
|
+
return { type: "control_request", request_id: requestId, request: request7 };
|
|
66655
|
+
}
|
|
66645
66656
|
function buildRcInterruptPayload(requestId) {
|
|
66646
|
-
return
|
|
66657
|
+
return buildRcControlRequestPayload(requestId, { subtype: "interrupt" });
|
|
66647
66658
|
}
|
|
66648
66659
|
function buildRcSetModelPayload(requestId, model) {
|
|
66649
|
-
return
|
|
66660
|
+
return buildRcControlRequestPayload(requestId, { subtype: "set_model", model });
|
|
66650
66661
|
}
|
|
66651
66662
|
function buildRcSetPermissionModePayload(requestId, mode) {
|
|
66652
|
-
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" };
|
|
66653
66702
|
}
|
|
66654
66703
|
async function answerRcControlRequest(deps, sessionId, requestId, decision) {
|
|
66655
66704
|
const credential = deps.credentialOf(sessionId);
|
|
@@ -66676,7 +66725,7 @@ async function answerRcControlRequest(deps, sessionId, requestId, decision) {
|
|
|
66676
66725
|
}
|
|
66677
66726
|
return result;
|
|
66678
66727
|
}
|
|
66679
|
-
async function
|
|
66728
|
+
async function deliverRcClientPayload(deps, sessionId, payload, action) {
|
|
66680
66729
|
const credential = deps.credentialOf(sessionId);
|
|
66681
66730
|
if (credential === void 0) {
|
|
66682
66731
|
return { ok: false, message: rcSessionNotObservedMessage(sessionId) };
|
|
@@ -66684,12 +66733,12 @@ async function interruptRcSession(deps, sessionId) {
|
|
|
66684
66733
|
if (credential.authorization === void 0) {
|
|
66685
66734
|
return noOAuthCredentialResult(sessionId);
|
|
66686
66735
|
}
|
|
66687
|
-
const body = JSON.stringify(buildRcEventWriteBody(
|
|
66736
|
+
const body = JSON.stringify(buildRcEventWriteBody(payload));
|
|
66688
66737
|
let answer;
|
|
66689
66738
|
try {
|
|
66690
66739
|
answer = await deps.dial.writeEvents(sessionId, rcWriteHeaders(credential), body);
|
|
66691
66740
|
} catch (error62) {
|
|
66692
|
-
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)}` };
|
|
66693
66742
|
}
|
|
66694
66743
|
const result = rcEventWriteResultFromAnswer(answer);
|
|
66695
66744
|
if (result.ok) {
|
|
@@ -66697,50 +66746,89 @@ async function interruptRcSession(deps, sessionId) {
|
|
|
66697
66746
|
}
|
|
66698
66747
|
return result;
|
|
66699
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
|
+
}
|
|
66700
66754
|
async function setRcSessionModel(deps, sessionId, model) {
|
|
66701
66755
|
if (model === "") {
|
|
66702
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" };
|
|
66703
66757
|
}
|
|
66704
|
-
const
|
|
66705
|
-
|
|
66706
|
-
|
|
66707
|
-
|
|
66708
|
-
|
|
66709
|
-
|
|
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" };
|
|
66710
66770
|
}
|
|
66711
|
-
const
|
|
66712
|
-
|
|
66713
|
-
|
|
66714
|
-
|
|
66715
|
-
|
|
66716
|
-
|
|
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" };
|
|
66717
66788
|
}
|
|
66718
|
-
|
|
66719
|
-
|
|
66720
|
-
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" };
|
|
66721
66791
|
}
|
|
66722
|
-
|
|
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;
|
|
66723
66795
|
}
|
|
66724
|
-
async function
|
|
66725
|
-
const
|
|
66726
|
-
|
|
66727
|
-
|
|
66728
|
-
|
|
66729
|
-
|
|
66730
|
-
|
|
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" };
|
|
66731
66809
|
}
|
|
66732
|
-
const
|
|
66733
|
-
|
|
66734
|
-
|
|
66735
|
-
|
|
66736
|
-
|
|
66737
|
-
|
|
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" };
|
|
66738
66817
|
}
|
|
66739
|
-
const
|
|
66740
|
-
|
|
66741
|
-
|
|
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" };
|
|
66742
66825
|
}
|
|
66743
|
-
|
|
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`);
|
|
66744
66832
|
}
|
|
66745
66833
|
|
|
66746
66834
|
// src/frontdoor/rcControl.ts
|
|
@@ -66752,6 +66840,25 @@ function answerJson(response, status, body) {
|
|
|
66752
66840
|
response.writeHead(status, { "content-type": "application/json", "content-length": String(Buffer.byteLength(text2, "utf8")) });
|
|
66753
66841
|
response.end(text2);
|
|
66754
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
|
+
}
|
|
66755
66862
|
async function readBody(request7) {
|
|
66756
66863
|
return await new Promise((resolve, reject) => {
|
|
66757
66864
|
const chunks = [];
|
|
@@ -66779,9 +66886,127 @@ function createRcControlHandler(deps) {
|
|
|
66779
66886
|
const pendingPath = `${CONTROL_PATH_PREFIX}/pending`;
|
|
66780
66887
|
const injectPath = `${CONTROL_PATH_PREFIX}/inject`;
|
|
66781
66888
|
const answerPath = `${CONTROL_PATH_PREFIX}/answer`;
|
|
66782
|
-
const
|
|
66783
|
-
|
|
66784
|
-
|
|
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
|
+
];
|
|
66785
67010
|
return (request7, response) => {
|
|
66786
67011
|
const presented = request7.headers.authorization;
|
|
66787
67012
|
const bearerPrefix = "bearer ".length;
|
|
@@ -66866,73 +67091,33 @@ function createRcControlHandler(deps) {
|
|
|
66866
67091
|
answerJson(response, HTTP_STATUS.badGateway, { error: result.message });
|
|
66867
67092
|
return;
|
|
66868
67093
|
}
|
|
66869
|
-
|
|
66870
|
-
if (
|
|
66871
|
-
|
|
66872
|
-
return;
|
|
66873
|
-
}
|
|
66874
|
-
let parsed;
|
|
66875
|
-
try {
|
|
66876
|
-
parsed = JSON.parse(await readBody(request7));
|
|
66877
|
-
} catch {
|
|
66878
|
-
answerJson(response, HTTP_STATUS.badRequest, { error: "the interrupt body is not readable JSON within the control body cap" });
|
|
66879
|
-
return;
|
|
66880
|
-
}
|
|
66881
|
-
if (typeof parsed !== "object" || parsed === null || !("session" in parsed) || typeof parsed.session !== "string" || parsed.session === "") {
|
|
66882
|
-
answerJson(response, HTTP_STATUS.badRequest, { error: "the interrupt body must be JSON naming a non-empty session id" });
|
|
66883
|
-
return;
|
|
66884
|
-
}
|
|
66885
|
-
const result = await deps.interrupt(parsed.session);
|
|
66886
|
-
if (result.ok) {
|
|
66887
|
-
answerJson(response, HTTP_STATUS.ok, { session: parsed.session, sequenceNums: result.sequenceNums });
|
|
66888
|
-
return;
|
|
67094
|
+
for (const route of controlWrites) {
|
|
67095
|
+
if (url2.pathname !== `${CONTROL_PATH_PREFIX}/${route.path}`) {
|
|
67096
|
+
continue;
|
|
66889
67097
|
}
|
|
66890
|
-
answerJson(response, HTTP_STATUS.badGateway, { error: result.message });
|
|
66891
|
-
return;
|
|
66892
|
-
}
|
|
66893
|
-
if (url2.pathname === setModelPath) {
|
|
66894
67098
|
if (request7.method !== "POST") {
|
|
66895
|
-
answerJson(response, HTTP_STATUS.methodNotAllowed, { error:
|
|
67099
|
+
answerJson(response, HTTP_STATUS.methodNotAllowed, { error: `the ${route.name} operation is a POST` });
|
|
66896
67100
|
return;
|
|
66897
67101
|
}
|
|
66898
67102
|
let parsed;
|
|
66899
67103
|
try {
|
|
66900
67104
|
parsed = JSON.parse(await readBody(request7));
|
|
66901
67105
|
} catch {
|
|
66902
|
-
answerJson(response, HTTP_STATUS.badRequest, { error:
|
|
66903
|
-
return;
|
|
66904
|
-
}
|
|
66905
|
-
if (typeof parsed !== "object" || parsed === null || !("session" in parsed) || !("model" in parsed) || typeof parsed.session !== "string" || typeof parsed.model !== "string" || parsed.session === "" || parsed.model === "") {
|
|
66906
|
-
answerJson(response, HTTP_STATUS.badRequest, { error: "the set-model body must be JSON naming a non-empty session id and model id" });
|
|
67106
|
+
answerJson(response, HTTP_STATUS.badRequest, { error: `the ${route.name} body is not readable JSON within the control body cap` });
|
|
66907
67107
|
return;
|
|
66908
67108
|
}
|
|
66909
|
-
|
|
66910
|
-
|
|
66911
|
-
answerJson(response, HTTP_STATUS.ok, { session: parsed.session, model: parsed.model, sequenceNums: result.sequenceNums });
|
|
66912
|
-
return;
|
|
66913
|
-
}
|
|
66914
|
-
answerJson(response, HTTP_STATUS.badGateway, { error: result.message });
|
|
66915
|
-
return;
|
|
66916
|
-
}
|
|
66917
|
-
if (url2.pathname === setPermissionModePath) {
|
|
66918
|
-
if (request7.method !== "POST") {
|
|
66919
|
-
answerJson(response, HTTP_STATUS.methodNotAllowed, { error: "the set-permission-mode operation is a POST" });
|
|
66920
|
-
return;
|
|
66921
|
-
}
|
|
66922
|
-
let parsed;
|
|
66923
|
-
try {
|
|
66924
|
-
parsed = JSON.parse(await readBody(request7));
|
|
66925
|
-
} catch {
|
|
66926
|
-
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` });
|
|
66927
67111
|
return;
|
|
66928
67112
|
}
|
|
66929
|
-
|
|
66930
|
-
|
|
67113
|
+
const read = route.read(parsed);
|
|
67114
|
+
if ("error" in read) {
|
|
67115
|
+
answerJson(response, HTTP_STATUS.badRequest, { error: read.error });
|
|
66931
67116
|
return;
|
|
66932
67117
|
}
|
|
66933
|
-
const result = await
|
|
67118
|
+
const result = await read.run();
|
|
66934
67119
|
if (result.ok) {
|
|
66935
|
-
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 });
|
|
66936
67121
|
return;
|
|
66937
67122
|
}
|
|
66938
67123
|
answerJson(response, HTTP_STATUS.badGateway, { error: result.message });
|
|
@@ -67050,6 +67235,19 @@ function sequenceNumsOfBody(body) {
|
|
|
67050
67235
|
}
|
|
67051
67236
|
return sequenceNums;
|
|
67052
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
|
+
}
|
|
67053
67251
|
function frontDoorRcControl(transport, token) {
|
|
67054
67252
|
const headers = { authorization: `Bearer ${token}` };
|
|
67055
67253
|
const listAnswerOf = async (path46, what) => {
|
|
@@ -67138,7 +67336,8 @@ function frontDoorRcControl(transport, token) {
|
|
|
67138
67336
|
if (sequenceNums === void 0) {
|
|
67139
67337
|
return { ok: false, message: `the door accepted the event but its answer named no sequence numbers: ${answer.body}` };
|
|
67140
67338
|
}
|
|
67141
|
-
|
|
67339
|
+
const requestId = requestIdOfBody(answer.body);
|
|
67340
|
+
return requestId === void 0 ? { ok: true, sequenceNums } : { ok: true, sequenceNums, requestId };
|
|
67142
67341
|
}
|
|
67143
67342
|
return { ok: false, message: describeFailure(answer.status, controlErrorOf(answer.body) ?? answer.body) };
|
|
67144
67343
|
};
|
|
@@ -67155,7 +67354,21 @@ function frontDoorRcControl(transport, token) {
|
|
|
67155
67354
|
),
|
|
67156
67355
|
interruptSession: async (sessionId) => await writeOf(`${CONTROL_PATH_PREFIX}/interrupt`, JSON.stringify({ session: sessionId }), (status, message) => `interrupting ${sessionId} failed (HTTP ${String(status)}): ${message}`),
|
|
67157
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}`),
|
|
67158
|
-
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}`)
|
|
67159
67372
|
};
|
|
67160
67373
|
}
|
|
67161
67374
|
|
|
@@ -71819,6 +72032,48 @@ var RcSetPermissionModeInputSchema = external_exports.strictObject({
|
|
|
71819
72032
|
session: external_exports.string().min(1),
|
|
71820
72033
|
mode: RcPermissionModeSchema
|
|
71821
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
|
+
});
|
|
71822
72077
|
var RcListOutputSchema = external_exports.strictObject({ sessions: external_exports.readonly(external_exports.array(RcSessionSummarySchema)) });
|
|
71823
72078
|
var RcStatusOutputSchema = external_exports.strictObject({ statuses: external_exports.readonly(external_exports.array(RcSessionStatusSchema)) });
|
|
71824
72079
|
var RcPendingOutputSchema = external_exports.strictObject({ pending: external_exports.readonly(external_exports.array(RcPendingRequestSummarySchema)) });
|
|
@@ -71826,6 +72081,11 @@ var RcWriteOutputSchema = external_exports.strictObject({
|
|
|
71826
72081
|
session: external_exports.string(),
|
|
71827
72082
|
sequenceNums: external_exports.readonly(external_exports.array(external_exports.number()))
|
|
71828
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
|
+
});
|
|
71829
72089
|
var RcAnswerOutputSchema = external_exports.strictObject({
|
|
71830
72090
|
session: external_exports.string(),
|
|
71831
72091
|
request: external_exports.string(),
|
|
@@ -71854,6 +72114,15 @@ function createRcApiRouter(deps) {
|
|
|
71854
72114
|
}
|
|
71855
72115
|
return result.sequenceNums;
|
|
71856
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
|
+
};
|
|
71857
72126
|
return {
|
|
71858
72127
|
rc: {
|
|
71859
72128
|
list: authed.output(RcListOutputSchema).handler(() => ({ sessions: deps.list() })),
|
|
@@ -71876,9 +72145,19 @@ function createRcApiRouter(deps) {
|
|
|
71876
72145
|
const decision = { approve: input2.approve, message: input2.approve || input2.text === void 0 ? void 0 : input2.text };
|
|
71877
72146
|
return { session: input2.session, request: input2.request, sequenceNums: delivered(input2.session, await deps.answer(input2.session, input2.request, decision)) };
|
|
71878
72147
|
}),
|
|
71879
|
-
interrupt: authed.input(RcInterruptInputSchema).output(
|
|
71880
|
-
setModel: authed.input(RcSetModelInputSchema).output(
|
|
71881
|
-
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)) })),
|
|
71882
72161
|
subscribe: authed.input(RcSessionQuerySchema).output(eventIterator(RcStreamEventSchema)).handler(async function* ({ input: input2, signal }) {
|
|
71883
72162
|
const queue = [];
|
|
71884
72163
|
let wake;
|
|
@@ -73453,1425 +73732,890 @@ function attachingProviderCredential(route, deps) {
|
|
|
73453
73732
|
};
|
|
73454
73733
|
}
|
|
73455
73734
|
|
|
73456
|
-
// src/frontdoor/
|
|
73735
|
+
// src/frontdoor/rcCommands.ts
|
|
73736
|
+
var import_node_crypto14 = require("node:crypto");
|
|
73737
|
+
|
|
73738
|
+
// src/frontdoor/rcSelfHostMint.ts
|
|
73457
73739
|
var import_node_path34 = __toESM(require("node:path"), 1);
|
|
73458
|
-
|
|
73459
|
-
|
|
73460
|
-
|
|
73461
|
-
|
|
73462
|
-
|
|
73463
|
-
|
|
73464
|
-
|
|
73465
|
-
|
|
73466
|
-
|
|
73467
|
-
|
|
73468
|
-
|
|
73469
|
-
|
|
73470
|
-
return void 0;
|
|
73471
|
-
}
|
|
73472
|
-
return {
|
|
73473
|
-
authorization,
|
|
73474
|
-
anthropicVersion: typeof anthropicVersion === "string" ? anthropicVersion : void 0,
|
|
73475
|
-
anthropicClientPlatform: typeof anthropicClientPlatform === "string" ? anthropicClientPlatform : void 0
|
|
73476
|
-
};
|
|
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";
|
|
73477
73752
|
}
|
|
73478
|
-
|
|
73479
|
-
|
|
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 };
|
|
73480
73784
|
}
|
|
73481
|
-
function
|
|
73482
|
-
|
|
73483
|
-
|
|
73484
|
-
|
|
73485
|
-
const
|
|
73486
|
-
|
|
73487
|
-
|
|
73488
|
-
|
|
73489
|
-
let parsed;
|
|
73490
|
-
try {
|
|
73491
|
-
parsed = JSON.parse(raw);
|
|
73492
|
-
} catch {
|
|
73493
|
-
return void 0;
|
|
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;
|
|
73494
73793
|
}
|
|
73495
|
-
|
|
73496
|
-
|
|
73497
|
-
return {
|
|
73498
|
-
read: (sessionId) => {
|
|
73499
|
-
if (!isSafeSessionId(sessionId)) {
|
|
73500
|
-
return void 0;
|
|
73501
|
-
}
|
|
73502
|
-
return credentialOfJson(readParsed(sessionId));
|
|
73503
|
-
},
|
|
73504
|
-
write: (sessionId, credential) => {
|
|
73505
|
-
if (!isSafeSessionId(sessionId)) {
|
|
73506
|
-
return;
|
|
73507
|
-
}
|
|
73508
|
-
const cursor3 = sequenceNumOfJson(readParsed(sessionId)?.highestSequenceNum);
|
|
73509
|
-
const record2 = { ...credential, ...cursor3 === void 0 ? {} : { highestSequenceNum: cursor3 } };
|
|
73510
|
-
fs20.writeFilePrivate(fileFor(sessionId), `${JSON.stringify(record2, null, 2)}
|
|
73511
|
-
`);
|
|
73512
|
-
},
|
|
73513
|
-
readCursor: (sessionId) => {
|
|
73514
|
-
if (!isSafeSessionId(sessionId)) {
|
|
73515
|
-
return void 0;
|
|
73516
|
-
}
|
|
73517
|
-
return sequenceNumOfJson(readParsed(sessionId)?.highestSequenceNum);
|
|
73518
|
-
},
|
|
73519
|
-
writeCursor: (sessionId, highestSequenceNum) => {
|
|
73520
|
-
if (!isSafeSessionId(sessionId)) {
|
|
73521
|
-
return;
|
|
73522
|
-
}
|
|
73523
|
-
const credential = credentialOfJson(readParsed(sessionId));
|
|
73524
|
-
const record2 = credential === void 0 ? { highestSequenceNum } : { ...credential, highestSequenceNum };
|
|
73525
|
-
fs20.writeFilePrivate(fileFor(sessionId), `${JSON.stringify(record2, null, 2)}
|
|
73526
|
-
`);
|
|
73527
|
-
},
|
|
73528
|
-
remove: (sessionId) => {
|
|
73529
|
-
if (!isSafeSessionId(sessionId)) {
|
|
73530
|
-
return;
|
|
73531
|
-
}
|
|
73532
|
-
fs20.removeRecursive(fileFor(sessionId));
|
|
73794
|
+
if (first === V4_CGNAT_OCTET && second >= V4_CGNAT_SECOND_FROM && second <= V4_CGNAT_SECOND_TO) {
|
|
73795
|
+
return true;
|
|
73533
73796
|
}
|
|
73534
|
-
|
|
73535
|
-
|
|
73536
|
-
|
|
73537
|
-
|
|
73538
|
-
|
|
73539
|
-
|
|
73540
|
-
|
|
73541
|
-
|
|
73542
|
-
|
|
73543
|
-
|
|
73544
|
-
|
|
73545
|
-
function apiBase() { return window.location.origin + API_PREFIX; }
|
|
73546
|
-
|
|
73547
|
-
// Calls one procedure: resolves the output's own value, rejects with the door's error message when it answered one.
|
|
73548
|
-
async function rpc(path, input) {
|
|
73549
|
-
var response = await fetch(apiBase() + '/' + path, {
|
|
73550
|
-
method: 'POST',
|
|
73551
|
-
headers: { 'content-type': 'application/json', authorization: 'Bearer ' + token },
|
|
73552
|
-
body: JSON.stringify({ json: input === undefined ? {} : input }),
|
|
73553
|
-
});
|
|
73554
|
-
var envelope = await response.json().catch(function () { return {}; });
|
|
73555
|
-
if (!response.ok) {
|
|
73556
|
-
var message = envelope && envelope.json && envelope.json.message ? envelope.json.message : ('HTTP ' + response.status);
|
|
73557
|
-
var error = new Error(message);
|
|
73558
|
-
error.status = response.status;
|
|
73559
|
-
throw error;
|
|
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;
|
|
73560
73807
|
}
|
|
73561
|
-
|
|
73562
|
-
|
|
73563
|
-
|
|
73564
|
-
|
|
73565
|
-
|
|
73566
|
-
var frames = [];
|
|
73567
|
-
var rest = buffer;
|
|
73568
|
-
for (;;) {
|
|
73569
|
-
var boundary = rest.indexOf('\\n\\n');
|
|
73570
|
-
if (boundary === -1) { break; }
|
|
73571
|
-
var raw = rest.slice(0, boundary);
|
|
73572
|
-
rest = rest.slice(boundary + 2);
|
|
73573
|
-
var frame = { event: 'message', id: '', data: '' };
|
|
73574
|
-
var lines = raw.split('\\n');
|
|
73575
|
-
for (var i = 0; i < lines.length; i++) {
|
|
73576
|
-
var line = lines[i];
|
|
73577
|
-
if (line === '' || line.charAt(0) === ':') { continue; }
|
|
73578
|
-
var colon = line.indexOf(':');
|
|
73579
|
-
var field = colon === -1 ? line : line.slice(0, colon);
|
|
73580
|
-
var value = colon === -1 ? '' : line.slice(colon + 1);
|
|
73581
|
-
if (value.charAt(0) === ' ') { value = value.slice(1); }
|
|
73582
|
-
if (field === 'event') { frame.event = value; }
|
|
73583
|
-
if (field === 'id') { frame.id = value; }
|
|
73584
|
-
if (field === 'data') { frame.data += (frame.data === '' ? '' : '\\n') + value; }
|
|
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] ?? "");
|
|
73585
73813
|
}
|
|
73586
|
-
if (
|
|
73814
|
+
if (lower === "::" || lower === "::1") {
|
|
73815
|
+
return true;
|
|
73816
|
+
}
|
|
73817
|
+
const firstGroup = Number.parseInt(/^([0-9a-f]+):/.exec(lower)?.[1] ?? "0", 16);
|
|
73818
|
+
if (Number.isNaN(firstGroup)) {
|
|
73819
|
+
return true;
|
|
73820
|
+
}
|
|
73821
|
+
if (firstGroup >= V6_UNIQUE_LOCAL_FROM && firstGroup <= V6_UNIQUE_LOCAL_TO) {
|
|
73822
|
+
return true;
|
|
73823
|
+
}
|
|
73824
|
+
if (firstGroup >= V6_LINK_LOCAL_FROM && firstGroup <= V6_LINK_LOCAL_TO) {
|
|
73825
|
+
return true;
|
|
73826
|
+
}
|
|
73827
|
+
return firstGroup >= V6_MULTICAST_FROM;
|
|
73587
73828
|
}
|
|
73588
|
-
return
|
|
73829
|
+
return true;
|
|
73589
73830
|
}
|
|
73590
|
-
|
|
73591
|
-
|
|
73592
|
-
function sequenceGap(tracker, event) {
|
|
73593
|
-
var previous = tracker[event.source];
|
|
73594
|
-
if (event.sequence > (previous === undefined ? 0 : previous)) { tracker[event.source] = event.sequence; }
|
|
73595
|
-
if (previous === undefined || event.sequence <= previous) { return 0; }
|
|
73596
|
-
return event.sequence - previous - 1;
|
|
73831
|
+
function isSuccessful3(status) {
|
|
73832
|
+
return status >= HTTP_STATUS.ok && status < SUCCESS_STATUS_MAX_EXCLUSIVE3;
|
|
73597
73833
|
}
|
|
73598
|
-
|
|
73599
|
-
|
|
73600
|
-
|
|
73601
|
-
// A bounded sketch of any payload, the feed's fallback rendering: one JSON line, cut with a marker rather than silently.
|
|
73602
|
-
function sketch(value, cap) {
|
|
73603
|
-
var text;
|
|
73604
|
-
try { text = JSON.stringify(value); } catch (e) { text = String(value); }
|
|
73605
|
-
if (text === undefined) { text = 'undefined'; }
|
|
73606
|
-
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;
|
|
73607
73836
|
}
|
|
73608
|
-
|
|
73609
|
-
|
|
73610
|
-
|
|
73611
|
-
var envelope = event.envelope;
|
|
73612
|
-
var payload = envelope.payload;
|
|
73613
|
-
var headline = '';
|
|
73614
|
-
if (payload !== undefined && payload !== null && typeof payload === 'object' && Array.isArray(payload.content)) {
|
|
73615
|
-
var parts = [];
|
|
73616
|
-
for (var i = 0; i < payload.content.length; i++) {
|
|
73617
|
-
var block = payload.content[i];
|
|
73618
|
-
if (block !== null && typeof block === 'object' && block.type === 'text' && typeof block.text === 'string') { parts.push(block.text); }
|
|
73619
|
-
}
|
|
73620
|
-
if (parts.length > 0) { headline = parts.join(' '); }
|
|
73837
|
+
function readableContentType(contentType) {
|
|
73838
|
+
if (contentType === void 0 || contentType.trim() === "") {
|
|
73839
|
+
return true;
|
|
73621
73840
|
}
|
|
73622
|
-
|
|
73623
|
-
|
|
73624
|
-
|
|
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;
|
|
73625
73847
|
}
|
|
73626
|
-
return
|
|
73848
|
+
return /json|xml|javascript|ecmascript|yaml|csv/.test(sub);
|
|
73627
73849
|
}
|
|
73628
|
-
|
|
73629
|
-
|
|
73630
|
-
|
|
73631
|
-
|
|
73632
|
-
|
|
73633
|
-
|
|
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") };
|
|
73634
73861
|
}
|
|
73635
|
-
|
|
73636
|
-
|
|
73637
|
-
|
|
73638
|
-
function renderEvent(event, gap) {
|
|
73639
|
-
if (ui === null) { return; }
|
|
73640
|
-
var list = ui.eventList;
|
|
73641
|
-
if (gap > 0) {
|
|
73642
|
-
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}`);
|
|
73643
73865
|
}
|
|
73644
|
-
|
|
73645
|
-
|
|
73646
|
-
line = rcLine(event.payload);
|
|
73647
|
-
} else {
|
|
73648
|
-
line = new Date().toLocaleTimeString() + ' [' + event.source + '] ' + sketch(event.payload, 240);
|
|
73866
|
+
if (deps.allowPrivate) {
|
|
73867
|
+
return void 0;
|
|
73649
73868
|
}
|
|
73650
|
-
|
|
73651
|
-
|
|
73652
|
-
|
|
73653
|
-
|
|
73654
|
-
|
|
73655
|
-
|
|
73656
|
-
if (ui === null) { return; }
|
|
73657
|
-
var list = ui.sessionList;
|
|
73658
|
-
list.textContent = '';
|
|
73659
|
-
if (statuses.length === 0) {
|
|
73660
|
-
list.appendChild(el('div', 'event dim', 'no Remote Control sessions observed'));
|
|
73661
|
-
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`);
|
|
73662
73875
|
}
|
|
73663
|
-
for (
|
|
73664
|
-
|
|
73665
|
-
|
|
73666
|
-
|
|
73667
|
-
row.appendChild(el('div', null, shortId(status.id)));
|
|
73668
|
-
row.appendChild(el('div', 'dim', worker + (status.pending.length === 0 ? '' : ', ' + String(status.pending.length) + ' pending') + ', seen ' + new Date(status.lastSeenAt).toLocaleTimeString()));
|
|
73669
|
-
row.addEventListener('click', selectSession(status.id));
|
|
73670
|
-
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
|
+
}
|
|
73671
73880
|
}
|
|
73881
|
+
return void 0;
|
|
73672
73882
|
}
|
|
73673
|
-
|
|
73674
|
-
|
|
73675
|
-
|
|
73676
|
-
|
|
73677
|
-
|
|
73678
|
-
|
|
73679
|
-
refreshSessions();
|
|
73680
|
-
refreshPending();
|
|
73681
|
-
};
|
|
73682
|
-
}
|
|
73683
|
-
|
|
73684
|
-
function renderPending(pending) {
|
|
73685
|
-
if (ui === null) { return; }
|
|
73686
|
-
var list = ui.pendingList;
|
|
73687
|
-
list.textContent = '';
|
|
73688
|
-
if (pending.length === 0) {
|
|
73689
|
-
list.appendChild(el('div', 'event dim', 'no control requests awaiting an answer'));
|
|
73690
|
-
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`);
|
|
73691
73889
|
}
|
|
73692
|
-
|
|
73693
|
-
|
|
73694
|
-
|
|
73695
|
-
|
|
73696
|
-
|
|
73697
|
-
|
|
73698
|
-
|
|
73699
|
-
|
|
73700
|
-
|
|
73701
|
-
|
|
73702
|
-
|
|
73703
|
-
|
|
73704
|
-
|
|
73705
|
-
|
|
73706
|
-
|
|
73707
|
-
|
|
73708
|
-
|
|
73709
|
-
|
|
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);
|
|
73710
73950
|
}
|
|
73711
73951
|
}
|
|
73712
|
-
|
|
73713
|
-
|
|
73714
|
-
|
|
73715
|
-
|
|
73716
|
-
|
|
73717
|
-
|
|
73718
|
-
|
|
73719
|
-
|
|
73720
|
-
|
|
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
|
|
73721
73983
|
};
|
|
73984
|
+
return async (url2) => await fetchThroughWebProxy(url2, deps);
|
|
73722
73985
|
}
|
|
73723
73986
|
|
|
73724
|
-
|
|
73725
|
-
|
|
73726
|
-
|
|
73727
|
-
|
|
73728
|
-
ui.send.disabled = true;
|
|
73729
|
-
rpc('rc/send', { session: ui.selected, text: text }).then(function (result) {
|
|
73730
|
-
ui.prompt.value = '';
|
|
73731
|
-
renderEvent({ source: 'page', sequence: 0, payload: { note: 'sent, sequence_num ' + result.sequenceNums.join(', ') } }, 0);
|
|
73732
|
-
}, function (error) { window.alert('send failed: ' + error.message); }).then(function () { ui.send.disabled = false; });
|
|
73733
|
-
}
|
|
73734
|
-
|
|
73735
|
-
function refreshSessions() {
|
|
73736
|
-
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";
|
|
73737
73991
|
}
|
|
73738
|
-
|
|
73739
|
-
|
|
73740
|
-
|
|
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);
|
|
73741
74015
|
}
|
|
73742
|
-
|
|
73743
|
-
|
|
73744
|
-
function authOrReport(what) {
|
|
73745
|
-
return function (error) {
|
|
73746
|
-
if (error.status === 401) {
|
|
73747
|
-
handleUnauthorized();
|
|
73748
|
-
return;
|
|
73749
|
-
}
|
|
73750
|
-
renderEvent({ source: 'page', sequence: 0, payload: { note: what + ' failed: ' + error.message } }, 0);
|
|
73751
|
-
};
|
|
74016
|
+
function isWebSearchRefusal(value2) {
|
|
74017
|
+
return !Array.isArray(value2);
|
|
73752
74018
|
}
|
|
73753
|
-
|
|
73754
|
-
|
|
73755
|
-
|
|
73756
|
-
if (feedAbort !== null) { feedAbort.abort(); }
|
|
73757
|
-
streamState('');
|
|
73758
|
-
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;
|
|
73759
74022
|
}
|
|
73760
|
-
|
|
73761
|
-
|
|
73762
|
-
|
|
73763
|
-
|
|
73764
|
-
|
|
73765
|
-
|
|
73766
|
-
|
|
73767
|
-
var tracker = {};
|
|
73768
|
-
for (;;) {
|
|
73769
|
-
if (feedAbort !== null) { feedAbort.abort(); }
|
|
73770
|
-
feedAbort = new AbortController();
|
|
73771
|
-
var filter = document.getElementById('source-filter').value;
|
|
73772
|
-
var input = filter === '' ? {} : { sources: [filter] };
|
|
73773
|
-
try {
|
|
73774
|
-
streamState('stream: connecting');
|
|
73775
|
-
var response = await fetch(apiBase() + '/events/subscribe', {
|
|
73776
|
-
method: 'POST',
|
|
73777
|
-
headers: { 'content-type': 'application/json', authorization: 'Bearer ' + token },
|
|
73778
|
-
body: JSON.stringify({ json: input }),
|
|
73779
|
-
signal: feedAbort.signal,
|
|
73780
|
-
});
|
|
73781
|
-
if (response.status === 401) { handleUnauthorized(); return; }
|
|
73782
|
-
if (!response.ok || response.body === null) { throw new Error('HTTP ' + String(response.status)); }
|
|
73783
|
-
streamState('stream: live');
|
|
73784
|
-
var reader = response.body.getReader();
|
|
73785
|
-
var decoder = new TextDecoder();
|
|
73786
|
-
var buffer = '';
|
|
73787
|
-
for (;;) {
|
|
73788
|
-
var chunk = await reader.read();
|
|
73789
|
-
if (chunk.done) { break; }
|
|
73790
|
-
buffer += decoder.decode(chunk.value, { stream: true });
|
|
73791
|
-
var parsed = parseSseFrames(buffer);
|
|
73792
|
-
buffer = parsed.rest;
|
|
73793
|
-
for (var i = 0; i < parsed.frames.length; i++) {
|
|
73794
|
-
var frame = parsed.frames[i];
|
|
73795
|
-
if (frame.event !== 'message') { continue; }
|
|
73796
|
-
var event;
|
|
73797
|
-
try { event = JSON.parse(frame.data).json; } catch (e) { continue; }
|
|
73798
|
-
if (event === undefined || event.source === undefined) { continue; }
|
|
73799
|
-
renderEvent(event, sequenceGap(tracker, event));
|
|
73800
|
-
}
|
|
73801
|
-
}
|
|
73802
|
-
} catch (error) {
|
|
73803
|
-
if (feedAbort !== null && feedAbort.signal.aborted) { return; }
|
|
73804
|
-
}
|
|
73805
|
-
streamState('stream: dropped, reconnecting');
|
|
73806
|
-
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);
|
|
73807
74030
|
}
|
|
74031
|
+
return void 0;
|
|
73808
74032
|
}
|
|
73809
|
-
|
|
73810
|
-
|
|
73811
|
-
if (
|
|
73812
|
-
|
|
73813
|
-
var form = document.getElementById('token-form');
|
|
73814
|
-
form.classList.remove('hidden');
|
|
73815
|
-
document.getElementById('token-error').textContent = message === undefined ? '' : message;
|
|
73816
|
-
document.getElementById('token-input').focus();
|
|
73817
|
-
}
|
|
73818
|
-
|
|
73819
|
-
function startPage(heldToken) {
|
|
73820
|
-
token = heldToken;
|
|
73821
|
-
ui = {
|
|
73822
|
-
main: document.getElementById('main'),
|
|
73823
|
-
eventList: document.getElementById('event-list'),
|
|
73824
|
-
sessionList: document.getElementById('session-list'),
|
|
73825
|
-
pendingList: document.getElementById('pending-list'),
|
|
73826
|
-
prompt: document.getElementById('prompt'),
|
|
73827
|
-
promptTarget: document.getElementById('prompt-target'),
|
|
73828
|
-
send: document.getElementById('send'),
|
|
73829
|
-
streamState: document.getElementById('stream-state'),
|
|
73830
|
-
selected: null,
|
|
73831
|
-
};
|
|
73832
|
-
document.getElementById('token-form').classList.add('hidden');
|
|
73833
|
-
ui.main.classList.remove('hidden');
|
|
73834
|
-
document.getElementById('refresh').addEventListener('click', function () { refreshSessions(); refreshPending(); });
|
|
73835
|
-
document.getElementById('send').addEventListener('click', sendPrompt);
|
|
73836
|
-
document.getElementById('source-filter').addEventListener('change', function () { runFeed(); });
|
|
73837
|
-
ui.prompt.addEventListener('keydown', function (event) {
|
|
73838
|
-
if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) { sendPrompt(); }
|
|
73839
|
-
});
|
|
73840
|
-
refreshSessions();
|
|
73841
|
-
refreshPending();
|
|
73842
|
-
window.setInterval(function () { refreshSessions(); refreshPending(); }, 4000);
|
|
73843
|
-
runFeed();
|
|
73844
|
-
document.getElementById('door-state').textContent = 'connected';
|
|
73845
|
-
}
|
|
73846
|
-
|
|
73847
|
-
function boot() {
|
|
73848
|
-
var held = null;
|
|
73849
|
-
var query = new URLSearchParams(window.location.search);
|
|
73850
|
-
var fromQuery = query.get('token');
|
|
73851
|
-
if (fromQuery !== null && fromQuery !== '') {
|
|
73852
|
-
window.sessionStorage.setItem(TOKEN_KEY, fromQuery);
|
|
73853
|
-
// 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.
|
|
73854
|
-
window.history.replaceState(null, '', window.location.pathname);
|
|
73855
|
-
held = fromQuery;
|
|
73856
|
-
} else {
|
|
73857
|
-
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;
|
|
73858
74037
|
}
|
|
73859
|
-
|
|
73860
|
-
|
|
73861
|
-
if (
|
|
73862
|
-
|
|
73863
|
-
|
|
73864
|
-
|
|
73865
|
-
|
|
73866
|
-
|
|
73867
|
-
});
|
|
73868
|
-
if (held !== null && held !== '') { startPage(held); }
|
|
73869
|
-
}
|
|
73870
|
-
|
|
73871
|
-
if (typeof document !== 'undefined' && document.getElementById('event-list') !== null) {
|
|
73872
|
-
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;
|
|
73873
74046
|
}
|
|
73874
|
-
|
|
73875
|
-
|
|
73876
|
-
|
|
73877
|
-
<head>
|
|
73878
|
-
<meta charset="utf-8">
|
|
73879
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
73880
|
-
<title>agent-shim Remote Control</title>
|
|
73881
|
-
<style>
|
|
73882
|
-
:root { color-scheme: dark; }
|
|
73883
|
-
* { box-sizing: border-box; }
|
|
73884
|
-
body { margin: 0; background: #101418; color: #d7dde3; font: 14px/1.45 ui-monospace, Menlo, Consolas, monospace; }
|
|
73885
|
-
header { display: flex; gap: 12px; align-items: baseline; padding: 10px 16px; background: #171d24; border-bottom: 1px solid #2a333d; }
|
|
73886
|
-
header h1 { font-size: 15px; margin: 0; font-weight: 600; }
|
|
73887
|
-
header .state { color: #8b98a5; }
|
|
73888
|
-
main { display: flex; gap: 1px; background: #2a333d; min-height: calc(100vh - 39px); }
|
|
73889
|
-
section { background: #101418; padding: 12px 16px; overflow-y: auto; }
|
|
73890
|
-
#sessions { flex: 0 0 320px; border-right: 1px solid #2a333d; }
|
|
73891
|
-
#right { flex: 1; display: flex; flex-direction: column; min-width: 0; }
|
|
73892
|
-
#feed { flex: 1; }
|
|
73893
|
-
#controls { border-top: 1px solid #2a333d; }
|
|
73894
|
-
h2 { font-size: 12px; text-transform: uppercase; letter-spacing: 0.08em; color: #8b98a5; margin: 8px 0 8px; font-weight: 600; }
|
|
73895
|
-
.session { padding: 6px 8px; border: 1px solid transparent; border-radius: 4px; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
73896
|
-
.session:hover { background: #171d24; }
|
|
73897
|
-
.session.selected { border-color: #3d6ea5; background: #17222e; }
|
|
73898
|
-
.session .dim { color: #8b98a5; }
|
|
73899
|
-
.event { padding: 2px 0; white-space: pre-wrap; word-break: break-word; }
|
|
73900
|
-
.event .dim { color: #8b98a5; }
|
|
73901
|
-
.event.gap { color: #d29922; }
|
|
73902
|
-
.event.error { color: #e5534b; }
|
|
73903
|
-
textarea, input[type=text], select { background: #171d24; color: #d7dde3; border: 1px solid #2a333d; border-radius: 4px; padding: 6px 8px; font: inherit; width: 100%; }
|
|
73904
|
-
textarea { resize: vertical; min-height: 44px; }
|
|
73905
|
-
button { background: #1f4a7a; color: #d7dde3; border: 1px solid #3d6ea5; border-radius: 4px; padding: 5px 12px; font: inherit; cursor: pointer; }
|
|
73906
|
-
button.secondary { background: #2a333d; border-color: #3a444f; }
|
|
73907
|
-
button:disabled { opacity: 0.5; cursor: default; }
|
|
73908
|
-
.row { display: flex; gap: 8px; margin: 6px 0; align-items: center; }
|
|
73909
|
-
.card { border: 1px solid #3d6ea5; border-radius: 4px; padding: 8px; margin: 6px 0; background: #17222e; }
|
|
73910
|
-
.card .dim { color: #8b98a5; }
|
|
73911
|
-
#token-form { max-width: 480px; margin: 48px auto; }
|
|
73912
|
-
#token-form p { color: #8b98a5; }
|
|
73913
|
-
.hidden { display: none !important; }
|
|
73914
|
-
</style>
|
|
73915
|
-
</head>
|
|
73916
|
-
<body data-agent-shim-rc-client>
|
|
73917
|
-
<header>
|
|
73918
|
-
<h1>agent-shim Remote Control</h1>
|
|
73919
|
-
<span class="state" id="door-state">connecting</span>
|
|
73920
|
-
<span class="state" id="stream-state"></span>
|
|
73921
|
-
</header>
|
|
73922
|
-
<div id="token-form">
|
|
73923
|
-
<h2>This door's control token</h2>
|
|
73924
|
-
<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>
|
|
73925
|
-
<div class="row"><input type="text" id="token-input" placeholder="control token" autocomplete="off"><button id="token-save">Use</button></div>
|
|
73926
|
-
<p id="token-error" class="event error"></p>
|
|
73927
|
-
</div>
|
|
73928
|
-
<main class="hidden" id="main">
|
|
73929
|
-
<section id="sessions">
|
|
73930
|
-
<h2>Sessions <button class="secondary" id="refresh">Refresh</button></h2>
|
|
73931
|
-
<div id="session-list"></div>
|
|
73932
|
-
</section>
|
|
73933
|
-
<section id="right">
|
|
73934
|
-
<div id="feed">
|
|
73935
|
-
<h2>Feed <select id="source-filter"><option value="">all sources</option><option value="rc">Remote Control only</option></select></h2>
|
|
73936
|
-
<div id="event-list"></div>
|
|
73937
|
-
</div>
|
|
73938
|
-
<div id="controls">
|
|
73939
|
-
<h2>Pending approvals</h2>
|
|
73940
|
-
<div id="pending-list"></div>
|
|
73941
|
-
<h2>Send a prompt</h2>
|
|
73942
|
-
<div class="row"><textarea id="prompt" placeholder="Prompt for the selected session"></textarea></div>
|
|
73943
|
-
<div class="row"><span class="dim" id="prompt-target">no session selected</span><span style="flex:1"></span><button id="send">Send</button></div>
|
|
73944
|
-
</div>
|
|
73945
|
-
</section>
|
|
73946
|
-
</main>
|
|
73947
|
-
<script>
|
|
73948
|
-
${RC_CLIENT_PAGE_SCRIPT}
|
|
73949
|
-
</script>
|
|
73950
|
-
</body>
|
|
73951
|
-
</html>
|
|
73952
|
-
`;
|
|
73953
|
-
function pageTokenOf(request7, url2) {
|
|
73954
|
-
const presented = request7.headers.authorization;
|
|
73955
|
-
const bearerPrefix = "bearer ".length;
|
|
73956
|
-
const header = typeof presented === "string" && presented.slice(0, bearerPrefix).toLowerCase() === "bearer " ? presented.slice(bearerPrefix) : void 0;
|
|
73957
|
-
const query = url2.searchParams.get("token") ?? void 0;
|
|
73958
|
-
return header ?? query;
|
|
74047
|
+
function singleHeader2(headers, name) {
|
|
74048
|
+
const value2 = headers[name];
|
|
74049
|
+
return typeof value2 === "string" ? value2 : void 0;
|
|
73959
74050
|
}
|
|
73960
|
-
function
|
|
73961
|
-
const
|
|
73962
|
-
if (
|
|
73963
|
-
|
|
73964
|
-
response.end("no such page under the door's client prefix");
|
|
73965
|
-
return;
|
|
74051
|
+
function bearerOf(headers) {
|
|
74052
|
+
const presented = singleHeader2(headers, "authorization");
|
|
74053
|
+
if (presented === void 0) {
|
|
74054
|
+
return void 0;
|
|
73966
74055
|
}
|
|
73967
|
-
|
|
73968
|
-
|
|
73969
|
-
|
|
73970
|
-
|
|
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";
|
|
73971
74062
|
}
|
|
73972
|
-
|
|
73973
|
-
|
|
73974
|
-
response.writeHead(HTTP_STATUS.unauthorized, { "Content-Type": "application/json" });
|
|
73975
|
-
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" }));
|
|
73976
|
-
return;
|
|
74063
|
+
if (presented.slice(0, "bearer ".length).toLowerCase() !== "bearer ") {
|
|
74064
|
+
return "not-a-bearer";
|
|
73977
74065
|
}
|
|
73978
|
-
|
|
73979
|
-
|
|
73980
|
-
|
|
73981
|
-
|
|
73982
|
-
|
|
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
|
+
});
|
|
73983
74088
|
});
|
|
73984
|
-
response.end(request7.method === "HEAD" ? void 0 : RC_CLIENT_PAGE_HTML);
|
|
73985
74089
|
}
|
|
73986
|
-
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() {
|
|
73987
74106
|
return {
|
|
73988
|
-
|
|
73989
|
-
|
|
73990
|
-
|
|
73991
|
-
|
|
73992
|
-
return { matched: true };
|
|
73993
|
-
}
|
|
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" } }
|
|
73994
74111
|
};
|
|
73995
74112
|
}
|
|
73996
|
-
|
|
73997
|
-
|
|
73998
|
-
|
|
73999
|
-
|
|
74000
|
-
|
|
74001
|
-
|
|
74002
|
-
|
|
74003
|
-
|
|
74004
|
-
|
|
74005
|
-
|
|
74006
|
-
|
|
74007
|
-
|
|
74008
|
-
|
|
74009
|
-
|
|
74010
|
-
|
|
74011
|
-
var V4_THIS_NETWORK_OCTET = 0;
|
|
74012
|
-
var V4_PRIVATE_A_OCTET = 10;
|
|
74013
|
-
var V4_LOOPBACK_OCTET = 127;
|
|
74014
|
-
var V4_CGNAT_OCTET = 100;
|
|
74015
|
-
var V4_CGNAT_SECOND_FROM = 64;
|
|
74016
|
-
var V4_CGNAT_SECOND_TO = 127;
|
|
74017
|
-
var V4_LINK_LOCAL_OCTET = 169;
|
|
74018
|
-
var V4_LINK_LOCAL_SECOND = 254;
|
|
74019
|
-
var V4_PRIVATE_B_OCTET = 172;
|
|
74020
|
-
var V4_PRIVATE_B_SECOND_FROM = 16;
|
|
74021
|
-
var V4_PRIVATE_B_SECOND_TO = 31;
|
|
74022
|
-
var V4_PRIVATE_C_OCTET = 192;
|
|
74023
|
-
var V4_PRIVATE_C_SECOND = 168;
|
|
74024
|
-
var V4_MULTICAST_FROM_OCTET = 224;
|
|
74025
|
-
var V6_UNIQUE_LOCAL_FROM = 64512;
|
|
74026
|
-
var V6_UNIQUE_LOCAL_TO = 65023;
|
|
74027
|
-
var V6_LINK_LOCAL_FROM = 65152;
|
|
74028
|
-
var V6_LINK_LOCAL_TO = 65215;
|
|
74029
|
-
var V6_MULTICAST_FROM = 65280;
|
|
74030
|
-
var IP_VERSION_4 = 4;
|
|
74031
|
-
var IP_VERSION_6 = 6;
|
|
74032
|
-
var HTTP_MOVED_PERMANENTLY = 301;
|
|
74033
|
-
var HTTP_FOUND = 302;
|
|
74034
|
-
var HTTP_SEE_OTHER = 303;
|
|
74035
|
-
var HTTP_TEMPORARY_REDIRECT = 307;
|
|
74036
|
-
var HTTP_PERMANENT_REDIRECT = 308;
|
|
74037
|
-
var SUCCESS_STATUS_MAX_EXCLUSIVE3 = 300;
|
|
74038
|
-
function refused(errorType, errorMessage2) {
|
|
74039
|
-
return { kind: "refused", errorType: `${ERROR_PREFIX}${errorType}`, errorMessage: errorMessage2 };
|
|
74040
|
-
}
|
|
74041
|
-
function isPrivateAddress(address) {
|
|
74042
|
-
const version2 = net3.isIP(address);
|
|
74043
|
-
if (version2 === IP_VERSION_4) {
|
|
74044
|
-
const octets = address.split(".").map((part) => Number(part));
|
|
74045
|
-
const first = octets[0] ?? -1;
|
|
74046
|
-
const second = octets[1] ?? -1;
|
|
74047
|
-
if (first === V4_THIS_NETWORK_OCTET || first === V4_PRIVATE_A_OCTET || first === V4_LOOPBACK_OCTET) {
|
|
74048
|
-
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
|
+
}
|
|
74049
74128
|
}
|
|
74050
|
-
|
|
74051
|
-
|
|
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
|
+
}
|
|
74052
74137
|
}
|
|
74053
|
-
|
|
74054
|
-
|
|
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;
|
|
74055
74149
|
}
|
|
74056
|
-
|
|
74057
|
-
|
|
74150
|
+
for (const minted of session.workerJwts) {
|
|
74151
|
+
if (isLiveCapability(presented, [minted])) {
|
|
74152
|
+
return true;
|
|
74153
|
+
}
|
|
74058
74154
|
}
|
|
74059
|
-
|
|
74060
|
-
|
|
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));
|
|
74061
74197
|
}
|
|
74062
|
-
|
|
74063
|
-
|
|
74064
|
-
|
|
74065
|
-
|
|
74066
|
-
|
|
74067
|
-
|
|
74068
|
-
|
|
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);
|
|
74069
74223
|
}
|
|
74070
|
-
|
|
74071
|
-
|
|
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
|
+
}
|
|
74072
74230
|
}
|
|
74073
|
-
const
|
|
74074
|
-
|
|
74075
|
-
return true;
|
|
74231
|
+
for (const sink of [...session.clientStreams]) {
|
|
74232
|
+
sink.write(frame);
|
|
74076
74233
|
}
|
|
74077
|
-
|
|
74078
|
-
|
|
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;
|
|
74079
74262
|
}
|
|
74080
|
-
|
|
74081
|
-
|
|
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;
|
|
74082
74271
|
}
|
|
74083
|
-
|
|
74084
|
-
|
|
74085
|
-
|
|
74086
|
-
|
|
74087
|
-
|
|
74088
|
-
|
|
74089
|
-
|
|
74090
|
-
|
|
74091
|
-
|
|
74092
|
-
}
|
|
74093
|
-
|
|
74094
|
-
if (contentType === void 0 || contentType.trim() === "") {
|
|
74095
|
-
return true;
|
|
74096
|
-
}
|
|
74097
|
-
const normalised = contentType.split(";")[0]?.trim().toLowerCase() ?? "";
|
|
74098
|
-
const slash = normalised.indexOf("/");
|
|
74099
|
-
const main2 = slash === -1 ? normalised : normalised.slice(0, slash);
|
|
74100
|
-
const sub = slash === -1 ? "" : normalised.slice(slash + 1);
|
|
74101
|
-
if (main2 === "text") {
|
|
74102
|
-
return true;
|
|
74103
|
-
}
|
|
74104
|
-
return /json|xml|javascript|ecmascript|yaml|csv/.test(sub);
|
|
74105
|
-
}
|
|
74106
|
-
async function readCapped(chunks) {
|
|
74107
|
-
const collected = [];
|
|
74108
|
-
let total = 0;
|
|
74109
|
-
for await (const chunk of chunks) {
|
|
74110
|
-
total += chunk.byteLength;
|
|
74111
|
-
if (total > RC_WEB_FETCH_MAX_BYTES) {
|
|
74112
|
-
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;
|
|
74113
74283
|
}
|
|
74114
|
-
|
|
74115
|
-
|
|
74116
|
-
|
|
74117
|
-
|
|
74118
|
-
|
|
74119
|
-
|
|
74120
|
-
|
|
74121
|
-
|
|
74122
|
-
|
|
74123
|
-
|
|
74124
|
-
|
|
74125
|
-
|
|
74126
|
-
|
|
74127
|
-
try {
|
|
74128
|
-
addresses = net3.isIP(hostname3) === 0 ? await deps.lookup(hostname3) : [hostname3];
|
|
74129
|
-
} catch {
|
|
74130
|
-
return refused("resolve", `${hostname3} did not resolve`);
|
|
74131
|
-
}
|
|
74132
|
-
for (const address of addresses) {
|
|
74133
|
-
if (isPrivateAddress(address)) {
|
|
74134
|
-
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;
|
|
74135
74297
|
}
|
|
74136
|
-
|
|
74137
|
-
|
|
74138
|
-
|
|
74139
|
-
|
|
74140
|
-
let current;
|
|
74141
|
-
try {
|
|
74142
|
-
current = new URL(url2);
|
|
74143
|
-
} catch {
|
|
74144
|
-
return refused("malformed_url", `${url2} is not a URL this proxy can dial`);
|
|
74145
|
-
}
|
|
74146
|
-
const controller = new AbortController();
|
|
74147
|
-
const timer = setTimeout(() => {
|
|
74148
|
-
controller.abort();
|
|
74149
|
-
}, deps.deadlineMs);
|
|
74150
|
-
timer.unref();
|
|
74151
|
-
try {
|
|
74152
|
-
let redirects = 0;
|
|
74153
|
-
for (; ; ) {
|
|
74154
|
-
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);
|
|
74155
74302
|
if (refusal !== void 0) {
|
|
74156
|
-
|
|
74303
|
+
await unauthorized(response, refusal);
|
|
74304
|
+
return;
|
|
74157
74305
|
}
|
|
74158
|
-
|
|
74159
|
-
|
|
74160
|
-
|
|
74161
|
-
}
|
|
74162
|
-
|
|
74163
|
-
|
|
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;
|
|
74164
74316
|
}
|
|
74165
|
-
|
|
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;
|
|
74166
74326
|
}
|
|
74167
|
-
if (
|
|
74168
|
-
|
|
74169
|
-
|
|
74170
|
-
|
|
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;
|
|
74171
74343
|
}
|
|
74172
|
-
|
|
74173
|
-
|
|
74174
|
-
|
|
74175
|
-
|
|
74176
|
-
controller.abort();
|
|
74177
|
-
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;
|
|
74178
74348
|
}
|
|
74179
|
-
const
|
|
74180
|
-
|
|
74181
|
-
|
|
74182
|
-
|
|
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) });
|
|
74183
74357
|
}
|
|
74184
|
-
|
|
74185
|
-
|
|
74186
|
-
|
|
74358
|
+
sweep();
|
|
74359
|
+
await answerJson2(response, HTTP_STATUS.ok, { results });
|
|
74360
|
+
return;
|
|
74187
74361
|
}
|
|
74188
|
-
if (
|
|
74189
|
-
|
|
74190
|
-
return
|
|
74362
|
+
if (workerTail === "events/stream" && method === "GET") {
|
|
74363
|
+
await holdStream(session, "workerStreams", (event) => event.toWorker, request7, response);
|
|
74364
|
+
return;
|
|
74191
74365
|
}
|
|
74192
|
-
|
|
74193
|
-
|
|
74194
|
-
|
|
74195
|
-
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;
|
|
74196
74370
|
}
|
|
74197
|
-
|
|
74198
|
-
|
|
74199
|
-
|
|
74200
|
-
|
|
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;
|
|
74201
74390
|
}
|
|
74202
|
-
|
|
74203
|
-
|
|
74204
|
-
|
|
74205
|
-
|
|
74206
|
-
|
|
74207
|
-
}
|
|
74208
|
-
|
|
74209
|
-
|
|
74210
|
-
|
|
74211
|
-
|
|
74212
|
-
|
|
74213
|
-
|
|
74214
|
-
|
|
74215
|
-
|
|
74216
|
-
|
|
74217
|
-
|
|
74218
|
-
|
|
74219
|
-
|
|
74220
|
-
|
|
74221
|
-
signal
|
|
74222
|
-
},
|
|
74223
|
-
(response) => {
|
|
74224
|
-
const location = response.headers.location;
|
|
74225
|
-
resolve({
|
|
74226
|
-
status: response.statusCode ?? 0,
|
|
74227
|
-
location: typeof location === "string" ? location : void 0,
|
|
74228
|
-
contentType: typeof response.headers["content-type"] === "string" ? response.headers["content-type"] : void 0,
|
|
74229
|
-
chunks: response
|
|
74230
|
-
});
|
|
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;
|
|
74231
74410
|
}
|
|
74232
|
-
|
|
74233
|
-
|
|
74234
|
-
|
|
74235
|
-
|
|
74236
|
-
|
|
74237
|
-
|
|
74238
|
-
|
|
74239
|
-
|
|
74240
|
-
|
|
74241
|
-
|
|
74242
|
-
|
|
74243
|
-
|
|
74244
|
-
|
|
74245
|
-
|
|
74246
|
-
return env[RC_SELF_HOST_ENV] === "1";
|
|
74247
|
-
}
|
|
74248
|
-
var RC_SELF_HOST_SCOPE_LIST = ["user:profile", "user:inference", "user:sessions:claude_code", "user:mcp_servers", "user:file_upload"];
|
|
74249
|
-
var RC_SESSIONS_PATH_PREFIX2 = "/v1/code/sessions";
|
|
74250
|
-
var RC_COMPAT_SESSIONS_PATH = "/v1/sessions";
|
|
74251
|
-
var RC_SESSION_ID_PREFIX2 = "cse_";
|
|
74252
|
-
var RC_SELF_HOST_API_BASE_URL = "https://api.anthropic.com";
|
|
74253
|
-
var RC_SELF_HOST_OAUTH_HOST = "platform.claude.com";
|
|
74254
|
-
var HTTP_CONFLICT = 409;
|
|
74255
|
-
var MS_PER_SECOND7 = 1e3;
|
|
74256
|
-
var HTTP_SERVICE_UNAVAILABLE = 503;
|
|
74257
|
-
var RC_SELF_HOST_WORKER_JWT_TTL_SECONDS = 46800;
|
|
74258
|
-
var RC_SELF_HOST_OAUTH_TOKEN_TTL_SECONDS = 3600;
|
|
74259
|
-
var RC_SELF_HOST_KEEPALIVE_MS = 15e3;
|
|
74260
|
-
var RC_SELF_HOST_RETENTION_MS = 6e5;
|
|
74261
|
-
var RC_PRESENCE_REFRESH_SECONDS = 60;
|
|
74262
|
-
var RC_SELF_HOST_MAX_BATCH_EVENTS = 100;
|
|
74263
|
-
var RC_SELF_HOST_WORKER_EPOCH = 1;
|
|
74264
|
-
var RC_STREAM_EVENT_NAME = "client_event";
|
|
74265
|
-
var RC_SOURCE_WORKER = "worker";
|
|
74266
|
-
var RC_SOURCE_CLIENT = "client";
|
|
74267
|
-
var CONFLICT_REASON_HEADER2 = "x-ccr-conflict-reason";
|
|
74268
|
-
var CONFLICT_REASON_EPOCH_STALE = "epoch_stale";
|
|
74269
|
-
function isRecord5(value2) {
|
|
74270
|
-
return typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
|
|
74271
|
-
}
|
|
74272
|
-
function isWebSearchRefusal(value2) {
|
|
74273
|
-
return !Array.isArray(value2);
|
|
74274
|
-
}
|
|
74275
|
-
function nonEmptyString2(record2, field) {
|
|
74276
|
-
const value2 = record2 === void 0 ? void 0 : record2[field];
|
|
74277
|
-
return typeof value2 === "string" && value2 !== "" ? value2 : void 0;
|
|
74278
|
-
}
|
|
74279
|
-
function numberOf(record2, field) {
|
|
74280
|
-
const value2 = record2 === void 0 ? void 0 : record2[field];
|
|
74281
|
-
if (typeof value2 === "number" && Number.isFinite(value2)) {
|
|
74282
|
-
return value2;
|
|
74283
|
-
}
|
|
74284
|
-
if (typeof value2 === "string" && /^[0-9]+$/.test(value2)) {
|
|
74285
|
-
return Number(value2);
|
|
74286
|
-
}
|
|
74287
|
-
return void 0;
|
|
74288
|
-
}
|
|
74289
|
-
function stringArray(record2, field) {
|
|
74290
|
-
const value2 = record2 === void 0 ? void 0 : record2[field];
|
|
74291
|
-
if (!Array.isArray(value2)) {
|
|
74292
|
-
return void 0;
|
|
74293
|
-
}
|
|
74294
|
-
const members2 = [];
|
|
74295
|
-
for (const member of value2) {
|
|
74296
|
-
if (typeof member !== "string") {
|
|
74297
|
-
return void 0;
|
|
74298
|
-
}
|
|
74299
|
-
members2.push(member);
|
|
74300
|
-
}
|
|
74301
|
-
return members2;
|
|
74302
|
-
}
|
|
74303
|
-
function singleHeader2(headers, name) {
|
|
74304
|
-
const value2 = headers[name];
|
|
74305
|
-
return typeof value2 === "string" ? value2 : void 0;
|
|
74306
|
-
}
|
|
74307
|
-
function bearerOf(headers) {
|
|
74308
|
-
const presented = singleHeader2(headers, "authorization");
|
|
74309
|
-
if (presented === void 0) {
|
|
74310
|
-
return void 0;
|
|
74311
|
-
}
|
|
74312
|
-
return presented.slice(0, "bearer ".length).toLowerCase() === "bearer " ? presented.slice("bearer ".length) : void 0;
|
|
74313
|
-
}
|
|
74314
|
-
function presentationRefusal(headers) {
|
|
74315
|
-
const presented = singleHeader2(headers, "authorization");
|
|
74316
|
-
if (presented === void 0) {
|
|
74317
|
-
return "absent";
|
|
74318
|
-
}
|
|
74319
|
-
if (presented.slice(0, "bearer ".length).toLowerCase() !== "bearer ") {
|
|
74320
|
-
return "not-a-bearer";
|
|
74321
|
-
}
|
|
74322
|
-
const bearer = presented.slice("bearer ".length);
|
|
74323
|
-
return bearer.slice(0, "bearer ".length).toLowerCase() === "bearer " ? "doubled-scheme-word" : "unrecognised";
|
|
74324
|
-
}
|
|
74325
|
-
async function readBody3(request7, capBytes) {
|
|
74326
|
-
return await new Promise((resolve, reject) => {
|
|
74327
|
-
const chunks = [];
|
|
74328
|
-
let total = 0;
|
|
74329
|
-
request7.on("data", (chunk) => {
|
|
74330
|
-
total += chunk.length;
|
|
74331
|
-
if (total > capBytes) {
|
|
74332
|
-
request7.destroy();
|
|
74333
|
-
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 } });
|
|
74334
74425
|
return;
|
|
74335
74426
|
}
|
|
74336
|
-
|
|
74337
|
-
|
|
74338
|
-
|
|
74339
|
-
|
|
74340
|
-
|
|
74341
|
-
|
|
74342
|
-
reject(new Error("the request body ended early"));
|
|
74343
|
-
});
|
|
74344
|
-
});
|
|
74345
|
-
}
|
|
74346
|
-
function parseJsonObject2(body) {
|
|
74347
|
-
let parsed;
|
|
74348
|
-
try {
|
|
74349
|
-
parsed = JSON.parse(body);
|
|
74350
|
-
} catch {
|
|
74351
|
-
return void 0;
|
|
74352
|
-
}
|
|
74353
|
-
return isRecord5(parsed) ? parsed : void 0;
|
|
74354
|
-
}
|
|
74355
|
-
function mintWorkerJwt(randomToken, issuedAt) {
|
|
74356
|
-
const encode3 = (value2) => Buffer.from(JSON.stringify(value2), "utf8").toString("base64url");
|
|
74357
|
-
const header = encode3({ alg: "HS256", typ: "JWT" });
|
|
74358
|
-
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) });
|
|
74359
|
-
return `${header}.${payload}.${Buffer.from(randomToken(), "utf8").toString("base64url")}`;
|
|
74360
|
-
}
|
|
74361
|
-
function growthBookFeatures() {
|
|
74362
|
-
return {
|
|
74363
|
-
tengu_ccr_bridge: { defaultValue: true },
|
|
74364
|
-
tengu_bridge_repl_v2: { defaultValue: true },
|
|
74365
|
-
tengu_bridge_repl_v2_cse_shim_enabled: { defaultValue: true },
|
|
74366
|
-
tengu_bridge_min_version: { defaultValue: { minVersion: "0.0.0" } }
|
|
74367
|
-
};
|
|
74368
|
-
}
|
|
74369
|
-
function createRcSelfHostSurface(deps) {
|
|
74370
|
-
const sessions = /* @__PURE__ */ new Map();
|
|
74371
|
-
const sweep = () => {
|
|
74372
|
-
const now = deps.now();
|
|
74373
|
-
for (const session of sessions.values()) {
|
|
74374
|
-
while (session.events[0] !== void 0 && now - session.events[0].storedAt > RC_SELF_HOST_RETENTION_MS) {
|
|
74375
|
-
const dropped = session.events.shift();
|
|
74376
|
-
const uuid3 = dropped === void 0 ? void 0 : nonEmptyString2(dropped.payload, "uuid");
|
|
74377
|
-
if (uuid3 !== void 0) {
|
|
74378
|
-
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;
|
|
74379
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;
|
|
74380
74455
|
}
|
|
74381
|
-
|
|
74382
|
-
|
|
74383
|
-
}
|
|
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;
|
|
74384
74458
|
}
|
|
74385
|
-
|
|
74386
|
-
|
|
74387
|
-
|
|
74388
|
-
|
|
74389
|
-
|
|
74390
|
-
|
|
74391
|
-
|
|
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 });
|
|
74392
74479
|
}
|
|
74480
|
+
sweep();
|
|
74481
|
+
await answerJson2(response, HTTP_STATUS.ok, { results });
|
|
74482
|
+
return;
|
|
74393
74483
|
}
|
|
74394
|
-
|
|
74395
|
-
|
|
74396
|
-
|
|
74397
|
-
const expected = deps.credentialRecord()?.accessToken;
|
|
74398
|
-
const presented = bearerOf(headers);
|
|
74399
|
-
return expected !== void 0 && presented !== void 0 && isLiveCapability(presented, [expected]);
|
|
74400
|
-
};
|
|
74401
|
-
const presentsWorkerJwt = (headers, session) => {
|
|
74402
|
-
const presented = bearerOf(headers);
|
|
74403
|
-
if (presented === void 0) {
|
|
74404
|
-
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;
|
|
74405
74487
|
}
|
|
74406
|
-
|
|
74407
|
-
|
|
74408
|
-
|
|
74409
|
-
}
|
|
74488
|
+
if (tail === "events/stream" && method === "GET") {
|
|
74489
|
+
await holdStream(session, "clientStreams", () => true, request7, response);
|
|
74490
|
+
return;
|
|
74410
74491
|
}
|
|
74411
|
-
|
|
74412
|
-
|
|
74413
|
-
|
|
74414
|
-
|
|
74415
|
-
const workerOrCredentialRefusal = (headers, session) => presentsWorkerJwt(headers, session) || presentsCredential(headers) ? void 0 : presentationRefusal(headers);
|
|
74416
|
-
const frameOf = (event) => {
|
|
74417
|
-
const envelope = {
|
|
74418
|
-
event_id: event.eventId,
|
|
74419
|
-
event_type: nonEmptyString2(event.payload, "type") ?? "event",
|
|
74420
|
-
sequence_num: event.sequenceNum,
|
|
74421
|
-
source: event.source,
|
|
74422
|
-
payload: event.payload,
|
|
74423
|
-
created_at: event.createdAt
|
|
74424
|
-
};
|
|
74425
|
-
return `event: ${RC_STREAM_EVENT_NAME}
|
|
74426
|
-
id: ${String(event.sequenceNum)}
|
|
74427
|
-
data: ${JSON.stringify(envelope)}
|
|
74428
|
-
|
|
74429
|
-
`;
|
|
74430
|
-
};
|
|
74431
|
-
const holdStream = async (session, which, wantsReplay, request7, response) => {
|
|
74432
|
-
const url2 = new URL(request7.url, "http://127.0.0.1");
|
|
74433
|
-
const cursor3 = numberOf({ value: url2.searchParams.get("from_sequence_num") }, "value") ?? numberOf({ value: singleHeader2(request7.headers, "last-event-id") }, "value");
|
|
74434
|
-
const replay = cursor3 === void 0 ? [] : session.events.filter((event) => wantsReplay(event) && event.sequenceNum > cursor3);
|
|
74435
|
-
response.start(HTTP_STATUS.ok, { "content-type": "text/event-stream", "cache-control": "no-cache" });
|
|
74436
|
-
response.flush();
|
|
74437
|
-
let pending = Promise.resolve();
|
|
74438
|
-
const sink = {
|
|
74439
|
-
write: (frame) => {
|
|
74440
|
-
pending = pending.then(async () => {
|
|
74441
|
-
await response.write(frame);
|
|
74442
|
-
}).catch(() => {
|
|
74443
|
-
});
|
|
74444
|
-
},
|
|
74445
|
-
retire: () => {
|
|
74446
|
-
session[which].delete(sink);
|
|
74447
|
-
}
|
|
74448
|
-
};
|
|
74449
|
-
session[which].add(sink);
|
|
74450
|
-
session.lastTrafficAt = deps.now();
|
|
74451
|
-
for (const event of replay) {
|
|
74452
|
-
sink.write(frameOf(event));
|
|
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;
|
|
74453
74496
|
}
|
|
74454
|
-
|
|
74455
|
-
request7.
|
|
74456
|
-
|
|
74457
|
-
}, { once: true });
|
|
74458
|
-
});
|
|
74459
|
-
sink.retire();
|
|
74460
|
-
response.end();
|
|
74461
|
-
};
|
|
74462
|
-
const publish = (session, source, payload) => {
|
|
74463
|
-
const uuid3 = nonEmptyString2(payload, "uuid");
|
|
74464
|
-
const duplicate = uuid3 !== void 0 && session.seenPayloadUuids.has(uuid3);
|
|
74465
|
-
session.sequenceNum += 1;
|
|
74466
|
-
const event = {
|
|
74467
|
-
sequenceNum: session.sequenceNum,
|
|
74468
|
-
eventId: uuid3 ?? deps.newUuid(),
|
|
74469
|
-
source,
|
|
74470
|
-
// 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.
|
|
74471
|
-
payload: source === RC_SOURCE_CLIENT ? { ...payload, server_received_wall_ms: deps.now() } : payload,
|
|
74472
|
-
createdAt: new Date(deps.now()).toISOString(),
|
|
74473
|
-
toWorker: source === RC_SOURCE_CLIENT,
|
|
74474
|
-
storedAt: deps.now()
|
|
74475
|
-
};
|
|
74476
|
-
session.events.push(event);
|
|
74477
|
-
if (uuid3 !== void 0) {
|
|
74478
|
-
session.seenPayloadUuids.add(uuid3);
|
|
74479
|
-
}
|
|
74480
|
-
session.lastTrafficAt = event.storedAt;
|
|
74481
|
-
const frame = frameOf(event);
|
|
74482
|
-
if (event.toWorker) {
|
|
74483
|
-
for (const sink of [...session.workerStreams]) {
|
|
74484
|
-
sink.write(frame);
|
|
74485
|
-
}
|
|
74486
|
-
}
|
|
74487
|
-
for (const sink of [...session.clientStreams]) {
|
|
74488
|
-
sink.write(frame);
|
|
74489
|
-
}
|
|
74490
|
-
return { sequenceNum: event.sequenceNum, eventId: event.eventId, duplicate };
|
|
74491
|
-
};
|
|
74492
|
-
const answerJson2 = async (response, status, body, extraHeaders = {}) => {
|
|
74493
|
-
const text2 = JSON.stringify(body);
|
|
74494
|
-
response.start(status, { "content-type": "application/json", "content-length": String(Buffer.byteLength(text2, "utf8")), ...extraHeaders });
|
|
74495
|
-
await response.write(text2);
|
|
74496
|
-
response.end();
|
|
74497
|
-
};
|
|
74498
|
-
const noCredential = async (response) => {
|
|
74499
|
-
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" } });
|
|
74500
|
-
};
|
|
74501
|
-
const epochStale = async (response) => {
|
|
74502
|
-
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 });
|
|
74503
|
-
};
|
|
74504
|
-
const refusalMessages = {
|
|
74505
|
-
absent: "the self-hosted Remote Control surface authenticates by the Authorization header, and this call carried none",
|
|
74506
|
-
"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",
|
|
74507
|
-
"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",
|
|
74508
|
-
unrecognised: "the self-hosted Remote Control surface did not recognise the presented credential"
|
|
74509
|
-
};
|
|
74510
|
-
const unauthorized = async (response, refusal) => {
|
|
74511
|
-
await answerJson2(response, HTTP_STATUS.unauthorized, { type: "error", error: { type: "authentication_error", message: refusalMessages[refusal] } });
|
|
74512
|
-
};
|
|
74513
|
-
const serveSessionPath = async (session, tail, request7, response) => {
|
|
74514
|
-
const method = request7.method.toUpperCase();
|
|
74515
|
-
if (session === void 0) {
|
|
74516
|
-
await answerJson2(response, HTTP_STATUS.notFound, { type: "error", error: { type: "api_error", message: "no such Remote Control session on this door" } });
|
|
74517
|
-
return;
|
|
74518
|
-
}
|
|
74519
|
-
session.lastTrafficAt = deps.now();
|
|
74520
|
-
if (tail === "") {
|
|
74521
|
-
if (method !== "GET") {
|
|
74522
|
-
await answerJson2(response, HTTP_STATUS.methodNotAllowed, { type: "error", error: { type: "api_error", message: "the session read is a GET" } });
|
|
74523
|
-
return;
|
|
74524
|
-
}
|
|
74525
|
-
await answerJson2(response, HTTP_STATUS.ok, { session: { id: session.id, title: session.title, created_at: new Date(session.createdAt).toISOString(), status: "active" } });
|
|
74526
|
-
return;
|
|
74527
|
-
}
|
|
74528
|
-
if (tail === "bridge" && method === "POST") {
|
|
74529
|
-
const refusal = credentialRefusal(request7.headers);
|
|
74530
|
-
if (refusal !== void 0) {
|
|
74531
|
-
await unauthorized(response, refusal);
|
|
74532
|
-
return;
|
|
74533
|
-
}
|
|
74534
|
-
const workerJwt = mintWorkerJwt(deps.randomToken, deps.now());
|
|
74535
|
-
session.workerJwts.add(workerJwt);
|
|
74536
|
-
deps.log?.(`rc selfhost ${session.id}: bridge registration minted a worker credential`);
|
|
74537
|
-
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 });
|
|
74497
|
+
if (tail === "mark_read" && method === "POST") {
|
|
74498
|
+
await readBody3(request7.body, CONTROL_BODY_CAP_BYTES);
|
|
74499
|
+
await answerJson2(response, HTTP_STATUS.ok, {});
|
|
74538
74500
|
return;
|
|
74539
74501
|
}
|
|
74540
|
-
if (tail === "
|
|
74541
|
-
|
|
74542
|
-
if (refusal !== void 0) {
|
|
74543
|
-
await unauthorized(response, refusal);
|
|
74544
|
-
return;
|
|
74545
|
-
}
|
|
74546
|
-
for (const sink of [...session.workerStreams, ...session.clientStreams]) {
|
|
74547
|
-
sink.retire();
|
|
74548
|
-
}
|
|
74549
|
-
sessions.delete(session.id);
|
|
74550
|
-
deps.log?.(`rc selfhost ${session.id}: archived at its client's request`);
|
|
74502
|
+
if (tail === "teleport-events" && method === "POST") {
|
|
74503
|
+
await readBody3(request7.body, CONTROL_BODY_CAP_BYTES);
|
|
74551
74504
|
await answerJson2(response, HTTP_STATUS.ok, {});
|
|
74552
74505
|
return;
|
|
74553
74506
|
}
|
|
74554
|
-
|
|
74555
|
-
|
|
74556
|
-
|
|
74557
|
-
|
|
74558
|
-
|
|
74559
|
-
|
|
74560
|
-
|
|
74561
|
-
|
|
74562
|
-
|
|
74563
|
-
|
|
74507
|
+
await answerJson2(response, HTTP_STATUS.notFound, { type: "error", error: { type: "api_error", message: `the self-hosted Remote Control surface serves no path "${tail}"` } });
|
|
74508
|
+
};
|
|
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);
|
|
74564
74519
|
return;
|
|
74565
74520
|
}
|
|
74566
|
-
if (
|
|
74567
|
-
|
|
74568
|
-
|
|
74569
|
-
if (claimedEpoch !== void 0 && claimedEpoch !== RC_SELF_HOST_WORKER_EPOCH) {
|
|
74570
|
-
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" } });
|
|
74571
74524
|
return;
|
|
74572
74525
|
}
|
|
74573
|
-
const
|
|
74574
|
-
if (
|
|
74575
|
-
|
|
74576
|
-
|
|
74577
|
-
if (isRecord5(body?.external_metadata)) {
|
|
74578
|
-
session.externalMetadata = body.external_metadata;
|
|
74526
|
+
const listRefusal = credentialRefusal(request7.headers);
|
|
74527
|
+
if (listRefusal !== void 0) {
|
|
74528
|
+
await unauthorized(response, listRefusal);
|
|
74529
|
+
return;
|
|
74579
74530
|
}
|
|
74580
|
-
await answerJson2(response, HTTP_STATUS.ok, {});
|
|
74581
|
-
return;
|
|
74582
|
-
}
|
|
74583
|
-
if (workerTail === "register" && method === "POST") {
|
|
74584
|
-
await readBody3(request7.body, CONTROL_BODY_CAP_BYTES);
|
|
74585
|
-
await answerJson2(response, HTTP_STATUS.ok, { worker_epoch: RC_SELF_HOST_WORKER_EPOCH });
|
|
74531
|
+
await answerJson2(response, HTTP_STATUS.ok, { data: [] });
|
|
74586
74532
|
return;
|
|
74587
74533
|
}
|
|
74588
|
-
if (
|
|
74589
|
-
await
|
|
74590
|
-
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}"` } });
|
|
74591
74536
|
return;
|
|
74592
74537
|
}
|
|
74593
|
-
if (
|
|
74594
|
-
|
|
74595
|
-
|
|
74596
|
-
|
|
74597
|
-
|
|
74598
|
-
|
|
74599
|
-
|
|
74600
|
-
|
|
74601
|
-
|
|
74602
|
-
|
|
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" } });
|
|
74603
74564
|
return;
|
|
74604
74565
|
}
|
|
74605
|
-
|
|
74606
|
-
|
|
74607
|
-
if (
|
|
74608
|
-
await
|
|
74566
|
+
if (method === "GET") {
|
|
74567
|
+
const readRefusal = credentialRefusal(request7.headers);
|
|
74568
|
+
if (readRefusal !== void 0) {
|
|
74569
|
+
await unauthorized(response, readRefusal);
|
|
74609
74570
|
return;
|
|
74610
74571
|
}
|
|
74611
|
-
|
|
74612
|
-
|
|
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;
|
|
74613
74574
|
}
|
|
74614
|
-
|
|
74615
|
-
await answerJson2(response, HTTP_STATUS.ok, { results });
|
|
74616
|
-
return;
|
|
74617
|
-
}
|
|
74618
|
-
if (workerTail === "events/stream" && method === "GET") {
|
|
74619
|
-
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)" } });
|
|
74620
74576
|
return;
|
|
74621
74577
|
}
|
|
74622
|
-
|
|
74623
|
-
|
|
74624
|
-
|
|
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" } });
|
|
74625
74584
|
return;
|
|
74626
74585
|
}
|
|
74627
|
-
|
|
74628
|
-
|
|
74629
|
-
|
|
74630
|
-
|
|
74631
|
-
|
|
74632
|
-
|
|
74633
|
-
|
|
74634
|
-
|
|
74635
|
-
|
|
74636
|
-
|
|
74637
|
-
|
|
74638
|
-
|
|
74639
|
-
|
|
74640
|
-
|
|
74641
|
-
|
|
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;
|
|
74642
74610
|
}
|
|
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;
|
|
74643
74613
|
}
|
|
74644
|
-
|
|
74645
|
-
|
|
74646
|
-
|
|
74647
|
-
|
|
74648
|
-
|
|
74649
|
-
const cursor3 = url2.searchParams.get("cursor");
|
|
74650
|
-
const from = cursor3 === null ? 0 : session.internalEvents.findIndex((event) => event.cursor === cursor3) + 1;
|
|
74651
|
-
const page = session.internalEvents.slice(from);
|
|
74652
|
-
await answerJson2(response, HTTP_STATUS.ok, { data: page.map((event) => event.body), next_cursor: page.length === 0 ? void 0 : page[page.length - 1]?.cursor });
|
|
74653
|
-
return;
|
|
74654
|
-
}
|
|
74655
|
-
if (workerTail === "diagnostics" && method === "POST") {
|
|
74656
|
-
await readBody3(request7.body, CONTROL_BODY_CAP_BYTES);
|
|
74657
|
-
await answerJson2(response, HTTP_STATUS.ok, {});
|
|
74658
|
-
return;
|
|
74659
|
-
}
|
|
74660
|
-
if (workerTail === "web-fetch" && method === "POST") {
|
|
74661
|
-
const body = parseJsonObject2(await readBody3(request7.body, CONTROL_BODY_CAP_BYTES));
|
|
74662
|
-
const target = nonEmptyString2(body, "url");
|
|
74663
|
-
if (target === void 0) {
|
|
74664
|
-
await answerJson2(response, HTTP_STATUS.badRequest, { type: "error", error: { type: "invalid_request_error", message: "a worker web-fetch carries one url" } });
|
|
74665
|
-
return;
|
|
74666
|
-
}
|
|
74667
|
-
const outcome = await deps.webFetch(target);
|
|
74668
|
-
if (outcome.kind === "fetched") {
|
|
74669
|
-
const answer = { url: target, destination_url: outcome.destinationUrl, text: outcome.text, ...outcome.contentType === void 0 ? {} : { content_type: outcome.contentType } };
|
|
74670
|
-
if (Buffer.byteLength(JSON.stringify(answer), "utf8") > RC_WEB_FETCH_MAX_BYTES) {
|
|
74671
|
-
deps.log?.(`rc selfhost ${session.id}: worker web-fetch of ${target} escaped past the reader cap when framed`);
|
|
74672
|
-
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` } });
|
|
74673
|
-
return;
|
|
74674
|
-
}
|
|
74675
|
-
deps.log?.(`rc selfhost ${session.id}: worker web-fetch served ${target} as ${outcome.destinationUrl}`);
|
|
74676
|
-
await answerJson2(response, HTTP_STATUS.ok, answer);
|
|
74677
|
-
return;
|
|
74678
|
-
}
|
|
74679
|
-
deps.log?.(`rc selfhost ${session.id}: worker web-fetch of ${target} refused: ${outcome.errorType}`);
|
|
74680
|
-
await answerJson2(response, HTTP_STATUS.ok, { error: { error_type: outcome.errorType, error_message: outcome.errorMessage } });
|
|
74681
|
-
return;
|
|
74682
|
-
}
|
|
74683
|
-
if (workerTail === "web-search" && method === "POST") {
|
|
74684
|
-
const body = parseJsonObject2(await readBody3(request7.body, CONTROL_BODY_CAP_BYTES));
|
|
74685
|
-
const query = nonEmptyString2(body, "query");
|
|
74686
|
-
if (query === void 0) {
|
|
74687
|
-
await answerJson2(response, HTTP_STATUS.badRequest, { type: "error", error: { type: "invalid_request_error", message: "a worker web-search carries one query" } });
|
|
74688
|
-
return;
|
|
74689
|
-
}
|
|
74690
|
-
const allowedDomains = stringArray(body, "allowed_domains");
|
|
74691
|
-
const blockedDomains = stringArray(body, "blocked_domains");
|
|
74692
|
-
if (body?.allowed_domains !== void 0 && allowedDomains === void 0 || body?.blocked_domains !== void 0 && blockedDomains === void 0) {
|
|
74693
|
-
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" } });
|
|
74694
|
-
return;
|
|
74695
|
-
}
|
|
74696
|
-
const searchProfile = nonEmptyString2(body, "search_profile");
|
|
74697
|
-
const backend = deps.webSearch;
|
|
74698
|
-
if (backend === void 0) {
|
|
74699
|
-
deps.log?.(`rc selfhost ${session.id}: worker web-search of "${query}" refused: no search backend serves this door`);
|
|
74700
|
-
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" } });
|
|
74701
|
-
return;
|
|
74702
|
-
}
|
|
74703
|
-
const answered = await backend({ query, ...allowedDomains === void 0 ? {} : { allowedDomains }, ...blockedDomains === void 0 ? {} : { blockedDomains }, ...searchProfile === void 0 ? {} : { searchProfile } });
|
|
74704
|
-
deps.log?.(`rc selfhost ${session.id}: worker web-search of "${query}" answered by its backend`);
|
|
74705
|
-
if (isWebSearchRefusal(answered)) {
|
|
74706
|
-
await answerJson2(response, HTTP_STATUS.ok, { results: [], error: { error_type: answered.errorType, error_message: answered.errorMessage } });
|
|
74707
|
-
return;
|
|
74708
|
-
}
|
|
74709
|
-
await answerJson2(response, HTTP_STATUS.ok, { results: answered.map((result) => ({ title: result.title ?? "", url: result.url, snippet: result.snippet ?? "" })) });
|
|
74710
|
-
return;
|
|
74711
|
-
}
|
|
74712
|
-
await answerJson2(response, HTTP_STATUS.notFound, { type: "error", error: { type: "api_error", message: `the self-hosted Remote Control surface serves no worker path "${workerTail}"` } });
|
|
74713
|
-
return;
|
|
74714
|
-
}
|
|
74715
|
-
const clientRefusal = credentialRefusal(request7.headers);
|
|
74716
|
-
if (clientRefusal !== void 0) {
|
|
74717
|
-
await unauthorized(response, clientRefusal);
|
|
74718
|
-
return;
|
|
74719
|
-
}
|
|
74720
|
-
if (tail === "events" && method === "POST") {
|
|
74721
|
-
const body = parseJsonObject2(await readBody3(request7.body, CONTROL_BODY_CAP_BYTES));
|
|
74722
|
-
const events = body?.events;
|
|
74723
|
-
if (!Array.isArray(events) || events.length === 0 || events.length > RC_SELF_HOST_MAX_BATCH_EVENTS) {
|
|
74724
|
-
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` } });
|
|
74725
|
-
return;
|
|
74726
|
-
}
|
|
74727
|
-
const results = [];
|
|
74728
|
-
for (const event of events) {
|
|
74729
|
-
if (!isRecord5(event) || !isRecord5(event.payload)) {
|
|
74730
|
-
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" } });
|
|
74731
|
-
return;
|
|
74732
|
-
}
|
|
74733
|
-
const published = publish(session, RC_SOURCE_CLIENT, event.payload);
|
|
74734
|
-
results.push({ sequence_num: String(published.sequenceNum), duplicate: published.duplicate });
|
|
74735
|
-
}
|
|
74736
|
-
sweep();
|
|
74737
|
-
await answerJson2(response, HTTP_STATUS.ok, { results });
|
|
74738
|
-
return;
|
|
74739
|
-
}
|
|
74740
|
-
if (tail === "events" && method === "GET") {
|
|
74741
|
-
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 })) });
|
|
74742
|
-
return;
|
|
74743
|
-
}
|
|
74744
|
-
if (tail === "events/stream" && method === "GET") {
|
|
74745
|
-
await holdStream(session, "clientStreams", () => true, request7, response);
|
|
74746
|
-
return;
|
|
74747
|
-
}
|
|
74748
|
-
if (tail === "client/presence" && method === "POST") {
|
|
74749
|
-
await readBody3(request7.body, CONTROL_BODY_CAP_BYTES);
|
|
74750
|
-
await answerJson2(response, HTTP_STATUS.ok, { refresh_after_seconds: RC_PRESENCE_REFRESH_SECONDS });
|
|
74751
|
-
return;
|
|
74752
|
-
}
|
|
74753
|
-
if (tail === "mark_read" && method === "POST") {
|
|
74754
|
-
await readBody3(request7.body, CONTROL_BODY_CAP_BYTES);
|
|
74755
|
-
await answerJson2(response, HTTP_STATUS.ok, {});
|
|
74756
|
-
return;
|
|
74757
|
-
}
|
|
74758
|
-
if (tail === "teleport-events" && method === "POST") {
|
|
74759
|
-
await readBody3(request7.body, CONTROL_BODY_CAP_BYTES);
|
|
74760
|
-
await answerJson2(response, HTTP_STATUS.ok, {});
|
|
74761
|
-
return;
|
|
74762
|
-
}
|
|
74763
|
-
await answerJson2(response, HTTP_STATUS.notFound, { type: "error", error: { type: "api_error", message: `the self-hosted Remote Control surface serves no path "${tail}"` } });
|
|
74764
|
-
};
|
|
74765
|
-
const recordMissing = () => deps.credentialRecord() === void 0;
|
|
74766
|
-
const route = {
|
|
74767
|
-
name: "rc-selfhost",
|
|
74768
|
-
headroomEligible: false,
|
|
74769
|
-
headroomUpstream: void 0,
|
|
74770
|
-
serve: async (request7, response) => {
|
|
74771
|
-
const method = request7.method.toUpperCase();
|
|
74772
|
-
const pathname = new URL(request7.url, "http://127.0.0.1").pathname;
|
|
74773
|
-
if (recordMissing()) {
|
|
74774
|
-
await noCredential(response);
|
|
74775
|
-
return;
|
|
74776
|
-
}
|
|
74777
|
-
if (pathname === RC_COMPAT_SESSIONS_PATH) {
|
|
74778
|
-
if (method !== "GET") {
|
|
74779
|
-
await answerJson2(response, HTTP_STATUS.methodNotAllowed, { type: "error", error: { type: "api_error", message: "the compatibility session list is a GET" } });
|
|
74780
|
-
return;
|
|
74781
|
-
}
|
|
74782
|
-
const listRefusal = credentialRefusal(request7.headers);
|
|
74783
|
-
if (listRefusal !== void 0) {
|
|
74784
|
-
await unauthorized(response, listRefusal);
|
|
74785
|
-
return;
|
|
74786
|
-
}
|
|
74787
|
-
await answerJson2(response, HTTP_STATUS.ok, { data: [] });
|
|
74788
|
-
return;
|
|
74789
|
-
}
|
|
74790
|
-
if (pathname !== RC_SESSIONS_PATH_PREFIX2 && !pathname.startsWith(`${RC_SESSIONS_PATH_PREFIX2}/`)) {
|
|
74791
|
-
await answerJson2(response, HTTP_STATUS.notFound, { type: "error", error: { type: "api_error", message: `the self-hosted Remote Control surface serves no path "${pathname}"` } });
|
|
74792
|
-
return;
|
|
74793
|
-
}
|
|
74794
|
-
if (pathname === RC_SESSIONS_PATH_PREFIX2) {
|
|
74795
|
-
if (method === "POST") {
|
|
74796
|
-
const createRefusal = credentialRefusal(request7.headers);
|
|
74797
|
-
if (createRefusal !== void 0) {
|
|
74798
|
-
await unauthorized(response, createRefusal);
|
|
74799
|
-
return;
|
|
74800
|
-
}
|
|
74801
|
-
const body = parseJsonObject2(await readBody3(request7.body, CONTROL_BODY_CAP_BYTES));
|
|
74802
|
-
const session = {
|
|
74803
|
-
id: `${RC_SESSION_ID_PREFIX2}${deps.newUuid()}`,
|
|
74804
|
-
createdAt: deps.now(),
|
|
74805
|
-
title: nonEmptyString2(body, "title"),
|
|
74806
|
-
workerJwts: /* @__PURE__ */ new Set(),
|
|
74807
|
-
sequenceNum: 0,
|
|
74808
|
-
events: [],
|
|
74809
|
-
seenPayloadUuids: /* @__PURE__ */ new Set(),
|
|
74810
|
-
workerStreams: /* @__PURE__ */ new Set(),
|
|
74811
|
-
clientStreams: /* @__PURE__ */ new Set(),
|
|
74812
|
-
internalEvents: [],
|
|
74813
|
-
workerStatus: void 0,
|
|
74814
|
-
externalMetadata: void 0,
|
|
74815
|
-
lastTrafficAt: deps.now()
|
|
74816
|
-
};
|
|
74817
|
-
sessions.set(session.id, session);
|
|
74818
|
-
deps.log?.(`rc selfhost ${session.id}: session created on the door's own surface`);
|
|
74819
|
-
await answerJson2(response, HTTP_STATUS.ok, { session: { id: session.id, title: session.title, created_at: new Date(session.createdAt).toISOString(), status: "active" } });
|
|
74820
|
-
return;
|
|
74821
|
-
}
|
|
74822
|
-
if (method === "GET") {
|
|
74823
|
-
const readRefusal = credentialRefusal(request7.headers);
|
|
74824
|
-
if (readRefusal !== void 0) {
|
|
74825
|
-
await unauthorized(response, readRefusal);
|
|
74826
|
-
return;
|
|
74827
|
-
}
|
|
74828
|
-
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" })) });
|
|
74829
|
-
return;
|
|
74830
|
-
}
|
|
74831
|
-
await answerJson2(response, HTTP_STATUS.methodNotAllowed, { type: "error", error: { type: "api_error", message: "the session collection is a POST (create) and a GET (list)" } });
|
|
74832
|
-
return;
|
|
74833
|
-
}
|
|
74834
|
-
const rest = pathname.slice(`${RC_SESSIONS_PATH_PREFIX2}/`.length);
|
|
74835
|
-
const slash = rest.indexOf("/");
|
|
74836
|
-
const id = slash === -1 ? rest : rest.slice(0, slash);
|
|
74837
|
-
const tail = slash === -1 ? "" : rest.slice(slash + 1);
|
|
74838
|
-
if (!id.startsWith(RC_SESSION_ID_PREFIX2)) {
|
|
74839
|
-
await answerJson2(response, HTTP_STATUS.notFound, { type: "error", error: { type: "api_error", message: "the session family names one cse_-prefixed session id" } });
|
|
74840
|
-
return;
|
|
74841
|
-
}
|
|
74842
|
-
await serveSessionPath(sessions.get(id), tail, request7, response);
|
|
74843
|
-
}
|
|
74844
|
-
};
|
|
74845
|
-
const answerServerJson = (response, status, body) => {
|
|
74846
|
-
const text2 = JSON.stringify(body);
|
|
74847
|
-
response.writeHead(status, { "content-type": "application/json", "content-length": String(Buffer.byteLength(text2, "utf8")) });
|
|
74848
|
-
response.end(text2);
|
|
74849
|
-
};
|
|
74850
|
-
const local = {
|
|
74851
|
-
parsesHost: (host) => host === RC_SELF_HOST_OAUTH_HOST,
|
|
74852
|
-
serve: async (host, request7, response) => {
|
|
74853
|
-
const method = (request7.method ?? "").toUpperCase();
|
|
74854
|
-
const pathname = new URL(request7.url ?? "/", "http://127.0.0.1").pathname;
|
|
74855
|
-
if (host === RC_SELF_HOST_OAUTH_HOST) {
|
|
74856
|
-
if (method === "POST" && pathname === "/v1/oauth/token") {
|
|
74857
|
-
const body = parseJsonObject2(await readBody3(request7, CONTROL_BODY_CAP_BYTES));
|
|
74858
|
-
const record2 = deps.credentialRecord();
|
|
74859
|
-
if (record2 === void 0) {
|
|
74860
|
-
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" });
|
|
74861
|
-
return true;
|
|
74862
|
-
}
|
|
74863
|
-
if (body?.grant_type !== void 0 && body.grant_type !== "refresh_token") {
|
|
74864
|
-
answerServerJson(response, HTTP_STATUS.badRequest, { error: "unsupported_grant_type" });
|
|
74865
|
-
return true;
|
|
74866
|
-
}
|
|
74867
|
-
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(" ") });
|
|
74868
|
-
return true;
|
|
74869
|
-
}
|
|
74870
|
-
if (method === "GET" && pathname === "/v1/oauth/hello") {
|
|
74871
|
-
answerServerJson(response, HTTP_STATUS.ok, {});
|
|
74872
|
-
return true;
|
|
74873
|
-
}
|
|
74874
|
-
return false;
|
|
74614
|
+
if (method === "GET" && pathname === "/v1/oauth/hello") {
|
|
74615
|
+
answerServerJson(response, HTTP_STATUS.ok, {});
|
|
74616
|
+
return true;
|
|
74617
|
+
}
|
|
74618
|
+
return false;
|
|
74875
74619
|
}
|
|
74876
74620
|
if (method === "POST" && pathname.startsWith("/api/eval/")) {
|
|
74877
74621
|
await readBody3(request7, CONTROL_BODY_CAP_BYTES);
|
|
@@ -74933,94 +74677,959 @@ data: ${JSON.stringify(envelope)}
|
|
|
74933
74677
|
}
|
|
74934
74678
|
|
|
74935
74679
|
// src/frontdoor/rcSelfHostMint.ts
|
|
74936
|
-
var import_node_path35 = __toESM(require("node:path"), 1);
|
|
74937
74680
|
var CREDENTIALS_FILE = ".credentials.json";
|
|
74938
74681
|
var CLAUDE_JSON2 = ".claude.json";
|
|
74939
74682
|
var RC_SELF_HOST_RECORD_DIR = "rc-selfhost";
|
|
74940
74683
|
var RC_SELF_HOST_RECORD_FILE = "credential.json";
|
|
74941
74684
|
var SELF_HOST_EMAIL = "selfhost@agent-shim.invalid";
|
|
74942
74685
|
var SELF_HOST_SUBSCRIPTION_TYPE = "max";
|
|
74943
|
-
function
|
|
74686
|
+
function isRecord5(value2) {
|
|
74944
74687
|
return typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
|
|
74945
74688
|
}
|
|
74946
|
-
function readJsonObject(fs20, file2) {
|
|
74947
|
-
const raw = fs20.readFileUtf8(file2);
|
|
74948
|
-
if (raw === void 0) {
|
|
74949
|
-
return void 0;
|
|
74950
|
-
}
|
|
74951
|
-
let parsed;
|
|
74952
|
-
try {
|
|
74953
|
-
parsed = JSON.parse(raw);
|
|
74954
|
-
} catch {
|
|
74955
|
-
return void 0;
|
|
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);
|
|
75511
|
+
}
|
|
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;
|
|
75597
|
+
}
|
|
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;
|
|
74956
75604
|
}
|
|
74957
|
-
|
|
74958
|
-
}
|
|
74959
|
-
|
|
74960
|
-
|
|
74961
|
-
if (parsed === void 0) {
|
|
74962
|
-
return void 0;
|
|
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;
|
|
74963
75609
|
}
|
|
74964
|
-
const
|
|
74965
|
-
if (
|
|
74966
|
-
|
|
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;
|
|
74967
75615
|
}
|
|
74968
|
-
|
|
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);
|
|
74969
75623
|
}
|
|
74970
|
-
function
|
|
74971
|
-
|
|
74972
|
-
|
|
74973
|
-
|
|
74974
|
-
|
|
74975
|
-
|
|
74976
|
-
|
|
74977
|
-
const recordDir = import_node_path35.default.join(deps.frontdoorDir, RC_SELF_HOST_RECORD_DIR);
|
|
74978
|
-
const recordFile = import_node_path35.default.join(recordDir, RC_SELF_HOST_RECORD_FILE);
|
|
74979
|
-
const previousRecord = readRcSelfHostRecord(deps.fs, deps.frontdoorDir);
|
|
74980
|
-
const existingCredentials = readJsonObject(deps.fs, credentialsFile);
|
|
74981
|
-
const existingOauth = isRecord6(existingCredentials?.claudeAiOauth) ? existingCredentials.claudeAiOauth : void 0;
|
|
74982
|
-
const existingToken = typeof existingOauth?.accessToken === "string" && existingOauth.accessToken !== "" ? existingOauth.accessToken : void 0;
|
|
74983
|
-
const existingIsOwn = existingToken !== void 0 && previousRecord?.accessToken === existingToken;
|
|
74984
|
-
if (existingToken !== void 0 && !existingIsOwn && !deps.force) {
|
|
74985
|
-
throw new Error(
|
|
74986
|
-
`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.`
|
|
74987
|
-
);
|
|
74988
|
-
}
|
|
74989
|
-
const accessToken = `sk-ant-oat${deps.randomToken()}`;
|
|
74990
|
-
const refreshToken = deps.randomToken();
|
|
74991
|
-
const organizationUuid = deps.newUuid();
|
|
74992
|
-
const accountUuid = deps.newUuid();
|
|
74993
|
-
const mintedAt = new Date(deps.now()).toISOString();
|
|
74994
|
-
deps.fs.mkdirPrivate(import_node_path35.default.dirname(credentialsFile));
|
|
74995
|
-
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)}
|
|
74996
|
-
`);
|
|
74997
|
-
const claudeJson = readJsonObject(deps.fs, claudeJsonFile) ?? {};
|
|
74998
|
-
const existingBlock = (value2) => isRecord6(value2) ? value2 : {};
|
|
74999
|
-
deps.fs.writeFilePrivate(claudeJsonFile, `${JSON.stringify({
|
|
75000
|
-
...claudeJson,
|
|
75001
|
-
oauthAccount: {
|
|
75002
|
-
...existingBlock(claudeJson.oauthAccount),
|
|
75003
|
-
accountUuid,
|
|
75004
|
-
emailAddress: SELF_HOST_EMAIL,
|
|
75005
|
-
organizationUuid,
|
|
75006
|
-
organizationName: "agent-shim self-hosted",
|
|
75007
|
-
// 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.
|
|
75008
|
-
billingType: "stripe_subscription",
|
|
75009
|
-
accountCreatedAt: mintedAt,
|
|
75010
|
-
subscriptionCreatedAt: mintedAt
|
|
75011
|
-
},
|
|
75012
|
-
cachedGrowthBookFeatures: {
|
|
75013
|
-
...existingBlock(claudeJson.cachedGrowthBookFeatures),
|
|
75014
|
-
tengu_ccr_bridge: true,
|
|
75015
|
-
tengu_bridge_repl_v2: true,
|
|
75016
|
-
tengu_bridge_repl_v2_cse_shim_enabled: true
|
|
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 };
|
|
75017
75631
|
}
|
|
75018
|
-
}
|
|
75019
|
-
`);
|
|
75020
|
-
deps.fs.mkdirPrivate(recordDir);
|
|
75021
|
-
deps.fs.writeFilePrivate(recordFile, `${JSON.stringify({ accessToken, refreshToken, organizationUuid, accountUuid }, null, 2)}
|
|
75022
|
-
`);
|
|
75023
|
-
return { identity: deps.identity, organizationUuid, credentialsFile, claudeJsonFile, recordFile, replaced: existingIsOwn };
|
|
75632
|
+
};
|
|
75024
75633
|
}
|
|
75025
75634
|
|
|
75026
75635
|
// src/frontdoor/rcStream.ts
|
|
@@ -75643,7 +76252,6 @@ function appendLog(paths, line) {
|
|
|
75643
76252
|
import_node_fs13.default.appendFileSync(paths.frontdoorLogPath, `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
|
|
75644
76253
|
`);
|
|
75645
76254
|
}
|
|
75646
|
-
var MINTED_TOKEN_RANDOM_BYTES = 32;
|
|
75647
76255
|
function rcObservingResolver(resolve, tracker, hub) {
|
|
75648
76256
|
return async (request7) => {
|
|
75649
76257
|
const resolution = await resolve(request7);
|
|
@@ -75675,8 +76283,8 @@ function realFrontDoorSupervisorPorts(paths) {
|
|
|
75675
76283
|
};
|
|
75676
76284
|
const rcSelfHostSurface = rcSelfHost ? createRcSelfHostSurface({
|
|
75677
76285
|
now: () => Date.now(),
|
|
75678
|
-
newUuid:
|
|
75679
|
-
randomToken: () => (0,
|
|
76286
|
+
newUuid: import_node_crypto15.randomUUID,
|
|
76287
|
+
randomToken: () => (0, import_node_crypto15.randomBytes)(MINTED_TOKEN_RANDOM_BYTES).toString("base64url"),
|
|
75680
76288
|
credentialRecord: () => readRcSelfHostRecord(realFarmFs, paths.frontdoorDir),
|
|
75681
76289
|
webFetch: realRcWebFetch({ allowPrivate: rcWebFetchAllowsPrivateFromEnv(process.env) }),
|
|
75682
76290
|
log
|
|
@@ -75695,7 +76303,7 @@ function realFrontDoorSupervisorPorts(paths) {
|
|
|
75695
76303
|
saveSequenceNum: rcCredentialStore.writeCursor,
|
|
75696
76304
|
dial: rcSelfHost ? lateRcStreamDial(rcSelfHostDialTarget) : realRcStreamDial(),
|
|
75697
76305
|
fanout: rcFanout,
|
|
75698
|
-
newClientId:
|
|
76306
|
+
newClientId: import_node_crypto15.randomUUID,
|
|
75699
76307
|
backoffMs: RC_STREAM_BACKOFF_MS,
|
|
75700
76308
|
sleep: async (ms) => {
|
|
75701
76309
|
await new Promise((resolve) => {
|
|
@@ -75706,7 +76314,7 @@ function realFrontDoorSupervisorPorts(paths) {
|
|
|
75706
76314
|
appendLog(paths, `frontdoor ${String(process.pid)}: ${line}`);
|
|
75707
76315
|
}
|
|
75708
76316
|
});
|
|
75709
|
-
const rcControlToken = (0,
|
|
76317
|
+
const rcControlToken = (0, import_node_crypto15.randomUUID)();
|
|
75710
76318
|
const resolveRoute = rcObservingResolver(
|
|
75711
76319
|
createProviderRouteResolver({
|
|
75712
76320
|
fs: realFsPort,
|
|
@@ -75746,8 +76354,8 @@ function realFrontDoorSupervisorPorts(paths) {
|
|
|
75746
76354
|
now: () => Date.now(),
|
|
75747
76355
|
headroomSocket: () => headroomSocketTarget(realFarmFs, realHeadroomSocketTrust, paths),
|
|
75748
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.
|
|
75749
|
-
hopSecret: (0,
|
|
75750
|
-
custody: createCredentialCustody(() => (0,
|
|
76357
|
+
hopSecret: (0, import_node_crypto15.randomUUID)(),
|
|
76358
|
+
custody: createCredentialCustody(() => (0, import_node_crypto15.randomUUID)()),
|
|
75751
76359
|
log
|
|
75752
76360
|
});
|
|
75753
76361
|
const onListenerError = (name) => (error62) => {
|
|
@@ -75768,12 +76376,22 @@ function realFrontDoorSupervisorPorts(paths) {
|
|
|
75768
76376
|
startProviderListener: async (preferredPort) => {
|
|
75769
76377
|
const authority = loadCa();
|
|
75770
76378
|
const rcDial = rcSelfHost ? lateRcEventDial(rcSelfHostDialTarget) : realRcEventDial();
|
|
75771
|
-
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);
|
|
75772
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);
|
|
75773
|
-
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 };
|
|
75774
76382
|
const rcInterrupt = async (sessionId) => await interruptRcSession(rcControlRequestDeps, sessionId);
|
|
75775
76383
|
const rcSetModel = async (sessionId, model) => await setRcSessionModel(rcControlRequestDeps, sessionId, model);
|
|
75776
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);
|
|
75777
76395
|
const controlDeps = {
|
|
75778
76396
|
usageSnapshots: () => listUsageSnapshots(realFarmFs, paths.usageSnapshotsDir),
|
|
75779
76397
|
usageSnapshotOf: (identity) => readUsageSnapshot(realFarmFs, paths.usageSnapshotsDir, identity),
|
|
@@ -75791,7 +76409,7 @@ function realFrontDoorSupervisorPorts(paths) {
|
|
|
75791
76409
|
},
|
|
75792
76410
|
log,
|
|
75793
76411
|
mintLeaf(authority, LOOPBACK_LEAF_NAMES, /* @__PURE__ */ new Date()),
|
|
75794
|
-
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 }),
|
|
75795
76413
|
[
|
|
75796
76414
|
createDoorApiNodeHandler({
|
|
75797
76415
|
expectedToken: rcControlToken,
|
|
@@ -75803,6 +76421,16 @@ function realFrontDoorSupervisorPorts(paths) {
|
|
|
75803
76421
|
interrupt: rcInterrupt,
|
|
75804
76422
|
setModel: rcSetModel,
|
|
75805
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,
|
|
75806
76434
|
fanout: rcFanout,
|
|
75807
76435
|
...controlDeps,
|
|
75808
76436
|
events: doorEvents
|
|
@@ -75822,7 +76450,7 @@ function realFrontDoorSupervisorPorts(paths) {
|
|
|
75822
76450
|
},
|
|
75823
76451
|
startConnectListener: async (preferredPort) => {
|
|
75824
76452
|
const wantedTransparentPort = process.env.AGENT_SHIM_TRANSPARENT_SURFACE === void 0 ? void 0 : Number(process.env.AGENT_SHIM_TRANSPARENT_SURFACE);
|
|
75825
|
-
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)();
|
|
75826
76454
|
if (transparentCapability !== void 0) {
|
|
75827
76455
|
writeFrontDoorSession(realFarmFs, paths.frontdoorSessionsDir, { pid: process.pid, startedAt: Date.now(), token: transparentCapability });
|
|
75828
76456
|
}
|
|
@@ -75866,123 +76494,41 @@ function realFrontDoorSupervisorPorts(paths) {
|
|
|
75866
76494
|
upstreamFor: (host) => ({ host, port: HTTPS_PORT, tls: true }),
|
|
75867
76495
|
isLiveCapability: isLiveToken,
|
|
75868
76496
|
...wantedTransparentPort === void 0 || Number.isNaN(wantedTransparentPort) ? {} : { transparentPort: wantedTransparentPort, ...transparentCapability === void 0 ? {} : { transparentCapability } },
|
|
75869
|
-
limits: CONNECT_LIMITS,
|
|
75870
|
-
...capture === void 0 ? {} : { capture }
|
|
75871
|
-
},
|
|
75872
|
-
realConnectEffects(),
|
|
75873
|
-
preferredPort
|
|
75874
|
-
);
|
|
75875
|
-
transparentPort = server.transparentPort;
|
|
75876
|
-
return {
|
|
75877
|
-
port: server.port,
|
|
75878
|
-
close: async () => {
|
|
75879
|
-
rcSelfHostSurface?.close();
|
|
75880
|
-
await server.close();
|
|
75881
|
-
}
|
|
75882
|
-
};
|
|
75883
|
-
},
|
|
75884
|
-
startDirectListener: async (preferredPort) => {
|
|
75885
|
-
const server = createFrontDoorServer(async (request7) => {
|
|
75886
|
-
await serveRouted(request7, pipelines.direct);
|
|
75887
|
-
}, log);
|
|
75888
|
-
const handle = await listenFrontDoor(server, {
|
|
75889
|
-
...preferredPort === void 0 ? {} : { preferredPort },
|
|
75890
|
-
onBound: (port) => {
|
|
75891
|
-
directPort = port;
|
|
75892
|
-
},
|
|
75893
|
-
onError: onListenerError("direct")
|
|
75894
|
-
});
|
|
75895
|
-
return { port: handle.port, close: handle.close };
|
|
75896
|
-
},
|
|
75897
|
-
closeRcStreamHub: rcHub.close,
|
|
75898
|
-
// 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.
|
|
75899
|
-
observeLaunchRegistry: createLaunchEventPublisher(doorEvents, () => Date.now()).observe,
|
|
75900
|
-
log: (line) => {
|
|
75901
|
-
appendLog(paths, line);
|
|
75902
|
-
}
|
|
75903
|
-
};
|
|
75904
|
-
}
|
|
75905
|
-
function formatRcSessionList(sessions) {
|
|
75906
|
-
if (sessions.length === 0) {
|
|
75907
|
-
return ["no Remote Control sessions observed"];
|
|
75908
|
-
}
|
|
75909
|
-
return sessions.map((session) => `${session.id} created ${new Date(session.createdAt).toISOString()} last seen ${new Date(session.lastSeenAt).toISOString()}`);
|
|
75910
|
-
}
|
|
75911
|
-
function formatRcSessionStatus(statuses) {
|
|
75912
|
-
if (statuses.length === 0) {
|
|
75913
|
-
return ["no Remote Control sessions observed"];
|
|
75914
|
-
}
|
|
75915
|
-
const lines = [];
|
|
75916
|
-
for (const status of statuses) {
|
|
75917
|
-
lines.push(`${status.id} created ${new Date(status.createdAt).toISOString()} last seen ${new Date(status.lastSeenAt).toISOString()}`);
|
|
75918
|
-
const worker = status.workerState === void 0 && status.workerIdleSeconds === void 0 ? "not observed" : [
|
|
75919
|
-
status.workerState === void 0 ? "state unknown" : `state ${status.workerState.value} since ${new Date(status.workerState.observedAt).toISOString()}`,
|
|
75920
|
-
status.workerIdleSeconds === void 0 ? "idle unknown" : `idle ${String(status.workerIdleSeconds.value)} s as of ${new Date(status.workerIdleSeconds.observedAt).toISOString()}`
|
|
75921
|
-
].join(", ");
|
|
75922
|
-
lines.push(` worker: ${worker}`);
|
|
75923
|
-
if (status.pending.length === 0) {
|
|
75924
|
-
lines.push(" pending: none");
|
|
75925
|
-
} else {
|
|
75926
|
-
lines.push(" pending:");
|
|
75927
|
-
for (const pending of status.pending) {
|
|
75928
|
-
lines.push(...formatRcPendingList([pending]).map((line) => ` ${line}`));
|
|
75929
|
-
}
|
|
75930
|
-
}
|
|
75931
|
-
}
|
|
75932
|
-
return lines;
|
|
75933
|
-
}
|
|
75934
|
-
function formatRcPendingList(pending) {
|
|
75935
|
-
if (pending.length === 0) {
|
|
75936
|
-
return ["no pending control requests observed"];
|
|
75937
|
-
}
|
|
75938
|
-
return pending.map((request7) => `${request7.sessionId} ${request7.requestId} ${request7.type}${request7.summary === "" ? "" : ` ${request7.summary}`} observed ${new Date(request7.observedAt).toISOString()}`);
|
|
75939
|
-
}
|
|
75940
|
-
function eventExcerpt(text2) {
|
|
75941
|
-
return text2.length <= RC_PENDING_SUMMARY_EXCERPT_CHARS ? text2 : `${text2.slice(0, RC_PENDING_SUMMARY_EXCERPT_CHARS)}...`;
|
|
75942
|
-
}
|
|
75943
|
-
function formatRcStreamEvent(event) {
|
|
75944
|
-
const { envelope } = event;
|
|
75945
|
-
const sketch = envelope.payload === void 0 ? "" : ` ${eventExcerpt(JSON.stringify(envelope.payload))}`;
|
|
75946
|
-
return `${event.session} ${envelope.event_type} sequence_num ${String(envelope.sequence_num)} source ${envelope.source}${sketch}`;
|
|
75947
|
-
}
|
|
75948
|
-
function formatRcSelfHostMint(result) {
|
|
75949
|
-
return [
|
|
75950
|
-
`identity: ${result.identity} (organisation ${result.organizationUuid})`,
|
|
75951
|
-
`credential: ${result.credentialsFile}${result.replaced ? " (replacing this door's previous mint)" : ""}`,
|
|
75952
|
-
`account block: ${result.claudeJsonFile} (oauthAccount and the feature-cache seed merged in)`,
|
|
75953
|
-
`door record: ${result.recordFile} (the copy the door's served surface authenticates against)`,
|
|
75954
|
-
"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"
|
|
75955
|
-
];
|
|
75956
|
-
}
|
|
75957
|
-
function frontDoorRcControlFromState(fsPort, paths) {
|
|
75958
|
-
const state = readFrontDoorState(fsPort, paths.frontdoorStateFile);
|
|
75959
|
-
if (state?.port === void 0) {
|
|
75960
|
-
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");
|
|
75961
|
-
}
|
|
75962
|
-
const ca = fsPort.readFileUtf8(paths.frontdoorCaCertFile);
|
|
75963
|
-
if (ca === void 0) {
|
|
75964
|
-
throw new Error(`the front door's CA certificate is missing at ${paths.frontdoorCaCertFile}, so its control listener cannot be authenticated`);
|
|
75965
|
-
}
|
|
75966
|
-
const token = fsPort.readFileUtf8(paths.frontdoorControlTokenFile)?.trim();
|
|
75967
|
-
if (token === void 0 || token === "") {
|
|
75968
|
-
throw new Error(`the serving front door's control token is missing at ${paths.frontdoorControlTokenFile}`);
|
|
75969
|
-
}
|
|
75970
|
-
return frontDoorRcControl(realRcControlTransport(state.port, ca), token);
|
|
75971
|
-
}
|
|
75972
|
-
function frontDoorRcApiFromState(fsPort, paths) {
|
|
75973
|
-
const state = readFrontDoorState(fsPort, paths.frontdoorStateFile);
|
|
75974
|
-
if (state?.port === void 0) {
|
|
75975
|
-
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");
|
|
75976
|
-
}
|
|
75977
|
-
const ca = fsPort.readFileUtf8(paths.frontdoorCaCertFile);
|
|
75978
|
-
if (ca === void 0) {
|
|
75979
|
-
throw new Error(`the front door's CA certificate is missing at ${paths.frontdoorCaCertFile}, so its typed API cannot be authenticated`);
|
|
75980
|
-
}
|
|
75981
|
-
const token = fsPort.readFileUtf8(paths.frontdoorControlTokenFile)?.trim();
|
|
75982
|
-
if (token === void 0 || token === "") {
|
|
75983
|
-
throw new Error(`the serving front door's control token is missing at ${paths.frontdoorControlTokenFile}`);
|
|
75984
|
-
}
|
|
75985
|
-
return frontDoorRcApiClient(state.port, ca, token);
|
|
76497
|
+
limits: CONNECT_LIMITS,
|
|
76498
|
+
...capture === void 0 ? {} : { capture }
|
|
76499
|
+
},
|
|
76500
|
+
realConnectEffects(),
|
|
76501
|
+
preferredPort
|
|
76502
|
+
);
|
|
76503
|
+
transparentPort = server.transparentPort;
|
|
76504
|
+
return {
|
|
76505
|
+
port: server.port,
|
|
76506
|
+
close: async () => {
|
|
76507
|
+
rcSelfHostSurface?.close();
|
|
76508
|
+
await server.close();
|
|
76509
|
+
}
|
|
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);
|
|
76530
|
+
}
|
|
76531
|
+
};
|
|
75986
76532
|
}
|
|
75987
76533
|
function registerFrontDoorCommand(program2, deps) {
|
|
75988
76534
|
const { paths } = deps;
|
|
@@ -76002,186 +76548,7 @@ function registerFrontDoorCommand(program2, deps) {
|
|
|
76002
76548
|
}),
|
|
76003
76549
|
["agent-shim frontdoor status", "agent-shim frontdoor status --json"]
|
|
76004
76550
|
);
|
|
76005
|
-
|
|
76006
|
-
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)."),
|
|
76007
|
-
["agent-shim frontdoor rc list", "agent-shim frontdoor rc pending"]
|
|
76008
|
-
);
|
|
76009
|
-
withExamples(
|
|
76010
|
-
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) => {
|
|
76011
|
-
const sessions = await frontDoorRcControlFromState(realFarmFs, paths).listSessions();
|
|
76012
|
-
if (options.json === true) {
|
|
76013
|
-
printJson({ sessions });
|
|
76014
|
-
return;
|
|
76015
|
-
}
|
|
76016
|
-
for (const line of formatRcSessionList(sessions)) {
|
|
76017
|
-
console.log(line);
|
|
76018
|
-
}
|
|
76019
|
-
}),
|
|
76020
|
-
["agent-shim frontdoor rc list", "agent-shim frontdoor rc list --json"]
|
|
76021
|
-
);
|
|
76022
|
-
withExamples(
|
|
76023
|
-
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) => {
|
|
76024
|
-
const statuses = await frontDoorRcControlFromState(realFarmFs, paths).statusOf(session);
|
|
76025
|
-
if (options.json === true) {
|
|
76026
|
-
printJson({ statuses });
|
|
76027
|
-
return;
|
|
76028
|
-
}
|
|
76029
|
-
for (const line of formatRcSessionStatus(statuses)) {
|
|
76030
|
-
console.log(line);
|
|
76031
|
-
}
|
|
76032
|
-
}),
|
|
76033
|
-
["agent-shim frontdoor rc status", "agent-shim frontdoor rc status cse_00000000-0000-4000-8000-000000000000 --json"]
|
|
76034
|
-
);
|
|
76035
|
-
withExamples(
|
|
76036
|
-
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) => {
|
|
76037
|
-
const pending = await frontDoorRcControlFromState(realFarmFs, paths).pendingOf(session);
|
|
76038
|
-
if (options.json === true) {
|
|
76039
|
-
printJson({ pending });
|
|
76040
|
-
return;
|
|
76041
|
-
}
|
|
76042
|
-
for (const line of formatRcPendingList(pending)) {
|
|
76043
|
-
console.log(line);
|
|
76044
|
-
}
|
|
76045
|
-
}),
|
|
76046
|
-
["agent-shim frontdoor rc pending", "agent-shim frontdoor rc pending --json"]
|
|
76047
|
-
);
|
|
76048
|
-
withExamples(
|
|
76049
|
-
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) => {
|
|
76050
|
-
const result = await frontDoorRcControlFromState(realFarmFs, paths).sendPrompt(options.session, options.text);
|
|
76051
|
-
if (!result.ok) {
|
|
76052
|
-
throw new Error(result.message);
|
|
76053
|
-
}
|
|
76054
|
-
if (options.json === true) {
|
|
76055
|
-
printJson({ session: options.session, sequenceNums: result.sequenceNums });
|
|
76056
|
-
return;
|
|
76057
|
-
}
|
|
76058
|
-
console.log(`sent to ${options.session}: sequence_num ${result.sequenceNums.join(", ")}`);
|
|
76059
|
-
}),
|
|
76060
|
-
['agent-shim frontdoor rc send --session cse_00000000-0000-4000-8000-000000000000 --text "run the tests"']
|
|
76061
|
-
);
|
|
76062
|
-
withExamples(
|
|
76063
|
-
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) => {
|
|
76064
|
-
if (options.approve === options.deny) {
|
|
76065
|
-
throw new UsageError("answer exactly one way: pass --approve or --deny, not both and not neither");
|
|
76066
|
-
}
|
|
76067
|
-
if (options.approve === true && options.text !== void 0) {
|
|
76068
|
-
throw new UsageError("--text is the denial message; an approval carries no text (the protocol's allow result has no message field)");
|
|
76069
|
-
}
|
|
76070
|
-
const result = await frontDoorRcControlFromState(realFarmFs, paths).answerRequest(options.session, options.request, { approve: options.approve === true, message: options.deny === true ? options.text : void 0 });
|
|
76071
|
-
if (!result.ok) {
|
|
76072
|
-
throw new Error(result.message);
|
|
76073
|
-
}
|
|
76074
|
-
if (options.json === true) {
|
|
76075
|
-
printJson({ session: options.session, request: options.request, sequenceNums: result.sequenceNums });
|
|
76076
|
-
return;
|
|
76077
|
-
}
|
|
76078
|
-
console.log(`${options.approve === true ? "approved" : "denied"} ${options.request} on ${options.session}: sequence_num ${result.sequenceNums.join(", ")}`);
|
|
76079
|
-
}),
|
|
76080
|
-
[
|
|
76081
|
-
"agent-shim frontdoor rc answer --session cse_00000000-0000-4000-8000-000000000000 --request req_00000000-0000-4000-8000-000000000000 --approve",
|
|
76082
|
-
'agent-shim frontdoor rc answer --session cse_00000000-0000-4000-8000-000000000000 --request req_00000000-0000-4000-8000-000000000000 --deny --text "not today"'
|
|
76083
|
-
]
|
|
76084
|
-
);
|
|
76085
|
-
withExamples(
|
|
76086
|
-
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) => {
|
|
76087
|
-
const result = await frontDoorRcControlFromState(realFarmFs, paths).interruptSession(options.session);
|
|
76088
|
-
if (!result.ok) {
|
|
76089
|
-
throw new Error(result.message);
|
|
76090
|
-
}
|
|
76091
|
-
if (options.json === true) {
|
|
76092
|
-
printJson({ session: options.session, sequenceNums: result.sequenceNums });
|
|
76093
|
-
return;
|
|
76094
|
-
}
|
|
76095
|
-
console.log(`interrupted ${options.session}: sequence_num ${result.sequenceNums.join(", ")}`);
|
|
76096
|
-
}),
|
|
76097
|
-
["agent-shim frontdoor rc interrupt --session cse_00000000-0000-4000-8000-000000000000"]
|
|
76098
|
-
);
|
|
76099
|
-
withExamples(
|
|
76100
|
-
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) => {
|
|
76101
|
-
const result = await frontDoorRcControlFromState(realFarmFs, paths).setModel(options.session, options.model);
|
|
76102
|
-
if (!result.ok) {
|
|
76103
|
-
throw new Error(result.message);
|
|
76104
|
-
}
|
|
76105
|
-
if (options.json === true) {
|
|
76106
|
-
printJson({ session: options.session, model: options.model, sequenceNums: result.sequenceNums });
|
|
76107
|
-
return;
|
|
76108
|
-
}
|
|
76109
|
-
console.log(`set ${options.session} to model ${options.model}: sequence_num ${result.sequenceNums.join(", ")}`);
|
|
76110
|
-
}),
|
|
76111
|
-
["agent-shim frontdoor rc set-model --session cse_00000000-0000-4000-8000-000000000000 --model claude-opus-5-5"]
|
|
76112
|
-
);
|
|
76113
|
-
withExamples(
|
|
76114
|
-
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) => {
|
|
76115
|
-
if (!isRcPermissionMode(options.mode)) {
|
|
76116
|
-
throw new UsageError(`--mode must be one of the SDK's own permission modes (${RC_PERMISSION_MODES.join(", ")}), not "${options.mode}"`);
|
|
76117
|
-
}
|
|
76118
|
-
const result = await frontDoorRcControlFromState(realFarmFs, paths).setPermissionMode(options.session, options.mode);
|
|
76119
|
-
if (!result.ok) {
|
|
76120
|
-
throw new Error(result.message);
|
|
76121
|
-
}
|
|
76122
|
-
if (options.json === true) {
|
|
76123
|
-
printJson({ session: options.session, mode: options.mode, sequenceNums: result.sequenceNums });
|
|
76124
|
-
return;
|
|
76125
|
-
}
|
|
76126
|
-
console.log(`set ${options.session} to permission mode ${options.mode}: sequence_num ${result.sequenceNums.join(", ")}`);
|
|
76127
|
-
}),
|
|
76128
|
-
[
|
|
76129
|
-
"agent-shim frontdoor rc set-permission-mode --session cse_00000000-0000-4000-8000-000000000000 --mode plan",
|
|
76130
|
-
"agent-shim frontdoor rc set-permission-mode --session cse_00000000-0000-4000-8000-000000000000 --mode acceptEdits"
|
|
76131
|
-
]
|
|
76132
|
-
);
|
|
76133
|
-
withExamples(
|
|
76134
|
-
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) => {
|
|
76135
|
-
const client = frontDoorRcApiFromState(realFarmFs, paths);
|
|
76136
|
-
const leave = new AbortController();
|
|
76137
|
-
const onInterrupt = () => {
|
|
76138
|
-
leave.abort();
|
|
76139
|
-
};
|
|
76140
|
-
process.once("SIGINT", onInterrupt);
|
|
76141
|
-
try {
|
|
76142
|
-
for await (const event of await client.rc.subscribe(session === void 0 ? {} : { session }, { signal: leave.signal })) {
|
|
76143
|
-
if (options.json === true) {
|
|
76144
|
-
console.log(JSON.stringify(event));
|
|
76145
|
-
continue;
|
|
76146
|
-
}
|
|
76147
|
-
console.log(formatRcStreamEvent(event));
|
|
76148
|
-
}
|
|
76149
|
-
} catch (error62) {
|
|
76150
|
-
if (!leave.signal.aborted) {
|
|
76151
|
-
throw error62;
|
|
76152
|
-
}
|
|
76153
|
-
} finally {
|
|
76154
|
-
process.off("SIGINT", onInterrupt);
|
|
76155
|
-
}
|
|
76156
|
-
}),
|
|
76157
|
-
["agent-shim frontdoor rc watch", "agent-shim frontdoor rc watch cse_00000000-0000-4000-8000-000000000000 --json"]
|
|
76158
|
-
);
|
|
76159
|
-
const selfhost = withExamples(
|
|
76160
|
-
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."),
|
|
76161
|
-
["agent-shim frontdoor rc selfhost mint rig"]
|
|
76162
|
-
);
|
|
76163
|
-
withExamples(
|
|
76164
|
-
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) => {
|
|
76165
|
-
const result = mintRcSelfHostCredential({
|
|
76166
|
-
fs: realFarmFs,
|
|
76167
|
-
identitiesDir: paths.identitiesDir,
|
|
76168
|
-
frontdoorDir: paths.frontdoorDir,
|
|
76169
|
-
identity,
|
|
76170
|
-
newUuid: import_node_crypto14.randomUUID,
|
|
76171
|
-
randomToken: () => (0, import_node_crypto14.randomBytes)(MINTED_TOKEN_RANDOM_BYTES).toString("base64url"),
|
|
76172
|
-
force: options.force === true,
|
|
76173
|
-
now: () => Date.now()
|
|
76174
|
-
});
|
|
76175
|
-
if (options.json === true) {
|
|
76176
|
-
printJson(result);
|
|
76177
|
-
return;
|
|
76178
|
-
}
|
|
76179
|
-
for (const line of formatRcSelfHostMint(result)) {
|
|
76180
|
-
console.log(line);
|
|
76181
|
-
}
|
|
76182
|
-
}),
|
|
76183
|
-
["agent-shim frontdoor rc selfhost mint rig", "agent-shim frontdoor rc selfhost mint rig --force"]
|
|
76184
|
-
);
|
|
76551
|
+
registerRcCommand(frontdoor, paths);
|
|
76185
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 () => {
|
|
76186
76553
|
const idleShutdownMinutes = readGlobalConfig(paths)?.frontdoor?.idleShutdownMinutes ?? FRONTDOOR_DEFAULT_IDLE_SHUTDOWN_MINUTES;
|
|
76187
76554
|
process.on("SIGTERM", () => {
|
|
@@ -76673,7 +77040,7 @@ var import_node_child_process6 = require("node:child_process");
|
|
|
76673
77040
|
var import_node_fs15 = __toESM(require("node:fs"), 1);
|
|
76674
77041
|
|
|
76675
77042
|
// src/frontdoor/ensure.ts
|
|
76676
|
-
var
|
|
77043
|
+
var import_node_crypto16 = require("node:crypto");
|
|
76677
77044
|
var FRONTDOOR_START_TIMEOUT_MS = 3e4;
|
|
76678
77045
|
var FRONTDOOR_LAUNCHER_POLL_MS = 100;
|
|
76679
77046
|
var FrontDoorStartError = class extends CliError {
|
|
@@ -76707,7 +77074,7 @@ function ensureFrontDoor(params) {
|
|
|
76707
77074
|
continue;
|
|
76708
77075
|
}
|
|
76709
77076
|
if (verdict.ok) {
|
|
76710
|
-
const token = (0,
|
|
77077
|
+
const token = (0, import_node_crypto16.randomUUID)();
|
|
76711
77078
|
writeFrontDoorSession(ports.fs, paths.frontdoorSessionsDir, { pid: params.launcherPid, startedAt: ports.now(), token });
|
|
76712
77079
|
return { port: state.port, connectPort: state.connectPort, token };
|
|
76713
77080
|
}
|
|
@@ -76811,7 +77178,7 @@ function probeFrontDoorSync(port, caPem) {
|
|
|
76811
77178
|
}
|
|
76812
77179
|
|
|
76813
77180
|
// src/frontdoor/trust.ts
|
|
76814
|
-
var
|
|
77181
|
+
var import_node_crypto17 = require("node:crypto");
|
|
76815
77182
|
var import_node_path38 = __toESM(require("node:path"), 1);
|
|
76816
77183
|
var BUNDLE_NAME_HEX_DIGITS = 16;
|
|
76817
77184
|
function resolveTrustBundle(params) {
|
|
@@ -76834,7 +77201,7 @@ function resolveTrustBundle(params) {
|
|
|
76834
77201
|
}
|
|
76835
77202
|
const combined = `${inheritedPem.trimEnd()}
|
|
76836
77203
|
${caPem}`;
|
|
76837
|
-
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`);
|
|
76838
77205
|
if (!fs20.exists(bundle)) {
|
|
76839
77206
|
fs20.mkdirp(bundlesDir);
|
|
76840
77207
|
fs20.writeFileAtomic(bundle, combined);
|
|
@@ -77539,12 +77906,13 @@ function registerRunCommand(program2, handler) {
|
|
|
77539
77906
|
}
|
|
77540
77907
|
|
|
77541
77908
|
// src/update/commands.ts
|
|
77909
|
+
var import_node_child_process7 = require("node:child_process");
|
|
77542
77910
|
var import_node_fs17 = __toESM(require("node:fs"), 1);
|
|
77543
77911
|
var import_node_os7 = __toESM(require("node:os"), 1);
|
|
77544
77912
|
var import_node_path41 = __toESM(require("node:path"), 1);
|
|
77545
77913
|
|
|
77546
77914
|
// src/update/update.ts
|
|
77547
|
-
var
|
|
77915
|
+
var import_node_crypto18 = require("node:crypto");
|
|
77548
77916
|
var import_node_path40 = __toESM(require("node:path"), 1);
|
|
77549
77917
|
var LATEST_RELEASE_URL = "https://github.com/ExaDev/agent-shim/releases/latest";
|
|
77550
77918
|
var DOWNLOAD_BASE_URL = "https://github.com/ExaDev/agent-shim/releases/latest/download";
|
|
@@ -77699,11 +78067,11 @@ async function downloadAndInstall(ports, asset, target) {
|
|
|
77699
78067
|
throw new UpdateDownloadError(`the downloaded files for ${asset} are missing from ${tempDir}`);
|
|
77700
78068
|
}
|
|
77701
78069
|
const expected = expectedSha256(sidecar);
|
|
77702
|
-
const actual = (0,
|
|
78070
|
+
const actual = (0, import_node_crypto18.createHash)("sha256").update(downloaded).digest("hex");
|
|
77703
78071
|
if (expected === void 0 || expected !== actual) {
|
|
77704
78072
|
throw new UpdateDownloadError(`checksum mismatch for ${asset} (expected ${expected ?? "(none)"}, got ${actual})`);
|
|
77705
78073
|
}
|
|
77706
|
-
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`);
|
|
77707
78075
|
ports.fs.writeFileBytes(tempBinary, downloaded);
|
|
77708
78076
|
try {
|
|
77709
78077
|
ports.fs.chmod(tempBinary, EXECUTABLE_MODE);
|
|
@@ -77874,11 +78242,41 @@ function formatUpdateReport(report) {
|
|
|
77874
78242
|
return report.action;
|
|
77875
78243
|
}
|
|
77876
78244
|
}
|
|
78245
|
+
function setUpdateMode(paths, mode) {
|
|
78246
|
+
const existing = readGlobalConfig(paths);
|
|
78247
|
+
applyPatch(paths.globalConfigFile, GlobalConfigSchema, { update: { ...existing?.update, mode } }, { defaults: {} });
|
|
78248
|
+
}
|
|
78249
|
+
function realLaunchUpdatePort(paths) {
|
|
78250
|
+
return {
|
|
78251
|
+
effectiveUrl: realUpdatePorts.http.effectiveUrl,
|
|
78252
|
+
readStamp: (filePath) => realUpdatePorts.fs.readFileUtf8(filePath),
|
|
78253
|
+
writeStamp: (filePath, contents) => {
|
|
78254
|
+
writeTextAtomic(filePath, contents);
|
|
78255
|
+
},
|
|
78256
|
+
now: () => Date.now(),
|
|
78257
|
+
spawnDetached: (args) => {
|
|
78258
|
+
const invocation = selfInvocation(args);
|
|
78259
|
+
const child = (0, import_node_child_process7.spawn)(invocation.command, invocation.args, { detached: true, stdio: "ignore", env: { ...process.env, AGENT_SHIM_HOME: paths.root } });
|
|
78260
|
+
child.unref();
|
|
78261
|
+
},
|
|
78262
|
+
writeErr: (line) => {
|
|
78263
|
+
console.error(line);
|
|
78264
|
+
}
|
|
78265
|
+
};
|
|
78266
|
+
}
|
|
77877
78267
|
function registerUpdateCommand(program2, deps, ports = realUpdatePorts) {
|
|
77878
78268
|
withExamples(
|
|
77879
78269
|
program2.command("update").description(
|
|
77880
|
-
"Download the newest release binary and install it over the running one. An installation that belongs to a package manager (Homebrew, npm, Scoop) names its channel and its upgrade command instead of updating in place."
|
|
77881
|
-
).option("--check", "Only report whether a newer release exists; download and change nothing.").option("--json", "Print the result as JSON.").action(async (options) => {
|
|
78270
|
+
"Download the newest release binary and install it over the running one. An installation that belongs to a package manager (Homebrew, npm, Scoop) names its channel and its upgrade command instead of updating in place. With --mode, set the launch-time update mode in the global config instead of updating now."
|
|
78271
|
+
).option("--check", "Only report whether a newer release exists; download and change nothing.").addOption(new Option("--mode <mode>", "Set what a launch does about a newer release (off: nothing; notify: print one line; auto: also apply it in the background) in the global config, then exit.").choices(UPDATE_MODES)).option("--json", "Print the result as JSON.").action(async (options) => {
|
|
78272
|
+
const mode = options.mode;
|
|
78273
|
+
if (mode !== void 0) {
|
|
78274
|
+
setUpdateMode(deps.paths, mode);
|
|
78275
|
+
reportMutation(options.json, { action: "updated", kind: "update", name: "mode", value: { mode } }, () => {
|
|
78276
|
+
console.log(`update mode set to ${mode}`);
|
|
78277
|
+
});
|
|
78278
|
+
return;
|
|
78279
|
+
}
|
|
77882
78280
|
const report = await runSelfUpdate(ports, {
|
|
77883
78281
|
currentVersion: package_default.version,
|
|
77884
78282
|
platform: process.platform,
|
|
@@ -77894,7 +78292,7 @@ function registerUpdateCommand(program2, deps, ports = realUpdatePorts) {
|
|
|
77894
78292
|
}
|
|
77895
78293
|
console.log(formatUpdateReport(report));
|
|
77896
78294
|
}),
|
|
77897
|
-
["agent-shim update", "agent-shim update --check", "agent-shim update --json"]
|
|
78295
|
+
["agent-shim update", "agent-shim update --check", "agent-shim update --mode auto", "agent-shim update --json"]
|
|
77898
78296
|
);
|
|
77899
78297
|
}
|
|
77900
78298
|
|
|
@@ -78182,10 +78580,10 @@ function buildProgram(deps) {
|
|
|
78182
78580
|
|
|
78183
78581
|
// src/launchWiring.ts
|
|
78184
78582
|
var import_node_os9 = __toESM(require("node:os"), 1);
|
|
78185
|
-
var
|
|
78583
|
+
var import_node_crypto20 = require("node:crypto");
|
|
78186
78584
|
|
|
78187
78585
|
// src/launcher.ts
|
|
78188
|
-
var
|
|
78586
|
+
var import_node_crypto19 = require("node:crypto");
|
|
78189
78587
|
var import_node_path43 = __toESM(require("node:path"), 1);
|
|
78190
78588
|
|
|
78191
78589
|
// src/launcher/cliOverride.ts
|
|
@@ -78479,6 +78877,68 @@ function spawnClaude(params) {
|
|
|
78479
78877
|
return params.proc.exit(exitCodeFor(result));
|
|
78480
78878
|
}
|
|
78481
78879
|
|
|
78880
|
+
// src/update/launchHook.ts
|
|
78881
|
+
var UPDATE_CHECK_COOLDOWN_MS = 864e5;
|
|
78882
|
+
function resolveUpdateMode(globalConfig2) {
|
|
78883
|
+
return globalConfig2?.update?.mode ?? "off";
|
|
78884
|
+
}
|
|
78885
|
+
function updateCheckDue(contents, nowMs, cooldownMs = UPDATE_CHECK_COOLDOWN_MS) {
|
|
78886
|
+
if (contents === void 0) {
|
|
78887
|
+
return true;
|
|
78888
|
+
}
|
|
78889
|
+
const checkedAt = Date.parse(contents.trim());
|
|
78890
|
+
if (Number.isNaN(checkedAt)) {
|
|
78891
|
+
return true;
|
|
78892
|
+
}
|
|
78893
|
+
const age = nowMs - checkedAt;
|
|
78894
|
+
return age < 0 || age >= cooldownMs;
|
|
78895
|
+
}
|
|
78896
|
+
var NOOP_HANDLE = { markChildStarted: () => void 0 };
|
|
78897
|
+
function updateNotifyLine(currentVersion, latest) {
|
|
78898
|
+
return `agent-shim: update available: ${currentVersion} -> ${latest} (run \`agent-shim update\` to install it)`;
|
|
78899
|
+
}
|
|
78900
|
+
function runLaunchUpdateCheck(params) {
|
|
78901
|
+
const { mode, checkPath, currentVersion, port } = params;
|
|
78902
|
+
if (mode === "off" || port === void 0) {
|
|
78903
|
+
return NOOP_HANDLE;
|
|
78904
|
+
}
|
|
78905
|
+
let childStarted = false;
|
|
78906
|
+
try {
|
|
78907
|
+
if (!updateCheckDue(port.readStamp(checkPath), port.now())) {
|
|
78908
|
+
return NOOP_HANDLE;
|
|
78909
|
+
}
|
|
78910
|
+
port.writeStamp(checkPath, new Date(port.now()).toISOString());
|
|
78911
|
+
} catch {
|
|
78912
|
+
return NOOP_HANDLE;
|
|
78913
|
+
}
|
|
78914
|
+
const answer = (url2) => {
|
|
78915
|
+
try {
|
|
78916
|
+
const latest = releaseTagFromUrl(url2);
|
|
78917
|
+
if (latest === void 0 || compareVersions3(latest, currentVersion) <= 0) {
|
|
78918
|
+
return;
|
|
78919
|
+
}
|
|
78920
|
+
if (mode === "auto") {
|
|
78921
|
+
port.spawnDetached(["update"]);
|
|
78922
|
+
}
|
|
78923
|
+
if (!childStarted) {
|
|
78924
|
+
port.writeErr(updateNotifyLine(currentVersion, latest));
|
|
78925
|
+
}
|
|
78926
|
+
} catch {
|
|
78927
|
+
}
|
|
78928
|
+
};
|
|
78929
|
+
port.effectiveUrl(LATEST_RELEASE_URL).then(
|
|
78930
|
+
(url2) => {
|
|
78931
|
+
answer(url2);
|
|
78932
|
+
},
|
|
78933
|
+
() => void 0
|
|
78934
|
+
);
|
|
78935
|
+
return {
|
|
78936
|
+
markChildStarted: () => {
|
|
78937
|
+
childStarted = true;
|
|
78938
|
+
}
|
|
78939
|
+
};
|
|
78940
|
+
}
|
|
78941
|
+
|
|
78482
78942
|
// src/launcher.ts
|
|
78483
78943
|
function resolveIdentityCredential(identity, credential, env, port) {
|
|
78484
78944
|
if (port === void 0) {
|
|
@@ -78644,6 +79104,12 @@ function prepareLaunch(params) {
|
|
|
78644
79104
|
log.info(
|
|
78645
79105
|
`agent-shim: identity ${identityDecision.name ?? "(none)"} (${identityDecision.source}${poolExplanation === void 0 ? "" : `, ${poolExplanation}`}), config profile ${configProfileDecision.name ?? "(none)"} (${configProfileDecision.source})` + (resolvedProvider === void 0 ? "" : `, provider ${resolvedProvider.name} (credential ${resolvedProvider.credential.target} from ${describeSource(resolvedProvider.credential.source)})`) + (identityCredential === void 0 ? "" : `, identity credential ${identityCredential.target} from ${describeSource(identityCredential.source)}`)
|
|
78646
79106
|
);
|
|
79107
|
+
const updateCheck = runLaunchUpdateCheck({
|
|
79108
|
+
mode: params.updateMode ?? "off",
|
|
79109
|
+
checkPath: import_node_path43.default.join(paths.root, "update.check"),
|
|
79110
|
+
currentVersion: package_default.version,
|
|
79111
|
+
port: params.update
|
|
79112
|
+
});
|
|
78647
79113
|
let cascadeLaunch;
|
|
78648
79114
|
if (farmContext !== void 0) {
|
|
78649
79115
|
const { farm: resyncFarmRuntime, identity: resyncIdentity, cascade } = farmContext;
|
|
@@ -78741,7 +79207,7 @@ function prepareLaunch(params) {
|
|
|
78741
79207
|
...identityCredential === void 0 ? {} : { identityCredential },
|
|
78742
79208
|
...frontDoor === void 0 ? {} : { frontdoor: frontDoor },
|
|
78743
79209
|
...headroom === void 0 ? {} : { headroom },
|
|
78744
|
-
sessionId: (0,
|
|
79210
|
+
sessionId: (0, import_node_crypto19.randomUUID)()
|
|
78745
79211
|
});
|
|
78746
79212
|
const releases = [...headroom === void 0 || headroomPort === void 0 ? [] : [headroomPort.release], ...frontDoorRelease === void 0 ? [] : [frontDoorRelease]];
|
|
78747
79213
|
const release = () => {
|
|
@@ -78749,11 +79215,12 @@ function prepareLaunch(params) {
|
|
|
78749
79215
|
registered();
|
|
78750
79216
|
}
|
|
78751
79217
|
};
|
|
78752
|
-
return { bin: discovered.path, args: finalArgv, env: finalEnv, release };
|
|
79218
|
+
return { bin: discovered.path, args: finalArgv, env: finalEnv, release, markChildStarted: updateCheck.markChildStarted };
|
|
78753
79219
|
}
|
|
78754
79220
|
function runLauncher(params) {
|
|
78755
79221
|
const plan = prepareLaunch(params);
|
|
78756
79222
|
try {
|
|
79223
|
+
plan.markChildStarted();
|
|
78757
79224
|
spawnClaude({ bin: plan.bin, args: plan.args, env: plan.env, spawn: params.spawn, proc: params.proc, beforeExit: plan.release });
|
|
78758
79225
|
} finally {
|
|
78759
79226
|
plan.release();
|
|
@@ -78768,6 +79235,7 @@ function buildFarmRuntime(paths, cwd) {
|
|
|
78768
79235
|
const loaded = loadCascadeInput({ paths, home, cwd, read });
|
|
78769
79236
|
const selections = readDirectorySelections(loaded);
|
|
78770
79237
|
const git = resolveGitBranch(realRunPort, cwd);
|
|
79238
|
+
const updateMode = resolveUpdateMode(loaded.globalConfig);
|
|
78771
79239
|
return {
|
|
78772
79240
|
runtime: {
|
|
78773
79241
|
fs: realFarmFs,
|
|
@@ -78786,14 +79254,15 @@ function buildFarmRuntime(paths, cwd) {
|
|
|
78786
79254
|
...cliOverride === void 0 ? {} : { cliOverride }
|
|
78787
79255
|
}).input,
|
|
78788
79256
|
now: () => Date.now(),
|
|
78789
|
-
uniqueSuffix: `${String(process.pid)}.${(0,
|
|
79257
|
+
uniqueSuffix: `${String(process.pid)}.${(0, import_node_crypto20.randomUUID)()}`,
|
|
78790
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.
|
|
78791
79259
|
lock: { pid: process.pid, isRunning: realIsProcessRunning, sleep: realSleepSync }
|
|
78792
79260
|
},
|
|
78793
79261
|
...selections.identity === void 0 ? {} : { directoryIdentity: selections.identity },
|
|
78794
79262
|
...selections.configProfile === void 0 ? {} : { directoryConfigProfile: selections.configProfile },
|
|
78795
79263
|
...loaded.globalConfig?.defaultConfigProfile === void 0 ? {} : { globalDefaultConfigProfile: loaded.globalConfig.defaultConfigProfile },
|
|
78796
|
-
...loaded.globalConfig?.pools === void 0 ? {} : { pools: loaded.globalConfig.pools }
|
|
79264
|
+
...loaded.globalConfig?.pools === void 0 ? {} : { pools: loaded.globalConfig.pools },
|
|
79265
|
+
...updateMode === "off" ? {} : { updateMode }
|
|
78797
79266
|
};
|
|
78798
79267
|
}
|
|
78799
79268
|
function realClaudeBinaryResolver(paths) {
|
|
@@ -78815,6 +79284,7 @@ function realPrepareLaunchParams(paths, options) {
|
|
|
78815
79284
|
...farm.directoryConfigProfile === void 0 ? {} : { directoryRuleConfigProfile: farm.directoryConfigProfile },
|
|
78816
79285
|
...farm.globalDefaultConfigProfile === void 0 ? {} : { globalDefaultConfigProfile: farm.globalDefaultConfigProfile },
|
|
78817
79286
|
...farm.pools === void 0 ? {} : { pools: farm.pools },
|
|
79287
|
+
...farm.updateMode === void 0 ? {} : { updateMode: farm.updateMode },
|
|
78818
79288
|
...options.allowMissingConfigProfile === void 0 ? {} : { allowMissingConfigProfile: options.allowMissingConfigProfile }
|
|
78819
79289
|
};
|
|
78820
79290
|
}
|
|
@@ -78918,6 +79388,8 @@ async function runClaude(argvOverride) {
|
|
|
78918
79388
|
farm,
|
|
78919
79389
|
allowMissingConfigProfile
|
|
78920
79390
|
}),
|
|
79391
|
+
// Wired here rather than in realPrepareLaunchParams because the port re-invokes this very binary: correct for the agent-shim command line, wrong for a library host whose own executable is not agent-shim.
|
|
79392
|
+
update: realLaunchUpdatePort(paths),
|
|
78921
79393
|
spawn: realSpawnPort
|
|
78922
79394
|
});
|
|
78923
79395
|
}
|