Skip to content

fix: clear scoped cloud-pair key on explicit disconnect - #1

Open
sharkwon wants to merge 600 commits into
developfrom
fix/clear-cloud-pair-token-on-explicit-disconnect
Open

fix: clear scoped cloud-pair key on explicit disconnect#1
sharkwon wants to merge 600 commits into
developfrom
fix/clear-cloud-pair-token-on-explicit-disconnect

Conversation

@sharkwon

@sharkwon sharkwon commented Aug 6, 2026

Copy link
Copy Markdown
Owner

Test PR to trigger fork CI

(cherry picked from commit 0dcaf1409bfcf1a9b5acf49d4847bbb75af7c765)
Extends elizaOS#17715 with the typed J4 source-state boundary found during live preview QA.

AI provider/model: OpenAI / gpt-5.6-sol
Client / agent tooling: Codex desktop
Contribution skill revision: 04ff898:packages/skills/skills/contribute-to-eliza
Attribution status: self-reported
— [codex/restack-demo-0804]

eliza-computer-attribution:v1 {"agent":"codex","provider":"openai","model":"gpt-5.6-sol","client":"codex-desktop","skill_revision":"04ff8982861e7aa5045c0a065fb7e1d8750e6e6b:packages/skills/skills/contribute-to-eliza","status":"self-reported","marker":"codex/restack-demo-0804"}
Closes elizaOS#17726

AI provider/model: OpenAI / gpt-5.6-sol
Client / agent tooling: Codex desktop
Contribution skill revision: 04ff898:packages/skills/skills/contribute-to-eliza
Attribution status: self-reported
— [codex/restack-demo-0804]

eliza-computer-attribution:v1 {"provider":"openai","model":"gpt-5.6-sol","tool":"codex-desktop","skill":"04ff8982861e7aa5045c0a065fb7e1d8750e6e6b:packages/skills/skills/contribute-to-eliza","status":"self-reported","lane":"codex/restack-demo-0804"}
harshith8gowda and others added 21 commits August 7, 2026 06:17
…OS#17686)

* fix(scripts): replace rg with git ls-files in packageJsonPaths

Removes undeclared ripgrep dependency. Uses git ls-files which is a hard
requirement everywhere. Adds ENOENT error handling to report missing
git binary instead of generic failure message.

Closes elizaOS#17681

* fix(scripts): keep packageJsonPaths matching only real package.json files

git's `*package.json` pathspec is a suffix match, so it would also return a
stray `foo-package.json` that `rg --files -g package.json` never matched. It
happens to return the same 153 files today, which is exactly the kind of
equivalence that stops being true silently. Filter on the basename so the
replacement is behaviour-preserving by construction rather than by coincidence.

---------

Co-authored-by: lalalune <shawmakesmagic@gmail.com>
* fix: repair Windows verification contracts

# Conflicts:
#	packages/app-core/scripts/ensure-shared-i18n-data.mjs
#	packages/import-conversations/package.json

* docs: sync native gateway lint guidance

---------

Co-authored-by: Shaw <shawmakesmagic@gmail.com>
…efore React boots (elizaOS#17893)

* fix(agent): seed elizaos:active-server accessToken in injected HTML before React boots

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(agent): keep window.__ELIZA_API_TOKEN__ alongside the boot-config seed

Seeding boot-config apiToken is the right fix — getElizaApiToken() reads
only the boot-config store, so the bare window global never reached it.
But the global is not dead: plugin-native-agent and
plugin-native-websiteblocker read `window.__ELIZA_API_TOKEN__` directly
(documented in both plugins' README/CLAUDE/AGENTS) and the Android
WebView hydrates it. They fall back only to sessionStorage, which nothing
seeds, so dropping it sent their API calls unauthenticated.

Emit all three sinks and document why each exists, including the
first-run-complete write, which only applies to already-provisioned
deployments.

Replaces the substring assertions with tests that execute the injected
script against a stubbed browser global, so a syntactically broken seed
can no longer pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: elizaOS Developer <developer@elizalabs.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: lalalune <autonomousresearcher@gmail.com>
Co-authored-by: Shaw <shawmakesmagic@gmail.com>
…ugin (elizaOS#17871)

Co-authored-by: Jeremy <jeremy@webtop-dev.0mod.com>
Co-authored-by: Shaw <shawmakesmagic@gmail.com>
* fix(ci): harden snap-publish workflow inputs

Move workflow_dispatch version/channel into env: and expand via shell
variables instead of interpolating ${{ }} into run: script text.

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

* ci: re-trigger PR gates after snap-publish hygiene

Empty commit to re-run Develop/Security Advisory gates that timed out.

* ci(snap-publish): validate version/channel before sed and upload

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
… and workflows (elizaOS#17599)

* fix(ci): re-land the review-hardened Bun contract on the refactored tree

develop adopted this PR's v1 skeleton (ci-bun-version.json + the early
line-based contract) via the refactor sweeps, then the same sweeps
regressed most of the site pins the contract exists to hold: bare
bun.sh/install in the deploy workflows and training cloud scripts,
presence-only install guards, a 1.3.10 default in docker-ci-smoke, a
1.3.13 Hetzner cloud-init download, a canary cell and mutable @v2 in
the computeruse CI template, and the contract execution dropped from
BOTH required lanes. Live demonstration of the drift class elizaOS#17044
names.

This commit is the review-hardened v2 contract (job-scoped structured
YAML resolution per maintainer review, Docker stage scoping, shell
default proving, job-scoped allowlist, 55-test suite) re-anchored to
the refactored tree: the gate list drops the two lanes the refactor
deleted (benchmark-tests, feed-env-audit), every regressed site is
re-pinned, and both required lanes execute the contract with the
exact-head inventory artifact again.

Contract green: 333 sites, 9 gate lanes, zero drift classifications.

* fix(ci): scan the surfaces the Bun contract was selecting itself out of

Addresses @lalalune's four findings on elizaOS#17599, rebased onto develop.

1. The scan precondition excluded exactly what invariant 2 targets. A
   non-.github YAML entered the inventory only if it already contained a
   `bun-version:` key — but a `oven-sh/setup-bun` step that wires no
   version has no such key, so the defective files selected themselves
   out and the gate reported a clean sweep of a set chosen to exclude its
   own counterexamples. 16 plugin workflows were running the action's
   floating "latest". The scan now also admits any file referencing
   oven-sh/setup-bun, which surfaced 30 violations on the first run, and
   all 15 files are pinned to the reviewed SHA plus bun-version 1.3.14.

2. test.yml had two contract invocations. Removed the bare one in the
   `changes` job; the `test-runner-vacuous-green-guard` copy is a strict
   superset (same check plus the inventory artifact), needs `changes`,
   and is deliberately outside the path gates, so coverage is unchanged.

3. A tracked path that will not open now fails with a named violation
   instead of a raw ENOENT. git ls-files can list a file the working tree
   lacks — sparse checkout, uninitialised submodule — and the crash named
   neither the contract nor the surface.

4. The snapcraft hardcode is reverted and the real gap is fixed. develop
   already pins BUN_VERSION="1.3.14" at line 86; inlining the literal at
   the use site made two places to bump. The reason the PR did that is
   that the shell-defaults scan was gated on `.sh`, so it never read the
   assignment inside a YAML block scalar and called the use unproven.
   That gate now accepts YAML, so develop's single pin proves its own
   use. Separately, deploy-railway.sh wrote `FROM oven/bun:canary-alpine`
   into a heredoc — scanDockerfile only inspects files NAMED Dockerfile,
   so a floating canary shipped past a contract whose headline promise is
   that none survive. Added an embedded-image scan (variant suffixes like
   -alpine allowed, versions not) and pinned it.

Verification on the rebased, develop-merged tree: contract exits 0 (368
sites, up from 363 before the widened scan), 60/60 in
ci-bun-version-contract.test.ts. Five new tests cover each finding, and
the headline one is mutation-checked — restoring the old precondition
fails "scans a non-.github workflow whose setup-bun wires no version".
The unreadable-path test builds a real git checkout, since a synthetic
tree uses the directory-walk fallback and cannot express a tracked-but-
absent file. Biome clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011gZy3feiGVKs61PDUzx1Cd

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix(cloud): scope durable onboarding admissions

* fix(cloud): harden onboarding durable replay cleanup

* fix(cloud): bound onboarding replay cleanup

* test(cloud): prove onboarding tenant isolation
Co-authored-by: lalalune <18633264+lalalune@users.noreply.github.com>
* fix(agent): bound what a snapshot capture materializes in the agent's own heap

Addresses review point 1 on elizaOS#17180 (elizaOS#17172 section 1).

The Cloud restorable-size check is strictly downstream of this process
having already assembled AND serialized the whole payload, so it bounds
what Cloud retains, never what the agent's heap burns getting there.
createAgentSnapshot ran five captures concurrently with no size
awareness at all: file capture base64-encodes whole files (~2.33x the
file size transiently) and the media set has no include predicate, so a
large agent could exhaust its container mid-capture. The memory watchdog
then force-restarts it, and the lifecycle sites that snapshot before
upgrade/shutdown/sleep degrade to a stale or missing backup.

Adds a produce-side budget charged as bytes are produced, so an
over-budget capture is refused at the first byte past the line instead
of after everything is resident. File entries are refused from the
declared stat size BEFORE the read, since charging afterwards is
charging after the allocation. An optional AbortSignal is threaded
through the directory walk so a cancelled capture stops descending. The
default ceiling is the shared canonical restorable limit, so the
producer refuses exactly what the consumer would.

Deliberately NOT claimed as complete: charging the Postgres rows bounds
the assembled snapshot, not the peak of the single query that produced
it — pool.query buffers the whole result set and node-postgres cannot
interrupt an in-flight SELECT. Bounding that peak needs batched keyset
reads or a cursor dependency in the agent image; that is a maintainer
call, called out in the code and the PR rather than papered over.

Stacked on elizaOS#17180, which introduces the shared limits module this
imports. Tests: 9/9 in packages/agent, including a red-control that the
oversized file is never read.

Refs elizaOS#17172 section 1.

* fix(agent): read agent-scoped Postgres tables in keyset batches

Closes the limitation the previous commit documented instead of solving:
charging the serialized rows bounded the assembled snapshot, but
pool.query buffers whatever a statement returns, so an unbounded
SELECT * WHERE agent_id = $1 still put an entire table in this process's
heap before the budget could see a byte. On a chat-heavy agent the
memories/embeddings/logs tables are the gigabytes.

The two unbounded shapes (the memories-joined embeddings read and the
generic owner-column read) now walk in keyset batches ordered by primary
key, charging the budget per batch, so the peak is one batch and an
oversized table is refused mid-walk. Keyset rather than OFFSET: OFFSET
re-scans from the start each page, and a snapshot taken while the agent
is live would skip or duplicate rows as they shift. The agent row is
already bounded by construction (WHERE id = $1), and a table with no
primary key keeps its single read with the post-read charge — documented
where it happens.

No new dependency: this is the keyset option, not the pg-cursor one that
would have needed a call about the agent container image.

Tests: 12/12, including that the walk covers a 1200-row table with no
duplicates or gaps, that every statement is ORDER BY + LIMIT and resumes
from the last id, and that an over-budget table stops early instead of
reading to the end.

Refs elizaOS#17172 section 1.

* fix(agent): make the snapshot budget reserve capacity and cover every capture path

Two holes from review. First, reserve() observed the projected total without
holding anything, so concurrent captures could each pass the same check, read
and encode in parallel, and only collide at charge time - after every payload
was already resident. reserve() now returns a settle-once token that claims
capacity other reserves must count; the actual encoded size is charged on
commit, and a failed read releases the hold.

Second, the PGlite dump path materialized the whole dump (Blob, ArrayBuffer,
Buffer, base64) without consulting the budget at all, and the pglite-files
fallback walk plus the character config read were equally unbudgeted. The dump
is now refused from Blob.size before arrayBuffer(), and every component of a
capture draws from the one budget.

A refusal in one component now also aborts the others: the capture owns an
internal AbortController wired into the shared budget signal, so siblings stop
at their next check instead of reading at full speed toward a result that is
already doomed.

The keyset-capture doc no longer implies transactional consistency; it bounds
memory, and says exactly that.

* fix(backups): close snapshot safety gaps

---------

Co-authored-by: standujar <s.andujar@proton.me>
Co-authored-by: moon <autonomousresearcher@gmail.com>
…lizaOS#17541)

* registry: add 4 x402 third-party plugins (concordance, trading-strategy-data, trustfetch, chainscope)

* registry: bump 4 x402 third-party plugins to 0.2.0 (real x402 payment fix)

* registry: bump 4 x402 third-party plugins to 0.3.0 (client-side payment policy fix)

Addresses the review on the prior PR: all 4 clients signed whatever
amount/network/asset/payTo a 402 challenge specified, with no local
ceiling, expected-recipient check, or asset/network pin. 0.3.0 adds a
local policy check before createPaymentHeader is ever called (exact
scheme/network/asset/payTo match plus a configurable per-request max
amount), with adversarial tests proving no signature is produced when
any of those fields is tampered with.

* registry: bump 4 x402 third-party plugins to 0.3.1 (fix source/npm mismatch + dependency audit)

0.3.0 was published to npm from local uncommitted changes that were
never pushed to the linked repos -- the review correctly couldn't
reproduce the payment-policy code or the adversarial test file from
the recorded gitHead. 0.3.1 is published from a commit that's actually
in each repo's history (verified: fresh clone + npm install + npm test
passes clean for all 4 -- 55/55 tests total).

Also resolves the high-severity axios/ws transitive advisories via
package.json overrides in each repo (both pulled in through x402's own
dependency tree, not direct deps of the plugins) -- 0 high remaining
per npm audit.

---------

Co-authored-by: Vohlsyr <vohlsyr@users.noreply.github.com>
Co-authored-by: Shaw <shawmakesmagic@gmail.com>
elizaOS#17933)

* fix(skills): keep the gh 2.45 --jq pagination form inside the retry loop

Rebased onto develop. Develop replaced gh's --slurp with `--jq .[]` because
Ubuntu 24.04 ships gh 2.45, which has no --slurp; this branch's retry loop was
built around the --slurp form. The loop now wraps develop's argument vector and
returns parsePaginatedJson(result.stdout, endpoint) so type errors name the
endpoint.

The retry tests were adapted to the line-delimited output --jq .[] produces
rather than slurped nested page arrays, the argv assertion now expects
--jq/.[], the spawn-failure case expects develop's endpoint-context wrapper
(which preserves `cause`), and the malformed-output case feeds a genuinely
non-JSON line, since a bare object is valid under the new form. Develop's
endpoint-context test is kept alongside the new retry coverage.

* fix(skills): repair retry validation and policy

* fix(skills): repair the spliced retry test block

Running the suite caught two defects introduced while adapting the retry tests
onto develop's `--jq .[]` output form:

- develop's "fails command and spawn errors with endpoint context" lost its
  closing `});`, so the retry tests nested inside it and the file ended with an
  unbalanced block (Unexpected end of file).
- three adapted `stdout` fixtures carried real newlines instead of escapes, so
  their single-quoted literals spanned lines (Unterminated string literal).

Both fixed; the suite runs green at 23 pass / 0 fail.

---------

Co-authored-by: lalalune <18633264+lalalune@users.noreply.github.com>
…am is real in CI (elizaOS#17956)

The two elizaOS#17768 gate-hint tests added by elizaOS#17770 require a live cache seam,
but the Cloud Tests workflow exports CACHE_ENABLED=false, which disables
the cache before MOCK_REDIS=1 is honored — every hint write returns
'unavailable' and both tests fail deterministically on every PR lane.
Pin CACHE_ENABLED=true at module top, matching inference-billing-fast-path
and inference-auth-cache.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
elizaOS#17545) (elizaOS#17930)

* docs(cloud): say what the release suite proves, and justify the fence asymmetry

krutftw's review of elizaOS#17545. Four documentation corrections, no behaviour change.

The 'concurrent releases' test cannot be concurrent on its harness: PGlite is a
single WASM backend behind one connection, so the two transactions queue rather
than interleave. It is renamed to what it actually pins — a second release of an
already-spent generation is a no-op — and the comment now states what is NOT
covered: real row-lock contention needs two independent PostgreSQL connections,
and under REPEATABLE READ the loser raises a serialization failure rather than
returning not-owned.

Also: record why dropping organization_id on the reap path is not a weaker fence
(agent_sandboxes.id is the primary key, so it selects the same row or none),
why the schema ensure is cheap on the teardown path, and why committing the
ownership flip is correct when the docker_nodes row is gone.

Refs elizaOS#17185

Rebased onto develop and renumbered the migration: develop landed
0189_agent_sandbox_lifecycle_revision_scope through 0192_oidc_request_binding
while this branch was open, so 0189_agent_sandbox_deletion_allocation_ownership
collided on both the ordinal and the journal idx. It is now 0193 at idx 192 with
when=1786392000000. The when value has to exceed the running maximum rather than
merely be unique, because the migrator computes pending work as
entry.when > lastAppliedCreatedAt.

* fix(cloud): restore deletion-allocation-ownership migration SQL dropped in rebase

The rebase renumbered the journal entry to tag
0193_agent_sandbox_deletion_allocation_ownership but lost the .sql file,
leaving the journal pointing at a migration that did not exist. Recover the
ALTER TABLE from the original PR elizaOS#17545 head as 0193, and add the
deletion_allocation_counted column to the agent_sandboxes fixture in the
app-container store recovery integration test so its inline schema matches
the query under test. Merges origin/develop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Zorba-the-buddhah <zorba.the.buddhah@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(core): record each model request once

* fix(openai): preserve cache usage in trajectories

* fix(core): surface trajectory fallback write failures

* test(core): align failed-provider dedupe expectation with recorded billed-but-failed attempts

Develop's elizaOS#17690 intentionally records a rejected provider attempt as a
finishReason="error" trajectory entry before the successful fallback's
generic record, so the wrapped-provider-failure case now sees two entries.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Shaw <shawgotbags@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…catalog checker into CI (elizaOS#17606)

* fix(i18n): restore 703 catalog entries from call-site defaults and purge 469 dead keys

en.json is the source catalog every other locale keys off, but 705 keys
used in source were absent from it — inline defaultValue masked the gap
for English users while making those strings untranslatable in all seven
other locales. 703 entries are restored with values extracted verbatim
from their call sites' defaultValue (zero conflicting defaults across
duplicate sites; additions-only diff). The remaining two keys pass
runtime-conditional defaults and are handled in the checker commit.

The reverse rot is purged in the same pass: 469 en.json keys referenced
by no literal, no source template prefix, and no allowlist entry — plus
one key translated in all seven locales but absent from en and used
nowhere — are deleted from every locale (3,741 lines, pure removals,
each file re-parsed after edit).

Refs elizaOS#17605

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgpHguHoGxK4pVEtZ6gY43

* fix(i18n): make the catalog checker wireable and wire it into the test gate

check-i18n.mjs existed to prevent exactly the drift the sibling commit
repairs, but its all-errors contract (any locale missing any key) made
it permanently red and it ended up invoked from no lane at all. The
contract now matches what CI can hold on every PR: source-locale
completeness, unused source keys, orphaned translations, and stale
allowlist entries are errors; non-source translation gaps print as
warning summaries, upgraded to errors under --strict-translations.

runI18nCheck is exported pure (no console/exit) with fixture tests per
failure mode, following the ci-bun-version-contract pattern. The
dynamic-keys allowlist gains an "uncatalogued" field for the two keys
whose call sites pass runtime-conditional defaultValues — cataloging
those would freeze one ternary branch and change rendered English, so
they stay deliberately uncataloged (with reasons) until the sites are
split into per-branch keys; a stale entry whose call site disappears
fails the contract. test.yml now runs the checker with the other
contracts.

Closes elizaOS#17605

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgpHguHoGxK4pVEtZ6gY43

* fix(i18n): teach the scanner tRef.current() and restore the key it wrongly purged

Review finding on elizaOS#17606 (@sharkwon): the dead-key purge deleted
documentsview.FailedToLoadDocumentsData from all eight locales while
DocumentsView.tsx:470 still calls it. Root cause is in this checker:
LITERAL_KEY_RE matched only `t("key")`, so the stable-ref indirection
`const tRef = useRef(t)` → `tRef.current("key")` — used in 12 places
across 9 files — was invisible, and keys reached only that way looked
unreferenced.

Two consequences, both real: seven locales lost a translated string and
now fall back to English, and English itself silently changed, because
the catalog value ("Failed to load knowledge data") outranks the call
site's defaultValue ("Failed to load Knowledge data") — the exact drift
class elizaOS#17605 exists to remove.

The regex now accepts the ref form, and the key plus all seven
translations are restored from origin/develop with their original
values, not from the defaultValue. Audited the whole purge against this
pattern: this key was the only casualty. Two fixture tests added — a
tRef.current key that IS catalogued must not be reported unused, and one
that is NOT catalogued must be reported missing.

Deliberately NOT adding `documentsview.` to the prefix allowlist as
suggested: with the scanner fixed the key is seen literally, and a
blanket prefix would suppress detection of genuinely dead documentsview
keys — weaker than the defect it would paper over.

Refs elizaOS#17605

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgpHguHoGxK4pVEtZ6gY43

* fix(i18n): re-land the catalog checker at its post-refactor home

The refactor relocated check-i18n.mjs to packages/app-core/scripts/
(rename detection was skipped — elizaOS#17695 changed 25k files — so it
presented as deleted; the factory's own cycle forensics caught the
move via git ls-tree). This re-lands the advisory-hardened checker AT
THAT PATH with two fixes the move surfaced: the repo-root resolve
gains the extra hop, and the direct-run predicate adopts
pathToFileURL (the same defect class elizaOS#17677 fixed — the old
`file://${argv[1]}` form breaks on spaced paths everywhere and on
Windows always). test.yml wiring repointed; the test suite moves
alongside and re-roots.

Catalogs are restored as PURE ADDITIONS over develop's live copies
(+116 keys per locale, en +847; the one true orphan removed from
every locale). The checker then caught 26 real gaps on live develop,
all fixed from their own sources: 24 missing keys filled from
call-site defaultValue / metadata defaultLabel+defaultTitle, and the
two state-dependent ternary call sites (cloudOverview localMode)
split into four distinct keys — one key can never carry two strings.

Checker: OK — 4434 literal keys, 8185 source keys × 8 locales.
Suite: 12/12 at the new path.

* test(i18n): budget the real-repo case for the tree it actually walks

The re-landed checker test's real-repo case scans packages/ui/src and
packages/app-core/src synchronously — 5.5s and 7.6s on two consecutive local
runs — against bun's 5s default per-test timeout, so it fails on duration alone.
Measured at 2402df0: 11 pass, 1 fail, the failure being 'this test timed out
after 5000ms'.

That is the wrong signal from the one case whose job is to prove the checker is
wireable into CI, and this PR's whole subject is wiring it into CI. A CI runner
on a cold filesystem is not faster than this machine.

Explicit 60s budget on that case only; every fixture case keeps the default.

* test(app): add a settings-surface walkthrough spec for PR evidence

The evidence gate now derives "UI surface PR" from changed files, so any PR
touching packages/ui owes a walkthrough recording rather than an N/A. This
spec records one: it renders and scrolls the settings surface with video
enabled per-context, so the rest of the smoke suite keeps its cost.

Deliberately does not drive section navigation — the shell overlay
intercepts pointer events in this harness, and a recording of navigation
would claim coverage this suite does not have. It captures the surface
rendering, which is what a catalog change owes.

* fix(i18n): fail closed on a missing allowlist and catalog the oidcContinue keys

Addresses @lalalune's three prioritised findings on elizaOS#17606, rebased onto
current develop.

1. The default allowlist path was wrong and the miss was silent. It
   pointed at packages/scripts/i18n-dynamic-keys.json; the file lives at
   packages/app-core/scripts/i18n-dynamic-keys.json. loadAllowlist
   answered the missing file with an empty allowlist, so the dynamic-key
   and uncatalogued rules did not run in the exact CI invocation and the
   checker still exited 0 — a green gate with two of its rules disabled.
   Path corrected and loadAllowlist now throws. "No allowlist" and "an
   empty allowlist" are different states; only the second is a
   configuration.

   The error text told operators to pass --allowlist, which did not
   exist, so that flag is now real, and the CLI translates the throw into
   a one-line message and exit 1 (error-policy:J1) instead of a stack
   trace in the CI log.

2. Rebased and catalogued the four cloud.oidcContinue.* keys, which
   oidc-continue-page.tsx uses and en.json lacked — the gate would have
   gone red on the merged tree. Values are each call site's own
   defaultValue verbatim, so no rendered English changes, and the
   {{envVar}} placeholder is preserved. Inserted textually to keep the
   catalog additive-only: `git diff --numstat` on en.json is 4/0.

3. Dropped the two stale uncatalogued entries
   (settings.cloudOverview.localModeLabel / localModeDescription) that
   this PR's own refactor obsoleted. With the path fixed the checker
   names them itself.

Verification: 14/14 in check-i18n.test.ts, up from a baseline of 11 pass
/ 1 fail — the fixed path also resolves the pre-existing failure. Two new
tests pin both halves of the contract, and the fail-closed one is
mutation-checked: restoring the silent-empty return fails it. Checker
against the rebased tree exits 0 with no en.json gaps and no uncatalogued
warnings; a bogus --allowlist exits 1 with the message. Biome clean.

The test fixture now always writes an allowlist, since under the new
contract a fixture that wants an empty one has to say so.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011gZy3feiGVKs61PDUzx1Cd

* fix(i18n): dedupe en.json keys reintroduced by the develop merge

22 keys existed twice with identical values: keys this branch restored
that develop later added independently (documentsview.Scope*,
triggerform.event.*, elizaosapps.phone.tab.dialer, desktop.tray.openEliza,
cloud.accountDetails.email) plus branch-internal duplicates
(cloud.account.securityLink*, cloud.admin.gate.signIn*,
cloud.privacyPanel.*Unavailable, cloud.redemptions.copyUserId,
cloud.security.*). Keep the first occurrence of each; biome
noDuplicateObjectKeys on the locales dir is clean and the catalog
checker passes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(agent): authenticate protected self-API callers

* fix(agent): resolve one self-API credential on both sides of the boundary

The caller normalized its credential (stripping a `Bearer ` prefix, falling
back to `ELIZA_API_AUTH_TOKEN`) while the server compared its own raw
configured value, so two documented configurations 401'd every protected
loopback caller: a `Bearer `-prefixed `ELIZA_API_TOKEN`, and a legacy-only
deployment the server never read.

`resolveSelfApiCredential` is now the single resolver. It owns canonical-over-
legacy precedence and Bearer normalization, and both the caller building
`Authorization` and the server resolving what it expects derive their bytes
from it, so the two cannot drift.

The integration test authorized with a local string compare against the token
it had just handed the client, which defined the server to agree with the
caller and could not observe drift in the code under test. It now runs the
production `isAuthorized` path, with the server's configured credential pinned
independently of the caller's so a mismatch is a real disagreement.

Reported by @0xSolace on elizaOS#17633; both reproductions confirmed against source.

* fix(agent): normalize the Bearer prefix at its source, not at each consumer

Takes the review's suggested shape for both blocking findings.

resolveApiSecurityConfig now strips an optional leading `Bearer ` from the
configured token, so every consumer agrees by construction. Previously
ELIZA_API_TOKEN="Bearer x" worked only because the server compared a raw
`Bearer x` against the client's `Authorization: Bearer x` header - moving the
strip to only some readers turned that working config into a 401 at
static-file-server.ts's injected dashboard token and at app-core's
resolveBoundaryRole. resolveSelfApiCredential shares the same helper, so caller
and server derive an identical credential.

getConfiguredApiToken goes back to resolveApiToken in both server-helpers-auth.ts
and auth-routes.ts. That helper is not self-API-only: it backs isAuthorized,
isWebSocketAuthorized, resolveWebSocketUpgradeRejection, pairingEnabled,
ensureApiTokenForBindHost and resolveTerminalRunRejection. Accepting the
plugin-level compat key ELIZA_API_AUTH_TOKEN there promoted it to the inbound
credential for every remote client, and under a legacy-only config it flipped
apiTokenEnabled true - 403-ing loopback terminal runs - while satisfying
ensureApiTokenForBindHost's early return so the elizaOS#12228 wildcard-bind hardening
never minted its 32-byte token. resolveSelfApiCredential stays what it was
introduced to be: the caller-side resolver for requests this process makes back
to its own API.

* fix(shared): align stripBearerPrefix with the null-based env resolver contract

develop's firstNonEmpty and ResolvedApiSecurityConfig.token use string | null,
so the undefined-based helper no longer typechecks at the token assignment.
Accept null | undefined and return string | null so resolveSelfApiCredential
returns the stripped credential directly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: lalalune <shawmakesmagic@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…e) (elizaOS#17890)

* feat(plugins): add plugin-zerollama (Ollama-compatible local inference)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(plugin-zerollama): finish provider integration and validation

---------

Co-authored-by: elizaOS Developer <developer@elizalabs.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Shaw <shawgotbags@gmail.com>
* fix(cloud): fence concurrent schema migrations

* fix(cloud): scope migration hash identity forward

* fix(cloud): resolve preflight migrations from module

* fix(cloud): validate mixed historical migration orders

* fix(cloud): harden migration activation safety

* fix(cloud): abort wedged migration preflights

* test(cloud): bound migration database cleanup

---------

Co-authored-by: Shaw <shawgotbags@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
… and steward TRADE orders (elizaOS#17775)

* fix(plugin-wallet): close GHSA-gh63 guard gap for gov votes and steward TRADE orders

The prompt-injection financial-write block fires only for transfer, swap, bridge, and pump_fun_buy. Two sibling financial writes skipped it: EVM governance votes/delegations routed through the wallet router (gov), and steward TRADE order submission, which dispatches through trade-action.ts rather than the router. Add gov and trade to the blocked subaction set and assert the guard in the trade action before order submission.

Signed-off-by: SashaMIT <sash@ela.city>
Co-authored-by: Cursor <cursoragent@cursor.com>

* style: wrap the injection-blocked callback to satisfy Biome

The new catch emitted a single-line callback that exceeded the configured
line width, failing the lint lane on a formatter diagnostic. No behavior
change.

* fix(plugin-wallet): accurate block message, header, lint, error-policy tag

The injection-guard message named only transfer/swap/bridge while the
guard now also blocks governance votes and steward TRADE orders; state
the full set. File header updated to match. Biome line-width fix at
trade-action.ts:614 and // error-policy:J1 on the new catch.

Signed-off-by: Sasha Mitchell <sash@ela.city>

---------

Signed-off-by: SashaMIT <sash@ela.city>
Signed-off-by: Sasha Mitchell <sash@ela.city>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: lalalune <shawmakesmagic@gmail.com>
…S#17750)

assertMacArtifactStagerLooksCorrect transcribed the staple-failure block as
independent substrings checked with script.includes in isolation. Three of them
are bare shell punctuation that recur throughout the file (exit 1 nine times,
fi fifty-eight), so the gate proved only that each line existed somewhere, in
any order, at any distance. Deleting the exit 1 that fails a require-staple
release left the gate green while the release shipped an unstapled DMG.

Assert that block as one contiguous ordered run instead, comparing trimmed
lines so reformatting does not turn the gate red while order and adjacency —
which carry the contract — are pinned.

Closes elizaOS#17680
@sharkwon
sharkwon force-pushed the fix/clear-cloud-pair-token-on-explicit-disconnect branch from deb1d1b to 95f525e Compare August 7, 2026 13:39
sharkwon and others added 8 commits August 7, 2026 06:41
…refactor (elizaOS#17572)

* feat: interactive Cloud login with type‑level contract (fix elizaOS#17129)

- Added handleInteractiveCloudLogin() entry point that pre‑opens the named
  popup window, preventing forgotten login popups.
- Updated AppContext, useCloudState, and 5 component call‑sites to use the new
  method.
- Refactored related test files to use the new method name.
- Bumped watchdog PR link and added PR footer attribution.

* fix(ui): restore interactive Cloud login tests broken by elizaOS#17129 refactor

- Re-add steward_session_token removal in the client-auth test: the stored
  bearer short-circuits login (the agents list is the connectivity probe),
  so the interactive entry point is only reachable when login is needed.
- Adapt the popup-close test to the elizaOS#17129 contract: the interactive entry
  point pre-opens the named window and closes it once auth lands.
- Drop dead preOpenWindow/closePreOpenedAuthWindow plumbing from
  startCloudProvisionFlow — popup lifecycle now lives entirely in the
  store's handleInteractiveCloudLogin.

Verified: 58/58 conductor tests, 346/346 first-run suite, 19/19
ChatOverlay.firstrun tests, biome clean.

* chore: drop unrelated action-search keywords regeneration from elizaOS#17129 branch

The interactive Cloud login contract change does not touch the action-search
keyword catalog; the 53k-line regeneration (reformat + reorder + stale entries)
was an unintended artifact of running the keywords generator. Revert the
generated file to the develop state so the PR diff stays scoped to the
Cloud login call-site contract and its tests.

* fix(ui): make the Cloud login recovery path separately named and type-scoped (elizaOS#17129)

- Rename the raw null-window entry point to handleCloudLoginRecovery:
  non-interactive by construction, takes no window, so a missed interactive
  caller cannot compile against it. handleInteractiveCloudLogin remains the
  only interactive entry point (pre-opens the named popup itself).
- Remove handleCloudLogin from the public AppActions surface (types.ts) and
  from ConfigPageView / ElizaCloudDashboard selectors; recovery is reachable
  only through the separately named handleCloudLoginRecovery wrapper.
- Add rationale for the boot-recovery conductor's then(releaseOverride)
  continuation and route App.tsx native re-auth through the recovery API.
- Add cloud-login-callsite-contract.test.ts: scans the real interactive
  surfaces (ConfigPageView, ElizaCloudDashboard, first-run-finish,
  CloudOverviewSection, CloudConnectorsUpsell) and fails if any of them
  invokes the raw path or the recovery API; recovery calls are allowed only
  at App.tsx and use-boot-recovery-conductor.ts. Mutation-proven.
- Update component tests to assert the new contract: interactive surfaces
  call handleInteractiveCloudLogin and do not pre-open the window
  themselves (pre-opening is the entry point's job, elizaOS#17129).

* test(ui): remove globals: true from vitest config

The globals: true setting was added without demonstrated need. The contract test
and all other tests explicitly import from vitest. Removing this unrelated
package-wide configuration change per review feedback (0xSolace).

* test(ui): make cloud-login-callsite-contract exhaustive over all sources

The previous contract pinned two fixed allowlists (INTERACTIVE_SURFACES and
RECOVERY_SITES) — a new production file elsewhere under packages/ui/src
could call handleCloudLoginRecovery() and the test stays green.

This revision walks the whole tree at test time, excludes
tests/specs/generated/stories plus the definition file (useCloudState.ts),
and asserts:
  1. No source invokes the raw null-window path.
  2. Sanctioned interactive surfaces use handleInteractiveCloudLogin.
  3. handleCloudLoginRecovery is only called at the two sanctioned sites
     (App.tsx, use-boot-recovery-conductor.ts) — a future file elsewhere
     in the tree that reaches the recovery API fails this assertion.
  4. types.ts does not expose the raw handleCloudLogin.
  5. types.ts does expose handleInteractiveCloudLogin.

164/164 focused tests pass (contract + first-run + connectors + settings +
App navigate-view); typecheck clean.

AI provider/model: deepseek/deepseek-v4-pro
Client / agent tooling: Hermes
Contribution skill revision: elizaOS/eliza@e379248:packages/skills/skills/contribute-to-eliza
Attribution status: self-reported
— [sharkwon]

* test(ui): add rendered popup evidence spec for interactive Cloud login (elizaOS#17129)

Proves the rendered Settings → Cloud connect CTA drives the interactive
entry point: clicking it opens a real popup window named eliza-cloud-auth
(window.open via preOpenCloudLoginWindow) which navigates to the
elizacloud.ai device-login URL, while the main page stays on /settings
(no same-tab fallback — the elizaOS#17064 defect class is unrepresentable).

Captures before/after desktop (1280x800) + mobile (390x844) screenshots,
a popup device-login capture, raw console/network logs, and an MP4
walkthrough (context closed to flush the recording before transcoding).
Cloud endpoints stubbed at the network boundary (established ui-smoke
pattern); renderer, store, and call-site wiring are the real modules.

164/164 focused tests + this spec (2/2) pass at head d357fa5.

AI provider/model: deepseek / deepseek-v4-pro
Client / agent tooling: Hermes Agent
Contribution skill revision: elizaOS/eliza@e379248:packages/skills/skills/contribute-to-eliza
Attribution status: self-reported
— [sharkwon]

* fix(ui): claim the Cloud-login popup synchronously inside the click gesture (elizaOS#17064)

The interactive login entry point pre-opened its popup via window.open
only AFTER the network awaits that precede it (first-run provisioning
awaits listAgents/getCloudStatusIfSupported; the Settings/dashboard CTAs
were the exception). Transient user activation does not survive those
round-trips, so the browser blocks the popup and the flow falls back to
same-tab /login — re-opening the exact elizaOS#17064 defect this PR closes,
reached through a delayed open instead of a missing one (krutktfw,
wtfsayo).

Fix: interactive surfaces now call claimCloudLoginWindow() synchronously
IN the click handler while activation is live; the entry point consumes
that stashed handle (takeClaimedCloudLoginWindow()). The type-level
contract is unchanged — handleCloudLoginRecovery stays the only
null-window route, and handleInteractiveCloudLogin still won't compile
against a raw window argument.

Call sites updated (Settings Cloud CTA, dashboard, ConfigPageView,
connectors upsell, first-run conductor flow launchers). Contract test now
(a) matches identifiers not just calls (alias `const x =
s.handleCloudLogin` caught), (b) requires EACH sanctioned recovery site
individually, (c) documents UI package-root path naming. Docs that named
the raw handleCloudLogin reworded.

190/190 focused tests, typecheck + biome clean.

AI provider/model: deepseek / deepseek-v4-pro
Client / agent tooling: Hermes Agent
Contribution skill revision: elizaOS/eliza@e379248:packages/skills/skills/contribute-to-eliza
Attribution status: self-reported
— [sharkwon]

* chore: trigger CI re-run

* chore: trigger CI re-run

* chore: trigger CI on fork with Actions enabled

* chore: trigger develop-pr workflow on fork

* fix(cloud-login): remove preOpenCloudLoginWindow fallback in handleInteractiveCloudLogin

The fallback to preOpenCloudLoginWindow() ran after awaits in
listOrAutoProvisionCloudAgent / runFirstRunFinish, when transient user
activation had lapsed. This caused the popup to be blocked and falling
back to same-tab navigation — the exact elizaOS#17064 regression this PR was
meant to close.

All interactive callers (ConfigPageView, ElizaCloudDashboard,
CloudOverviewSection, CloudConnectorsUpsell, use-first-run-conductor)
already call claimCloudLoginWindow() synchronously in their click handlers.
The entry point now strictly consumes the stashed handle with no fallback.

* fix(ui): release never-consumed gesture-claimed cloud login popups

Flows that claim the popup inside the user gesture but never reach
interactive login (local-runtime finish, already-authenticated cloud
provision) left the about:blank window open forever. The flow launchers
now release the stash in their finally, and a re-claim closes any prior
unconsumed handle before overwriting it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Ubuntu <ubuntu@localhost.localdomain>
Co-authored-by: sharkwon <sharkwon@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…nsfers (elizaOS#17773)

* fix(plugin-wallet): apply recipient-authorization guard to Solana transfers

The GHSA-7qxr-x6cg-r9cc recipient guard in runWalletRouter only fired when params.recipient matched /^0x[0-9a-fA-F]{40}$/, so a Solana base58 recipient skipped assertEvmTransferRecipientAuthorized entirely. A transfer recipient inferred from prior session context, token metadata, or embedded addresses — the exact class the EVM guard closed — was unchallenged on the Solana path.

Add messageAuthorizesSolanaRecipient/assertSolanaTransferRecipientAuthorized (case-sensitive base58 presence in the current user message or structured action parameters, mirroring the EVM logic) and dispatch on address form in the router gate. Structured action parameters remain authoritative, so programmatic callers are unaffected.

Signed-off-by: SashaMIT <sash@ela.city>
Co-authored-by: Cursor <cursoragent@cursor.com>

* test(plugin-wallet): pin Solana recipient case-sensitivity

A message containing the recipient with mangled case must not authorize the transfer; base58 is case-sensitive, so the comparison deliberately does not lowercase like the EVM path.

Signed-off-by: SashaMIT <sash@ela.city>
Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(plugin-wallet): sanitize Solana addresses in display labels, fix lint

sanitizeWalletDisplayLabel stripped only EVM addresses, so a poisoned
token label carrying an embedded base58 address was rendered verbatim;
wire the exported SOLANA_ADDRESS_PATTERN in. Also: biome line-width fix
at wallet-context-safety.ts:86, and the file header now states the
Solana guard and dual-family label sanitization.

Signed-off-by: Sasha Mitchell <sash@ela.city>

---------

Signed-off-by: SashaMIT <sash@ela.city>
Signed-off-by: Sasha Mitchell <sash@ela.city>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…, visible purge failures

- CloudPairRelay: when no owning agent resolves, install the exchanged
  bearer for the live session only (in-memory installs, no storage write)
  and show a distinct 'signed in for this session only' state with an
  explicit continue — never a plain success that drops the spent one-time
  token's credential.
- clearCloudPairApiToken: the agent-scoped branch no longer removes the
  legacy global key — on a pre-migration install that key may hold a
  different agent's only bearer; only the global (no-agentId) clear
  purges it.
- cloud-pair-token: storage-enumeration failures during the global purge
  are tagged error-policy:J6 and logged at warn so a vacated purge is
  visible.
- clearStalePairCredentialsForAgent: JSDoc now matches the unconditional
  per-agent clear instead of the retired active-server guard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…#17929)

* resolve rebase conflicts: update Dockerfile + test for gateway-discord

* style(cloud): wrap long expect lines in dockerfile guard test

Biome format fix for the gateway guard-test loop; no behavior change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Shaw <shaw.nicola.walters@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…onnect

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…bal clear, session-only fallback capture

- Phase 3 now proves the agent-scoped clear leaves the legacy global key
  (unknown owner) and the other agent's key untouched; new phase 4 proves
  the global clear purges every scoped key plus the legacy key.
- New browser-real test renders the actual CloudPairRelay (real modules
  through the Vite graph, real styles) on a non-dedicated origin and
  captures the pairing before-state and the new session-only fallback
  state, asserting the in-memory install, empty storages, and the
  explicit continue instead of a silent success redirect.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
# Conflicts:
#	packages/app/src/main.tsx
#	packages/app/test/cloud-pair-session-token.test.ts
#	packages/ui/src/state/useCloudState.backend-sign-out.test.tsx
#	packages/ui/src/state/useCloudState.ts
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.