Release 0.1.5: stdout-encoding onboarding fix → production#155
Merged
Conversation
…153) Jamf's recurring check-in runs the MDM onboarding policy from a launchd context with no LANG/LC_* set, so the interpreter falls back to the ASCII stdout codec. The first diagnostic line setup prints — the migration banner "[migration] python->binary sweep" — contained a U+2192 arrow, which raised UnicodeEncodeError and aborted `setup` (exit 1 -> UNBOUND_INSTALL_FAILED step=setup) on every check-in. Interactive `sudo jamf policy` runs inherit a UTF-8 locale and passed, which masked it during canary. The output is purely diagnostic; a cosmetic write should never kill the install. - main(): reconfigure stdout/stderr to UTF-8 (errors='replace') before dispatching any subcommand, so no print can raise UnicodeEncodeError. Also hardens the `hook` path, whose stdout carries UTF-8 JSON. - setup_cmd: swap the banner arrow for ASCII '->' as defense in depth. - test: drive main(["setup", ...]) with ASCII-backed streams; reproduces the exact crash pre-fix, passes post-fix. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Lockstep bump of both binaries' __version__ (unbound-hook + discovery) so the release workflow's publish-safety assert passes when tagging runtime-v0.1.5. This is the release that carries the stdout-encoding fix (#153) to the fleet. No behavior change. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment on lines
+40
to
+43
| except (ValueError, OSError): | ||
| # Detached/closed stream — nothing we can do, and not worth | ||
| # crashing onboarding over. | ||
| pass |
There was a problem hiding this comment.
Silent
pass leaves stream reconfiguration failures unobservable. If stdout.reconfigure() fails but stderr is still functional (or vice versa), a failure silently leaves one stream on the ASCII codec, which could still produce a crash downstream if a non-ASCII character is printed.
Suggested change
| except (ValueError, OSError): | |
| # Detached/closed stream — nothing we can do, and not worth | |
| # crashing onboarding over. | |
| pass | |
| except (ValueError, OSError) as exc: | |
| # Detached/closed stream — best-effort: try to warn on the other | |
| # stream rather than silently leaving it on the ASCII codec. | |
| other = sys.stderr if stream is sys.stdout else sys.stdout | |
| try: | |
| other.write( | |
| f"[unbound-hook] warning: could not reconfigure stream to UTF-8: {exc}\n" | |
| ) | |
| except Exception: | |
| pass |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
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.
Release 0.1.5 → production
Promotes
stagingtomainfor theruntime-v0.1.5release. Pinnedrelease/0.1.5branch so the production delta is exactly these two commits, nothing in-flight:fix(unbound-hook): stdout-encoding crash.unbound-hook setupaborted on Jamf's recurring check-in because the launchd context has noLANG/LC_*, so the interpreter fell back to the ASCII stdout codec and the migration banner's→raisedUnicodeEncodeError.main()now reconfigures stdout/stderr to UTF-8 so a diagnostic print can never abort the install. Reproduced on a real Mac under the field environment; fixed, 16/16 setup tests pass.chore(release): bump both binaries'__version__0.1.4 → 0.1.5 (required or the release workflow's publish-safety assert fails).After merge
Push tag
runtime-v0.1.5onmainto cut the signed/notarized build → publishes tos3://unbound-release-artifacts/macos/0.1.5/and repoints thelatest/onboard.shthe fleet installs from.Customer impact
Clears the check-in install failure reported during the Salesloft fleet rollout. The interim env-wrapper workaround in use today can be dropped once 0.1.5 is live.
Greptile Summary
This release promotes the
stdout-encodingcrash fix to production:_force_utf8_io()is called at the top ofmain()to reconfigure stdout/stderr to UTF-8 witherrors='replace', preventingUnicodeEncodeErrorfrom aborting onboarding in Jamf's launchd context where noLANG/LC_*is set. The migration banner's non-ASCII arrow is also replaced with ASCII->as a belt-and-suspenders measure, and both binaries are version-bumped to 0.1.5._force_utf8_io()added tounbound_hook/main.py: guards all stdout/stderr output for the hook binary against locale-less launchd environments; a regression test verifies the fix end-to-end by monkeypatching an ASCII-only stream.setup_cmd.pybanner updated: the→character that triggered the original crash is replaced with->, removing the source of the problem independently of the stream reconfiguration.packaging/unbound_discovery_entry.pyreceives only a version bump; the same_force_utf8_io()guard is not applied despite this binary running in the identical LaunchDaemon/no-locale environment.Confidence Score: 3/5
The hook binary fix is correct and well-tested, but the discovery binary ships in the same launchd/no-locale environment without the same guard, leaving an unresolved crash path.
The core fix in unbound_hook/main.py is sound and the regression test exercises the exact field scenario. However, unbound_discovery_entry.py runs as a root LaunchDaemon under the identical locale-free environment and calls upstream library code that can print arbitrary Unicode — the same class of crash that this PR fixes in the hook binary remains reachable in the discovery binary.
packaging/unbound_discovery_entry.py — missing the stdout reconfiguration applied to the hook binary, despite running in the same environment.
Important Files Changed
Sequence Diagram
sequenceDiagram participant Jamf as Jamf LaunchDaemon (no LANG/LC_*) participant main as unbound-hook main() participant utf8 as _force_utf8_io() participant stdout as sys.stdout (ASCII) participant setup as setup_cmd.run() Jamf->>main: execv unbound-hook setup --api-key ... main->>utf8: call _force_utf8_io() utf8->>stdout: "stream.reconfigure(encoding=utf-8, errors=replace)" stdout-->>utf8: stream now UTF-8 utf8-->>main: return main->>setup: setup_cmd.run(args) setup->>stdout: "print([migration] python->binary sweep)" stdout-->>setup: write OK (non-ASCII safe) setup-->>main: return 0 main-->>Jamf: exit 0Comments Outside Diff (1)
packaging/unbound_discovery_entry.py, line 68-78 (link)unbound_discovery_entry.pyships as a separate binary that also runs from a root LaunchDaemon with noLANG/LC_*set — the exact same launchd context that produced the crash inunbound_hook. The upstream library calls (ai_tools_discovery.main(),scan_single_mcp_server.main()) can print arbitrary Unicode (tool names, file paths, server URIs), and if any non-ASCII character reaches stdout before this binary is shipped with the fix, it will raiseUnicodeEncodeErrorand abort discovery on every check-in, just as the hook binary did. The_force_utf8_io()function now exists in the same repo — calling it at the top ofmain()here would close the gap.Reviews (1): Last reviewed commit: "chore(release): bump runtime version 0.1..." | Re-trigger Greptile