Add packageStorage(): package-bucket storage that survives static imports - #816
Conversation
…bucket Saved-package code authored against ambient storage breaks when statically imported into a foreign execution context (ad hoc execute, another package) because the binding is per-run while authors mean "my package's bucket". PRs #812/#814 made the resulting error articulate; this makes it work. - New kody:runtime export packageStorage(): returns the same storage interface as ambient storage, always bound to the declaring package's bucket (package:<packageId> under the calling user), writable, with the same entitlement enforcement as other storage writes. - Bundle-time identity stamping: modules originating from a saved package import kody:runtime through a per-package virtual runtime module (.__kody_virtual__/package-runtime/<hex(packageId)>.js) whose packageStorage closes over the package's immutable id. The closure survives esbuild inlining (the #814 lesson). Hydration regenerates the stamped modules from the id encoded in the path. - Security: the executor grants bucket access only from host-controlled provenance (the run's own package context, packageId entries recorded in bundle static dependency metadata, and host-installed dynamic-import artifacts). Hand-written source claiming another package's id is rejected; ambient storage behavior is unchanged everywhere. - Workers tests run real buildKodyModuleBundle output end to end for the behavior matrix, plus node-unit coverage of stamping and provenance. - Docs: docs/use/packages.md, docs/contributing/packages-and-manifests.md, execute tool sandbox surface, and the #812 storage nextStep now mention packageStorage().
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (21)
📝 WalkthroughWalkthroughPackage-scoped storage is added through bundle-time package provenance, per-package virtual runtime modules, guarded storage tools, and propagated dependency metadata. Bundling, invocation, hydration, runtime guidance, documentation, and end-to-end tests are updated accordingly. ChangesPackage-scoped runtime storage
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant PackageBundle
participant RuntimeRegistry
participant PackageStorageTools
participant PackageBucket
PackageBundle->>RuntimeRegistry: execute stamped runtime module
RuntimeRegistry->>PackageStorageTools: provide granted package IDs
PackageBundle->>PackageStorageTools: call packageStorage operation
PackageStorageTools->>PackageBucket: route authorized request
PackageBucket-->>PackageBundle: return package-scoped data
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
🔎 Preview deployed: https://kody-pr-816.kody-a99.workers.dev Worker: Mocks:
|
Problem
Saved-package authors write
import { storage } from 'kody:runtime'meaning"my package's bucket" — a lexical intent — but the binding is dynamic, per
execution context. When package code is statically imported
(
import x from 'kody:@scope/pkg/export') it runs in the caller's context,where the declaring package's storage is not bound: in ad hoc
executewithouta
storageId,storageisundefined; inside another package, it is thehost package's bucket. Either way, the imported code can never reach its own
data outside its own runtime.
PRs #812 and #814 made the resulting failure articulate (the
runtime_helper_unboundhint). This PR makes it work: package code gets away to reach the declaring package's own bucket from any execution context,
without weakening the isolation rules that keep packages out of each other's
data.
Approach
New
kody:runtimeexportpackageStorage()— argument-less, returns thesame storage interface as ambient
storage(get/set/list/sql/delete/clear/id), always bound to the declaring package's bucket(
package:<packageId>under the calling user), writable, with the sameentitlement enforcement as other storage writes
(
assertStorageRunnerWriteWithinEntitlement). Ambientstorageis untouchedeverywhere.
Three cooperating layers:
Bundle-time identity stamping (
module-graph.ts). The runtime modulecannot know which package a module came from, so the bundler stamps it:
modules originating from a saved package get their
kody:runtimeimportrewritten to a per-package virtual runtime module,
.__kody_virtual__/package-runtime/<hex(packageId)>.js, which re-exports theshared runtime and overrides
packageStoragewith a variant that closesover the package's immutable UUID. A closure survives esbuild inlining —
the exact gap that broke Explain unbound optional kody:runtime helper access in execute errors #812's first iteration (fixed in Match inlined runtime helper declarations in unbound-helper detection #814) — so
per-module identity holds after the graph collapses into one module. Stamps
apply to statically imported package sources (via
ensurePackageLoadedprovenance) and to a package's own root modules when builders pass the new
rootPackageId(publish, invocation, jobs, apps, services, repo checks),so stamps persist into published artifacts that later get composed into
foreign bundles. Hydration (
refreshKodyRuntimeModules) regenerates stampedmodules from the id encoded in the path, exactly like the shared runtime
module, and materializes their sibling shared-runtime import target.
Unstamped modules fall back to the run's own
packageContext; with noprovenance at all,
packageStorage()throws an actionable error naming thestorageIdandpackages.invokeCheckedremedies.Host-side provenance grants (
run-kody-registry.ts,storage-runner.ts). The stamp routes identity but is not the securityboundary. New
kody.package_storage_*tools take apackageIdper call andhonor it only when it is in the run's grant set, which
collectPackageStorageGrantIdscomputes exclusively from host-controlledprovenance: the run's own package context, the
packageIdentries recordedin the bundle's static dependency metadata (new field on
BundleArtifactDependency, populated from the resolved saved-package row atbundle time), and the published artifacts the host itself installs for
literal dynamic package imports during hydration. Sandbox-supplied strings
never extend the set, so a malicious community fork running as the installing
user cannot claim another installed package's bucket — a forged
kody.package_storage_get({ packageId: victim })is rejected with astructured message. Cross-user access stays structurally impossible
(
buildStorageRunnerNamekeys the Durable Object on the calling user's id).Shared bucket identity (
package-invocations/service.ts,storage-runner.ts).buildPackageInvocationStorageIdnow delegates to thenew
buildPackageStorageId, so a package's own runtime (ambientstorage)and
packageStorage()provably reach the same bucket.Behavior matrix delivered:
packageStorage()storagestorage)storageIdundefined(unchanged)storageId(unchanged)Docs:
docs/use/packages.mdgains a "Package storage" section(
storagevspackageStorage()vspackages.invokeChecked),docs/contributing/packages-and-manifests.mddocuments the stamping andgrant model, the execute tool's sandbox-surface text names the new helper,
package-author typings (
repo/checks.ts) declare it, and the #812runtime_helper_unboundnextStep forstoragenow mentionspackageStorage().Known edge (intentional): grants cover directly recorded provenance. If
package A statically imports package B, and a caller statically imports A,
B's stamped modules in the caller's bundle are denied (with the structured
message pointing at
packages.invokeChecked) because the caller's bundlemetadata records only A. Extending grants transitively would require
persisting transitive provenance in artifact metadata; deferred until a real
use case shows up, and the conservative default is the safer one.
Tests
Learning from #814's lesson (synthetic module maps masked the inlining gap),
the new
package-storage.workers.test.tsruns realbuildKodyModuleBundle/buildKodyImportableModuleBundleoutput end toend through
runBundledModuleWithRegistry:packageStorage().sql(...)reads its own seeded bucket from an ad hoc execute call while ambient
storagestaysundefined(the acceptance scenario);bucket;
while the host package's
packageStorage()and ambientstorageagree;package_storage_getwith a hand-written victim package id isrejected and leaks nothing.
Node-unit coverage (
module-graph.node.test.ts) exercises the stampingmechanics: module-path round trip for stamped ids (including artifact-prefix
nesting), root-module stamping via
rootPackageIdvs the unstamped fallback,dependency-module stamping, hydration regeneration of stale stamped modules
plus their sibling shared runtime, and the resolution ladder (stamp → run
package context → provenance error → availability error).
executor.node.test.tscovers the extendedstoragenextStep, and existingsuites were updated for the
hydrateKodyRuntimeModulesreturn-shape changeand the new
packageIddependency field.Local gate:
npm run typecheck,npm run lint(0 errors),npm run format:check, andnpm run test(317 files / 1001 tests) all passon
main@29bf8084. The Playwright/MCP E2E halves ofnpm run validatewere not run locally (untouched surfaces); CI covers them.
System recap — extends existing primitives (medium risk)
Mode: recap · Base:
main@29bf8084· Head:d3039a99Classification: extends — new
kody:runtimeexport plus bundle-metadatafield riding existing provenance tracking; no new primitive added.
Primitives touched
package-runtimerootPackageId, hydration refreshcapabilities-executepackage_storage_*tools gated by host-derived provenance grantspackage-storagemcp-serverruntime_helper_unboundnextStep mention itSystem map
The bundler stamps each saved-package module with its immutable package id;
at execution, the runtime resolves the stamp to a
package_storage_*callthat the host honors only when the id is in the bundle's recorded provenance.
Legend: green = composes · amber = extended by this PR · red = new primitive
· gray = context.
Before / after
Package code doing
packageStorage().sql(...), statically imported into an adhoc execute call with no
storageId:storagecode failed withthe Explain unbound optional kody:runtime helper access in execute errors #812
runtime_helper_unboundhint and had no working alternative shortof
packages.invokeChecked.(
package:<packageId>under the calling user); ambientstoragestaysundefined; hand-written ids for other packages are rejected with astructured denial.
Invariants
Per-user isolation untouched: buckets remain Durable Objects named by
buildStorageRunnerName(userId, storageId). Cross-package access within oneaccount is granted only from bundler/host-controlled provenance (own package
context, recorded static dependency
packageIds, host-installed dynamic-importartifacts) — never from sandbox-supplied strings. Ambient
storagesemanticsare byte-for-byte unchanged; writes through
packageStorage()pass the samestorage-bytes entitlement checks as every other storage write.
Summary by CodeRabbit
New Features
packageStorage(), allowing saved packages to securely read and write their own data.Bug Fixes