Skip to content

Release 0.1.5: stdout-encoding onboarding fix → production#155

Merged
vigneshsubbiah16 merged 2 commits into
mainfrom
release/0.1.5
Jun 15, 2026
Merged

Release 0.1.5: stdout-encoding onboarding fix → production#155
vigneshsubbiah16 merged 2 commits into
mainfrom
release/0.1.5

Conversation

@vigneshsubbiah16

@vigneshsubbiah16 vigneshsubbiah16 commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator

Release 0.1.5 → production

Promotes staging to main for the runtime-v0.1.5 release. Pinned release/0.1.5 branch so the production delta is exactly these two commits, nothing in-flight:

  • fix(unbound-hook): never let stdout encoding abort onboarding setup #153fix(unbound-hook): stdout-encoding crash. unbound-hook setup aborted on Jamf's recurring check-in because the launchd context has no LANG/LC_*, so the interpreter fell back to the ASCII stdout codec and the migration banner's raised UnicodeEncodeError. 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 runtime version 0.1.4 -> 0.1.5 #154chore(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.5 on main to cut the signed/notarized build → publishes to s3://unbound-release-artifacts/macos/0.1.5/ and repoints the latest/onboard.sh the 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-encoding crash fix to production: _force_utf8_io() is called at the top of main() to reconfigure stdout/stderr to UTF-8 with errors='replace', preventing UnicodeEncodeError from aborting onboarding in Jamf's launchd context where no LANG/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 to unbound_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.py banner 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.py receives 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

Filename Overview
binary/src/unbound_hook/main.py Adds _force_utf8_io() called at main() entry to reconfigure stdout/stderr to UTF-8 with errors='replace'; fix is sound but exception handler silently swallows failures without attempting to log to the other stream.
packaging/unbound_discovery_entry.py Version bumped to 0.1.5, but the same UTF-8 stdout reconfiguration fix applied to unbound_hook/main.py is absent here despite this binary running in the same launchd/no-locale environment.
binary/src/unbound_hook/setup_cmd.py Replaces the non-ASCII → arrow with ASCII -> in the migration banner, complementing the _force_utf8_io() fix by eliminating the known crash-triggering character at its source.
binary/tests/test_setup_migration.py Adds a well-targeted regression test that monkeypatches sys.stdout/stderr to ASCII-only wrappers and verifies main() reconfigures them to UTF-8 and completes successfully.
binary/src/unbound_hook/init.py Version bump from 0.1.4 to 0.1.5 — straightforward, required by the release workflow's publish-safety assert.

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 0
Loading

Comments Outside Diff (1)

  1. packaging/unbound_discovery_entry.py, line 68-78 (link)

    P1 Discovery binary missing the same UTF-8 reconfiguration

    unbound_discovery_entry.py ships as a separate binary that also runs from a root LaunchDaemon with no LANG/LC_* set — the exact same launchd context that produced the crash in unbound_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 raise UnicodeEncodeError and 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 of main() here would close the gap.

Reviews (1): Last reviewed commit: "chore(release): bump runtime version 0.1..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

vigneshsubbiah16 and others added 2 commits June 15, 2026 10:54
…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>
@vigneshsubbiah16 vigneshsubbiah16 requested a review from a team June 15, 2026 18:09
Comment on lines +40 to +43
except (ValueError, OSError):
# Detached/closed stream — nothing we can do, and not worth
# crashing onboarding over.
pass

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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!

@vigneshsubbiah16 vigneshsubbiah16 merged commit 1280b63 into main Jun 15, 2026
3 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.

1 participant