fix: refuse one-shot prompts on an adopted session (release review)

`POST /api/sessions/:id/run` had no adopted gate, so while the wrapper had no
attached PTY it spawned `claude -p --dangerously-skip-permissions <prompt>`
in /tmp, a launch every other adopted route refuses.

The route answers INVALID_INPUT, and `runPrompt()` refuses too, which also
covers the legacy `sendInput()` path and leaves no direct spawn in Session
that an adopted session can reach.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-11-06 07:10:49 +01:00
1 parent 7e70f6f0c3
commit 2c084966f1
6 files changed
+43 -5

No files matched your search

+6
View File
@@ -251,4 +251,10 @@ describe('the direct-PTY fallback', () => {
await expect(s[start]()).rejects.toThrow(/tmux/);
expect(ptySpawn).not.toHaveBeenCalled();
});
it('refuses a one-shot prompt, a new local claude rather than the agent in that pane', async () => {
const { s } = session('claude', { adopt });
await expect(s.runPrompt('hello')).rejects.toThrow(/adopted/i);
expect(ptySpawn).not.toHaveBeenCalled();
});
});
@@ -82,6 +82,27 @@ describe('adopted sessions refuse every auto-op that drives the pane', () => {
});
}
it('refuses a one-shot prompt, which would start a new local claude in /tmp', async () => {
const h = await createRouteTestHarness(registerSessionRoutes, { sessionId: SID });
(h.ctx._session as unknown as { isAdopted: boolean }).isAdopted = true;
const res = await h.app.inject({
method: 'POST',
url: `/api/sessions/${SID}/run`,
payload: { prompt: 'hello' },
});
expect(res.json().success).toBe(false);
expect(res.json().error).toMatch(/adopted/i);
expect(h.ctx._session.runPrompt).not.toHaveBeenCalled();
await h.app.close();
});
it('still runs a one-shot prompt on a session Codeman started', async () => {
const h = await createRouteTestHarness(registerSessionRoutes, { sessionId: SID });
await h.app.inject({ method: 'POST', url: `/api/sessions/${SID}/run`, payload: { prompt: 'hello' } });
expect(h.ctx._session.runPrompt).toHaveBeenCalledWith('hello');
await h.app.close();
});
it('refuses respawn, for a harder reason: it kills and restarts the pane', async () => {
const h = await createRouteTestHarness(registerRespawnRoutes, { sessionId: SID });
(h.ctx._session as unknown as { isAdopted: boolean }).isAdopted = true;