Skip to content

Align Jarvis app metadata and installation with repository move - #924

Merged
kavin-114 merged 44 commits into
mainfrom
develop
Aug 20, 2026
Merged

Align Jarvis app metadata and installation with repository move#924
kavin-114 merged 44 commits into
mainfrom
develop

Conversation

@Vigneshsekar

Copy link
Copy Markdown
Member

No description provided.

Vigneshsekar and others added 5 commits August 14, 2026 18:36
Running an agent that is ENABLED on the bench but not yet pushed to the
container produced a raw fleet 502 surfaced as a 500 to the SPA:
"invalid_spec: agent_id 'agent-<slug>' is not an installed delegate on
<container>". Enabling only flags the catalog dirty; the skill reaches the
container on APPLY ("Apply catalog changes", reviewer/admin-gated). So the
operator simply skipped (or has a pending) Apply — but the error read like a
crash.

_launch_audit now detects the fleet-agent's "not an installed delegate" failure
and translates it into an actionable, user-facing message ("This agent is not
loaded on your container yet. Open the Agents page, click 'Apply catalog
changes' to push it, then run it again."), stamped on the failed Run and
re-raised as a clean frappe error instead of the raw AdminUnreachableError. The
Run is still marked terminal failed (never left stuck "running"); only the
surfaced exception changes.

Regression test: an enabled-but-unapplied run_agent_now raises the apply hint
and leaves the Run failed with that message.
chore: align Jarvis install and CI with repository move
kavin-114 and others added 24 commits August 19, 2026 17:12
… bubble

New users now land on the same greeting-and-starter-cards welcome screen
as everyone else. The one-time plain-text intro bubble
(WelcomeAssistantMessage) and its entire home_intro backend
(mark_home_intro_seen / record_home_intro_event endpoints, version
fields on Jarvis Settings and Jarvis User Settings, backfill patches)
are removed.

Claude-Session: https://claude.ai/code/session_01WZJ3nhp1CqHuWzhGibypRa
jarvis/tools/_export/document/sanitizer.py imports nh3, but the package was
never declared - it works on Frappe v16 only because frappe itself ships nh3
there. On a Frappe v15 bench the import fails and takes the whole tool registry
(and the test suite) down with ModuleNotFoundError at collection time.

Found by the first version-15 CI run (PR #927).

Claude-Session: https://claude.ai/code/session_01Ui9kZy3ZCgzkwz2jRb5uci
… before 0.2.5

Mirrors frappe v16's own nh3 pin so the ranges are identical on both majors.
Review finding on PR #928.

Claude-Session: https://claude.ai/code/session_01Ui9kZy3ZCgzkwz2jRb5uci
fix: declare nh3 as a direct dependency
Frappe 15's only_for() returns early when flags.in_test is set, so all 11
denial tests that route through it never checked roles on a v15 bench and
failed their assertRaises. Frappe 16 removed the bypass, which is why they
pass there. Production requests never set the flag, so the guards were
always enforced on real sites on both majors - this is a test-view gap
only, no security hole.

Adds jarvis/tests/_role_guard.py with enforced_role_guards(), which probes
only_for's source for the bypass (compat doctrine: capability, not version
string) and clears flags.in_test around the denial call, then wraps the 11
affected assertions.

Batch 1 of the v15 gap burn-down (group 5 of the 163 failures measured by
the version-15 CI on PR #927).

Claude-Session: https://claude.ai/code/session_01Ui9kZy3ZCgzkwz2jRb5uci
…mplate

fix: show the welcome template for first-time users by default
Batch 2 of the v15 gap burn-down (groups 2, 5, 7 of the 163 failures the
version-15 CI measured on PR #927), ~44 tests.

- frappe.in_test (6 tests): v16-only module attribute; v15 uses
  frappe.flags.in_test. Add compat.in_test() (keeps v16's check as the first
  operand, so no v16 behavior change) and route run_import.py and
  _prepared_reports.py through it. Three tests mock.patch("frappe.in_test")
  directly, which cannot work on v15 (no attribute to patch); repoint them at
  the jarvis.compat.in_test seam.
- DataImport.set_delimiters_flag (36 tests): v16-only method for the custom-CSV
  delimiter feature v15 lacks entirely. Guard it in compat.set_delimiters_flag()
  (skip on v15, where the default comma parse is already correct) and call that
  from _import_preview.py's two sites.
- wiki_graph locking-read spy (2 tests): NOT an app bug. wiki.py is unchanged;
  the test's rigid spy(dt, name, field) signature could not absorb v15's
  internal get_value calling convention. Make both spies signature-flexible and
  pass args through verbatim.

Verified: pre-commit green. v16 CI on this PR proves no regression; the
cherry-pick onto version-15 is what proves the v15 fix.

Claude-Session: https://claude.ai/code/session_01Ui9kZy3ZCgzkwz2jRb5uci
Addresses the three findings on PR #931:

- in_test() now branches on whether frappe exposes the module attribute rather
  than ORing it with frappe.flags.in_test. On Frappe 16 the attribute is
  canonical, so a stray flags.in_test can no longer widen the result and let the
  scheduler-paused guard in run_import/_prepared_reports run work inline in
  production. On 15 the flag stays authoritative. (PLAUSIBLE finding.)
- Add TestInTestAcrossMajors + TestSetDelimitersFlagAcrossMajors exercising the
  real helper bodies (including a simulated-v15 attribute-absent path and the
  no-widening assertion); existing callers only mocked them out.
- Factor the duplicated signature-flexible get_value spy into one
  _manual_links_lock_spy() helper, used by both locking-read tests.

Claude-Session: https://claude.ai/code/session_01Ui9kZy3ZCgzkwz2jRb5uci
test: enforce only_for denial assertions on Frappe 15
fix: three Frappe-15 compat gaps in import/report tools
Batch 3 of the v15 gap burn-down (groups 1 and 6 of the 163 failures the
version-15 CI measured on PR #927), ~60 tests. Test-infra only; no app code.

- pypika QueryBuilder.run (57 tests): Frappe 16 attaches .run to the base
  pypika.queries.QueryBuilder, but Frappe 15 attaches it to the dialect subclass
  (frappe.qb._BuilderClasss) and leaves the base without it, so the 19
  patch("pypika.queries.QueryBuilder.run") sites in test_query.py raised
  AttributeError on 15. App code is unaffected (it calls .run on a real builder
  instance, which has it on both). Add _patch_qb_run(), which targets whichever
  class actually carries .run, and route all 19 sites through it.
- orjson (3 tests): Frappe 16 serialises HTTP responses with orjson and depends
  on it; Frappe 15 uses stdlib json and does not ship orjson, so the tests'
  bare `import orjson` raised ModuleNotFoundError. Add dumps_like_response(),
  which serialises the way THIS Frappe serialises a response (orjson on 16,
  stdlib json on 15), and route the three round-trips through it. The
  dict-ness assertions still run on both majors.

Verified: pre-commit green. v16 CI here proves no regression; the cherry-pick
onto version-15 is the v15 proof.

Claude-Session: https://claude.ai/code/session_01Ui9kZy3ZCgzkwz2jRb5uci
…port

Addresses the #933 finding: branching on `try: import orjson` (a) silently
masked a broken orjson on v16, where production 500s on response.py's own
top-level import while these tests stayed green, and (b) would route a v15 run
through orjson if it were present transitively, when v15 production uses stdlib
json. Discriminate on frappe.utils.orjson_dumps -- the v16-only symbol that IS
the production response serializer -- mirroring _patch_qb_run's probe-real-state
approach.

Claude-Session: https://claude.ai/code/session_01Ui9kZy3ZCgzkwz2jRb5uci
test: fix pypika .run and orjson test-infra gaps on Frappe 15
Batch 4 of the v15 gap burn-down (group 3, ~28 tests). This is a real v15
PRODUCTION bug, not just a test artifact.

Root cause: on Frappe 15, RedisWrapper.get_value() on a MISS writes
frappe.local.cache[key] = None (the miss write-back) unless called with
expires=True, while set_value(key, val, expires_in_sec=N) deliberately SKIPS
frappe.local.cache for expiring keys (writes Redis only). So within one request
the sequence read(cold miss) -> set(TTL) -> read returns the poisoned None
forever, never consulting Redis again. Frappe 16 masks it because its set_value
unconditionally overwrites frappe.local.cache. Jarvis was written against 16, so
~15 read sites omit the expires=True that 15 requires; on a real v15 site this
silently double-computes schema/OAuth/telemetry caches and crashes where code
subscripts a cached dict it expects to be non-None.

Fix (Frappe 15's own documented get_value contract, inert on 16):
- Add expires=True to the 38 app-code get_value() reads of TTL keys. No
  generator=/shared=/user= sites exist, so no fallback is disabled.
- pump.py's two lease/gateway reads passed use_local_cache=False, a Frappe-16-only
  kwarg that TypeErrors on 15 (currently swallowed, so the cache was silently dead
  there). Route them through new compat.cache_get_fresh(), which probes for the
  kwarg and falls back to expires=True on 15 for the same always-fresh read.
- Add TestCacheGetFreshAcrossMajors, incl. the cold-miss -> set -> read shape.

Verified: pre-commit green. v16 CI here proves no regression (existing schema/
oauth/telemetry cache tests still pass); the cherry-pick onto version-15 is the
v15 proof.

Claude-Session: https://claude.ai/code/session_01Ui9kZy3ZCgzkwz2jRb5uci
…en fresh read

Addresses the three #939 findings:

- wiki.py cooldown_key: the survey missed this TTL key because it is written via
  raw cache.set(nx, ex), not set_value, so my grep skipped it. It poisons on v15
  exactly like its _NUDGE_OFF_KEY sibling two lines up, causing the wiki nudge
  nag-loop the code warns against. Add expires=True.
- Memoization regression: expires=True skips get_value's hit write-back, so the
  three re-read-hot sites (triggers/engine _triggers_map on the every-save
  wildcard hook, account gate verdict, list_filters schema) lost their
  per-request local-cache memoization and now hit Redis every dispatch. Add
  compat.cache_get_memoized() -- read with expires=True (no poison) then store a
  real hit back into local.cache by hand -- and route those three through it.
- cache_get_fresh v15 branch: stop relying on the "TTL keys are never in
  local.cache" assumption for the correctness-critical pump leases. Evict any
  local copy first, then read with expires=True, guaranteeing the live Redis
  read regardless of prior local-cache state.
- Add TestCacheGetMemoizedAcrossMajors (memoized hit + no-poison miss).

Claude-Session: https://claude.ai/code/session_01Ui9kZy3ZCgzkwz2jRb5uci
…I key)

Google discontinued consumer login-with-Google for Gemini on 2026-06-18 (the
gemini-cli OAuth client now returns UNSUPPORTED_CLIENT), so the Gemini chat
*subscription* cannot work at runtime on any agent version. Remove the option to
connect one; Gemini via API key is unaffected.

Frontend: drop "Google Gemini" from the direct-connect fallback list, the
onboarding upstream picker, and the upstream label maps (api-key Gemini rows and
PROVIDER_DEFAULTS kept).

Backend: remove the "Google Gemini" entry from the OAuth provider map, its
hardcoded client id/secret in hooks.py, the whole oauth/gemini_cli_secret.py
bundle-scan (and its bundled client-secret), the google-gemini-cli revoke
endpoint, the turn-handler CliBackend mapping, and the Gemini subscription seed
models. Drop the @google/gemini-cli npm dep + stale root lockfile.

Catalog: regenerate jarvis/_model_catalog.py from the updated admin PROVIDER_SEED
so the bundled fallback offers Gemini as api-key only (empty renderer_id /
auth_profile_id, supports_subscription False, api_key-tier models intact).

Tests updated to the new reality (Gemini is api-key-only, not an OAuth provider);
api-key + vision coverage kept. Pairs with jarvis_admin_v2 PROVIDER_SEED change +
patch v1_38_remove_gemini_subscription.

Claude-Session: https://claude.ai/code/session_01MWxxyRgfRQV8wqsfgvtwGg
- Reset stranded sites: new patch v2_15_reset_removed_gemini_subscription flips
  any Jarvis Settings still on a Gemini direct-subscription (llm_auth_mode
  oauth/subscription + llm_provider "Google Gemini") to api_key mode and clears
  the stale OAuth account fields, so chat dispatch does not mis-route after the
  "Google Gemini" -> "google-gemini-cli" mapping is gone. (No connected tenants
  exist today; this is defensive for stale test-site rows.)
- Fix user-facing copy that still invited a Gemini sign-in: the direct-connect
  card blurb and the pool editor "Chat subscription" description now name only
  ChatGPT.
- Refresh stale comments/docstrings that still referenced google-gemini-cli /
  "two entries" (DirectSubscriptionCard fallback comment, agent_provider_for and
  turn_handler docstrings).

Claude-Session: https://claude.ai/code/session_01MWxxyRgfRQV8wqsfgvtwGg
- test_oauth_api: the pool-signin-no-pool-scope case (switched to xAI Grok when
  Gemini was removed) asserted the raw scope "grok-cli:access", but the authorize
  URL encodes the colon -> assert "grok-cli%3Aaccess".
- test_pending_oauth_capture: apply ruff-format (was reformatted by the pinned
  hook; missed in the earlier per-file lint pass).

Claude-Session: https://claude.ai/code/session_01MWxxyRgfRQV8wqsfgvtwGg
fix: prevent Frappe 15 cache local-poisoning on TTL keys
…tion-ready design

Stabilise the single composed-document template so any LLM-generated PDF
(letter, memo, report, ...) looks polished and correctly aligned. Only the
composed-content rich path changes; ERP record exports keep the Frappe
Print Format.

- theme.py: refined type scale + vertical rhythm; p/ul/ol/blockquote/code/hr
  rules; tool-built title masthead + geometry-aware full cover (fixes the
  collapsed cover + raw-markdown title + edge-bleed); .num right-aligns the
  header too; lighter repeating thead; callout as a distinct card;
  section-divider is a rule with a new .page-break; sharper bar charts.
- furniture.py: resolve_brand (default Letter Head logo, else company
  name+address; fail-safe); tool-built masthead + running brand header;
  --header-spacing + reserved top margin; footer -> text zones so page
  numbers coexist with branding; page-size-aware watermark; sized logo.
- export_document.py: title/subtitle/meta/cover params; md numeric-column
  alignment promoted to .num pre-sanitize; brand + masthead wired in.
- graphics.py: css_bar title/caption + bounded per-row series color (additive).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Remove the Gemini chat-subscription option (Gemini stays via API key)
kavin-114 and others added 15 commits August 20, 2026 09:52
Batch 5a of the Frappe 15 support burn-down. Test-only; no app code
changes, so Frappe 16 behavior is identical.

- test_realtime_handlers: _FakeConf only implemented .get(), but the v15
  dispatch path reads real attributes off frappe.conf (_log_query ->
  conf.allow_tests, RedisWrapper.make_key -> conf.db_name) that v16 doesn't
  hit here. Delegate unknown attributes to the real conf (never None --
  db_name re-keys the whole cache namespace). Fixes 3 ERRORs.
- test_list_filters_smoke: frappe.whitelisted is a set on v16 but a list on
  v15; coerce to set before intersecting. Fixes 1 ERROR.
- test_platform_agents_api_hardening: the singles value_cache sub-dict is
  created lazily by the first get_single_value, so index via setdefault
  instead of assuming the doctype key exists. Fixes 1 ERROR.

Claude-Session: https://claude.ai/code/session_01Ui9kZy3ZCgzkwz2jRb5uci
…ed set

- test_realtime_handlers: document that the code under test reads conf only
  via .get() (mapping-backed, never through __getattr__), so the attribute
  delegation serves framework internals only and the handler's own reads stay
  isolated to each test's declared values.
- test_list_filters_smoke: hoist set(frappe.whitelisted) out of the per-member
  loop; the collection is stable for a scan, so build it once.

Claude-Session: https://claude.ai/code/session_01Ui9kZy3ZCgzkwz2jRb5uci
Two Criticals + the Important/Minor wave from the 11-lane review:

- Cover height now accounts for the running-header top-margin reservation
  (shared HEADER_RESERVE_MM), so a branded cover fills the page instead of
  spilling onto page 2 (the bug reintroduced for tenants with a logo).
- title/subtitle/meta/cover now activate the rich path, so a titled export
  always gets the masthead (title-only previously rendered no visible title).
- Chart title/caption forwarded from _splice_charts to css_bar (dead feature);
  _decide_cover matches page-break as a class token (multi-class); title/
  subtitle/meta length cap; page geometry validated up front so the HTML path
  rejects a bad page_size/margin like PDF; _series_class requires a real int.
- Brand-resolution FAILURE (vs legitimate absence) surfaces a degrade note +
  detailed log; the company name is kept when only the address errors; a Letter
  Head logo in the footer field is used as a fallback.
- Centered md columns promote to .text-center; h5 differentiated from body;
  lists share the prose measure cap; geometry str-coerced + margin-clamped.
- Coverage: the company/address resolver (was untested), _decide_cover AND
  corners, _promote_table_alignment branches, cover-height header wiring,
  title activation, chart forwarding, _text_only, and more.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
test(v15): batch 5a - fix 5 test-infra assumptions about Frappe 16 internals
Batch 5b of the Frappe 15 support burn-down. One product/security fix
(applies to v16 too); the rest are test-only, so v16 behavior is unchanged
and its CI gate is the proof. Diagnosed against a real frappe@version-15
checkout, not from memory.

PRODUCT (v15 and v16):
- _export/safety.py escape_formula: the formula-injection guard only checked
  the raw first char, so a payload with a leading space (e.g. " =HYPERLINK(...)",
  as produced by unwrapping HTML to text) smuggled a live formula past it. Now
  also checks the whitespace-stripped first char (OR, so genuine tab/CR-leading
  cells stay escaped). Verified bypassable on v16 today; v15 just surfaced it
  first via its older markdownify pin.

TEST-ONLY (v15 harness/fixture assumptions):
- 5 atomic-rollback tests: bad-Link fixture used assigned_by, which has a
  fetch_from sibling on ToDo; Frappe 15's get_invalid_links silently skips the
  invalid-link check for such fields. Point the bad Link at allocated_to (no
  sibling) so it is rejected on both majors.
- test_account datetime canary: accept both None (v15, no cast) and
  datetime(1,1,1) (v16 cast sentinel) for an empty Datetime single.
- test_admin_client, test_prewarm: route TTL-key reads through
  compat.cache_get_fresh; on v15 a bare get_value returns a locally-memoized
  value before it consults expires, so expires=True alone is not enough.
- test_pending_confirm mint: also patch cache.setex; v15 set_value routes TTL
  writes through setex, not set.
- test_seq_watermark_migration: also bust the v15 table-columns redis hash
  ("table_columns" field "tab<doctype>"), not just the v16 string key.
- test_review_api_rework followup: add the enforced_role_guards() wrapper its
  three sibling PLAIN tests already use (v15 only_for no-ops under in_test).

Claude-Session: https://claude.ai/code/session_01Ui9kZy3ZCgzkwz2jRb5uci
…onale

- test_export_seam: add TestFormulaEscape coverage for the widened
  escape_formula (leading-whitespace-then-trigger is neutralized, including the
  HTML-unwrap "  =HYPERLINK(...)  " shape; leading whitespace with no trigger
  stays untouched). The guard only runs on strings, so numeric cells are
  unaffected; this locks the intended string behavior against regression.
- Dedupe the allocated_to/get_invalid_links rationale: keep the full
  explanation canonical at test_bulk_tools._BAD_USER and point the two other
  sites at it instead of repeating it.

Claude-Session: https://claude.ai/code/session_01Ui9kZy3ZCgzkwz2jRb5uci
fix(v15): batch 5b - formula-injection hardening + 12 v15 test fixes
Uninstalling one installation could null the recurrence pointer
(last_seen_run) of a DIFFERENT installation's surviving finding on Frappe 15,
instead of detaching it to the finding's own first_seen_run. That corrupts the
other customer's audit history: get_findings' run drill-down INNER JOINs
last_seen_run, so the row vanishes from its owner, and Frappe link validation
then rejects that owner's next save.

Root cause: _detach_last_seen_run read first_seen_run as a get_all field. On
Frappe 15 a get_all projection of Jarvis Agent Finding omits that Link field
(confirmed via a CI probe: the raw DB row holds the value, but the get_all
dict comes back without the key), so row.first_seen_run was None and the
pointer was set to None. Frappe 16 returned the field, which is why this only
surfaced on v15. Read first_seen_run with a direct db.get_value, which returns
the true column value on both majors.

Closes the last Frappe 15 test gap (test_cross_install_recurrence_bump_is_
detached_not_deleted).

Claude-Session: https://claude.ai/code/session_01Ui9kZy3ZCgzkwz2jRb5uci
Address review on the Frappe 15 detach fix: instead of plucking names then
issuing one db.get_value per row, read name + first_seen_run in a single raw
db.sql SELECT. Raw SQL (not get_all) is still required because Frappe 15's
get_all omits the first_seen_run Link field from its projection; this keeps
that correctness fix while making it one round trip regardless of finding count.

Claude-Session: https://claude.ai/code/session_01Ui9kZy3ZCgzkwz2jRb5uci
fix: detach-last-seen-run nulled another install's finding on Frappe 15
…e one

A gated ERP write parks a confirmation card server-side and the browser is
told about it once, over a best-effort action:pending socket frame. If that
frame is dropped there is no replay, so the card never renders: no Confirm
button, and because a typed "go ahead" is resolved from the tokens of the
cards currently on screen (approval_tokens), the typed path silently falls
through to the model too. The only re-reads ran on the turn's terminal and
bailed on a single strict-read blip, so a lost card could stay lost for the
rest of a multi-step run - the "card/button not showing" and "go ahead does
nothing" reports.

Make the client pull server truth instead of trusting one push:
- Poll the parked-card list on a short interval while a turn is live (started
  on run:start and on any action:pending), with a couple of trailing reads
  after it settles, then self-stop when the chat goes idle.
- resyncPendingConfirmations now reconciles to the server's live set for the
  conversation (drops stale/consumed tokens, adds any the push missed) rather
  than only appending.
- A transient ok:false (the strict owner-index read) keeps the on-screen cards
  and retries next tick instead of wiping the backstop.

Confirming stays sequential server-side (one live card per conversation), so
the poll never surfaces more than the flow intends; it just stops losing the
one card the chain is waiting on. Holds up across a long chain of cards
(1000-record runs park one card per batch).

Frontend-only; SPA build green.

Claude-Session: https://claude.ai/code/session_01MkE6urYua85gxhwqNSmgfN
Restore multilingual voice transcription with Gemini Flash
fix(agents): actionable error when running an un-applied agent
fix(chat): pull parked confirmation cards so a dropped push can't lose one
@kavin-114
kavin-114 merged commit 65c0b8b into main Aug 20, 2026
17 checks passed
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.

5 participants