Prerequisites
Server Hardware
Start9 Server (purchased ~2023); exact model unknown
StartOS Version
0.4.0-beta.10
Client OS
Windows
Client OS Version
11
Browser
Firefox
Browser Version
153
Current Behavior
Value.file() is a typed, exported SDK primitive with no implementation behind it. An action InputSpec field defined with Value.file({...}) always fails validation on submit, because the web UI puts the raw browser File object into the form value and sends it verbatim over JSON-RPC — where it serializes to {}.
There is no upload step anywhere in the action-input path: no continuation guid handed to the client, no upload endpoint on the action RPC namespace, and nothing that would deliver the bytes into the service container. This is not a broken handshake — the handshake was never built.
Submitting the action produces:
RPC ERROR: Action Failed
[
{ "expected": "string", "code": "invalid_type", "path": ["file", "path"],
"message": "Invalid input: expected string, received undefined" },
{ "expected": "object", "code": "invalid_type", "path": ["file", "commitment"],
"message": "Invalid input: expected object, received undefined" }
]
Both subfields are undefined rather than the file key being absent — because the value submitted is {}. A File has no own enumerable properties, so JSON.stringify({ file: someFile }) is {"file":{}}.
This is the unfinished half of #2699. File uploads were stripped out for 0.3.6 on the explicit understanding that "they will be available in 040". 0.4.0 has now shipped without them, and Value.file was left exported rather than commented out — so a packager can write an action against it, compile clean, and only discover at runtime that nothing implements it.
The reported version is not the issue. I hit this on 0.4.0-beta.10 with SDK 2.0.1, but confirmed by source inspection that every code path below is unchanged on master at feac0516 (2026-07-25) and in the current released line — StartOS 0.4.0.1, SDK 2.0.10.
Expected Behavior
The action handler receives input.file as a populated FileInfo:
{ path: string, commitment: { hash: string, size: number } }
path pointing at the uploaded file, readable from within the service container.
Steps to Reproduce
-
Define an action with an InputSpec containing a Value.file() field:
const inputSpec = InputSpec.of({
file: Value.file({
name: 'Export File',
description: '...',
warning: null,
extensions: ['.json'],
required: true,
}),
})
-
Install the package, open the service in the StartOS web UI, and go to the Actions tab.
-
Run the action. Provide a file — drag-and-drop onto the browser window and the field's own file picker both reproduce.
-
Submit.
-
Observe the validation error above.
Anything else?
I/Jolls found an issue with sonnet, and then had Opus 4.8-extrahard look into it a lot more. Seems legitimate, below are some additional comments from opus about the issue. not sure the history lesson is relevant but i left it in.
Root cause
The form value for a file spec is a raw browser File, and it reaches the wire untouched:
projects/start-os/web/ui/src/app/services/form.service.ts:150 — the file case creates a control seeded null with only tuiCreateFileFormatValidator + Validators.required. No upload hook.
projects/start-os/web/ui/src/app/routes/portal/components/form/controls/file.component.ts:115 — FormFileComponent extends Control<IST.ValueSpecFile, TuiFileLike>; [(ngModel)] binds the File directly.
projects/start-os/web/ui/src/app/routes/portal/components/form.component.ts:150 — handler(this.form.value as T) passes the value through verbatim.
projects/start-os/web/ui/src/app/services/action.service.ts:57 → embassy-live-api.service.ts:585 — package.action.run with that value as input.
The SDK's fileInfoParser (shared-libs/ts-modules/start-core/lib/actions/input/builder/value.ts:34) then produces exactly the error above.
package action get-input <pkg> <action> confirms the spec sent to the client is well-formed, so nothing is wrong with how the package declares the field:
{ "spec": { "file": { "type": "file", "extensions": [".json"], "required": true, ... } }, "value": {} }
This is not a client-side-only fix
Wiring the widget to upload would have nowhere to upload to, and nowhere for the bytes to land:
ActionInput (shared-libs/ts-modules/start-core/lib/osBindings/ActionInput.ts) carries only eventId / spec / value — the client is never handed an upload guid.
- The action RPC namespace (
shared-libs/crates/start-core/src/action.rs:22-44) is get-input, run, clear-task. There is no upload endpoint.
run_action (shared-libs/crates/start-core/src/action.rs:349) forwards input to the service verbatim. The Rust backend has no notion of ValueSpec at all — the input spec is opaque JSON to it.
upload.rs and the /rest/rpc/{guid} continuation machinery exist, but their only consumers are sideload (install/mod.rs) and registry assets (registry/asset.rs).
FileInfo has no consumers repo-wide — it appears only in its own definition and one type signature in inputSpecTools.ts:208. Nothing resolves a path into the service container's filesystem.
A fix therefore spans web + Rust + container-runtime.
History — removed on purpose, with a commitment to restore it
#2699 ("Comment out config file upload feature from SDK", 2024-07-30) states:
We will not support file uploads for config in 036. Please comment out. They will be available in 040.
It was closed as completed on 2024-12-19. The removal half happened — 89e3273 "remove file uploads from config" (same day) deleted the working 0.3.x flow: ConfigModal.uploadFiles() scanned the config for File instances, POSTed each to /rest/upload, and substituted the returned hash. Ten files, 142 deletions, including uploadFile on the API service and the form-file component.
The restore half never happened, and I can find no open issue tracking it. The SDK side was also not commented out as the issue asked — Value.file and Value.dynamicFile are still exported and fully typed, so the primitive now advertises a capability 0.4.0 does not have.
The question was left open in-tree too. projects/start-os/web/ui/src/app/routes/portal/routes/backups/types/target.ts:42:
// @TODO Matt do we just drop file specs?
// key: ISB.Value.file({
// name: 'Private Key File',
// description: 'Your Google Drive service account private key file (.json file)',
// ...
The Google Drive backup target wanted a key-file upload and had to comment it out. Same in api.fixures.ts:1682.
The OS's own UI works around it with an as any cast
gateways/item.component.ts:247 and gateways.component.ts:259 use ISB.Value.file(...) and then do:
: await (input.config.value.file as any as File).text(),
The as any as File double cast concedes that the runtime value contradicts the declared FileInfo type. This works only because the OS reads the bytes in the browser and passes them to its own RPC method. A package cannot do that — its form value goes straight into package.action.run.
Impact and workaround
Any package needing a file-upload action — config imports, restore flows, cert/key uploads — cannot use one. The only workaround is Value.textarea (paste raw contents), which fails for large inputs; in my case the realistic payload is a multi-hundred-MB JSON export.
Value.file is also absent from the packaging book — grep -rl "Value.file" projects/start-sdk/docs/ returns nothing. So a packager discovers it from the types, writes an action against it, and finds out at runtime.
Prerequisites
Server Hardware
Start9 Server (purchased ~2023); exact model unknown
StartOS Version
0.4.0-beta.10
Client OS
Windows
Client OS Version
11
Browser
Firefox
Browser Version
153
Current Behavior
Value.file()is a typed, exported SDK primitive with no implementation behind it. An actionInputSpecfield defined withValue.file({...})always fails validation on submit, because the web UI puts the raw browserFileobject into the form value and sends it verbatim over JSON-RPC — where it serializes to{}.There is no upload step anywhere in the action-input path: no continuation guid handed to the client, no upload endpoint on the action RPC namespace, and nothing that would deliver the bytes into the service container. This is not a broken handshake — the handshake was never built.
Submitting the action produces:
Both subfields are
undefinedrather than thefilekey being absent — because the value submitted is{}. AFilehas no own enumerable properties, soJSON.stringify({ file: someFile })is{"file":{}}.This is the unfinished half of #2699. File uploads were stripped out for 0.3.6 on the explicit understanding that "they will be available in 040". 0.4.0 has now shipped without them, and
Value.filewas left exported rather than commented out — so a packager can write an action against it, compile clean, and only discover at runtime that nothing implements it.The reported version is not the issue. I hit this on 0.4.0-beta.10 with SDK 2.0.1, but confirmed by source inspection that every code path below is unchanged on
masteratfeac0516(2026-07-25) and in the current released line — StartOS 0.4.0.1, SDK 2.0.10.Expected Behavior
The action handler receives
input.fileas a populatedFileInfo:pathpointing at the uploaded file, readable from within the service container.Steps to Reproduce
Define an action with an
InputSpeccontaining aValue.file()field:Install the package, open the service in the StartOS web UI, and go to the Actions tab.
Run the action. Provide a file — drag-and-drop onto the browser window and the field's own file picker both reproduce.
Submit.
Observe the validation error above.
Anything else?
I/Jolls found an issue with sonnet, and then had Opus 4.8-extrahard look into it a lot more. Seems legitimate, below are some additional comments from opus about the issue. not sure the history lesson is relevant but i left it in.
Root cause
The form value for a
filespec is a raw browserFile, and it reaches the wire untouched:projects/start-os/web/ui/src/app/services/form.service.ts:150— thefilecase creates a control seedednullwith onlytuiCreateFileFormatValidator+Validators.required. No upload hook.projects/start-os/web/ui/src/app/routes/portal/components/form/controls/file.component.ts:115—FormFileComponent extends Control<IST.ValueSpecFile, TuiFileLike>;[(ngModel)]binds theFiledirectly.projects/start-os/web/ui/src/app/routes/portal/components/form.component.ts:150—handler(this.form.value as T)passes the value through verbatim.projects/start-os/web/ui/src/app/services/action.service.ts:57→embassy-live-api.service.ts:585—package.action.runwith that value asinput.The SDK's
fileInfoParser(shared-libs/ts-modules/start-core/lib/actions/input/builder/value.ts:34) then produces exactly the error above.package action get-input <pkg> <action>confirms the spec sent to the client is well-formed, so nothing is wrong with how the package declares the field:{ "spec": { "file": { "type": "file", "extensions": [".json"], "required": true, ... } }, "value": {} }This is not a client-side-only fix
Wiring the widget to upload would have nowhere to upload to, and nowhere for the bytes to land:
ActionInput(shared-libs/ts-modules/start-core/lib/osBindings/ActionInput.ts) carries onlyeventId/spec/value— the client is never handed an upload guid.shared-libs/crates/start-core/src/action.rs:22-44) isget-input,run,clear-task. There is no upload endpoint.run_action(shared-libs/crates/start-core/src/action.rs:349) forwardsinputto the service verbatim. The Rust backend has no notion ofValueSpecat all — the input spec is opaque JSON to it.upload.rsand the/rest/rpc/{guid}continuation machinery exist, but their only consumers are sideload (install/mod.rs) and registry assets (registry/asset.rs).FileInfohas no consumers repo-wide — it appears only in its own definition and one type signature ininputSpecTools.ts:208. Nothing resolves apathinto the service container's filesystem.A fix therefore spans web + Rust + container-runtime.
History — removed on purpose, with a commitment to restore it
#2699 ("Comment out config file upload feature from SDK", 2024-07-30) states:
It was closed as completed on 2024-12-19. The removal half happened — 89e3273 "remove file uploads from config" (same day) deleted the working 0.3.x flow:
ConfigModal.uploadFiles()scanned the config forFileinstances,POSTed each to/rest/upload, and substituted the returned hash. Ten files, 142 deletions, includinguploadFileon the API service and the form-file component.The restore half never happened, and I can find no open issue tracking it. The SDK side was also not commented out as the issue asked —
Value.fileandValue.dynamicFileare still exported and fully typed, so the primitive now advertises a capability 0.4.0 does not have.The question was left open in-tree too.
projects/start-os/web/ui/src/app/routes/portal/routes/backups/types/target.ts:42:The Google Drive backup target wanted a key-file upload and had to comment it out. Same in
api.fixures.ts:1682.The OS's own UI works around it with an
as anycastgateways/item.component.ts:247andgateways.component.ts:259useISB.Value.file(...)and then do:The
as any as Filedouble cast concedes that the runtime value contradicts the declaredFileInfotype. This works only because the OS reads the bytes in the browser and passes them to its own RPC method. A package cannot do that — its form value goes straight intopackage.action.run.Impact and workaround
Any package needing a file-upload action — config imports, restore flows, cert/key uploads — cannot use one. The only workaround is
Value.textarea(paste raw contents), which fails for large inputs; in my case the realistic payload is a multi-hundred-MB JSON export.Value.fileis also absent from the packaging book —grep -rl "Value.file" projects/start-sdk/docs/returns nothing. So a packager discovers it from the types, writes an action against it, and finds out at runtime.