Skip to content

feat(sdk): compose local agents with Desktop and Workstation - #222

Merged
abonneth merged 45 commits into
mainfrom
charlie/placement-python-sdk
Oct 5, 2026
Merged

abonneth merged 45 commits into
mainfrom
charlie/placement-python-sdk

Conversation

@cm2435-hcomp

@cm2435-hcomp cm2435-hcomp commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

What

Expose local agent execution through the existing Python Agent API client, composed with local Desktop or remote Workstation environments. Own the local runtime lifecycle, authenticated attachment, command interruption and session resources; keep the hosted client default unchanged. Permission prompts run on the main thread.

Why

Each product currently has to manage its own local agent process and device bridge. The SDK should own that lifecycle behind the existing session interface.

How

Add Client(mode="local"), await AsyncClient.local(), and local runtime/process ownership, authenticated attachment, session resources and interruption through existing device bridges. Execute OS permission preflight on the main thread.

Validation and dependencies

P4; depends on P1/P2 HAI contracts/drivers/runtime and the matching P3 generation overlays. Runtime manifest defaults are preserved from the existing release, so this PR MUST NOT publish or merge until a verified compatible runtime and dependency floors are pinned. Explicit candidate overrides are for QA only.

Validation: 225 non-integration SDK tests pass, 12 skipped, on current main with the review runtime sources. Prior live QA covered local and cloud environments, file round trips, follow-ups and owned-command Stop. SDK workstation work already merged in #220 is excluded from this diff.

QA scope: real local-agent/local-environment and local-agent/cloud-environment runs cover files, follow-ups, Stop and reuse. Candidate hosted-agent combinations and packaged visual parity remain unverified. HoloWork integration is deferred.

Stack navigation: P1 contract → P2 runtime; P3 generation + P4 SDK; P5 checks: runtime, generation, SDK pins; P6 CLI. P7 duplicate pin cleanup follows only after P6 ships. HoloWork integration is subsequent work.

Review guide: intent, architecture, service tradeoffs and merge order.

CI dependency constraint: the published hai-drivers dependency lacks DesktopCommandRunner, so the real SDK/driver Stop regression fails collection in hosted CI until the compatible driver is available. It passes with candidate drivers locally; it has not been skipped to hide the dependency. Changed Python files pass Ruff lint and format.

Lifecycle refinements from review: downloads run outside the per-port spawn lock; idle probes close their HTTP pools; failed cancellation retains the exit retry; startup cleanup preserves the original error; credential cleanup uses the startup lock; asynchronous startup has an awaited, cancellation-safe factory. Regression tests exercise these boundaries.


Note

High Risk
New local runtime spawn, verified downloads, token files, and authenticated loopback attachment are security- and lifecycle-sensitive; bridge/session stop and concurrent startup locking affect long-running agent sessions.

Overview
Adds Client.local() / AsyncClient.local() so agents run against a loopback hai-agent-runtime while reusing the existing sessions API; hosted Client() behavior is unchanged. Clients can start or attach to a runtime, optionally wire self-hosted inference, and close tears down bridged sessions and stops an owned runtime when idle.

Introduces hai_agents_local.runtime: pinned sha256-verified binary download/install, spawn/health checks, owner-only token/pid state, startup locking, and HMAC challenge–response so HTTP clients reject the wrong process on the port. scripts/bump_runtime.py and pin.json maintain the manifest; publish CI gains a macOS runtime-pin gate before PyPI.

Local sessions now track owned bridges, cancel/stop more reliably (including 410 channel closed as clean exit), run macOS permission preflight on the main thread before async bridge startup, and interrupt drivers when Stop is requested. README documents local-agent usage.

Reviewed by Cursor Bugbot for commit 89a992e. Bugbot is set up for automated code reviews on this repo. Configure here.

@cm2435-hcomp

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread src/hai_agents_local/sessions.py Outdated
Comment thread src/hai_agents/client.py Outdated
Comment thread src/hai_agents_local/manager.py
Comment thread src/hai_agents_local/runtime/state.py
@cm2435-hcomp

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread src/hai_agents_local/manager.py

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread src/hai_agents/client.py
Comment thread src/hai_agents_local/runtime/runtime.py
…t.local()

Hand-written runtime management lives outside the generated package, so a
codegen sync cannot wipe it. Local clients are built with Client.local() and
AsyncClient.local(), keeping the generated constructors and their typing.
HTTP(S)_PROXY no longer receives the local runtime bearer token.
Every request to a local runtime carries a fresh X-Hai-Runtime-Challenge, and
every response must carry X-Hai-Runtime-Proof, the HMAC-SHA256 of the
challenge keyed by the runtime token. Unproven responses raise
LocalRuntimeError, so a server squatting the runtime port never receives the
bearer token and its commands never reach a device bridge.

/health is proven before any bearer-authenticated request, both when
spawning and when attaching.
…to charlie/placement-sdk-pins

# Conflicts:
#	src/hai_agents_local/runtime/manifest.py

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread src/hai_agents_local/runtime/runtime.py Outdated
Comment thread src/hai_agents_local/runtime/runtime.py
Comment thread src/hai_agents_local/transport.py Outdated
@abonneth

abonneth commented Oct 1, 2026

Copy link
Copy Markdown
Collaborator

Pushed review fixes directly (as agreed).

Commit What Why
d21f77f Local code moves to hai_agents_local/runtime/; new Client.local() / await AsyncClient.local() The generated package is overwritten on every sync. Hosted Client(...) keeps its typed constructor.
0f5f9a4 Loopback HTTP uses trust_env=False Proxy env vars could route the local bearer through a proxy.
547d2a3 Every request sends a challenge; responses must carry an HMAC proof from the runtime's token Any process on the port could collect the bearer and feed commands to the bridges.
1446f8e Token and pid files are written atomically, only after the child proves the port A failed spawn could overwrite a live runtime's token.
8e9b38f HTTP 410 is a clean bridge stop Every normal session end was logged as a crash and sent a second cancel.
f738f10 cancel_session(id, *, request_options) matches the generated signature; close() only cancels live sessions, and 404/409 count as stopped cancel_session(id=...) raised TypeError. close() raised once old sessions were evicted.
d02c2de close() stops an owned runtime only when no other client's session is active One client closing killed sessions of other clients attached to the same runtime.
1d6640b 15 s SIGTERM grace before SIGKILL The runtime needs up to 10 s to release environments on shutdown.
c2c9a96 Publish blocks unless the pinned runtime downloads, verifies and serves the shared recipe 0.1.8 has no shared recipe, so a release today would ship a broken Client.local().

Tests: 228 passed, 12 skipped. The release gate was checked both ways: it fails on the 0.1.8 pin and passes against a runtime that serves the shared recipe.

Needs a runtime release with identity proofs plus a pin bump before this can ship.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread src/hai_agents_local/runtime/runtime.py
Comment thread src/hai_agents_local/sessions.py
@abonneth

abonneth commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator

Folded #223 into this PR (fast-forward, no other changes). Merge order unchanged.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread src/hai_agents_local/runtime/runtime.py

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread src/hai_agents_local/runtime/runtime.py
@abonneth

abonneth commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator

Simplification pass (6 commits, net -139 lines, no behavior change)

Change Why Lines
Drop LocalRuntime.force_kill() / health() No callers; pid file kept (still read by stop tooling) -20
README: local section = usage + 1 inference line Internal/volatile details drift -40
verify_runtime set once in sessions._localize (class default on bridge) Was threaded through 7 signatures -13
Runtime pin moved to runtime/pin.json; bump script = load, validate, dump No regex rewriting of Python source; same validation (partial bump, unknown platform, placeholder sha) -31
Shared base for sync/async local sessions (state, close-failure handling) Only the awaits differ 0
One-line docstrings, drop narrating comments Readability -35

Tests

  • pytest: exit 0, 247 (was 245; +2 new bump-script cases), same 13 skips
  • ruff check + format --check clean on changed files
  • Wheel build: pin.json ships; manifest imports from an installed wheel with identical URLs
  • client.py unchanged

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit f4659fd. Configure here.

Comment thread src/hai_agents_local/runtime/acquire.py Outdated
@abonneth
abonneth merged commit f9e0067 into main Oct 5, 2026
6 checks passed
@abonneth
abonneth deleted the charlie/placement-python-sdk branch October 5, 2026 13:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants