sdk: author provisioned tools against typed capabilities - #121
Merged
Conversation
weilueluo
force-pushed
the
capability-provisioned-path
branch
from
August 31, 2026 13:36
75cea0e to
cb0173c
Compare
weilueluo
added a commit
that referenced
this pull request
Sep 1, 2026
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.
The provisioned half of the capability contract (#119): typed tool authoring, environment capability providers, the tool artifact build, and the SDK-owned ESM host runtime.
What
tool({ ..., requires, bindings })derives the run context type from the declaration (Pick<CapabilityHandles, requires[number]>plus schema-typedbindingsvalues), so using an undeclared capability does not compile. Handle interfaces per the contract (ExecHandle,FsHandle,NetHandle,JsHandle,PageHandle) surface failures as typedCapabilityErrors; grant policy stays invisible to tools.provide.exec(...),provide.fs(...), … register providers whose factory receives{ instance, grants }; the registered set becomes theprovideslist on the setup and attach receipts (replacing the hardcodedprovides: []), feeding the kernel'srequires ⊆ providescheck. A sharedclamphelper covers exec bounds (clamp(opts, grants.exec)) and fs-root confinement (clamp.pathrejects any resolved path escaping the root with apath_escapeCapabilityError).brain buildemits<name>.tool.jsonper tool: thetool/v1manifest (schemas from zod,requires,binding_names,hosting: "provisioned", payload identity = sha-256 of the bundle) plus the self-contained single-file ESM bundle. Binding values are structurally impossible in the artifact. Build warns — never errors — on a declared capability whose handle never appears in the authored code, checked against a dependencies-external bundle so capability names inside the SDK or third-party packages cannot mask it.host.esm({ artifacts })opts an environment into hosting. Payloads are cached by content identity and imported +initialized once at provision, so a throwing bundle, an unprovided capability, or a missing binding value fails the attach receipt, never the first model call. At invoke the host validates input against the tool's schema (invalid_input), wires handles in-process to the environment's providers for exactly the tool'srequires, injects binding values from attach, and maps result / thrown error (identifier-shapedcodepreserved) / deadline (timeout) / cancel (cancelled) onto the one Outcome envelope.Confinement choice
Plain
import()of the bundle (data: URL) under a trusted-artifact assumption, stated in the code: worker/vm isolation would need an RPC layer to carry capability handles across the boundary, which is not worth its weight until an untrusted-tool story needs it. Payload identity is verified before import.Deviations from the interface draft
provide.*andhost.esm()hang off the objectauthor.open(...)returns (alongside the existingrun/close/method/stream), not the top-level author — that is whereInstanceis typed, so provider factories get a typedinstance.options,requestId(alias ofcallId),workspace?, andprogress()from the existing surface alongside the draft's handles/bindings/signal/deadline/callId.host.esm({ artifacts })is how a process registers what it can serve.Tests
Type-level fixture compiled by
tscin-test (undeclared capability and undeclared binding fail via@ts-expect-error), provision-fail-at-attach (broken bundle, missing capability, missing binding, unknown identity), invoke happy path + input-validation failure + thrown error + deadline timeout, fs-root escape, exec clamp, unused-requires warning, and a manifest golden test that also runs the emitted artifact end to end on a hosting environment.Verification
npm run genclean ·npm run build·npm testgreen (SDK 17/17, examples build) ·cargo test --workspace --all-targetsgreen (no Rust changes).Note: overlaps with the in-flight callback PR (
capability-callback-path) inextensions.ts,index.ts, anddocs/concepts/tool.mdx; whichever lands second needs a small merge.🤖 Generated with Claude Code