-
Notifications
You must be signed in to change notification settings - Fork 1
feat: forward short-lived Privy access token per prompt to sandbox #13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -67,7 +67,7 @@ export async function POST(req: Request) { | |
| return parsedBody.response; | ||
| } | ||
|
|
||
| const { messages } = parsedBody.body; | ||
| const { messages, recoupAccessToken } = parsedBody.body; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Search for existing Privy/Recoup token verification or secret indirection helpers.
rg -n -C3 --type=ts \
'(verify.*(Privy|AccessToken|Jwt|JWT)|PrivyClient|@privy-io/server-auth|recoupAccessToken|secretRef|secret reference|workflow.*secret)' .Repository: recoupable/open-agents Length of output: 10057 🌐 Web query:
💡 Result: To verify Privy access tokens (also called auth tokens) server-side in TypeScript, use the official Citations:
🏁 Script executed: cat -n apps/web/app/api/chat/route.ts | sed -n '60,250p'Repository: recoupable/open-agents Length of output: 7572 🏁 Script executed: rg -n -A20 "export.*function parseChatRequestBody|const parseChatRequestBody" apps/web/app/api/chat/_lib/request.tsRepository: recoupable/open-agents Length of output: 657 🏁 Script executed: rg -n -B5 "interface ChatRequestBody|type ChatRequestBody" apps/web/app/api/chat/_lib/request.tsRepository: recoupable/open-agents Length of output: 161 🏁 Script executed: sed -n '1,30p' apps/web/app/api/chat/_lib/request.tsRepository: recoupable/open-agents Length of output: 774 🏁 Script executed: rg -n "recoupAccessToken" apps/web/app/api/chat/_lib/ -A3 -B3Repository: recoupable/open-agents Length of output: 531 Verify and bind the bearer token to userId before forwarding to the durable workflow. Line 70 extracts 🤖 Prompt for AI Agents |
||
|
|
||
| // 2. Require sessionId and chatId to ensure sandbox ownership verification | ||
| const chatIdentifiers = requireChatIdentifiers(parsedBody.body); | ||
|
|
@@ -236,6 +236,7 @@ export async function POST(req: Request) { | |
| : {}), | ||
| ...(skills.length > 0 && { skills }), | ||
| customInstructions: assistantFileLinkPrompt, | ||
| ...(recoupAccessToken ? { recoupAccessToken } : {}), | ||
| }, | ||
| ...(shouldAutoCommitPush && | ||
| sessionRecord.repoOwner && | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| import { getRecoupAccessToken } from "./get-recoup-access-token"; | ||
|
|
||
| /** | ||
| * Build a per-invocation env override carrying the Recoupable access | ||
| * token when one is available, so outbound shell commands (curl, scripts) | ||
| * can authenticate without the token persisting on the sandbox. | ||
| */ | ||
| export function buildRecoupExecEnv( | ||
| experimental_context: unknown, | ||
| ): { RECOUP_ACCESS_TOKEN: string } | undefined { | ||
| const token = getRecoupAccessToken(experimental_context); | ||
| if (!token) { | ||
| return undefined; | ||
| } | ||
|
|
||
| return { RECOUP_ACCESS_TOKEN: token }; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| import { isAgentContext } from "./utils"; | ||
|
|
||
| /** | ||
| * Read the per-prompt Recoupable access token from agent context. | ||
| * Returns undefined when no token is present (e.g. callers that do not | ||
| * authenticate against the Recoupable API). | ||
| */ | ||
| export function getRecoupAccessToken( | ||
| experimental_context: unknown, | ||
| ): string | undefined { | ||
| const context = isAgentContext(experimental_context) | ||
| ? experimental_context | ||
| : undefined; | ||
| return context?.recoupAccessToken; | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -371,6 +371,88 @@ describe("tools execute behavior", () => { | |||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| test("bashTool forwards recoupAccessToken to foreground exec only, never detached", async () => { | ||||||||||||||||||||||||||
| const execCalls: Array<{ | ||||||||||||||||||||||||||
| command: string; | ||||||||||||||||||||||||||
| cwd: string; | ||||||||||||||||||||||||||
| options?: { signal?: AbortSignal; env?: Record<string, string> }; | ||||||||||||||||||||||||||
| }> = []; | ||||||||||||||||||||||||||
| const detachedCalls: Array<{ command: string; cwd: string }> = []; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const sandbox = { | ||||||||||||||||||||||||||
| workingDirectory: "/repo", | ||||||||||||||||||||||||||
| exec: async ( | ||||||||||||||||||||||||||
| command: string, | ||||||||||||||||||||||||||
| cwd: string, | ||||||||||||||||||||||||||
| _timeoutMs: number, | ||||||||||||||||||||||||||
| options?: { signal?: AbortSignal; env?: Record<string, string> }, | ||||||||||||||||||||||||||
| ) => { | ||||||||||||||||||||||||||
| execCalls.push({ command, cwd, options }); | ||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||
| success: true, | ||||||||||||||||||||||||||
| exitCode: 0, | ||||||||||||||||||||||||||
| stdout: "", | ||||||||||||||||||||||||||
| stderr: "", | ||||||||||||||||||||||||||
| truncated: false, | ||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
| execDetached: async (command: string, cwd: string) => { | ||||||||||||||||||||||||||
| detachedCalls.push({ command, cwd }); | ||||||||||||||||||||||||||
| return { commandId: "cmd-detached" }; | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
|
Comment on lines
+399
to
+402
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: This test claims detached exec never gets the token, but it no longer asserts that Prompt for AI agents
Suggested change
|
||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const context = { | ||||||||||||||||||||||||||
| ...createContext(sandbox), | ||||||||||||||||||||||||||
| recoupAccessToken: "privy-jwt-abc", | ||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| await bashTool().execute?.( | ||||||||||||||||||||||||||
| { command: "curl https://developers.recoupable.com/ping" }, | ||||||||||||||||||||||||||
| executionOptions(context), | ||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| expect(execCalls).toHaveLength(1); | ||||||||||||||||||||||||||
| expect(execCalls[0]?.options?.env).toEqual({ | ||||||||||||||||||||||||||
| RECOUP_ACCESS_TOKEN: "privy-jwt-abc", | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Detached processes outlive the prompt — token must NOT be injected. | ||||||||||||||||||||||||||
| await bashTool().execute?.( | ||||||||||||||||||||||||||
| { command: "npm run dev", detached: true }, | ||||||||||||||||||||||||||
| executionOptions(context), | ||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| expect(detachedCalls).toHaveLength(1); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const contextWithoutToken = createContext({ | ||||||||||||||||||||||||||
| workingDirectory: "/repo", | ||||||||||||||||||||||||||
| exec: async ( | ||||||||||||||||||||||||||
| command: string, | ||||||||||||||||||||||||||
| cwd: string, | ||||||||||||||||||||||||||
| _timeoutMs: number, | ||||||||||||||||||||||||||
| options?: { signal?: AbortSignal; env?: Record<string, string> }, | ||||||||||||||||||||||||||
| ) => { | ||||||||||||||||||||||||||
| execCalls.push({ command, cwd, options }); | ||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||
| success: true, | ||||||||||||||||||||||||||
| exitCode: 0, | ||||||||||||||||||||||||||
| stdout: "", | ||||||||||||||||||||||||||
| stderr: "", | ||||||||||||||||||||||||||
| truncated: false, | ||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| await bashTool().execute?.( | ||||||||||||||||||||||||||
| { command: "ls" }, | ||||||||||||||||||||||||||
| executionOptions(contextWithoutToken), | ||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| expect(execCalls).toHaveLength(2); | ||||||||||||||||||||||||||
| expect(execCalls[1]?.options?.env).toBeUndefined(); | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| test("commandNeedsApproval flags only rm -rf commands", () => { | ||||||||||||||||||||||||||
| expect(commandNeedsApproval("ls -la")).toBe(false); | ||||||||||||||||||||||||||
| expect(commandNeedsApproval("git status --short")).toBe(false); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2:
messagesvalidation only checks that the value is an array, not that each item has the required message shape. Validate message elements to prevent malformed payloads from reaching workflow logic.Prompt for AI agents