Wire the A1 catalog hook into the two display paths a sandbox-only custom
falls through today:
- async_get_integration_descriptions (loader.py): append catalog
descriptors to the custom integration/helper buckets so the add-
integration picker lists them. On-disk customs carry richer metadata,
so the disk scan wins on a domain collision.
- _async_get_component_strings (helpers/translation.py): when a domain
has no on-disk Integration (IntegrationNotFound on main), take its
"title" from the catalog — a localized title_translations[lang] if
present, otherwise degrading to the descriptor name.
Tests: catalog entry appears in descriptions with picker name + defaults
+ helper-bucket routing; on-disk custom wins a collision; title fallback
uses title_translations and degrades to name when absent.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a separate, display-only catalog hook so a custom integration whose
code lives only in a sandbox (never on main's disk) can be listed and
named in the add-integration picker without spawning a sandbox.
Core (homeassistant/loader.py) owns the registry because core consumes
it: SandboxIntegrationDescriptor, SandboxCatalogProvider,
DATA_SANDBOX_CATALOG_PROVIDERS, async_register_sandbox_catalog_provider,
async_get_sandbox_catalog. This mirrors the Phase B translation-provider
precedent (hook + consumer co-located in core).
homeassistant/components/sandbox/catalog.py re-exports the hook so HACS
registers through a sandbox namespace parallel to the source resolver —
but the catalog stays deliberately separate from the sha-pinned, security-
critical source resolver: it is eager, enumerable and cosmetic only.
Wired into descriptions + title fallback in A2.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implement SandboxTranslationProvider and register it into core's translation
hook from async_setup (unregistered on stop). For each requested component it:
- resolves the owning sandbox group — a loaded entry's .sandbox field wins,
else the live SandboxFlowProxy of a brand-new custom's in-progress flow
(new sandbox_group accessor on the proxy);
- carves out built-ins (Integration.is_built_in ⇒ main reads its byte-identical
disk copy, never the wire);
- batches each group's custom domains into one get_translations RPC per
language (5s timeout), and degrades to empty strings on a down/closed/slow
channel so the cache-lock overlay never blocks the frontend.
router.async_unload_entry now invalidates a sandboxed entry's cached
translations, so a reload at a new integration-source ref re-pulls fresh
strings on the next fetch.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a sandbox-agnostic seam to the translation cache, mirroring the
sandbox.sources source-resolver convention:
- async_register_sandbox_translation_provider(hass, provider): a HassKey-backed
registry with an unregister callback. The provider is awaited inside the
cache load and returns {language: {domain: raw_strings}} for only the domains
it owns.
- _TranslationCache._async_load overlays the provider result onto
translation_by_language_strings after async_get_integrations and before
_build_category_cache, so sandboxed strings flow through the same flatten /
English-fallback / loaded machinery as on-disk strings. A custom sandboxed
domain (IntegrationNotFound on main) thus stops resolving to {}.
- _TranslationCache.async_invalidate + async_invalidate_translations wrapper:
the first eviction API (translations were never unloaded), called by the
sandbox when a custom integration is re-fetched at a new ref.
Core never raises on a provider; degrade-to-empty is the provider's contract.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Register a sandbox/get_translations handler in SandboxRuntime. It loads raw
translation strings for the requested domains from the sandbox's own
filesystem (built-in from the bundled package, custom from the fetched
<config>/custom_components/<domain>) by reusing core's
_async_get_component_strings against the sandbox-private hass — which also
pre-fills 'title' from integration.name. Main cannot run that fallback for a
custom domain because it holds no Integration, so the title must be injected
here. Replies with {language, strings: {domain: raw dict}}.
Tests cover built-in title pass-through, custom title injection, the empty
case, the Struct packing, and the no-flow-runner guard.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the sandbox/get_translations message pair to the control-channel proto
and regenerate the checked-in gencode for both no-cross-import mirrors.
Mirror MSG_GET_TRANSLATIONS in both protocol.py files and register the
message pair in both messages.py REGISTRY copies.
Request {language, domains[]}; result {language, strings: {domain: raw
strings.json dict}} — main batches a group's custom domains into one call;
built-in domains never cross the wire.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Brainstorm → plan for forwarding a sandboxed integration's translations
into main: live pull-RPC (Phase B) for running integrations + a catalog
provider (Phase A) for picker discoverability. Includes interview,
research notes, scratchpad, and the phased plan.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rewrote the briefing so it never frames the brief as a file or mentions
the former tempfile handoff: compose it, pipe it straight into the session
(heredoc), claude-screen pastes it as one message. Dropped the file-pipe
example and the "no tempfile dance" aside.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Now that claude-screen pastes multi-line directly, the brief no longer
needs a tempfile + "Read /tmp/X" pointer. Step 1 reads "Compose the brief"
(source it from a heredoc or any scratch file) instead of "Write the
brief"; step 2 shows both heredoc and file pipes. The brief is just stdin,
not a handoff artifact.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Follow-up to the status/ reorg: the brief's STATUS path and the monitor
until-loop both point at sandbox/status/STATUS-<plan>.md.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Tidy the directory: the 29 per-phase + per-plan landing records
(STATUS-phase-*.md, STATUS-plan-*.md) move out of the sandbox/ root into
sandbox/status/ (git mv, blame preserved). Live current-state docs
(CLAUDE.md, README.md, OVERVIEW.md, FOLLOWUPS.md, architecture.html, the
docker-compose harness comment) now point at status/. Historical records
(the STATUS bodies themselves, plans/*.md, plan.md) keep their original
text by convention.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
claude-screen pastes multi-line prompts directly: bracketed-paste markers
keep embedded newlines literal, and the submit \r is sent as a separate
keystroke a beat later (the concatenated \r was what raced the paste and
submitted mid-prompt). Verified live — a 3-line prompt lands as one
message. Doc no longer mentions any file-handoff.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The single-line file-handoff is now built into claude-screen itself (it
detects a newline in the piped prompt, stashes the brief to a tempfile,
and pastes a pointer). So the doc just pipes the brief straight in; the
manual "write to /tmp + echo a single line" dance is gone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Make explicit that each sub-session steps through its plan with the
phx:work skill (task-by-task with per-step compile/test verification),
not ad-hoc edits. Added as a brief hard rule + a why-this-shape bullet.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Documents the loop used to build this batch: write a brief to a tempfile,
spawn a fresh Claude in a screen window via single-line file-handoff, watch
for a STATUS marker, verify independently, push, kill the window. Captures
the gotchas that bit (single-line stdin, prompt-submit confirmation,
prefix-match window names, orchestrator-only push).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Drop the build-phase scaffolding from the test name; it just verifies ai_task
and image pin to ALWAYS_MAIN. -> test_ai_task_and_image_pin_to_main.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The one-shot full cross-sweep that produced the original backlog
(run_compat_full.py + categorize_failures.py + generate_backlog.py) and its
machine-generated outputs (COMPAT_FULL.md/.csv, COMPAT_LATEST.md, COMPAT.csv,
BACKLOG_FAILURES.json) were Phase-16 measurement scaffolding; the gate is long
cleared. Keep the single ongoing runner (run_compat.py) and the two curated
summaries (COMPAT.md, BACKLOG.md). Git-ignore the per-run machine output so it
stops being checked in. Living docs updated; recover the full-sweep tooling
from git history if a fresh tree-wide sweep is ever needed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The registered-service forwarder (_build_service_forwarder._forward) rebuilt its
own pb.CallService request and duplicated the ChannelRemoteError/
ChannelClosedError translation that _raw_call_service already does. With the
batcher gone, _raw_call_service is the single low-level send helper — have
_forward call it and keep only its response-extraction logic. No behaviour
change (the channel-closed error message is now the shared generic one).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The spike (hass_client/spike/: bridge_a, bridge_b, rig, synthetic_light,
transport + tests/components/sandbox/test_spike.py) was a one-off bake-off to
choose between entity-bridge designs. Option B was chosen and shipped long ago;
nothing in production imports the spike, only its own test did. Delete it.
docs/entity-bridge-decision.md keeps the rationale and the measured numbers as
the decision record, with a note that the harness is recoverable from git
history.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Each proxy entity service call now forwards as its own single
`sandbox/call_service` RPC. The per-loop-tick coalescing batcher
(_CallServiceBatcher / _BatchBucket) added complexity the first iteration
doesn't need, so it is removed; async_call_service calls _raw_call_service
directly. Behaviour is unchanged except a multi-entity area call now pays one
RPC per entity instead of one coalesced RPC.
Coalescing same-tick calls is recorded as a future optimisation in
docs/FOLLOWUPS.md (with the 200-light perf benchmark that validated it). Living
docs updated; the phase-history records are left as-is.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A service call is never fire-and-forget: each batched caller awaits the
coalesced RPC's completion via its future, which resolves with the result or
the raised error, so every caller learns when its call finished. Batching only
shares the *wire* call, not the await; only a response *value* can't be
coalesced (hence the response bypass). Wording-only; no behaviour change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The final deliverable should not carry the scaffolding of the phases it was
built in. Reword comments, docstrings, and generated-output strings that named
build phases (Phase N / T1-T3 / Phase A1-A2) to describe what the code does,
and rename the phase-numbered test files:
test_phase4_subprocess -> test_subprocess
test_phase9_shutdown -> test_shutdown
test_phase13_proxies -> test_domain_proxies
test_phase14 -> test_schema_and_unload
test_phase19_devices -> test_device_registry
Comments/docstrings/filenames only; no logic changes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Replace a stray function-local `import json` (json.dumps behind a bogus
'keeps json off the integration boot path' noqa) with HA's orjson
json_bytes_sorted helper for the call-service batch key.
- A response-returning entity call now bypasses the per-tick batcher.
Coalescing forces every caller in a bucket to share one combined response,
which is wrong when a caller needs its own value; response calls go out as
their own single-entity RPC. The batcher is now fire-and-forget only, so its
dead return_response plumbing is dropped.
- Also removes development-phase references from this file's docstrings.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rewrite docs/auth-scoping-decision.md to lead with the shipped design: the
sandbox holds no credential and cannot fabricate a Context; main restores
attribution from a TTL cache of contexts it issued and falls back to
user_id=None. The reverted, never-shipped scoped-token mechanism is kept as a
clearly-marked appendix for whenever the sandbox->main websocket lands. Update
the CLAUDE.md pointer to match.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rename identifiers (SandboxV2Data->SandboxData, DATA_SANDBOX_V2->
DATA_SANDBOX, SandboxV2Error->SandboxError), env vars (SANDBOX_V2_*->
SANDBOX_*), and stale sandbox_v2/ paths in the current-state docs
(OVERVIEW, README, CLAUDE, plan, architecture.html, COMPAT*, docs/*),
and reword prose that named the current sandbox "v2". Historical
STATUS-phase-*/plans/* records are left intact as point-in-time history.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The rename sweep missed several identifiers, env vars, and the
pre-commit drift-guard hook (whose entry/files paths still pointed at
the non-existent sandbox_v2/ tree, leaving the hook broken). Rename:
- SandboxV2Data -> SandboxData, DATA_SANDBOX_V2 -> DATA_SANDBOX,
SandboxV2Error -> SandboxError (+ all references and tests)
- SANDBOX_V2_ERRORS_DIR/TRANSPORT/SOCKET_PATH -> SANDBOX_* env vars
- pre-commit hook id/entry/files: sandbox_v2/proto -> sandbox/proto
- stale sandbox_v2 paths and 'v2' wording in .dockerignore + scripts
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The reverted Phase-7 auth-scoping mechanism never shipped, so no
real auth store carries a legacy "scopes" key. Remove the defensive
pop in AuthStore (RefreshToken is built by explicit field mapping, so
unknown keys are ignored anyway) and its test. Reword the
ConfigEntry.sandbox load comment to state the real reason the key is
optional (non-sandboxed entries omit it) instead of referencing an
unreleased phase.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Squash of the pre-83a0c28229f sandbox history onto current dev. The
original commits (including the grafted standalone hass-client repo,
root cc2428c2b56) cannot replay through git rebase; the full history
remains available at the local sandbox-backup-20260707 branch.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QCotUYum6AoisyrxshoiJJ