agent-shim 8.5.5 → 8.6.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 +3 -0
- package/dist/cli.cjs +2183 -1983
- package/dist/index.cjs +170 -8
- package/dist/index.mjs +162 -8
- package/dist/types/index.d.ts +60 -7
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -75,6 +75,7 @@ __export(index_exports, {
|
|
|
75
75
|
ProviderSchema: () => ProviderSchema,
|
|
76
76
|
RC_IDLE_EXPIRY_MS: () => RC_IDLE_EXPIRY_MS,
|
|
77
77
|
RC_PENDING_DEADLINE_MS: () => RC_PENDING_DEADLINE_MS,
|
|
78
|
+
RC_PERMISSION_MODES: () => RC_PERMISSION_MODES,
|
|
78
79
|
RC_REQUEST_PARSE_CAP_BYTES: () => RC_REQUEST_PARSE_CAP_BYTES,
|
|
79
80
|
RC_SESSIONS_PATH_PREFIX: () => RC_SESSIONS_PATH_PREFIX,
|
|
80
81
|
ROUTED_PATH_PREFIX: () => ROUTED_PATH_PREFIX,
|
|
@@ -91,6 +92,9 @@ __export(index_exports, {
|
|
|
91
92
|
buildLayoutPaths: () => buildLayoutPaths,
|
|
92
93
|
buildRcControlResponsePayload: () => buildRcControlResponsePayload,
|
|
93
94
|
buildRcEventWriteBody: () => buildRcEventWriteBody,
|
|
95
|
+
buildRcInterruptPayload: () => buildRcInterruptPayload,
|
|
96
|
+
buildRcSetModelPayload: () => buildRcSetModelPayload,
|
|
97
|
+
buildRcSetPermissionModePayload: () => buildRcSetPermissionModePayload,
|
|
94
98
|
buildRcUserMessagePayload: () => buildRcUserMessagePayload,
|
|
95
99
|
carryOver: () => carryOver,
|
|
96
100
|
checkReportHasWarnings: () => checkReportHasWarnings,
|
|
@@ -115,8 +119,10 @@ __export(index_exports, {
|
|
|
115
119
|
generateCa: () => generateCa,
|
|
116
120
|
identityExists: () => identityExists,
|
|
117
121
|
injectRcUserMessage: () => injectRcUserMessage,
|
|
122
|
+
interruptRcSession: () => interruptRcSession,
|
|
118
123
|
isIdentityDirectoryName: () => isIdentityDirectoryName,
|
|
119
124
|
isInterceptedHost: () => isInterceptedHost,
|
|
125
|
+
isRcPermissionMode: () => isRcPermissionMode,
|
|
120
126
|
listDirectoryRules: () => listDirectoryRules,
|
|
121
127
|
listIdentities: () => listIdentities,
|
|
122
128
|
listProfiles: () => listProfiles,
|
|
@@ -172,6 +178,8 @@ __export(index_exports, {
|
|
|
172
178
|
setProfileEntries: () => setProfileEntries,
|
|
173
179
|
setProfileLaunchFlags: () => setProfileLaunchFlags,
|
|
174
180
|
setProfileMetadata: () => setProfileMetadata,
|
|
181
|
+
setRcSessionModel: () => setRcSessionModel,
|
|
182
|
+
setRcSessionPermissionMode: () => setRcSessionPermissionMode,
|
|
175
183
|
snapshotPath: () => snapshotPath,
|
|
176
184
|
startConnectServer: () => startConnectServer,
|
|
177
185
|
topLevelNames: () => topLevelNames,
|
|
@@ -4070,6 +4078,19 @@ function buildRcControlResponsePayload(requestId, decision) {
|
|
|
4070
4078
|
const result = decision.approve ? { behavior: "allow" } : { behavior: "deny", message: decision.message ?? "" };
|
|
4071
4079
|
return { type: "control_response", response: { subtype: "success", request_id: requestId, response: result } };
|
|
4072
4080
|
}
|
|
4081
|
+
var RC_PERMISSION_MODES = ["default", "acceptEdits", "bypassPermissions", "plan", "dontAsk", "auto"];
|
|
4082
|
+
function isRcPermissionMode(value) {
|
|
4083
|
+
return RC_PERMISSION_MODES.some((mode) => mode === value);
|
|
4084
|
+
}
|
|
4085
|
+
function buildRcInterruptPayload(requestId) {
|
|
4086
|
+
return { type: "control_request", request_id: requestId, request: { subtype: "interrupt" } };
|
|
4087
|
+
}
|
|
4088
|
+
function buildRcSetModelPayload(requestId, model) {
|
|
4089
|
+
return { type: "control_request", request_id: requestId, request: { subtype: "set_model", model } };
|
|
4090
|
+
}
|
|
4091
|
+
function buildRcSetPermissionModePayload(requestId, mode) {
|
|
4092
|
+
return { type: "control_request", request_id: requestId, request: { subtype: "set_permission_mode", mode } };
|
|
4093
|
+
}
|
|
4073
4094
|
async function answerRcControlRequest(deps, sessionId, requestId, decision) {
|
|
4074
4095
|
const credential = deps.credentialOf(sessionId);
|
|
4075
4096
|
if (credential === void 0) {
|
|
@@ -4094,9 +4115,64 @@ async function answerRcControlRequest(deps, sessionId, requestId, decision) {
|
|
|
4094
4115
|
}
|
|
4095
4116
|
return result;
|
|
4096
4117
|
}
|
|
4118
|
+
async function interruptRcSession(deps, sessionId) {
|
|
4119
|
+
const credential = deps.credentialOf(sessionId);
|
|
4120
|
+
if (credential === void 0) {
|
|
4121
|
+
return { ok: false, message: `the front door has not observed Remote Control session ${sessionId}: it may never have passed through this door, or it ended or expired (an entry lives only a bounded idle period past its last observed traffic)` };
|
|
4122
|
+
}
|
|
4123
|
+
if (credential.authorization === void 0) {
|
|
4124
|
+
return noOAuthCredentialResult(sessionId);
|
|
4125
|
+
}
|
|
4126
|
+
const body = JSON.stringify(buildRcEventWriteBody(buildRcInterruptPayload(deps.newUuid())));
|
|
4127
|
+
let answer;
|
|
4128
|
+
try {
|
|
4129
|
+
answer = await deps.dial.writeEvents(sessionId, rcWriteHeaders(credential), body);
|
|
4130
|
+
} catch (error) {
|
|
4131
|
+
return { ok: false, message: `the door could not reach the API host to interrupt session ${sessionId}: ${error instanceof Error ? error.message : String(error)}` };
|
|
4132
|
+
}
|
|
4133
|
+
return rcEventWriteResultFromAnswer(answer);
|
|
4134
|
+
}
|
|
4135
|
+
async function setRcSessionModel(deps, sessionId, model) {
|
|
4136
|
+
if (model === "") {
|
|
4137
|
+
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" };
|
|
4138
|
+
}
|
|
4139
|
+
const credential = deps.credentialOf(sessionId);
|
|
4140
|
+
if (credential === void 0) {
|
|
4141
|
+
return { ok: false, message: `the front door has not observed Remote Control session ${sessionId}: it may never have passed through this door, or it ended or expired (an entry lives only a bounded idle period past its last observed traffic)` };
|
|
4142
|
+
}
|
|
4143
|
+
if (credential.authorization === void 0) {
|
|
4144
|
+
return noOAuthCredentialResult(sessionId);
|
|
4145
|
+
}
|
|
4146
|
+
const body = JSON.stringify(buildRcEventWriteBody(buildRcSetModelPayload(deps.newUuid(), model)));
|
|
4147
|
+
let answer;
|
|
4148
|
+
try {
|
|
4149
|
+
answer = await deps.dial.writeEvents(sessionId, rcWriteHeaders(credential), body);
|
|
4150
|
+
} catch (error) {
|
|
4151
|
+
return { ok: false, message: `the door could not reach the API host to set the model on session ${sessionId}: ${error instanceof Error ? error.message : String(error)}` };
|
|
4152
|
+
}
|
|
4153
|
+
return rcEventWriteResultFromAnswer(answer);
|
|
4154
|
+
}
|
|
4155
|
+
async function setRcSessionPermissionMode(deps, sessionId, mode) {
|
|
4156
|
+
const credential = deps.credentialOf(sessionId);
|
|
4157
|
+
if (credential === void 0) {
|
|
4158
|
+
return { ok: false, message: `the front door has not observed Remote Control session ${sessionId}: it may never have passed through this door, or it ended or expired (an entry lives only a bounded idle period past its last observed traffic)` };
|
|
4159
|
+
}
|
|
4160
|
+
if (credential.authorization === void 0) {
|
|
4161
|
+
return noOAuthCredentialResult(sessionId);
|
|
4162
|
+
}
|
|
4163
|
+
const body = JSON.stringify(buildRcEventWriteBody(buildRcSetPermissionModePayload(deps.newUuid(), mode)));
|
|
4164
|
+
let answer;
|
|
4165
|
+
try {
|
|
4166
|
+
answer = await deps.dial.writeEvents(sessionId, rcWriteHeaders(credential), body);
|
|
4167
|
+
} catch (error) {
|
|
4168
|
+
return { ok: false, message: `the door could not reach the API host to set the permission mode on session ${sessionId}: ${error instanceof Error ? error.message : String(error)}` };
|
|
4169
|
+
}
|
|
4170
|
+
return rcEventWriteResultFromAnswer(answer);
|
|
4171
|
+
}
|
|
4097
4172
|
|
|
4098
4173
|
// src/frontdoor/rcControl.ts
|
|
4099
4174
|
var https2 = __toESM(require("node:https"), 1);
|
|
4175
|
+
var RC_PERMISSION_MODE_LIST = RC_PERMISSION_MODES.join(", ");
|
|
4100
4176
|
var CONTROL_PATH_PREFIX = "/__agent-shim/rc";
|
|
4101
4177
|
var CONTROL_BODY_CAP_BYTES = 10485760;
|
|
4102
4178
|
function answerJson(response, status, body) {
|
|
@@ -4131,6 +4207,9 @@ function createRcControlHandler(deps) {
|
|
|
4131
4207
|
const pendingPath = `${CONTROL_PATH_PREFIX}/pending`;
|
|
4132
4208
|
const injectPath = `${CONTROL_PATH_PREFIX}/inject`;
|
|
4133
4209
|
const answerPath = `${CONTROL_PATH_PREFIX}/answer`;
|
|
4210
|
+
const interruptPath = `${CONTROL_PATH_PREFIX}/interrupt`;
|
|
4211
|
+
const setModelPath = `${CONTROL_PATH_PREFIX}/set-model`;
|
|
4212
|
+
const setPermissionModePath = `${CONTROL_PATH_PREFIX}/set-permission-mode`;
|
|
4134
4213
|
return (request4, response) => {
|
|
4135
4214
|
const presented = request4.headers.authorization;
|
|
4136
4215
|
const bearerPrefix = "bearer ".length;
|
|
@@ -4215,6 +4294,78 @@ function createRcControlHandler(deps) {
|
|
|
4215
4294
|
answerJson(response, HTTP_STATUS.badGateway, { error: result.message });
|
|
4216
4295
|
return;
|
|
4217
4296
|
}
|
|
4297
|
+
if (url.pathname === interruptPath) {
|
|
4298
|
+
if (request4.method !== "POST") {
|
|
4299
|
+
answerJson(response, HTTP_STATUS.methodNotAllowed, { error: "the interrupt operation is a POST" });
|
|
4300
|
+
return;
|
|
4301
|
+
}
|
|
4302
|
+
let parsed;
|
|
4303
|
+
try {
|
|
4304
|
+
parsed = JSON.parse(await readBody(request4));
|
|
4305
|
+
} catch {
|
|
4306
|
+
answerJson(response, HTTP_STATUS.badRequest, { error: "the interrupt body is not readable JSON within the control body cap" });
|
|
4307
|
+
return;
|
|
4308
|
+
}
|
|
4309
|
+
if (typeof parsed !== "object" || parsed === null || !("session" in parsed) || typeof parsed.session !== "string" || parsed.session === "") {
|
|
4310
|
+
answerJson(response, HTTP_STATUS.badRequest, { error: "the interrupt body must be JSON naming a non-empty session id" });
|
|
4311
|
+
return;
|
|
4312
|
+
}
|
|
4313
|
+
const result = await deps.interrupt(parsed.session);
|
|
4314
|
+
if (result.ok) {
|
|
4315
|
+
answerJson(response, HTTP_STATUS.ok, { session: parsed.session, sequenceNums: result.sequenceNums });
|
|
4316
|
+
return;
|
|
4317
|
+
}
|
|
4318
|
+
answerJson(response, HTTP_STATUS.badGateway, { error: result.message });
|
|
4319
|
+
return;
|
|
4320
|
+
}
|
|
4321
|
+
if (url.pathname === setModelPath) {
|
|
4322
|
+
if (request4.method !== "POST") {
|
|
4323
|
+
answerJson(response, HTTP_STATUS.methodNotAllowed, { error: "the set-model operation is a POST" });
|
|
4324
|
+
return;
|
|
4325
|
+
}
|
|
4326
|
+
let parsed;
|
|
4327
|
+
try {
|
|
4328
|
+
parsed = JSON.parse(await readBody(request4));
|
|
4329
|
+
} catch {
|
|
4330
|
+
answerJson(response, HTTP_STATUS.badRequest, { error: "the set-model body is not readable JSON within the control body cap" });
|
|
4331
|
+
return;
|
|
4332
|
+
}
|
|
4333
|
+
if (typeof parsed !== "object" || parsed === null || !("session" in parsed) || !("model" in parsed) || typeof parsed.session !== "string" || typeof parsed.model !== "string" || parsed.session === "" || parsed.model === "") {
|
|
4334
|
+
answerJson(response, HTTP_STATUS.badRequest, { error: "the set-model body must be JSON naming a non-empty session id and model id" });
|
|
4335
|
+
return;
|
|
4336
|
+
}
|
|
4337
|
+
const result = await deps.setModel(parsed.session, parsed.model);
|
|
4338
|
+
if (result.ok) {
|
|
4339
|
+
answerJson(response, HTTP_STATUS.ok, { session: parsed.session, model: parsed.model, sequenceNums: result.sequenceNums });
|
|
4340
|
+
return;
|
|
4341
|
+
}
|
|
4342
|
+
answerJson(response, HTTP_STATUS.badGateway, { error: result.message });
|
|
4343
|
+
return;
|
|
4344
|
+
}
|
|
4345
|
+
if (url.pathname === setPermissionModePath) {
|
|
4346
|
+
if (request4.method !== "POST") {
|
|
4347
|
+
answerJson(response, HTTP_STATUS.methodNotAllowed, { error: "the set-permission-mode operation is a POST" });
|
|
4348
|
+
return;
|
|
4349
|
+
}
|
|
4350
|
+
let parsed;
|
|
4351
|
+
try {
|
|
4352
|
+
parsed = JSON.parse(await readBody(request4));
|
|
4353
|
+
} catch {
|
|
4354
|
+
answerJson(response, HTTP_STATUS.badRequest, { error: "the set-permission-mode body is not readable JSON within the control body cap" });
|
|
4355
|
+
return;
|
|
4356
|
+
}
|
|
4357
|
+
if (typeof parsed !== "object" || parsed === null || !("session" in parsed) || !("mode" in parsed) || typeof parsed.session !== "string" || parsed.session === "" || !isRcPermissionMode(parsed.mode)) {
|
|
4358
|
+
answerJson(response, HTTP_STATUS.badRequest, { 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})` });
|
|
4359
|
+
return;
|
|
4360
|
+
}
|
|
4361
|
+
const result = await deps.setPermissionMode(parsed.session, parsed.mode);
|
|
4362
|
+
if (result.ok) {
|
|
4363
|
+
answerJson(response, HTTP_STATUS.ok, { session: parsed.session, mode: parsed.mode, sequenceNums: result.sequenceNums });
|
|
4364
|
+
return;
|
|
4365
|
+
}
|
|
4366
|
+
answerJson(response, HTTP_STATUS.badGateway, { error: result.message });
|
|
4367
|
+
return;
|
|
4368
|
+
}
|
|
4218
4369
|
answerJson(response, HTTP_STATUS.notFound, { error: `no such control route: ${url.pathname}` });
|
|
4219
4370
|
};
|
|
4220
4371
|
handle().catch((error) => {
|
|
@@ -4429,7 +4580,10 @@ function frontDoorRcControl(transport, token) {
|
|
|
4429
4580
|
`${CONTROL_PATH_PREFIX}/answer`,
|
|
4430
4581
|
JSON.stringify({ session: sessionId, request: requestId, approve: decision.approve, ...decision.approve || decision.message === void 0 ? {} : { text: decision.message } }),
|
|
4431
4582
|
(status, message) => `answering control request ${requestId} on ${sessionId} failed (HTTP ${String(status)}): ${message}`
|
|
4432
|
-
)
|
|
4583
|
+
),
|
|
4584
|
+
interruptSession: async (sessionId) => await writeOf(`${CONTROL_PATH_PREFIX}/interrupt`, JSON.stringify({ session: sessionId }), (status, message) => `interrupting ${sessionId} failed (HTTP ${String(status)}): ${message}`),
|
|
4585
|
+
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}`),
|
|
4586
|
+
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}`)
|
|
4433
4587
|
};
|
|
4434
4588
|
}
|
|
4435
4589
|
|
|
@@ -4589,7 +4743,7 @@ function ensureFrontDoor(params) {
|
|
|
4589
4743
|
ports.stopSupervisor(supervisorPid);
|
|
4590
4744
|
while (ports.isRunning(supervisorPid)) {
|
|
4591
4745
|
if (ports.now() >= deadline) {
|
|
4592
|
-
throw new FrontDoorStartError(`
|
|
4746
|
+
throw new FrontDoorStartError(`the front door (pid ${String(supervisorPid)}, protocol ${String(state.protocol ?? 0)}) is older than this launcher (protocol ${String(FRONT_DOOR_PROTOCOL)}) and did not exit when asked. Daemon log: ${paths.frontdoorLogPath}`);
|
|
4593
4747
|
}
|
|
4594
4748
|
ports.sleep(FRONTDOOR_LAUNCHER_POLL_MS);
|
|
4595
4749
|
}
|
|
@@ -4603,13 +4757,13 @@ function ensureFrontDoor(params) {
|
|
|
4603
4757
|
}
|
|
4604
4758
|
lastRejection = `the listener on 127.0.0.1:${String(state.port)} (recorded for pid ${String(supervisorPid)}) failed authentication: ${verdict.reason}`;
|
|
4605
4759
|
if (spawned) {
|
|
4606
|
-
throw new FrontDoorStartError(
|
|
4760
|
+
throw new FrontDoorStartError(`${lastRejection}; refusing to send this session's credentials to it. Daemon log: ${paths.frontdoorLogPath}`);
|
|
4607
4761
|
}
|
|
4608
4762
|
distrusted.add(supervisorPid);
|
|
4609
4763
|
continue;
|
|
4610
4764
|
}
|
|
4611
4765
|
if (state?.lastError !== void 0) {
|
|
4612
|
-
throw new FrontDoorStartError(`
|
|
4766
|
+
throw new FrontDoorStartError(`the front door reported a fatal error and is not serving: ${state.lastError} (daemon log: ${paths.frontdoorLogPath})`);
|
|
4613
4767
|
}
|
|
4614
4768
|
} else if (!spawned) {
|
|
4615
4769
|
ports.fs.mkdirp(paths.frontdoorDir);
|
|
@@ -4629,7 +4783,7 @@ function ensureFrontDoor(params) {
|
|
|
4629
4783
|
if (ports.now() >= deadline) {
|
|
4630
4784
|
const lastError = state?.lastError;
|
|
4631
4785
|
throw new FrontDoorStartError(
|
|
4632
|
-
`
|
|
4786
|
+
`the front door did not become ready within ${String(FRONTDOOR_START_TIMEOUT_MS)}ms` + (lastError === void 0 ? "" : ` (last error: ${lastError})`) + (lastRejection === void 0 ? "" : ` (${lastRejection})`) + `. Daemon log: ${paths.frontdoorLogPath}`
|
|
4633
4787
|
);
|
|
4634
4788
|
}
|
|
4635
4789
|
ports.sleep(FRONTDOOR_LAUNCHER_POLL_MS);
|
|
@@ -4968,14 +5122,14 @@ function ensureHeadroom(params) {
|
|
|
4968
5122
|
const holders = sessionsForSupervisor(ports.fs, paths.headroomSessionsDir, state.supervisorPid).filter((session) => ports.isRunning(session.pid));
|
|
4969
5123
|
if (holders.length > 0) {
|
|
4970
5124
|
throw new HeadroomStartError(
|
|
4971
|
-
`
|
|
5125
|
+
`the running headroom daemon was started before this launch's front door address and would refuse every request this launch sends through it. It restarts once the launches using it end (pid ${holders.map((session) => String(session.pid)).join(", ")}); end them and launch again, or launch with --no-headroom.`
|
|
4972
5126
|
);
|
|
4973
5127
|
}
|
|
4974
5128
|
awaitingAllowlist = true;
|
|
4975
5129
|
}
|
|
4976
5130
|
if (state.lastError !== void 0) {
|
|
4977
5131
|
throw new HeadroomStartError(
|
|
4978
|
-
`
|
|
5132
|
+
`the headroom daemon reported a fatal error and is not serving: ${state.lastError} (daemon log: ${paths.headroomLogPath})`
|
|
4979
5133
|
);
|
|
4980
5134
|
}
|
|
4981
5135
|
} else if (!spawnedSupervisor) {
|
|
@@ -4999,7 +5153,7 @@ function ensureHeadroom(params) {
|
|
|
4999
5153
|
if (ports.now() >= deadline) {
|
|
5000
5154
|
const lastError = state?.lastError;
|
|
5001
5155
|
throw new HeadroomStartError(
|
|
5002
|
-
`
|
|
5156
|
+
`the headroom daemon did not become ready within ${String(HEADROOM_START_TIMEOUT_MS)}ms` + (lastError === void 0 ? "" : ` (last error: ${lastError})`) + (awaitingAllowlist ? " (the running daemon's allowlist does not admit this launch's front door, and it did not restart)" : "") + `. Daemon log: ${paths.headroomLogPath}`
|
|
5003
5157
|
);
|
|
5004
5158
|
}
|
|
5005
5159
|
ports.sleep(HEADROOM_LAUNCHER_POLL_MS);
|
|
@@ -9379,6 +9533,7 @@ function prepareClaudeLaunch(options) {
|
|
|
9379
9533
|
ProviderSchema,
|
|
9380
9534
|
RC_IDLE_EXPIRY_MS,
|
|
9381
9535
|
RC_PENDING_DEADLINE_MS,
|
|
9536
|
+
RC_PERMISSION_MODES,
|
|
9382
9537
|
RC_REQUEST_PARSE_CAP_BYTES,
|
|
9383
9538
|
RC_SESSIONS_PATH_PREFIX,
|
|
9384
9539
|
ROUTED_PATH_PREFIX,
|
|
@@ -9395,6 +9550,9 @@ function prepareClaudeLaunch(options) {
|
|
|
9395
9550
|
buildLayoutPaths,
|
|
9396
9551
|
buildRcControlResponsePayload,
|
|
9397
9552
|
buildRcEventWriteBody,
|
|
9553
|
+
buildRcInterruptPayload,
|
|
9554
|
+
buildRcSetModelPayload,
|
|
9555
|
+
buildRcSetPermissionModePayload,
|
|
9398
9556
|
buildRcUserMessagePayload,
|
|
9399
9557
|
carryOver,
|
|
9400
9558
|
checkReportHasWarnings,
|
|
@@ -9419,8 +9577,10 @@ function prepareClaudeLaunch(options) {
|
|
|
9419
9577
|
generateCa,
|
|
9420
9578
|
identityExists,
|
|
9421
9579
|
injectRcUserMessage,
|
|
9580
|
+
interruptRcSession,
|
|
9422
9581
|
isIdentityDirectoryName,
|
|
9423
9582
|
isInterceptedHost,
|
|
9583
|
+
isRcPermissionMode,
|
|
9424
9584
|
listDirectoryRules,
|
|
9425
9585
|
listIdentities,
|
|
9426
9586
|
listProfiles,
|
|
@@ -9476,6 +9636,8 @@ function prepareClaudeLaunch(options) {
|
|
|
9476
9636
|
setProfileEntries,
|
|
9477
9637
|
setProfileLaunchFlags,
|
|
9478
9638
|
setProfileMetadata,
|
|
9639
|
+
setRcSessionModel,
|
|
9640
|
+
setRcSessionPermissionMode,
|
|
9479
9641
|
snapshotPath,
|
|
9480
9642
|
startConnectServer,
|
|
9481
9643
|
topLevelNames,
|
package/dist/index.mjs
CHANGED
|
@@ -3886,6 +3886,19 @@ function buildRcControlResponsePayload(requestId, decision) {
|
|
|
3886
3886
|
const result = decision.approve ? { behavior: "allow" } : { behavior: "deny", message: decision.message ?? "" };
|
|
3887
3887
|
return { type: "control_response", response: { subtype: "success", request_id: requestId, response: result } };
|
|
3888
3888
|
}
|
|
3889
|
+
var RC_PERMISSION_MODES = ["default", "acceptEdits", "bypassPermissions", "plan", "dontAsk", "auto"];
|
|
3890
|
+
function isRcPermissionMode(value) {
|
|
3891
|
+
return RC_PERMISSION_MODES.some((mode) => mode === value);
|
|
3892
|
+
}
|
|
3893
|
+
function buildRcInterruptPayload(requestId) {
|
|
3894
|
+
return { type: "control_request", request_id: requestId, request: { subtype: "interrupt" } };
|
|
3895
|
+
}
|
|
3896
|
+
function buildRcSetModelPayload(requestId, model) {
|
|
3897
|
+
return { type: "control_request", request_id: requestId, request: { subtype: "set_model", model } };
|
|
3898
|
+
}
|
|
3899
|
+
function buildRcSetPermissionModePayload(requestId, mode) {
|
|
3900
|
+
return { type: "control_request", request_id: requestId, request: { subtype: "set_permission_mode", mode } };
|
|
3901
|
+
}
|
|
3889
3902
|
async function answerRcControlRequest(deps, sessionId, requestId, decision) {
|
|
3890
3903
|
const credential = deps.credentialOf(sessionId);
|
|
3891
3904
|
if (credential === void 0) {
|
|
@@ -3910,9 +3923,64 @@ async function answerRcControlRequest(deps, sessionId, requestId, decision) {
|
|
|
3910
3923
|
}
|
|
3911
3924
|
return result;
|
|
3912
3925
|
}
|
|
3926
|
+
async function interruptRcSession(deps, sessionId) {
|
|
3927
|
+
const credential = deps.credentialOf(sessionId);
|
|
3928
|
+
if (credential === void 0) {
|
|
3929
|
+
return { ok: false, message: `the front door has not observed Remote Control session ${sessionId}: it may never have passed through this door, or it ended or expired (an entry lives only a bounded idle period past its last observed traffic)` };
|
|
3930
|
+
}
|
|
3931
|
+
if (credential.authorization === void 0) {
|
|
3932
|
+
return noOAuthCredentialResult(sessionId);
|
|
3933
|
+
}
|
|
3934
|
+
const body = JSON.stringify(buildRcEventWriteBody(buildRcInterruptPayload(deps.newUuid())));
|
|
3935
|
+
let answer;
|
|
3936
|
+
try {
|
|
3937
|
+
answer = await deps.dial.writeEvents(sessionId, rcWriteHeaders(credential), body);
|
|
3938
|
+
} catch (error) {
|
|
3939
|
+
return { ok: false, message: `the door could not reach the API host to interrupt session ${sessionId}: ${error instanceof Error ? error.message : String(error)}` };
|
|
3940
|
+
}
|
|
3941
|
+
return rcEventWriteResultFromAnswer(answer);
|
|
3942
|
+
}
|
|
3943
|
+
async function setRcSessionModel(deps, sessionId, model) {
|
|
3944
|
+
if (model === "") {
|
|
3945
|
+
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" };
|
|
3946
|
+
}
|
|
3947
|
+
const credential = deps.credentialOf(sessionId);
|
|
3948
|
+
if (credential === void 0) {
|
|
3949
|
+
return { ok: false, message: `the front door has not observed Remote Control session ${sessionId}: it may never have passed through this door, or it ended or expired (an entry lives only a bounded idle period past its last observed traffic)` };
|
|
3950
|
+
}
|
|
3951
|
+
if (credential.authorization === void 0) {
|
|
3952
|
+
return noOAuthCredentialResult(sessionId);
|
|
3953
|
+
}
|
|
3954
|
+
const body = JSON.stringify(buildRcEventWriteBody(buildRcSetModelPayload(deps.newUuid(), model)));
|
|
3955
|
+
let answer;
|
|
3956
|
+
try {
|
|
3957
|
+
answer = await deps.dial.writeEvents(sessionId, rcWriteHeaders(credential), body);
|
|
3958
|
+
} catch (error) {
|
|
3959
|
+
return { ok: false, message: `the door could not reach the API host to set the model on session ${sessionId}: ${error instanceof Error ? error.message : String(error)}` };
|
|
3960
|
+
}
|
|
3961
|
+
return rcEventWriteResultFromAnswer(answer);
|
|
3962
|
+
}
|
|
3963
|
+
async function setRcSessionPermissionMode(deps, sessionId, mode) {
|
|
3964
|
+
const credential = deps.credentialOf(sessionId);
|
|
3965
|
+
if (credential === void 0) {
|
|
3966
|
+
return { ok: false, message: `the front door has not observed Remote Control session ${sessionId}: it may never have passed through this door, or it ended or expired (an entry lives only a bounded idle period past its last observed traffic)` };
|
|
3967
|
+
}
|
|
3968
|
+
if (credential.authorization === void 0) {
|
|
3969
|
+
return noOAuthCredentialResult(sessionId);
|
|
3970
|
+
}
|
|
3971
|
+
const body = JSON.stringify(buildRcEventWriteBody(buildRcSetPermissionModePayload(deps.newUuid(), mode)));
|
|
3972
|
+
let answer;
|
|
3973
|
+
try {
|
|
3974
|
+
answer = await deps.dial.writeEvents(sessionId, rcWriteHeaders(credential), body);
|
|
3975
|
+
} catch (error) {
|
|
3976
|
+
return { ok: false, message: `the door could not reach the API host to set the permission mode on session ${sessionId}: ${error instanceof Error ? error.message : String(error)}` };
|
|
3977
|
+
}
|
|
3978
|
+
return rcEventWriteResultFromAnswer(answer);
|
|
3979
|
+
}
|
|
3913
3980
|
|
|
3914
3981
|
// src/frontdoor/rcControl.ts
|
|
3915
3982
|
import * as https2 from "node:https";
|
|
3983
|
+
var RC_PERMISSION_MODE_LIST = RC_PERMISSION_MODES.join(", ");
|
|
3916
3984
|
var CONTROL_PATH_PREFIX = "/__agent-shim/rc";
|
|
3917
3985
|
var CONTROL_BODY_CAP_BYTES = 10485760;
|
|
3918
3986
|
function answerJson(response, status, body) {
|
|
@@ -3947,6 +4015,9 @@ function createRcControlHandler(deps) {
|
|
|
3947
4015
|
const pendingPath = `${CONTROL_PATH_PREFIX}/pending`;
|
|
3948
4016
|
const injectPath = `${CONTROL_PATH_PREFIX}/inject`;
|
|
3949
4017
|
const answerPath = `${CONTROL_PATH_PREFIX}/answer`;
|
|
4018
|
+
const interruptPath = `${CONTROL_PATH_PREFIX}/interrupt`;
|
|
4019
|
+
const setModelPath = `${CONTROL_PATH_PREFIX}/set-model`;
|
|
4020
|
+
const setPermissionModePath = `${CONTROL_PATH_PREFIX}/set-permission-mode`;
|
|
3950
4021
|
return (request4, response) => {
|
|
3951
4022
|
const presented = request4.headers.authorization;
|
|
3952
4023
|
const bearerPrefix = "bearer ".length;
|
|
@@ -4031,6 +4102,78 @@ function createRcControlHandler(deps) {
|
|
|
4031
4102
|
answerJson(response, HTTP_STATUS.badGateway, { error: result.message });
|
|
4032
4103
|
return;
|
|
4033
4104
|
}
|
|
4105
|
+
if (url.pathname === interruptPath) {
|
|
4106
|
+
if (request4.method !== "POST") {
|
|
4107
|
+
answerJson(response, HTTP_STATUS.methodNotAllowed, { error: "the interrupt operation is a POST" });
|
|
4108
|
+
return;
|
|
4109
|
+
}
|
|
4110
|
+
let parsed;
|
|
4111
|
+
try {
|
|
4112
|
+
parsed = JSON.parse(await readBody(request4));
|
|
4113
|
+
} catch {
|
|
4114
|
+
answerJson(response, HTTP_STATUS.badRequest, { error: "the interrupt body is not readable JSON within the control body cap" });
|
|
4115
|
+
return;
|
|
4116
|
+
}
|
|
4117
|
+
if (typeof parsed !== "object" || parsed === null || !("session" in parsed) || typeof parsed.session !== "string" || parsed.session === "") {
|
|
4118
|
+
answerJson(response, HTTP_STATUS.badRequest, { error: "the interrupt body must be JSON naming a non-empty session id" });
|
|
4119
|
+
return;
|
|
4120
|
+
}
|
|
4121
|
+
const result = await deps.interrupt(parsed.session);
|
|
4122
|
+
if (result.ok) {
|
|
4123
|
+
answerJson(response, HTTP_STATUS.ok, { session: parsed.session, sequenceNums: result.sequenceNums });
|
|
4124
|
+
return;
|
|
4125
|
+
}
|
|
4126
|
+
answerJson(response, HTTP_STATUS.badGateway, { error: result.message });
|
|
4127
|
+
return;
|
|
4128
|
+
}
|
|
4129
|
+
if (url.pathname === setModelPath) {
|
|
4130
|
+
if (request4.method !== "POST") {
|
|
4131
|
+
answerJson(response, HTTP_STATUS.methodNotAllowed, { error: "the set-model operation is a POST" });
|
|
4132
|
+
return;
|
|
4133
|
+
}
|
|
4134
|
+
let parsed;
|
|
4135
|
+
try {
|
|
4136
|
+
parsed = JSON.parse(await readBody(request4));
|
|
4137
|
+
} catch {
|
|
4138
|
+
answerJson(response, HTTP_STATUS.badRequest, { error: "the set-model body is not readable JSON within the control body cap" });
|
|
4139
|
+
return;
|
|
4140
|
+
}
|
|
4141
|
+
if (typeof parsed !== "object" || parsed === null || !("session" in parsed) || !("model" in parsed) || typeof parsed.session !== "string" || typeof parsed.model !== "string" || parsed.session === "" || parsed.model === "") {
|
|
4142
|
+
answerJson(response, HTTP_STATUS.badRequest, { error: "the set-model body must be JSON naming a non-empty session id and model id" });
|
|
4143
|
+
return;
|
|
4144
|
+
}
|
|
4145
|
+
const result = await deps.setModel(parsed.session, parsed.model);
|
|
4146
|
+
if (result.ok) {
|
|
4147
|
+
answerJson(response, HTTP_STATUS.ok, { session: parsed.session, model: parsed.model, sequenceNums: result.sequenceNums });
|
|
4148
|
+
return;
|
|
4149
|
+
}
|
|
4150
|
+
answerJson(response, HTTP_STATUS.badGateway, { error: result.message });
|
|
4151
|
+
return;
|
|
4152
|
+
}
|
|
4153
|
+
if (url.pathname === setPermissionModePath) {
|
|
4154
|
+
if (request4.method !== "POST") {
|
|
4155
|
+
answerJson(response, HTTP_STATUS.methodNotAllowed, { error: "the set-permission-mode operation is a POST" });
|
|
4156
|
+
return;
|
|
4157
|
+
}
|
|
4158
|
+
let parsed;
|
|
4159
|
+
try {
|
|
4160
|
+
parsed = JSON.parse(await readBody(request4));
|
|
4161
|
+
} catch {
|
|
4162
|
+
answerJson(response, HTTP_STATUS.badRequest, { error: "the set-permission-mode body is not readable JSON within the control body cap" });
|
|
4163
|
+
return;
|
|
4164
|
+
}
|
|
4165
|
+
if (typeof parsed !== "object" || parsed === null || !("session" in parsed) || !("mode" in parsed) || typeof parsed.session !== "string" || parsed.session === "" || !isRcPermissionMode(parsed.mode)) {
|
|
4166
|
+
answerJson(response, HTTP_STATUS.badRequest, { 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})` });
|
|
4167
|
+
return;
|
|
4168
|
+
}
|
|
4169
|
+
const result = await deps.setPermissionMode(parsed.session, parsed.mode);
|
|
4170
|
+
if (result.ok) {
|
|
4171
|
+
answerJson(response, HTTP_STATUS.ok, { session: parsed.session, mode: parsed.mode, sequenceNums: result.sequenceNums });
|
|
4172
|
+
return;
|
|
4173
|
+
}
|
|
4174
|
+
answerJson(response, HTTP_STATUS.badGateway, { error: result.message });
|
|
4175
|
+
return;
|
|
4176
|
+
}
|
|
4034
4177
|
answerJson(response, HTTP_STATUS.notFound, { error: `no such control route: ${url.pathname}` });
|
|
4035
4178
|
};
|
|
4036
4179
|
handle().catch((error) => {
|
|
@@ -4245,7 +4388,10 @@ function frontDoorRcControl(transport, token) {
|
|
|
4245
4388
|
`${CONTROL_PATH_PREFIX}/answer`,
|
|
4246
4389
|
JSON.stringify({ session: sessionId, request: requestId, approve: decision.approve, ...decision.approve || decision.message === void 0 ? {} : { text: decision.message } }),
|
|
4247
4390
|
(status, message) => `answering control request ${requestId} on ${sessionId} failed (HTTP ${String(status)}): ${message}`
|
|
4248
|
-
)
|
|
4391
|
+
),
|
|
4392
|
+
interruptSession: async (sessionId) => await writeOf(`${CONTROL_PATH_PREFIX}/interrupt`, JSON.stringify({ session: sessionId }), (status, message) => `interrupting ${sessionId} failed (HTTP ${String(status)}): ${message}`),
|
|
4393
|
+
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}`),
|
|
4394
|
+
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}`)
|
|
4249
4395
|
};
|
|
4250
4396
|
}
|
|
4251
4397
|
|
|
@@ -4405,7 +4551,7 @@ function ensureFrontDoor(params) {
|
|
|
4405
4551
|
ports.stopSupervisor(supervisorPid);
|
|
4406
4552
|
while (ports.isRunning(supervisorPid)) {
|
|
4407
4553
|
if (ports.now() >= deadline) {
|
|
4408
|
-
throw new FrontDoorStartError(`
|
|
4554
|
+
throw new FrontDoorStartError(`the front door (pid ${String(supervisorPid)}, protocol ${String(state.protocol ?? 0)}) is older than this launcher (protocol ${String(FRONT_DOOR_PROTOCOL)}) and did not exit when asked. Daemon log: ${paths.frontdoorLogPath}`);
|
|
4409
4555
|
}
|
|
4410
4556
|
ports.sleep(FRONTDOOR_LAUNCHER_POLL_MS);
|
|
4411
4557
|
}
|
|
@@ -4419,13 +4565,13 @@ function ensureFrontDoor(params) {
|
|
|
4419
4565
|
}
|
|
4420
4566
|
lastRejection = `the listener on 127.0.0.1:${String(state.port)} (recorded for pid ${String(supervisorPid)}) failed authentication: ${verdict.reason}`;
|
|
4421
4567
|
if (spawned) {
|
|
4422
|
-
throw new FrontDoorStartError(
|
|
4568
|
+
throw new FrontDoorStartError(`${lastRejection}; refusing to send this session's credentials to it. Daemon log: ${paths.frontdoorLogPath}`);
|
|
4423
4569
|
}
|
|
4424
4570
|
distrusted.add(supervisorPid);
|
|
4425
4571
|
continue;
|
|
4426
4572
|
}
|
|
4427
4573
|
if (state?.lastError !== void 0) {
|
|
4428
|
-
throw new FrontDoorStartError(`
|
|
4574
|
+
throw new FrontDoorStartError(`the front door reported a fatal error and is not serving: ${state.lastError} (daemon log: ${paths.frontdoorLogPath})`);
|
|
4429
4575
|
}
|
|
4430
4576
|
} else if (!spawned) {
|
|
4431
4577
|
ports.fs.mkdirp(paths.frontdoorDir);
|
|
@@ -4445,7 +4591,7 @@ function ensureFrontDoor(params) {
|
|
|
4445
4591
|
if (ports.now() >= deadline) {
|
|
4446
4592
|
const lastError = state?.lastError;
|
|
4447
4593
|
throw new FrontDoorStartError(
|
|
4448
|
-
`
|
|
4594
|
+
`the front door did not become ready within ${String(FRONTDOOR_START_TIMEOUT_MS)}ms` + (lastError === void 0 ? "" : ` (last error: ${lastError})`) + (lastRejection === void 0 ? "" : ` (${lastRejection})`) + `. Daemon log: ${paths.frontdoorLogPath}`
|
|
4449
4595
|
);
|
|
4450
4596
|
}
|
|
4451
4597
|
ports.sleep(FRONTDOOR_LAUNCHER_POLL_MS);
|
|
@@ -4784,14 +4930,14 @@ function ensureHeadroom(params) {
|
|
|
4784
4930
|
const holders = sessionsForSupervisor(ports.fs, paths.headroomSessionsDir, state.supervisorPid).filter((session) => ports.isRunning(session.pid));
|
|
4785
4931
|
if (holders.length > 0) {
|
|
4786
4932
|
throw new HeadroomStartError(
|
|
4787
|
-
`
|
|
4933
|
+
`the running headroom daemon was started before this launch's front door address and would refuse every request this launch sends through it. It restarts once the launches using it end (pid ${holders.map((session) => String(session.pid)).join(", ")}); end them and launch again, or launch with --no-headroom.`
|
|
4788
4934
|
);
|
|
4789
4935
|
}
|
|
4790
4936
|
awaitingAllowlist = true;
|
|
4791
4937
|
}
|
|
4792
4938
|
if (state.lastError !== void 0) {
|
|
4793
4939
|
throw new HeadroomStartError(
|
|
4794
|
-
`
|
|
4940
|
+
`the headroom daemon reported a fatal error and is not serving: ${state.lastError} (daemon log: ${paths.headroomLogPath})`
|
|
4795
4941
|
);
|
|
4796
4942
|
}
|
|
4797
4943
|
} else if (!spawnedSupervisor) {
|
|
@@ -4815,7 +4961,7 @@ function ensureHeadroom(params) {
|
|
|
4815
4961
|
if (ports.now() >= deadline) {
|
|
4816
4962
|
const lastError = state?.lastError;
|
|
4817
4963
|
throw new HeadroomStartError(
|
|
4818
|
-
`
|
|
4964
|
+
`the headroom daemon did not become ready within ${String(HEADROOM_START_TIMEOUT_MS)}ms` + (lastError === void 0 ? "" : ` (last error: ${lastError})`) + (awaitingAllowlist ? " (the running daemon's allowlist does not admit this launch's front door, and it did not restart)" : "") + `. Daemon log: ${paths.headroomLogPath}`
|
|
4819
4965
|
);
|
|
4820
4966
|
}
|
|
4821
4967
|
ports.sleep(HEADROOM_LAUNCHER_POLL_MS);
|
|
@@ -9194,6 +9340,7 @@ export {
|
|
|
9194
9340
|
ProviderSchema,
|
|
9195
9341
|
RC_IDLE_EXPIRY_MS,
|
|
9196
9342
|
RC_PENDING_DEADLINE_MS,
|
|
9343
|
+
RC_PERMISSION_MODES,
|
|
9197
9344
|
RC_REQUEST_PARSE_CAP_BYTES,
|
|
9198
9345
|
RC_SESSIONS_PATH_PREFIX,
|
|
9199
9346
|
ROUTED_PATH_PREFIX,
|
|
@@ -9210,6 +9357,9 @@ export {
|
|
|
9210
9357
|
buildLayoutPaths,
|
|
9211
9358
|
buildRcControlResponsePayload,
|
|
9212
9359
|
buildRcEventWriteBody,
|
|
9360
|
+
buildRcInterruptPayload,
|
|
9361
|
+
buildRcSetModelPayload,
|
|
9362
|
+
buildRcSetPermissionModePayload,
|
|
9213
9363
|
buildRcUserMessagePayload,
|
|
9214
9364
|
carryOver,
|
|
9215
9365
|
checkReportHasWarnings,
|
|
@@ -9234,8 +9384,10 @@ export {
|
|
|
9234
9384
|
generateCa,
|
|
9235
9385
|
identityExists,
|
|
9236
9386
|
injectRcUserMessage,
|
|
9387
|
+
interruptRcSession,
|
|
9237
9388
|
isIdentityDirectoryName,
|
|
9238
9389
|
isInterceptedHost,
|
|
9390
|
+
isRcPermissionMode,
|
|
9239
9391
|
listDirectoryRules,
|
|
9240
9392
|
listIdentities,
|
|
9241
9393
|
listProfiles,
|
|
@@ -9291,6 +9443,8 @@ export {
|
|
|
9291
9443
|
setProfileEntries,
|
|
9292
9444
|
setProfileLaunchFlags,
|
|
9293
9445
|
setProfileMetadata,
|
|
9446
|
+
setRcSessionModel,
|
|
9447
|
+
setRcSessionPermissionMode,
|
|
9294
9448
|
snapshotPath,
|
|
9295
9449
|
startConnectServer,
|
|
9296
9450
|
topLevelNames,
|