Skip to content

[bug]: Value.file is unimplemented end-to-end — action file inputs submit {} and can never succeed #3585

Description

@Jolls

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

  1. 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,
      }),
    })
  2. Install the package, open the service in the StartOS web UI, and go to the Actions tab.

  3. Run the action. Provide a file — drag-and-drop onto the browser window and the field's own file picker both reproduce.

  4. Submit.

  5. 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:

  1. 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.
  2. projects/start-os/web/ui/src/app/routes/portal/components/form/controls/file.component.ts:115FormFileComponent extends Control<IST.ValueSpecFile, TuiFileLike>; [(ngModel)] binds the File directly.
  3. projects/start-os/web/ui/src/app/routes/portal/components/form.component.ts:150handler(this.form.value as T) passes the value through verbatim.
  4. projects/start-os/web/ui/src/app/services/action.service.ts:57embassy-live-api.service.ts:585package.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.

Metadata

Metadata

Assignees

Labels

BugSomething isn't workingNeeds TriageNeeds to be assigned a priority

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions