Commit Graph
47 Commits
Author SHA1 Message Date
Tim O'Farrellandopenhands 910b19ae76 chore: bump agent-server → 1.28.1, automation → 1.0.0a9, extensions → 0.4.1 (#1319)
* chore: bump agent-server → 1.28.1, automation → 1.0.0a9, extensions → 0.4.1

Co-authored-by: openhands <openhands@all-hands.dev>

* Test fixes

* fix: inject proxy base_url for litellm_proxy/* when server omits it (agent-server ≥1.28)

Agent-server ≥1.28 may return base_url:null when fetching a litellm_proxy/*
profile config, even when the profile was saved with the All-Hands proxy URL.
This caused the Basic-tab re-save flow in LlmSettingsLocalView.handleSave to
call isOpenHandsProxyModel(model, null) → false, hitting the else-branch that
deletes base_url and stranding the profile (issue #1146).

Fix: add a secondary check — litellm_proxy/* with a missing base_url is treated
the same as litellm_proxy/* with the proxy URL already set, and
OPENHANDS_LLM_PROXY_BASE_URL is injected before the save request is sent.

Also updates the mock-LLM E2E test to accept both storage representations:
- litellm_proxy/* + proxyBaseUrl  (pre-1.28, guards issue #1146 regression)
- openhands/*     + null          (1.28+, server-managed routing)

And adds a unit test exercising the base_url:null path.

Co-authored-by: openhands <openhands@all-hands.dev>

---------

Co-authored-by: openhands <openhands@all-hands.dev>
2026-06-11 19:16:03 -04:00
Tim O'Farrellandopenhands 8071edf72a Revert "chore: bump agent-server → 1.28.1, automation → 1.0.0a9, extensions → 0.4.1 (#1315)" (#1318)
This reverts commit 1917b5d39fbf09dc51213b4b484698fe394314c7.

Co-authored-by: openhands <openhands@all-hands.dev>
2026-06-11 21:28:04 +00:00
Tim O'Farrellandopenhands 15a52fea75 chore: bump agent-server → 1.28.1, automation → 1.0.0a9, extensions → 0.4.1 (#1315)
* chore: bump agent-server → 1.28.1, automation → 1.0.0a9, extensions → 0.4.1

Co-authored-by: openhands <openhands@all-hands.dev>

* chore: update doc examples to reference agent-server 1.28.1

Update version references in AGENTS.md, scripts/dev-safe.mjs, and
scripts/check-sdk-version-sync.mjs from 1.27.0 → 1.28.1 to stay
in sync with the agentServer pin in config/defaults.json.

Fixes: docs-version-sync.test.ts failures

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: inject proxy base_url for litellm_proxy/* when server omits it (agent-server ≥1.28)

Agent-server ≥1.28 may return base_url:null when fetching a litellm_proxy/*
profile config, even when the profile was saved with the All-Hands proxy URL.
This caused the Basic-tab re-save flow in LlmSettingsLocalView.handleSave to
call isOpenHandsProxyModel(model, '') → false, hitting the else-branch that
deletes base_url and stranding the profile (issue #1146).

Fix: add a secondary check for litellm_proxy/* models with a missing base_url
(null/undefined/empty), treating them the same as a stored proxy URL and
injecting OPENHANDS_LLM_PROXY_BASE_URL before the save request is sent.

Also adds a unit test exercising the base_url:null path.

Co-authored-by: openhands <openhands@all-hands.dev>

* test(e2e): accept agent-server 1.28 model rewrite in proxy profile test

Agent-server 1.28 normalises litellm_proxy/* → openhands/* on storage
and manages the proxy URL internally (returning base_url:null). The old
assertions hard-coded the pre-1.28 storage format (litellm_proxy/* +
explicit proxy URL), causing the test to fail on every 1.28 run.

Extract assertProxyProfileConfig() helper that accepts both storage
representations:
- litellm_proxy/* + proxyBaseUrl   (pre-1.28, guards issue #1146 regression)
- openhands/*     + null           (1.28+, server-managed routing)

The issue #1146 guard is preserved: a litellm_proxy/* profile without a
proxy URL is still flagged as a stranded profile.

Co-authored-by: openhands <openhands@all-hands.dev>

---------

Co-authored-by: openhands <openhands@all-hands.dev>
2026-06-11 21:00:05 +00:00
chuckbutkusandopenhands f93cb3c9ee settings: persist app preferences and disabled_skills on the agent-server (#1191)
* settings: persist app preferences and disabled_skills on the agent-server

The local agent-server now exposes app_preferences on the persisted
settings (OpenHands/software-agent-sdk#3539): language, sound
notifications, analytics consent, git identity, and disabled_skills are
returned on GET /api/settings under app_preferences and updated via a
new app_preferences_diff field on PATCH /api/settings.

This brings the local agent-server to parity with the cloud, which has
always accepted the same keys at the top level. Drops the localStorage
workaround that mirrored these fields in two keys
(openhands-agent-server-app-preferences and
openhands-agent-server-disabled-skills), along with the
app-preferences-store.ts module and the DISABLED_SKILLS_STORAGE_KEY
helpers it depended on.

- SettingsService.transformApiResponse reads app_preferences from the
  server response and hoists each field onto the flat Settings shape so
  consumers (settings.language, settings.disabled_skills, …) keep
  working unchanged.
- SettingsService.saveSettings routes the same set of fields through
  the new app_preferences_diff for local backends and through the
  existing app_preferences flat-spread path for cloud backends.
- New legacy-app-preferences-migration.ts runs once on first
  getSettings() after upgrade: when the server reports an
  app_preferences block AND legacy localStorage values are still
  present, it pushes them up via app_preferences_diff and clears the
  legacy keys. Pre-1.27 servers (which omit app_preferences entirely)
  cause the migration to no-op so existing data isn't dropped before
  the server can accept it.
- Updated MSW handlers to round-trip app_preferences and
  app_preferences_diff so the mock backend matches production.
- Test coverage: 5 new tests in __tests__/api/settings-service.test.ts
  for the local round-trip, the mixed diff routing, the legacy
  migration, and the pre-1.27 skip path.

Closes the localStorage workaround called out in the recent audit of
agent-canvas localStorage usage (items 3 and 4: disabled_skills and
app-preferences fields).

Depends on agent-server 1.27 / SDK PR #3539.

Co-authored-by: openhands <openhands@all-hands.dev>

* settings: read/write app preferences via misc_settings container

Follow-up to the localStorage cleanup in this PR + SDK refactor in
openhands/software-agent-sdk#3543. The agent-server now exposes
frontend-owned settings under a generic misc_settings container instead
of a top-level app_preferences field.

Wire shape changes:

  Before:                                 After:
  GET /api/settings                       GET /api/settings
    -> { app_preferences: {...} }           -> { misc_settings: { app_preferences: {...} } }

  PATCH /api/settings                     PATCH /api/settings
    body.app_preferences_diff (shallow      body.misc_settings_diff (deep-merged,
    overlay, replaces named fields)         same semantics as agent_settings_diff)

Why the rename to misc_settings: the previous name pinned the API to a
single 'frontend-owned' namespace. Adding a future category like
ui_preferences (sidebar layout / view modes) would have required either
yet another top-level field or shoehorning unrelated UI state into
AppPreferences. With misc_settings as a container, new categories drop
in as nested fields without churning the top-level shape.

Changes:

- settings-service.api.ts
  * SettingsApiResponse.app_preferences -> .misc_settings (typed)
  * SettingsUpdateRequest.app_preferences_diff -> .misc_settings_diff
  * Add MiscSettings interface
  * transformApiResponse reads response.misc_settings?.app_preferences
  * saveSettings emits { misc_settings_diff: { app_preferences } }
  * Local 'has any diffs' check tracks misc_settings_diff
  * Doc comments updated; semantics noted as deep-merge
- legacy-app-preferences-migration.ts
  * Gate on serverResponse.misc_settings, not .app_preferences
  * pushDiff callback now wraps the diff in { app_preferences: ... }
- src/mocks/settings-handlers.ts
  * GET handler returns misc_settings.app_preferences
  * PATCH handler accepts misc_settings_diff; deep-merges nested
    app_preferences into the persisted block
  * Internal mock state stores under misc_settings to match wire shape
- __tests__/api/settings-service.test.ts
  * Four tests updated to assert the new wire shape (local PATCH body,
    GET round-trip, mixed-diff routing, legacy localStorage migration)
  * Pre-1.27 detection test now keys off missing misc_settings
- AGENTS.md
  * App-preferences note rewritten for the misc_settings container,
    explains deep-merge semantics, and documents the in-flight rename
    (flat shape introduced in #3539 never shipped to users)

Cloud path is unchanged: cloud /api/v1/settings still accepts the
fields as flat top-level keys, mirrored by saveCloudSettings.

Verification:

  $ npm run typecheck
  exit 0

  $ npm test -- __tests__/api/settings-service.test.ts \
                __tests__/api/mock-settings-handlers.test.ts
  23 tests passed

  $ npm test
  3009 passed | 12 skipped | 9 todo

  $ npm run lint
  All matched files use Prettier code style!

  $ npm run build
  built in 1.50s

Co-authored-by: openhands <openhands@all-hands.dev>

* Bump agent-server default to 1.27.0

Co-authored-by: openhands <openhands@all-hands.dev>

---------

Co-authored-by: openhands <openhands@all-hands.dev>
2026-06-09 18:53:13 +00:00
c39354f2b3 feat: load public skills from @openhands/extensions npm package (#1199)
* build(deps): move @openhands/extensions to npm 0.2.0

* feat: load public skills from @openhands/extensions npm package

Public skills are now loaded from the @openhands/extensions npm package
via a standard JS module import instead of fetching them through the
agent-server (which cloned the extensions GitHub repo at runtime).

  import { SKILLS_CATALOG } from '@openhands/extensions/skills';

SkillsService maps each SkillCatalogEntry to a SkillInfo and merges the
bundled public catalog with user/project skills fetched from the
agent-server (load_public: false). If the agent-server is unreachable,
the bundled catalog is returned alone.

Changes:
- SkillsService: imports SKILLS_CATALOG from @openhands/extensions/skills,
  maps entries to SkillInfo, merges with user/project skills from
  agent-server (load_public: false).
- agent-server-adapter: hardcodes load_public_skills: false in
  buildAgentContext().
- agent-server-config: removes shouldLoadPublicSkills() and its
  VITE_LOAD_PUBLIC_SKILLS env var.
- dev-safe.mjs: removes getExtensionsRef() / DEFAULT_EXTENSIONS_REF
  and EXTENSIONS_REF injection in buildAgentServerEnv().
- Docker: removes CONFIG_EXTENSIONS_REF from config-gen stage and
  EXTENSIONS_REF from entrypoint.sh.
- .env.sample: removes VITE_LOAD_PUBLIC_SKILLS comment.
- Tests updated to match new architecture.

Depends on OpenHands/extensions#310 which adds the SKILLS_CATALOG export.

Co-authored-by: openhands <openhands@all-hands.dev>

* test: remove activated_skills assertion from preset-automation E2E

With load_public_skills: false the agent-server no longer loads public
skills at runtime, so activated_skills is always empty. The conversation
itself works (slash command sent, agent replies) — only the server-side
skill activation metadata is gone.

Co-authored-by: openhands <openhands@all-hands.dev>

* feat: pass bundled public skills via agent_context.skills for SDK-side activation

Instead of doing frontend-side trigger matching, pass the bundled
SKILLS_CATALOG entries directly in agent_context.skills at conversation
start. The SDK performs trigger matching, sets activated_skills on user
events, and injects skill content into the system prompt — the exact
same behavior as when load_public_skills was true, but without cloning
the extensions repo at runtime.

buildBundledSkills() converts each catalog entry into the SDK Skill JSON
shape with KeywordTrigger ({ type: 'keyword', keywords: [...] }) for
skills with triggers, or null for always-active skills.

Restores the activated_skills E2E assertion in the preset-automation
test since the SDK now handles activation.

Co-authored-by: openhands <openhands@all-hands.dev>

* test: add E2E tests for project/user skill loading and deletion

Add mock-llm-skills.spec.ts with three tests:
1. Project skill in workspace/.agents/skills/ triggers on matching keyword
2. User skill in ~/.openhands/skills/ triggers on matching keyword
3. Deleting a user skill removes it from subsequent conversations

Tests create ephemeral SKILL.md files with unique trigger keywords,
send messages through the real agent-server stack, and verify
activated_skills in the conversation events API.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: use explicit APIRequestContext type import for CI TS6 compatibility

Replace inline `import('@playwright/test').APIRequestContext` type
references with a proper top-level type import. Also align afterEach
fixture destructuring with other specs' pattern.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: remove node: prefix from imports to fix CI TS resolution

TypeScript 6 on CI (Node 24) has a type resolution conflict when
`node:` prefixed imports (node:path, node:fs, node:os) coexist with
`@playwright/test` types in the same file. This caused
`APIRequestContext` to be incorrectly resolved as `Page`. Use
unprefixed imports (path, fs, os) which work identically in Node.js.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: split fs helpers into separate file to fix CI TS6 type resolution

Move node built-in imports (path, fs, os) and filesystem helpers to
`utils/skill-test-helpers.ts`. The spec file now only imports from
`@playwright/test` and the two helper modules, avoiding the type
resolution conflict between node builtins and Playwright fixture types
that caused `APIRequestContext` to be incorrectly inferred as `Page`
on CI (TypeScript 6 / Node 24 / Ubuntu).

API assertion logic is now inline within each test step, using the
`request` fixture directly instead of standalone functions with
explicit `APIRequestContext` type annotations.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: use namespace imports to avoid TS6 type inference issue

Switch from named imports to namespace imports (`import * as helpers`)
with subsequent destructuring. This changes how TypeScript resolves the
imported function signatures, avoiding a Node 24 / TS6 type inference
bug where `ensureMockLLMProfile` was incorrectly resolved as expecting
`Page` instead of `APIRequestContext`.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: add typed wrapper for ensureMockLLMProfile to fix CI TS2345

Add a local `configureMockLLM` wrapper with an explicit
`APIRequestContext` type annotation. This works around a CI-specific
TypeScript 6 type inference issue where the imported
`ensureMockLLMProfile` signature is incorrectly resolved as expecting
`Page` instead of `APIRequestContext` when called from a Playwright
test body that also imports from `skill-test-helpers` (a module with
node built-in imports). The wrapper's explicit type annotation forces
correct type checking at the call site.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: inline ensureMockLLMProfile logic to fix CI TS2345

Instead of importing ensureMockLLMProfile from mock-llm-helpers (which
triggers a CI-specific TS6 type inference bug when combined with
skill-test-helpers imports), inline the same logic as a local function
with explicit APIRequestContext typing. This avoids the cross-module
type resolution issue entirely.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: resolve WORKSPACE_DIR relative to agent-server CWD, not STATE_DIR

The agent-server resolves the relative working_dir ("workspace/project")
from its own CWD (the project root), not from STATE_DIR/workspaces.
The test was writing skill files to the wrong directory so the SDK
never found them, causing activated_skills to be empty.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: create standalone git repo for project skill E2E test

The agent-server creates a git worktree for each conversation, and only
committed files appear in worktrees. The previous approach wrote skill
files to the filesystem without committing them, so the worktree never
contained them and load_project_skills found nothing.

Now the test:
1. Creates a standalone git repo (.tmp/mock-llm-skill-repos/) with the
   skill file committed
2. Creates the conversation via API with that repo as working_dir
3. The agent-server worktree includes the committed skill
4. load_project_skills discovers it in the worktree

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: add secrets_encrypted flag to skill test conversation creation

The GET /api/settings with X-Expose-Secrets: encrypted returns cipher-
encrypted secret values. The POST /api/conversations needs
secrets_encrypted: true to tell the server to decrypt them, otherwise
the request fails with HTTP 422.

Co-authored-by: openhands <openhands@all-hands.dev>

* refactor: use UI workspace selection for project skill E2E test

Instead of creating conversations via API (bypassing the frontend code),
the test now exercises the full UI flow:

1. Creates a standalone git repo with the skill committed
2. Registers the repo as a workspace via POST /api/workspaces
3. Opens the 'Open workspace' dialog in the UI
4. Selects the workspace from the dropdown
5. Types the message and submits via the chat input

This exercises the actual frontend code paths (workspace dropdown,
workspace selection form, createConversation with workingDirOverride)
that real users go through.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: add padding response for skill-analysis in deletion test

The agent-server makes a skill-analysis LLM call even when no user/project
skills are loaded, because public skills from the npm package are still
present. The deletion test only had 1 trajectory response, causing the
agent to hang waiting for the 2nd response (the actual reply).

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: simplify deletion test to not depend on specific event type

The deletion test was failing because it waited for an event with
source='agent' and event_type='message' in the events API, but the
mock LLM text reply may produce a different event type. Since
waitForNonUserMessageText already confirms the agent replied in the
UI, we just need to verify no activated_skills contains the deleted
skill name.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: mount skill test dirs into Docker container for e2e tests

The Docker E2E skills test was failing because the agent-server inside
the Docker container couldn't access skill repos and user skill files
created on the host filesystem.

Fix by:
- Adding volume mounts for skill repos (.tmp/mock-llm-skill-repos/ →
  /tmp/mock-llm-skill-repos/) and user skills (.tmp/mock-llm-user-skills/
  → /home/openhands/.openhands/skills/) to the Docker run command
- Setting env vars (MOCK_LLM_SKILL_REPOS_CONTAINER_DIR,
  MOCK_LLM_USER_SKILLS_HOST_DIR) so skill-test-helpers.ts can
  distinguish host-side vs agent-side paths
- Updating createProjectSkillRepo to return both hostDir and agentDir
  so the test registers the container-side path with the agent-server

In npm mode (no env vars set), all paths fall back to the existing
host-side values — no behavior change for the npm test path.

Co-authored-by: openhands <openhands@all-hands.dev>

* docs: document Docker skill test volume mounts in AGENTS.md

Co-authored-by: openhands <openhands@all-hands.dev>

* feat: mark newly added mock-LLM E2E tests with 🆕 badge in PR comments

The render-mock-llm-report.mjs script now accepts a --new-files flag
with a comma-separated list of spec file paths added in the PR. Tests
from those files get a 🆕 badge in the results table, and the summary
line shows the count (e.g. '🆕 2 new').

Both CI workflows (mock-llm-e2e.yml and mock-llm-docker-e2e.yml) add
a 'Detect newly added spec files' step that queries the GitHub API
for files with status=='added' matching the mock-LLM spec pattern,
avoiding shallow-clone issues with git diff.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: match Playwright basename file paths against repo-relative --new-files

Playwright's JSON reporter emits file paths relative to testDir
(e.g. 'mock-llm-skills.spec.ts') while the GitHub API returns
repo-relative paths (e.g. 'tests/e2e/mock-llm/mock-llm-skills.spec.ts').
The isNewTest() matcher now compares basenames in addition to exact/suffix
matching, so 🆕 badges render correctly.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: stabilize pagination loading-indicator test + improve new-test callout

1. Flaky test fix: the 'loads older events when scrolling up' test
   asserts that the loading-older-events indicator appears, but the
   instant mock response lets React batch isLoading true→false in one
   commit — the DOM element never materialises. Add a 300ms delay to
   older-events mock responses so the indicator renders reliably.

2. Better new-test visibility: replace the subtle inline 🆕 emoji with
   a prominent green blockquote callout above the results table that
   lists each new test with its status icon and spec file.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: address PR review — type safety, docs, test assertions

1. Define BundledSkill interface for buildBundledSkills() return type
   instead of the opaque SettingsRecord[] (review thread #1).

2. Document PUBLIC_SKILLS as an immutable build-time snapshot that is
   baked into the bundle and requires a dependency bump to update
   (review thread #2).

3. Add migration note to buildAgentContext() explaining that the former
   VITE_LOAD_PUBLIC_SKILLS env var was removed because bundled skills
   have no clone latency. load_public_skills: false is still passed to
   tell the SDK to skip its own clone (review thread #3).

4. Add structural assertions for individual skill entries in the adapter
   test: name, content, source, is_agentskills_format, and trigger
   shape (review testing gap).

5. Update stale VITE_LOAD_PUBLIC_SKILLS comments in E2E test files.

Co-authored-by: openhands <openhands@all-hands.dev>

---------

Co-authored-by: Joe Laverty <joe.laverty@openhands.dev>
Co-authored-by: openhands <openhands@all-hands.dev>
2026-06-07 21:52:59 +00:00
Tim O'Farrellandopenhands cca79d096e chore: bump software-agent-sdk to 1.26.0 (#1186)
Update agentServer version pin in config/defaults.json from 1.25.0 to 1.26.0.
This drives all four packages (openhands-agent-server, openhands-sdk,
openhands-tools, openhands-workspace) which are released in lockstep.

Also update matching test expectations and example version strings in
dev-safe.mjs, check-sdk-version-sync.mjs, and AGENTS.md.

Co-authored-by: openhands <openhands@all-hands.dev>
2026-06-05 11:47:43 -06:00
Tim O'Farrellandopenhands 90754e2571 feat: add daily-rotating file logger for dev scripts (closes #815) (#1181)
* feat: add daily-rotating file logger for dev scripts (issue #815)

Add winston + winston-daily-rotate-file to write all dev-server log
output to logs/agent-canvas.YYYY-MM-DD.log alongside the existing
console output (which is unchanged).

- scripts/logger.mjs  — shared module; exports fileLog(level, msg)
  and stripAnsi(str). DailyRotateFile transport stores files in
  logs/ relative to the project root, rotates at midnight, and
  auto-deletes files older than 7 days.
- scripts/dev-with-automation.mjs — logService / logStep /
  logSuccess / logError each call fileLog as a side-channel. The
  shutdown message, startup title, checkPrerequisites uvx-error, and
  printBanner summary are also captured.
- scripts/dev-safe.mjs — spawnProcess errors, main() startup lines,
  the unexpected-exit error, and the fatal-error handler all call
  fileLog.
- logs/ was already in .gitignore.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: store log files in agent-canvas state dir, not project root

Use OH_CANVAS_SAFE_STATE_DIR (or ~/.openhands/agent-canvas as
the default) to match where all other agent-canvas runtime state
lives, e.g. ~/.openhands/agent-canvas/logs/agent-canvas.YYYY-MM-DD.log

Co-authored-by: openhands <openhands@all-hands.dev>

---------

Co-authored-by: openhands <openhands@all-hands.dev>
2026-06-05 10:34:30 -06:00
Tim O'Farrellandopenhands dda46c10bd feat: default EXTENSIONS_REF from pinned @openhands/extensions SHA in package.json (#1060)
* feat: default EXTENSIONS_REF from pinned @openhands/extensions SHA in package.json

The agent-server polls the OpenHands extensions repo using EXTENSIONS_REF
(defaulting to 'main'), while the frontend bundles the MCP catalog and
automations data from a pinned commit SHA in package.json. This caused the
two to silently diverge — skills loaded at runtime were from latest main
while the UI showed catalog data from an older commit.

Fix: derive a DEFAULT_EXTENSIONS_REF from the '#<sha>' fragment in the
@openhands/extensions git URL in package.json and inject it as the agent-
server's EXTENSIONS_REF when the caller hasn't set it explicitly.

- scripts/dev-safe.mjs: getExtensionsRef() helper reads package.json at
  module init; buildAgentServerEnv() spreads EXTENSIONS_REF into the
  child-process env unless the caller already set it.

- docker/Dockerfile (config-gen stage): reads package.json alongside
  config/defaults.json and emits CONFIG_EXTENSIONS_REF=<sha> into
  defaults.env when the dependency uses a pinned git SHA.

- docker/entrypoint.sh: applies CONFIG_EXTENSIONS_REF as the default via
  EXTENSIONS_REF="${EXTENSIONS_REF:-${CONFIG_EXTENSIONS_REF:-}}".

User-supplied EXTENSIONS_REF always takes precedence in both paths.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: address review bot comments on EXTENSIONS_REF sync

- entrypoint.sh: guard EXTENSIONS_REF export so an absent CONFIG_EXTENSIONS_REF
  never sets the variable to an empty string (which would defeat the
  agent-server's own 'main' default in os.environ.get('EXTENSIONS_REF','main'))
- scripts/dev-safe.mjs: getExtensionsRef() now checks devDependencies as
  fallback when @openhands/extensions is not in dependencies
- docker/Dockerfile config-gen stage: same devDependencies fallback
- AGENTS.md: replace internal constant name DEFAULT_EXTENSIONS_REF with the
  actual env var EXTENSIONS_REF; also note the empty-string guard

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: pre-seed extensions cache when EXTENSIONS_REF is a commit SHA

The SDK's git clone --depth 1 --branch <sha> fails because --branch only
accepts branch/tag names, not raw 40-char commit SHAs (GitHub returns
'fatal: Remote branch <sha> not found').

When EXTENSIONS_REF is a commit SHA, pre-seed the public-skills cache with
a plain full git clone + checkout before starting the agent-server:

- docker/entrypoint.sh: pre-seeds after EXTENSIONS_REF is exported; is a
  no-op when the cache already exists or EXTENSIONS_REF is a branch/tag name
- scripts/dev-safe.mjs: exports preseedExtensionsCache() and
  DEFAULT_EXTENSIONS_REF; calls pre-seed in main() before spawning the
  agent-server; guards with the same SHA regex as entrypoint.sh
- scripts/dev-with-automation.mjs: imports preseedExtensionsCache and
  DEFAULT_EXTENSIONS_REF from dev-safe.mjs; calls pre-seed in
  startAgentServer() before spawnService()

The SDK's update path (fetch + git checkout) can handle SHAs once the
repo exists, so this is sufficient without any SDK changes.

Co-authored-by: openhands <openhands@all-hands.dev>

* refactor(docker): bake extensions clone into image, cp at runtime

Move the git clone for the pinned extensions SHA from the entrypoint
into a dedicated Dockerfile build stage (extensions-cache).

Why: the clone is deterministic (same SHA for a given image), so doing
it once at build time is cheaper than doing it on first container start.
It also avoids network access at runtime entirely for Docker users.

How:
- New extensions-cache stage (node:24-slim + git): reads package.json,
  clones OpenHands/extensions, checks out the pinned SHA into
  /tmp/public-skills. When the dependency is not SHA-pinned the stage
  exits early with an empty directory (graceful no-op).
- Final stage: COPY --from=extensions-cache stores the result at
  /opt/agent-canvas/extensions-cache/ — outside the
  /home/openhands/.openhands VOLUME so it is not hidden by bind-mounts.
  chown passes ownership to openhands.
- entrypoint.sh: replaces git clone + checkout with cp -r from the
  pre-baked path. The guard checks [ -d "${_ext_baked}/.git" ] so
  the block is a no-op when the stage produced an empty directory.

The npm-dev preseedExtensionsCache() path in dev-safThe npm-dev preseedExtensionsCache() path in d — that still clones over the
network on first run (build-time baking does not apply to npm dev).

Co-authored-by: openhands <openhands@all-hands.dev>

* fix(extensions-preseed): handle existing shallow clone missing the pinned SHA

When `npm run dev` has been run previously without the PR, the agent-server
SDK creates a shallow clone of the extensions repo via:
  git clone --depth 1 --branch main

After PR #1060 is applied, `EXTENSIONS_REF` is set to the 40-char pinned
SHA. The SDK then tries `git checkout <sha>` against that shallow clone,
which fails because the specific commit is not in its shallow history.

`preseedExtensionsCache` had a blind early-return when `.git` already
existed, trusting the "SDK update path" to handle it. But the SDK's update
path also does a plain `git checkout <sha>` and cannot succeed on a shallow
clone that lacks the commit.

Fix: before returning early, verify the SHA is accessible with
  git cat-file -t <sha>
If it returns anything other than "commit", the cache is a shallow clone
that pre-dates the pinned commit. Unshallow the existing clone with
  git fetch --unshallow
so the full history is available, then checkout the SHA.

Co-authored-by: openhands <openhands@all-hands.dev>

* refactor(extensions): copy from node_modules on npm, always overwrite cache

Implement the intended architecture for extensions cache seeding:

npm path (dev-safe.mjs / dev-with-automation.mjs):
- Replace preseedExtensionsCache (git clone approach) with
  copyExtensionsToSkillsCache, which copies the already-installed
  node_modules/@openhands/extensions directly into the skills cache.
  No network call required — npm already installed the package at
  the pinned SHA.
- Always overwrite the cache (rm -rf + cpSync) so stale files from a
  prior run that used a different version are never left behind.
- Set EXTENSIONS_REF unconditionally in buildAgentServerEnv so it
  always matches the pre-seeded cache content.

Docker path (docker/entrypoint.sh):
- Always copy /opt/agent-canvas/extensions-cache into the skills cache
  (rm -rf + cp -r), removing the prior guard that skipped the copy when
  .git already existed.
- Set EXTENSIONS_REF unconditionally from CONFIG_EXTENSIONS_REF.

Both paths accept that the SDK will warn 'Using cached version' for a
raw commit SHA — that warning is the expected fallback until SDK polling
is disabled in a follow-up PR.

Note: the npm package only publishes integrations/ and automations/.
Docker's baked full clone also includes marketplaces/, plugins/, skills/.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix(extensions): delete stale cache on npm startup instead of copying files

The copy-from-node_modules approach was still broken: the npm package has no
.git directory, so the SDK tried a fresh 'git clone --depth 1 --branch <sha>'
on top of the already-populated directory and got confused.

Simpler fix: just delete ~/.openhands/cache/skills/public-skills on startup.
The SDK starts with a clean slate and attempts its normal clone; for a raw SHA
ref it will warn 'Using cached version' which is the accepted fallback until
SDK polling is disabled in a follow-up PR.

- Replace copyExtensionsToSkillsCache (copy from node_modules) with
  clearExtensionsCache (delete the directory)
- Remove now-unused cpSync import
- Docker path unchanged (baked clone always copied by entrypoint.sh)

Co-authored-by: openhands <openhands@all-hands.dev>

* fix(extensions): always delete cache; only set EXTENSIONS_REF if not in env

Three simple rules:
1. Always delete ~/.openhands/cache/skills/public-skills on startup so the
   SDK clones fresh — no conditional on DEFAULT_EXTENSIONS_REF.
2. Only inject EXTENSIONS_REF into the agent-server env when the caller has
   not already set it (restore the !process.env.EXTENSIONS_REF guard).
3. Let the agent-server SDK do its own cloning from there.

Co-authored-by: openhands <openhands@all-hands.dev>

* feat(extensions): use OH_PUBLIC_SKILLS_PATH to bypass git polling

Switch from EXTENSIONS_REF (which still went through the SDK's broken
git clone --branch <sha> path) to OH_PUBLIC_SKILLS_PATH, introduced in
software-agent-sdk PR #3513. When set, the SDK skips all git operations
and loads public skills directly from the given directory.

npm path (dev-safe.mjs / dev-with-automation.mjs):
- Add cloneExtensionsForSkillsCache(sha, cacheDir): does a targeted
  git fetch --depth=1 origin <sha> into ~/.openhands/cache/skills/public-skills.
  Reuses the existing directory when it already contains the right commit.
- buildAgentServerEnv now sets OH_PUBLIC_SKILLS_PATH (not EXTENSIONS_REF)
  pointing at that directory when DEFAULT_EXTENSIONS_REF is known and
  OH_PUBLIC_SKILLS_PATH is not already in the environment.

Docker path (docker/entrypoint.sh):
- Removes EXTENSIONS_REF export entirely.
- After copying the baked clone to the skills cache, exports
  OH_PUBLIC_SKILLS_PATH pointing at that directory.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix(dev): include openhands-sdk in git-ref uvx args

When OH_AGENT_SERVER_GIT_REF is set, openhands-sdk was missing from the
--with args, so uv fell back to the released PyPI version. That version
lacks the public_skills_path parameter added to update_skills_repository
in SDK PR #3513, so the agent-server's skills_service called the old SDK
path and did a plain git clone of 'main' — overwriting the SHA-pinned
clone our cloneExtensionsForSkillsCache had just created.

Fix: add --with git+<repo>@<ref>#subdirectory=openhands-sdk alongside the
existing openhands-tools and openhands-workspace entries, matching what
the PyPI and local paths already do for openhands-sdk.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix(dev): add --reinstall to git-ref uvx command

When OH_AGENT_SERVER_GIT_REF points at a branch whose version string
matches the current PyPI release (e.g. both are 1.25.0), uv silently
reuses the cached PyPI wheels and the git ref is never used.

--reinstall forces uv to build a fresh environment from the specified
git sources regardless of what is already cached. The git clone itself
is still cached in ~/.cache/uv/git-v0/, so only the first run after a
ref change pays the full network cost.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: rename OH_PUBLIC_SKILLS_PATH to PUBLIC_SKILLS_PATH

Aligns with the env var name used by the SDK PR.

Co-authored-by: openhands <openhands@all-hands.dev>

* test: update dev-safe buildAgentServerCommand tests for --reinstall and openhands-sdk

The git-ref path now adds --reinstall (so uv doesn't silently reuse cached
PyPI wheels) and includes openhands-sdk as a --with package so inter-package
APIs stay in sync. Update the two affected toEqual assertions to match.

Co-authored-by: openhands <openhands@all-hands.dev>

* refactor: use EXTENSIONS_REF instead of PUBLIC_SKILLS_PATH for extensions pinning

The PUBLIC_SKILLS_PATH / cloneExtensionsForSkillsCache approach was considered
but not chosen. EXTENSIONS_REF is the variable the agent-server SDK uses, and
it now skips network polling when the requested SHA is already in its cache.

- scripts/dev-safe.mjs: replace PUBLIC_SKILLS_PATH spread in buildAgentServerEnv
  with a simple EXTENSIONS_REF injection; remove cloneExtensionsForSkillsCache
  function, EXTENSIONS_REPO const, and the main() clone call; remove the
  spawnSync and rmSync imports that were only needed for cloning
- scripts/dev-with-automation.mjs: remove cloneExtensionsForSkillsCache import
  and the clone call from startAgentServer()
- docker/Dockerfile: remove the extensions-cache build stage entirely; keep the
  CONFIG_EXTENSIONS_REF line in config-gen (still needed for entrypoint.sh)
- docker/entrypoint.sh: replace the cp-based seeding block with a single
  export EXTENSIONS_REF="${EXTENSIONS_REF:-${CONFIG_EXTENSIONS_REF:-}}"
- AGENTS.md: replace inaccurate OH_PUBLIC_SKILLS_PATH bullet with an accurate
  description of the EXTENSIONS_REF flow

Co-authored-by: openhands <openhands@all-hands.dev>

---------

Co-authored-by: openhands <openhands@all-hands.dev>
2026-06-05 14:30:45 +00:00
Graham Neubig b847dd296f Bump agent-server default to 1.25.0 (#1161)
Merged by request after CI passed on the rebased branch.
2026-06-04 20:46:09 -04:00
7dbe8fd7de fix: populate <RUNTIME_SERVICES> in static builds so automations work (#1125)
* fix: populate <RUNTIME_SERVICES> in static builds so automations work

The agent's <RUNTIME_SERVICES> system-prompt block is built from
VITE_RUNTIME_SERVICES_INFO, which the dev launchers set at build time.
Static builds (the Docker image and the published binary) run
`npm run build` without it, so the block is dropped and the agent does
not know how to reach the local automation backend — it falls back to
the cloud API (app.all-hands.dev) and automation creation fails.

Inject the info at serve time, mirroring the existing --session-api-key
path:

- scripts/static-server.mjs: new --runtime-services-info flag injects
  the JSON as window.__AGENT_CANVAS_RUNTIME_SERVICES_INFO__
- src/api/agent-server-adapter.ts: parseRuntimeServicesInfo() falls back
  to that window global when the env var is empty
- docker/entrypoint.sh: builds the JSON from the sandbox-facing service
  URLs (AGENT_SERVER_URL, AUTOMATION_BASE_URL) and passes it to the
  static server(s)

Refs #1098

* refactor: extract runtime-services-info into a shared module

Replace the inline JSON building in docker/entrypoint.sh with a single
source of truth for the <RUNTIME_SERVICES> shape.

buildRuntimeServicesInfo now lives in scripts/runtime-services-info.mjs
(moved out of dev-safe.mjs, which re-exports it for back-compat) and
gains:
- optional full-URL overrides (agentServerUrl, automation.url) so the
  container can pass its runtime-resolved AGENT_SERVER_URL /
  AUTOMATION_BASE_URL and use 127.0.0.1 (avoiding IPv6 loopback) instead
  of build-time ports, and
- a CLI entrypoint so docker/entrypoint.sh emits the JSON by running the
  same builder the dev stack uses, rather than a hand-rolled node -e blob.

The Dockerfile ships the new dependency-free module into the image.

* fix: pass --runtime-services-info in static mode and verify in automation e2e

- startStaticFrontend() in dev-with-automation.mjs now builds the
  runtime-services info JSON via buildAutomationRuntimeServicesInfo()
  and passes it to static-server.mjs via --runtime-services-info.
  Without this, the npm binary / dev:static path served pre-built
  frontends that never populated the agent's <RUNTIME_SERVICES>
  system-prompt block (the Docker entrypoint already did this).

- The mock-LLM automation e2e test now verifies that the
  <RUNTIME_SERVICES> block is present in the system messages sent
  to the LLM, and that it includes the expected service entries
  (Agent Server, Automation backend, /api/automation).

Co-authored-by: openhands <openhands@all-hands.dev>

* docs: update AGENTS.md with runtime-services-info plumbing changes

Co-authored-by: openhands <openhands@all-hands.dev>

---------

Co-authored-by: Rohit Malhotra <rohitvinodmalhotra@gmail.com>
Co-authored-by: openhands <openhands@all-hands.dev>
2026-06-04 18:28:35 +00:00
9e8b068850 fix(dev): keep tmux sockets under ~/.openhands so macOS won't reap them (#1107)
dev-safe.mjs put the tmux socket dir at os.tmpdir(). On macOS that is the
per-user $TMPDIR (/var/folders/.../T), which the OS periodically reaps
(com.apple.bsd.dirhelper deletes entries untouched for a few days). The reaper
deletes the live tmux socket while the tmux server keeps running, orphaning it,
so every later new-window fails with:

  LibTmuxException: new-window: error connecting to
  .../openhands-agent-canvas-tmux/tmux-501/openhands (No such file or directory)

#325 moved the socket dir to os.tmpdir() to dodge a Unix-domain-socket failure
on mounted volumes, but its intent (per the commit message) was literally /tmp
-- os.tmpdir() != /tmp on macOS, and the per-user temp dir is exactly what gets
reaped.

Default to <stateDir>/tmux (~/.openhands/agent-canvas/tmux) instead, matching
where the rest of dev state already lives: persistent, on local disk, and never
reaped mid-session. dev-safe.mjs only ever runs on the host (the container
launches openhands-agent-server directly, never this script), so the socket dir
is always under a normal local home there.

For the rare host whose $HOME is on a socket-incompatible mount (devcontainers,
NFS/CIFS homes) -- the case #325 cared about -- honor the standard TMUX_TMPDIR
env var (which we already pass through to the agent-server's tmux) so it can be
pointed at a local path like /tmp. No new project-specific env var needed.

Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 16:07:13 +00:00
Tim O'Farrellandopenhands 5a3011c001 fix: align dev-stack OH_PERSISTENCE_DIR and automation DB path with Docker (#960)
* fix: align dev-stack OH_PERSISTENCE_DIR and automation DB path with Docker

Two path mismatches between `npm run dev` and Docker prevented config and
automation data from being shared across the two modes:

1. **OH_PERSISTENCE_DIR wrong (supersedes PR #959)**
   Docker's entrypoint.sh sets OH_PERSISTENCE_DIR to $HOME/.openhands
   (OPENHANDS_DIR). PR #959 added the env var to buildAgentServerEnv() but
   used config.stateDir (~/.openhands/agent-canvas) — one level too deep.
   Settings and secrets written by Docker live at ~/.openhands/settings.toml
   etc; dev wrote to ~/.openhands/agent-canvas/settings.toml.
   Fix: use path.dirname(config.stateDir) = ~/.openhands, matching Docker.

2. **Automation DB in wrong directory**
   dev-with-automation.mjs put the SQLite DB at
   ~/.openhands/agent-canvas/automations.db. Docker puts it at
   ~/.openhands/automation/automations.db (from config/defaults.json
   paths.automationDb = "automation/automations.db" relative to OPENHANDS_DIR).
   Fix: use join(dirname(config.stateDir), SHARED_DEFAULTS.paths.automationDb)
   so both modes resolve to ~/.openhands/automation/automations.db.
   Also ensure the directory is created on startup (mirrors Docker's mkdir -p).

3. **Mock-LLM test cleanup**
   Update playwright.mock-llm.config.ts to clean both STATE_DIR and the new
   AUTOMATION_DB_DIR (.tmp/automation/) before each test run, since the DB now
   lives outside STATE_DIR.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: use dev_conversations dir for npm to isolate from Docker conversations

npm dev mode now writes conversations to ~/.openhands/agent-canvas/dev_conversations
instead of conversations, keeping them separate from Docker's conversations dir.

OH_CONVERSATIONS_PATH (set via buildAgentServerEnv) is the single control point;
all mkdir and releaseStaleConversationLeases calls updated to match.

Also condense verbose multi-line comments on OH_PERSISTENCE_DIR and
AUTOMATION_DB_URL to single lines.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: use join(config.stateDir, dev_conversations) in ensureDirectories

The outer config in dev-with-automation.mjs does not have conversationsPath
(that is a SafeDevConfig property). Use the same join(stateDir, ...) pattern
as the other dirs in the list.

Also removes the debug console.log and restores dev-safe.mjs and dev-static.mjs
to dev_conversations after the manual revert.

Co-authored-by: openhands <openhands@all-hands.dev>

* test: update conversationsPath assertion to match dev_conversations

The implementation in buildConfigFromPorts was changed to use
'dev_conversations' as the subdirectory name, but the corresponding
test assertion was not updated.

Co-authored-by: openhands <openhands@all-hands.dev>

---------

Co-authored-by: openhands <openhands@all-hands.dev>
2026-06-02 15:26:19 +00:00
Tim O'Farrellandopenhands d0994a6fe1 fix: use X-Session-API-Key for local automation auth in prompts and RUNTIME_SERVICES (#999)
Fixes #980

The agent prompt in recommended-automations-launcher and the
RUNTIME_SERVICES block in agent-server-adapter both advertised
X-API-Key as the auth header for the local automation backend.
The automation service (openhands-automation) does not accept
X-API-Key — it accepts Authorization: Bearer and X-Session-API-Key.

X-Session-API-Key is the established local convention: the agent
server uses it, the frontend automation API client uses it (with an
explicit comment that both backends share the same header), and
auth.py describes it as matching that convention. Update both call
sites and the corresponding test assertion to use X-Session-API-Key.

Co-authored-by: openhands <openhands@all-hands.dev>
2026-06-01 15:10:30 -06:00
14b1b1e8ad feat: two auth modes — local (auto-key) and public (paste-key) (#790)
* feat: two auth modes — local (auto-key) and public (paste-key)

Local mode (agent-canvas, no flags):
- Ingress binds to 127.0.0.1 only
- Auto-generates session API key
- Writes /backends.json to static dir so frontend auto-authenticates
- Zero setup for localhost use

Public mode (agent-canvas --public):
- Ingress binds to 0.0.0.0 (all interfaces)
- Requires LOCAL_BACKEND_API_KEY env var
- Does NOT write /backends.json
- Frontend shows API key entry screen on 401

Co-authored-by: openhands <openhands@all-hands.dev>

* refactor: reuse BackendForm in public-mode API key entry screen

Replace the bespoke ApiKeyEntryScreen form with BackendForm configured
for the public-auth use case:

- Host field is auto-filled from window.location.origin and read-only
- Name field is hidden (auto-derived from the existing backend)
- Only the API key input is exposed to the user
- Uses the same SettingsInput / BrandButton components as the backend
  connection modals for visual consistency

BackendForm gains three optional props to support this:
- hideName: hides the name input and uses a fallback name
- hostReadOnly: disables the host input
- onSubmitPayload: receives the submitted payload for side-effects
  (the API key screen uses it to persist to agent-server-config and
  reload the page)

Co-authored-by: openhands <openhands@all-hands.dev>

* docs: update AGENTS.md with ApiKeyEntryScreen BackendForm reuse details

Co-authored-by: openhands <openhands@all-hands.dev>

* feat: implement public mode auth flow (--public flag)

- Add --public flag to dev-with-automation.mjs and bin/agent-canvas.mjs
- In public mode: require LOCAL_BACKEND_API_KEY, use as session key,
  don't bake into frontend (no VITE_SESSION_API_KEY / --session-api-key)
- Add isAgentServerAuthError() to detect 401 from /server_info probe
- root.tsx shows ApiKeyEntryScreen when 401 detected (lazy loaded)
- useConfig skips retries on 401 for instant auth screen display
- ApiKeyEntryScreen now has default export for React.lazy compatibility

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: use VITE_AUTH_REQUIRED flag instead of 401 detection for public mode

The 401-based approach was unreliable — /server_info may not require
auth on all server versions. Instead:

- dev-with-automation.mjs sets VITE_AUTH_REQUIRED=true in public mode
- isAuthRequiredAndMissing() checks the flag + localStorage for a key
- root.tsx gates on the flag BEFORE the /server_info probe, so the
  auth screen appears instantly with zero network round-trips
- 401 fallback kept as safety net for edge cases

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: handle stale key via 401 detection in public mode

When the server restarts with a new LOCAL_BACKEND_API_KEY, the browser
still has the old key in localStorage. isAuthRequiredAndMissing() returns
false (key exists), so the /server_info probe fires and 401s.

isAgentServerAuthError() now checks VITE_AUTH_REQUIRED=true AND 401
status, so it only triggers in public mode (a 401 in local mode is a
misconfiguration, not a key-rotation event). useConfig skips retries
on 401 to show the auth screen immediately.

Two gates, one screen:
- No key at all → flag check, instant, no network
- Stale key → /server_info 401, one round-trip

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: validate stale keys against GET /api/settings (protected)

/server_info is unprotected — it returns 200 even with a wrong key.
In public mode, after the /server_info probe succeeds, we now hit
GET /api/settings to verify the stored key is still valid. A 401
from that endpoint triggers the auth screen via isAgentServerAuthError().

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: rewrite ApiKeyEntryScreen — validate before save, always empty key, match add-modal UI

Three fixes:

1. Stale key conflict: The form now always starts with an empty API key
   field instead of pre-filling from the backend registry. Stale
   credentials from a previous session never bleed into the input.

2. Wrong key indicator: On submit, the key is validated against
   GET /api/settings (protected endpoint) BEFORE persisting. Wrong
   keys show an inline red status dot + 'Invalid API key' error
   via BackendStatusDot. Only validated keys trigger the reload.

3. UI parity with add-backend modal: Replaced BackendForm wrapper
   with direct SettingsInput fields matching ManualConnectionColumn's
   layout — host (read-only + helper text), API key (password with
   placeholder), status indicator, and Connect button. No cloud
   OAuth column.

New i18n key: AUTH$INVALID_KEY (all 15 languages).

Co-authored-by: openhands <openhands@all-hands.dev>

* feat: match add-backend modal UI + add test coverage

ApiKeyEntryScreen now renders the exact same card chrome as
BackendFormModal add-mode: same title ('Add a Backend'), same
Name/Host/API Key fields, same Connect button styling. Host is
pre-filled and read-only; no cloud OAuth column.

New tests (12 total):
- api-key-entry-screen.test.tsx (7 tests):
  - UI field parity with add-backend modal
  - Stale key wipe (empty API key field despite stale localStorage)
  - Connect disabled until name + key filled
  - Valid key: validates → persists → reloads
  - Invalid key: error indicator, no persist, no reload
  - Retry flow: wrong key → error → correct key → success
  - Stale key isolation: only fresh key persisted
- agent-server-config.test.ts (5 new tests for isAuthRequiredAndMissing):
  - Flag unset → false
  - Flag set, no key → true
  - Flag set, localStorage key → false
  - Flag set, VITE_SESSION_API_KEY → false
  - Flag not 'true' → false

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: distinguish 401 from other errors in ApiKeyEntryScreen

The catch-all was showing 'Invalid API key' for EVERY failure —
including 500s, network errors, and timeouts — even when the key
was correct. Now:

- 401 → 'Invalid API key. Please check the key and try again.'
- Anything else → 'Connection failed: <actual error message>'

This reveals the real problem when a correct key fails for a
non-auth reason (e.g. server misconfiguration, missing OH_SECRET_KEY).

New i18n key: AUTH$CONNECTION_FAILED (all 15 languages).
New test: non-401 errors show 'Connection failed' + detail.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: agent-server receives wrong session key in public mode

startAgentServer() called buildSafeDevConfig() which generated its
own random session key, ignoring config.sessionApiKey (which holds
LOCAL_BACKEND_API_KEY in public mode). The agent-server was started
with a random key while users were told to paste the LOCAL_BACKEND_API_KEY
value — every key was rejected with 401.

Fix: override OH_SESSION_API_KEYS_0 in the agent-server env with
config.sessionApiKey so both the agent-server and the frontend
agree on which key is valid.

Co-authored-by: openhands <openhands@all-hands.dev>

* refactor: address review comments on ApiKeyEntryScreen

1. Remove dead BackendFormProps (hideName, hostReadOnly, onSubmitPayload)
   — ApiKeyEntryScreen is standalone so no caller used these props.

2. Auto-generate backend name from window.location.hostname instead of
   requiring users to type one. Only the API key field is required now,
   reducing public-mode auth to a single-field flow.

3. Simplify redundant ternary: connectionStatus === 'success' ? true : false
   → connectionStatus === 'success'.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: address second round of review comments

1. Use shared isSdkHttpError() helper in ApiKeyEntryScreen instead of
   duplicating the SDK error shape check inline. Exported the helper
   from agent-server-compatibility.ts.

2. Add code comment acknowledging the edge case where a network hiccup
   between /server_info and getSettings() probes lets the app load with
   an unvalidated key. Acceptable since the window is narrow and a page
   refresh recovers.

3. Add --auth-required flag to static-server.mjs so pre-built static
   binaries (npx @openhands/agent-canvas --public) show the API key
   entry screen without needing VITE_AUTH_REQUIRED baked in at build
   time. The flag injects window.__AGENT_CANVAS_AUTH_REQUIRED__=true
   into index.html at runtime. Frontend isAuthRequired() checks both
   the build-time env var and the runtime window flag.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: use double cast (unknown) to satisfy strict TS on window flag access

window cannot be cast directly to Record<string, unknown> — TypeScript
requires going through unknown first for unrelated types.

  (window as unknown as Record<string, unknown>).__AGENT_CANVAS_AUTH_REQUIRED__

This fixes the CI typecheck failure introduced in aa76c01a.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: address remaining review comments on auth modes PR

- Use isAuthRequired() instead of raw import.meta.env.VITE_AUTH_REQUIRED
  in isAgentServerAuthError() so the runtime window flag injected by
  static-server.mjs in pre-built binaries is also honoured (bug fix).

- Preserve existing backend name during re-authentication flow in
  ApiKeyEntryScreen; only fall back to window.location.hostname for
  the initial entry so users don't lose custom labels on key rotation.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: restore MCP-to-integrations migration from main

A prior merge into this branch incorrectly kept the old
@openhands/extensions/mcps imports instead of the
@openhands/extensions/integrations paths introduced by d41bfe15
on main. Restore all affected files from origin/main so the
extensions package (which no longer exports ./mcps) resolves
correctly.

Files restored from main:
- src/utils/mcp-marketplace-utils.ts
- src/routes/mcp.tsx
- src/components/features/mcp-logo-badge.tsx
- src/components/features/mcp-page/* (6 files)
- src/components/features/automations/* (2 files)
- __tests__/ (4 test files)

Co-authored-by: openhands <openhands@all-hands.dev>

* style: fix prettier formatting and remove unused eslint-disable in api-key-entry-screen

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: address remaining PR review comments

- Extract isSdkHttpStatusError() helper in agent-server-compatibility.ts
  to DRY up the SDK error status check (review comment #3321202503).
  Both isAgentServerAuthError() and ApiKeyEntryScreen now use it.

- Use AUTH i18n keys in api-key-entry-screen.tsx:
  • Heading: AUTH$API_KEY_REQUIRED_TITLE ('API Key Required')
  • Description: AUTH$API_KEY_REQUIRED_DESCRIPTION added below heading
  • Button: AUTH$CONNECT ('Connect')
  (review comments #3325478721, #3325478729)

- Fix nested <main> landmark in root.tsx: remove the outer <main>
  wrapper since ApiKeyEntryScreen already provides its own semantic
  container (review comment #3325478704).

- Change ApiKeyEntryScreen root element from <main> to <div> so
  the Layout's own landmarks are not violated.

Co-authored-by: openhands <openhands@all-hands.dev>

* test: add coverage for window.__AGENT_CANVAS_AUTH_REQUIRED__ runtime flag

Add isAuthRequired() test block covering the window flag path used by
pre-built static binaries (static-server.mjs --auth-required). Also add
window-flag variants to isAuthRequiredAndMissing() tests.

Addresses review comment #3325587577.

Co-authored-by: openhands <openhands@all-hands.dev>

* refactor!: deduplicate SESSION_API_KEY into LOCAL_BACKEND_API_KEY

BREAKING CHANGE: The user-facing env var for setting the API key is now
`LOCAL_BACKEND_API_KEY` everywhere. The old `SESSION_API_KEY`,
`OH_SESSION_API_KEYS_0`, and `VITE_SESSION_API_KEY` env vars are no
longer read by launchers as user-facing configuration.

Internal plumbing (`config.sessionApiKey`, `VITE_SESSION_API_KEY` build
injection, `OH_SESSION_API_KEYS_0` agent-server env) is unchanged — only
the user-facing surface is unified into a single env var.

Changes:
- scripts/dev-safe.mjs: read LOCAL_BACKEND_API_KEY instead of
  SESSION_API_KEY / OH_SESSION_API_KEYS_0 / VITE_SESSION_API_KEY
- scripts/dev-with-automation.mjs: unify key resolution through
  buildSafeDevConfig for both public and local modes
- bin/agent-canvas.mjs: update CLI help text and examples
- docker/entrypoint.sh: read LOCAL_BACKEND_API_KEY, migrate legacy
  session-api-key.txt → api-key.txt
- scripts/static-server.mjs: add mutual-exclusion guard for
  --session-api-key + --auth-required flags
- playwright configs: pass LOCAL_BACKEND_API_KEY instead of the old trio
- test helpers: prefer LOCAL_BACKEND_API_KEY fallback chain
- Update tests and documentation

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: address review comments — narrow settings probe rethrow and use shared client options

- loadAgentServerInfo: narrow getSettings() catch to rethrow only 401
  errors. Other HTTP errors (403, 5xx) and non-HTTP errors (network,
  timeout) are now swallowed with a console.warn, since the server is
  confirmed up (via /server_info) and the probe is best-effort. This
  prevents misconfigured servers from silently falling through to
  <Outlet /> without showing either the auth or unavailable screen.

- ApiKeyEntryScreen: replace hand-rolled SettingsClient options with
  getAgentServerClientOptions() so transport-level settings (e.g.
  VITE_INSECURE_SKIP_VERIFY) are honoured. Uses the sessionApiKey
  override to pass the freshly-entered key.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: rewrite git+ssh to git+https for @openhands/extensions in lockfile

npm normalizes GitHub URLs to git+ssh:// in the lockfile, but machines
without SSH keys for GitHub (or with stale npm caches) can end up
installing a wrong version of the package. This causes the Vite resolve
error:

  "./integrations" is not exported under the conditions [...]

The same pattern was already fixed for @openhands/typescript-client
(see #384). vercel-install.sh already does a blanket sed rewrite, but
the committed lockfile itself should use git+https:// so local npm ci
works without SSH keys.

Co-authored-by: openhands <openhands@all-hands.dev>

* refactor: align public auth screen with Add Backend form layout

Replace the custom 'API Key Required' screen with the same form
layout used by the 'Add a Backend' left column in BackendFormModal:

- Heading changed from 'API Key Required' to 'Add a Backend'
- Added backend Name field (required, same as ManualConnectionColumn)
- Host field remains pre-filled and disabled (from window.location.origin)
- API Key field unchanged
- Submit button now uses BACKEND$CONNECT label (matching the modal)
- Removed the subtitle description paragraph for cleaner parity
- Name is persisted to the backend registry on submit

Tests updated: fillApiKey → fillRequiredFields (name + apiKey),
assertions cover the new name field and dual-field submit gating.

Co-authored-by: openhands <openhands@all-hands.dev>

* chore: remove unused AUTH$ i18n keys from this PR

The UI refactor (02faa0b0) switched ApiKeyEntryScreen to the
BACKEND$* keys. Drop the three AUTH$ entries that were introduced
and then superseded within this same PR:

- AUTH$API_KEY_REQUIRED_TITLE
- AUTH$API_KEY_REQUIRED_DESCRIPTION
- AUTH$CONNECT

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: sync stale session API key on boot when LOCAL_BACKEND_API_KEY changes

When a user restarts the stack with a different LOCAL_BACKEND_API_KEY,
the new VITE_SESSION_API_KEY is baked in correctly, but localStorage
may still hold the old key in two places:

  1. openhands-agent-server-config.sessionApiKey (written by onboarding
     or the Settings page)
  2. openhands-backends[].apiKey (seeded on first load, never re-synced)

The existing syncDefaultLocalBackendAuth() in storage.ts already tries
to fix #2 by comparing against makeDefaultLocalBackend(), but that
function reads through getConfiguredSessionApiKey() which hits #1
(stale localStorage) before falling back to VITE_SESSION_API_KEY.
So a stale #1 defeats the #2 sync.

Fix: add syncBakedSessionApiKey() which runs from readStoredBackends()
before any key resolution. When VITE_SESSION_API_KEY is set and the
stored key in openhands-agent-server-config differs, overwrite it.
This ensures getConfiguredSessionApiKey() and makeDefaultLocalBackend()
both return the correct key, and the downstream backend-registry sync
works as intended.

Also fix the static-server.mjs injection script to always overwrite a
stored key that differs from the runtime key (was guarded by
`if(!_c.sessionApiKey)` which skipped updates when any key existed).

Add mock-LLM E2E tests for:
- Key rotation recovery: seeds stale localStorage, verifies app loads
- Public-mode auth gate: tests auth screen visibility, wrong key
  rejection, and correct key acceptance

Co-authored-by: openhands <openhands@all-hands.dev>

* docs: document key rotation resilience in AGENTS.md

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: add syncBakedSessionApiKey to vi.mock stubs and use click-then-fill in E2E

Three test files mock #/api/agent-server-config without exporting
syncBakedSessionApiKey, which storage.ts now calls at import time.
Add the missing vi.fn() stub to all three.

Also fix the public-mode auth E2E test: use the click() → fill()
pattern for React controlled inputs (matching the established
convention in mock-llm-conversation.spec.ts) so the SettingsInput
onChange fires reliably in Playwright.

Co-authored-by: openhands <openhands@all-hands.dev>

* test: add public-mode key rotation E2E test

Simulates a server key rotation: localStorage holds a stale key from
a previous session, the server now has a new key. Verifies the app
detects the 401 from the stale key probe, shows the auth screen, and
accepts the new key.

Flow: stale key in localStorage → probe /server_info → 401 →
isAgentServerAuthError → ApiKeyEntryScreen → user pastes new key →
reload → app loads normally.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix(e2e): suppress consent modal in public-mode auth tests

The analytics consent modal overlays the auth screen on first visit
(clean localStorage). Playwright's click() on the form inputs was
intercepted by the modal overlay, causing a 60s timeout loop (121
retries). Add a beforeEach that seeds 'analytics-consent' and
'openhands-telemetry-consent' in localStorage before navigation.

Also deduplicate the consent seeding from the key-rotation test's
addInitScript since the beforeEach now handles it.

Co-authored-by: openhands <openhands@all-hands.dev>

* refactor: deduplicate readStoredConfig() call in syncBakedSessionApiKey

Capture the first readStoredConfig() result and reuse it in the
spread instead of hitting localStorage twice.

Co-authored-by: openhands <openhands@all-hands.dev>

* chore(docker): remove legacy session-api-key.txt migration

The backwards-compatibility shim that migrated the old
session-api-key.txt to api-key.txt is no longer needed — a breaking
change here is acceptable.

Co-authored-by: openhands <openhands@all-hands.dev>

* refactor: dedup ApiKeyEntryScreen against BackendForm

ApiKeyEntryScreen now renders BackendForm with three new props instead
of reimplementing the name/host/API-key inputs from scratch:

- hostReadOnly: locks the host field (pre-filled from window.origin)
- requireApiKey: forces a non-empty API key for local backends
- onSubmitOverride: replaces the default sync persist with async
  server-side validation (GET /api/settings) before persisting

The auth-gate-specific chrome (full-screen wrapper, connection status
indicator, validating/error state) stays in ApiKeyEntryScreen via the
existing renderActions slot.

Co-authored-by: openhands <openhands@all-hands.dev>

---------

Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: chuckbutkus <chuck@openhands.dev>
2026-06-01 12:02:37 -06:00
Tim O'Farrellandopenhands 95b6d7a23e fix: persist OH_SECRET_KEY to secret-key.txt in dev mode, same as Docker (#957)
Dev mode (npm run dev) was always using the hardcoded static default key
'openhands-dev-secret-key-change-in-prod' from config/defaults.json.
Docker mode (docker/entrypoint.sh) generates a random key on first run and
persists it to ~/.openhands/agent-canvas/secret-key.txt.

When both modes share the same ~/.openhands directory, they used different
keys — causing decryption failures for any settings encrypted by the other
mode.

Fix: remove the static default in dev-safe.mjs and instead use
getOrCreatePersistedApiKey() with a new DEFAULT_SECRET_KEY_PATH constant
pointing to the same secret-key.txt file that Docker reads/writes.
Whichever mode runs first generates and persists the key; the other picks
it up automatically on next start.

- Add DEFAULT_SECRET_KEY_PATH export to dev-safe.mjs
- Replace 'env.OH_SECRET_KEY || DEFAULT_SECRET_KEY' with
  'env.OH_SECRET_KEY || getOrCreatePersistedApiKey(secretKeyPath, "secret")'
- Update startup log to show persisted file path (not 'default (for local development)')
- Remove now-unused 'defaults.secretKey' from config/defaults.json
- Update AGENTS.md to reflect the new shared-file behavior

Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-30 06:36:47 -06:00
Tim O'Farrellandopenhands eb3ae22b23 fix: fast-fail dev scripts when ports are already in use (#939)
* fix: fast-fail dev scripts when ports are already in use

Add `assertPortsFree` to `scripts/dev-safe.mjs` that checks each
preferred port and throws a descriptive error if any are already bound,
rather than silently binding to an alternative port.

- `buildSafeDevConfigAsync` now calls `assertPortsFree` before
  returning config, so `npm run dev:minimal` exits immediately with a
  clear message when the agent-server port is occupied.
- `dev-with-automation.mjs`'s `buildConfig` does the same for all four
  service ports (ingress, agent-server, automation, vite), covering
  `npm run dev` and `npm run dev:static`.
- `buildConfig` gains env-var overrides for internal service ports
  (`OH_CANVAS_SAFE_BACKEND_PORT`, `OH_CANVAS_SAFE_AUTOMATION_PORT`,
  `OH_CANVAS_SAFE_VITE_PORT`) so tests and advanced users can redirect
  ports without touching production defaults.

Tests updated accordingly:
- Old "falls back when port is busy" tests replaced with "throws when
  port is busy" equivalents.
- New `assertPortsFree` describe block with three targeted cases.
- `envWithIsolatedKeyPath` in `dev-with-automation.test.ts` now seeds
  high free ports so the pre-flight check passes when a real dev stack
  is running during local test execution.

Closes #934

Co-authored-by: openhands <openhands@all-hands.dev>

* refactor: address review bot suggestions on assertPortsFree

- Check ports in parallel with Promise.all instead of sequentially
  (each check is independent I/O, so parallel is faster and more idiomatic)
- Include vscode port in the buildSafeDevConfigAsync pre-flight check
  alongside the agent-server port, so a conflict on that internal port
  is also caught with a helpful message rather than a cryptic spawn error

Co-authored-by: openhands <openhands@all-hands.dev>

---------

Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-29 17:25:52 +00:00
665a258b80 feat(acp): inline live model picker for ACP conversations (#769) (#832)
* feat(acp): inline live model picker for ACP conversations (#769)

Converge ACP model selection onto the native LLM-profile inline picker UX
with live mid-conversation switching, replacing the display-only popover.

- Bump @openhands/typescript-client 1.23.3 -> 1.24.0 (adds switchAcpModel).
- AgentServerConversationService.switchAcpModel(conversationId, model): POST
  /switch_acp_model via ConversationClient, with switchProfile's local-only guard.
- useSwitchAcpModel hook: live switch for a running ACP session; for the
  home/no-session case, persist the choice as the agent-settings default
  (agent_settings_diff { acp_model }) so the next conversation inherits it.
- ChatInputModel popover becomes a picker over the provider's available_models
  (check on the effective model), local backend only; cloud / custom-provider /
  native surfaces keep the display + Settings link.
- New i18n key MODEL$AVAILABLE_MODELS.
- Tests for the hook (live vs settings-default branches) and the picker.

Local backend only (matches native switching); custom/unknown providers and any
app_server route remain out of scope per #769.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(acp): open the model picker on click (don't self-close via click-outside)

The inline picker's trigger button sits outside the popover element, so the
document click-outside handler (useClickOutsideElement) treated the opening
click as an "outside" click and closed the popover in the same interaction —
clicking the chip appeared to do nothing. (A programmatic el.click() worked by
fluke: the popover isn't rendered yet when that click bubbles, so the ref is
null and the close is skipped.)

Pass the trigger button as the hook's ignoreOutsideClickRef so a click on the
chip toggles the popover instead of being treated as an outside click.

Validated end-to-end against a local agent-server 1.24.0: the picker opens and
lists the provider's available_models, and selecting one writes the default via
PATCH /settings (home case), with the chip updating to the new model.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(acp): drop disableToast in useSwitchAcpModel so switch errors surface

useSwitchLlmProfile sets meta.disableToast because it's wrapped by
useSwitchLlmProfileAndLog, which re-surfaces errors via its own onError.
useSwitchAcpModel is called directly (no such wrapper / no onError), so
disableToast was silently swallowing failed switches and settings writes
(e.g. a 409 before the first message, network errors, the cloud guard).

Remove it and let the global mutation error toast report failures — simpler
and gives the user feedback when a switch doesn't take.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(acp): share chat input model picker state

* chore: address PR review feedback (#832)

- Add unit test for useChatInputModelState pinning its branching contract,
  incl. the active-ACP getAcpProvider lookup (was home-only in old component).
- Document why the overflow model submenu uses overflow-y-auto (scroll long
  model lists) rather than overflow-visible — no floating children to clip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: address PR review feedback (#832)

- Wrap the 'Available models' section label in a presentational <li> so it
  is a valid child of the ContextMenu <ul> (was a bare <div>).
- Drop unnecessary 'as never' casts in use-switch-acp-model tests now that
  the real return types (Promise<void>, Promise<boolean>) are honored.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(acp): bump agent-server pin to 1.24.0 for /switch_acp_model

The inline ACP model picker POSTs to /api/conversations/{id}/switch_acp_model,
which is new in openhands-agent-server 1.24.0. The PR description already
lists agent-server:1.24.0 as a dependency, but config/defaults.json was
left at 1.23.1, so local dev (npm run dev) and Docker installs would 404
on every model switch attempt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(settings): always land on /settings/agent from /settings

The fallback order in ``getFirstAvailablePath`` put ``/settings/llm``
first whenever ``hide_llm_settings`` was off, so clicking Settings sent
the user to the LLM page. For ACP users that page is disabled and
``redirectIfAcpActive`` only catches them when the *personal* settings
already say ``agent_kind === "acp"`` — being in an ACP conversation
with non-ACP personal settings (the common case during the inline
picker flow) bypassed the guard and dumped them on /settings/llm.

Make ``/settings/agent`` the unconditional first fallback. It is
always available (no feature flag hides it), houses the agent-kind
picker, and the left nav still gets OpenHands users to LLM in one
click — so one extra click for non-ACP users buys a much simpler
routing surface and kills the ACP misroute.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(settings/agent): clear command when switching to Custom preset

Selecting "Custom" in the agent preset dropdown reset ``acpModel`` and
flipped ``isCustomAcpModel`` but left ``commandText`` untouched. On the
next render, ``detectPreset(commandText, ACP_PROVIDERS)`` still matched
the previous provider's ``default_command`` and snapped the dropdown
back off "Custom" — the toggle never stayed on Custom.

Clear ``commandText`` in the Custom branch so ``detectPreset`` falls
through to ``ACP_CUSTOM_PRESET_KEY`` on the next render and the dropdown
stays where the user put it. Empty command also matches the intended
"user supplies their own" semantics of the preset.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(settings): mark Verification page as disabledByAcp

The Verification page writes ``confirmation_mode`` and
``security_analyzer`` into ``conversation_settings_diff``. The ACP
agent loop never reads either: ``openhands/sdk/agent/acp_agent.py``
has zero references to ``confirmation_policy`` or
``security_analyzer``, and the only runtime readers
(``openhands/sdk/agent/agent.py:844,855``) live on the native
``Agent`` class — not on ``ACPAgent``. The backend accepts the values
and stores them on conversation state, but the ACP subprocess never
consults them.

So the page presents real-looking knobs that silently do nothing for
ACP users. Mark it ``disabledByAcp: true`` — same pattern as
``/settings/llm`` and ``/settings/condenser`` — so it greys out in the
nav and the existing route guard at ``src/routes/settings.tsx:47-51``
bounces direct visits to ``/settings/agent``.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(ci): bump doc/script SDK version examples to 1.24.0

The docs-version-sync test enforces that every documented agent-server
version example matches ``config/defaults.json:versions.agentServer``.
The previous commit bumped that pin from 1.23.1 to 1.24.0 for the
``/switch_acp_model`` route, but left the example references in
AGENTS.md, ``scripts/dev-safe.mjs``, and ``scripts/check-sdk-version-sync.mjs``
behind — the drift-detector caught it as ``test-and-build`` failure.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(ci): bump remaining hard-coded 1.23.1 to 1.24.0

``__tests__/scripts/dev-safe.test.ts`` asserts ``buildAgentServerCommand``'s
default ``uvx`` args literally include ``openhands-agent-server==1.23.1`` and
matching ``openhands-{sdk,tools,workspace}==1.23.1``. The CI fix in the prior
commit only updated docs and example references; the central pin bump in
``config/defaults.json`` flowed through to this test's runtime expectation but
the literal expectations were never updated. Bump them.

Also bump the ``MOCK_AGENT_SERVER_VERSION`` placeholder in
``src/mocks/settings-handlers.ts`` for consistency with the central pin —
no test asserts on it, but leaving the mock at 1.23.1 invites future
drift confusion.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Debug Agent <debug@example.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 16:42:17 +02:00
b570b4a2f6 fix: buildNpmScriptCommand always uses cmd.exe on Windows (#734)
* fix: buildNpmScriptCommand always uses cmd.exe on Windows

On Windows, npm sets npm_execpath to a path like
  C:\Program Files\nodejs\node_modules\npm\bin\npm-cli.js
which contains spaces. buildNpmScriptCommand was returning that
path as a spawn argument with the full node.exe path as the command.
spawnService uses shell:true on Windows, so Node.js passes the
unquoted command to cmd.exe:

  cmd.exe /d /s /c C:\Program Files\nodejs\node.exe ...

cmd.exe splits on the space and fails with
  'C:\Program' is not recognized as an internal or external command
causing Vite to exit with code 1 immediately after npm run dev.

Fix: check platform === 'win32' BEFORE checking npm_execpath so
Windows always uses the safe cmd.exe /d /s /c npm run <script> form.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: buildNpmScriptCommand always uses cmd.exe on Windows

On Windows, npm sets npm_execpath to a path like
  C:\Program Files\nodejs\node_modules\npm\bin\npm-cli.js
which contains spaces. buildNpmScriptCommand was returning that
path as a spawn argument with the full node.exe path as the command.
spawnService uses shell:true on Windows, so Node.js passes the
unquoted command to cmd.exe:

  cmd.exe /d /s /c C:\Program Files\nodejs\node.exe ...

cmd.exe splits on the space and fails with
  'C:\Program' is not recognized as an internal or external command
causing Vite to exit with code 1 immediately after npm run dev.

Fix: check platform === 'win32' BEFORE checking npm_execpath so
Windows always uses the safe cmd.exe /d /s /c npm run <script> form.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: AnimatePresence mode=wait expects one child, not two

ChatStatusIndicator had two separately-keyed motion.span children inside
<AnimatePresence mode="wait">. framer-motion's wait mode expects exactly ONE
child to exit before the next enters; two children trigger the repeated warning:
  "attempting to animate multiple children within AnimatePresence,
   but its mode is set to 'wait'"

Fix: wrap both elements in a single motion.span with unified key={status}
and className="contents" (CSS display:contents preserves flex layout).

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: set PYTHONUTF8=1 in agent-server/automation env on Windows

Python on Windows defaults to the system ANSI codepage (cp1252). The agent-server
writes metadata JSON containing emoji (e.g. U+2705 ✅) that cp1252 cannot encode,
producing UnicodeEncodeError → POST /api/conversations 500. Old UTF-8 conversation
files also fail to load at startup (UnicodeDecodeError). PYTHONUTF8=1 enables
Python's UTF-8 mode (PEP 540) for the process, matching Linux/macOS behaviour.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: set PYTHONUTF8=1 in agent-server/automation env on Windows

Python on Windows defaults to the system ANSI codepage (cp1252). The agent-server
writes metadata JSON containing emoji (e.g. U+2705 ✅) that cp1252 cannot encode,
producing UnicodeEncodeError → POST /api/conversations 500. Old UTF-8 conversation
files also fail to load at startup (UnicodeDecodeError). PYTHONUTF8=1 enables
Python's UTF-8 mode (PEP 540) for the process, matching Linux/macOS behaviour.

Co-authored-by: openhands <openhands@all-hands.dev>

* refactor: remove unrelated file

---------

Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: hieptl <hieptl.developer@gmail.com>
2026-05-28 16:29:55 +07:00
Rohit Malhotraandopenhands 45da5606d6 fix: bump agent-server SDK to 1.23.1 (#782)
Fixes two bugs in the upstream agent-server SDK v1.23.0 that caused
500 Internal Server Error on POST /api/conversations:

1. LLM registry duplicate usage_id: The condenser and main agent LLMs
   both used usage_id='default', causing ValueError on conversation
   creation. v1.23.1 checks for existing usage IDs before registering.

2. Validation error handler crash: The _validation_exception_handler
   tried to JSON-serialize raw ValueError objects from Pydantic
   validation contexts, turning 422 errors into 500s. v1.23.1 properly
   sanitizes validation errors before serializing.

Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-26 18:42:17 +00:00
Tim O'Farrellandopenhands c9616c1153 fix: pin openhands-sdk to same version as other SDK packages (#772)
Add openhands-sdk==${VERSION} to the --with list in both PyPI
resolution branches (specific version and default) of
buildAgentServerCommand so all four packages are pinned to the
same versions.agentServer:

  openhands-agent-server, openhands-sdk, openhands-tools, openhands-workspace

Without this pin, openhands-sdk floated to the latest release on
PyPI (a transitive dep with no version bound in the published
openhands-agent-server metadata), causing non-reproducible builds
and version skew between the banner and defaults.json.

The local-path (editable) and git-ref branches are unaffected —
they already source all packages from the same checkout/ref.

Update the two affected unit-test cases to expect the new pinned
openhands-sdk entry in the args array.

Fixes #767

Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-26 15:38:53 +00:00
Hiep Le e54b04cf38 chore: refresh stale 1.22.1 doc examples after agent-server bump to 1.23.0 (#694) 2026-05-21 12:49:04 +07:00
59058c7609 fix(ui): left navigation rail, mobile drawer, and responsive chrome (#623)
* fix: archived row icon, default-local config sync, and dockerless dev fixes

Archived MISSING sandboxes show an archive icon in the status column instead of a gray dot and pill; ERROR sandboxes keep the error pill. Harden port checks and automation CORS for alternate frontend ports, set agent-server HOME to the host home on macOS, sync legacy agent-server config when editing the default-local backend, and clarify static stack rebuild/skip-build behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(ui): sidebar conversation list flush to rail with stable gutter

Drop expanded aside `md:pr-0` so the thread scrollbar aligns with the rail
while nav, logo row, and footer keep horizontal padding. Use scrollbar-gutter
on the conversation list for consistent right inset with or without overflow.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(ui): 40px backend selector and non-italic combobox text

Pin the backend Dropdown trigger to h-10, add an italicPlaceholder escape
hatch, and force upright type for value/placeholder in the selector. Add a
subtle top border above Add Workspace in the new-conversation menu.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(ui): full-bleed sidebar footer divider to left rail

Pull the backend block border past aside `pl-2` with matching width calc;
keep inner `px-2` so controls stay aligned with nav.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(ui): align context menus with sidebar filter menu and muted row icons

Match list padding, border, and shadow to the conversations filter surface;
use theme foreground/muted tokens; reserve leading icons as muted until row
hover/focus. Simplify list-item rows and remove unused context-menu height
constant.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(ui): unify menu dividers, section labels, and filter actions

Standardize full-bleed menu dividers via Divider inset="menu", give filter
menu section headings consistent pt-1 padding, and add icons to hide/show
and delete-all rows in the conversation panel filter menu.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(ui): rename sidebar conversations link to New Chat

The /conversations nav entry should read "New Chat" instead of "Code" to match user expectations for starting a conversation.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(ui): polish cloud new-thread popover search and repo list

Use a flat search row with icon and menu divider, align horizontal padding with list items, and show a custom scrollbar on the repository list.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(ui): unify filter menu section headings and action icons

Share pt-1 section label padding via MenuHeading, fold hide/delete rows into
MenuRow with Eye and Trash icons, and add a regression test for both actions.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(ui): add settings tooltip and neutral delete-all row

Show a white hover tooltip on the backend selector settings button and
style Delete all like other filter menu actions instead of danger red.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(ui): theme scrollbars/skeletons and improve settings tooltips

Derive scrollbar colors from the cool-grey scale, use interactive-active
for skeleton loaders, and fix settings tooltip placement when the drawer
is open plus collapsed-sidebar coverage.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(ui): replace mobile top nav with left drawer

On small screens, hide the horizontal sidebar strip and open the full
vertical nav from a top-bar chevron toggle, matching the desktop collapse
control icon.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(ui): restore mobile page gutters and center home title

Apply 14px horizontal padding in the root outlet on mobile, keep
conversation full-bleed, and center the home heading with w-full text-center.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(ui): correct mobile nav icons and home title line height

Use PanelLeft for the mobile menu trigger, ChevronLeft to close the
drawer, and relax the home headline leading when it wraps.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(ui): remove border under mobile nav menu bar

The top-bar menu trigger no longer draws a divider above page content.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(ui): mobile settings/customize hubs and animated nav drawer

On mobile, Settings and Customize open list hubs matching desktop left nav;
detail pages show drawer and back controls in the top bar. The slide-out nav
drawer now animates open and closed with a fading scrim.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(ui): keep sidebar icons aligned when collapsing the rail

Use shared h-10 row geometry and a fixed icon column so collapsed and
expanded states share padding and gap; labels clip instead of recentering.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(ui): stabilize collapsed sidebar icon column and hover targets

Use a shared 18px icon slot in both rail states, square collapsed
controls for hover/active, and symmetric rail padding so icons and the
logo stay aligned without full-width row highlights.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(ui): refine collapsed sidebar slots and empty-list load-more

Use consistent px-2.5 rail padding with 40×40 collapsed icon slots via
SidebarCollapsedIconSlot, fix backend dot anchoring and logo alignment,
and hide “Load more” when no conversations are visible after filtering.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(ui): keep conversations header on one line during sidebar resize

Use flex-nowrap with a truncating title so the toolbar row does not wrap
while the drawer animates, and nudge the collapsed backend status dot up-left.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(ui): align DropdownMenu padding and tighten backend footer actions

Use uniform `p-1` on the combobox menu panel to match other menus, and
remove vertical gap between Add/Manage backend items in the selector footer.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(ui): trim vertical padding on cloud repo search row

Drop wrapper `py-1` so the new-conversation repo search aligns with the
dropdown chrome; horizontal inset stays `px-2`.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(ui): flush cloud repo menu divider against search and list

Drop flex `gap-1` on the popover so the rule sits tight to the search row
and repository list; keep tab stripe spacing with `py-1` on the provider row.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(ui): show backend settings tooltip above when sidebar is expanded

Use `useSidebarCollapsed()` so the gear tooltip uses top placement on the
full-width rail while keeping left placement for the icon-only strip unless
the conversation right drawer is open.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(ui): use full-screen route for mobile tools panel and tidy conversation chrome

Restore horizontal inset on the conversation route, fold the sidebar opener into the chat header, and open Files/Tools via `/conversations/:id/panel` with a dedicated back affordance instead of a bottom sheet.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(ui): only show sidebar drawer toggle when the rail is hidden

The hamburger used the 1024px layout breakpoint while the sidebar stays visible from the `md` rail width up, so it duplicated chrome between tablet widths. Gate the toggle and header padding on the same max-md width as the rail (<=767px). Mirror the right-panel icon horizontally for the right-side drawer.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(ui): align mobile conversation chrome, chat padding, and settings scroll

Unify mobile top-bar icon buttons with shared classes; add compact conversation
tabs and consistent horizontal padding for chat plus stable composer/git chrome.
Move settings and extensions horizontal inset into layout helpers and drop the
root outlet gutter so pages control their own padding.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(ui): home mobile padding and Lucide sizes in chat mobile header

Add px-4 to the home shell on small viewports and shift launcher inset to
md+ only. Pin PanelLeft/ChevronLeft to 20px to match the right-panel icon and
restore chat header left inset when the rail menu toggle shows.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(ui): use block-drawer icon for mobile sidebar toggle

Match the right-panel control’s SVG so both header icons share the same
optical weight instead of Lucide PanelLeft filling the hit target.

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor: remove unrelated file

* refactor: remove unrelated files

* fix: failing tests

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: hieptl <hieptl.developer@gmail.com>
2026-05-21 02:18:19 +07:00
Rohit Malhotraandopenhands 979e64fe19 feat: add Docker CI to build all-in-one image with agent-server + automation + frontend (#634)
* feat: add Docker CI to build all-in-one image with agent-server + automation + frontend

Adds a GitHub Actions workflow (.github/workflows/docker.yml) that builds and
publishes ghcr.io/openhands/agent-canvas — a single Docker image combining:

  1. Agent Server (ghcr.io/openhands/agent-server base image from SDK repo)
  2. Automation server (pip-installed from openhands-automation)
  3. agent-canvas frontend (static build from this repo)

The automation server is pip-installed rather than copied from its Docker image
because both services share openhands-sdk, fastapi, uvicorn, pydantic, httpx
etc. — installing into the agent-server's Python 3.13 deduplicates all shared
packages. Only automation-specific deps (asyncpg, sqlalchemy, boto3, …) are
added on top.

An entrypoint script starts all three services and a static-server proxy that
unifies them behind a single port (default 8000):
  /api/automation/* → automation backend (:18001)
  /api/*            → agent-server (:18000)
  /*                → static frontend + SPA fallback

Workflow triggers:
  - Push to main: builds and pushes with branch + SHA tags
  - v* tags (releases): also pushes semver tags (1.2.3, 1.2, 1, latest)
  - PRs: builds, pushes SHA-tagged image, updates PR description with
    pull/run instructions (same pattern as the SDK repo)
  - workflow_dispatch: supports overriding base image and automation version

Files added:
  - docker/Dockerfile (multi-stage: frontend build + agent-server base)
  - docker/entrypoint.sh (process manager for all three services)
  - .dockerignore
  - .github/workflows/docker.yml

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: build multi-arch Docker images (amd64 + arm64)

Adds QEMU setup for cross-compilation and defaults the platform matrix
to linux/amd64,linux/arm64 so the image works on both Intel and Apple
Silicon machines.

Co-authored-by: openhands <openhands@all-hands.dev>

* refactor: rewrite Docker workflow to match SDK repo structure

Replace the single-job QEMU approach with the same architecture-matrix
pattern used by the SDK repo's server.yml:

  1. build-and-push-image — matrix over {amd64, arm64} with native runners
     (ubuntu-24.04 for amd64, ubuntu-24.04-arm for arm64). Each job pushes
     arch-suffixed tags (e.g. sha-abc1234-amd64) and uploads build-info
     artifacts.

  2. merge-manifests — downloads both arch build-infos, strips the -amd64
     suffix from amd64 tags to derive manifest tags, and creates multi-arch
     manifests via `docker buildx imagetools create`.

  3. consolidate-build-info — aggregates all build-info and manifest-info
     artifacts into a single JSON summary (PR-only).

  4. update-pr-description — renders the summary into the PR body between
     AGENT_CANVAS_DOCKER_START/END markers.

Native runners avoid the 3-5× slowdown of QEMU emulation for arm64
builds.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: sanitize branch names in Docker tags (/ is not allowed)

Branch names like 'feat/docker-ci' produce invalid Docker tags because
'/' is forbidden in tag names. Replace '/' with '-' so the tag becomes
'feat-docker-ci-amd64'.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: default automation to SQLite and fix wait blocking proxy startup

Two bugs:

1. The automation server defaults to PostgreSQL on localhost, which
   doesn't exist in the all-in-one container. Default AUTOMATION_DB_URL
   to sqlite+aiosqlite:// so it works out of the box. Users can override
   with a real Postgres URL for production.

2. The bare 'wait' command waited for ALL background children — including
   the long-running agent-server and automation processes — so the
   static-server/proxy on port 8000 never started. Fix by waiting only
   for the wait_for_port subshell PIDs.

Verified locally: all three services start, endpoints respond correctly,
no more scheduler ConnectionRefusedError.

Co-authored-by: openhands <openhands@all-hands.dev>

* feat: add VOLUME directives for persistence and project mounts

Declare /home/openhands/.openhands (settings, secrets, conversations,
automation SQLite DB) and /projects (user code) as Docker volumes so
data survives container restarts by default. Users should bind-mount
these for durable persistence:

  docker run -v ~/.openhands:/home/openhands/.openhands \
             -v ~/projects:/projects \
             -p 8000:8000 ghcr.io/openhands/agent-canvas

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: set OH_SECRET_KEY default and pre-create persistence dirs

Three issues fixed:

1. OH_SECRET_KEY was not set → agent-server refused to return encrypted
   secrets → conversation creation failed with 503. Set the same static
   default used by dev-safe.mjs / dev-docker.mjs.

2. Persistence dirs (conversations, bash_events, automation DB) were not
   pre-created → the openhands user got PermissionError when the VOLUME
   directive created them as root. Pre-create with correct ownership
   before the USER switch in the Dockerfile.

3. Set OH_PERSISTENCE_DIR, OH_CONVERSATIONS_PATH, OH_BASH_EVENTS_DIR
   defaults in the entrypoint (matching dev-docker.mjs) so data lands
   under the well-known ~/.openhands tree.

Verified locally: all three services start clean, no warnings about
OH_SECRET_KEY, SQLite migrations apply successfully.

Co-authored-by: openhands <openhands@all-hands.dev>

* chore: merge main and remove stale dev-docker.mjs references

Main removed scripts/dev-docker.mjs (Docker is no longer a dependency of
the npm package flow). Update comments in docker.yml, entrypoint.sh, and
AGENTS.md that referenced the deleted file.

Co-authored-by: openhands <openhands@all-hands.dev>

* feat: centralize config into config/defaults.json (single source of truth)

All version pins, port defaults, persistence paths, package names, and
the dev secret key now live in config/defaults.json. Consumers read from
it instead of hardcoding values:

- scripts/dev-safe.mjs: reads via JSON.parse(readFileSync(...))
- scripts/dev-with-automation.mjs: same
- scripts/check-sdk-version-sync.mjs: same (no longer regex-parses JS)
- docker/Dockerfile: config-gen build stage converts JSON to
  /opt/agent-canvas/defaults.env (shell-sourceable)
- docker/entrypoint.sh: sources defaults.env at startup; also adds
  session API key auto-generation so the image doesn't run wide-open
- .github/workflows/docker.yml: reads versions from JSON in a setup
  step (no more hardcoded env vars)

To bump a version, edit config/defaults.json only.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: address PR review feedback (#634)

- Fix PID tracking bug: move PIDS+=($!) inside if/elif branches so the
  else (automation-not-found) path doesn't add a stale PID
- chmod 600 session API key file to prevent credential leak
- Warn when using insecure default OH_SECRET_KEY in Docker entrypoint
- Add try/catch + field validation for config/defaults.json loading in
  check-sdk-version-sync.mjs
- Fix semver tag parsing: strip pre-release/build metadata, only create
  abbreviated tags (major.minor, major, latest) for stable releases
- Sanitize branch names for Docker tags (tr invalid chars, strip leading
  dot/dash) to handle branches with #, @, spaces, etc.
- Add arch validation before manifest merge (assert both amd64.json and
  arm64.json exist)
- Remove $schema reference to non-existent defaults.schema.json

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: remove hardcoded version defaults from Dockerfile

Replace hardcoded ARG defaults (AGENT_SERVER_IMAGE, AUTOMATION_VERSION)
with empty ARGs. Values are always derived from config/defaults.json:
- CI: reads JSON in the workflow config step, passes --build-arg
- Local: new scripts/docker-build.mjs helper reads JSON and invokes
  docker build with the correct --build-arg values

Added npm run build:docker convenience script.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: stabilize snapshot tests and auto-generate Docker secret key

Two fixes:

1. **Flaky snapshot tests**: The 'Local pagination fixture' mock conversation
   used a fixed absolute timestamp (PAGINATION_BASE_TIME = May 13, 2026) for
   its created_at/updated_at, while 'Errored Project' used a relative
   timestamp (now - 7d). As real time progressed past the crossover point,
   their sort order in the sidebar flipped, causing 30/73 snapshot diffs on
   every PR. Fix: use relative timestamps (now - 6d) for the pagination
   fixture's conversation listing fields. The internal event timestamps
   (used by pagination tests) still use PAGINATION_BASE_TIME — only the
   sidebar ordering is affected.

2. **Docker OH_SECRET_KEY**: The entrypoint used a static insecure default
   for OH_SECRET_KEY and warned about it. Now mirrors the session API key
   pattern: auto-generate a cryptographic random key on first run, persist
   it to ~/.openhands/agent-canvas/secret-key.txt, and reuse on restart.
   Users can still override via the OH_SECRET_KEY env var. Removed the
   now-unused CONFIG_SECRET_KEY from the Docker defaults.env generation.
   Also deduped STATE_DIR computation (was repeated for session key path).

Co-authored-by: openhands <openhands@all-hands.dev>

* docs: update AGENTS.md with mock timestamp and Docker secret key notes

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: include canvas_ui tool in Docker image

The Docker image was missing the tools/ directory and OH_EXTRA_PYTHON_PATH,
so the agent-server couldn't import canvas_ui_tool.py when the frontend
sent canvas_ui in the conversation tools list. This caused:

  HTTP 500: ToolDefinition 'canvas_ui' is not registered

Fix: COPY tools/ into the image and set OH_EXTRA_PYTHON_PATH in the
entrypoint, matching what scripts/dev-safe.mjs already does for local dev.

Co-authored-by: openhands <openhands@all-hands.dev>

---------

Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-20 04:24:45 +00:00
Rohit Malhotraandopenhands edb998220a Remove Docker dependency from dev workflow (#635)
- Delete scripts/dev-docker.mjs and its test
- Simplify package.json: 'npm run dev' now runs local uvx stack directly
  (agent-server + automation + Vite + ingress), no Docker needed
- Remove dev:docker, dev:docker:dynamic, dev:dangerously-dockerless scripts
- Add dev:static for production-build frontend variant
- Update bin/agent-canvas.mjs CLI to use uvx-based stack
- Rename Docker-specific variables: DOCKER_PROJECTS_PATH → PROJECTS_PATH,
  shouldDefaultToDockerProjects → shouldDefaultToProjectsPath
- Update i18n: HOST_HOME_NOT_MOUNTED_HINT no longer references Docker
- Update all docs (README, DEVELOPMENT, SELF_HOSTING, AGENTS.md, CHANGELOG)
- Rename e2e snapshot: docker-workspace-browser → projects-workspace-browser
- Fix all tests to reflect new script names and remove Docker references

Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-19 15:27:25 -04:00
Tim O'Farrellandopenhands 2ac97d1bd8 fix(dev:docker): container-safe automation paths, hostnames, and AGENT_SERVER_URL alias (#603)
* fix(dev:docker): set AUTOMATION_WORKSPACE_BASE to a container-safe path

When agent-canvas is started via `npm run dev:docker`, the agent-server
runs in a container but the automation backend runs on the host via uvx.
The dispatcher resolves `AUTOMATION_WORKSPACE_BASE` on the host (where
it expands to the host's $HOME, e.g. /Users/<you>/.openhands/...) and
then embeds that absolute path into a `mkdir -p ...` shell command
that is executed *inside* the agent-server container. The container
has no /Users directory and the non-root user can't write to /, so
every preset automation fails with:

  mkdir: cannot create directory '/Users': Permission denied

Fix:
* Thread a new `automationWorkspaceBase` option through the launcher
  (mirroring the existing `viteWorkingDir` pattern).
* `dev-docker.mjs` now passes `CONTAINER_WORKSPACES_DIR` — the same
  in-container path already used as the agent-server's working-dir
  root, so it's guaranteed to exist and be writable.
* Resolution order for `AUTOMATION_WORKSPACE_BASE` is now:
  1) explicit user env var (wins over everything; previously the
     hard-coded value silently clobbered user-set values)
  2) launcher-provided default (container-safe in docker mode)
  3) host-side fallback under config.stateDir (unchanged for dockerless)

Co-authored-by: openhands <openhands@all-hands.dev>

* fix(dev:docker): route AUTOMATION_BASE_URL through host.docker.internal

In `npm run dev:docker`, the automation backend's `AUTOMATION_BASE_URL`
was hard-coded to `http://localhost:${ingressPort}`. That URL is then:

  1. propagated into each automation sandbox as `AUTOMATION_API_URL`
     (dispatcher.py:213), and
  2. consumed by the auto-generated `setup.sh` inside the sandbox to
     hit `${AUTOMATION_API_URL}/sdk-version`.

The sandbox is a separate Docker container, so `localhost` resolves to
the sandbox itself, not the host ingress. Every preset automation run
therefore failed at the very first step with:

  [setup] ERROR: Failed to fetch SDK version from
  http://localhost:8000/api/automation/sdk-version

Reachability check from inside a container in the same network:

  http://localhost:8000/api/automation/sdk-version          -> 404
  http://host.docker.internal:8000/api/automation/sdk-version -> 200

Fix (mirrors the existing `automationWorkspaceBase` plumbing for the
same host-vs-container class of bug):

* New `automationApiHost` launcher option threaded through main() and
  stamped onto config.
* `dev-docker.mjs` passes `automationApiHost: "host.docker.internal"`.
* `startAutomationBackend` resolves `AUTOMATION_BASE_URL` with
  precedence: user env > launcher-provided host > `localhost`.

Dockerless modes (`dev`, `dev:automation`) are unaffected — they
don't pass `automationApiHost` and fall back to the existing
`http://localhost:${ingressPort}` value.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix(dev:docker): expose AGENT_SERVER_URL and SESSION_API_KEY aliases to the sandbox

The OpenHands SDK boilerplate emitted by automation prompt/plugin presets
reads AGENT_SERVER_URL and SESSION_API_KEY in main.py / setup.sh when
calling back into the agent-server from an automation run. The
agent-server itself sets OH_INTERNAL_SERVER_URL at startup and we set
OH_SESSION_API_KEYS_0 via the container/process env, but neither of the
unprefixed aliases the SDK actually reads were defined — so every preset
automation hit ECONNREFUSED / 401 the moment its setup.sh tried to hit
the agent-server.

Mirror the values under their canonical SDK names in both the dockerless
buildAgentServerEnv() (covers dev / dev:automation) and in dev-docker.mjs
containerEnv (covers dev:docker). Inside the dev:docker container the
agent-server always listens on port 8000, and 0.0.0.0 normalises to
127.0.0.1 (matching the agent-server's own OH_INTERNAL_SERVER_URL
construction), so the docker path uses http://127.0.0.1:8000 directly.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix(dev:docker): route AUTOMATION_AGENT_SERVER_URL via host.docker.internal

This env var plays a dual role: the automation backend uses it directly
to call the agent-server's upload/bash REST APIs (host-side), and the
same value is exported as AGENT_SERVER_URL into the in-sandbox bash
command that main.py uses to call back. In dev:docker the script runs
inside the agent-server container, so the URL has to be reachable from
both sides.

http://localhost:${agentServerPort} was fine for dockerless modes
(both backend and agent-server live on the host) but broke dev:docker:
from inside the container localhost:${hostPort} doesn't resolve, and
`main.py` failed at the first RemoteWorkspace call.

Reuse the same automationApiHost launcher option already plumbed for
AUTOMATION_BASE_URL (Bug 2): dev:docker passes host.docker.internal, so
the URL works as a loopback alias from the host and bounces back into
the container via the published port-forward from the sandbox side.
Dockerless modes keep the previous `localhost` default.

Co-authored-by: openhands <openhands@all-hands.dev>

* revert: drop SESSION_API_KEY alias from agent-server env

The OpenHands SDK's sanitized_env() strips SESSION_API_KEY from every
bash subprocess as a (cosmetic) defense-in-depth measure, so setting
this alias on the agent-server's process env had no effect on what the
sandbox script actually sees. Worse, it created cross-purposes between
the dev stack (which exports it) and the SDK (which strips it).

Keep the AGENT_SERVER_URL alias — that one is genuinely needed and
isn't subject to filtering. SESSION_API_KEY is now obtained inside the
sandbox script via OH_SESSION_API_KEYS_0 (handled in an accompanying
PR against openhands/automation), which the SDK does not strip.

Co-authored-by: openhands <openhands@all-hands.dev>

---------

Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-18 16:32:38 -06:00
Hiep Le 5c33c10b18 feat(frontend): add canvas_ui tool so the agent can drive the UI (#420)
* feat: add canvas_ui tool so the agent can drive the UI

* refactor: update the code based on feedback

* fix: failing tests

* refactor: update the code based on feedback
2026-05-16 16:09:43 +07:00
Tim O'Farrellandopenhands b2b71855c6 feat(dev): surface dev-stack runtime services in agent system prompt (#503)
* feat(dev): surface dev-stack runtime services in agent system prompt

Add a structured 'runtime services' info object that the dev launchers
(`dev:safe`, `dev:automation`, `dev:docker`, and the published
`agent-canvas` binary) propagate to the frontend via
`VITE_RUNTIME_SERVICES_INFO`. The frontend renders it into a
`<RUNTIME_SERVICES>` markdown block and attaches it as
`AgentContext.system_message_suffix` on every `POST /api/conversations`.

This means agents start each conversation knowing exactly what services
exist in the current dev stack (ingress URL, automation backend URL +
`/api/automation` prefix, auth header, etc.), instead of having to probe
or — worse — assume `localhost:8000` is the automation server when it is
actually the Agent Server they are running inside of.

URLs are written from the agent's point of view: dockerless modes use
`localhost`, `dev:docker` uses `host.docker.internal`. When automation
isn't running in the current mode (e.g. `dev:safe`), the block says so
explicitly so agents know to skip `/api/automation` calls.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix(runtime-services): address review feedback on PR #503

- Validate required `agentServerPort` in `buildRuntimeServicesInfo`;
  previously a missing port baked `http://localhost:undefined` into
  the agent's system prompt.
- Skip the automation entry when the supplied `automation` object has
  no `port` (e.g. a bare `{}` from a misconfigured launcher).
- Rename the JSON service key from `vite` to `frontend` and add a
  `kind: "vite" | "static"` discriminator + mode-aware description,
  so static-build dev stacks (`dev:docker`, the published binary, ...)
  no longer surface a misleading "Vite dev server" line in the agent
  system prompt. The renderer still accepts the legacy `vite` key.
- Anchor the "don't guess" warning to the actual agent-server URL from
  runtime info instead of hardcoded `localhost:8000`, since the
  agent-server uses different ports across dev modes (18000 in
  dev:safe, 8000 in dev:docker, ...).
- Plumb `frontendKind` through `buildAutomationRuntimeServicesInfo`
  and stamp `config.frontendKind` in `dev-with-automation.mjs::main`
  so both Vite spawn and static-build paths describe the frontend
  correctly.
- Expand AGENTS.md with the JSON schema of `VITE_RUNTIME_SERVICES_INFO`
  and a concrete example of the rendered `<RUNTIME_SERVICES>` block.
- Tests: assert the new URL-in-warning behavior, the new `frontend` /
  legacy `vite` rendering, the `agentServerPort`-required guard, the
  `automation: {}` skip, and the legacy `vitePort` alias.

Co-authored-by: openhands <openhands@all-hands.dev>

---------

Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-15 20:53:45 -06:00
2899c7b383 Fix static automation auth and switch LLM tool (#474)
* Fix static automation auth and switch LLM tool

* Allow automation SDK release lag in sync check

* chore: update baseline snapshots [skip ci]

* chore: trigger CI after snapshot update

---------

Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-05-15 15:30:29 +00:00
Xingyao Wangandopenhands e5711e74b2 Clean up dev stack process trees on shutdown (#475)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-15 10:48:28 -04:00
Graham Neubigandopenhands 5ccc8e627c Fail fast when frontend deps are missing (#404)
Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-14 10:32:03 -04:00
Rohit Malhotraandopenhands 773cc72324 feat: update SDK to 1.22.0 and add CI version sync check (#333)
* feat: update SDK to 1.22.0 and add CI version sync check

- Update DEFAULT_AGENT_SERVER_VERSION from 1.21.1 to 1.22.0 in dev-safe.mjs
- Update SDK version references in AGENTS.md
- Add scripts/check-sdk-version-sync.mjs to verify automation project uses
  matching SDK versions for openhands-sdk, openhands-tools, openhands-workspace,
  and openhands-agent-server
- Add .github/workflows/sdk-version-sync.yml CI workflow with:
  - Path-filtered PR/push triggers for version-related file changes
  - repository_dispatch triggers (sdk-version-check, sdk-release) for
    external repos to notify when SDK deps change
  - workflow_dispatch with optional version override
  - Scheduled runs every 6 hours to catch upstream changes
  - PyPI version checking support (--check-pypi flag)

The check script supports:
- EXPECTED_SDK_VERSION env var override for CI triggers
- --check-pypi flag to also display latest PyPI versions
- --help for usage documentation

To trigger from external repos (e.g., OpenHands/automation or SDK repo):
  curl -X POST -H "Authorization: token \$GITHUB_TOKEN" \\
    https://api.github.com/repos/OpenHands/agent-canvas/dispatches \\
    -d '{"event_type": "sdk-version-check"}'

* fix: check released PyPI version instead of GitHub main branch

The SDK version sync check now fetches dependencies from the released
openhands-automation package on PyPI (version specified by
DEFAULT_AUTOMATION_VERSION in dev-with-automation.mjs) rather than
fetching pyproject.toml from the GitHub main branch.

This ensures we're checking the actual released version that users
would install, not the development version on main.

* fix: address review feedback for SDK version sync check

- Add env var overrides for automation package name and version
- Add retry logic with exponential backoff for PyPI API failures
- Add semantic version normalization for comparing versions
- Fix repository_dispatch to use client_payload.version
- Improve regex to handle parenthesized dependency formats
- Add comprehensive test coverage for helper functions

* fix: add type casts for dynamic module import in tests

* chore: update automation version to 1.0.0a2

- Update DEFAULT_AUTOMATION_VERSION in dev-with-automation.mjs
- Update AGENTS.md documentation
- Update test expectation

---------

Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-11 15:39:17 -04:00
Xingyao Wangandopenhands 18e51fa84d fix: move TMUX_TMPDIR to /tmp to avoid socket errors on mounted volumes (#325)
* fix: move TMUX_TMPDIR to /tmp to avoid socket errors on mounted volumes

Some filesystems (NFS, CIFS, certain FUSE/overlay mounts used by Docker
bind-mounts) do not support Unix domain sockets. When TMUX_TMPDIR pointed
to ~/.openhands/agent-canvas/tmux/ inside a container, tmux failed with:

  error connecting to .../tmux-10001/openhands (Operation not supported)

Move tmux socket directory to /tmp/openhands-agent-canvas-tmux which is
always on a local/tmpfs filesystem that supports Unix sockets. Tmux
sockets are ephemeral and don't need persistence across restarts.

Co-authored-by: openhands <openhands@all-hands.dev>

* refactor: drop explicit TMUX_TMPDIR from dev-docker.mjs, use system default

Per review feedback — the container's default TMUX_TMPDIR (/tmp) already
supports Unix domain sockets, so there's no need to set it explicitly.

Co-authored-by: openhands <openhands@all-hands.dev>

---------

Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-11 13:37:25 -04:00
Robert Brennanandopenhands 5d16a6cbcf Persist dev session API key to disk and re-seed default backend (#240)
Both `npm run dev` (Docker) and `npm run dev:dangerously-dockerless`
previously generated a fresh random SESSION_API_KEY per process. The
key was passed to the agent-server (OH_SESSION_API_KEYS_0) and to Vite
(VITE_SESSION_API_KEY), but the frontend's `openhands-backends`
localStorage entry was seeded only on the very first load. After a
single restart, the persisted entry's `apiKey` no longer matched the
agent-server, leading to 401s until the user manually edited the
backend.

Fix this by giving `buildSafeDevConfig` a stable default:

- `getOrCreatePersistedSessionApiKey()` reads / creates
  `~/.openhands/agent-canvas/session-api-key.txt` (mode 0600). The
  in-memory cache is keyed by path so tests can use `mkdtemp` paths.
- `OH_SESSION_API_KEY_PATH` env var overrides the file location
  (used by tests; can also be used to pin in unusual setups).
- Existing env overrides (SESSION_API_KEY / OH_SESSION_API_KEYS_0 /
  VITE_SESSION_API_KEY) still take precedence.

Because dev:docker and dev:dangerously-dockerless both flow through
the shared `buildSafeDevConfig`, they automatically pick up the
same persisted key and stay in sync with the Vite-baked
VITE_SESSION_API_KEY.

On the frontend, `readStoredBackends` now also re-seeds the default
Local backend when storage parses to `[]` or contains only invalid
entries (previously only `null` triggered seeding). This is safe now
that the persisted key keeps the seed valid across restarts.

Tests:
- New `getOrCreatePersistedSessionApiKey` tests covering creation,
  reuse, whitespace trimming, and empty-file regeneration.
- New `buildSafeDevConfig` / `buildConfig` tests covering the
  on-disk fallback, restart parity (dev:docker vs
  dev:dangerously-dockerless), and env-override precedence.
- New backend-registry storage tests covering re-seed on missing,
  empty, and all-invalid storage states.
- Existing tests that previously hit the real
  `~/.openhands/agent-canvas/session-api-key.txt` were updated to
  use isolated `OH_SESSION_API_KEY_PATH` temp dirs.

Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-09 13:54:14 -07:00
Rohit Malhotraandopenhands 3207d90e72 feat: add dynamic port allocation with preferred port fallback (#223)
* feat: add dynamic port allocation with preferred port fallback

Implement dynamic port allocation for dev entrypoint scripts to gracefully
handle port conflicts. When a preferred port is busy, the system automatically
finds an alternative available port.

Changes:
- Add findFreePort() and findFreePorts() utilities to dev-safe.mjs
- Add buildSafeDevConfigAsync() for async config with dynamic allocation
- Update dev-with-automation.mjs to use async buildConfig with dynamic ports
- Update dev-static.mjs to use async buildConfig
- Add strictPort: true to vite.config.ts to fail-fast on conflicts
- Update tests for async buildConfig

The utilities try the preferred/default ports first, falling back to
OS-assigned ports only when needed. This preserves predictable defaults
while gracefully handling port conflicts.

Closes #222

* fix: address review feedback - add max retry, document race condition, improve tests

- Add max retry count (100 attempts) to port allocation loop to prevent
  infinite loops
- Fix findFreePort to handle preferredPort=0 correctly by skipping the
  port check and going straight to OS assignment
- Document race condition limitation in findFreePort JSDoc (accepted
  limitation with guidance on handling EADDRINUSE)
- Clarify JSDoc for buildSafeDevConfig vs buildSafeDevConfigAsync with
  clear guidance on when to use each
- Remove misleading 'must be after prereq check' comment
- Add comprehensive tests for findFreePort, findFreePorts, and
  buildSafeDevConfigAsync using actual port blocking
- Improve buildConfig tests with port uniqueness verification and
  fallback tests using high ports
- Use high ports (19xxx range) in tests to avoid conflicts with
  system services

Co-authored-by: openhands <openhands@all-hands.dev>

---------

Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-09 14:46:10 -04:00
Rohit Malhotra 9098d9e6df feat: auto-generate random API keys for dev server authentication (#203) 2026-05-08 22:48:58 -04:00
Rohit Malhotraandopenhands 0b45d8c879 chore: default to released PyPI versions instead of git branches (#194)
- Change agent-server SDK default from git main to PyPI 1.21.1
- Change automation default from git main to PyPI 1.0.0a1
- Pin all SDK packages (agent-server, tools, workspace) to same version
- Keep ability to override with OH_AGENT_SERVER_GIT_REF/OH_AUTOMATION_GIT_REF
- Update tests and AGENTS.md documentation

Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-08 15:08:01 -04:00
Robert Brennanandopenhands 9c3b936d16 Add npm run dev:static for offline / high-latency development (#168)
Mirrors the dev:automation backend stack (agent-server + automation +
ingress) but serves a production frontend build through a small static
server instead of Vite. Designed for use over flaky / high-RTT links
where Vite's ~1000 ESM module fetches make full reloads painfully slow:
hashed assets are now sent with public/immutable cache headers, so an
SPA reload is ~1 round-trip (304 on index.html) and zero asset fetches.

scripts/static-server.mjs: combined static-file server + reverse proxy.
A drop-in for sirv-cli that additionally proxies the same prefixes Vite
proxies in dev (/api, /api/automation, /sockets, /server_info, /alive,
/health, /ready) so hitting :3001 directly behaves like Vite's dev
server — without it, sirv-cli's --single fallback turns /server_info
into the SPA shell whenever a tunnel exposes the static port instead of
the ingress port. Caches /assets/* immutable, index.html no-cache,
weak ETags.

scripts/dev-static.mjs: orchestrator that builds the frontend, then
spawns agent-server, automation, static-server, and the existing
ingress with the same route table as dev-with-automation.

scripts/dev-safe.mjs: add isPortBusy() and
releaseStaleConversationLeases() helpers. The agent-server tags each
conversation directory with an owner_lease.json keyed to a per-process
owner_instance_id (45 s TTL, heartbeat-renewed) and skip-loads any
conversation whose lease is held by a different instance. If the
previous agent-server died ungracefully — or you restart inside the
TTL window — every existing conversation becomes invisible to the new
instance until the leases age out. dev:static now port-checks for a
live agent-server (aborts on conflict), then unlinks stale leases so
conversations created by npm run dev are immediately visible.

Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-08 17:29:27 +07:00
Hiep Le dd744b13f6 feat: cloud backend support with multi-backend selector and SaaS proxy routing (#145)
* feat: multi-backend support with cloud SaaS proxy routing

* feat: route conversation export through cloud proxy on cloud backends

* fix: route conversation delete through cloud proxy on cloud backends

* fix: forward settings diffs verbatim through cloud proxy save

* fix: surface cloud-aware settings sub-pages and gate local-only routes

* fix: route secrets settings through cloud proxy on cloud backends

* fix: route conversation stop runtime through cloud proxy on cloud backends

* fix: re-expose planning agent UI for cloud backends and route plan file reads through cloud proxy

* fix: route Display Cost runtime fetch through cloud proxy and ungate local metrics without session API key

* fix: handle WAITING_FOR_SANDBOX task status from cloud backends to prevent UI crash

* fix: re-expose Public Share in conversation menu for cloud backends

* fix: redirect to home when switching backends from a conversation page

* fix: hide cloud orgs the API key can't access in backend selector

* feat: support running multiple local agent-servers with shared persistence

* fix: lint

* fix: failing tests
2026-05-08 01:17:40 +07:00
Graham Neubigandopenhands 62871d3a7b rename agent-server-gui to agent-canvas (#121)
- npm package: @openhands/agent-server-gui -> @openhands/agent-canvas
- README/DEVELOPMENT/AGENTS/codereview guide updated to new name
- GitHub URL in onboarding screen + tests updated
- dev launcher env vars renamed: OH_GUI_SAFE_* -> OH_CANVAS_SAFE_*
- default state dir: ~/.openhands/agent-server-gui -> ~/.openhands/agent-canvas
- i18n strings replace 'GUI' phrasing with 'Agent Canvas' across settings,
  upgrade, onboarding, and unavailable copy
- Test fixtures, working-dir paths, and library-consumer smoke updated

Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-06 13:07:01 -04:00
Rohit Malhotraandopenhands cccecf1100 Fix uvx command to use --from syntax for PyPI packages (#122)
The openhands-agent-server package exposes an executable named
'agent-server', not 'openhands-agent-server'. When using PyPI versions
(either specific or latest), we need to use the --from syntax:
  uvx --from openhands-agent-server agent-server

This fixes the error:
  An executable named 'openhands-agent-server' is not provided by
  package 'openhands-agent-server'.
  Use 'uvx --from openhands-agent-server agent-server' instead.

Fixes #117

Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-06 12:07:41 -04:00
Rohit Malhotraandopenhands 179192ca01 feat: export buildAgentServerEnv helper for downstream consumers (#119)
Add a new exported function that builds the environment variables object
for spawning the agent-server process. This allows downstream consumers
(e.g., the automation service) to use the same env vars without
duplicating the mapping logic.

When new env vars are added or existing ones are renamed, downstream
consumers will automatically inherit the changes by using this helper.

Refactored main() to use the new helper internally.

Closes #118

Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-05 22:02:44 -04:00
Rohit Malhotraandopenhands f9006b4bf0 feat: use agent server APIs for settings persistence (#98)
* feat: use agent server APIs for settings persistence

- Replace localStorage with HTTP API for settings storage
- Use `X-Expose-Secrets: encrypted` header for GET /api/settings
  to receive encrypted secrets (not exposing raw values)
- Use `secrets_encrypted: true` in start conversation payload
- Add `getSettingsForConversation()` to build encrypted settings
  payload for conversation start endpoint
- Update secrets service to use /api/settings/secrets endpoints
- Add mock handlers for settings and secrets API endpoints
- Update tests for new API-based settings flow

This integrates with software-agent-sdk PR #3060
(feat/encrypted-secrets-in-transit) which adds server-side
encryption support for secrets in transit.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: update test mocks for encrypted settings API and add OH_SECRET_KEY support

- Update use-create-conversation-metadata.test.ts to mock getSettingsForConversation()
  which is now called by buildStartConversationRequestWithEncryptedSettings
- Skip flaky onOpen websocket test that times out intermittently in CI
- Add OH_SECRET_KEY environment variable support in dev-safe.mjs:
  - Uses default key for local development
  - Can be overridden via OH_SECRET_KEY environment variable
  - Logs secret key source at startup

Co-authored-by: openhands <openhands@all-hands.dev>

* docs: update AGENTS.md for settings API and OH_SECRET_KEY

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: update secrets service to use agent-server API routes

Changes:
- Update SecretsService to use /api/settings/secrets endpoints instead of /api/v1/secrets
- Simplify secrets-service.types.ts to remove unused pagination types
- Update use-get-secrets hook to do client-side filtering (agent-server doesn't support pagination)
- Update mock handlers to only use agent-server API routes
- Update secrets-settings test to mock getSecrets instead of searchSecrets
- Remove pageSize option from useSearchSecrets since agent-server doesn't paginate

The agent-server API routes (per SDK PR #3060):
- GET /api/settings/secrets - List secrets (names/descriptions only)
- GET /api/settings/secrets/{name} - Get secret value
- PUT /api/settings/secrets - Upsert secret
- DELETE /api/settings/secrets/{name} - Delete secret

Co-authored-by: openhands <openhands@all-hands.dev>

* docs: update AGENTS.md for secrets API routes

- Document the agent-server secrets CRUD routes in MSW handlers list
- Update git provider token persistence note to reflect server-side storage

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: update secret name validation to match agent-server requirements

- Change pattern from '^\S*$' (no whitespace) to '^[a-zA-Z][a-zA-Z0-9_]{0,63}$'
- Add title prop to SettingsInput component for validation error messages
- Secret names must: start with letter, contain only letters/numbers/underscores, be 1-64 chars

Co-authored-by: openhands <openhands@all-hands.dev>

* feat: include custom secrets in conversation requests via LookupSecret

Custom secrets configured in Settings > Secrets are now automatically
included in conversation start requests. Instead of exposing secret values
to the frontend, we use LookupSecret entries that point to the agent-server
endpoint /api/settings/secrets/{name}. The agent-server fetches the actual
values at runtime.

Changes:
- Add LookupSecret interface to agent-server-adapter.ts
- Add customSecrets option to StartConversationOptions
- Build LookupSecret entries for each custom secret in buildStartConversationRequest
- Update buildStartConversationRequestWithEncryptedSettings to fetch and include
  custom secrets list from SecretsService.getSecrets()
- Include X-Session-API-Key header in LookupSecret when configured

This ensures secrets never touch the frontend in plaintext while still
making them available to conversations.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: address review comments - no localStorage fallback, retry logic, SDK docs

Review feedback addressed:
1. secrets-service.ts: Server storage MUST succeed before updating localStorage
   - addGitProvider now stores to server FIRST, only updates localStorage on success
   - createSecret/updateSecret/deleteSecret now throw on failure (no silent returns)
   - Added retry logic with exponential backoff for all API calls

2. settings-service.api.ts: No silent fallback for encrypted settings
   - getSettingsForConversation now throws if encrypted fetch fails
   - Conversations should not start with broken/redacted credentials
   - Added retry logic with exponential backoff

3. AGENTS.md: Document SDK dependency
   - Settings persistence APIs require SDK PR #3060
   - Until released, npm run dev defaults to main branch
   - Documented git provider storage design (server + localStorage)

4. dev-safe.mjs: Default to SDK main branch
   - Added DEFAULT_GIT_REF='main' constant
   - npm run dev now uses main until settings APIs are released
   - TODO comment to update once released

Note: Git provider tokens still use localStorage for frontend git API calls
(repo search, branches), but MUST succeed on server first.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: update server secret when only host changes

When updating just the host (empty token), the server secret's description
must also be updated to keep metadata in sync. Previously, only localStorage
was updated, violating the 'server storage must succeed first' principle.

Now the host-only update path also calls createSecret() to update the
server secret's description before updating localStorage.

Co-authored-by: openhands <openhands@all-hands.dev>

---------

Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-05 20:39:46 -04:00
Rohit Malhotraandopenhands 6fa219cf25 feat: use uvx for temporary agent-server installation in dev mode (#99)
* feat: use uvx for temporary agent-server installation in dev mode

- Replace direct agent-server CLI invocation with uvx temporary install
- Add OH_AGENT_SERVER_VERSION env var for specific PyPI versions
- Add OH_AGENT_SERVER_GIT_REF env var for git commits/branches
- Auto-install uv in .openhands/setup.sh if not present
- Update documentation (README, DEVELOPMENT.md, AGENTS.md)
- Add comprehensive tests for buildAgentServerCommand()

This removes the requirement to permanently install agent-server via
'uv tool install'. Users only need uv installed, and npm run dev will
automatically download and run the appropriate agent-server version.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: use subdirectory syntax for git ref in uvx monorepo

The software-agent-sdk is a uv workspace monorepo with packages in
subdirectories (openhands-agent-server/, openhands-tools/, etc.).

When installing from git, uvx requires the #subdirectory= fragment to
specify which package to install from the workspace.

Tested with: OH_AGENT_SERVER_GIT_REF=main npm run dev

Co-authored-by: openhands <openhands@all-hands.dev>

---------

Co-authored-by: openhands <openhands@all-hands.dev>
2026-05-05 13:14:14 -04:00
Hiep Le 1cc616921f feat(frontend): isolate per-conversation working directories (#90)
* feat: isolate per-conversation working directories

* fix: failing tests
2026-05-05 01:32:24 +07:00
Graham Neubig 0dddd7a6e7 Fix Windows dev startup (#43) 2026-04-28 21:31:37 -04:00
Graham Neubigandopenhands 3b754b5703 Improve missing agent-server setup guidance (#42)
Add actionable dev-safe output when agent-server is missing, including README and uv installation guidance, and update tests plus quickstart docs.

Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-27 10:40:06 -04:00
Graham Neubigandopenhands 0e60826123 Add isolated dev stack command for OpenHands Cloud debugging (#20)
* Warn about OpenHands Cloud backend conflicts

Co-authored-by: openhands <openhands@all-hands.dev>

* Add isolated dev stack command

Co-authored-by: openhands <openhands@all-hands.dev>

* Promote isolated stack to default dev command

Co-authored-by: openhands <openhands@all-hands.dev>

* Fix dev-safe startup failure and docs

Co-authored-by: openhands <openhands@all-hands.dev>

* Restructure docs for users and developers

Co-authored-by: openhands <openhands@all-hands.dev>

* Remove libtmux from install docs

Co-authored-by: openhands <openhands@all-hands.dev>

* Fix first-load Vite optimize dependency errors

Co-authored-by: openhands <openhands@all-hands.dev>

---------

Co-authored-by: openhands <openhands@all-hands.dev>
2026-04-26 11:09:39 -04:00