Repository navigation
Keep framework runtime tools out of delegated app calls - #8140
Conversation
|
Here's a visual recap of what changed: Open the full interactive recap |
) Restrict direct MCP runtime exposure and gate the framework automation actions. Retain MCP provenance on definitions and require per-write approval for marked runs. Delegated containment follows in #8140 in the same release, with no Core publish between them.
|
The latest review confirms the hidden-action fixes in |
There was a problem hiding this comment.
Builder reviewed your changes and found 1 potential issue 🟡
Review Details
Code Review Summary
This update continues PR #8140’s effort to keep framework runtime tools out of delegated runs and to carry outside-caller provenance through automation management and approval replay. The approval ancestry changes in Factory save and Analytics schedule now use the shared origin-aware check, and the new replay/context coverage is a solid safeguard. The review threads for those two previously reported nested-approval issues were verified fixed and resolved.
Risk assessment: High — this PR changes approval and delegated execution boundaries. Both independent review passes identified a confirmed regression in the MCP-managed background automation action filter: it replaces the external-agent eligibility policy with only agentTool/uiOnly checks, so explicitly MCP-hidden actions and raw-query inputs can be exposed to unattended automation reads. The new runner spec exercises a mcpTool: false SQL action completing successfully, confirming the widened capability. This should be corrected before merge by retaining the external-agent/MCP eligibility checks while also excluding framework runtime actions.
The review environment could not run tests: setup failed during the toolkit TypeScript build. 🧪 Browser testing: Skipped — PR changes backend/action logic, prompts, and tests, with no user-facing UI impact.
#8140 landed the same isOutsideAgentCall helper and its server-entry export, with its own changeset. What's left here for Core is the browser-entry export. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#8071 made the A2A approval route read the owner's session with getBrowserCookieSession. The automation-approval spec from #8140, which merged first, still mocked only getSession, so on main the route found no session and its review request returned 401. The spec now mocks getBrowserCookieSession with the same session. This is the same hunk #8294 carries, so the two merge cleanly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ers (#8147) * Respect recording Trash and comment settings * Apply recording policies to comment reads * Apply Trash access to comment cleanup * Apply comment visibility settings to recording history * Let Content document readers play and comment on Clips uploaded to the document * Expire Content playback grants in five minutes and close review gaps * Test Content Clips workspace resolution, token time fields and lifetime edges * Reset core's workspace caches between Content Clips tests * Give each Content Clips workspace test its own reader * Accept only the Content-Clips peer secret and make signed-out host access opt-in Clips' Content routes now verify tokens with CONTENT_CLIPS_PEER_SECRET alone, a core config field read through resolveContentClipsPeerSecret. Tokens signed with the deployment A2A_SECRET or a workspace secret are refused, and an unset or unusable secret answers a typed 503 (content_clips_not_configured) without reading or writing anything. Media grants are signed with a key derived from the peer secret, through a new signingKey option on core's short-lived tokens. Host access resolvers are asked about signed-out requests only when they register with { includeSignedOut: true }; Clips is the only one that does. * Play Clips inside Content pages behind content.clip-embed A Clips link pasted into a video block's Link tab becomes a Clip block that plays in Content's own player, with the Clip's comments as markers on the scrubber. Content asks Clips for playback and comments over the peer contract, signed with the Content-Clips peer secret, and checks document access first, so signed-out readers of public pages can watch too. Registry blocks now keep their source on the clipboard, so a copied Clip pastes with its data instead of dropping out of the save. * Hide Clip blocks from the slash menu, add their parity row, and type unreadable Clips bodies * Evaluate the Clip flag for a signed-out reader in the page's org * Let a Clip's loading and error card fill the player's width * Keep the text toolbar off a selected registry block * Hold a registry block's place on a public page before its editor mounts, instead of showing its MDX source * Check every pasted non-Clips link in the browser, and keep a Clip's retry and comment failures visible * Read at most 500 comment threads from Clips, say when there are more, and keep Clip reads out of the browser's saved cache * Name each block on a public page before it loads, keep a Clip's place through a failed source, and check pasted blocks - The public page's first paint parses the saved page and shows each registry block by its saved title or registry label; a Clip is a link to its recording on Clips, built from the block and deployment config only. - A source swapped out before its metadata loaded no longer leaves its restore behind, and the next source restores the reader's place. - The toolkit reads a pasted block only through its data attributes, checks the type and id format, and keeps a pasted source only when the app reads it back. Content keeps one only when it is exactly one registered block of the declared type, and view-screen quotes only ids and types Content writes. * Keep a Clip's grant to its page, cap what Content reads from Clips, count only opened Clips against the rate limit, and drop pasted blocks with no source - The Clip block's player is keyed by page and recording, so a block that ends up under another page never keeps the last page's grant. - Content reads each Clips answer through a byte cap sized from the contract. Clips now cuts a listed comment's text to 280 characters and its author's name and email to 320, so the longest list has a known size. - Clips counts a request against its rate limit only after it has opened the recording, so made-up recording ids make no buckets. - With readPastedSource set, a pasted registry block with no source is dropped instead of saving as nothing. Changeset and Content changelog updated. * Limit made-up Clip ids per reader and page, and cut Clip titles Clips now counts requests whose recording doesn't open against a bucket per reader and page, and refuses them before any database read once it is spent, unless the Clip already opened for that reader. A comment list with no limit gets the most threads the contract allows, and the playback answer's title is cut to a length both contract files declare. * Count Clip misses per Clip, so one reader can't block another's first open A Clip's misses now share its own key, and a Clip is refused when either its opened bucket or its miss bucket is full. A Clip that opened once and then went away stays bounded, and made-up ids spend only their own misses. * Keep a playing Clip going when Clips misses a refresh, and bring it back on focus where the reader left off * Show unavailable state for missing shared recordings * Keep unavailable recordings out of share metadata * Explain replacement of share retry assertions * Respect recording lifecycle in bug report context * A full Clip rate-limit map drops its oldest bucket instead of refusing new readers * Keep recording reactions independent of comments * Apply recording Trash policy to activity recipients * Narrow recording context rows before lifecycle checks * Preserve comment cleanup and recording search availability * Read optional recording rows consistently across compiler checks * Format the recording search changelog entry * Align recording expiry checks across shared access * Match expiry timestamp precision across recording readers * Share expiry access rules with agent reads and Slack unfurls * Apply recording expiry across Clips readers * Check recording expiry when content completes * Enforce recording expiry at mutation boundaries * Revalidate recording expiry before delayed writes * Preserve expiry checks through queued and locked writes * Honor linked recording permissions in meeting reads * Remove unused meeting visibility test import * Recheck meeting editor access after finalization work * Meet main's strict-lint and compiler-strictness guards on Content Clips lines * Add #8129's attachment rule to the delegated-prompt snapshot #8140 landed without it * Name the prompt behavior the snapshot update records * Answer a blocked media URL under a Content grant with 502, not the expired-grant 403 * Meet main's strict-lint and compiler-strictness guards on the Clip block's lines * Answer a storage provider's 403 under a Content grant with 502 * Send a signed-out page reader's Clip comments without commenter emails * Let a comment author delete through a document whatever case their email was stored in * Declare what the three Clip actions in Content return * Set the mixed-case author without a non-null assertion * Count a Clip request against its limits from the check, so a burst can't all pass it before the first opens the Clip
* Expose Mail draft review to connected agents * Require fresh approval for draft deletion * Limit draft deletion approval to outside agents * Guard delegated draft deletes and preserve existing drafts * Align caller helper with shared approval policy * Cover core's isOutsideAgentCall re-export with an export-surface spec The changed-lines mutation check needs a spec named after every Core file a branch adds lines to, and index.ts had none. The spec also guards the export Mail's draft approval check imports. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Clean up a new Gmail draft when the Mail draft write throws Creating a draft saves it to Gmail first, then stores the Mail draft with a compare-and-set. A collision already removed the new Gmail draft, but a write that threw left it behind with nothing pointing at it. Now a throw rereads the key. Our own draft there means the write committed, so create succeeds. Anything else means it didn't, so the Gmail draft is removed and the original error is rethrown. If the reread fails too, the Gmail draft stays, since Mail may still point at it. Cleanup failures are logged instead of replacing the error that matters. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Export isOutsideAgentCall from core's browser entry too Browser builds resolve @agent-native/core to index.browser, which mirrors the action helpers but didn't have the new check, so a browser bundle importing it would miss the named export. action-caller has only a type import, so the browser entry can carry it. The export-surface spec now checks both entries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Give the agent-loop draft spec the compare-and-set create now uses The spec arrived from main with #8226 and mocks Core's app state without compareAndSetAppState, so the retried create threw instead of saving. The mock now inserts only when the key is empty, like the other spec. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Narrow the changeset to the browser export main didn't ship #8140 landed the same isOutsideAgentCall helper and its server-entry export, with its own changeset. What's left here for Core is the browser-entry export. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Add the attachment rule to the prompt-surface snapshot #8129 added the "Read the whole attachment" rule to the shared prompt and wrote it into agent-chat-plugin.surface.spec.ts.snap, which no spec reads. The spec agent-chat-plugin-surface.spec.ts checks agent-chat-plugin-surface.spec.ts.snap, so its full-prompt snapshot fails on main. This adds the one line to the snapshot that spec reads. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Say a kept Gmail draft exists when a draft's storage can't be confirmed When both the draft write and its reread fail, create keeps the Gmail draft it saved. Rethrowing the bare write error hid that, so a direct caller retrying could make a duplicate. The error now names the kept Gmail draft and asks the caller to check before creating again, with the write error as its cause. Core's agent loop already stops on this case because manage-draft isn't retry-safe. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Explain the prompt-surface snapshot change for the oracle guard The no-oracle-loosening guard flags any changed snapshot body without a reason. The full-prompt snapshot changed only because #8129's new attachment rule never reached this snapshot file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Reread a thrown draft write for every create, not only Gmail-backed ones A create with no Gmail draft rethrew a failed compare-and-set without checking whether it had committed, so a stored draft could be reported as failed, and a retry with a generated ID would make a duplicate. The store-and-reread now lives in storeNewComposeDraft and covers every create. A Gmail draft ID decides ownership when there is one; otherwise only the exact draft this call wrote counts. An unreadable outcome names the draft to check either way. Mail's ES2020 lib has no Error options, so the uncertainty error sets cause directly instead of through new Error's second argument. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Report an unconfirmed draft store as a typed Mail error The no-bare-error-in-actions guard requires actions to fail through fail() rather than a bare Error. When the draft write throws and the reread also fails, create now fails with draft_store_unconfirmed (503) and details naming the draft and any Gmail draft it kept. The original write error is logged, since ES2020's Error has no cause option. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Name the Gmail draft left behind when its cleanup fails When create can't store its draft and removing the Gmail draft it saved fails too, the caller had no way to learn that an unsent draft stayed in Gmail. The cleanup helper now reports whether it removed the draft. A collision keeps its draft_already_exists (409) and adds the leftover Gmail draft's ID to the message and details. A failed write that leaves a Gmail draft fails with draft_store_failed (503) naming it, and logs the original write error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Prove a forged frontend header can't delete every Mail draft manage-draft lets in-app calls delete drafts without approval, and the action route used to take a client-set x-agent-native-frontend header as proof of an in-app call. #8071 made the route require the UI's own proof. This spec sends delete-all through the real action route with that header. Without UI proof the route refuses it (403 ui_capability_required); with a bearer token it needs approval (409 approval_required). Either way no draft is read or deleted. Against core built before #8071, both requests deleted the drafts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Mock the browser session the A2A approval route now reads #8071 made the A2A approval route read the owner's session with getBrowserCookieSession. The automation-approval spec from #8140, which merged first, still mocked only getSession, so on main the route found no session and its review request returned 401. The spec now mocks getBrowserCookieSession with the same session. This is the same hunk #8294 carries, so the two merge cleanly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…e document (BuilderIO#8090) * Respect recording Trash and comment settings * Apply recording policies to comment reads * Apply Trash access to comment cleanup * Apply comment visibility settings to recording history * Let Content document readers play and comment on Clips uploaded to the document * Expire Content playback grants in five minutes and close review gaps * Test Content Clips workspace resolution, token time fields and lifetime edges * Reset core's workspace caches between Content Clips tests * Give each Content Clips workspace test its own reader * Accept only the Content-Clips peer secret and make signed-out host access opt-in Clips' Content routes now verify tokens with CONTENT_CLIPS_PEER_SECRET alone, a core config field read through resolveContentClipsPeerSecret. Tokens signed with the deployment A2A_SECRET or a workspace secret are refused, and an unset or unusable secret answers a typed 503 (content_clips_not_configured) without reading or writing anything. Media grants are signed with a key derived from the peer secret, through a new signingKey option on core's short-lived tokens. Host access resolvers are asked about signed-out requests only when they register with { includeSignedOut: true }; Clips is the only one that does. * Show unavailable state for missing shared recordings * Keep unavailable recordings out of share metadata * Explain replacement of share retry assertions * Respect recording lifecycle in bug report context * Keep recording reactions independent of comments * Apply recording Trash policy to activity recipients * Narrow recording context rows before lifecycle checks * Preserve comment cleanup and recording search availability * Read optional recording rows consistently across compiler checks * Format the recording search changelog entry * Align recording expiry checks across shared access * Match expiry timestamp precision across recording readers * Share expiry access rules with agent reads and Slack unfurls * Apply recording expiry across Clips readers * Check recording expiry when content completes * Enforce recording expiry at mutation boundaries * Revalidate recording expiry before delayed writes * Preserve expiry checks through queued and locked writes * Honor linked recording permissions in meeting reads * Remove unused meeting visibility test import * Recheck meeting editor access after finalization work * Meet main's strict-lint and compiler-strictness guards on Content Clips lines * Add BuilderIO#8129's attachment rule to the delegated-prompt snapshot BuilderIO#8140 landed without it * Name the prompt behavior the snapshot update records * Answer a blocked media URL under a Content grant with 502, not the expired-grant 403 * Answer a storage provider's 403 under a Content grant with 502
…e page (#8182) * Respect recording Trash and comment settings * Apply recording policies to comment reads * Apply Trash access to comment cleanup * Apply comment visibility settings to recording history * Let Content document readers play and comment on Clips uploaded to the document * Expire Content playback grants in five minutes and close review gaps * Test Content Clips workspace resolution, token time fields and lifetime edges * Reset core's workspace caches between Content Clips tests * Give each Content Clips workspace test its own reader * Accept only the Content-Clips peer secret and make signed-out host access opt-in Clips' Content routes now verify tokens with CONTENT_CLIPS_PEER_SECRET alone, a core config field read through resolveContentClipsPeerSecret. Tokens signed with the deployment A2A_SECRET or a workspace secret are refused, and an unset or unusable secret answers a typed 503 (content_clips_not_configured) without reading or writing anything. Media grants are signed with a key derived from the peer secret, through a new signingKey option on core's short-lived tokens. Host access resolvers are asked about signed-out requests only when they register with { includeSignedOut: true }; Clips is the only one that does. * Play Clips inside Content pages behind content.clip-embed A Clips link pasted into a video block's Link tab becomes a Clip block that plays in Content's own player, with the Clip's comments as markers on the scrubber. Content asks Clips for playback and comments over the peer contract, signed with the Content-Clips peer secret, and checks document access first, so signed-out readers of public pages can watch too. Registry blocks now keep their source on the clipboard, so a copied Clip pastes with its data instead of dropping out of the save. * Hide Clip blocks from the slash menu, add their parity row, and type unreadable Clips bodies * Evaluate the Clip flag for a signed-out reader in the page's org * Let a Clip's loading and error card fill the player's width * Keep the text toolbar off a selected registry block * Hold a registry block's place on a public page before its editor mounts, instead of showing its MDX source * Check every pasted non-Clips link in the browser, and keep a Clip's retry and comment failures visible * Read at most 500 comment threads from Clips, say when there are more, and keep Clip reads out of the browser's saved cache * Name each block on a public page before it loads, keep a Clip's place through a failed source, and check pasted blocks - The public page's first paint parses the saved page and shows each registry block by its saved title or registry label; a Clip is a link to its recording on Clips, built from the block and deployment config only. - A source swapped out before its metadata loaded no longer leaves its restore behind, and the next source restores the reader's place. - The toolkit reads a pasted block only through its data attributes, checks the type and id format, and keeps a pasted source only when the app reads it back. Content keeps one only when it is exactly one registered block of the declared type, and view-screen quotes only ids and types Content writes. * Keep a Clip's grant to its page, cap what Content reads from Clips, count only opened Clips against the rate limit, and drop pasted blocks with no source - The Clip block's player is keyed by page and recording, so a block that ends up under another page never keeps the last page's grant. - Content reads each Clips answer through a byte cap sized from the contract. Clips now cuts a listed comment's text to 280 characters and its author's name and email to 320, so the longest list has a known size. - Clips counts a request against its rate limit only after it has opened the recording, so made-up recording ids make no buckets. - With readPastedSource set, a pasted registry block with no source is dropped instead of saving as nothing. Changeset and Content changelog updated. * Limit made-up Clip ids per reader and page, and cut Clip titles Clips now counts requests whose recording doesn't open against a bucket per reader and page, and refuses them before any database read once it is spent, unless the Clip already opened for that reader. A comment list with no limit gets the most threads the contract allows, and the playback answer's title is cut to a length both contract files declare. * Count Clip misses per Clip, so one reader can't block another's first open A Clip's misses now share its own key, and a Clip is refused when either its opened bucket or its miss bucket is full. A Clip that opened once and then went away stays bounded, and made-up ids spend only their own misses. * Make a Content page's upload a private Clip bound to the page, take its chunks from the paired Content, and list it under From Content instead of Library * Tell a Content upload start that is too large or not JSON apart from one that fails the schema, and document Content uploads in the Clips sharing skill * Answer a preflight to the Content chunk route for the paired Content alone, before the auth guard can allow other origins and credentials * Let a Content page cancel an upload it gave up, and list an uploaded Clip in the folder it was moved into * Keep a playing Clip going when Clips misses a refresh, and bring it back on focus where the reader left off * Tell a Content page whether its cancel stopped the upload or Clips finished it first, and treat an upload whose page is in Trash as inactive * Answer a start Content repeats for one picked file with the upload it made, and leave an upload whose final chunk Clips took to finish when the page cancels * Keep every ticket a page's overlapping repeats hand out working, refuse a repeat for another file, settle a final chunk against a cancel with one claim on both chunk paths, and ask a repeat that beats its recording to try again * Show unavailable state for missing shared recordings * Keep unavailable recordings out of share metadata * Explain replacement of share retry assertions * Respect recording lifecycle in bug report context * A full Clip rate-limit map drops its oldest bucket instead of refusing new readers * Keep recording reactions independent of comments * Read where a Content upload stands in one place, so a cancel never changes a final chunk in flight, a parked upload cancels, and an abandoned claim expires * Drop an unused import from the Content upload tests * Answer a final chunk sent again with where the upload stands, never take a live claim over, and keep a failed start's binding for the next repeat to start again with the same recording * Say in the comments that a failed start is taken over only when it left no recording, and that an ended key's file starts again under a new key * Renew a Content upload's lease before its final chunk claims it, so the lease decides between the chunk and the reaper, and drop the reaper's phase check * Apply recording Trash policy to activity recipients * Hide a Content upload whose page is in Trash from folders too, and ask a start repeated during the final chunk to try again rather than say the file is in * Leave a Content upload whose page is in Trash out of every Clips list and folder count, spaces and Shared included * Refuse a Content upload's ticket on the agent chunk route, so its chunks always pass the page check and the final chunk's claim * Narrow recording context rows before lifecycle checks * Preserve comment cleanup and recording search availability * Read optional recording rows consistently across compiler checks * Format the recording search changelog entry * fix(clips): clean up a failed start's binding only as that start left it * Align recording expiry checks across shared access * Match expiry timestamp precision across recording readers * fix(clips): refuse a page's upload on every chunk route but Content's, and release only the ticket a failed start made * Share expiry access rules with agent reads and Slack unfurls * fix(clips): drop a final chunk's outcome when a resumed upload takes chunks again * Apply recording expiry across Clips readers * Check recording expiry when content completes * Enforce recording expiry at mutation boundaries * Revalidate recording expiry before delayed writes * Preserve expiry checks through queued and locked writes * Honor linked recording permissions in meeting reads * Remove unused meeting visibility test import * Recheck meeting editor access after finalization work * Meet main's strict-lint and compiler-strictness guards on Content Clips lines * Add #8129's attachment rule to the delegated-prompt snapshot #8140 landed without it * Name the prompt behavior the snapshot update records * Answer a blocked media URL under a Content grant with 502, not the expired-grant 403 * Meet main's strict-lint and compiler-strictness guards on the Clip block's lines * Answer a storage provider's 403 under a Content grant with 502 * Send a signed-out page reader's Clip comments without commenter emails * Let a comment author delete through a document whatever case their email was stored in * Declare what the three Clip actions in Content return * Set the mixed-case author without a non-null assertion * Count a Clip request against its limits from the check, so a burst can't all pass it before the first opens the Clip * Number the upload binding migrations 96 to 98, after main's 94 and 95 * Clear Clips strict-lint and strict-compiler findings on the Content upload path Type the upload tests' mocks to the real route and action signatures, default destructured rows to null, omit undefined optional fields, keep stored-ticket token parsing defensive, and create the bindings table the expiry test's list query now reads.
* Respect recording Trash and comment settings
* Apply recording policies to comment reads
* Apply Trash access to comment cleanup
* Apply comment visibility settings to recording history
* Let Content document readers play and comment on Clips uploaded to the document
* Expire Content playback grants in five minutes and close review gaps
* Test Content Clips workspace resolution, token time fields and lifetime edges
* Reset core's workspace caches between Content Clips tests
* Give each Content Clips workspace test its own reader
* Accept only the Content-Clips peer secret and make signed-out host access opt-in
Clips' Content routes now verify tokens with CONTENT_CLIPS_PEER_SECRET
alone, a core config field read through resolveContentClipsPeerSecret.
Tokens signed with the deployment A2A_SECRET or a workspace secret are
refused, and an unset or unusable secret answers a typed 503
(content_clips_not_configured) without reading or writing anything.
Media grants are signed with a key derived from the peer secret, through
a new signingKey option on core's short-lived tokens.
Host access resolvers are asked about signed-out requests only when they
register with { includeSignedOut: true }; Clips is the only one that does.
* Play Clips inside Content pages behind content.clip-embed
A Clips link pasted into a video block's Link tab becomes a Clip block that
plays in Content's own player, with the Clip's comments as markers on the
scrubber. Content asks Clips for playback and comments over the peer
contract, signed with the Content-Clips peer secret, and checks document
access first, so signed-out readers of public pages can watch too.
Registry blocks now keep their source on the clipboard, so a copied Clip
pastes with its data instead of dropping out of the save.
* Hide Clip blocks from the slash menu, add their parity row, and type unreadable Clips bodies
* Evaluate the Clip flag for a signed-out reader in the page's org
* Let a Clip's loading and error card fill the player's width
* Keep the text toolbar off a selected registry block
* Hold a registry block's place on a public page before its editor mounts, instead of showing its MDX source
* Check every pasted non-Clips link in the browser, and keep a Clip's retry and comment failures visible
* Read at most 500 comment threads from Clips, say when there are more, and keep Clip reads out of the browser's saved cache
* Name each block on a public page before it loads, keep a Clip's place through a failed source, and check pasted blocks
- The public page's first paint parses the saved page and shows each
registry block by its saved title or registry label; a Clip is a link to
its recording on Clips, built from the block and deployment config only.
- A source swapped out before its metadata loaded no longer leaves its
restore behind, and the next source restores the reader's place.
- The toolkit reads a pasted block only through its data attributes, checks
the type and id format, and keeps a pasted source only when the app reads
it back. Content keeps one only when it is exactly one registered block of
the declared type, and view-screen quotes only ids and types Content writes.
* Keep a Clip's grant to its page, cap what Content reads from Clips, count only opened Clips against the rate limit, and drop pasted blocks with no source
- The Clip block's player is keyed by page and recording, so a block that
ends up under another page never keeps the last page's grant.
- Content reads each Clips answer through a byte cap sized from the
contract. Clips now cuts a listed comment's text to 280 characters and its
author's name and email to 320, so the longest list has a known size.
- Clips counts a request against its rate limit only after it has opened the
recording, so made-up recording ids make no buckets.
- With readPastedSource set, a pasted registry block with no source is
dropped instead of saving as nothing. Changeset and Content changelog
updated.
* Limit made-up Clip ids per reader and page, and cut Clip titles
Clips now counts requests whose recording doesn't open against a bucket
per reader and page, and refuses them before any database read once it
is spent, unless the Clip already opened for that reader. A comment
list with no limit gets the most threads the contract allows, and the
playback answer's title is cut to a length both contract files declare.
* Count Clip misses per Clip, so one reader can't block another's first open
A Clip's misses now share its own key, and a Clip is refused when either
its opened bucket or its miss bucket is full. A Clip that opened once and
then went away stays bounded, and made-up ids spend only their own misses.
* Make a Content page's upload a private Clip bound to the page, take its chunks from the paired Content, and list it under From Content instead of Library
* Tell a Content upload start that is too large or not JSON apart from one that fails the schema, and document Content uploads in the Clips sharing skill
* Answer a preflight to the Content chunk route for the paired Content alone, before the auth guard can allow other origins and credentials
* Send a video uploaded into a Content page to Clips as a Clip bound to the page, hold it in the block until Clips can take it, and show its progress, making and unfinished states
* Ask Clips for upload storage with GET when the editor checks again
* Keep a Clip block's hooks in order from upload to player, show a failed check while a Clip is made, count a lost last-chunk answer as sent, and tell the agent when Clips couldn't make an uploaded Clip
* Tell an upload Clips refused or answered unreadably apart from Clips not answering
* Refresh the Upload tab's free clips left once an upload starts
* List the Clip upload actions in Content's parity matrix
* Let a Content page cancel an upload it gave up, and list an uploaded Clip in the folder it was moved into
* Keep a playing Clip going when Clips misses a refresh, and bring it back on focus where the reader left off
* Cancel an upload the page gives up, slow and stop checks on a Clip being made, and refuse video types Clips won't take
* Tell a Content page whether its cancel stopped the upload or Clips finished it first, and treat an upload whose page is in Trash as inactive
* Say when Clips kept an upload a page gave up, forget uploads whose block is gone, and upload nothing for a file removed while the gate checks
* Answer a start Content repeats for one picked file with the upload it made, and leave an upload whose final chunk Clips took to finish when the page cancels
* Keep every ticket a page's overlapping repeats hand out working, refuse a repeat for another file, settle a final chunk against a cancel with one claim on both chunk paths, and ask a repeat that beats its recording to try again
* Show unavailable state for missing shared recordings
* Send one upload key with every start of a picked file and place a Clip Clips already has, keep a removed block's five seconds running after the editor closes, and ask again while Clips is still making the first start's Clip
* Keep unavailable recordings out of share metadata
* Explain replacement of share retry assertions
* Respect recording lifecycle in bug report context
* A full Clip rate-limit map drops its oldest bucket instead of refusing new readers
* Keep recording reactions independent of comments
* Read where a Content upload stands in one place, so a cancel never changes a final chunk in flight, a parked upload cancels, and an abandoned claim expires
* Drop an unused import from the Content upload tests
* Retry a Content upload's start, chunks and cancel with the same key when Clips doesn't answer or is still finishing, and show the upload hint only when it's true
* Answer a final chunk sent again with where the upload stands, never take a live claim over, and keep a failed start's binding for the next repeat to start again with the same recording
* Say in the comments that a failed start is taken over only when it left no recording, and that an ended key's file starts again under a new key
* Start a Content upload under a new key once Clips says its key's upload ended, hold the file when Clips stays busy, time out each cancel, cancel a parked upload whose block goes, and forget a closed page's uploads
* Renew a Content upload's lease before its final chunk claims it, so the lease decides between the chunk and the reaper, and drop the reaper's phase check
* Forget a Content upload that ends its sending after its page closed, retry a failed start only when nothing readable came back, and refuse an empty file before starting
* Apply recording Trash policy to activity recipients
* Hide a Content upload whose page is in Trash from folders too, and ask a start repeated during the final chunk to try again rather than say the file is in
* Say in Content's copy of the Clips contract that a start repeated during the final chunk is asked to try again
* Leave a Content upload whose page is in Trash out of every Clips list and folder count, spaces and Shared included
* Refuse a Content upload's ticket on the agent chunk route, so its chunks always pass the page check and the final chunk's claim
* Narrow recording context rows before lifecycle checks
* Preserve comment cleanup and recording search availability
* Read optional recording rows consistently across compiler checks
* Format the recording search changelog entry
* Remove the block made for a dropped or pasted video when Clips refuses its file outright
* fix(clips): clean up a failed start's binding only as that start left it
* Align recording expiry checks across shared access
* Match expiry timestamp precision across recording readers
* fix(clips): refuse a page's upload on every chunk route but Content's, and release only the ticket a failed start made
* Share expiry access rules with agent reads and Slack unfurls
* fix(clips): drop a final chunk's outcome when a resumed upload takes chunks again
* Apply recording expiry across Clips readers
* Check recording expiry when content completes
* Enforce recording expiry at mutation boundaries
* Revalidate recording expiry before delayed writes
* Preserve expiry checks through queued and locked writes
* Honor linked recording permissions in meeting reads
* Remove unused meeting visibility test import
* Recheck meeting editor access after finalization work
* Meet main's strict-lint and compiler-strictness guards on Content Clips lines
* Add #8129's attachment rule to the delegated-prompt snapshot #8140 landed without it
* Name the prompt behavior the snapshot update records
* Answer a blocked media URL under a Content grant with 502, not the expired-grant 403
* Meet main's strict-lint and compiler-strictness guards on the Clip block's lines
* Answer a storage provider's 403 under a Content grant with 502
* Send a signed-out page reader's Clip comments without commenter emails
* Let a comment author delete through a document whatever case their email was stored in
* Declare what the three Clip actions in Content return
* Set the mixed-case author without a non-null assertion
* Count a Clip request against its limits from the check, so a burst can't all pass it before the first opens the Clip
* Number the upload binding migrations 96 to 98, after main's 94 and 95
* Clear Clips strict-lint and strict-compiler findings on the Content upload path
Type the upload tests' mocks to the real route and action signatures, default destructured rows to null, omit undefined optional fields, keep stored-ticket token parsing defensive, and create the bindings table the expiry test's list query now reads.
* Give the media controls test a QueryClient now that the video block holds upload hooks
Main's video comment controls test renders VideoBlock, which now mounts the Clip upload mutation and storage query. Keep Clip uploads off there and wrap the render in a QueryClientProvider.
* Clear Content strict-lint, strict-compiler and output-schema findings on the Clip upload path
The upload-started callback now carries the page id the gate already
checked, so VideoBlock no longer asserts it. create-clip-upload and
get-clip-upload-status declare an outputSchema for the answers they
already returned. Chunk retries walk the delay list with an iterator, and
the tests read through defined() instead of non-null assertions.

Problem
Connected agents can ask an app to perform work through
ask_appor A2A. Those delegated runs currently receive internal framework tools even when direct MCP dispatch excludes them. Several automation management paths also let outside calls create or change jobs with ordinary unattended authority.This PR gives delegated runs the app's own actions and a bounded static documentation reader, while excluding the framework runtime. The covered framework and app automation actions require outside approval and record provenance; marked runs require approval for each write.
What changed
manage-jobs.mcpTool: falseand server-guarded raw SQL. ReapplyagentToolanduiOnlyrestrictions after request-scoped merges so hidden entries cannot be dispatched by name, searched, or replayed. Analytics' warehouse actions remain available. Keep app definitions on name collisions, including a customdocs-search; use eager skill content when that name is occupied. Requireddb-*,provider-api-*, andseed-*exclusions remain. The served-registry report lists removals by template and name.caller: "tool"in delegated execution and approval replay. Carry MCP/A2A network provenance separately into approval contexts andisOutsideAgentCall, so existing template tool behavior stays intact.mcpManagedfrom server context; Factory restore retains the current marker. Factory creation uses bounded conditional inserts without overwriting an existing definition. Single-line automation settings now reject line breaks. Creation stamping in factory-scheduler-job.ts uses the same strict normalization boundary as save and restore. Trusted, unmarked saves and restores of headerless text retain legacy behavior without constructing headers; prompts starting with a header block still undergo frontmatter/provenance validation. Outside or marked writes with incomplete settings return400 invalid_requestbefore history or persistence; previews remain lenient.build-data-indexin marked automation runs, while excluding framework runtime tools. Each prepared write still requires approval, including writes without their own gate; persistent grants do not satisfy it. Ordinary in-app definitions remain unmarked; unmarked runs retain their runtime registry and use each action's approval predicate.Scope and release order
This PR follows merged #8070, reusing its framework markers and prepared-write approval boundary. The branch includes normal merges of main at
e9d5ffadand798991fd, preserving reviewed history. The release order remains #8070, then this PR in the same release, with no core publish between them; #8070 alone leaves delegated calls unchanged.Through the automation actions covered here, A2A peers need approval to create or change definitions, and marked prompt runs need approval for each write. Unattended authority must come from the person rather than a peer agent. Framework automation management, Factory saves and Analytics index-schedule changes retain enclosing outside-call provenance through
isOutsideAutomationOrigin; the sharedisOutsideAgentCallhelper is unchanged. Existing template approval predicates otherwise remain in place.Not in this PR
A store-level guarantee that every write path preserves and stamps outside provenance is planned as a separate follow-up. This PR covers the actions listed above.
CLI dispatch and embed-session gating remain in their owning PRs. Direct MCP eligibility and its allowlist are unchanged. This PR does not change Mail's connector catalog, Design's instructions, or catalog selectors/toolsets.
The CRM-to-Clips setup probe on main
4472c549failed before any automation was created, so delivery was not observed there. A separate app-created personal automation reproduced the missing-org delivery gap. Outcome truthfulness, org-bound personal runs and the setup handoff are separate follow-ups and do not gate this stack.Verification
The nested-origin repair at
3c51e63f3d3fpasses 587 tests: 338 Factory owner tests across eight suites, 20 Analytics scheduler tests, and 229 Core approval/origin/export tests across eight suites. The new cases use real enclosing MCP/A2A frames with trusted inner contexts, require approval before any effect, reject changed inputs, and verify a single approved Factory write retains provenance through the real core parser. The gate controls first reproduced eight intended failures; independently reverting only the Factory provenance change reproduced four marker failures. Core build, Core/Factory/Analytics typechecks, repository lint/format, strict compiler coverage and seven named guards pass. Both shared caller-helper blobs are unchanged. Sol xhigh reviewed the bounded delta and supplied integration snippets with no P1/P2 findings; this was a source-packet review, not independent test execution. The latest Builder review confirms the two nested-origin fixes. The marked-registry comment was dispositioned under the documented delegated policy; all review threads are resolved. All nine required hosted checks are satisfied on this head: six pass and three are not applicable/skipped. Michael Wang's approving review remains in place.The following verification was recorded on the preceding main-merge snapshot:
The latest main update is
89ce39024fb3, merging798991fdwithout manual conflict edits. It passes 402 focused tests: 180 delegated/registry owners, 215 direct-MCP and durable A2A approval tests, two marked/ordinary runner controls, and five public-SDK/Analytics inventory cases. All 41 approval snapshots matched; the committed snapshot comments are preserved. Core build and typecheck, repository lint/format, strict compiler coverage, six named guards and seven positive execution-lease models pass. The shared caller-helper blobs are unchanged. Dependency linking completed; workspace postinstall still stops on the previously observed Toolkit React-type diagnostics, with no Toolkit or lockfile workaround. All nine required hosted checks pass on89ce39024fb3; the final bounded review also examined the supplied integration snippets.The following verification was recorded before this latest main merge:
The latest visibility repair passes 180 tests across the complete delegated-runtime, additional-actions, filter and eligibility suites; both marked/ordinary runner controls and all three ordinary Analytics inventory checks also pass. The new regressions reproduced four intended failures on the preceding implementation. Core typecheck, strict compiler coverage, repository lint/format, focused guards and the four positive execution-lease models pass. The final bounded Sol review examined the supplied integration snippets alongside the nested-origin repair.
The latest cleanup preserves Factory's ES2020 target with checked indexing; no compiler configuration changes are included. Its normal typecheck, compiler strictness, all 334 Factory owner tests, formatting and repository lint pass. Two new public-SDK approval tests also pass; removing the helper export first produced two intended failures. Mutation-owner discovery now finds all owners, including the public entrypoint. This is not a mutation-score claim, and the mutation runner is unchanged.
Main-update checks pass: 199 Core approval/origin tests across four suites, 137 direct-MCP tests across three suites, all 334 Factory tests across eight suites, and all 16 Analytics scheduler tests. The delegated-entry smoke and the marked/unmarked runtime controls also pass individually. This is focused validation, not a full Core-suite pass.
Repository formatting and lint pass. The compiler strictness check passes on all three affected projects; the named MCP, source-reading, escape-hatch, assertion, strict-lint and localization guards pass. Execution-lease verification passes its four positive model configurations with all 44 coverage entries intact, without updating coverage.
Core build and typecheck passed on the main-merge snapshot before the final type/fixture cleanup. The broader local dependency prebuild stops on a Toolkit React-type identity mismatch; no Toolkit or lockfile workaround is included. All nine required hosted checks pass on
3a227a0477c5; the later bounded Sol review examined the supplied integration snippets.The initial combined Core run was stopped after three failures. The entry smoke passes in isolation, and the two registry cases pass after moving their real dependency loading into setup while retaining the 30-second case budgets and all assertions. Those selected runner checks emitted database-close teardown warnings; that limitation is retained with the local results.
Earlier validation below predates this main update:
The initial Fast tests lane 2 job on
3c51e63f3d3freported one Content editor stable-retry assertion (changedinstead ofprepared), with 7,284 other Content tests passing. The test, editor and flush participant are byte-identical to the merged main base. The exact case passes in isolation and the complete unchanged owner file passes 49/49 locally. Lane 2 passes on its single rerun. No source or assertion changes were made for it.An additional unchanged
scripts/docs/framework-search.spec.tscase fails locally on Windows because its existingURL.pathnamepath handling cannot find package docs. Standalone reproduction and byte-identical baseline evidence are retained; the required suites above pass. No source or assertion workaround was added; Linux CI remains authoritative for that extra suite.The changed-Core mutation job on
3c51e63f3d3frepeats the initial-test failure seen on the preceding heads: the relocated Core-only sandbox resolves the template inventory's repository path topackages/core/templates, which is absent there. Stryker instruments 283 candidates but runs no mutants and produces no mutation score. This is the runtime-filesystem selection gap also observed on #8070; the mutation runner and inventory assertions remain unchanged, and the ordinary Analytics inventory passes locally.Earlier checks on the stacked base encountered the database mock omission fixed by #8120 and the standalone Chat approval/queue race fixed by #8115. The main update includes both fixes; all nine required hosted checks pass on
3a227a0477c5.The repair ledger preserves exact expectation changes and their reasons. Local specs exercise real registries, agent-loop dispatch, self/remote routing, durable A2A approvals, single-use replay, static-reader privacy, ordinary in-app controls and failing-first regressions. A model asking for approval in prose before issuing a tool call remains separate from the server approval lifecycle and is not claimed fixed here.
Test expectation changes
automations/service.spec.ts: already-applied changecaller: "frontend"; exactchanged === falseremains.audit/attribution.spec.ts: copied approved contexta2a/task; caller remainstool; no child approval grant.a2a/task; exact expired-grant assertions remain.a2a/task; exact missing child grant remains.a2a/task; caller remainstooland no child grant appears.action-filters-a2a.spec.ts: replay contexta2a→tool; require exact A2A protocol/task, bound grant andisOutsideAgentCall(ctx) === true.a2a→tool; exact protocol/task/lease/grant fields remain.agent-chat-plugin.surface.spec.ts: native actionsagent-chat-plugin.a2a.spec.ts: initial automation toolmanage-automations.delegated-runtime.spec.ts: ten inline-MCP catalog casesUnknown or unavailable tool: manage-automations→ exactError: Unknown tool "manage-automations"; approval-false/no-definition checks remain.inAppRun()→inAppRun(true)fornativeActionsInDev; exact result/caller/no-protocol checks remain.beforeAll.defineAction."system" as const.invalid_requestand affected field.400 invalid_request, plus no history lookup or conditional write.false→ broader outside-origin resulttrue; nested MCP/trusted-frame controls retained and tool-over-MCP/A2A controls added.undefined; first-call and header reads are narrowed.agentTool: false/uiOnlyentries in the final delegated map → reapply app-agent visibility after the merge.mcpTool: falseand raw-input app controls remain callable.No assertion was deleted or relaxed.
An app-provided
docs-searchentry is preserved on both delegated loops instead of being overwritten by the framework reader. The bounded static reader is installed only as a fallback; when the app owns that name, skills are included eagerly and the exact app entry remains callable.