feat: sandbox session tool visibility
This commit is contained in:
@@ -17,7 +17,37 @@ const SessionsHistoryToolSchema = Type.Object({
|
||||
includeTools: Type.Optional(Type.Boolean()),
|
||||
});
|
||||
|
||||
export function createSessionsHistoryTool(): AnyAgentTool {
|
||||
function resolveSandboxSessionToolsVisibility(
|
||||
cfg: ReturnType<typeof loadConfig>,
|
||||
) {
|
||||
return cfg.agent?.sandbox?.sessionToolsVisibility ?? "spawned";
|
||||
}
|
||||
|
||||
async function isSpawnedSessionAllowed(params: {
|
||||
requesterSessionKey: string;
|
||||
targetSessionKey: string;
|
||||
}): Promise<boolean> {
|
||||
try {
|
||||
const list = (await callGateway({
|
||||
method: "sessions.list",
|
||||
params: {
|
||||
includeGlobal: false,
|
||||
includeUnknown: false,
|
||||
limit: 500,
|
||||
spawnedBy: params.requesterSessionKey,
|
||||
},
|
||||
})) as { sessions?: Array<Record<string, unknown>> };
|
||||
const sessions = Array.isArray(list?.sessions) ? list.sessions : [];
|
||||
return sessions.some((entry) => entry?.key === params.targetSessionKey);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function createSessionsHistoryTool(opts?: {
|
||||
agentSessionKey?: string;
|
||||
sandboxed?: boolean;
|
||||
}): AnyAgentTool {
|
||||
return {
|
||||
label: "Session History",
|
||||
name: "sessions_history",
|
||||
@@ -30,11 +60,37 @@ export function createSessionsHistoryTool(): AnyAgentTool {
|
||||
});
|
||||
const cfg = loadConfig();
|
||||
const { mainKey, alias } = resolveMainSessionAlias(cfg);
|
||||
const visibility = resolveSandboxSessionToolsVisibility(cfg);
|
||||
const requesterInternalKey =
|
||||
typeof opts?.agentSessionKey === "string" && opts.agentSessionKey.trim()
|
||||
? resolveInternalSessionKey({
|
||||
key: opts.agentSessionKey,
|
||||
alias,
|
||||
mainKey,
|
||||
})
|
||||
: undefined;
|
||||
const resolvedKey = resolveInternalSessionKey({
|
||||
key: sessionKey,
|
||||
alias,
|
||||
mainKey,
|
||||
});
|
||||
const restrictToSpawned =
|
||||
opts?.sandboxed === true &&
|
||||
visibility === "spawned" &&
|
||||
requesterInternalKey &&
|
||||
!requesterInternalKey.toLowerCase().startsWith("subagent:");
|
||||
if (restrictToSpawned) {
|
||||
const ok = await isSpawnedSessionAllowed({
|
||||
requesterSessionKey: requesterInternalKey,
|
||||
targetSessionKey: resolvedKey,
|
||||
});
|
||||
if (!ok) {
|
||||
return jsonResult({
|
||||
status: "forbidden",
|
||||
error: `Session not visible from this sandboxed agent session: ${sessionKey}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
const limit =
|
||||
typeof params.limit === "number" && Number.isFinite(params.limit)
|
||||
? Math.max(1, Math.floor(params.limit))
|
||||
|
||||
@@ -44,7 +44,16 @@ const SessionsListToolSchema = Type.Object({
|
||||
messageLimit: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
});
|
||||
|
||||
export function createSessionsListTool(): AnyAgentTool {
|
||||
function resolveSandboxSessionToolsVisibility(
|
||||
cfg: ReturnType<typeof loadConfig>,
|
||||
) {
|
||||
return cfg.agent?.sandbox?.sessionToolsVisibility ?? "spawned";
|
||||
}
|
||||
|
||||
export function createSessionsListTool(opts?: {
|
||||
agentSessionKey?: string;
|
||||
sandboxed?: boolean;
|
||||
}): AnyAgentTool {
|
||||
return {
|
||||
label: "Sessions",
|
||||
name: "sessions_list",
|
||||
@@ -54,6 +63,20 @@ export function createSessionsListTool(): AnyAgentTool {
|
||||
const params = args as Record<string, unknown>;
|
||||
const cfg = loadConfig();
|
||||
const { mainKey, alias } = resolveMainSessionAlias(cfg);
|
||||
const visibility = resolveSandboxSessionToolsVisibility(cfg);
|
||||
const requesterInternalKey =
|
||||
typeof opts?.agentSessionKey === "string" && opts.agentSessionKey.trim()
|
||||
? resolveInternalSessionKey({
|
||||
key: opts.agentSessionKey,
|
||||
alias,
|
||||
mainKey,
|
||||
})
|
||||
: undefined;
|
||||
const restrictToSpawned =
|
||||
opts?.sandboxed === true &&
|
||||
visibility === "spawned" &&
|
||||
requesterInternalKey &&
|
||||
!requesterInternalKey.toLowerCase().startsWith("subagent:");
|
||||
|
||||
const kindsRaw = readStringArrayParam(params, "kinds")?.map((value) =>
|
||||
value.trim().toLowerCase(),
|
||||
@@ -86,8 +109,9 @@ export function createSessionsListTool(): AnyAgentTool {
|
||||
params: {
|
||||
limit,
|
||||
activeMinutes,
|
||||
includeGlobal: true,
|
||||
includeUnknown: true,
|
||||
includeGlobal: !restrictToSpawned,
|
||||
includeUnknown: !restrictToSpawned,
|
||||
spawnedBy: restrictToSpawned ? requesterInternalKey : undefined,
|
||||
},
|
||||
})) as {
|
||||
path?: string;
|
||||
|
||||
@@ -33,6 +33,7 @@ const SessionsSendToolSchema = Type.Object({
|
||||
export function createSessionsSendTool(opts?: {
|
||||
agentSessionKey?: string;
|
||||
agentSurface?: string;
|
||||
sandboxed?: boolean;
|
||||
}): AnyAgentTool {
|
||||
return {
|
||||
label: "Session Send",
|
||||
@@ -47,11 +48,64 @@ export function createSessionsSendTool(opts?: {
|
||||
const message = readStringParam(params, "message", { required: true });
|
||||
const cfg = loadConfig();
|
||||
const { mainKey, alias } = resolveMainSessionAlias(cfg);
|
||||
const visibility =
|
||||
cfg.agent?.sandbox?.sessionToolsVisibility ?? "spawned";
|
||||
const requesterInternalKey =
|
||||
typeof opts?.agentSessionKey === "string" && opts.agentSessionKey.trim()
|
||||
? resolveInternalSessionKey({
|
||||
key: opts.agentSessionKey,
|
||||
alias,
|
||||
mainKey,
|
||||
})
|
||||
: undefined;
|
||||
const resolvedKey = resolveInternalSessionKey({
|
||||
key: sessionKey,
|
||||
alias,
|
||||
mainKey,
|
||||
});
|
||||
const restrictToSpawned =
|
||||
opts?.sandboxed === true &&
|
||||
visibility === "spawned" &&
|
||||
requesterInternalKey &&
|
||||
!requesterInternalKey.toLowerCase().startsWith("subagent:");
|
||||
if (restrictToSpawned) {
|
||||
try {
|
||||
const list = (await callGateway({
|
||||
method: "sessions.list",
|
||||
params: {
|
||||
includeGlobal: false,
|
||||
includeUnknown: false,
|
||||
limit: 500,
|
||||
spawnedBy: requesterInternalKey,
|
||||
},
|
||||
})) as { sessions?: Array<Record<string, unknown>> };
|
||||
const sessions = Array.isArray(list?.sessions) ? list.sessions : [];
|
||||
const ok = sessions.some((entry) => entry?.key === resolvedKey);
|
||||
if (!ok) {
|
||||
return jsonResult({
|
||||
runId: crypto.randomUUID(),
|
||||
status: "forbidden",
|
||||
error: `Session not visible from this sandboxed agent session: ${sessionKey}`,
|
||||
sessionKey: resolveDisplaySessionKey({
|
||||
key: sessionKey,
|
||||
alias,
|
||||
mainKey,
|
||||
}),
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
return jsonResult({
|
||||
runId: crypto.randomUUID(),
|
||||
status: "forbidden",
|
||||
error: `Session not visible from this sandboxed agent session: ${sessionKey}`,
|
||||
sessionKey: resolveDisplaySessionKey({
|
||||
key: sessionKey,
|
||||
alias,
|
||||
mainKey,
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
const timeoutSeconds =
|
||||
typeof params.timeoutSeconds === "number" &&
|
||||
Number.isFinite(params.timeoutSeconds)
|
||||
|
||||
@@ -160,6 +160,7 @@ async function runSubagentAnnounceFlow(params: {
|
||||
export function createSessionsSpawnTool(opts?: {
|
||||
agentSessionKey?: string;
|
||||
agentSurface?: string;
|
||||
sandboxed?: boolean;
|
||||
}): AnyAgentTool {
|
||||
return {
|
||||
label: "Sessions",
|
||||
@@ -185,6 +186,15 @@ export function createSessionsSpawnTool(opts?: {
|
||||
const cfg = loadConfig();
|
||||
const { mainKey, alias } = resolveMainSessionAlias(cfg);
|
||||
const requesterSessionKey = opts?.agentSessionKey;
|
||||
if (
|
||||
typeof requesterSessionKey === "string" &&
|
||||
requesterSessionKey.trim().toLowerCase().startsWith("subagent:")
|
||||
) {
|
||||
return jsonResult({
|
||||
status: "forbidden",
|
||||
error: "sessions_spawn is not allowed from sub-agent sessions",
|
||||
});
|
||||
}
|
||||
const requesterInternalKey = requesterSessionKey
|
||||
? resolveInternalSessionKey({
|
||||
key: requesterSessionKey,
|
||||
@@ -199,6 +209,17 @@ export function createSessionsSpawnTool(opts?: {
|
||||
});
|
||||
|
||||
const childSessionKey = `subagent:${crypto.randomUUID()}`;
|
||||
if (opts?.sandboxed === true) {
|
||||
try {
|
||||
await callGateway({
|
||||
method: "sessions.patch",
|
||||
params: { key: childSessionKey, spawnedBy: requesterInternalKey },
|
||||
timeoutMs: 10_000,
|
||||
});
|
||||
} catch {
|
||||
// best-effort; scoping relies on this metadata but spawning still works without it
|
||||
}
|
||||
}
|
||||
const childSystemPrompt = buildSubagentSystemPrompt({
|
||||
requesterSessionKey,
|
||||
requesterSurface: opts?.agentSurface,
|
||||
|
||||
Reference in New Issue
Block a user