Repository navigation
Obtain a telemetry-only collector token for a build elsewhere, and use one the emitter is given - #34
Merged
Merged
Conversation
The Modal runner will compare this origin with the origin a collector token was obtained for. Fails: `version` prints only the versions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The origin is the one the emitter resolves with no module setting, the same one `resolve_collector_origin()` gives the modules. The `hello` answer is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The command exchanges the operator's Hugging Face login for a `telemetry:write` collector token with a requested lifetime, and prints the token, its expiry and the collector origin as JSON on standard output only. Each failure has its own exit code: no login (3), a refused login (4), an unavailable collector (5), and a collector without `lifetime_seconds` (6, its strict exchange body answers 422). The fake collector now takes `lifetime_seconds`, as the collector's exchange for builds on other machines will. Fails: there is no `telemetry-token` command. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
It exchanges this machine's Hugging Face credential, at the collector origin the emitter resolves, for a token with scope `telemetry:write` and the requested `lifetime_seconds`. It prints `access_token`, `expires_at` (counted from before the request, so never later than the token's real expiry) and `collector_origin` as JSON on standard output, and nothing else. A failure prints one line on standard error, without the token or the collector's answer, and exits 3 (no credential), 4 (credential refused), 5 (collector unavailable) or 6 (the collector answers 422 to `lifetime_seconds`). `--development-collector-url` accepts a loopback collector only, as the module setting of the same name does; no option or environment variable can send the credential anywhere else. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With MICROCOSM_COLLECTOR_TOKEN, MICROCOSM_COLLECTOR_TOKEN_EXPIRES_AT and MICROCOSM_COLLECTOR_ORIGIN set, as the Modal runner will set them from `telemetry-token`'s output, telemetry delivery must send that token and never read or exchange a Hugging Face credential, and a session for another scope must have no credential. The telemetry module must refuse to start, before it opens the spool and with a message naming the variable, when the origin differs from the one it resolved, a variable is missing, or the expiry is unreadable. Fails: the emitter ignores the variables and reads the Hugging Face credential. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`CollectorSession` reads MICROCOSM_COLLECTOR_TOKEN, MICROCOSM_COLLECTOR_TOKEN_EXPIRES_AT and MICROCOSM_COLLECTOR_ORIGIN. With them, it returns that token and never reads or exchanges a Hugging Face credential; a session for another scope, such as collector submissions, has no credential, because the token only writes telemetry. MICROCOSM_COLLECTOR_ORIGIN is only compared with the origin the session was created for, which the module settings resolve as before. A different origin, a missing variable or an unreadable expiry raises `GivenCollectorTokenError`, whose message names the variables and never their values. The telemetry module prints why and refuses to start, now before it opens the spool, because the host prints only a fixed startup failure. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A given token cannot be renewed where it is used. Once it has expired, or the collector answers 401, the run must stay pending, recording `collector_token_expired` or `collector_token_rejected`, rather than become local-only, which is permanent; the token must not be sent again, and one line must say so. A later emitter with a Hugging Face login must then deliver the run and clear the reason. The telemetry module must not hold the build's exit for the drain period while it waits for such a credential. Fails: an expired token is still sent, a refused one is sent again with no reason recorded, and the module waits out the drain period. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A given token cannot be renewed where it is used. After it expires, or after a 401, the session answers `token_expired` or `token_rejected` instead of sending it again. Telemetry delivery then leaves each run pending and records `collector_token_expired` or `collector_token_rejected` in the run's existing `local_only_reason` column, so no spool migration is needed, and says once on standard error that the events wait for an emitter with a Hugging Face login. `local_only`, which is permanent, is no longer reached this way. A successful registration clears the reason, so a later emitter that delivers the run leaves no stale reason. The module's drain at shutdown stops as soon as nothing more can be delivered with the given token, instead of holding the build's exit for the drain period. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds the README section on `telemetry-token`, its exit codes, the three variables an emitter on another machine is given, and what happens when the token stops working, and an `added` changelog fragment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 28, 2026
The three test modules each set MICROCOSM_COLLECTOR_TOKEN, MICROCOSM_COLLECTOR_TOKEN_EXPIRES_AT and MICROCOSM_COLLECTOR_ORIGIN in their own way, and two defined the synthetic token. They now use `given_token_variables` in tests/support/given_token.py, and the credential isolation fixture clears the three variables as it clears HF_TOKEN. No test changes what it checks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`telemetry-token` refuses a 200 exchange answer without a non-empty token and a positive integer `expires_in` (exit 5), but `CollectorSession` defaults a missing lifetime to an hour, raises a zero one to a minute, reads one sent as text, and accepts an empty token. The two must share one check. The fake collector can now answer every exchange with a chosen 200 body. Fails: the session answers `ok` in all four cases. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`exchange_huggingface_credential` now holds the request, the status mapping and the check of a 200 answer that `CollectorSession` and `telemetry-token` each had, with different rules. Both use the command's stricter check: a 200 answer needs a non-empty `access_token` and a positive integer `expires_in`. Every collector release sends an integer `expires_in`, so the session's default of an hour is gone with `DEFAULT_TOKEN_LIFETIME_SECONDS`; it still reuses a token for at least `MINIMUM_TOKEN_LIFETIME_SECONDS`. The result is a typed `TokenExchange`. `exchange_telemetry_token` and its `post`, `credential` and `clock` parameters, which nothing passed, are gone: the command calls the shared function and formats `expires_at` from the time it recorded before the request. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`telemetry-token` maps only `OSError` and `TimeoutError`. A Hugging Face credential with a line break makes http.client raise a `ValueError` whose text quotes the whole Authorization header, and the command exits 1 with a traceback. Any failure outside the mapping must exit with a documented code (7), print nothing about the error but its type, and send nothing. A given collector token with a line break passes the start check today and fails the same way on its first request; it must stop the emitter at start, naming the variable and not its value. The failure test now also covers usage errors (exit 2), which already pass. Fails: the command raises `ValueError: Invalid header value b'Bearer <the credential>'`, and the start check does not raise. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`main` now catches any exception from `telemetry-token` and exits 7 with one line naming only the error's type; nothing else about it is printed, since its text can quote the credential. Two checks keep credentials out of exception text as well: - `_http_post` refuses a bearer credential that is not visible ASCII without spaces, with a fixed message, before http.client can quote it in its own error. Every Hugging Face credential and collector token has that form. - The start check refuses a given `MICROCOSM_COLLECTOR_TOKEN` of any other form, naming the variable and not its value, as it refuses the other unusable values. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The collector decodes tokens with no clock leeway, and the session sends a given token until the second it expires, so requests in its last seconds get a 401 and the run records `collector_token_rejected` instead of `collector_token_expired`. The exchanged-token path stops `TOKEN_REFRESH_MARGIN_SECONDS` (30 s) early; a given token must too. The expiry row of the pending-run test now uses a token with 10 s left. Fails: the token is still sent, and the first delivery succeeds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The session stops sending a given token `TOKEN_REFRESH_MARGIN_SECONDS` before `expires_at`, as early as it renews an exchanged one. Requests that would have reached the collector in the token's last seconds, or from a machine whose clock is slightly behind, now leave the run pending with `collector_token_expired` instead of drawing a 401. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Orrery module gets its credential from the session it shares with telemetry, and sends it to its `publication_origin`, by default another service. With a given token that means a token lasting up to a day goes to a service other than the collector it was obtained from. And a 401 from that origin, or from the storage Orrery uploads to, ends the token for telemetry as well, although the collector never refused it. With a given token, Orrery must have no credential (`missing`) unless it publishes to the collector origin, and a 401 it receives must end the token for Orrery only. Fails: Orrery gets the token for its default publication origin; with that assertion removed, telemetry answers `token_rejected` after Orrery's 401. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two changes for a collector token given in MICROCOSM_COLLECTOR_TOKEN: - **Only to the collector.** `CollectorSession` takes the `destination` its caller sends the credential to. The Orrery module passes its `publication_origin`; unless that is the collector origin, a session with a given token has no credential (`missing`), so Orrery's jobs stay queued with `credential_missing` and the token never leaves for another service. Exchanged tokens are unaffected. - **A 401 ends it for one module.** `shared_session` still shares one session, and so one exchanged token, among modules. With a given token it gives each module a session of its own, since there is nothing to renew: a 401 one module receives ends the token for that module only. The Orrery module now opens its queue after this check, as the telemetry module opens its spool after it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An unusable given token makes `initialize` raise in the telemetry or Orrery module, and the host then exits before any module runs, not only telemetry. The telemetry module's line says "Microcosm telemetry cannot start", and the Orrery module, which the host may start first, prints nothing. Each must print a line that says the emitter cannot start and names the variable, before opening its database. Fails: the telemetry module's line names only telemetry; the Orrery module prints nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`shared_session` now prints the reason, so the telemetry and Orrery modules both say why, whichever the host starts first. The line says the emitter cannot start and exits before any of its modules runs, which is what the host does after any failing `initialize`, rather than that telemetry cannot start. The origin error no longer points only at `microcosm-emitter version`: that prints the origin used without a module setting, and a `collector_origin` or `development_collector_url` setting chooses another. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`CollectorSession.credential()` returns a `SessionCredential`: a named pair of a `SessionResult` (a `Literal` of the seven answers, including `token_expired` and `token_rejected`) and the token. It unpacks as before, and its repr leaves the token out. The telemetry delivery types its map from session answers to pending reasons with it, and the Orrery queue and the submission client name it in their `credential` parameters, so each module that reads these answers can see them. The session's `environment` parameter, which nothing passed, is gone: it reads the process environment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When a module's `initialize` fails, the host closes the modules it has already started, which may include the Hugging Face module and its workers, and exits before accepting a request. "Before any of its modules runs" was not exact; the start error now says what the build sees. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The README section, the changelog fragment and the docstrings now say: - the Orrery module gets its credential through the shared session, and with a given token has none unless it publishes to the collector origin; - an unusable value in any of the three variables stops the whole emitter at start, with the exact line printed, and what the host and the interface then do; an emitter with only the Hugging Face module starts with collector submissions off; - a given token counts as expired 30 seconds early, and a 401 ends it only for the module that received it; - `telemetry-token` exits 7 for any error the other codes do not name, usage errors print the usage text, and no output contains a credential. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 28, 2026
…-spool Brings in #34's round-one changes, up to 25d40c4. Conflicts: - README.md: #34's rewritten bullets on given tokens, followed by this branch's section on a copied spool. - src/microcosm_emitter/cli.py: both sides' imports, codes and messages. `telemetry-token` uses #34's `exchange_huggingface_credential` and its exit 7 for unexpected failures, with this branch's `--development-collector-url` argument and `CREDENTIAL_FAILURES` name. Both sides use exit 7 for different failures; the following commits give the spool errors their own code. #34 removed `CollectorSession`'s `environment` parameter and `exchange_telemetry_token`, which this branch used: - `deliver_spool` now builds `CollectorSession(origin)`. That session reads a given token from the environment, so `deliver` now uses a build's token left in the shell; the next commit stops that. The existing test fails: 1 failed, 24 passed in the two command test modules (`assert 5 == 8`). - The copied-spool tests obtain the build's token with `exchange_huggingface_credential`, and set the given-token variables with #34's `given_token_variables`, only while the build's session is created. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 28, 2026
#34 removed `CollectorSession`'s `environment` parameter, through which `deliver_spool` kept the session from reading MICROCOSM_COLLECTOR_TOKEN and its two companion variables. Since the merge, a build's token left in the operator's shell was sent instead of the operator's login: the run stayed pending with `collector_token_expired` until the time limit, and `deliver` exited 5. `CollectorSession` now takes `use_given_token`, true by default. With false, which only `deliver_spool` passes, it reads none of the three variables and exchanges this machine's Hugging Face credential. Before: 1 failed, 24 passed (`assert 5 == 8`). After: 25 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 28, 2026
#34 merges first, and its `telemetry-token` exits 7 for any failure it does not name. `status --spool` and `deliver --spool` also exit 7 for a file they cannot use as a spool database, so the two meanings share a code. The spool file errors move to 9, the next code neither command uses. Fails: 6 failed, 5 passed (`assert 7 == 9`). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 28, 2026
The commands' exit codes are now members of one `ExitCode` table in `cli.py`, marked `@unique`, which refuses two members with one value when the module is imported. A new code therefore cannot take a meaning another command already gives it, as exit 7 did when #34's unexpected failures met this branch's spool file errors. The table also reserves 1 (the host's start failure, and Python's code for an uncaught exception) and 2 (argparse's usage error). `status --spool` and `deliver --spool` exit 9 for a file they cannot use as a spool database; 7 keeps #34's meaning, and `deliver` keeps 8 for a run another Hugging Face user registered. The README says so. Before: 6 failed, 5 passed (`assert 7 == 9`). After: 11 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 28, 2026
`deliver_spool` declared its own `LoginFailure` literal for three of the session's answers, from before #34 typed them. `SpoolDelivery` now has the outcome `login_failed` and carries the session's answer in `login`, typed with #34's `SessionResult`. Any answer other than `ok` stops `deliver` before it opens the file, where only the three named answers did. `deliver` maps the answer to its exit code and message with the same `CREDENTIAL_FAILURES` table as `telemetry-token`. No behaviour changes for the answers a session without a given token or a scope gives: 25 passed in the two command test modules. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
exchange_huggingface_credential now returns ExchangedToken, whose token and lifetime are always set, or ExchangeFailure with the reason and HTTP status. CollectorSession maps each failure reason to its own answer through one table, instead of passing an exchange result through as a session result: lifetime_unsupported, which the session never causes, counts as unavailable. No behaviour changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A credential with a pasted space must be handled like one the collector refuses: the run becomes local-only with huggingface_credential_rejected, nothing is sent, and the delivery keeps running. Today the request's check for values no HTTP header can carry raises ValueError through CollectorDelivery.flush_once, which stops the telemetry module. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
exchange_huggingface_credential checks the credential before it sends it, and answers unsendable for one an HTTP header cannot carry, such as one with a pasted space. CollectorSession counts that as rejected, which is what the collector answered such a credential (401) before the emitter checked: the telemetry module makes the run local-only with huggingface_credential_rejected, says so once, and keeps recording events, instead of stopping on a ValueError. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An unsendable credential exits 4 with its own message, an unreadable saved login exits 3, and a collector answer that is not HTTP exits 5. Exit 7 stays for unexpected errors, which print only their type. Today all three exit 7: the first with a KeyError, the others with PermissionError and BadStatusLine, which are neither an OSError nor caught. The fake collector can now answer an exchange with bytes that are not an HTTP answer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…odes - An unsendable credential exits 4, as a refused one, with a fixed message that says it was not sent and why. - A saved login that huggingface_hub cannot read exits 3, naming only the error's type. - A collector answer that is not valid HTTP (http.client.HTTPException) counts as unavailable: exit 5 here, and a retry in the emitter's sessions instead of an uncaught error. Exit 7 is left for unexpected errors. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With a given collector token, the Orrery module's session answers missing for its publication origin, so its jobs record credential_missing, the code of a machine without a login. The answer should say that the token is withheld from that destination on purpose. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A session holding a given collector token now answers withheld_from_destination, not missing, to a caller that sends its credential to another destination. The Orrery module's jobs then record credential_withheld_from_destination, so a receipt from a Modal build says that the token was kept from the publication origin on purpose, rather than looking like a machine without a login. Another scope still answers missing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… repr An expiry time without a time zone stops the emitter at start, naming MICROCOSM_COLLECTOR_TOKEN_EXPIRES_AT, as the README says. The session's answer and the given token it holds leave the token out of their repr. Both already hold; these cover them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The changelog no longer says no output has a credential: the command prints the token on standard output. It names the exception for an emitter with only the Hugging Face module, the code Orrery's jobs record, and that an unsendable Hugging Face credential counts as refused. - The README's exit-code table lists an unreadable saved login under 3, an unsendable credential under 4 and an answer that is not valid HTTP under 5, leaving 7 for unexpected errors. It names credential_withheld_from_destination, and says that only an emitter of 0.11.0 or later clears a pending run's reason. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 28, 2026
…-spool Brings in #34's round 2 fixes up to ca37144. Two files conflicted; both sides are kept: - cli: #36's ExitCode table and messages, with #34's unsendable-credential entry (exit 4), unreadable-login message (exit 3) and ExchangeFailure check. CREDENTIAL_FAILURES is typed by the exchange's failure reasons and the session's answers, which telemetry-token and deliver look up. - README: #34's corrected paragraph on a token that stops working, followed by #36's section on a copied spool. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 28, 2026
#34 makes a Hugging Face credential that an HTTP header cannot carry count as refused, and never sends it. deliver gets that answer from its session as rejected, so it exits 4 but says the collector refused the credential, which never reached the collector. It should print telemetry-token's message for an unsendable credential. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Hugging Face credential with a pasted space is never sent, but the workstation telemetry module warns that it "was not accepted as a PolicyEngine organization member". The test now checks each case's warning: a refused credential keeps that message, and an unsendable one must say that it contains characters that cannot be sent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… sent CollectorSession now answers unsendable, instead of rejected, for a Hugging Face credential it did not send because it contains characters that cannot be sent as a bearer token. The workstation telemetry module still marks the run local-only with huggingface_credential_rejected, but prints its own warning naming that cause and the places to check. Collector submissions still end the hand-over failed with CREDENTIAL_REJECTED. The Orrery module's jobs record the answer as credential_unsendable and are retried, as for credential_rejected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dToken's repr The submissions client's refusal test now has a case for a credential with a pasted space: the submission fails with CREDENTIAL_REJECTED and the credential is never sent. A second test exchanges a credential with the test collector and checks that the token is absent from the repr of the ExchangedToken it returns. Both pass; with the refusal mapping or repr=False removed, each fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The docs and messages said a space is a character "that an HTTP header cannot carry", but HTTP header values may contain spaces. What the emitter refuses is any credential that is not visible ASCII, such as one with a space, a tab, a line break or a non-ASCII character: a bearer token (RFC 6750) never contains those characters, so the collector would refuse it, and Python's HTTP client raises an error that quotes the whole header value for a line break. The command's message, the request error, the comments, the README and the changelog now say that the credential contains characters that cannot be sent as a bearer token. The README and the changelog also name the new telemetry warning and the Orrery module's credential_unsendable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 28, 2026
…-spool Brings #34 up to a6cf212: the unsendable credential's own telemetry warning, the corrected wording for an unsendable credential, and the tests for the submissions refusal and ExchangedToken's repr. Conflicts: - core/auth.py: both sides added the session answer `unsendable`; kept #34's comment, which says how each caller handles it, including Orrery's credential_unsendable. - cli.py: kept #36's ExitCode table and CREDENTIAL_FAILURES, with #34's wording for the unsendable message and the comment above it. Also resolved: - telemetry/service/collector.py merged without a conflict but with #36's `result in ("rejected", "unsendable")` ahead of #34's own `unsendable` branch, so the new warning would never print; the rejected branch is `result == "rejected"` again. - test_copied_spool checks the unsendable message's new wording. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 28, 2026
… login The Orrery module records the session's answer as credential_<answer>, so a graph job whose Hugging Face credential cannot be sent as a bearer token waits with credential_unsendable, not credential_rejected. A comment where the code is recorded now lists the credential codes, beside the session comment, README and changelog that #34 updated. The README and changelog also say that deliver stops, leaving the copy unchanged, for a saved login it cannot read (exit 3), and the remaining comments that said a space is something "an HTTP header cannot carry" now say the credential contains characters that cannot be sent as a bearer token. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
marked this pull request as ready for review
October 29, 2026 14:44
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.
Fixes #33
Summary
A Microcosm build on Modal has no Hugging Face credential, so its emitter could never authenticate to the collector. This PR is the emitter's half of the accepted design: the operator's workstation obtains a telemetry-only collector token at launch, and the emitter in the container uses it.
microcosm-emitter telemetry-token --lifetime-seconds N(on the workstation). It exchanges this machine's Hugging Face credential at the emitter's own resolved collector origin, with the body{"scope": "telemetry:write", "lifetime_seconds": N}. It prints exactly one JSON object on standard output,{"access_token", "expires_at", "collector_origin"}, and nothing else.expires_atis an ISO 8601 UTC time counted from before the request, so it never overstates the token's real expiry. It prints the token only on standard output and never prints the Hugging Face credential; an unexpected error exits 7 and prints only the error's type.MICROCOSM_COLLECTOR_TOKEN,MICROCOSM_COLLECTOR_TOKEN_EXPIRES_ATandMICROCOSM_COLLECTOR_ORIGINset,CollectorSessionreturns that token and never reads or exchanges a Hugging Face credential. A session for another scope (the Hugging Face module'sartifacts:submitsession) has no credential (missing), because the token only writes telemetry. The token is sent only to the collector origin it was obtained from (see "The Orrery module and a given token").MICROCOSM_COLLECTOR_ORIGINwith the origin the module settings resolved (unchanged:PRODUCTION_COLLECTOR_URL, a trustedcollector_origin, or a loopback development URL). A different origin, a missing variable, an expiry without a time zone, or a token that is not visible ASCII without spaces raisesGivenCollectorTokenError. That stops the whole emitter, not only telemetry: whichever of the telemetry and Orrery modules the host starts first printsThe Microcosm emitter cannot start: <reason>. It exits before accepting any request from the build.(naming the variable, never its value), before opening its database. The host then closes any module it had started and exits with code 1 before accepting a request, so no module of that emitter, the Hugging Face module included, serves the build; the interface'sstart_emitterraisesEmitterStartupError(child_exit). An emitter that loads only the Hugging Face module starts, with collector submissions off.expires_at(TOKEN_REFRESH_MARGIN_SECONDS, when an exchanged token is renewed; the collector allows no clock leeway), or after a 401 on registration or events, it is never sent again; each run stayspendingwith the new reasoncollector_token_expiredorcollector_token_rejected, instead oflocal_only, which is permanent. One line on standard error says so, and the module's drain at shutdown ends at once instead of holding the build's exit for the drain period. A later emitter with a Hugging Face login of the same user delivers the run; with 0.11.0 or later, its successful registration also clears the reason.microcosm-emitter versionalso printscollector_origin: the origin the emitter sends to without a module setting. Thehelloanswer is unchanged.The command's name
telemetry-tokennames what the command prints and the only thing the token allows. The design called itdelegate; I didn't use that, because it describes an intent rather than the output, and it suggests the emitter grants authority when the collector does. It also reads likeversion: a noun for what is printed.Exit codes of
telemetry-token--development-collector-urlthat is not loopback); nothing was senthf auth loginthat this user cannot read; nothing was sentaccess_tokenand positive integerexpires_inlifetime_seconds, so it predates tokens for other machinesA usage error prints argparse's usage text. Every other failure prints one line on standard error, without the token, the Hugging Face credential, the collector's answer body or an exception's text. An unsendable credential and an unreadable saved login each have their own fixed message.
exchange_huggingface_credentialchecks the credential before sending it, and_http_poststill refuses any bearer value that is not visible ASCII without spaces with a fixed message, so http.client never quotes one in its own error.One exchange for the session and the command
exchange_huggingface_credentialincore.authis the only code that posts to the exchange, maps its statuses and checks a 200 answer;CollectorSessionandtelemetry-tokenboth call it. Both now use the command's check: a non-emptyaccess_tokenand a positive integerexpires_in. For the session that is stricter than before (it defaulted a missing lifetime to an hour and read one sent as text); every collector release sends an integerexpires_in. A collector answer that is not valid HTTP (http.client.HTTPException) counts as no answer.The exchange returns
ExchangedToken, whose token and lifetime are always set, orExchangeFailure, with the reason and the HTTP status. The session maps each failure reason to its own answer through one table,_FAILED_EXCHANGE_ANSWERS, rather than passing the exchange's result on as its own. Session answers are typed:SessionCredential, a named pair of aSessionResultliteral and the token, which unpacks as before and leaves the token out of its repr. The repository runs no type checker (its quality check is Ruff only), so these types were checked by reading.An unsendable Hugging Face credential counts as refused. A credential that is not visible ASCII, such as one with a pasted space, a tab, a line break or a non-ASCII character, is never sent: the exchange answers
unsendable, and so does the session. Such a value cannot be a bearer token (RFC 6750), so the collector would refuse it: before this PR the emitter sent one with a space and the collector answered 401, and one with a line break made http.client raise an error quoting the whole header value. Each caller handles it as a refused credential:local_onlywithhuggingface_credential_rejectedand keeps recording events, instead of stopping on aValueError. Its warning, printed once, says the credential contains characters that cannot be sent as a bearer token, and where to check, rather than that it was not accepted as a PolicyEngine organization member.failedwithCREDENTIAL_REJECTED.credential_unsendable, and are retried as forcredential_rejected.A given collector token with such characters is still refused at start.
The Orrery module and a given token
The Orrery module receives its credential through the collector session (
shared_session), and sends it to itspublication_originsetting, which by default is another service, not the collector. With a given token, the session hands the token only to a caller whose destination is the collector origin it was obtained from, so Orrery sends the given token only to the emitter's resolved collector origin: with any other publication origin, its default included, the session answerswithheld_from_destination, and Orrery's jobs stay queued withcredential_withheld_from_destination, so a receipt says the token was withheld on purpose rather than looking like a machine without a login. Today that loses nothing, since that service cannot yet authorize a publication with a collector token.A 401 ends a given token only for the module that received it. Modules still share one session, and so one exchanged token and its renewal. With a given token there is nothing to renew, so
shared_sessiongives each module a session of its own: a 401 that Orrery receives from its publication origin or from the storage it uploads to no longer stops telemetry.Where the pending reason is stored
In the run's existing
local_only_reasoncolumn, whileupload_statestayspending. A new column or a new upload state would need a spool migration, and released emitters that share the spool database refuse a revision they don't know. Released emitters only look atupload_state, so they deliver these runs normally.Environment variables
All three are new. None of them chooses where the emitter sends: the destination is still resolved only from
PRODUCTION_COLLECTOR_URL, the trustedcollector_originsetting or a loopbackdevelopment_collector_url, andMICROCOSM_COLLECTOR_ORIGINis only compared with it.MICROCOSM_COLLECTOR_TOKENtelemetry-token'saccess_token(tracker task 6.4 · 3, not written yet)core.auth.given_collector_token, through everyCollectorSessionin the emitter host. The telemetry module sends the token; the Orrery module only if it publishes to the collector origin; the Hugging Face module's submission session neverMICROCOSM_COLLECTOR_TOKEN_EXPIRES_ATexpires_atMICROCOSM_COLLECTOR_ORIGINcollector_originProvider and wiring: unverified until the runner exists. The design passes them as a Modal secret created for each launch (
modal.Secret.from_dict), exempted from the runner's credential removal, and inherited by the build and its emitter child process. Nothing in this repository sets them. The tests' credential isolation fixture now clears them, as it clearsHF_TOKEN.Depends on
lifetime_secondson the exchange, in progress) must be deployed beforetelemetry-tokenworks against the deployed collector. Until then the deployed collector answers 422 to the new field and the command exits 6. The command is tested only against the loopback fake collector intests/support/fake_collector.py, which now acceptslifetime_secondswithtelemetry:writeonly, caps it at 24 hours, answers 422 when told to behave like an older collector, and can answer every exchange with a chosen 200 body.Overlaps
Open #5, #6 and #7 (stacked) change local-only handling in
telemetry/service/collector.py. This PR doesn't change their code paths: it adds two branches to_collector_token(for an unsendable credential and for an unusable given token), one call in_register's 201 path, and the new_keep_pending. Whichever merges second mergesmainin; the conflicts should be limited to those spots.Tests
Red, then green, for each behaviour (Python 3.14, macOS):
versionreports the collector origincollector_origin)telemetry-tokenand its exit codesinvalid choice: 'telemetry-token')ValueError)ok; the command already exited 5)ValueError: Invalid header valuequoting the credential; a given token with a line break passed the start check); the two usage cases passedtoken_rejectedafter Orrery's 401)local_only, nothing is sent, delivery keeps runningValueErrorfrom_http_post's check, raised throughCollectorDelivery.flush_once)telemetry-token: an unsendable credential exits 4 with its own message, an unreadable saved login 3, an answer that is not HTTP 5, and an unexpected error still 7KeyError,PermissionErrorandBadStatusLine)'missing' != 'withheld_from_destination')Commits without a behaviour test: 7b7c052 sets the three variables through one test helper (
tests/support/given_token.py); 6191d15 types the session's answers and removes the session's unusedenvironmentparameter; b080f9e words the start error; 25d40c4 updates the README, the changelog fragment and docstrings. In round 2: 25551bb splits the exchange's result into success and failure types; 96bee3e tests an expiry without a time zone and the token's absence from the session's repr (both held already); ca37144 corrects the changelog and the README. In round 3: f44f9b7 tests that collector submissions refuse an unsendable credential without sending it, and thatExchangedToken's repr leaves the token out (both held already; each test fails with the code it covers removed); a6cf212 says what an unsendable credential is and why it is refused, in the command's message, the comments, the README and the changelog, which also name the new warning andcredential_unsendable.At a6cf212:
uv run --no-sync pytest -m "not collector"passed in full (809 passed, 9 deselected), andtools/check-quality.shandtools/check-changelog.shpassed. All 10 CI checks pass on a6cf212. The collector job's pinned collector predates scopes andlifetime_seconds, so this PR adds no collector-job test.Follow-up
#37 tracks two problems older than this PR, which it leaves as they are: a saved login that cannot be read, or a malformed collector answer during delivery, still stops the workstation telemetry module; and the exchange accepts a collector token that contains a space.
Release
The fragment is
added, so merging releases 0.11.0 of both distributions to PyPI.🤖 Generated with Claude Code