Support creating new Google Docs via createExternalResource - #428
Draft
ndisidore wants to merge 14 commits into
Draft
Support creating new Google Docs via createExternalResource#428ndisidore wants to merge 14 commits into
ndisidore wants to merge 14 commits into
Conversation
|
Vendors advertise creatable resource types via SupportedResource.creatable; GatekeeperUser.createResource() mints a provisional identity (no provider call, no user interaction) and returns an imbued gatekeeper class the way getGatekeeperClassFor() does; Gatekeeper.submitCreationAction() queues the "create this resource" action so the provider-side creation rides the normal action-approval flow while the gatekeeper simulates the resource locally. All three are optional and additive; no existing gatekeeper changes.
Like requestConnection except it creates a brand-new resource of a creatable type through an already-connected account, and no user action gates the binding: it lands in the chat's env immediately (the createGadget pattern -- recorded output drives replay and chatScopeNames), the turn continues, and the creation is an ordinary pending action attributed to the chat, so its card rides the existing consumeCapturedActions splice. * agent.ts: tool definition, AgentHooks.createExternalResource, replay case, a system-prompt sentence. Fixable rejections (bad name, unknown vendor or type, no usable account, ambiguous accounts) return non-error tool results so the model retries in-turn; with several connected accounts the message enumerates ids so the agent can retry with accountId or ask the user. * user.ts: createResourceGatekeeper(), the urlPattern-to-capability chokepoint for creations, applying the same admin disable-set checks as getGatekeeperClassFor(). * overseer.ts: the hook implementation (vendor/creatable validation, mint, addGatekeeper, submitCreationAction through an ApprovalQueueImpl scoped to the new workpiece and the creating chat, with removeGatekeeper on failure); GatekeeperRecord.provisional drives a one-shot describe refresh (guarded on the URL changing, since an invalidated edit can apply before the creation) in applyPendingAction once the creation applies, since nothing else ever re-denormalizes resourceTitle/resourceUrl; listConnectableResources annotates creatable types. * ChatInterface.tsx: the six exhaustive tool-display switch cases. * integration tests: the fixture gatekeeper grows a creatable type with provisional URLs and a rejection-kills-the-binding path; new end-to-end coverage for fixable retry, pre-approval use, the action card, the describe refresh after approval, replay across turns, and rejection.
… scan. buildCompactionState folded createGadget/createWorktree outputs into the checkpoint's chatBindings but not createExternalResource, so once compaction crossed the creation call the binding replay re-establishes from the recorded output silently vanished. prepareChatBindings' naming chokepoint had the same gap: the quick-model namer could hand a pasted capsule a name replay would bind to a created resource. Both now mirror the replay semantics: a structured output binds, a string (rejection) output only reserves the name.
…owner. createExternalResource resolved the workspace owner's user DO, so a collaborator-driven turn enumerated the owner's connected accounts (the ambiguous-accounts error lists ids and names into the transcript) and created resources under the owner's credentials. Follow the actor-scoped pattern of listAvailableBlueprints: the hook now takes the turn's initiator and resolves their user DO. Owner-initiated turns are behavior-identical. The vendor listing stays owner-scoped -- it exposes only static vendor classes.
createExternalResource makes the gatekeeper record and its pending creation
action durable immediately, but the tool call reaches the chat log only at
commitAgentStep -- a DO restart mid-turn left an orphan gatekeeper plus an
approvable creation action bound to nothing, and the resumed turn could mint
a duplicate. Mirror the gadget pending lifecycle: stamp
GatekeeperRecord.pending = {chatId} in the same put as `provisional`, clear
it at the step barrier in addChatMessages (same transaction as the message
write), and sweep unstamped markers from reconcilePendingGadgets' schedule
and chat deletion. The sweep keeps a gatekeeper whose creation was actually
applied (approved-action fallback covers a stuck `provisional` from the
best-effort describe refresh); otherwise it rejects the queued actions and
removes the workpiece in one durable step.
- createResourceGatekeeper: check the admin disable-set after the vendor RPC against the resolved resource.urlPattern, mirroring getGatekeeperClassFor (the vendor is the authority on which type a request maps to). - applyPendingAction's post-apply refresh: re-read the gatekeeper record after the describe() await so a concurrent removeGatekeeper isn't resurrected by the stale put, and refresh creationSpec.resourceUrl too so blueprint export with suggestValue doesn't export the dead provisional URL. - ChatInterface: label a rejected creation "Tried to create external resource" instead of claiming it was created. - submitCreationAction doc: ordering is the gatekeeper's responsibility -- manual approval has no platform-side ordering guard, so the gatekeeper must reject applyAction of dependent actions until the creation applies. The test-gatekeeper fixture now models that guard like the Google gatekeeper.
…ation failure.
Observations and hooks are stamped approved at creation, so without the type
gate a vendor authorizing an observation before its creation action would fake
an approval injection (and first-encounter delete would silence the real card).
The gatekeeper row is now read only when the state is approved, the only case
that consumes it. The awaitDecision latch a failed submitCreationAction set is
restored to its prior value: the settled action can never be decided, and
suspending the turn would hide {created: false} from the model. Also: the
transcript row now uses isCreatedResourceSuccess (an errored call rendered
"Created external resource" beside its Error badge), and the reap comment no
longer claims the action index is resolved-only (pending records index under
their type too; the state check is load-bearing).
The creation card is where replay injects the user's decision; compacting it away orphaned the decision, so in any sufficiently long chat the recorded tool result's "does not exist yet" became the model's permanent last word. Mirror the pending-connectionRequest rule: replay notes the earliest still-undecided creation card and findProtectedFromSequence keeps the boundary behind it. Once the decision lands, the next replay injects it before the projection is built from the same model messages, so the summary absorbs it and the card compacts normally. Same trade-off as connection requests: an undecided creation defers compaction until the user decides.
GOOGLE_DOC_RESOURCE is the first creatable type. createResource() checks the account holds the doc grant (documents.create needs the write scope that grant already carries -- no new scopes), mints a provisional-<uuid> document id, and returns a GoogleDocGatekeeperImpl imbued with creation props. The facet queues a createDocument action as pending action #1, so the existing in-order approval rule applies the creation before any queued edit. Until then the binding simulates over a synthetic empty snapshot with zero provider traffic (describe(), getMetadata(), and getContent() all answer locally); sourceMap/bodyEndIndex are safe to fake because materialization only runs at apply time, when the document exists. Apply calls documents.create and late-binds provisional-to-real via the kit's ProvisionalIds; edit actions resolve the real id before touching the API. Rejecting the creation invalidates every queued edit and marks the binding dead, so session methods explain instead of simulating against nothing. A crash between documents.create and the binding write can leak one duplicate document on a retried approval (the API has no idempotency key and the edit write-marker protocol cannot cover creation); accepted for now. The workerd docs suite covers simulation-without-traffic, create-then-edit ordering, retried-approval idempotency, and the rejection cascade.
Document creation keeps its own action kind, separate from the edits one, so enabling hands-free creation is a distinct choice -- the case that wants it is a scheduled task minting a new doc each run, whose queued edits would otherwise stall in-order behind a creation card every time. Nothing is auto-applied without the user enabling the createDocument rule.
ndisidore
force-pushed
the
nathan/create-external-resource-google
branch
from
September 4, 2026 19:50
6011068 to
faee59d
Compare
Preview:
|
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Google Docs is the first creatable resource. A doc title isn't globally contended, so this proves the kernel API without the approval-time rename that github repo names will need. Also supports a common use case, i.e. a scheduled task minting a doc each run, whose queued appends would otherwise wait on a human click every time
A created doc gets a
provisional-<uuid>id and resolves to the real id when the creation applies, via the kit's ProvisionalIds (cherry-picked at its own paths so nathan/gatekeeper-kit rebases cleanly). Before that, the binding simulates over an empty document and makes no provider calls. The existing in-order rule applies the creation before any queued edit. Rejecting the creation invalidates every queued edit and makes session methods fail with an explanation of what happened. No new OAuth scopes; the doc grant already carries write access.Creation is auto-approvable under its own opt-in rule, separate from edits.