Skip to content

Support creating new Google Docs via createExternalResource - #428

Draft
ndisidore wants to merge 7 commits into
nathan/create-external-resourcefrom
nathan/create-external-resource-google
Draft

ndisidore wants to merge 7 commits into
nathan/create-external-resourcefrom
nathan/create-external-resource-google

Conversation

@ndisidore

Copy link
Copy Markdown
Member

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.

@github-actions github-actions Bot added the gatekeeper Changes to a gatekeeper integration label Sep 2, 2026
@ask-bonk

ask-bonk Bot commented Sep 2, 2026

Copy link
Copy Markdown
  • High packages/gatekeeper-google/src/google.ts:1811: Pending observers are not tracked or rechecked after creation, allowing unauthorized access to the real document.
  • High packages/gatekeeper-google/src/google.ts:1675: Removing the action immediately makes a lost apply response unrecoverable; retries throw “Unknown pending” and the approval remains stuck.
  • Medium packages/gatekeeper-google/src/google.ts:822: The provisional URL remains in creationSpec, so suggested blueprint bindings recreate a permanently unresolved document.
  • Medium packages/gatekeeper-google/src/google.ts:1584: Auto-approval rules are gatekeeper-ID scoped. Each creation gets a new ID, so opting in cannot auto-approve future scheduled creations.
  • Medium packages/gatekeeper-kit/vitest.config.ts:1: No Vite+ test task is defined, so root pnpm test skips the new package’s tests.

github run

@ndisidore
ndisidore force-pushed the nathan/create-external-resource-google branch from 6011068 to faee59d Compare September 4, 2026 19:50
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown

Preview: pr428-nathan-create-bd0fd285

https://pr428-nathan-create-bd0fd285-router.cloudflare-os-previews.workers.dev

Dashboard · deleted when this PR closes

@ask-bonk

ask-bonk Bot commented Sep 4, 2026

Copy link
Copy Markdown
  • Low packages/gatekeeper-google/src/google.ts:1553: Creation is advertised for every Google Doc binding, including existing, materialized, or rejected documents. This exposes a rule that can never match. Only return CREATE_DOCUMENT_ACTION for an unresolved pending creation.

github run

… a flat scalar map the kernel carries but never reads, validated by the vendor and reflected on its approval card.
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.
getGatekeeperClassFor refuses such an id, so a provisional URL that outlives
its creation -- one a rejection stranded, or that a blueprint captured under
suggestValue and an install auto-assigned -- cannot mint a binding whose
document never resolves. The guard sits there rather than in
parseResourceUrl, which must keep parsing these URLs for describe().

It accepts no creation options: assertNoCreationOptions refuses the whole map,
quoting the type's own creatable.description, so a placement the agent supplies
is a fixable rejection rather than a document silently created somewhere else.
The refusal runs before the grant round-trip, because the agent can fix its own
call within the turn while a missing grant ends it on a user action.

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.

Observers are tracked rather than asserted. A provisional binding has no ACL
to consult, and admitting one unchecked has to be paid for later: the
overseer re-runs addObserver on every open but never on a session already
open, so a collaborator who joined before the creation landed would keep
observing the real document, including content written to it outside the
workspace. The document is one tracked set in the package's ObserverTracker
-- a binding minted against an existing document seeds it, keeping admission
the hasDocAccess check it was, while a created one seeds nothing and the
first read after the creation forward-excludes whoever joined before it.
commit() runs only once the overseer authorizes, so the set is promoted only
after every named observer was torn down or provably cannot reach it.
Session reads funnel through one #authorizeRead, and removeObserver is real.

Settling an action the facet already settled is a no-op, not an error. The
overseer marks its record approved only after applyAction returns, so a
decision whose reply is lost leaves a pending record with no facet-side
counterpart; answering "Unknown pending" stranded a card that could then be
neither approved nor rejected, blocking every queued edit behind it under
the in-order rule and, for a creation, the binding itself.
PendingActionStore.issued() separates a never-submitted id, which still
throws, from a settled one -- the at-least-once contract gatekeeper-kit
documents. A crash between documents.create and the binding write can still
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, observer withholding across the
creation, 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
ndisidore force-pushed the nathan/create-external-resource-google branch from faee59d to 07dfdf2 Compare September 14, 2026 19:15
@github-actions github-actions Bot added workshop/frontend Changes to the Workshop frontend kernel Changes to the Workshop kernel workshop/shared Changes to shared Workshop APIs labels Sep 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gatekeeper Changes to a gatekeeper integration kernel Changes to the Workshop kernel workshop/frontend Changes to the Workshop frontend workshop/shared Changes to shared Workshop APIs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant