Connections panel — fork-side slices (credential store, routes, tab UI) - #33
Conversation
…n fixture (159/S1 AC1)
Read/write/clear seam in the fork server's amicode module space; the
company-compute backend writes the FROZEN cloud.json byte shape
({base_url trimmed of trailing slashes, token}, nothing else), locked by
a golden fixture the amicode-side tests can consume byte-for-byte. Path
resolution honors the same AMICO_CLOUD_FILE override the CLI's
remote-config reader uses, so the test seam is the compat seam.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Write a sibling tmp file with mode 0o600 passed at creation (never the 0666&~umask default, never a post-hoc chmod), then rename over the target — the inode swap also corrects a pre-existing wrong-permission file. The rename step is an injectable hook so the test asserts the TMP file's mode before it ever becomes the target. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… writer (159/S1 AC3) A thrown rename leaves the old credential byte-for-byte intact (or no file at all on a first write) and never a tmp leftover. Verified the tests bite via a mutation check: removing the writer's tmp cleanup fails exactly these two tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t rest (159/S1 AC4)
pasqal.json fixture locks the {project_id, token, expires_at?} byte
shape. Poison tests push password/username-bearing objects through the
seam (both backends): the write throws (message never echoes a value),
the seeded credential stays byte-identical, and a whole-dir byte scan
proves the secret exists in no file. Null tokens are rejected; unknown
non-poison keys are allowlist-dropped. Mutation check: disabling the
poison guard fails exactly the poison test.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…(159/S1 AC5) Missing, unparseable, and off-schema (hand-edited) credential files all read back as undefined; the golden fixture bytes parse through the same read path, closing the reader side of the cross-repo contract. Mutation check: a throwing read path fails the unparseable-file test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ys (165 AC2 core) 401 → invalid, 2xx/403/404 → valid, everything else (incl. network failure) → unreachable — the parent #159 fake-task probe contract. Token rides the Authorization header only, asserted never-in-URL. FetchImpl is the injectable seam; no live network in tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
….json cache (165 AC3) GET body is built ONLY through a whitelist parser that rebuilds fresh objects from declared, type-checked fields — poison test seeds token/ password keys into the cache file AND the in-memory overlay at every level and asserts absence. Cache rides the sibling ops-dir env-override idiom ($AMICODE_CONNECTIONS_FILE → ~/.amico/connections.json); the credential file stays the truth for 'connected'; overlay entries render 'validating'; stale is computed freshness metadata (STALE_MS). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…minal status, one round trip (165 AC1+AC2) Valid class writes via the #162 CredentialStore and answers 'connected' in the same response; 401 answers 'invalid' and writes NOTHING (an existing credential survives untouched); server/network trouble answers 'unreachable' with a fixed token-free message. While the probe runs the in-flight overlay renders 'validating' for concurrent GETs. Malformed bodies fail value-free without firing a probe. Status-cache writes ride the same whitelist + atomic writer, so a poisoned file is scrubbed on the next write. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…165 AC4) Disconnect clears the credential via the #162 seam, drops the cache entry, answers needs-key; idempotent on an absent credential. Revalidate re-runs the probe from the STORED credential — the secret never rides the request — and refreshes validated_at on every outcome; invalid keeps the credential (re-entry is the user's call, only disconnect deletes). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nds with a distinct error (165 AC5) isLoopbackHostname recognizes the mdns gate's loopback family widened to 127/8 + the v4-mapped form; undefined (in-process webHandler, no socket) counts as loopback. Server.listen will record the bind at listen time; setBindHostname doubles as the test seam, and MutationDeps takes a pure bindHostname override. Refusal code: non_loopback. The read-only status route keeps serving. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…serve lifecycle with no extension host (165 AC6) Route block rides the sibling raw-route idiom + the same authOnlyRouterLayer auth wrapper (#163): GET /amicode/connections, POST /amicode/connections/{credential,disconnect,revalidate}. The credential rides the POST body (library idiom) — never query params. Server.listen records its bind hostname so mutations refuse beyond loopback; proven over live HTTP against real 0.0.0.0 and 127.0.0.1 listeners, with a node:http stub standing in for the solve service. Route-level auth (401 before any handler), poison, and invalid-class coverage included. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ning) Full-suite run exposed it: the mdns test's stopped 0.0.0.0 listener left the recorded bind behind, and a later in-process mutation was refused. Server.listen now restores the PREVIOUS bind when the listener scope closes (or the listen fails), asserted in the real-bind route test; the connections tests also reset bind state in beforeEach for isolation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e in revalidate Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…/fields collapse safely (166 AC4) Single consumer of the #165 status contract on the UI side (vaults.ts idiom): GET {ok,connections} and POST {ok,connection} both parse through one whitelisting entry parser that never throws — unknown states render via the "unknown" fallback with the raw word preserved, unknown fields (including a poisoned token) have no path into the view. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…imo + write the switching request (167 AC1+AC3)
A VALID save now writes BOTH shared ops-dir artifacts the amicode extension
consumes: entitlements.toml (issimo granted, existing codes + expired list
preserved, byte-compatible with applyEntitlementForMode) and solver-mode.json
({mode:"hp",status:"switching"}, the exact shape watchSolverMode re-preps
from). Paths resolve via $AMICODE_OPS_DIR → ~/.amico/amicode, the extension's
amicodeOpsDir resolution, so tests stay hermetic. Headless the artifacts are
plain durable files pending the next extension attach.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tate, safe unknown fallback (166 AC1) stateCopy/cardModel are the pure state→props mapping the Company Compute card projects: connected exposes actions + validated_at (+ identity when present), the three re-key states expose the form, validating freezes it, and unknown keeps every exit open while badging the raw wire word. The repo has no tsx component harness, so this mapping carries the card's behavioral contract exhaustively. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…dempotent (167 AC4) Invalid/unreachable/malformed/refused submits produce no flip artifacts; failed revalidation and disconnect never revoke (the flip is one-way — the user's toggle owns reverting). A repeat valid save on an already-granted, already-hp setup writes NOTHING: the switching request only goes out when a re-prep would change something (mode not hp yet, or the grant was missing), so the watcher — whose re-prep restarts this very server — is never poked for a no-op. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…it is a no-op, validating in flight, terminal from the same response (166 AC2+AC3)
submitPayload returns undefined for any empty/whitespace field, so the
card fires no request and touches no state (AC3). applyConnectionOverlay
is the pure wrapper the app layer puts around ONE POST: {validating:id}
renders the in-flight card, then {terminal} — parsed from the SAME
response — replaces it; no polling loop, base view never mutated,
sibling cards untouched (Pasqal-ready).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…+ fixed value-free warning (167 partial failure) requestHpFlip never throws; a failed entitlement/solver-mode write surfaces as ok:true + connected with the FIXED "hp_flip_failed: …" string in the response error field (the module's code:detail shape, promoted to a warning channel on renderCurrent). No token, path, or errno ever rides the message; the credential and status cache stand exactly as saved. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…AC1/AC3/AC4 at route level) Extends the #165 route surface: a valid submit through the standalone-serve route tree writes both ops-dir artifacts (no extension host in the process — headless durability), disconnect leaves them untouched (one-way flip), and a 401 submit produces no flip artifacts at all. Ops dir rides $AMICODE_OPS_DIR in both connections suites so every flip write stays hermetic. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n the status popover (166 AC1) connections-tab.tsx is a thin projection of the tested cardModel: status dot + distinct per-state copy, key form (base_url prefills from the wire when offered; token rides a password-masked input, cleared once a submit lands connected, never echoed back), disconnect/revalidate when connected, raw-word badge for unknown states. Registered as a Tabs.Trigger/Content pair beside the vaults tab with the same per-active-server authenticated fetch; mutations wrap ONE round trip via applyConnectionOverlay (validating in flight → terminal from the same response). Re-export shim rides the components/* wildcard; copy lands in en.ts (locale fallback idiom, same as vaults). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ersistence (169 AC1+AC2) Submit for id=pasqal-cloud spawns the #164 validator through an injectable PasqalSpawn seam: argv is <python> <script> only (no secret ever rides argv) and the child env is EXACTLY {PATH, PASQAL_USERNAME, PASQAL_PASSWORD, PASQAL_PROJECT_ID} — never a process.env spread, asserted against a canary. Interpreter resolves $AMICO_PYTHON → python3; the script resolves $AMICO_PASQAL_VALIDATOR → the amicode-staged copy under the shared ops dir. A valid run persists the minted token ONLY (#162 seam: project_id + token + expiry) with identity + device metadata in the status cache, refreshed on every submit. Pasqal never touches solver mode — the HP flip stays company-compute-only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…entitled + distinct config warning (169 AC5) Exit 2/3/4 map to invalid / unreachable / unentitled (project-unauthorized) with nothing written and any stored credential untouched. Exit 1, unknown exits, off-contract stdout on exit 0, and spawn failure are the config class: unreachable-state plus the FIXED value-free PASQAL_CONFIG_WARNING on the #167 warning channel — distinct from a plain service-unreachable verdict. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…after restart (169 AC4) A valid run that mints NO token parks its connected claim — identity, devices, validated_at — in the new in-memory sessionOnlyOverlay: nothing reaches the credential file or the status cache, the wire status carries a session_only marker, and a fresh status build from disk (a restarted server) renders needs-key. Every terminal submit outcome supersedes the claim; disconnect ends it immediately. The session store is a whitelisted input like the in-flight overlay — the poison test now seeds it too. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… byte on disk (169 AC3) After the full Pasqal flow through every class — valid+token, session-only, invalid, unreachable, unentitled, config, off-contract stdout, a spawn error whose message embeds the password — a raw-bytes scan of the whole credential/ops tree finds the minted token artifacts but never the password or username; responses and the in-memory stores are equally clean, and a disconnect scrubs the token artifact too. Guard verified by mutation: a deliberately leaky persistStatus fails the scan. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…pawn (169 AC1-AC5 at route level) A stub validator honoring the #164 contract is staged OUTSIDE the scanned tree and launched through the DEFAULT spawner ($AMICO_PYTHON bound to the bun binary): the child records its env view, proving the real spawn hands over exactly {PATH, PASQAL_USERNAME, PASQAL_PASSWORD, PASQAL_PROJECT_ID} — no process.env spread, no argv beyond interpreter + script. Full lifecycle over the routes: valid submit → connected + devices + token-only at rest; revalidate never respawns (record file stays absent); disconnect scrubs the token; exit 2/4/1 → invalid/unentitled/config-warning; null token → session-only. Raw-byte scans of the credential/ops tree find no password. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ession-only + unentitled/expired copy (169) The wire parser now admits expired + unentitled as first-class states (distinct copy: token-expired and project-unauthorized), parses devices to display names and session_only to the sessionOnly marker — all tolerant, nothing throws. cardModel gains showDevices (connected only) and showSessionOnly; connectionFormKind routes pasqal-cloud to a username/password(masked)/project_id form whose payload gate (pasqalSubmitPayload) trims identity fields, passes the password verbatim, and yields nothing on any empty field. The password signal clears the moment a submit lands connected. Labels wired through the status popover + en.ts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ate (170 AC1) A credential file hand-edited after its last validation now renders stale:true (credentialFileMtime seam, MTIME_STALE_SLACK_MS guard for the connect path's own write order). Stale connected GETs render the cache IMMEDIATELY and kick a deduped background revalidation — company-compute probes the stored credential, pasqal reruns the local token-freshness check — whose result lands for the NEXT read. Tests join the work via backgroundRevalidationsSettled(); no sleeps, no live network. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… for both backends (170 AC2) Corrupt cloud.json / pasqal.json (garbage, wrong JSON shapes, empty) render needs-key with no stale badge, no identity, no crash — even when the status cache still claims connected with every staleness trigger armed; no background revalidate fires and the mutation path answers needs-key instead of probing. Behavior existed via the #162 tolerant read; these tests pin the honesty. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ver invalid (170 AC3)
A background revalidation that cannot REACH the service marks the cache
entry offline:true and leaves the connected state, validated_at (the
last-verified marker), identity and credential untouched — offline is a
connected-only presentation flag through the whitelist (poison-tested;
only the literal true survives). The card renders 'Offline — last
verified <at> as <identity>' via a {{slot}} label template filled with
display values (fillLabelTemplate/offlineCopy in the ui module); a
successful refresh clears the flag.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…0 AC4, canary for 2026-07-19)
The probe seam now surfaces an optional identity echo — a valid probe
response body carrying {submitter} (stubbed here; live endpoint is
aws-infra#185) — and a connected card shows it as its identity. A later
revalidation (manual OR background) answering a DIFFERENT submitter
persists identity_drift beside the UNTOUCHED stored record: the card
renders 'This key answered as X, was Y — historical runs may stop
authorizing'. Agreement clears the drift; no echo leaves record and
drift standing; re-submitting credentials is the human reconciliation
that resets the record. identity_drift rides the redacting whitelist +
tolerant parser (poison-tested at both ends).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…time, both backends (170 AC5) renderStatus now maps a connected claim whose expires_at has passed to the 'expired' wire state at READ time — company-compute behaves identically to pasqal's #169 path, no revalidation needed to notice. Expired is its own state: never a stale cue, never an offline marker, never a background kick; the credential stays on disk (reconnect ≠ disconnect) and the UI's existing expired copy ('Token expired — reconnect to mint a fresh one') is the prompt. Absent/unparseable expiry expires nothing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…r pass (170) Route-level coverage extending #165's suite: an offline boot against a dead loopback port (stale GET answers at once, the failed background refresh marks offline — never invalid — and the credential survives); the identity echo + drift diff against the stubbed identity endpoint (stub solve service now answers a scripted JSON body, the aws-infra#185 shape); read-time expiry answering 'expired' with the credential kept. Route afterEach drains background revalidations before the env flips. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
With OPENCODE_SERVER_PASSWORD set, the document authenticates via ?auth_token= but its own <script>/<link> sub-resource fetches cannot carry a credential — the shell loaded and its bundles 401'd, blanking the entire UI. Found live-testing the Connections panel (amicode#159): the password was never armed before amicode#163, so the narrow PUBLIC_UI_PATHS allowlist was unreachable dead code until now. The content-hashed compiled shell carries no secrets; the API surface (including all /amicode/* routes) stays fully authed. Bypass is GET-only and prefix-exact — POST /assets/* and /assetsish/* still 401 (covered by the new regression test). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Live test-drive of the packaged build surfaced a latent auth gap, fixed in c76749a: with a server password set (first armed by amicode#163), the app document authenticated via ?auth_token= but its fingerprinted /assets/* sub-resource fetches — which cannot carry a credential — 401'd, blanking the whole UI. The narrow PUBLIC_UI_PATHS allowlist was unreachable dead code until auth became real. Fix: GET-only /assets/ prefix joins the public-UI bypass (shell carries no secrets; every API route stays authed); regression test covers the bypass + the POST//assetsish boundary. |
Same constraint class as the /assets/ fix one commit back: widget
frames are iframe DOCUMENT requests (/amicode/widget-frame?id=) that
cannot carry ?auth_token= or a Basic header, so with a password armed
every widget 401'd and hit its 5s boot timeout ("widget did not boot
within 5s" across the whole home grid — found live-testing amicode#159).
The served document embeds only registry widget code + the frame
runtime (CSP default-src 'none'; config/context/data arrive via the
mediated postMessage bridge after boot), so it is public-shell class
within the same-user trust model. The widget registry route
(/amicode/widgets) and every other API route stay authed — covered by
the new regression test's boundary assertions.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Second live-test find, fixed in ca69ef0: widget frames are iframe document requests (/amicode/widget-frame) — credential-less by the same constraint as the /assets/ sub-resources — so with auth armed every widget 401'd into its 5s boot timeout. The frame doc is public-shell class (registry code + runtime only, CSP default-src 'none', data via the mediated bridge post-boot); it joins the public-UI bypass. The widget registry route and all other API routes stay authed (boundary-tested). These two finds are exactly why the panel work needed a packaged test drive — no unit layer loads a document's sub-resources/iframes with auth required. |
AMICODE_HIDE_STATUS_POPOVER was set 2026-07-07 to suppress the popover
when its only content was upstream MCP/LSP operator noise ("slot
reserved for a future solver-health panel"). The Vaults tab (7/15) and
the Connections tab (amicode#166) have since made it the home of
amicode-first UI — the stale flag orphaned both: the Connections panel
was compiled in but unreachable. Found live-testing amicode#159.
Follow-up (discoverability): a Connections entry on the home chrome so
"connect to cloud" doesn't require an open session.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Third live-test find, fixed in a0b67cb: the status popover — the only mount point of the Vaults AND Connections tabs — was hidden fork-wide by AMICODE_HIDE_STATUS_POPOVER, set 7/7 when the popover's only content was upstream MCP/LSP noise ('slot reserved for a future solver-health panel'). Both tabs were compiled in but unreachable. Flag flipped with a dated comment; follow-up filed mentally for a home-chrome Connections entry (connect-to-cloud shouldn't require an open session — will add to #159's polish list). |
Inside the VS Code webview iframe native paste never fires and native copy never reaches the OS clipboard, so every editable outside the prompt input silently ignores mod+V and poisons the next paste on mod+C. New utils/global-clipboard module: capture-phase mod+V/C/X on editable targets (text-like inputs incl. password, textareas, contenteditables), routed over the existing extension-host bridge, with selection-aware insertion/extraction and bubbling InputEvents so Solid-controlled inputs update. data-amc-clipboard="self" opts an element's own paste handling out; copy/cut still mirror there since nothing element-local handles them. Unframed: untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ab set (#174) Pure, tested decisions for the popover's two mounts: the session trigger renders unconditionally (the show-status setting scopes down to the health dot), and the home-chrome global surface hosts exactly vaults + connections with connections pre-selected. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…heir paste (175) entry.tsx swaps the paste-event fallback (dead in the webview — the paste event never fires there, which is why Connections credential fields ignored mod+V) and the copy mirror for the single tested installGlobalClipboardFallback. The prompt input's editor roots and home-cards' profile fields mark data-amc-clipboard="self" so their own bridged paste handlers keep sole ownership — no double insertion. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… trigger (#174) AC1 — home chrome: a Connections pill in the top-right cluster (between Sessions and the settings gear, matching the Sessions pill idiom) opens StatusPopoverGlobalBody — the same Vaults + Connections wiring the session status popover hosts, extracted into createAmicodeStatusTabs (one wiring, two mounts). The global body hosts only the global tabs (mcp/lsp/plugins are per-directory; home mounts no directory-scoped sync context) and pre-selects Connections via Tabs defaultValue. Manage-vaults is injected: home starts a fresh draft session with the manage prompt (no composer exists on home). Collapse-to-hamburger CSS covers the new pill; home's menu outside-click handler now treats portaled popover content as inside so the dropdown cannot display:none the popover's anchor mid-credential-entry. AC2 — session header: the status trigger renders whenever a session is open; settings.general.showStatus (default OFF, UI copy 'Server status') scopes down to the health-dot overlay only, per statusTriggerVisibility in the tested policy module. Settings description updated to match. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Live-test session wrap: three more slices landed on this branch since the last note — 64edf12 (#175: capture-phase clipboard bridge for every editable — the webview never dispatches paste events, the old bubble-phase fallback was dead code) and 5b0def9 (#174: global Connections pill on the home chrome, Connections pre-selected; session trigger un-gated from showStatus which defaults OFF and was orphaning the tabs; setting rescoped to the health-dot). Full tally for this PR: 6 panel slices + 5 live-test finds, all with regression tests; gates green throughout (app 393/0 · ui 270/0 · server suites 114+/0). |
Connect / Revalidate / Disconnect / Retry rendered as underlined text links; the status-popover surface already uses the ui Button component. Connect = primary, Revalidate = secondary, Disconnect/Retry = ghost, all size small; disabled state rides the component. Kate's live-testing feedback (amicode#159). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fork-side integration branch for the Connections panel DAG (harmoniqs/amicode#159). All fork-side slices complete, every gate green (114/0 server connections+credentials suites · 270/0 ui · 388/0 app · tsgo + prettier clean). Companion amicode PR: harmoniqs/amicode#172.
Merged slices:
For the drift canary to light up live, aws-infra#185's probe response needs one string field
submitteron auth-passed bodies.🤖 Generated with Claude Code