You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
docs/DX: the TypeScript AppHost model — hand-edit-vs-regen, service→plugin references, native-driver permissions
Filed as dogfooding feedback from wiring eis-chat (an AI-stack app) to @netscript/*0.0.1-alpha.19 and standing up MCP-over-HTTP services. The framework itself worked; the friction below was almost entirely discoverability — the runtime model is great, but a few load-bearing facts about the generated Aspire AppHost are only learnable by reading generated code or hitting a boot crash. One of the five (service→plugin refs) reads like a genuine framework gap, not just docs.
Everything here is grounded in real incidents. Where an existing doc already covers something, I say so and scope the ask down.
Context for the reader: the generated model in this app is aspire/apphost.mts → .helpers/register-{infrastructure,services,plugins,background,apps,tools}.mts, driven by root appsettings.json (NetScript.{Services,Plugins,BackgroundProcessors,Apps}). Each register-*.mts is headed // GENERATED BY @netscript/cli. DO NOT EDIT. Re-run 'netscript generate' ....
1. Restart semantics: which changes need a full aspire run relaunch vs a per-resource restart
What happened. Adding new resources / new references in appsettings.json + register-*.mts did not take effect on a per-resource restart from the dashboard — the AppHost has to be re-materialized (netscript generate if config changed, then a full aspire run relaunch). We initially assumed restarting the changed resource would pick it up; it silently didn't, because the resource graph is fixed at builder.build().run() time.
What's already covered (credit where due).
how-to/discover-services.mddoes state the ServiceReferences path explicitly: "Adding a ServiceReferences entry does nothing until you re-run netscript service generate and restart aspire start." Good — for that one field.
Aspire's own Hot Reload and watch doc covers the general principle (AppHost-model changes restart the whole app; per-resource restart doesn't re-evaluate the model; defaultWatchEnabled is opt-in and restart-based).
The gap. Neither generalizes the rule to all AppHost-model changes in a NetScript project. There's no single "which change needs which restart" table for the NetScript layer, e.g.:
Change
Per-resource restart enough?
Needs netscript generate?
Needs full relaunch?
Service/plugin code edit (with watch/HMR)
yes
no
no
New service/plugin/app/background-processor in appsettings.json
no
yes
yes
New ServiceReferences / PluginReferences
no
yes
yes
Permission-flag change
no
yes
yes
Env-var value change in appsettings.json
no
(regen)
yes
Proposal. A short "Restart & regeneration semantics" section on explanation/aspire.md (or the Aspire reference) with a table like the above — generalizing the rule that discover-services.md already states for ServiceReferences to the whole config surface.
2. Hand-edit-vs-regen: the "DO NOT EDIT" header has no documented escape hatch
What happened.explanation/aspire.md is emphatic and correct for the common case: "You do not edit apphost.mts to add a service… you never enumerate processes by hand." But real wiring in this app had to be hand-edited into the generated register-*.mts files, and there is no documented story for it:
MCP-over-HTTP services (legacy-archeo-mcp, excalidraw-mcp) run as their own Aspire executable resources serving Streamable HTTP, wired by a hand-edited block in register-services.mts + endpoint injection in register-apps.mts.
Each is tagged in-repo with a HAND-EDITED (keep on regen) comment — a convention we invented precisely because the docs don't offer one. The tension: the header says don't edit, the framework's own netscript generate will silently drop anything you add, and yet there's no config-only way to express these cases. So users are stuck between "can't do it in config" and "editing a file that says don't."
The gap. No documented answer to: what's safe to hand-edit, how do I survive netscript generate, and which of these should instead be expressible in appsettings.json? (Note: this app already leans on the appsettings.jsonServices.<name>.Env/Permissions fields for the MCP case, so some of it is config-expressible — that pattern just isn't documented as the sanctioned path.)
Proposal. A "Customizing the generated AppHost" how-to that covers: (a) the set of things fully expressible in appsettings.json today (custom Env, Permissions, Port, Workdir per resource — these already work and are under-advertised); (b) for anything not config-expressible, whether netscript generate has (or should have) a merge/preserve mechanism (e.g. a # @netscript:keep marker, or user-owned register-*.custom.mts hooks that survive regen); (c) an honest "here be dragons, these edits are dropped on regen" note if no preserve mechanism exists yet.
3. Native DB drivers force Deno permissions that the generator doesn't grant to every resource that needs them
What happened (concrete boot crash). alpha.19 @netscript/plugin-workers loads @tursodatabase/database, a native FFI addon. The generated launch args for the workers background processor (and initially workers-api) lacked --allow-ffi, so the process crashed at boot with NotCapable: Requires ffi access. Sibling resources (streams, and eischat after its own fix) already had --allow-ffi, so it was inconsistent across resources that all ultimately touch the same native driver. Fixed in eis-chat by hand-adding --allow-ffi (commit 6fa91d7).
Interesting related detail: netscript #174 reportedly added --allow-ffi for DB-driver services in .19 — but not for plugin-workers' processor/API executables, which is exactly the one that broke here.
What's already covered.how-to/restrict-worker-task-permissions.md documents the Deno task permission path well (.permissions({ ffi: true }) → --allow-ffi, the buildDenoPermissionFlags table, the --allow-all footgun). That's the worker-task-adapter surface.
The gap. That's a different layer from resource-level launch permissions — the resolvePermissions(...) flags baked into each builder.addExecutable(...) in register-{services,plugins,background}.mts. There's no doc on: which resource archetype gets which default permission set, and — the sharp edge — that a plugin whose runtime pulls a native DB driver (@tursodatabase/database, libsql) requires --allow-ffi/--allow-sys on every executable that loads it (API and background processor), not just the "database service." A one-liner mapping native-driver → required flags, and a note that plugin-workers' executables need --allow-ffi in .19, would have turned a boot crash into a config line.
Proposal. Extend the permissions how-to (or the plugin-workers reference) with a "resource-level launch permissions vs task permissions" distinction + a native-driver → required-flags note. If plugin-workers should self-declare --allow-ffi on its own executables via its Aspire contribution, that's arguably a framework fix too (partner to #174).
4. A Service cannot declare PluginReferences — likely a framework gap, not just docs
What happened. eis-chat's eischatservice needs to enqueue KB-ingestion jobs on workers-api (a plugin). Its workersClient (serviceName workers-api) resolves the URL via the standard service-discovery env var services__workers-api__http__0 — exactly the mechanism how-to/discover-services.md documents. But there was no way to declare that dependency in config:
Apps and BackgroundProcessors accept ServiceReferencesandPluginReferences in appsettings.json (this app uses Apps.dashboard.{ServiceReferences,PluginReferences} and BackgroundProcessors.workers.PluginReferences: ["workers-api"], both auto-wired).
A Service entry has no PluginReferences field, so a service→plugin edge is unrepresentable. We wired it by hand in register-background.mts — mapping workers-api's endpoint onto the eischat service resource manually (commit aa0da26).
Why this reads as a framework gap.explanation/aspire.md presents ServiceReferences/PluginReferences as the generic reference vocabulary ("@netscript/aspire extracts three kinds of dependency from each entry") and even gives workers-api → sagas-api as a PluginReferences example — but the worked examples only ever put these on Apps/BackgroundProcessors, and the ServiceEntry schema apparently doesn't carry PluginReferences. So a service calling a plugin API — a totally ordinary edge (any service enqueuing background work through a plugin's API hits it) — falls through. The two-pass resolver already knows how to inject services__<plugin>__http__0; it's "just" that ServiceEntry doesn't expose the field to opt in.
Proposal (framework, with docs to match). Let ServiceEntry declare PluginReferences (and, symmetrically, ServiceReferences if not already) so register-services.mts wires the service→plugin endpoint in pass 2 the same way it does for Apps/BackgroundProcessors — removing the need for the manual cross-map. If there's an intentional reason services can't reference plugins, documenting that (and the sanctioned workaround) would resolve it on the docs side. Either way explanation/aspire.md's reference-fields table should state, per resource archetype, which reference kinds are actually accepted.
Listed for completeness only. When we first hit the services__<name>__http__0 / VITE_services__… convention and the "create all, then wire refs via getEndpoint('http')" two-pass model, it felt undocumented — but it isn't:
how-to/discover-services.md documents the env-var convention, the two-pass registration, the VITE_-prefixed browser form, and even the manual/non-Aspire resolution (getServiceUrl, getAllServices).
explanation/aspire.md has the full two-pass diagram and the reference-field semantics.
So no ask here — this friction was self-inflicted (we read generated code before finding the how-to). Flagging it so this issue doesn't over-claim. The one adjacent improvement is that these two good pages could be cross-linked from the register-*.mts header comment or a netscript generate output line, so the next person lands on them before reading generated code.
Summary of asks
#
Type
Ask
1
docs
Generalize the "regenerate + full relaunch" rule (already stated for ServiceReferences) into a restart-semantics table for the whole config surface.
2
docs (+ maybe framework)
"Customizing the generated AppHost" how-to: what's config-expressible today, and whether netscript generate preserves hand-edits.
3
docs (+ maybe framework)
Resource-level launch permissions vs task permissions; native-driver → required-flags note; plugin-workers executables need --allow-ffi in .19 (partner to #174).
4
framework (+ docs)
Let a Service declare PluginReferences (service→plugin edge), or document why not + the workaround.
5
—
No ask; already covered by discover-services.md. Optional: cross-link it from generated-file headers.
Non-prescriptive throughout — happy to be wrong about #4 being a framework gap if services-can't-reference-plugins is intentional; in that case this is purely a docs ask. Thanks for a genuinely good runtime model; this is all about making the AppHost layer as legible as the rest of it.
docs/DX: the TypeScript AppHost model — hand-edit-vs-regen, service→plugin references, native-driver permissions
Filed as dogfooding feedback from wiring eis-chat (an AI-stack app) to
@netscript/*0.0.1-alpha.19and standing up MCP-over-HTTP services. The framework itself worked; the friction below was almost entirely discoverability — the runtime model is great, but a few load-bearing facts about the generated Aspire AppHost are only learnable by reading generated code or hitting a boot crash. One of the five (service→plugin refs) reads like a genuine framework gap, not just docs.Everything here is grounded in real incidents. Where an existing doc already covers something, I say so and scope the ask down.
Context for the reader: the generated model in this app is
aspire/apphost.mts→.helpers/register-{infrastructure,services,plugins,background,apps,tools}.mts, driven by rootappsettings.json(NetScript.{Services,Plugins,BackgroundProcessors,Apps}). Eachregister-*.mtsis headed// GENERATED BY @netscript/cli. DO NOT EDIT. Re-run 'netscript generate' ....1. Restart semantics: which changes need a full
aspire runrelaunch vs a per-resource restartWhat happened. Adding new resources / new references in
appsettings.json+register-*.mtsdid not take effect on a per-resource restart from the dashboard — the AppHost has to be re-materialized (netscript generateif config changed, then a fullaspire runrelaunch). We initially assumed restarting the changed resource would pick it up; it silently didn't, because the resource graph is fixed atbuilder.build().run()time.What's already covered (credit where due).
how-to/discover-services.mddoes state theServiceReferencespath explicitly: "Adding aServiceReferencesentry does nothing until you re-runnetscript service generateand restartaspire start." Good — for that one field.defaultWatchEnabledis opt-in and restart-based).The gap. Neither generalizes the rule to all AppHost-model changes in a NetScript project. There's no single "which change needs which restart" table for the NetScript layer, e.g.:
netscript generate?appsettings.jsonServiceReferences/PluginReferencesappsettings.jsonProposal. A short "Restart & regeneration semantics" section on
explanation/aspire.md(or the Aspire reference) with a table like the above — generalizing the rule thatdiscover-services.mdalready states forServiceReferencesto the whole config surface.2. Hand-edit-vs-regen: the "DO NOT EDIT" header has no documented escape hatch
What happened.
explanation/aspire.mdis emphatic and correct for the common case: "You do not editapphost.mtsto add a service… you never enumerate processes by hand." But real wiring in this app had to be hand-edited into the generatedregister-*.mtsfiles, and there is no documented story for it:legacy-archeo-mcp,excalidraw-mcp) run as their own Aspire executable resources serving Streamable HTTP, wired by a hand-edited block inregister-services.mts+ endpoint injection inregister-apps.mts.--allow-ffipermission fix (see Migrate shared Zod datetime codecs to Temporal #3) and the eischat→workers-api wire (see Wave 0b·A — Plan-Gate reinforcement (Plan & Design — READY FOR REVIEW) #4) are also hand-edits insideDO NOT EDITfiles.Each is tagged in-repo with a
HAND-EDITED (keep on regen)comment — a convention we invented precisely because the docs don't offer one. The tension: the header says don't edit, the framework's ownnetscript generatewill silently drop anything you add, and yet there's no config-only way to express these cases. So users are stuck between "can't do it in config" and "editing a file that says don't."The gap. No documented answer to: what's safe to hand-edit, how do I survive
netscript generate, and which of these should instead be expressible inappsettings.json? (Note: this app already leans on theappsettings.jsonServices.<name>.Env/Permissionsfields for the MCP case, so some of it is config-expressible — that pattern just isn't documented as the sanctioned path.)Proposal. A "Customizing the generated AppHost" how-to that covers: (a) the set of things fully expressible in
appsettings.jsontoday (customEnv,Permissions,Port,Workdirper resource — these already work and are under-advertised); (b) for anything not config-expressible, whethernetscript generatehas (or should have) a merge/preserve mechanism (e.g. a# @netscript:keepmarker, or user-ownedregister-*.custom.mtshooks that survive regen); (c) an honest "here be dragons, these edits are dropped on regen" note if no preserve mechanism exists yet.3. Native DB drivers force Deno permissions that the generator doesn't grant to every resource that needs them
What happened (concrete boot crash). alpha.19
@netscript/plugin-workersloads@tursodatabase/database, a native FFI addon. The generated launch args for theworkersbackground processor (and initiallyworkers-api) lacked--allow-ffi, so the process crashed at boot withNotCapable: Requires ffi access. Sibling resources (streams, andeischatafter its own fix) already had--allow-ffi, so it was inconsistent across resources that all ultimately touch the same native driver. Fixed in eis-chat by hand-adding--allow-ffi(commit6fa91d7).Interesting related detail: netscript #174 reportedly added
--allow-ffifor DB-driver services in .19 — but not forplugin-workers' processor/API executables, which is exactly the one that broke here.What's already covered.
how-to/restrict-worker-task-permissions.mddocuments the Deno task permission path well (.permissions({ ffi: true })→--allow-ffi, thebuildDenoPermissionFlagstable, the--allow-allfootgun). That's the worker-task-adapter surface.The gap. That's a different layer from resource-level launch permissions — the
resolvePermissions(...)flags baked into eachbuilder.addExecutable(...)inregister-{services,plugins,background}.mts. There's no doc on: which resource archetype gets which default permission set, and — the sharp edge — that a plugin whose runtime pulls a native DB driver (@tursodatabase/database, libsql) requires--allow-ffi/--allow-syson every executable that loads it (API and background processor), not just the "database service." A one-liner mapping native-driver → required flags, and a note that plugin-workers' executables need--allow-ffiin .19, would have turned a boot crash into a config line.Proposal. Extend the permissions how-to (or the plugin-workers reference) with a "resource-level launch permissions vs task permissions" distinction + a native-driver → required-flags note. If plugin-workers should self-declare
--allow-ffion its own executables via its Aspire contribution, that's arguably a framework fix too (partner to #174).4. A Service cannot declare
PluginReferences— likely a framework gap, not just docsWhat happened. eis-chat's
eischatservice needs to enqueue KB-ingestion jobs onworkers-api(a plugin). ItsworkersClient(serviceNameworkers-api) resolves the URL via the standard service-discovery env varservices__workers-api__http__0— exactly the mechanismhow-to/discover-services.mddocuments. But there was no way to declare that dependency in config:ServiceReferencesandPluginReferencesinappsettings.json(this app usesApps.dashboard.{ServiceReferences,PluginReferences}andBackgroundProcessors.workers.PluginReferences: ["workers-api"], both auto-wired).PluginReferencesfield, so a service→plugin edge is unrepresentable. We wired it by hand inregister-background.mts— mappingworkers-api's endpoint onto theeischatservice resource manually (commitaa0da26).Why this reads as a framework gap.
explanation/aspire.mdpresentsServiceReferences/PluginReferencesas the generic reference vocabulary ("@netscript/aspireextracts three kinds of dependency from each entry") and even givesworkers-api → sagas-apias aPluginReferencesexample — but the worked examples only ever put these on Apps/BackgroundProcessors, and theServiceEntryschema apparently doesn't carryPluginReferences. So a service calling a plugin API — a totally ordinary edge (any service enqueuing background work through a plugin's API hits it) — falls through. The two-pass resolver already knows how to injectservices__<plugin>__http__0; it's "just" thatServiceEntrydoesn't expose the field to opt in.Proposal (framework, with docs to match). Let
ServiceEntrydeclarePluginReferences(and, symmetrically,ServiceReferencesif not already) soregister-services.mtswires the service→plugin endpoint in pass 2 the same way it does for Apps/BackgroundProcessors — removing the need for the manual cross-map. If there's an intentional reason services can't reference plugins, documenting that (and the sanctioned workaround) would resolve it on the docs side. Either wayexplanation/aspire.md's reference-fields table should state, per resource archetype, which reference kinds are actually accepted.5. Service-discovery convention & two-pass wiring — already well documented (dropping this)
Listed for completeness only. When we first hit the
services__<name>__http__0/VITE_services__…convention and the "create all, then wire refs viagetEndpoint('http')" two-pass model, it felt undocumented — but it isn't:how-to/discover-services.mddocuments the env-var convention, the two-pass registration, theVITE_-prefixed browser form, and even the manual/non-Aspire resolution (getServiceUrl,getAllServices).explanation/aspire.mdhas the full two-pass diagram and the reference-field semantics.So no ask here — this friction was self-inflicted (we read generated code before finding the how-to). Flagging it so this issue doesn't over-claim. The one adjacent improvement is that these two good pages could be cross-linked from the
register-*.mtsheader comment or anetscript generateoutput line, so the next person lands on them before reading generated code.Summary of asks
ServiceReferences) into a restart-semantics table for the whole config surface.netscript generatepreserves hand-edits.--allow-ffiin .19 (partner to #174).PluginReferences(service→plugin edge), or document why not + the workaround.discover-services.md. Optional: cross-link it from generated-file headers.Links / references
6fa91d7(--allow-ffi fix, Migrate shared Zod datetime codecs to Temporal #3),aa0da26(eischat→workers-api manual wire, Wave 0b·A — Plan-Gate reinforcement (Plan & Design — READY FOR REVIEW) #4). In-repoHAND-EDITED (keep on regen)markers live inaspire/.helpers/register-{services,plugins,background,apps}.mts.Non-prescriptive throughout — happy to be wrong about #4 being a framework gap if services-can't-reference-plugins is intentional; in that case this is purely a docs ask. Thanks for a genuinely good runtime model; this is all about making the AppHost layer as legible as the rest of it.