Skip to content

feat(gateway)!: clock writes take the control token, a watch its computer's read token, and a waiting write names a computer that stopped watching (FD-5472) - #634

Closed
earakely-scale wants to merge 2 commits into
mainfrom
edgararakelyan/fd-5472-hb12-clock-read-tokens-the-watch-and
Closed

earakely-scale wants to merge 2 commits into
mainfrom
edgararakelyan/fd-5472-hb12-clock-read-tokens-the-watch-and

Conversation

@earakely-scale

@earakely-scale earakely-scale commented Nov 12, 2026 •

Copy link
Copy Markdown
Collaborator

Part of FD-5420: a computer's own clock follows the gateway's universe clock. This is FD-5472. Each watch on /clock/watch is a computer's acknowledgement of the version it shows, so a watch must come from that computer. And nothing but core may move the clock.

What changes

  • The clock's writes take the gateway's control token. /clock/set-time, /clock/set-rate, /clock/advance and /clock/clear are now among CONTROL_ROUTES.
    • This closes a real hole: until now anyone who could reach a gateway could move its clock, an agent with a shell included, since its own MCP config names the gateway.
    • Core reaches the clock through DeployedEnv.invoke, which already sends the env's control headers.
    • A gateway started without a token, as a Harbor export's is, stays open.
  • A watch takes a read token, under its computer's own name. /clock/watch takes the control token or a declared computer's read token (CLOCK_FOLLOWER_TOKENS). A computer's token must name that computer as follower.
    • Without a token it answers 401.
    • An unknown token, another computer's name, or no name answers 403.
    • Only a computer's own token acknowledges, as before. The control token reads without acknowledging.
    • Every interface calls the value the read token. For now core passes each computer's control token, so a separately minted token can replace it later with no protocol change.
  • /clock/state and /clock/time stay open. They show only the universe's time, its rate and which version each computer shows. Two outside readers depend on them: the agentic-DQ skill uses /clock/state as its liveness probe, and psi_bench's event feed finds gateways with it.
  • A waiting write doesn't wait for a computer that has stopped watching. A follower holds a watch open up to 25 s and reconnects at once. One whose last watch is older than that plus 5 s (ClockFollowers.QUIET_S) has lost its connection.
    • A write given wait_followers_s now names such a computer at once instead of waiting out its full timeout. One that goes quiet during the wait ends it at its quiet deadline.
    • A computer that hasn't watched yet is still waited for, since it may be starting.
    • The 504 now carries followers: each computer's last shown version and when it last watched.

The design's accepted gap stands: the session user can read the computer's control token today, so it could open watches as its own computer. Minting a separate read token later closes it.

Outside callers

  • psi_bench's event feed (synthetic-pipelines) arms the clock over plain HTTP. Its fix, scaleapi/synthetic-pipelines#1885, sends the env's control token: --gateway-token / set_gateway(url, token=...), and a gateway it finds on the host gives its own, from its container's environment.
  • Harbor exports (agentenvhub-sdk) call /clock/set-time with no token. Their gateways run without one, so nothing changes for them.
  • agent-env-website only describes the routes. The docs should say the clock's writes take the env's control token, and that a watch takes a computer's read token under its name.

Testing

  • Unit: tst/unit/env/gateway/control_token_test.py covers the clock's writes among the control routes:

    • refused without the token, and with a computer's read token;
    • time and state read without one.

    clock_watch_test.py covers:

    • the seven ways a watch can come (its own token and name, another name, no name, an unknown token, none, the control token, the control token naming a computer), and which ones acknowledge;
    • a computer that stopped watching named at once, with when it last watched;
    • one that goes quiet during the wait named at its quiet deadline, not after the whole wait;
    • a computer that hasn't watched yet waited for its full time.

    CI's unit jobs pass, and all 32 checks are green on 1614208.

  • The write token stays off computers. multi_env_computers_test.py now scans the deployed computer's container for the gateway's control token, as root and as the session user. It checks every file under /etc, /opt, /home, /root, /tmp, /run, /var, /srv and /usr/local, and every process's environment and command line. The token is in none of them.

  • Integration, on an EC2 host (x86_64), with local sandboxes:

    • gateway_test.py's clock tests pass: the clock extension, its rate accuracy, the time triggers, the autonomous trigger and sync_env_clock.
    • The trigger and event hook tests pass in full: test_register_env_triggers_local.py, test_next_env_event_hook_local.py and test_advance_to_next_event_hook_local.py, 8 tests.
    • multi_env_computers_test.py's gateway test passes, the token scan included.
  • E2E against the branch's gateway image, with two real follower processes long-polling under their read tokens:

    Case Result
    set-time, set-rate, advance, clear without a token; set-time with a computer's read token 401
    set-time with the control token 200
    /clock/state, /clock/time without a token 200
    A watch: no token / unknown token / another computer's name / no name 401 / 403 / 403 / 403
    A watch: the control token / a computer's own token and name 200 / 200
    An advance waiting for both computers 200, in 4 ms
    One computer paused (SIGSTOP) for 36 s, then an advance waiting up to 10 s 504 in 4 ms, naming it (last watched 36 s before); the other acknowledged
    The paused one resumed, then an advance waiting 200, in 3 ms
    The gateway restarted, then a re-arm waiting 200

Docs

The clock extension's page should cover:

  • the writes taking the env's control token;
  • a watch taking a computer's read token under its own name (401 and 403);
  • the open time and state;
  • a waiting write naming a computer that has stopped watching at once.

🤖 Generated with Claude Code

RetriggerView in GreptileConfidence Score: 5/5

The PR appears safe to merge; no new blocking issues were found.

Summary

The PR protects clock writes with the gateway's control token and ties each watch to its computer's read token.

  • Clock writes now need the gateway’s control token.
  • A clock watch now belongs to the computer named by its token.
  • Clock writes stop waiting when a follower goes quiet.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  W[Clock write with control token] --> C[Change clock and answer held watches]
  C --> Q{Wait requested?}
  Q -->|No| OK[Return success]
  Q -->|Yes| A[Check each computer's shown version]
  A -->|All caught up| OK
  A -->|Still behind| T[Wait for a watch, quiet deadline, or write deadline]
  T --> A
  A -->|No further wait needed| E[Return 504 naming computers still behind]
Loading

Reviews (7) · Last reviewed commit: "fix(gateway): a waiting write wakes when..." · Reviewed by Greptile

…ter's read token, and a waiting write names a computer that stopped watching (FD-5472)

- /clock/set-time, /clock/set-rate, /clock/advance and /clock/clear are control routes: anyone who could reach a
  gateway could move its clock, an agent with a shell included. Core already sends the token (DeployedEnv.invoke); a
  gateway without one, as a Harbor export's, stays open.
- /clock/watch takes the control token or a declared computer's read token, and a computer's token names that
  computer (401 without a token, 403 for an unknown one or another name). /clock/state and /clock/time stay open.
- A write given wait_followers_s doesn't wait for a computer whose last watch is older than a hold and a reconnect
  (ClockFollowers.QUIET_S): it names it at once, its 504 carrying when each computer last watched. One that hasn't
  watched yet is still waited for.
- multi_env_computers_test checks the gateway's control token is in no file, environment or command line on a
  computer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@earakely-scale
earakely-scale requested a review from a team as a code owner November 12, 2026 05:07
… quiet; name the token scan's size limit (FD-5472)

stale_after only woke on an acknowledgement, so a computer that went quiet during the wait (its last watch 29 s before
a 30 s wait) still held the write for all of it. It now also wakes at the next quiet deadline of a computer it waits
for.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@earakely-scale

Copy link
Copy Markdown
Collaborator Author

Closing: we're deferring the control-token half of FD-5472 (clock writes behind the control token, watches bound to each computer's read token), which Hark's CUA work doesn't need yet. Revisiting it should also cover moving tokens out of environment variables. The fail-fast part, which doesn't touch tokens, moved to #636. The branch stays for when FD-5472 comes back.

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