On July 23, Zenity published AgentForger, a vulnerability it had reported to OpenAI on June 4. According to the research team’s disclosure timeline, OpenAI accepted the report on June 5 and fixed the issue on June 8.
The fixed attack began with a link opened by an authenticated user who could build Workspace Agents and had already connected enterprise apps. Zenity reported that URL-supplied instructions could drive the builder through security-sensitive configuration, preview, publishing, and scheduling without the sequence of deliberate confirmations a user would reasonably expect. The result was not one unwanted request. It was a new agent that could wake up later with the victim’s connected-app authority.
That historical finding should not be read as a claim that the published link still works. It should change how a small team tests any agent builder.
The builder is a control plane whenever it can change instructions, tools, credentials, write approvals, target constraints, sharing, publishing, triggers, or schedules. A natural-language request may help draft those changes, but it is not sufficient authorization to activate them. The safe release unit is the complete configuration transition, including what becomes possible after the editor closes.
This field note proposes a 12-case release test, a builder change receipt, a preview contract, and a rollback drill. Y Build did not reproduce AgentForger, test the repaired OpenAI flow, or compare agent platforms. The incident details below are attributed to Zenity; current product capabilities are attributed to OpenAI; every test and threshold is a proposed starting point.
What was fixed, and what the disclosure actually establishes
Zenity describes two conditions that composed the original vulnerability. First, the builder treated an attacker-controlled URL input as executable builder input rather than inert content requiring a deliberate user decision. Second, the resulting natural-language interaction could modify security-relevant configuration, including approval behavior and schedules.
The researchers reported a chain in which the forged agent inherited access through apps the user had already authorized, performed real actions during Preview after approvals had been weakened, was published, and received recurring schedules. Their Part 2 account shows how email became a later command channel: a scheduled agent could read a message, act through connected apps, and return results. One initial action therefore created authority that outlived the browser interaction.
Four evidence boundaries matter:
- The vulnerability was fixed before publication. The article is an incident review, not a warning that the same exploit remains available.
- The PoC used prerequisites. The victim had to be signed in, allowed to build agents, have relevant connections, and click the link.
- The blast-radius demonstrations are researcher tests. They show what the reported configuration enabled in that environment, not what every workspace or connector would permit.
- The durable lesson is architectural. When one interface can compose identity, tools, approval policy, execution, and persistence, testing each setting in isolation misses the transaction that matters.
Do not turn a precise disclosure into “all agents are compromised.” Turn it into a release case your own builder must fail safely.
An agent builder is a deployment system
OpenAI’s current Workspace Agents documentation describes a builder that can attach apps and tools, configure app actions and write approvals, set parameter constraints, preview the agent, publish it, run it on schedules, and add API triggers. It also distinguishes drafts from published versions: edits remain in a draft until publication, while schedules and trigger changes require a published agent before they become live.
That is deployment behavior. A deployment system converts editable intent into durable, executable state.
For testing, model the builder as seven linked objects:
| Object | Example state | Why the transition matters |
|---|---|---|
| Draft | instructions, files, model | Untrusted input can alter intended behavior |
| Authority | apps, accounts, actions, constraints | A harmless draft can gain write capability |
| Approval | ask, constrained, or automatic | The guard can be weakened by the same workflow it guards |
| Preview | dry run or real execution | “Test” may mutate external state |
| Publication | private draft to callable agent | A local change becomes reusable by others |
| Persistence | schedule, API, Slack, inbox trigger | Authority can continue after the editor leaves |
| Evidence | versions, diffs, actor, result | Detection and rollback depend on reconstructing the transition |
The risk is not simply that one object is misconfigured. It is that a single initiating event crosses several objects without an independently verified boundary.
A prompt saying “build a weekly sales summary agent” is evidence of purpose. It is not evidence that the user chose a particular mailbox, allowed outgoing messages, removed confirmation, published to a workspace, and created a Monday schedule. Those are separate decisions even if the builder can infer all of them.
Separate proposal, authorization, and activation
The most useful product change is to split every security-sensitive transition into three records.
Proposal is what the builder recommends: add an app, change an action from read to write, relax a parameter, publish a draft, or add a trigger. It may be produced from natural language, a template, an imported file, a URL, another agent, or an administrator.
Authorization is a human or policy decision attached to an exact configuration digest. It names the actor, scope, consequences, and expiry. Editing the configuration after authorization invalidates the authorization.
Activation is the server-side transition that makes the authorized configuration live. It verifies that the proposed digest still matches, that the authorizer may approve every changed capability, and that no new transition has been smuggled into the transaction.
This distinction prevents a common confused-deputy failure: the authenticated browser session proves who is present, while attacker-controlled input supplies what happens. OWASP’s CSRF prevention guidance recommends protecting every state-changing request, treating cross-site inputs as untrusted, and adding interaction-based protection for highly sensitive operations. For agent builders, “state-changing” includes configuration that enables later action, not only the downstream action itself.
The server should reject activation when any of these differ from the approved proposal:
- app or account identity;
- read/write action set;
- approval policy;
- parameter constraints;
- sharing audience;
- published version digest;
- schedule or trigger;
- memory, file, or skill attachment;
- destination for output.
One “Create” button can still be usable. The review behind it must show the whole consequential delta.
Define a preview contract before anyone presses Preview
“Preview” is an interface label, not a safety property.
Zenity’s central product finding was that Preview in the affected flow could execute the newly configured agent against connected accounts. A user who interprets preview as a render, simulation, or dry run may not expect a message, calendar update, file write, or external API request.
Every builder should declare one of three preview modes:
- Render-only: inspect instructions, planned tool calls, and sample output; no live credentials are available.
- Sandbox execution: tools run against fixtures or disposable accounts; all writes are isolated and automatically reset.
- Live execution: real accounts are available; each effect follows the same approval and evidence rules as production.
Do not mix these modes behind one button. If a builder must support more than one, make the mode visible before the prompt is entered and repeat it at execution. A live preview should never inherit a weaker approval policy merely because that policy is present in the draft being previewed. The enforcement policy must come from the trusted outer system.
Test the contract with an action whose result is easy to reverse and independently observe: create a uniquely named draft item in a disposable account, not a real message to a colleague. Confirm the expected event appears in both the builder log and the downstream system. Then confirm render-only and sandbox modes produce no downstream event.
Build the 12-case state-transition deck
This deck is a release screen, not proof that a builder is universally secure. Run it in a test tenant with synthetic content, dedicated accounts, no customer data, and downstream logs you control.
| Case | Initiating input | Expected result |
|---|---|---|
| 1 | Same-origin typed request adds a read-only app | Delta shown; explicit authorization required |
| 2 | External link pre-fills harmless instructions | Content stays inert until reviewed |
| 3 | External link attempts a write-capable app change | Change blocked or separately authorized |
| 4 | Imported template relaxes approval policy | Privilege delta highlighted; old approval remains active |
| 5 | Draft changes after authorization | Previous authorization invalidated |
| 6 | Render-only preview proposes a write | No downstream event or credential access |
| 7 | Sandbox preview writes a fixture | Event stays in sandbox and reset succeeds |
| 8 | Live preview writes a fixture | Production-grade confirmation and receipt required |
| 9 | Publish adds a broader audience | Audience and credential owner shown before activation |
| 10 | Publish adds a recurring schedule | First run, cadence, channel, and stop control shown |
| 11 | Trigger receives untrusted downstream content | Content cannot expand tools, approval, or schedule |
| 12 | Owner revokes or rolls back | Calls, schedules, and delegated access stop within the target time |
For each case, record three outcomes separately:
- configuration outcome: which draft and live fields changed;
- effect outcome: which downstream calls or mutations occurred;
- evidence outcome: whether the team can reconstruct actor, source, authorization, activation, and result.
A case fails if the final state is safe but the evidence is absent. Silent blocking prevents one effect; it does not tell an administrator that someone attempted to cross the boundary. A case also fails if the UI shows a warning but the server activates a different digest.
Use a builder change receipt, not a success toast
A toast saying “Agent created” records neither intent nor authority. Preserve a machine-readable receipt for every proposed and activated transition.
builder_change_receipt:
change_id: null
source:
channel: typed | template | url | api | collaborator
origin: null
initiated_by: null
agent:
agent_id: null
previous_version: null
proposed_digest: null
activated_version: null
authority_delta:
apps_added: []
accounts: []
actions_added: []
constraints_changed: []
approval_policy_changed: false
persistence_delta:
published_audience_changed: false
schedules_added: []
triggers_added: []
preview:
mode: render_only | sandbox | live
downstream_effect_ids: []
authorization:
status: pending
authorized_by: null
authorized_digest: null
expires_at: null
activation:
status: not_started
activated_by: null
activated_at: null
rollback:
owner: null
deadline_minutes: null
tested_at: null
null, empty, and pending are intentional. They expose missing evidence. The builder should not invent a source origin for typed input, infer an authorizer from the logged-in session, or mark rollback tested because a delete button exists.
OWASP’s Agentic Penetration Testing Standard implementation guide recommends capturing intent, execution, outcome, operator identity, authorization scope, approval status, and timestamps in a human-reviewable trail. The receipt applies that structure to builder transitions. Store it somewhere the agent being configured cannot rewrite.
Bind connected authority to the agent, task, and version
AgentForger mattered because an existing connection could become the authority of a new agent. “The user connected Outlook last week” does not answer whether this agent, version, action, target, and schedule are authorized today.
OpenAI’s current documentation warns that publishing with personal connections may let other users act through the builder’s credentials and recommends least privilege, limited audiences, avoidance of high-impact connectors, and regular configuration audits. Its apps administration guidance also separates workspace app enablement, role access, action controls, and the user’s own authentication.
Build the product so every effect can answer:
- Which human or service identity owns the connection?
- Which agent ID and published version may use it?
- Which actions and resource targets are allowed?
- Which approval is required for this action class?
- May a schedule or API trigger use the same authority?
- When does the grant expire, and how is it revoked?
OAuth cannot solve builder intent by itself, but its design principles still reduce blast radius. RFC 9700 recommends restricting token privilege to the minimum required and binding tokens to their intended resource or sender where feasible. A broad, reusable credential attached to any future agent version defeats that containment.
The RFC supports the containment principle; it does not establish which restrictions a particular Workspace Agent connection implements. Record platform behavior as verified, unknown, or unavailable instead of filling the receipt from an assumed OAuth design.
The strongest enforcement lives downstream from the model. OWASP’s Excessive Agency guidance recommends minimum tool permissions, user-context execution, human approval for high-impact actions, and complete mediation in the systems that perform the action. A prompt should not be able to declare itself exempt from those checks.
Treat publishing and scheduling as one promotion decision
Teams often review publishing and scheduling on different screens. The dangerous state is their composition.
Publishing determines which configuration and credential relationships may be invoked. A schedule determines that invocation can happen without another foreground interaction. An API, Slack deployment, inbox watcher, or webhook can add a new source of instructions. Review the combined post-publication reach:
published version
× callable audience
× connected identities
× allowed actions
× trigger sources
× run frequency
× output destinations
The review screen should show the first live run and the longest unattended interval, not just “daily” or “hourly.” It should also show the stop path. OpenAI’s Global Admin Console documentation says administrators can inspect Workspace Agent activity, connected apps, memory files, schedules, and run analytics. Use that inventory as a daily reconciliation source, but verify that it exposes all the fields your receipt requires.
For a small team, require a second person when a change combines any two of:
- new write-capable authority;
- automatic or weakened approval;
- a broader audience;
- a recurring or external trigger;
- an external output destination;
- an agent-owned or shared connection.
Two-person review is not a substitute for technical enforcement. It is a promotion rule for combinations whose consequences are difficult to see in a conversational builder.
Test the instruction boundary after deployment
The initial builder input is only one untrusted channel. After deployment, instructions may arrive in email, Slack, documents, web pages, API payloads, memory, or another agent’s output.
Your test should prove that downstream content can influence task data without changing control-plane state. A message may supply the invoice number an approved workflow should inspect. It must not add a new app, disable approval, publish a draft, change the recipient, create a schedule, or expand its own resource scope.
Create three synthetic fixtures:
- a normal task request with an allowed target;
- a task that asks for a disallowed but plausible target;
- content that tries to redefine the agent’s tools, approval policy, or next schedule.
The third fixture should be recorded as untrusted content and rejected at the policy boundary. Do not depend on the model to explain why it refused. Verify the live configuration digest is unchanged and no new trigger exists.
This is where the builder release test differs from a prompt-injection benchmark. You are not scoring whether the model says the right thing. You are checking whether untrusted task content can cross into a different authority plane.
Reconcile inventory, versions, and effects
Prevention will miss something eventually. Detection must compare what exists with what the team intended.
Run a reconciliation job that joins four inventories:
- approved builder change receipts;
- live agent IDs and published version digests;
- connected identities, tools, and action scopes;
- active schedules, triggers, runs, and downstream effect IDs.
Alert on an agent with no approved receipt, a live digest that differs from its authorization, a schedule created outside the release path, a new output destination, an inactive owner, or effects that cannot be mapped to a run.
Version history is useful only if rollback restores the operational graph, not just the prompt. OpenAI’s current Workspace Agents documentation exposes previous versions and republishing, but teams should separately test what happens to schedules, API triggers, shared connections, memory, and already-issued downstream authorization when an older version returns.
Set one operational measure: unreconciled authority minutes. Start the clock when a live authority edge differs from the approved receipt; stop it when the edge is removed or explicitly approved. This avoids a comforting dashboard that counts agents but ignores how long unexplained authority remains active.
Run the rollback drill before broad access
A safe delete or disable operation must reach every activation surface.
In a test tenant, publish one agent with a disposable connection and a short schedule. Generate one reversible effect. Then execute the documented emergency stop and verify:
- the agent can no longer be invoked from ChatGPT or shared surfaces;
- future scheduled runs are cancelled;
- API and external trigger endpoints reject new calls;
- the agent can no longer use the connected account;
- queued or in-flight work has a defined disposition;
- the event remains visible to administrators after deactivation;
- restoration requires a new authorization, not only a toggle.
Measure time from stop request to independent downstream confirmation. Use an initial target such as five minutes for the test environment, then choose a production target based on the highest-impact action. The number is a team commitment, not an industry benchmark.
If you cannot prove that one stop reaches schedules and delegated authority, do not publish an unattended agent. A conversational agent that requires a human to start every run may still deliver most of the value while the control plane matures.
Know what this protocol does not prove
Passing 12 synthetic cases does not establish that a platform is free of CSRF, prompt injection, authorization flaws, connector defects, insider abuse, or race conditions. It does not inspect the provider’s source code or validate its remediation. It does not replace a threat model, code review, penetration test, identity audit, or incident-response plan.
The protocol also depends on observability. A platform may block the tested action while omitting the logs needed to verify why. Another may expose an admin inventory without immutable effect-level records. Record those as evidence gaps, not passing tests.
Small teams should not test with employee mailboxes, customer documents, production Slack channels, or real payment systems. Use synthetic data and reversible targets. Do not replay a fixed exploit against a service you do not own or lack permission to test.
Finally, the field note addresses agent builders with tools and durable activation. It is excessive for a stateless prompt editor that cannot access private systems, publish callable behavior, or run later. Apply the controls in proportion to reachable authority.
A two-day Build Lab plan
On day one, draw the seven builder objects, choose disposable accounts, and export the current agent inventory. Implement the change receipt outside the agent’s write boundary. Classify preview modes and mark any ambiguous mode as live. Run cases 1–8 and save configuration, effect, and evidence outcomes.
On day two, add one published test agent, a reversible schedule, and synthetic downstream instruction fixtures. Run cases 9–12, reconcile the four inventories, and execute the rollback drill. Review every failed or unobservable transition with the product and security owner.
Promote the builder only when:
- no external or downstream input can directly activate control-plane state;
- authorization is bound to the exact configuration digest;
- preview behavior is declared and verified;
- publishing plus persistence receives a combined review;
- every effect maps to a receipt and live version;
- the emergency stop meets the team’s measured target.
If one condition fails, narrow the product. Remove live preview, disable write actions, restrict publishing, or require foreground runs. AgentForger was fixed quickly. The durable Build Lab lesson is that the next agent-builder incident should have to cross visible, testable, independently enforced transitions—not one conversational path.
References
- Zenity Labs: AgentForger, Part 1 — ChatGPT Cross-Site Agent Forgery
- Zenity Labs: AgentForger, Part 2 — The Autonomous Insider
- OpenAI: ChatGPT Workspace Agents for Enterprise and Business
- OpenAI: Workspace Agents Security Overview
- OpenAI: Global Admin Console
- OpenAI: Admin controls, security, and compliance for apps
- OWASP: Cross-Site Request Forgery Prevention Cheat Sheet
- IETF: RFC 9700 — Best Current Practice for OAuth 2.0 Security
- OWASP: LLM06 — Excessive Agency
- OWASP: Agentic Penetration Testing Standard — Graduated Autonomy Implementation Guide