fix: builder plugin deep-review — auth, backend, adapter, install-script hardening - #97
Open
iap wants to merge 16 commits into
Open
fix: builder plugin deep-review — auth, backend, adapter, install-script hardening#97iap wants to merge 16 commits into
iap wants to merge 16 commits into
Conversation
…rship - auth/sso_oidc: treat clientSecretExpiresAt==0 as never-expires (unblock silent refresh); guard token save against logout stop; preserve prior refresh_token; atomic mkstemp secret writes - adapter: emit finish_reason 'stop' on chat-only completion - __init__: guard adapter-port parse, wrap provider registration, set _registered right after tool registration - _provider: urlsplit-based ownership detection (no port-prefix false positive); guard int(env) - ci: verify job checks out hermes-agent + PYTHONPATH so the provider check actually runs - delete dead _dashboard.py; align SSE-shape and api_mode test expectations
H1: setup.sh and uninstall.sh rewrote config.yaml via yaml.safe_load +
safe_dump, silently stripping every comment and formatting on each run.
- setup.sh: replace the 'already installed' provider refresh and the
plugins.enabled step with line-based surgery that preserves comments and
unrelated keys verbatim.
- setup.sh: fix empty / {} providers handling (M14) - the old detection
crashed with 'aws-builder in None' on an empty providers: stub and
appended a duplicate providers: key for providers: {}.
- setup.sh: drop the redundant _QuotedDumper/_num_like code (L3).
- uninstall.sh: line-based removal + atomic write (temp + os.replace) +
restore the backup on any failure, and prune now-empty provider/toolset/
model containers like the old yaml path did.
Verified: all 5 heredoc bodies py_compile; 17 functional checks pass
(comment preservation, provider insert/remove, empty-stub pruning).
Replace the regex scan over a lossy UTF-8 decode of Q's binary application/vnd.amazon.eventstream response with a real frame reader. - _parse_eventstream_frames: parses [total_len][headers_len][prelude_crc] [headers][payload][message_crc] frames (big-endian), validating both CRC32s and header structure. - _parse_eventstream_headers: decodes header name/value pairs (string, bool, and fixed-size numeric value types). - _extract_from_frames: pulls assistant text from payloads carrying both content and modelId, plus conversationId / toolUseId / error __type. - _extract_answer_with_conversation_id: tries framing first, falls back to the existing lenient text path for bare JSON (offline tests). Adds 6 tests: CRC-valid frames, multi-frame concatenation, split-across- chunks, framed error event, conversation/tool id extraction, and CRC- corruption fallback. Full suite: 193 passed, 1 skipped.
The setup.sh/uninstall.sh Python heredocs print ✓/→/✗ glyphs, which crash with UnicodeEncodeError when stdout is a non-UTF-8 pipe (e.g. Windows cp1252 under redirect). Export PYTHONUTF8=1 so the heredocs always emit UTF-8 regardless of locale.
… M7) M2 (backend): wrap requests.post in try/except so Timeout/ConnectionError surface as a clear RuntimeError instead of a raw requests exception; cap the buffered response at 50 MB so a hung/malicious stream can't exhaust memory. M3 (backend): broaden the silent refresh-then-retry to any 400/401 (was gated on a literal 'invalid' substring that missed other auth-failure wording). M6 (adapter): validate Host and reject non-loopback Origin on the token proxy; refuse wildcard binds even under AWS_BUILD_ADAPTER_ALLOW_PUBLIC and warn on any non-loopback bind. M7 (adapter): cap the XML tool-call loop at 20 (was uncapped, only the JSON fallback was capped); strip bare/fenced JSON tool calls from assistant content (previously only XML was stripped, leaking raw JSON to the user).
The background poll thread and get_status() both called _poll_once() on the same flow without sharing slow-down state: the thread kept a local interval snapshot and never re-read the persisted flow, so a SlowDownException bump made by get_status() (same or another process) was ignored — doubling the request rate and causing repeated SlowDownException. - _poll_loop now re-reads the persisted interval each pass (monotonic max), honouring slow_down bumps from any poller. - get_status() skips its manual poll when a live background thread is already driving the flow, avoiding same-process double-polling.
…sh, status exit code Addresses low-severity but real behavioral issues: - backend._resolve_model_id: accept case-variants of catalog models and return the canonical lowercase form instead of silently downgrading to 'auto' (L10). - backend.list_models/load_tags: reload the plugin.yaml override when its mtime changes, making the 'editing plugin.yaml is picked up on the next call' docstring true again (L2). - auth/sso_oidc.start_login: only short-circuit when the token is actually valid (or refreshable), not merely present — an expired non-refreshable token now starts a fresh flow (L8). - auth/sso_oidc.refresh_token: stop retrying terminal InvalidGrantException 3x (dead refresh token); still retry transient ClientError/network errors (L8). - build_cli.cmd_status: exit non-zero (1) when not authenticated, so 'logged out' is scriptable (L6).
…er (L5, L9) - verify.py: replace the two-key snake_case denylist with a case-insensitive, recursive scan for secret-named JSON keys (catches accessToken/clientSecret/ refreshToken/password/etc., without false-positiveing on token_type or has_refresh_token) (L5). - adapter.py __main__: start() already runs serve_forever() in a daemon thread; the main thread now waits on an Event instead of launching a second redundant serve loop (L9).
uv deprecated [tool.uv] dev-dependencies in favor of [project.optional-dependencies] / dependency-groups. The block duplicated the 'dev' extra verbatim and triggered a deprecation warning on every uv run.
…d uninstall, lint - adapter.py: the M6 Host guard now respects AWS_BUILD_ADAPTER_ALLOW_PUBLIC=1, so a non-loopback Host is only rejected when the operator has NOT opted into public binding (the opt-in was previously unusable — every real request 403'd). Origin protection remains in both modes. - scripts/uninstall.sh: replace raw text/indentation matching with a path-scoped cleanup pass, so an unrelated user key/list that merely shares the name 'builder' is no longer removed. - tests: fix UP031 lint (percent-format -> f-strings) and add a regression test for the public-bind Host guard.
Greptile re-review: the ancestor-based - builder match was still too broad. It removed any such item below a plugins.enabled or toolset *descendant* (e.g. plugins.enabled.user_groups, platform_toolsets.cli.user_groups, known_plugin_toolsets.cli.user_groups) — user-owned lists that merely share the name. Require the exact list paths instead: plugins.enabled, or directly under a toolset sub-key (platform_toolsets.<sub> / known_plugin_toolsets.<sub>). Add tests/test_uninstall.py, which execs the script's own embedded helpers (not a mirror) and covers the happy path plus all scoping regressions, including the three nested-path cases from the review.
CodeQL flags verify.py's secret-leak check because it interpolates the leaked field *names* into the check message, which reaches a print sink (py/clear-text-logging-sensitive-data). The names are metadata, not secret values, so this is a false positive — but it fails the CodeQL check. Report the count of leaked field names instead; secret detection is unchanged.
Greptile re-review round 3: _prune_empty removed every empty container under a
managed top-level key, including user-owned ones (e.g. plugins.user_groups: [],
platform_toolsets.extra: {}). Track the exact container paths _cleanup emptied
(providers, plugins.enabled, platform_toolsets.<sub>, known_plugin_toolsets.<sub>,
model) and prune only those, cascading to their empty parents. Unrelated empty
containers are preserved.
Add a regression test for unrelated empty-container preservation.
Greptile round 4: _cleanup removed '- builder' from any direct child of platform_toolsets / known_plugin_toolsets, deleting user-owned custom toolset lists (e.g. platform_toolsets.extra). Only platform_toolsets.cli and known_plugin_toolsets.cli are installer-owned. Restrict the toolset list match to those exact paths; pruning already follows the emptied-path set so a custom sub-key is never touched. Add a regression test for custom toolset list preservation.
Greptile round 5: in the compact YAML form (cli: followed by - builder at the same column), the indent stack popped the cli ancestor before the sequence item was evaluated, so the builder toolset reference survived uninstall. Treat a block sequence item's content as starting after the '- ' indicator (column+2), so it stays nested under its same-column mapping key. Add a regression test for the compact form.
Comment on lines
+201
to
+202
| if _indent(nxt) <= ind: | ||
| break |
There was a problem hiding this comment.
Compact toolset sibling is re-parented
When a compact toolset list contains Builder and another entry, such as cli:\n - builder\n - ask_q, removing Builder causes _prune_empty to treat ask_q as outside cli. It removes the cli: mapping and leaves ask_q directly under platform_toolsets or known_plugin_toolsets, changing the surviving toolset's configuration path. Determine children using the mapping key's logical content indentation so compact sequence items remain under cli:.
Artifacts
Exact compact YAML uninstall test script source
- This captured source is the exact generated script that builds the temporary config, invokes the real uninstaller, and asserts the malformed resulting nesting; the takeaway is that the reproduction procedure is fully inspectable.
Compact YAML configuration before builder uninstall
- This setup-command capture shows `builder` and `ask_q` correctly nested under `cli:` in both affected mappings before uninstall; the takeaway is that the initial configuration has the intended parent-child structure.
Compact YAML configuration after builder uninstall
- This real-uninstall capture shows both `cli:` keys absent and each surviving `ask_q` directly under its enclosing toolsets mapping; the takeaway is that uninstall reproduces the claimed re-parenting bug.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Builder Plugin — Review & Fixes (PR Summary)
Deep review of the Hermes
builderplugin (Amazon Q / AWS Builder ID integration) foundand fixed 5 high, 11 medium, and 8 low severity issues across the SSO-OIDC device
flow, the chat backend (Amazon Q event-stream parsing), the HTTP adapter, and the
install/uninstall shell scripts.
Test status:
204 passed, 1 skipped(the single skip is the live OIDC test).Commits (9, on
mainoverffb4ba3)222022192760a4ad2711afc89bbfb21244e19ca324e9ae2af73286d0serve_forever(L5, L9)7b86d23[tool.uv] dev-dependencies(L4)High severity (H1–H5)
setup.sh/uninstall.shrewroteconfig.yamlvia YAMLround-trips, silently deleting user comments. Replaced with line-based, comment-preserving
block surgery.
_dashboard.py: removed a stray root-level file that shadowed the realdashboard/package and could break imports.handling hardened (see also M11/M12).
verify.pyso the install-time self-checkis exercised on every push.
Medium severity (M2–M14)
backend.pyHTTP call wrapped with a clear error + 50 MB stream cap."invalid"gate).application/vnd.amazon.eventstreamframereader (both CRC32s validated) with a lenient text fallback.
finish_reasonhandling corrected.Host, rejects non-loopbackOrigin, and refuses wildcard bindseven under the public opt-in.
<tool_call>loop capped at 20; bare/fenced JSON tool calls stripped from content._provider.pyurlsplitfixed.__init__.pyexports corrected.get_status()now share slow-down state (no moredoubled request rate / repeated
SlowDownException).setup.shnow handles emptyproviders: {}/[]stubs.Low severity (L2–L10)
list_models/load_tags) reloadsplugin.yamlon mtime change._QuotedDumper/_num_like(PyYAML already quotes these).[tool.uv] dev-dependencies.build_cli.py statusexits non-zero when logged out (scriptable).start_login()no longer falsely reportsalready_authenticatedfor an expiredtoken;
refresh_token()stops retrying terminalInvalidGrantException3×.adapter.py __main__no longer callsserve_forever()twice._resolve_model_idis case-insensitive (no more silent downgrade of"Claude-Sonnet-4.5"to"auto").Deferred (deliberately skipped)
core's
save_config(), not fix it).setup.shtemp-file cleanup (marginal).ty/shellcheckjobs (no runtime value).Push status
All 9 commits are local-only on
main. Pushing requires a fork or write access tohttps://github.com/iap/builder.git— not yet provided.Greptile Summary
This change improves Builder configuration cleanup and public adapter binding behavior. Focused executions confirmed that uninstall now preserves unrelated Builder-named entries, nested lists, unrelated empty plugin settings, and custom toolset lists, while still removing compact managed
clientries; the public-bind opt-in also accepts a non-loopback Host while continuing to reject non-loopback Origins. One issue remains: removing Builder from a compactclilist can move another surviving toolset entry out of itsclimapping.Confidence Score: 4/5
Not ready to merge until compact YAML toolset cleanup preserves the
cliparent for surviving entries.A verified non-security configuration corruption issue remains in the uninstall path.
Files Needing Attention: scripts/uninstall.sh
What T-Rex did
Comments Outside Diff (5)
General comment
- buildervalues inplugins.enabled.user_groups,platform_toolsets.cli.user_groups, andknown_plugin_toolsets.cli.user_groups. Running the actual uninstall script removed all three and pruned their containing branches. Control entries inplugins.disabledand an unrelated top-level list survived.scripts/uninstall.sh:114removes any builder item whose ancestor stack containsenabledbeneath top-levelplugins;scripts/uninstall.sh:118removes any builder item under either top-level toolset key. Neither condition requires the expected direct list path.plugins.enabledand the intended direct toolset lists), rather than matching arbitrary descendants by a shared ancestor.General comment
plugins.enabled: [- builder]and unrelatedplugins.user_groups: [], the real uninstall script removes both the owned Builder entries and the user-owned emptyuser_groupscontainer, leaving an empty config. The expecteduser_groups: []line is absent in the after capture._prune_emptytreats every nested line under top-levelplugins,providers,platform_toolsets,known_plugin_toolsets, ormodelas managed (scripts/uninstall.sh:153,168). It then removes any empty literal (scripts/uninstall.sh:172) even when that literal was pre-existing and unrelated to the Builder removal.plugins.enabled, direct toolset list paths, and removed provider/model containers). Do not prune arbitrary nested[]or{}values merely because their top-level ancestor is managed.General comment
platform_toolsets.extra: [- builder]andknown_plugin_toolsets.custom: [- builder]from a temporary user configuration.scripts/uninstall.sh:124-128treats every child toolset name as managed, and pruning removes the now-empty mappings.platform_toolsets.cliandknown_plugin_toolsets.cli.General comment
cli:and its- builderitem share indentation, uninstall reports that no entries were found and leaves the builder entries in both installer-owned toolset paths.scripts/uninstall.sh:88-89, the stack-pop condition uses>= ind. On the same-indented list item it pops thecliancestor beforepathis computed at line 96, so the path no longer matches either guarded toolset list at line 125.clistack entry before list-item path evaluation), while retaining correct stack handling for sibling mapping keys.General comment
, uninstall removescli:even thoughask_q` survives, placing it beneath the enclosing mapping.cliparent.Reviews (6): Last reviewed commit: "fix(builder): handle compact same-indent..." | Re-trigger Greptile