Let an agent complete tasks in an OpenAI-hosted browser.
Computer use lets an agent navigate websites and interact with browser interfaces
to test a website, collect information, or use an application through its UI.
The Agents API runs the browser in an OpenAI-hosted environment. Your application
starts the session and follows its events; the agent uses what it observes in
the browser to decide what to do next.
Set environment.type to openai_hosted and environment.desktop.enabled to true.
The JavaScript examples below form one walkthrough: create a session, handle
website approvals, then send a task and print the answer. Start by creating a
browser session with screenshots enabled. This does not start a task.
Create a browser session
JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22# Requires Bash and jq. Keep the session ID for follow-up requests.set -o pipefailif ! session_id=$(curl --silent --show-error --fail https://api.openai.com/v1/agents/sessions \ -H "OpenAI-Beta: agents=v1" \ -H "Authorization: Bearer $OPENAI_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "agent": { "model": "gpt-6-astra", "instructions": "Read public documentation in the browser. Do not sign in or change any website data. Report the page title and URL you find.", "tools": [{ "type": "computer_use", "include_screenshots": true }] }, "environment": { "type": "openai_hosted", "desktop": { "enabled": true }, "network": { "access": "enabled" } } }' | jq --exit-status --raw-output '.id // empty'); then echo "Session creation failed or its outcome is unknown. Do not retry automatically." >&2 exit 1fiprintf 'Session ID: %s\n' "$session_id"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18import OpenAI from"openai";import { open } from"node:fs/promises";constclient=newOpenAI();constsession=await client.beta.agents.sessions.create({ agent: { model: "gpt-6-astra", instructions:"Read public documentation in the browser. Do not sign in or change any website data. Report the page title and URL you find.", tools: [{ type: "computer_use", include_screenshots: true }], }, environment: { type: "openai_hosted", desktop: { enabled: true }, network: { access: "enabled" }, },});console.log("Session ID:", session.id);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21import base64import osfrom pathlib import Pathfrom openai import OpenAIclient = OpenAI()session = client.beta.agents.sessions.create( agent={ "model": "gpt-6-astra", "instructions": "Read public documentation in the browser. Do not sign in or change any website data. Report the page title and URL you find.", "tools": [{"type": "computer_use", "include_screenshots": True}], }, environment={ "type": "openai_hosted", "desktop": {"enabled": True}, "network": {"access": "enabled"}, },)print("Session ID:", session.id, flush=True)ready_to_delete = False
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31import ( "bufio" "context" "encoding/base64" "fmt" "os" "strings" "github.com/openai/openai-go/v3" "github.com/openai/openai-go/v3/option")ctx := context.Background()client := openai.NewClient()session, err := client.Beta.Agents.Sessions.New(ctx, openai.BetaAgentSessionNewParams{ Agent: openai.BetaAgentSessionNewParamsAgent{ Model: openai.String("gpt-6-astra"), Instructions: openai.String("Read public documentation in the browser. Do not sign in or change any website data. Report the page title and URL you find."), Tools: []openai.AgentToolParamUnion{{ OfParamComputerUse: &openai.AgentToolParamComputerUse{IncludeScreenshots: openai.Bool(true)}, }}, }, Environment: openai.EnvironmentParamUnion{OfParamOpenAIHosted: &openai.EnvironmentParamOpenAIHosted{ Desktop: openai.EnvironmentParamOpenAIHostedDesktop{Enabled: openai.Bool(true)}, Network: openai.EnvironmentParamOpenAIHostedNetwork{Access: "enabled"}, }},})if err != nil { return err}fmt.Println("Session ID:", session.ID)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27using System.ClientModel;using System.ClientModel.Primitives;using System.Text.Json;using OpenAI;using OpenAI.Agents;#pragma warning disable OPENAI001string key = Environment.GetEnvironmentVariable("OPENAI_API_KEY")!;OpenAIClientOptions options = new() { RetryPolicy = new ClientRetryPolicy(maxRetries: 0) };AgentClient client = new OpenAIClient(new ApiKeyCredential(key), options).GetAgentClient();AgentSession session = await client.CreateAgentSessionAsync( new AgentSessionCreationOptions { Agent = new SessionAgentConfigParam { Model = "gpt-6-astra", Instructions = "Read public documentation in the browser. Do not sign in or change any website data. Report the page title and URL you find.", Tools = [new AgentToolConfigParamComputerUse { IncludeScreenshots = true }], }, Environment = new EnvironmentParamOpenaiHosted { Desktop = new DesktopParam(true), Network = new NetworkPolicyParam(NetworkAccessParam.Enabled), }, });Console.WriteLine($"Session ID: {session.Id}");
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22require "base64"require "openai"client = OpenAI::Client.newsession = client.beta.agents.sessions.create( agent: { model: "gpt-6-astra", instructions: "Read public documentation in the browser. Do not sign in or change any website data. Report the page title and URL you find.", tools: [ { type: "computer_use", include_screenshots: true } ] }, environment: { type: "openai_hosted", desktop: { enabled: true }, network: { access: "enabled" } })puts "Session ID: #{session.id}"
Handle origin access
The browser requires the user’s approval before accessing each new website
origin, including public websites. Enabling network access does not approve
these requests.
To handle an origin approval:
On agent.session.requires_action, retrieve the session and inspect its current required_actions.
Find pending computer_use_approval_request entries whose nested request.type is browser_origin_access.
Show the requested origin and reason (if provided), then collect an approve, deny, or cancel decision. Submit it through the session events endpoint using the same request_id and a nested response containing type: "browser_origin_access" and decision, as shown below.
Origin approval does not enforce confirmation before individual actions
If your application must guarantee confirmation before purchases, destructive
changes, or other consequential actions, restrict the hosted browser to
resources that cannot perform them, or use a browser runtime you control.
Asking for confirmation through a function tool relies on the agent calling
that function.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43static void respondToOriginApproval( OpenAIClient client, String sessionId, AgentSession.RequiredAction.ComputerUseApprovalRequest approval) { var console = System.console(); if (console == null) throw new IllegalStateException("Run this example in an interactive terminal."); var result = AgentSessionInputComputerUseApprovalRequestResult.builder().requestId(approval.requestId()); if (approval.request().browserOriginAccess().isPresent()) { var request = approval.request().browserOriginAccess().get(); console.printf("Origin: %s%n", request.origin()); console.printf("Reason: %s%n", request.reason().orElse("Not supplied")); String decision; while (true) { String input = console.readLine("Allow browser access? [approve/deny/cancel; default deny] "); if (input == null) throw new IllegalStateException("Approval input closed."); decision = input.strip().toLowerCase(java.util.Locale.ROOT); if (decision.isEmpty()) decision = "deny"; if (List.of("approve", "deny", "cancel").contains(decision)) break; console.printf("Enter approve, deny, or cancel.%n"); } result.response( Response.BrowserOriginAccess.builder() .decision(Response.BrowserOriginAccess.Decision.of(decision)) .build()); } else if (approval.request().browserAuthentication().isPresent()) { console.printf("Sign-in is outside this public-page task; cancelling the request.%n"); result.response( Response.BrowserAuthentication.ofCancel( Response.BrowserAuthentication.Cancel.builder().build())); } else { throw new IllegalStateException("Unsupported computer-use approval request."); } client .withOptions(options -> options.maxRetries(0)) .beta() .agents() .sessions() .events() .create(EventCreateParams.builder().sessionId(sessionId).addEvent(result.build()).build());}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46static async Task RespondToOriginApprovalAsync( AgentClient client, string sessionId, SessionRequiredActionResourceComputerUseApprovalRequest approval){ if (Console.IsInputRedirected) { throw new InvalidOperationException("Run this example in an interactive terminal."); } ComputerUseApprovalResponseParam response; if (approval.Request is ComputerUseApprovalRequestKindResourceBrowserOriginAccess origin) { Console.WriteLine($"Origin: {origin.Origin}"); Console.WriteLine($"Reason: {origin.Reason ?? "Not supplied"}"); string choice; while (true) { Console.Write("Allow browser access? [approve/deny/cancel; default deny] "); choice = (Console.ReadLine() ?? throw new EndOfStreamException("Approval input closed.")).Trim().ToLowerInvariant(); if (choice.Length == 0) choice = "deny"; if (choice is "approve" or "deny" or "cancel") break; Console.WriteLine("Enter approve, deny, or cancel."); } BrowserOriginAccessDecisionParam decision = choice switch { "approve" => BrowserOriginAccessDecisionParam.Approve, "cancel" => BrowserOriginAccessDecisionParam.Cancel, _ => BrowserOriginAccessDecisionParam.Deny, }; response = new ComputerUseApprovalResponseParamBrowserOriginAccess(decision); } else if (approval.Request is ComputerUseApprovalRequestKindResourceBrowserAuthentication) { Console.WriteLine("Sign-in is outside this public-page task; cancelling the request."); response = new ComputerUseApprovalResponseParamBrowserAuthenticationCancel(); } else { throw new InvalidOperationException("Unsupported computer-use approval request."); } await client.CreateAgentSessionEventsAsync( sessionId, new CreateSessionEventsParams( [new SessionInputParamAgentSessionInputComputerUseApprovalRequestResult(approval.RequestId, response)] ) );}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40def respond_to_origin_approval(client, session_id, approval) request = approval.request case request.type.to_s when "browser_origin_access" puts request.reason || "The browser needs access to an origin." puts "Origin: #{request.origin}" decision = loop do print "Allow this origin? [approve/deny/cancel; default: deny] " input = $stdin.gets || raise(EOFError, "Input closed before an origin decision.") choice = input.strip.downcase choice = "deny" if choice.empty? break choice if ["approve", "deny", "cancel"].include?(choice) puts "Enter approve, deny, or cancel." end response = { type: "browser_origin_access", decision: decision } when "browser_authentication" puts "This public-page task does not sign in; cancelling the request." response = { type: "browser_authentication", action: "cancel" } else raise "Unsupported computer-use approval: #{request.type}" end client.beta.agents.sessions.events.create( session_id, events: [ { type: "agent.session.input.computer_use_approval_request_result", request_id: approval.request_id, response: response } ], request_options: { max_retries: 0 } )end
Keep the stream open and handle every pending approval. Use request_id to
track requests, and remove approval controls when a request is no longer in the
session’s required_actions. Cancelling an approval request does not cancel
the task.
A 202 response means the decision was accepted, not that navigation has
completed.
Run a browser task
Ask the agent to find the Agents API quickstart on the public developer site and
report its title and URL.
Open the event stream before sending the task so your application receives the
first progress events. Handle origin approvals as they
arrive to let the browser continue.
Send the task and follow its result
JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42# Terminal 1: use session_id from the creation request.# Keep this stream open. Wait for HTTP 200 before sending input.set -o pipefailcurl --silent --show-error --fail --no-buffer --dump-header - \ --suppress-connect-headers \ "https://api.openai.com/v1/agents/sessions/$session_id/events" \ -H "OpenAI-Beta: agents=v1" \ -H "Authorization: Bearer $OPENAI_API_KEY" \ -H "Accept: text/event-stream" \ | jq --raw-input --unbuffered ' if startswith("HTTP/") then . elif startswith("data:") then (ltrimstr("data:") | fromjson?) | if .type == "agent.session.turn.output_text.done" then {type, text} elif .type == "agent.session.requires_action" or .type == "error" or .type == "agent.session.failed" or .type == "agent.session.environment.failed" or (.type | test("^agent.session.turn.(completed|failed|cancelled)$")) then {type, turn_id: .turn.id, subagent_id: .turn.subagent_id} else empty end else empty end'# Terminal 2: replace sess_123 with the ID printed in terminal 1.# Export OPENAI_API_KEY in this terminal too.session_id="sess_123"curl --fail-with-body "https://api.openai.com/v1/agents/sessions/$session_id/events" \ -H "OpenAI-Beta: agents=v1" \ -H "Authorization: Bearer $OPENAI_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "events": [{ "type": "agent.session.input.message", "input": [{ "role": "user", "content": [{ "type": "input_text", "text": "Open https://developers.openai.com in the browser. Find the Agents API quickstart, then report its page title and URL." }] }] }] }'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54handled_requests = set()completed = Falsewith client.beta.agents.sessions.events.stream(session.id) as events: client.beta.agents.sessions.events.create( session.id, events=[ { "type": "agent.session.input.message", "input": [ { "role": "user", "content": [ { "type": "input_text", "text": "Open https://developers.openai.com in the browser. Find the Agents API quickstart, then report its page title and URL.", } ], } ], } ], ) for event in events: if event.type == "agent.session.requires_action": current = client.beta.agents.sessions.retrieve(session.id) for approval in current.required_actions: if ( approval.type == "computer_use_approval_request" and approval.request_id not in handled_requests ): respond_to_origin_approval(client, session.id, approval) handled_requests.add(approval.request_id) elif event.type == "agent.session.turn.output_text.done": print(event.text, flush=True) elif event.type == "agent.session.turn.completed": if event.turn.subagent_id is None: completed = True print() break elif event.type in { "agent.session.turn.failed", "agent.session.turn.cancelled", }: if event.turn.subagent_id is None: raise RuntimeError(f"Browser task ended: {event.type}") elif event.type == "error": raise RuntimeError(event.error.message) elif event.type in { "agent.session.failed", "agent.session.environment.failed", }: raise RuntimeError(f"Session failed: {event.type}") else: raise RuntimeError("Stream closed before the browser task finished.")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69handledRequests := map[string]bool{} events := client.Beta.Agents.Sessions.Events.StreamStreaming(ctx, session.ID) defer events.Close() if err := events.Err(); err != nil { return err } err = client.Beta.Agents.Sessions.Events.New(ctx, session.ID, openai.BetaAgentSessionEventNewParams{ Events: []openai.AgentSessionInputParamUnion{{ OfParamAgentSessionInputMessage: &openai.AgentSessionInputParamAgentSessionInputMessage{ Input: []openai.AgentSessionInputMessageParam{{ Role: "user", Content: []openai.InputContentParamUnion{{ OfParamInputText: &openai.InputContentParamInputText{ Text: "Open https://developers.openai.com in the browser. Find the Agents API quickstart, then report its page title and URL.", }, }}, }}, }, }}, }) if err != nil { return err } completed := falseeventLoop: for events.Next() { event := events.Current() switch event.Type { case "agent.session.requires_action": current, err := client.Beta.Agents.Sessions.Get(ctx, session.ID) if err != nil { return err } for _, action := range current.RequiredActions { if action.Type != "computer_use_approval_request" { continue } approval := action.AsComputerUseApprovalRequest() if handledRequests[approval.RequestID] { continue } if err := respondToOriginApproval(ctx, &client, session.ID, approval); err != nil { return err } handledRequests[approval.RequestID] = true } case "agent.session.turn.output_text.done": fmt.Println(event.Text) case "agent.session.turn.completed": if event.Turn.SubagentID == "" { completed = true break eventLoop } case "agent.session.turn.failed", "agent.session.turn.cancelled": if event.Turn.SubagentID == "" { return fmt.Errorf("browser task ended: %s", event.Type) } case "error": return fmt.Errorf("agent error: %s", event.Error.Message) case "agent.session.failed", "agent.session.environment.failed": return fmt.Errorf("session failed: %s", event.Type) } } if err := events.Err(); err != nil { return err } if !completed { return fmt.Errorf("stream closed before the browser task finished") }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56var handledRequests = new HashSet<String>();try (var events = client.beta().agents().sessions().events().streamStreaming(session.id())) { client .beta() .agents() .sessions() .events() .create( EventCreateParams.builder() .sessionId(session.id()) .addEvent( AgentSessionInputParam.AgentSessionInputMessage.builder() .addInput( AgentSessionInputMessageParam.builder() .addInputTextContent( "Open https://developers.openai.com in the browser. Find" + " the Agents API quickstart, then report its page" + " title and URL.") .build()) .build()) .build()); boolean completed = false; var iterator = events.stream().iterator(); while (iterator.hasNext()) { var event = iterator.next(); if (event.requiresAction().isPresent()) { var current = client.beta().agents().sessions().retrieve(session.id()); for (var action : current.requiredActions()) { if (action.computerUseApprovalRequest().isEmpty()) continue; var approval = action.computerUseApprovalRequest().get(); if (!handledRequests.contains(approval.requestId())) { respondToOriginApproval(client, session.id(), approval); handledRequests.add(approval.requestId()); } } } event.turnOutputTextDone().ifPresent(text -> System.out.println(text.text())); if (event.turnCompleted().filter(e -> e.turn().subagentId().isEmpty()).isPresent()) { completed = true; break; } if (event.turnFailed().filter(e -> e.turn().subagentId().isEmpty()).isPresent() || event.turnCancelled().filter(e -> e.turn().subagentId().isEmpty()).isPresent()) { throw new IllegalStateException("Browser task failed or was cancelled."); } if (event.error().isPresent()) { throw new IllegalStateException(event.error().get().error().message()); } if (event.failed().isPresent() || event.environmentFailed().isPresent()) { throw new IllegalStateException("The browser session failed."); } } if (!completed) { throw new IllegalStateException("Stream closed before the browser task finished."); }}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67HashSet<string> handledRequests = new(StringComparer.Ordinal);await using var events = await client.GetAgentSessionEventsAsync(session.Id);await client.CreateAgentSessionEventsAsync( session.Id, new CreateSessionEventsParams( [ new SessionInputParamAgentSessionInputMessage( [ new InputMessageParam( [new InputContentParamInputText("Open https://developers.openai.com in the browser. Find the Agents API quickstart, then report its page title and URL.")] ), ] ), ] ));bool completed = false;await foreach (var message in events){ using JsonDocument document = JsonDocument.Parse(message.Data.ToMemory()); JsonElement current = document.RootElement; string? type = current.GetProperty("type").GetString(); if (type == "agent.session.requires_action") { AgentSession latest = await client.RetrieveAgentSessionAsync(session.Id); foreach (SessionRequiredActionResource action in latest.RequiredActions) { if (action is SessionRequiredActionResourceComputerUseApprovalRequest approval && !handledRequests.Contains(approval.RequestId)) { await RespondToOriginApprovalAsync(client, session.Id, approval); handledRequests.Add(approval.RequestId); } } } else if (type == "agent.session.turn.output_text.done") { Console.WriteLine(current.GetProperty("text").GetString()); } else if (type is "agent.session.turn.completed" or "agent.session.turn.failed" or "agent.session.turn.cancelled") { JsonElement turn = current.GetProperty("turn"); if (turn.TryGetProperty("subagent_id", out JsonElement subagent) && subagent.ValueKind != JsonValueKind.Null) { continue; } if (type != "agent.session.turn.completed") { throw new InvalidOperationException($"Browser task ended: {type}"); } completed = true; break; } else if (type == "error") { throw new InvalidOperationException(current.GetProperty("error").GetProperty("message").GetString()); } else if (type is "agent.session.failed" or "agent.session.environment.failed") { throw new InvalidOperationException($"Session failed: {type}"); }}if (!completed){ throw new InvalidOperationException("Stream closed before the browser task finished.");}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52handled_requests = Set.newevents = client.beta.agents.sessions.events.stream_streaming(session.id)begin client.beta.agents.sessions.events.create( session.id, events: [ { type: "agent.session.input.message", input: [ { role: "user", content: [ { type: "input_text", text: "Open https://developers.openai.com in the browser. Find the Agents API quickstart, then report its page title and URL." } ] } ] } ] ) completed = events.any? do |event| case event when OpenAI::Beta::AgentSessionRequiresActionEvent current = client.beta.agents.sessions.retrieve(session.id) current.required_actions.each do |approval| next unless approval.is_a?(OpenAI::Beta::AgentSession::RequiredAction::ComputerUseApprovalRequest) next if handled_requests.include?(approval.request_id) respond_to_origin_approval(client, session.id, approval) handled_requests.add(approval.request_id) end false when OpenAI::Beta::AgentSessionTurnOutputTextDoneEvent puts event.text when OpenAI::Beta::AgentSessionTurnCompletedEvent event.turn.subagent_id.nil? when OpenAI::Beta::AgentSessionTurnFailedEvent, OpenAI::Beta::AgentSessionTurnCancelledEvent raise "Browser task ended: #{event.type}" if event.turn.subagent_id.nil? when OpenAI::Beta::AgentSessionErrorEvent raise event.error.message when OpenAI::Beta::AgentSessionFailedEvent, OpenAI::Beta::AgentSessionEnvironmentFailedEvent raise "Session failed: #{event.type}" else false end end raise "Stream closed before the browser task finished." unless completedensure events.closeend
The example prints the agent’s answer. Check that it includes the title and URL
of the quickstart.
Closing the event stream does not stop the task. To stop it, cancel the turn.
For connection failures or uncertain outcomes, follow
recovery guidance. The
expanded cURL example includes transport and error
diagnostics.
Follow browser activity
Browser operations appear as computer_use_call items in session output. Streamed
activity and saved session history use the same item shape.
Field
Meaning
id
The activity item’s identifier.
turn_id
The turn that produced the activity.
title
A description of the browser activity, or null.
status
in_progress, completed, failed, or incomplete.
output
Screenshot output, when available.
Use the title and status to show progress in your application. A browser activity
item describes a tool operation; it’s not the agent’s final answer or the
completion status of the whole turn. See
Events and items for the session event model.
Include screenshots
To display the browser’s progress in your application, set include_screenshots
to true on the computer_use tool. Screenshots are excluded from API output
by default; the agent can still observe them.
Each browser operation returns its last emitted screenshot in output, when
available:
Use image_url to render the screenshot. Some operations return output: null,
even with screenshots enabled, so your application should handle activity items
without an image.
Screenshots can contain sensitive page or account data. Show them only to
authorized users and keep them out of application logs.
Retrieve saved browser activity after the turn completes. The SDK examples save
the latest available screenshot to browser-screenshot.jpg.
Read browser activity
JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17# Save the first page privately; image data stays out of terminal output.activity_file=$(mktemp)curl --silent --show-error --fail-with-body --get \ "https://api.openai.com/v1/agents/sessions/$session_id/items" \ -H "OpenAI-Beta: agents=v1" \ -H "Authorization: Bearer $OPENAI_API_KEY" \ --data-urlencode "order=asc" --data-urlencode "limit=100" \ --output "$activity_file"jq '.data[] | select(.type == "computer_use_call") | {id, title: (.title // "Browser activity"), status, has_screenshot: (.output != null)}' "$activity_file"jq '{has_more, last_id}' "$activity_file"printf 'Saved activity JSON: %s\n' "$activity_file"# If has_more is true, repeat the GET with --data-urlencode "after=LAST_ID".# Use the returned last_id and keep order=asc on every page.
The SDK examples do not overwrite existing files. Move or remove
browser-screenshot.jpg before running them again.
Handle sign-in
Tasks such as reading issues in a private GitHub repository require an
authenticated browser. Your application handles sign-in so users can choose a
login method and enter credentials outside the chat.
Sign-in can involve several requests. For example, a site might ask the user to
choose email sign-in, enter an email address, and then enter a verification code.
Build your UI from each request’s login methods and fields.
Only the main agent can request browser authentication;
subagents cannot. This flow
supports email addresses, passwords, and verification codes, but not passkeys
or QR-code sign-in. Sites that require an unsupported method cannot complete
sign-in through this flow.
Keep the task’s event stream open and handle
origin approvals as they arrive. On
agent.session.requires_action, retrieve the session and look in its current
required_actions for computer_use_approval_request entries whose nested
request.type is browser_authentication.
Use the nested request to render your sign-in UI:
Field
How to use it
reason
Explain why input is needed, if provided. Can be null.
credential_origin
Show the destination the credentials are for. Can be null.
fields
Render inputs using each field’s id, label, type, and required values. Can be empty.
options
Show the available login methods. Each option has an id, label, and field_ids identifying its inputs.
If the request offers login methods, let the user choose one and show its
associated fields. Otherwise, show the request’s fields directly.
Ask users to enter credentials only for a destination they can verify. If the
credential origin is missing or unfamiliar and they cannot verify it
independently, cancel the authentication request.
Submit the user’s input using the outer action’s
request_id, or cancel the authentication request if they
decline. Continue handling requests as they arrive. Submitting a response does
not establish that sign-in succeeded; follow the task through completion and
check its result.
Example: Choose a method, then enter a code
A site offering email-code and password sign-in might first ask the user to
choose a method, without requesting any fields:
Choose a sign-in method
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15{"type": "computer_use_approval_request","turn_id": "turn_example","request_id": "request_choose_method","request": {"type": "browser_authentication","reason": "Choose how to sign in to the issue tracker","credential_origin": "https://issues.example.com","fields": [],"options": [ { "id": "email_code", "label": "Email me a code", "field_ids": [] }, { "id": "password", "label": "Use a password", "field_ids": [] } ] }}
If the user chooses email-code sign-in, submit selected_option: "email_code"
with fields: [] using this request’s request_id. The site may then request an
email address and verification code in separate requests. Render each new request
using its own fields and IDs, and include selected_option only when that request
offers options.
Return the user’s input
When the user completes a sign-in request, send their response through the
session events endpoint.
Set action to submit and use the request and field IDs from the pending
approval. For example, a response to a request for an email address looks like
this:
If the request offers login methods, include the chosen method’s ID in
response.selected_option. Submit only the fields listed in that method’s
field_ids, with a nonempty value for each required field. If the method has no
fields, send fields: [].
If no login methods are offered, omit selected_option and submit the request’s
fields directly. When the request lists fields, include at least one, even if all
are optional.
Send sign-in values only through this dedicated event. Submitted values stay
outside the agent’s model input and are omitted from authentication response
items in session history.
Treat every value, including email addresses, as sensitive. Mask entered values,
keep them out of logs, analytics, and saved UI state, and clear the form after
submission. Do not send credentials in ordinary messages or function-tool
results.
A 202 response with an empty body confirms that the submission was accepted,
not that sign-in succeeded. Continue following session events for further
requests or resumed work. Disable automatic HTTP or SDK retries for credential
submissions. If you’re unsure whether a submission was accepted,
refresh the session before continuing.
Let the user cancel
If the user declines to sign in, respond to the pending request with
action: "cancel". Use its request_id and omit fields and selected_option:
This cancels the authentication request. To stop the task itself, cancel the turn.
Run an authenticated browser task
Your application handles origin approvals and sign-in requests while following
session events. Define the helper below before running the task. It shows the
destination, collects input with entered values hidden, and submits the response.
If the user declines, it cancels the sign-in request.
Handle browser approvals and sign-in
JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15# Run in a second terminal when the private task requires input.# Set session_id to that task's actual session ID first.curl --silent --show-error --fail-with-body "https://api.openai.com/v1/agents/sessions/$session_id" \ -H "OpenAI-Beta: agents=v1" \ -H "Authorization: Bearer $OPENAI_API_KEY" \ | jq '.required_actions[] | select(.type == "computer_use_approval_request")'# Save a submission or cancellation payload from the preceding sections# to browser-auth-response.json using the pending request and field IDs.# Restrict file access to your user; remove it after submission.curl --fail-with-body --retry 0 "https://api.openai.com/v1/agents/sessions/$session_id/events" \ -H "OpenAI-Beta: agents=v1" \ -H "Authorization: Bearer $OPENAI_API_KEY" \ -H "Content-Type: application/json" \ --data-binary @browser-auth-response.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99from getpass import getpassdef read_authentication_response(request): cancel = {"type": "browser_authentication", "action": "cancel"} print(request.reason or "The agent needs you to sign in.") print("Credential origin:", request.credential_origin or "Not supplied") consent = input("Have you verified the sign-in destination? [y/N] ") if consent.strip().lower() not in {"y", "yes"}: return cancel selected_option = None active_fields = request.fields if request.options: for index, option in enumerate(request.options, start=1): print(f"{index}. {option.label}") while True: choice = input("Choose a sign-in method, or enter cancel: ").strip().lower() if choice == "cancel": return cancel if choice.isdigit() and 1 <= int(choice) <= len(request.options): option = request.options[int(choice) - 1] break print("Enter a method number from the list, or cancel.") selected_option = option.id fields_by_id = {field.id: field for field in request.fields} active_fields = [fields_by_id[field_id] for field_id in option.field_ids] values = [] while True: for field in active_fields: while True: label = field.label if field.required else f"{field.label} (optional)" value = getpass(f"{label} (leave blank for options): ") if value: values.append({"field_id": field.id, "value": value}) break choices = {"", "enter", "cancel"} if field.required: question = "Enter a value or cancel sign-in? [enter/cancel, default enter] " else: choices.add("skip") question = "Skip this field, enter a value, or cancel sign-in? [skip/enter/cancel, default skip] " while True: action = input(question).strip().lower() if action in choices: break if action == "cancel": return cancel if not field.required and action in {"", "skip"}: break if selected_option is not None or not active_fields or values: break print("This form requires at least one field. Enter a value or cancel sign-in.") response = { "type": "browser_authentication", "action": "submit", "fields": values, } if selected_option is not None: response["selected_option"] = selected_option return responsedef respond_to_computer_use_approval(client, session_id, approval): request = approval.request approval_client = client if request.type == "browser_origin_access": print(request.reason or "The browser needs access to an origin.") print("Origin:", request.origin) while True: decision = ( input("Allow this origin? [approve/deny/cancel; default: deny] ") .strip() .lower() or "deny" ) if decision in {"approve", "deny", "cancel"}: break print("Enter approve, deny, or cancel.") response = {"type": "browser_origin_access", "decision": decision} elif request.type == "browser_authentication": response = read_authentication_response(request) approval_client = client.with_options(max_retries=0) else: raise RuntimeError(f"Unsupported computer-use approval: {request.type}") approval_client.beta.agents.sessions.events.create( session_id, events=[ { "type": "agent.session.input.computer_use_approval_request_result", "request_id": approval.request_id, "response": response, } ], ) # Admission does not establish sign-in or navigation success; keep reading events.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203import ( "context" "fmt" "io" "os" "strconv" "strings" "github.com/openai/openai-go/v3" "github.com/openai/openai-go/v3/option" "golang.org/x/term")func respondToComputerUseApproval(ctx context.Context, client *openai.Client, sessionID string, approval openai.AgentSessionRequiredActionComputerUseApprovalRequest) error { send := func(response openai.AgentSessionInputParamAgentSessionInputComputerUseApprovalRequestResultResponseUnion) error { return client.Beta.Agents.Sessions.Events.New(ctx, sessionID, openai.BetaAgentSessionEventNewParams{ Events: []openai.AgentSessionInputParamUnion{{ OfParamAgentSessionInputComputerUseApprovalRequestResult: &openai.AgentSessionInputParamAgentSessionInputComputerUseApprovalRequestResult{ RequestID: approval.RequestID, Response: response, }, }}, }, option.WithMaxRetries(0)) } readLine := func(prompt string) (string, error) { fmt.Print(prompt) var value strings.Builder var input [1]byte for { if _, err := io.ReadFull(os.Stdin, input[:]); err != nil { return "", err } if input[0] == '\n' { return strings.TrimSpace(value.String()), nil } value.WriteByte(input[0]) } } cancelAuthentication := func() error { return send(openai.AgentSessionInputParamAgentSessionInputComputerUseApprovalRequestResultResponseUnion{ OfBrowserAuthentication: &openai.AgentSessionInputParamAgentSessionInputComputerUseApprovalRequestResultResponseBrowserAuthentication{Action: "cancel"}, }) } switch approval.Request.Type { case "browser_origin_access": request := approval.Request.AsBrowserOriginAccess() fmt.Println("Requested origin:", request.Origin) if request.Reason != "" { fmt.Println(request.Reason) } response := openai.AgentSessionInputParamAgentSessionInputComputerUseApprovalRequestResultResponseBrowserOriginAccess{Decision: "deny"} for { choice, err := readLine("Allow this origin? [approve/deny/cancel; default: deny] ") if err != nil { return err } switch strings.ToLower(choice) { case "approve": response.Decision = "approve" case "", "deny": response.Decision = "deny" case "cancel": response.Decision = "cancel" default: fmt.Println("Enter approve, deny, or cancel.") continue } break } return send(openai.AgentSessionInputParamAgentSessionInputComputerUseApprovalRequestResultResponseUnion{OfBrowserOriginAccess: &response}) case "browser_authentication": // Collect credentials only for an authentication request. default: return fmt.Errorf("unsupported computer-use approval: %s", approval.Request.Type) } challenge := approval.Request.AsBrowserAuthentication() reason := challenge.Reason if reason == "" { reason = "The agent needs you to sign in." } origin := challenge.CredentialOrigin if origin == "" { origin = "Not supplied" } fmt.Println(reason) fmt.Println("Credential origin:", origin) consent, err := readLine("Have you verified the sign-in destination? [y/N] ") if err != nil { return err } if strings.ToLower(consent) != "y" && strings.ToLower(consent) != "yes" { return cancelAuthentication() } response := openai.AgentSessionInputParamAgentSessionInputComputerUseApprovalRequestResultResponseBrowserAuthentication{ Action: "submit", Fields: []openai.AgentSessionInputParamAgentSessionInputComputerUseApprovalRequestResultResponseBrowserAuthenticationField{}, } activeFields := challenge.Fields if len(challenge.Options) > 0 { for index, option := range challenge.Options { fmt.Printf("%d. %s\n", index+1, option.Label) } for { choice, err := readLine("Choose a sign-in method [number/cancel]: ") if err != nil { return err } if strings.EqualFold(choice, "cancel") { return cancelAuthentication() } index, err := strconv.Atoi(choice) if err != nil || index < 1 || index > len(challenge.Options) { fmt.Println("Enter a method number from the list, or enter cancel.") continue } option := challenge.Options[index-1] response.SelectedOption = openai.String(option.ID) activeFields = nil for _, fieldID := range option.FieldIDs { found := false for _, field := range challenge.Fields { if field.ID == fieldID { activeFields = append(activeFields, field) found = true break } } if !found { return fmt.Errorf("sign-in method references an unknown field: %s", fieldID) } } break } }collectFields: for { response.Fields = response.Fields[:0] for _, field := range activeFields { fieldInput: for { choices := "enter/cancel; default: enter" if !field.Required { choices = "enter/skip/cancel; default: enter" } choice, err := readLine(fmt.Sprintf("%s [%s]: ", field.Label, choices)) if err != nil { return err } switch strings.ToLower(choice) { case "cancel": return cancelAuthentication() case "skip": if field.Required { fmt.Println("This field is required. Enter a value or cancel sign-in.") continue } break fieldInput case "", "enter": fmt.Printf("%s (hidden): ", field.Label) value, err := term.ReadPassword(int(os.Stdin.Fd())) fmt.Println() if err != nil { return err } if len(value) == 0 { fmt.Println("No value entered. Choose an action for this field.") continue } response.Fields = append(response.Fields, openai.AgentSessionInputParamAgentSessionInputComputerUseApprovalRequestResultResponseBrowserAuthenticationField{ FieldID: field.ID, Value: string(value), }) clear(value) break fieldInput default: fmt.Println("Choose one of the listed actions.") } } } if len(challenge.Options) > 0 || len(challenge.Fields) == 0 || len(response.Fields) > 0 { break } for { choice, err := readLine("Enter at least one field or cancel sign-in [retry/cancel; default: cancel]: ") if err != nil { return err } switch strings.ToLower(choice) { case "retry": continue collectFields case "", "cancel": return cancelAuthentication() default: fmt.Println("Enter retry or cancel.") } } } // Admission does not establish login success; keep following the session. return send(openai.AgentSessionInputParamAgentSessionInputComputerUseApprovalRequestResultResponseUnion{ OfBrowserAuthentication: &response, })}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161import com.openai.client.OpenAIClient;import com.openai.client.okhttp.OpenAIOkHttpClient;import com.openai.models.beta.agents.AgentSession;import com.openai.models.beta.agents.AgentSessionInputMessageParam;import com.openai.models.beta.agents.AgentSessionInputParam;import com.openai.models.beta.agents.AgentSessionInputParam.AgentSessionInputComputerUseApprovalRequestResult;import com.openai.models.beta.agents.AgentSessionInputParam.AgentSessionInputComputerUseApprovalRequestResult.Response;import com.openai.models.beta.agents.AgentToolParam;import com.openai.models.beta.agents.EnvironmentParam;import com.openai.models.beta.agents.sessions.SessionCreateParams;import com.openai.models.beta.agents.sessions.events.EventCreateParams;import java.util.Arrays;import java.util.HashSet;import java.util.List;static void cancelAuthentication(OpenAIClient client, String sessionId, String requestId) { var cancellation = AgentSessionInputComputerUseApprovalRequestResult.builder() .requestId(requestId) .response( Response.BrowserAuthentication.ofCancel( Response.BrowserAuthentication.Cancel.builder().build())) .build(); client .withOptions(options -> options.maxRetries(0)) .beta() .agents() .sessions() .events() .create(EventCreateParams.builder().sessionId(sessionId).addEvent(cancellation).build());}static void respondToComputerUseApproval( OpenAIClient client, String sessionId, AgentSession.RequiredAction.ComputerUseApprovalRequest approval) { var console = System.console(); if (console == null) throw new IllegalStateException("Run this example in an interactive terminal."); var result = AgentSessionInputComputerUseApprovalRequestResult.builder().requestId(approval.requestId()); if (approval.request().browserOriginAccess().isPresent()) { var request = approval.request().browserOriginAccess().get(); console.printf("Origin: %s%n", request.origin()); console.printf("Reason: %s%n", request.reason().orElse("Not supplied")); String decision; while (true) { String input = console.readLine("Allow browser access? [approve/deny/cancel; default deny] "); if (input == null) throw new IllegalStateException("Approval input closed."); decision = input.strip().toLowerCase(java.util.Locale.ROOT); if (decision.isEmpty()) decision = "deny"; if (List.of("approve", "deny", "cancel").contains(decision)) break; console.printf("Enter approve, deny, or cancel.%n"); } result.response( Response.BrowserOriginAccess.builder() .decision(Response.BrowserOriginAccess.Decision.of(decision)) .build()); } else if (approval.request().browserAuthentication().isPresent()) { var challenge = approval.request().browserAuthentication().get(); console.printf("%s%n", challenge.reason().orElse("The agent needs you to sign in.")); console.printf( "Credential origin: %s%n", challenge.credentialOrigin().orElse("Not supplied")); String consent = console.readLine("Have you verified the sign-in destination? [y/N] "); if (consent == null || !List.of("y", "yes").contains(consent.strip().toLowerCase(java.util.Locale.ROOT))) { cancelAuthentication(client, sessionId, approval.requestId()); return; } var response = Response.BrowserAuthentication.Submit.builder().fields(List.of()); var activeFields = challenge.fields(); if (!challenge.options().isEmpty()) { for (int i = 0; i < challenge.options().size(); i++) { console.printf("%d. %s%n", i + 1, challenge.options().get(i).label()); } while (true) { String choice = console.readLine("Choose a sign-in method, or enter cancel: "); if (choice == null || choice.strip().equalsIgnoreCase("cancel")) { cancelAuthentication(client, sessionId, approval.requestId()); return; } int index; try { index = Integer.parseInt(choice.strip()) - 1; } catch (NumberFormatException e) { console.printf("Enter a method number from the list, or cancel.%n"); continue; } if (index < 0 || index >= challenge.options().size()) { console.printf("Enter a method number from the list, or cancel.%n"); continue; } var option = challenge.options().get(index); response.selectedOption(option.id()); activeFields = challenge.fields().stream() .filter(field -> option.fieldIds().contains(field.id())) .toList(); break; } } while (true) { int fieldsSubmitted = 0; for (var field : activeFields) { while (true) { String choice = console.readLine( field.required() ? "%s [enter/cancel; default enter]: " : "%s [enter/skip/cancel; default skip]: ", field.label()); if (choice == null || choice.strip().equalsIgnoreCase("cancel")) { cancelAuthentication(client, sessionId, approval.requestId()); return; } choice = choice.strip().toLowerCase(java.util.Locale.ROOT); if (choice.isEmpty()) choice = field.required() ? "enter" : "skip"; if (choice.equals("skip") && !field.required()) break; if (!choice.equals("enter")) { console.printf("Choose one of the displayed options.%n"); continue; } char[] characters = console.readPassword("%s: ", field.label()); if (characters == null) { cancelAuthentication(client, sessionId, approval.requestId()); return; } String value = new String(characters); Arrays.fill(characters, '\0'); if (value.isEmpty()) { if (!field.required()) break; console.printf("This field is required.%n"); continue; } response.addField( Response.BrowserAuthentication.Submit.Field.builder() .fieldId(field.id()) .value(value) .build()); fieldsSubmitted++; break; } } if (!challenge.options().isEmpty() || activeFields.isEmpty() || fieldsSubmitted > 0) break; console.printf("Enter at least one field to submit this form, or cancel.%n"); } result.response(Response.BrowserAuthentication.ofSubmit(response.build())); } else { throw new IllegalStateException("Unsupported computer-use approval request."); } // An uncertain approval must not resend credentials automatically. client .withOptions(options -> options.maxRetries(0)) .beta() .agents() .sessions() .events() .create(EventCreateParams.builder().sessionId(sessionId).addEvent(result.build()).build()); // Admission does not establish sign-in or navigation success; keep following the session.}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176using System.ClientModel;using System.ClientModel.Primitives;using System.Globalization;using System.Text;using System.Text.Json;using OpenAI;using OpenAI.Agents;#pragma warning disable OPENAI001static string ReadLine(string prompt){ Console.Write(prompt); return Console.ReadLine() ?? "cancel";}static string ReadHidden(string prompt){ if (Console.IsInputRedirected) { throw new InvalidOperationException("Run this sign-in example in a terminal."); } Console.Write(prompt); StringBuilder value = new(); while (true) { ConsoleKeyInfo key = Console.ReadKey(intercept: true); if (key.Key == ConsoleKey.Enter) { Console.WriteLine(); return value.ToString(); } if (key.Key == ConsoleKey.Backspace) { if (value.Length > 0) { value.Length--; } } else if (!char.IsControl(key.KeyChar)) { value.Append(key.KeyChar); } }}static async Task RespondToComputerUseApprovalAsync( AgentClient client, string sessionId, SessionRequiredActionResourceComputerUseApprovalRequest approval){ if (Console.IsInputRedirected) { throw new InvalidOperationException("Run this example in an interactive terminal."); } ComputerUseApprovalResponseParam response; if (approval.Request is ComputerUseApprovalRequestKindResourceBrowserOriginAccess origin) { Console.WriteLine($"Origin: {origin.Origin}"); Console.WriteLine($"Reason: {origin.Reason ?? "Not supplied"}"); string choice; while (true) { Console.Write("Allow browser access? [approve/deny/cancel; default deny] "); choice = (Console.ReadLine() ?? throw new EndOfStreamException("Approval input closed.")).Trim().ToLowerInvariant(); if (choice.Length == 0) choice = "deny"; if (choice is "approve" or "deny" or "cancel") break; Console.WriteLine("Enter approve, deny, or cancel."); } BrowserOriginAccessDecisionParam decision = choice switch { "approve" => BrowserOriginAccessDecisionParam.Approve, "cancel" => BrowserOriginAccessDecisionParam.Cancel, _ => BrowserOriginAccessDecisionParam.Deny, }; response = new ComputerUseApprovalResponseParamBrowserOriginAccess(decision); } else if (approval.Request is ComputerUseApprovalRequestKindResourceBrowserAuthentication challenge) { async Task CancelAuthenticationAsync() { await client.CreateAgentSessionEventsAsync( sessionId, new CreateSessionEventsParams( [new SessionInputParamAgentSessionInputComputerUseApprovalRequestResult( approval.RequestId, new ComputerUseApprovalResponseParamBrowserAuthenticationCancel())] ) ); } Console.WriteLine(challenge.Reason ?? "The agent needs you to sign in."); Console.WriteLine($"Credential origin: {challenge.CredentialOrigin ?? "Not supplied"}"); string consent = ReadLine("Have you verified the sign-in destination? [y/N] ").Trim(); if (!consent.Equals("y", StringComparison.OrdinalIgnoreCase) && !consent.Equals("yes", StringComparison.OrdinalIgnoreCase)) { await CancelAuthenticationAsync(); return; } ComputerUseApprovalResponseParamBrowserAuthenticationSubmit submission = new([]); var activeFields = challenge.Fields.ToList(); if (challenge.Options.Count > 0) { for (int index = 0; index < challenge.Options.Count; index++) { Console.WriteLine($"{index + 1}. {challenge.Options[index].Label}"); } while (true) { string choice = ReadLine("Choose a sign-in method, or enter cancel: ").Trim(); if (choice.Equals("cancel", StringComparison.OrdinalIgnoreCase)) { await CancelAuthenticationAsync(); return; } if (!int.TryParse(choice, NumberStyles.None, CultureInfo.InvariantCulture, out int index) || index < 1 || index > challenge.Options.Count) { Console.WriteLine("Enter a method number from the list, or cancel."); continue; } var option = challenge.Options[index - 1]; submission.SelectedOption = option.Id; var fieldsById = challenge.Fields.ToDictionary(field => field.Id, StringComparer.Ordinal); activeFields = option.FieldIds.Select(id => fieldsById[id]).ToList(); break; } } while (true) { foreach (var field in activeFields) { while (true) { string choices = field.Required ? "enter/cancel; default enter" : "enter/skip/cancel; default skip"; string choice = ReadLine($"{field.Label} [{choices}]: ").Trim().ToLowerInvariant(); if (choice == "cancel") { await CancelAuthenticationAsync(); return; } if (choice.Length == 0) choice = field.Required ? "enter" : "skip"; if (choice == "skip" && !field.Required) break; if (choice != "enter") { Console.WriteLine("Choose one of the displayed options."); continue; } string value = ReadHidden($"{field.Label}: "); if (value.Length == 0) { if (!field.Required) break; Console.WriteLine("This field is required."); continue; } submission.Fields.Add(new BrowserAuthenticationFieldValueParam(field.Id, value)); break; } } if (challenge.Options.Count > 0 || activeFields.Count == 0 || submission.Fields.Count > 0) break; Console.WriteLine("Enter at least one field to submit this form, or cancel."); } response = submission; } else { throw new InvalidOperationException("Unsupported computer-use approval request."); } await client.CreateAgentSessionEventsAsync( sessionId, new CreateSessionEventsParams( [new SessionInputParamAgentSessionInputComputerUseApprovalRequestResult(approval.RequestId, response)] ) ); // Admission does not establish sign-in or navigation success; keep following the session.}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125require "io/console"def computer_use_sign_in_choice(prompt) print prompt input = $stdin.gets || raise(EOFError, "Input closed before a sign-in choice.") input.strip.downcaseenddef computer_use_authentication_response(request) cancel = { type: "browser_authentication", action: "cancel" } puts request.reason || "The agent needs you to sign in." puts "Credential origin: #{request.credential_origin || "Not supplied"}" consent = computer_use_sign_in_choice("Have you verified the sign-in destination? [y/N] ") return cancel unless ["y", "yes"].include?(consent) selected_option = nil active_fields = request.fields unless request.options.empty? request.options.each_with_index do |option, index| puts "#{index + 1}. #{option.label}" end option = loop do choice = computer_use_sign_in_choice("Choose a sign-in method [number/cancel]: ") return cancel if choice == "cancel" if choice.match?(/\A\d+\z/) && choice.to_i.between?(1, request.options.length) break request.options[choice.to_i - 1] end puts "Enter a method number from the list, or enter cancel." end selected_option = option.id fields_by_id = request.fields.to_h { |field| [field.id, field] } active_fields = option.field_ids.map { |field_id| fields_by_id.fetch(field_id) } end values = [] loop do values.clear active_fields.each do |field| loop do choices = field.required ? "enter/cancel; default: enter" : "enter/skip/cancel; default: enter" choice = computer_use_sign_in_choice("#{field.label} [#{choices}]: ") case choice when "cancel" return cancel when "skip" break unless field.required puts "This field is required. Enter a value or cancel sign-in." when "", "enter" value = $stdin.getpass("#{field.label} (hidden): ") if value.empty? puts "No value entered. Choose an action for this field." next end values << { field_id: field.id, value: value } break else puts "Choose one of the listed actions." end end end break unless request.options.empty? && !request.fields.empty? && values.empty? loop do choice = computer_use_sign_in_choice("Enter at least one field or cancel sign-in [retry/cancel; default: cancel]: ") return cancel if ["", "cancel"].include?(choice) break if choice == "retry" puts "Enter retry or cancel." end end response = { type: "browser_authentication", action: "submit", fields: values } response[:selected_option] = selected_option unless selected_option.nil? responseenddef respond_to_computer_use_approval(client, session_id, approval) request = approval.request case request.type.to_s when "browser_origin_access" puts request.reason || "The browser needs access to an origin." puts "Origin: #{request.origin}" decision = loop do print "Allow this origin? [approve/deny/cancel; default: deny] " input = $stdin.gets || raise(EOFError, "Input closed before an origin decision.") choice = input.strip.downcase choice = "deny" if choice.empty? break choice if ["approve", "deny", "cancel"].include?(choice) puts "Enter approve, deny, or cancel." end response = { type: "browser_origin_access", decision: decision } when "browser_authentication" response = computer_use_authentication_response(request) else raise "Unsupported computer-use approval: #{request.type}" end client.beta.agents.sessions.events.create( session_id, events: [ { type: "agent.session.input.computer_use_approval_request_result", request_id: approval.request_id, response: response } ], request_options: { max_retries: 0 } ) # Admission does not establish sign-in or navigation success; keep reading events.end
The following example asks the agent to read issues from a private GitHub
repository. Replace https://github.com/acme/private-repo/issues with an issue
page you can access.
Open the event stream before sending the task, and call the helper whenever the
session requires input.
Read private repository issues
JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24# Terminal 1: create a new session and stream its first task.# Replace the illustrative repository URL with one you can access.curl --no-buffer --fail-with-body https://api.openai.com/v1/agents/sessions \ -H "OpenAI-Beta: agents=v1" \ -H "Authorization: Bearer $OPENAI_API_KEY" \ -H "Content-Type: application/json" \ -H "Accept: text/event-stream" \ -d '{ "agent": { "model": "gpt-6-astra", "instructions": "Read the requested GitHub issue list in the browser. Request sign-in when needed. Do not create, edit, comment on, or close issues.", "tools": [{ "type": "computer_use", "include_screenshots": false }] }, "environment": { "type": "openai_hosted", "desktop": { "enabled": true }, "network": { "access": "enabled" } }, "input": "Open https://github.com/acme/private-repo/issues in the browser. Sign in if needed, then report the title and URL of the most recently updated open issue. Do not make changes.", "stream": true }'# Copy session.id from agent.session.created into terminal 2.# Keep this stream open while responding to origin and sign-in approvals there.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100import OpenAI from"openai";constclient=newOpenAI();constsession=await client.beta.agents.sessions.create({ agent: { model: "gpt-6-astra", instructions:"Read the requested GitHub issue list in the browser. Request sign-in when needed. Do not create, edit, comment on, or close issues.", tools: [{ type: "computer_use", include_screenshots: false }], }, environment: { type: "openai_hosted", desktop: { enabled: true }, network: { access: "enabled" }, },});console.log("Session ID:", session.id);let completed =false;let readyToDelete =false;try {constevents=await client.beta.agents.sessions.events.stream(session.id);consthandledRequests=newSet();try {await client.beta.agents.sessions.events.create(session.id, { events: [ { type: "agent.session.input.message", input: [ { role: "user", content: [ { type: "input_text",// Replace this illustrative URL with your private repository. text: "Open https://github.com/acme/private-repo/issues in the browser. Sign in if needed, then report the title and URL of the most recently updated open issue. Do not make changes.", }, ], }, ], }, ], });forawait (consteventof events) {switch (event.type) {case"agent.session.requires_action": {constcurrent=await client.beta.agents.sessions.retrieve( session.id );for (constapprovalof current.required_actions) {if ( approval.type ==="computer_use_approval_request"&&!handledRequests.has(approval.request_id) ) {awaitrespondToComputerUseApproval(client, session.id, approval); handledRequests.add(approval.request_id); } }break; }case"agent.session.turn.output_text.done": console.log(event.text);break;case"error":thrownewError(event.error.message);case"agent.session.failed":case"agent.session.environment.failed":thrownewError(`Agent lifecycle failure: ${event.type}`);case"agent.session.turn.failed":if (event.turn.subagent_id ===null) {thrownewError(event.turn.error?.message ??"Browser task failed"); }break;case"agent.session.turn.cancelled":if (event.turn.subagent_id ===null) {thrownewError("Browser task was cancelled"); }break;case"agent.session.turn.completed":if (event.turn.subagent_id ===null) completed =true;break; }if (completed) break; }if (!completed) {thrownewError("Stream closed before the browser task finished"); } console.log(); } finally { events.controller.abort(); } readyToDelete = completed;} catch (error) { console.error(`Session ${session.id} was kept. Use the same ID to check its status before trying again.` );throw error;} finally {if (readyToDelete) await client.beta.agents.sessions.delete(session.id);}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85from openai import OpenAIclient = OpenAI()session = client.beta.agents.sessions.create( agent={ "model": "gpt-6-astra", "instructions": "Read the requested GitHub issue list in the browser. Request sign-in when needed. Do not create, edit, comment on, or close issues.", "tools": [{"type": "computer_use", "include_screenshots": False}], }, environment={ "type": "openai_hosted", "desktop": {"enabled": True}, "network": {"access": "enabled"}, },)print("Session ID:", session.id, flush=True)handled_requests = set()completed = Falseready_to_delete = Falsetry: with client.beta.agents.sessions.events.stream(session.id) as events: client.beta.agents.sessions.events.create( session.id, events=[ { "type": "agent.session.input.message", "input": [ { "role": "user", "content": [ { "type": "input_text", # Replace this illustrative URL with your private repository. "text": "Open https://github.com/acme/private-repo/issues in the browser. Sign in if needed, then report the title and URL of the most recently updated open issue. Do not make changes.", } ], } ], } ], ) for event in events: if event.type == "agent.session.requires_action": current = client.beta.agents.sessions.retrieve(session.id) for approval in current.required_actions: if ( approval.type == "computer_use_approval_request" and approval.request_id not in handled_requests ): respond_to_computer_use_approval(client, session.id, approval) handled_requests.add(approval.request_id) elif event.type == "agent.session.turn.output_text.done": print(event.text, flush=True) elif event.type == "agent.session.turn.completed": if event.turn.subagent_id is None: completed = True print() break elif event.type in { "agent.session.turn.failed", "agent.session.turn.cancelled", }: if event.turn.subagent_id is None: raise RuntimeError(f"Browser task ended: {event.type}") elif event.type == "error": raise RuntimeError(event.error.message) elif event.type in { "agent.session.failed", "agent.session.environment.failed", }: raise RuntimeError(f"Session failed: {event.type}") else: raise RuntimeError("Stream closed before the browser task finished.") ready_to_delete = completedexcept (Exception, KeyboardInterrupt): print( f"Session {session.id} was kept. Use the same ID to check its status before trying again.", flush=True, ) raisefinally: if ready_to_delete: client.beta.agents.sessions.delete(session.id) client.close()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95ctx := context.Background()client := openai.NewClient()session, err := client.Beta.Agents.Sessions.New(ctx, openai.BetaAgentSessionNewParams{ Agent: openai.BetaAgentSessionNewParamsAgent{ Model: openai.String("gpt-6-astra"), Instructions: openai.String("Read the requested GitHub issue list in the browser. Request sign-in when needed. Do not create, edit, comment on, or close issues."), Tools: []openai.AgentToolParamUnion{{ OfParamComputerUse: &openai.AgentToolParamComputerUse{IncludeScreenshots: openai.Bool(false)}, }}, }, Environment: openai.EnvironmentParamUnion{OfParamOpenAIHosted: &openai.EnvironmentParamOpenAIHosted{ Desktop: openai.EnvironmentParamOpenAIHostedDesktop{Enabled: openai.Bool(true)}, Network: openai.EnvironmentParamOpenAIHostedNetwork{Access: "enabled"}, }},})if err != nil { return err}fmt.Println("Session ID:", session.ID)completed := falsedefer func() { if !completed { fmt.Fprintf(os.Stderr, "Session %s was not deleted. Retrieve it and check required_actions before attempting recovery.\n", session.ID) return } if _, err := client.Beta.Agents.Sessions.Delete(ctx, session.ID); err != nil { fmt.Fprintf(os.Stderr, "Could not delete completed session %s: %v\n", session.ID, err) }}()handledRequests := map[string]bool{}events := client.Beta.Agents.Sessions.Events.StreamStreaming(ctx, session.ID)defer events.Close()if err := events.Err(); err != nil { return err}err = client.Beta.Agents.Sessions.Events.New(ctx, session.ID, openai.BetaAgentSessionEventNewParams{ Events: []openai.AgentSessionInputParamUnion{{ OfParamAgentSessionInputMessage: &openai.AgentSessionInputParamAgentSessionInputMessage{ Input: []openai.AgentSessionInputMessageParam{{ Role: "user", Content: []openai.InputContentParamUnion{{ OfParamInputText: &openai.InputContentParamInputText{ // Replace this illustrative URL with your private repository. Text: "Open https://github.com/acme/private-repo/issues in the browser. Sign in if needed, then report the title and URL of the most recently updated open issue. Do not make changes.", }, }}, }}, }, }},})if err != nil { return err}for events.Next() { event := events.Current() switch event.Type { case "agent.session.requires_action": current, err := client.Beta.Agents.Sessions.Get(ctx, session.ID) if err != nil { return err } for _, action := range current.RequiredActions { if action.Type != "computer_use_approval_request" { continue } approval := action.AsComputerUseApprovalRequest() if handledRequests[approval.RequestID] { continue } if err := respondToComputerUseApproval(ctx, &client, session.ID, approval); err != nil { return err } handledRequests[approval.RequestID] = true } case "agent.session.turn.output_text.done": fmt.Println(event.Text) case "agent.session.turn.completed": if event.Turn.SubagentID == "" { completed = true return nil } case "agent.session.turn.failed", "agent.session.turn.cancelled": if event.Turn.SubagentID == "" { return fmt.Errorf("browser task ended: %s", event.Type) } case "error": return fmt.Errorf("agent error: %s", event.Error.Message) case "agent.session.failed", "agent.session.environment.failed": return fmt.Errorf("session failed: %s", event.Type) }}if err := events.Err(); err != nil { return err}return fmt.Errorf("stream closed before the browser task finished")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102var client = OpenAIOkHttpClient.fromEnv();var session = client .beta() .agents() .sessions() .create( SessionCreateParams.builder() .agent( SessionCreateParams.Agent.builder() .model("gpt-6-astra") .instructions( "Read the requested GitHub issue list in the browser. Request" + " sign-in when needed. Do not create, edit, comment on, or" + " close issues.") .addTool( AgentToolParam.ComputerUse.builder() .includeScreenshots(false) .build()) .build()) .environment( EnvironmentParam.OpenAIHosted.builder() .desktop( EnvironmentParam.OpenAIHosted.Desktop.builder() .enabled(true) .build()) .network( EnvironmentParam.OpenAIHosted.Network.builder() .access(EnvironmentParam.OpenAIHosted.Network.Access.ENABLED) .build()) .build()) .build());System.out.println("Session ID: " + session.id());var handledRequests = new HashSet<String>();try { try (var events = client.beta().agents().sessions().events().streamStreaming(session.id())) { client .beta() .agents() .sessions() .events() .create( EventCreateParams.builder() .sessionId(session.id()) .addEvent( AgentSessionInputParam.AgentSessionInputMessage.builder() .addInput( AgentSessionInputMessageParam.builder() // Replace this illustrative URL with your private repository. .addInputTextContent( "Open https://github.com/acme/private-repo/issues in the" + " browser. Sign in if needed, then report the title" + " and URL of the most recently updated open issue. Do" + " not make changes.") .build()) .build()) .build()); boolean completed = false; var iterator = events.stream().iterator(); while (iterator.hasNext()) { var event = iterator.next(); if (event.requiresAction().isPresent()) { var current = client.beta().agents().sessions().retrieve(session.id()); for (var action : current.requiredActions()) { if (action.computerUseApprovalRequest().isEmpty()) continue; var approval = action.computerUseApprovalRequest().get(); if (!handledRequests.contains(approval.requestId())) { respondToComputerUseApproval(client, session.id(), approval); handledRequests.add(approval.requestId()); } } } event.turnOutputTextDone().ifPresent(text -> System.out.println(text.text())); if (event.turnCompleted().filter(e -> e.turn().subagentId().isEmpty()).isPresent()) { completed = true; break; } if (event.turnFailed().filter(e -> e.turn().subagentId().isEmpty()).isPresent() || event.turnCancelled().filter(e -> e.turn().subagentId().isEmpty()).isPresent()) { throw new IllegalStateException("Browser task failed or was cancelled."); } if (event.error().isPresent()) { throw new IllegalStateException(event.error().get().error().message()); } if (event.failed().isPresent() || event.environmentFailed().isPresent()) { throw new IllegalStateException("The browser session failed."); } } if (!completed) { throw new IllegalStateException("Stream closed before the browser task finished."); } } catch (Exception error) { System.err.printf( "Session %s was kept. Retrieve it and check pending actions before sending anything" + " again.%n", session.id()); throw error; } client.beta().agents().sessions().delete(session.id());} finally { client.close();}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98string key = Environment.GetEnvironmentVariable("OPENAI_API_KEY")!;// An uncertain approval must not resend credentials automatically.OpenAIClientOptions options = new() { RetryPolicy = new ClientRetryPolicy(maxRetries: 0) };AgentClient client = new OpenAIClient(new ApiKeyCredential(key), options).GetAgentClient();AgentSession session = await client.CreateAgentSessionAsync( new AgentSessionCreationOptions { Agent = new SessionAgentConfigParam { Model = "gpt-6-astra", Instructions = "Read the requested GitHub issue list in the browser. Request sign-in when needed. Do not create, edit, comment on, or close issues.", Tools = [new AgentToolConfigParamComputerUse { IncludeScreenshots = false }], }, Environment = new EnvironmentParamOpenaiHosted { Desktop = new DesktopParam(true), Network = new NetworkPolicyParam(NetworkAccessParam.Enabled), }, });Console.WriteLine($"Session ID: {session.Id}");HashSet<string> handledRequests = new(StringComparer.Ordinal);try{ await using var events = await client.GetAgentSessionEventsAsync(session.Id); await client.CreateAgentSessionEventsAsync( session.Id, new CreateSessionEventsParams( [ new SessionInputParamAgentSessionInputMessage( [ new InputMessageParam( // Replace this illustrative URL with your private repository. [new InputContentParamInputText("Open https://github.com/acme/private-repo/issues in the browser. Sign in if needed, then report the title and URL of the most recently updated open issue. Do not make changes.")] ), ] ), ] ) ); bool completed = false; await foreach (var message in events) { using JsonDocument document = JsonDocument.Parse(message.Data.ToMemory()); JsonElement current = document.RootElement; string? type = current.GetProperty("type").GetString(); if (type == "agent.session.requires_action") { AgentSession latest = await client.RetrieveAgentSessionAsync(session.Id); foreach (SessionRequiredActionResource action in latest.RequiredActions) { if (action is SessionRequiredActionResourceComputerUseApprovalRequest approval && !handledRequests.Contains(approval.RequestId)) { await RespondToComputerUseApprovalAsync(client, session.Id, approval); handledRequests.Add(approval.RequestId); } } } else if (type == "agent.session.turn.output_text.done") { Console.WriteLine(current.GetProperty("text").GetString()); } else if (type is "agent.session.turn.completed" or "agent.session.turn.failed" or "agent.session.turn.cancelled") { JsonElement turn = current.GetProperty("turn"); if (turn.TryGetProperty("subagent_id", out JsonElement subagent) && subagent.ValueKind != JsonValueKind.Null) { continue; } if (type != "agent.session.turn.completed") { throw new InvalidOperationException($"Browser task ended: {type}"); } completed = true; break; } else if (type == "error") { throw new InvalidOperationException(current.GetProperty("error").GetProperty("message").GetString()); } else if (type is "agent.session.failed" or "agent.session.environment.failed") { throw new InvalidOperationException($"Session failed: {type}"); } } if (!completed) { throw new InvalidOperationException("Stream closed before the browser task finished."); }}catch{ Console.Error.WriteLine($"Session {session.Id} was kept. Retrieve it and check pending actions before sending anything again."); throw;}await client.DeleteAgentSessionAsync(session.Id);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87require "openai"client = OpenAI::Client.newsession = client.beta.agents.sessions.create( agent: { model: "gpt-6-astra", instructions: "Read the requested GitHub issue list in the browser. Request sign-in when needed. Do not create, edit, comment on, or close issues.", tools: [ { type: "computer_use", include_screenshots: false } ] }, environment: { type: "openai_hosted", desktop: { enabled: true }, network: { access: "enabled" } })puts "Session ID: #{session.id}"handled_requests = Set.newbegin events = client.beta.agents.sessions.events.stream_streaming(session.id) begin client.beta.agents.sessions.events.create( session.id, events: [ { type: "agent.session.input.message", input: [ { role: "user", content: [ { type: "input_text", # Replace this illustrative URL with your private repository. text: "Open https://github.com/acme/private-repo/issues in the browser. Sign in if needed, then report the title and URL of the most recently updated open issue. Do not make changes." } ] } ] } ] ) completed = events.any? do |event| case event when OpenAI::Beta::AgentSessionRequiresActionEvent current = client.beta.agents.sessions.retrieve(session.id) current.required_actions.each do |approval| next unless approval.is_a?(OpenAI::Beta::AgentSession::RequiredAction::ComputerUseApprovalRequest) next if handled_requests.include?(approval.request_id) respond_to_computer_use_approval(client, session.id, approval) handled_requests.add(approval.request_id) end false when OpenAI::Beta::AgentSessionTurnOutputTextDoneEvent puts event.text when OpenAI::Beta::AgentSessionTurnCompletedEvent event.turn.subagent_id.nil? when OpenAI::Beta::AgentSessionTurnFailedEvent, OpenAI::Beta::AgentSessionTurnCancelledEvent raise "Browser task ended: #{event.type}" if event.turn.subagent_id.nil? when OpenAI::Beta::AgentSessionErrorEvent raise event.error.message when OpenAI::Beta::AgentSessionFailedEvent, OpenAI::Beta::AgentSessionEnvironmentFailedEvent raise "Session failed: #{event.type}" else false end end raise "Stream closed before the browser task finished." unless completed ensure events.close endrescue StandardError, Interrupt warn "Session #{session.id} was not deleted. Retrieve it and check required_actions before attempting recovery." raiseelse begin client.beta.agents.sessions.delete(session.id) rescue => error warn "Could not delete completed session #{session.id}: #{error.message}" raise endend
Sign-in may require several requests, so keep handling approvals until the task
finishes. Check the agent’s result against the requested task—for this example,
verify the reported issue title and URL.
Read sign-in history
Authentication requests and accepted responses appear in session history and in
agent.session.turn.item.added events. Request items contain the sign-in form
metadata; response items record the accepted action without submitted credential
values.
Use this history to review past interactions. To determine whether a sign-in form
still needs input, retrieve the session’s current required_actions. A recorded
response does not establish that sign-in succeeded.
Origin approvals have no dedicated request or response history items. Handle
them through required_actions.
Ask the user a question
To ask for clarification or let the user make a choice during a task, define a
function tool in agent.tools.
For example, you could define request_user_response to present a question and
collect an answer. Your application supplies the tool’s name, argument schema,
and UI.
When you receive agent.session.requires_action, find the pending
function_call for your tool and use its arguments to display the question.
Return the answer through agent.session.input.tool_result, using the action’s
turn_id and call_id. Set success: true and put the answer in output,
serializing structured answers as a JSON string. If the user declines, return
success: false with an error message.
Continue following session events after returning the answer. The browser
approval helpers shown earlier handle origin access and sign-in; extend your
event handler to handle your question tool as well.
Function-tool results are visible to the model and saved in session history.
Collect passwords and verification codes through browser
authentication.
Recover approval handling
If your application disconnects or an approval response fails, retrieve the same
session and inspect its current required_actions before continuing. Rebuild
forms only for requests that are still pending, and remove controls for requests
that are no longer present.
For a disconnected event stream, follow
stream recovery
to resume receiving events. Reconnecting must not automatically resend the task
or an approval response.
Use the response status to decide what to do next:
Result
What your application should do
202
Clear submitted values and follow session events for the outcome.
400
Check the response type, selected option, field IDs, and required values against the pending request. Omit fields for cancellation and origin-access responses.
404
Check the session ID, request ID, and response type. Retrieve the session again; the request may no longer be available.
409
Refresh the session. The request may have expired, its turn may have ended, or a different response may already have been accepted.
Connection lost before acknowledgement
Treat acceptance as unknown. Reconnect and retrieve the session before deciding whether to retry.
Authentication requests expire after five minutes, and their owning turn can end
while the user is entering input. Refresh the session before restoring a sign-in
form.
If you retry an authentication submission, use the same request_id, selected
option, and field-value mapping. An identical retry does not fill the browser form
again. Changing values after a submission has been accepted returns 409; a new
sign-in attempt requires a new request from the agent.
For origin approvals, retry the same decision if delivery fails. Changing an
accepted decision returns 409. Origin requests remain pending for their owning
turn and do not have the five-minute authentication timeout.
Control network access
Use the hosted environment’s network configuration to control outbound access
for both the browser and code running in the environment. Allow the destination
website and any domains needed for page resources or redirects.
Origin approval is a separate user decision and does not override the network
policy. See
network access settings
to configure the environment.
Continue and clean up
Reuse the same session for follow-up tasks that need its browser state. Login
cookies can expire, and recycling the environment clears the browser state.
When you’re finished, retrieve any results you need and
delete the session to request
environment cleanup:
Delete the browser session
JavaScript
# Run after the root turn completes, fails, or is cancelled.curl --fail-with-body -X DELETE "https://api.openai.com/v1/agents/sessions/$session_id" \ -H "OpenAI-Beta: agents=v1" \ -H "Authorization: Bearer $OPENAI_API_KEY"
begin client.beta.agents.sessions.delete(session.id) if completedrescue => error warn "Could not delete completed session #{session.id}: #{error.message}" raiseend
Each submission can include up to six field values, with each field included
once. Values can contain up to 16,384 characters each; the serialized field
values and selected option must fit within 120 KiB.
Use the request and field IDs from the pending approval and provide a nonempty
value for each required field. See the
session events API reference
for the request schema.
Expanded cURL request and stream handling
Use this version when you need to distinguish transport failures, HTTP errors,
and invalid session-creation responses. It also filters streamed output and
reports error types and codes. It follows the same two-terminal workflow as the
first example; handle approvals in the second terminal as they arrive.
Create a session and inspect stream failures
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117create_browser_session() {unsetsession_idlocal result http_status body curl_statusif result=$(curl--silent--fail-with-body--write-out'\n%{http_code}'\https://api.openai.com/v1/agents/sessions\-H"OpenAI-Beta: agents=v1"\-H"Authorization: Bearer $OPENAI_API_KEY"\-H"Content-Type: application/json"\-d'{ "agent": { "model": "gpt-6-astra", "instructions": "Read public documentation in the browser. Do not sign in or change any website data. Report the page title and URL you find.", "tools": [{ "type": "computer_use", "include_screenshots": true }] }, "environment": { "type": "openai_hosted", "desktop": { "enabled": true }, "network": { "access": "enabled" } } }'); then curl_status=0; else curl_status=$?; fi http_status=$(printf'%s\n'"$result"|tail-n1) body=$(printf'%s\n'"$result"|sed'$d') [[ "$http_status"=~ ^[0-9]{3}$ ]] || http_status=000if [ "$curl_status"-ne0 ] || [[ !"$http_status"=~ ^2[0-9][0-9]$ ]]; thenprintf'%s'"$body"|jq--raw-input--slurp--compact-output\--argstatus"$http_status"--argcurl_status"$curl_status"' def identifier: if type == "string" and test("^[A-Za-z][A-Za-z0-9_]{0,79}$") then . else null end; (try fromjson catch {}) as $response | (if ($response | type) == "object" then $response.error // $response else {} end) as $error | if $curl_status != "0" and $curl_status != "22" then {status: $status, type: "transport_error", code: ("curl_" + $curl_status)} else {status: $status, type: ((try ($error.type | identifier) catch null) // "http_error"), code: (try ($error.code | identifier) catch null)} end'>&2return1fiif! session_id=$(printf'%s'"$body"|jq--exit-status--raw-output\'select(type == "object") | .id | select(type == "string" and length > 0)'2>/dev/null); thenunsetsession_idprintf'{"status":"%s","type":"invalid_response","code":"missing_session_id"}\n'"$http_status">&2return1fiprintf'Session ID: %s\n'"$session_id"}create_browser_session# Terminal 1: use session_id from the creation request.# Keep this stream open. Wait for HTTP 200 before sending input.set-opipefailcurl--silent--show-error--dump-header---suppress-connect-headers\--no-buffer--fail-with-body\"https://api.openai.com/v1/agents/sessions/$session_id/events"\-H"OpenAI-Beta: agents=v1"\-H"Authorization: Bearer $OPENAI_API_KEY"\-H"Accept: text/event-stream"\|jq--null-input--raw-input--compact-output--unbuffered' def identifier: if type == "string" and test("^[A-Za-z][A-Za-z0-9_]{0,79}$") then . else null end; def safe_error: if type == "object" then {type: (.type | identifier), code: (.code | identifier)} else null end; def public_event: if .type == "agent.session.turn.output_text.done" then {type, text} elif .type == "agent.session.turn.completed" or .type == "agent.session.turn.failed" or .type == "agent.session.turn.cancelled" then {type, turn_id: .turn.id, subagent_id: .turn.subagent_id, error: (.turn.error | safe_error)} elif .type == "error" or .type == "agent.session.failed" or .type == "agent.session.environment.failed" then {type, error: ((.error // .session.error // .environment.error) | safe_error)} elif .type == "agent.session.requires_action" then {type} else empty end; foreach inputs as $raw ( {body: "", headers: false, failed: false, output: []}; .output = [] | ($raw | rtrimstr([13] | implode)) as $line | if ($line | startswith("HTTP/")) then (try ($line | capture("^(?<protocol>HTTP/[0-9.]+) (?<status>[0-9]{3})(?: |$)")) catch null) as $http | if $http == null then . else .body = "" | .headers = true | .status = $http.status | .failed = (($http.status | tonumber) >= 300) | .output = [$http.protocol + " " + $http.status] end elif .headers then if $line == "" then .headers = false else . end elif ($line | startswith("data:")) then (try ($line | ltrimstr("data:") | fromjson) catch null) as $event | .output = [$event | select(type == "object") | public_event] elif .failed and .body != null then .body += ($line + "\n") | (try (.body | fromjson) catch null) as $body | if ($body | type) == "object" then (($body.error // $body) | safe_error) as $error | .output = [{status: .status, type: ($error.type // "http_error"), code: $error.code}] | .body = null else . end else . end; .output[])'# Terminal 2: replace sess_123 with the ID printed in terminal 1.# Export OPENAI_API_KEY in this terminal too.session_id="sess_123"curl--fail-with-body"https://api.openai.com/v1/agents/sessions/$session_id/events"\-H"OpenAI-Beta: agents=v1"\-H"Authorization: Bearer $OPENAI_API_KEY"\-H"Content-Type: application/json"\-d'{ "events": [{ "type": "agent.session.input.message", "input": [{ "role": "user", "content": [{ "type": "input_text", "text": "Open https://developers.openai.com in the browser. Find the Agents API quickstart, then report its page title and URL." }] }] }] }'