Skip to content

feat: add /tabs/:tabId/upload endpoint for file attachment - #8273

Merged
skyfallsin merged 2 commits into
jo-inc:masterfrom
tracefinance:feat/upload-endpoint
Aug 2, 2026
Merged

feat: add /tabs/:tabId/upload endpoint for file attachment#8273
skyfallsin merged 2 commits into
jo-inc:masterfrom
tracefinance:feat/upload-endpoint

Conversation

@leoneparise

Copy link
Copy Markdown
Contributor

Summary

Adds a POST /tabs/:tabId/upload endpoint that attaches a file to an upload control without going through the native OS file dialog (which isn't answerable in a headless/containerized browser).

How it works

Two strategies are tried in order:

  1. Existing input — if an <input type="file"> is already present, call Playwright setInputFiles on it directly (works for hidden inputs, skips the OS dialog entirely).
  2. Triggered chooser/panel — otherwise arm a filechooser listener, activate the trigger element (ref or selector) via keyboard (focus + Enter) with a forced click as fallback, then either:
    • setFiles on the resulting native file chooser, or
    • poll for an in-app panel <input type=file> that mounts a beat after activation and setInputFiles on it.

The panel-input path is preferred over the native chooser (preference order, not a race) so a control that surfaces both — e.g. LinkedIn's media picker, which A/B-tests a native chooser and a hidden panel input for the same button — attaches the file exactly once rather than producing a duplicate ("1 of 2") image.

Request

POST /tabs/:tabId/upload
{
  "userId": "agent1",
  "path": "/data/photo.png",   // absolute container-side path, or string[]
  "ref": "e36",                // optional trigger ref
  "selector": "button.upload"  // optional trigger selector
}

path must be visible inside the container (e.g. a bind-mounted directory). The route guards with fs.existsSync and returns 400 file_not_found otherwise. ref/selector are optional when an input[type=file] already exists.

Runs under the same per-user (withUserLimit) and per-tab (withTabLock) locks as the other interaction routes, refreshes element refs afterward, and emits a tab:upload plugin event.

Testing

  • Adds tests/unit/uploadEndpoint.test.js — request-validation logic plus source-contract assertions (route registered, container file guard, both attach strategies, locking), following the typeKeyboardMode.test.js / navigationTimeout.test.js convention.
  • OpenAPI docs regenerated (openapi.json, +1 path); openapi.test.js "up to date" check passes.
  • 28 passed across the upload + openapi suites.

Purely additive — no existing routes or behavior changed.

Attach a file to an upload control without going through the native OS
file dialog. Two strategies are tried in order:

  1. If an <input type="file"> is already present, call Playwright
     setInputFiles on it directly (works for hidden inputs).
  2. Otherwise arm a filechooser listener, activate the trigger element
     (ref or selector) via keyboard (focus + Enter) with a forced click
     as fallback, and setFiles on the resulting chooser. Also polls for
     an in-app panel <input type=file> that mounts after activation.

The panel-input path is preferred over the native chooser so a control
that surfaces both (e.g. LinkedIn's media picker) attaches the file
exactly once rather than producing a duplicate.

Paths must be visible inside the container (e.g. a bind-mounted dir);
the route guards with fs.existsSync and returns 400 file_not_found
otherwise. Runs under the same per-user and per-tab locks as the other
interaction routes.

Includes OpenAPI documentation and unit tests (request validation +
source-contract assertions).
…eout` arg

The upload route had inline millisecond literals (4000, 12000, 3000,
10000, 500, 1500) scattered through its two attach strategies. Replace
them with named UPLOAD_*_MS constants declared next to the route, and
expose the overall wait budget as an optional `timeout` request field.

- UPLOAD_UI_TIMEOUT_MS (default 12000) backs the request's `timeout`:
  the budget to wait for an upload UI (panel input or native chooser).
  Non-numeric / <= 0 values fall back to the default.
- The panel-poll window derives from that budget minus
  UPLOAD_PANEL_MARGIN_MS, preserving the original 10000/12000 split so a
  late native chooser is still caught after polling stops.
- UPLOAD_INPUT/FOCUS/CLICK/REFS/POLL/SETTLE_MS name the per-call bounds.

Defaults reproduce the previous behavior exactly. OpenAPI documents the
new `timeout` field; tests cover timeout resolution and assert the route
carries no bare millisecond literals.
@skyfallsin
skyfallsin merged commit 8e3cd9b into jo-inc:master Aug 2, 2026
2 checks passed
@skyfallsin

Copy link
Copy Markdown
Contributor

Thanks, @leoneparisePR #8273, “feat: add /tabs/:tabId/upload endpoint for file attachment,” is now merged to master. It adds headless file attachment through existing upload inputs or file choosers.

Your original commits are preserved in merge commit 8e3cd9b. We added a small security follow-up so each requested file must resolve inside CAMOFOX_UPLOADS_DIR; external paths, symlink escapes, and non-regular files are rejected. We also added a real HTTP/browser end-to-end test that uploads an in-root file and verifies the page receives it.

Thank you for the implementation and the careful upload-strategy handling.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants