Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions lib/chat/runs/__tests__/provisionRunSession.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ vi.mock("@/lib/github/getServiceGithubToken", () => ({
vi.mock("@/lib/sandbox/markSessionSandboxActive", () => ({
markSessionSandboxActive: vi.fn(),
}));
vi.mock("@/lib/skills/discoverSkills", () => ({ discoverSkills: vi.fn(async () => []) }));
vi.mock("@/lib/skills/discoverSkills", () => ({ discoverSkills: vi.fn() }));
vi.mock("@/lib/skills/getSandboxSkillDirectories", () => ({
getSandboxSkillDirectories: vi.fn(async () => ["/skills"]),
}));
Expand All @@ -42,6 +42,7 @@ const sandbox = {
getState: () => ({ type: "vercel" }),
workingDirectory: "/work",
};
const platformApiSkill = { name: "recoup-platform-api-access" };

describe("provisionRunSession", () => {
beforeEach(() => {
Expand All @@ -53,6 +54,8 @@ describe("provisionRunSession", () => {
} as never);
vi.mocked(connectSandbox).mockResolvedValue(sandbox as never);
vi.mocked(markSessionSandboxActive).mockResolvedValue(updated as never);
// Default: the platform API-access skill is present after discovery.
vi.mocked(discoverSkills).mockResolvedValue([platformApiSkill] as never);
});

it("installs global skills into the sandbox before discovering them", async () => {
Expand All @@ -69,12 +72,21 @@ describe("provisionRunSession", () => {
expect(installOrder).toBeLessThan(discoverOrder);
});

it("still completes the run when skill install fails (best-effort)", async () => {
it("still completes the run when skill install fails but discovery finds the skill", async () => {
vi.mocked(installSessionGlobalSkills).mockRejectedValueOnce(new Error("install boom"));

const result = await provisionRunSession({ accountId: "account-1", title: "t" });

expect(result.session).toEqual(updated);
expect(discoverSkills).toHaveBeenCalled();
});

it("aborts the run when the platform API-access skill is missing after discovery", async () => {
vi.mocked(discoverSkills).mockResolvedValue([] as never);

// Fail closed: better a missed run than an ungrounded (fabricated) one (chat#1822).
await expect(provisionRunSession({ accountId: "account-1", title: "t" })).rejects.toThrow(
/recoup-platform-api-access/,
);
});
});
15 changes: 15 additions & 0 deletions lib/chat/runs/provisionRunSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ import type { SkillMetadata } from "@/lib/skills/skillTypes";

const SANDBOX_TIMEOUT_MS = ms("30m");

// The platform API-access skill the agent needs to reach the Recoup API for
// account data. If it's missing post-provision the run is aborted (chat#1822).
const REQUIRED_PLATFORM_API_SKILL = "recoup-platform-api-access";

export type ProvisionedRunSession = {
session: Tables<"sessions">;
chat: Tables<"chats">;
Expand Down Expand Up @@ -101,6 +105,17 @@ export async function provisionRunSession({
console.error("[provisionRunSession] skill discovery failed; using defaults:", error);
}

// Fail closed: if the platform API-access skill isn't available after install
// + discovery, abort instead of running an agent that can't reach the Recoup
// API — which guesses endpoints and fabricates ungrounded data (chat#1822).
// A missed run is recoverable; a fabricated report sent to a customer is not.
// The caller maps this throw to a 5xx and revokes any minted ephemeral key.
if (!skills.some(skill => skill.name === REQUIRED_PLATFORM_API_SKILL)) {
throw new Error(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Abort path leaks a provisioned active sandbox/session when required skill discovery fails. Add cleanup/failed-session handling before throwing, or move the validation before persisting ACTIVE state if possible.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/chat/runs/provisionRunSession.ts, line 114:

<comment>Abort path leaks a provisioned active sandbox/session when required skill discovery fails. Add cleanup/failed-session handling before throwing, or move the validation before persisting ACTIVE state if possible.</comment>

<file context>
@@ -101,6 +105,17 @@ export async function provisionRunSession({
+  // A missed run is recoverable; a fabricated report sent to a customer is not.
+  // The caller maps this throw to a 5xx and revokes any minted ephemeral key.
+  if (!skills.some(skill => skill.name === REQUIRED_PLATFORM_API_SKILL)) {
+    throw new Error(
+      `[provisionRunSession] required skill '${REQUIRED_PLATFORM_API_SKILL}' unavailable after install/discovery — aborting to avoid an ungrounded run`,
+    );
</file context>

`[provisionRunSession] required skill '${REQUIRED_PLATFORM_API_SKILL}' unavailable after install/discovery — aborting to avoid an ungrounded run`,
);
}

return {
session: updated,
chat,
Expand Down