Skip to content

Fix the app tools, the system mode, the chat screens and the build and browser tools (four review passes) - #48

Merged
oblomov-dev merged 4 commits into
mainfrom
claude/wizardly-ritchie-7sj2br
Oct 7, 2026
Merged

oblomov-dev merged 4 commits into
mainfrom
claude/wizardly-ritchie-7sj2br

Conversation

@oblomov-dev

@oblomov-dev oblomov-dev commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

Behaviour fixes from four review passes, covering:

  • the system mode
  • the app client and the app tool handlers
  • the two chat screens: MCP Apps and Adaptive Cards
  • the build and browser tools

Every fix has a regression test, and each one was reproduced before it was fixed.

System mode (lib/system.mjs)

  • Breaker race: a wrong password reaches the system once under parallel calls. Before, parallel calls each passed the breaker before the first 401 came back, and SAP locks a user after a few attempts.
  • Password command: the command runs once for parallel first calls.
  • Timeouts: system_status and app_list give up after 30 s.
  • Startup: a malformed % in A2UI5_MCP_SYSTEM_URL no longer stops the server from starting.
  • Credentials: a URL that does not parse no longer echoes its credentials.
  • Redirects: a 3xx answer names its Location.

App client (lib/appclient.mjs, vendored by the playground, protocol and the VS Code extension)

  • Queued acts: an act queued behind one in flight runs only if it still names what its caller saw. Before, two app_act a1 sent at once fired NEXT and then DELETE_ALL.
  • Eviction: sessions are evicted least recently used first, and an evicted session does not come back.
  • Event by name: without row, the screen action of that name wins over a row action.
  • Error texts: breaker refusals pass through unchanged, a non-JSON body is sanitised, and errors echo at most 80 characters.

App tools (server.mjs, lib/args.mjs, lib/budget.mjs)

  • Answer size: snapshot answers are fitted to ANSWER_BUDGET. Long values are cut and become read-only, then rows are dropped from the end, with a note saying what was cut.
  • Card size: the Adaptive Card counts together with the snapshot.
  • Arguments: an argument a tool does not declare is refused, and max_rows refuses booleans, arrays and blank strings.

Build and browser tools (lib/runtime.mjs, lib/npm-backend.mjs, lib/spawn.mjs)

  • interact_app timing: each action waits for the roundtrip it started. Before, the frontend dropped an event fired during a roundtrip, and both clicks were reported as performed.
  • Start during a build: a backend start while build_backend runs is refused. Before, it booted the old output, which the build then reported as built.
  • Stop during a start: backend stop now stops a start in flight, and the start reports that it was stopped.
  • Picture size: run_app and interact_app cut a page taller than 4096 px. screenshot_view leaves out pictures over 8000 px or past its image budget. Each call gets its own PNG file.
  • Lint:
    • The JSON for the whole sandbox is read in full. Before, one app with many findings broke every later lint.
    • The first 100 findings are listed, counted per rule.
  • Large answers:
    • read_app pages by lines (from_line).
    • A full run_unit_tests is counted per object.
    • migrate_report's files are named, not inlined, once they pass the budget.
  • Framework classes: the npm build refuses a dev object named like a framework class, whichever tool wrote it.

Adaptive Card and MCP Apps (lib/adaptive-card.mjs, lib/mcp-app-view.mjs)

  • Card rows: the card shows only the snapshot's rows, and hidden rows are no edits.
  • Untouched inputs: untouched means "as rendered", so CRLF, newlines, multi-select order, choices and check boxes are no longer written back.
  • Typing and focus survive a failed act.
  • Refresh follows a session that moved on and keeps max_rows.
  • Buttons that need a value only a browser computes are disabled.
  • Height: the reported height shrinks again.

Snapshot (lib/viewxml.mjs, lib/snapshot.mjs)

  • Character references: an out-of-range reference such as � no longer throws the snapshot.
  • stripTags is linear.

Not in this PR

  • Tool races, left as a follow-up:
    • a deploy during a build (the object list is read before the transpile, and read_app staleness is judged against it);
    • cancelling a build that other callers joined;
    • deploy_app erroring after it wrote the class when the lint config is missing;
    • remove_app leaving interfaces and tables behind;
    • add_agent_setup with a dangling .claude link.
  • Password inputs: they are still shown in clear text in MCP Apps. Fixing this is a snapshot contract change.
  • Re-vendoring: the playground and protocol re-vendor appclient/snapshot/viewxml, and this repo re-vendors the protocol renderer (Adaptive Cards: app text is never Markdown; linear htmlToText protocol#4, merged). Both come as follow-up PRs.

Checks

A2UI5_MCP_SKIP_NETWORK_TESTS=1 npm test (with Chromium): 439 passed, 16 skipped, 0 failed.

🤖 Generated with Claude Code

https://claude.ai/code/session_013nvd2W2zUT5ZyemG91n2oq

- system mode: requests go one at a time until the logon was accepted, so
  parallel calls send a wrong password once; the password command's answer
  is shared; system_status/app_list are bounded (30 s); a redirect names its
  target; a malformed % in the URL no longer stops the server; a URL that
  does not parse no longer echoes its credentials
- appclient: least-recently-used eviction, an evicted session is not
  brought back by a late act; without `row` a screen action wins over a row
  action of the same name; a breaker refusal is passed through; a non-JSON
  2xx body is shown sanitised
- adaptive card: tables cut to the snapshot's rows, unshown rows are no
  edits, a card over the answer budget is left out
- MCP Apps: untouched choices and check boxes send nothing; message box
  choices with parentheses
- snapshot: out-of-range character references, linear stripTags

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nvd2W2zUT5ZyemG91n2oq
oblomov-dev pushed a commit to abap2UI5-addons/abap-agent-runtime that referenced this pull request Oct 7, 2026
A "delete selected" button after a table with a DELETE per row was
reachable by its id only: without `row` the first action of the name won,
and that was the row action, which then asked for a row. Same rule as
mcp-server's findAction (abap2UI5/mcp-server#48).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nvd2W2zUT5ZyemG91n2oq
claude added 2 commits October 7, 2026 20:24
- an act queued behind one in flight is resolved against the screen its
  caller saw and refused, sending nothing, when its action or fields name
  something else on the screen that answered (NEXT then DELETE_ALL)
- a backend start during build_backend is refused: it booted the output
  from before the build, which the build then reported as built
- snapshot answers are fitted to ANSWER_BUDGET (long values cut and made
  read-only, rows dropped from the end, a note beside); the adaptive card
  counts together with the snapshot
- an argument a tool does not declare is refused; max_rows refuses
  booleans, arrays and blanks; errors echo at most 80 characters

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nvd2W2zUT5ZyemG91n2oq
…sessions

- untouched inputs are compared with what was rendered, not with the
  snapshot: CRLF in a textarea, a newline in a text input, a multi-select
  in another order or with a key without an option were written back on
  every press; a textarea keeps a leading newline
- what the user typed and the focus survive a render of the same screen
  (while an act runs, after a failed one)
- Refresh follows a session that moved on and reads its current screen; an
  act refused for it says Refresh shows it
- a button whose arguments only a browser computes is disabled
- the size report measures the content (it never shrank) and starts after
  ui/notifications/initialized; Refresh keeps max_rows

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nvd2W2zUT5ZyemG91n2oq
@oblomov-dev oblomov-dev changed the title Fix the system mode breaker race, session eviction and the chat screens Fix the app tools, the system mode and the chat screens (three review passes) Oct 7, 2026
oblomov-dev pushed a commit to abap2UI5-addons/abap-agent-runtime that referenced this pull request Oct 7, 2026
… values

Parity with abap2UI5/mcp-server#48:
- an argument a tool does not take is refused (app_act { value, event }
  fired the event without the edits)
- snapshot answers are fitted to 60,000 characters (fit_snapshot: long
  values cut and read-only, rows dropped from the end, a note beside)
- errors echo at most 80 characters of an argument (viewxml=>echo)
- an act's max_rows is stored with the snapshot it built; app_describe
  answered the stored one for any max_rows asked
- @CLOSE_POPUP / @CLOSE_POPOVER with a row or args is refused
- the copilot's apply_pending refuses a value typed into a dialog instead
  of writing the stale main-model value over the user's

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nvd2W2zUT5ZyemG91n2oq
…res and answers

- interact_app waits for the roundtrip each action started before the next
  (the frontend dropped an event fired during one; both were "performed")
- backend stop kills a start in flight and the start says it was stopped
- run_app/interact_app cut a page taller than 4096 px; screenshot_view
  leaves out pictures over 8000 px or past its image budget; one PNG file
  per call
- the lint reads the sandbox's whole JSON (one app's many findings broke
  every later lint) and lists the first 100 findings; read_app pages by
  lines (from_line); a full run_unit_tests is counted per object;
  migrate_report's files are named, not inlined, past the budget
- the npm build refuses a dev object named like a framework class
- errors echo at most 80 characters (classNameOf, oneOf, boundedInt,
  parseActions, parseSizes, scaffold_app, migrate_report)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nvd2W2zUT5ZyemG91n2oq
oblomov-dev added a commit to abap2UI5-addons/abap-agent-runtime that referenced this pull request Oct 7, 2026
…ass (#4)

* LLM client, session, generative UI and copilot: fixes from a review pass

Language model (z2ui5_cl_agent_llm*, anthropic provider):
- effort and the server-side fallback are sent only to models that take
  them: Claude Haiku 4.5 and Sonnet 4.5 answered every call with a 400.
- The system prompt goes as a cached block (cache_control ephemeral) - the
  generative UI's vocabulary is the same on every call, twice per view.
- Only end_turn and stop_sequence are an answer, as the interface says: a
  pause_turn or a provider's guardrail reason no longer passes as one.
- The audit keeps the answer first and every prompt cut before escaping,
  so the 2000 characters never lose the answer and stay JSON; cache token
  counts are audited.

Session, settings, MCP:
- app_act masks the audited values against the screen they were typed
  into: masked after the act, a value sent with an event that opened a
  popup or another app went into the log as it was (an IBAN, a password).
  A key the screen does not know is also checked as the path /<key>.
- An APP deny rule applies to every event (get_policy), not only at
  app_start and app_attach - an app reached by navigation, or denied after
  the session started, was operable.
- app_attach rolls back what the restore ran when the app is refused.
- An MCP session in use keeps its row (changed_at refreshed hourly) instead
  of expiring a day after initialize.

Generative UI and copilot:
- The copilot validates, checks and fires the action by its id: two
  buttons with one event and different arguments showed "Low" and ran
  "High".
- A literal other than a text takes no line break and no brace: ^ and $
  match per line, and "10rem" + newline + "{/PATH}" passed as a size.
- icon, src and href are icon URIs whatever the profile types them
  (sap.m.SegmentedButtonItem's icon took any URL).
- Event arguments go in the app's order, left out only from the end - the
  app reads them by position.
- Sample rows (genui_samples) carry only the listed fields, sensitive
  ones masked.

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

* Copilot: a sensitive field read by a composite cell binding is masked

A cell bound to several fields or an expression ("{IBAN} ({BANK})") is a
column COL<n> in the snapshot, and check_sensitive( ) on that name never
matched - the IBAN went to the model in clear. The column is now judged by
the fields its binding reads.

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

* Concurrent acts, unknown MCP sessions, sensitive texts, NUL in JSON

- app_act claims the session before it fires an event (MCP mode): two
  calls on one session both ran main( ) and fired the event - a document
  posted twice, two forks of the session. The claim moves changed_at
  under the old value and is committed at once (abap2UI5 rolls back around
  main( )); the second call is refused before anything runs. Not in the
  copilot, which runs inside an abap2UI5 roundtrip.
- A request on an unknown or ended Mcp-Session-Id is a 404 (Streamable
  HTTP), so the client starts a new session instead of losing its name in
  the audit log; an initialize carries no session to check.
- The snapshot keeps which model paths each text of /texts was read from,
  beside the JSON (the contract is unchanged); the copilot masks a text
  that shows a sensitive field ("IBAN: DE89...").
- json_string( ) escapes NUL as \u0000, as JSON.stringify does - built on
  its own, so a platform that cannot build the character leaves the rest
  of the escaping as it was.

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

* A claim that holds while the act runs; admin and audit app fixes

Session (follow-up to the claim of the last commit):
- claim( ) puts changed_at ahead (the claim time plus 600 s) instead of to
  the present: a call that loaded the row after the claim saw the claimed
  value as current, claimed it again and fired the event a second time
  while the first one's main( ) still ran. A row ahead of now is taken;
  save( ) writes the present back and releases it, a refused or failed act
  gives the old value back (release( )).
- Every act that saves is claimed in MCP mode, values only and a closed
  layer included: saved over a session another act had moved on from, it
  brought the old one back.

Admin app:
- Save writes only the language model settings the form changed since it
  was loaded: written from a stale form, a privacy switch another
  administrator turned off was back on.
- A new URL, destination or provider without a key typed anew removes the
  stored key: the write-only key was sent to whatever host an administrator
  named, and Test call showed the answer.
- The last ADMIN rule cannot be deleted - without one nobody may change
  the settings again.

Audit app:
- The outcome filter runs in the database, and a search looks through the
  newest 5000 entries rather than 500; the title says when the window was
  full or the list cut, instead of a "0 call(s)" that was not true.

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

* Follow-ups from a review of this PR's own changes

- Copilot: a COL<n> cell is judged by every path its binding reads - an
  expression's references and absolute paths too - and by the row path
  (/T_PARTNER/*/IBAN) as well as the table path; the snapshot's
  binding_paths( ) is public for it.
- Admin app: a form opened on a draft from before mt_llm_shown removes no
  key on Save; the last-admin guard counts the stored ADMIN rows, not the
  form's stale list.
- MCP: the changed_at refresh is committed at once - an app start's
  roundtrip rolled it back.
- README: the request body (cached system block, effort and fallbacks per
  model), the stop-reason allow list, the audit's 400-character cut, the
  copilot's masking of composite cells and texts.

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

* app_act by event name: a screen action before a row action without row

A "delete selected" button after a table with a DELETE per row was
reachable by its id only: without `row` the first action of the name won,
and that was the row action, which then asked for a row. Same rule as
mcp-server's findAction (abap2UI5/mcp-server#48).

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

* MCP: unknown arguments, answer size, echoes, max_rows, copilot dialog values

Parity with abap2UI5/mcp-server#48:
- an argument a tool does not take is refused (app_act { value, event }
  fired the event without the edits)
- snapshot answers are fitted to 60,000 characters (fit_snapshot: long
  values cut and read-only, rows dropped from the end, a note beside)
- errors echo at most 80 characters of an argument (viewxml=>echo)
- an act's max_rows is stored with the snapshot it built; app_describe
  answered the stored one for any max_rows asked
- @CLOSE_POPUP / @CLOSE_POPOVER with a row or args is refused
- the copilot's apply_pending refuses a value typed into a dialog instead
  of writing the stale main-model value over the user's

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
@oblomov-dev oblomov-dev changed the title Fix the app tools, the system mode and the chat screens (three review passes) Fix the app tools, the system mode, the chat screens and the build and browser tools (four review passes) Oct 7, 2026
@oblomov-dev
oblomov-dev merged commit 3b695bf into main Oct 7, 2026
3 checks passed
@oblomov-dev
oblomov-dev deleted the claude/wizardly-ritchie-7sj2br branch October 7, 2026 22:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants