Skip to content

fix(api): move seat code tiers and inbox horizon, raise volume floors - #37

Draft
crewletbot wants to merge 24 commits into
mainfrom
crewlet-code-companion
Draft

crewletbot wants to merge 24 commits into
mainfrom
crewlet-code-companion

Conversation

@crewletbot

@crewletbot crewletbot commented Oct 26, 2026 •

Copy link
Copy Markdown
Collaborator

What & why

This is the companion of crewlet/crewlet#298, which hosts a company's git repositories inside the engine. That change moves several things this operator renders, copies, runs and links. Once it merges, the operator's next run against Crewlet's main fails without this pull request:

  • the render hands the engine keys the engine now refuses: a seat's GitHub tier, and the inbox horizon under the tracker;
  • every engine the suites boot refuses to start on the broker the operator gives it.

Land right after crewlet/crewlet#298, once main-image.yml has rebuilt ghcr.io/crewlet/crewlet:main. This repository's suites run that image (ADR-0002), and until then it is an engine without the code host. Between the two merges, a Company whose Seat sets a GitHub tier is held (Applied=False, EngineRefuses). It keeps running its last revision, in which the engine reads every seat's tier as read_only. Likewise, a Company that sets spec.tracker.worklet.inboxRetentionDays is held, and the revision it keeps running carries no horizon that engine reads, so a person's notices age out at Crewlet's default of a year. So the two land back to back.

What changes for someone running the operator

  • A seat's code tier is now spec.code.access. spec.integrations.github.tier is gone. Move its value to spec.code.access: ReadOnly (the default), Review or FullAccess. The API refuses spec.code on a Human seat. No release has shipped the old field (the API is v1alpha1 and there is no tag), so this is a plain move with no alias.
  • A company's inbox horizon is now spec.inbox.retentionSeconds. spec.tracker.worklet, which held only inboxRetentionDays, is gone: Crewlet now keeps one horizon for every notice in a person's inbox, whatever tracker the company runs, Jira or none included. Like this API's other durations, the new field counts seconds, in whole days, from 2592000 (30 days) to 315360000 (3650 days), such as 7776000 for 90 days. Unset is still Crewlet's default of a year. engineConfig may not set inbox. This too is a plain move with no alias.
  • Data volumes need more room. An EngineClass's data.storage.size must be at least 14Gi with objects.backend: NATS (was 12Gi) and 10Gi with S3 (was 9Gi). Two things need a manual step, and the installing and troubleshooting guides say how to do each:
    • A company created at the old floor stops booting on the new engine until its claim is grown.
    • An EngineClass stored below the new floor keeps handing its size to new Companies until it is raised.
  • The startup windows are shorter. A data pod's window is 1461 s, or 1601 s with S3 (was 1636 s and 1776 s). A seat pod's is 1137 s, or 1187 s (was 1142 s and 1192 s). The minimum data.startupSeconds follows, and so does every window derived from a volume.
  • A broker that stops answering no longer holds a data pod's boot until the startup probe restarts it. The engine now gives up a read of a state log after 30 seconds, where it used to ask again for as long as the boot lasted. A boot that cannot read where the fleet stands then fails by itself, and the pod's previous log ends on the read nobody answered. The troubleshooting guide says where to look.
  • A public address no sandbox can dial now holds the pods back with RuntimeReady=False, reason AddressRefused, naming the variable, rather than leaving them to crash-loop. The API refuses the cases it can see in a Company's publicBaseURL and in a class's webhooks.hostname.
  • engineConfig refuses code. This operator does not offer the engine's code host yet: no pod it runs has the git role or a repository cache.
  • A sandbox run's env may not name the variables Crewlet sets in a coding run itself. The API now refuses CREWLET_GIT_TOKEN, CREWLET_GIT_URL, CREWLET_GIT_TRAILER_SEAT, CREWLET_GIT_TRAILER_RUN and CREWLET_GIT_TRAILER_WORK_ITEM as a variable's name in a Seat's spec.sandbox.env and in a setup step's env (spec.sandbox.setup[].env, on a Company or a Seat). The engine refuses a company document that sets one, so such a Company or Seat was admitted and then held with EngineRefuses. Give a variable of your own, such as a token for another git host, another name. An LLMProvider's env is not affected.
  • A Seat's stop now says it also refuses its runs' writes. With spec.stopRunning, Crewlet ends the turn the seat is on and, until the seat is resumed, refuses every change its detached coding runs would make: a call through the tool bridge of any of the seat's tools not marked read-only, and a push from their boxes. Their reads still answer. The field's doc, status.pause.stopRunning, the Paused condition's message and the Drifted and PauseNotManaged texts now say so. Nothing in the API changes shape.

What each commit follows in the engine

  1. refactor(api): move a seat's code tier to spec.code.access
    • Engine: the change removes role.integrations.github.tier and refuses it as an unknown field. The tier is now role.code.access (read_only, review or full_access; unset is read only), and role.code is refused on a human seat. A new advisory, code.access <tier> grants nothing, is given when nothing applies the tier: the company has no enabled GitHub integration, or the seat has no integrations.github block.
    • Here: the Seat gets spec.code.access, rendered as role.code.access. GitHubAccessTier becomes CodeAccessTier, spec.integrations.github is rendered only for repos or app, and code joins the Human seat's CEL list of agent-only fields. A new code-access golden case holds the render to the advisory. The goldens, the sample Seat, and the credentials, integrations, company-document and troubleshooting pages follow.
  2. fix(api): raise the data volume floor to the engine's eight logs
    • Engine: the code log is the eighth state log, and every node reserves it at its 1 GiB floor whether or not the company hosts code. A fresh node now needs 8053063680 bytes for its logs, and 10737418239 with the NATS object bucket (was 6979321856 and 9305762474). It refuses to boot below that.
    • Here: the EngineClass floors follow (10Gi with S3, 14Gi with NATS). The engine harness declares 11274289152 bytes, three quarters of 14Gi. The e2e classes and the quickstart's class take 14Gi, and the quickstart asks for about 12 GiB free per company. A new CRD case states what the tightened rules do to a class stored below them.
  3. fix(controller): shorten the boot windows to the engine's new bounds
    • Engine: the join reads every log's trim floor once, by key, all at once (500 s becomes 70 s). The admission reads eight maintenance windows (40 s becomes 45 s). The object store's fleet record is agreed in casRetry attempts, a bound of attempts rather than time; the windows count the one attempt a boot meets unless someone removes the record (90 s becomes 85 s).
    • Here: dataPodBootSeconds and seatPodBootSeconds, the EngineClass's minimum data.startupSeconds, and the runtime and troubleshooting pages follow.
  4. test(render): read again the boot bounds the engine moved
    • Engine: the change moves 16 of the declarations the mirror table fingerprints, and removes two of them, (*FleetStore).Floors and retryRaces.
    • Here: the moved rows are read again and their digests recorded. (*FleetStore).Floor, readFloors and readFloorsOf take the place of Floors, and casRetry and fleetCASAttempts the place of retryRaces. trimFloor leaves the table, since only a write's fence reads it now. ReadWith, hedge and (*DomainLog).Stats, stateInfo and At join it, because a log's reads on the boot path are now asked again while the broker answers none. The decision texts that stopped being true are rewritten.
  5. fix(render): hold a pod back on a public address no sandbox can dial
    • Engine: at every boot, CREWLET_MCP_BRIDGE_URL and CREWLET_SANDBOX_OTEL_RECEIVER_URL are read through sandbox.ParseBoxURL (via engine.ReadBoxURLs). The boot is refused on a value a sandbox's WHATWG URL parser cannot read: an IPv6 zone, a %, <, > or ] in the host, or a numeric last label on a host that is not an IPv4 address.
    • Here: the render judges each address the controller hands a pod's sandboxes by that grammar, and the controller holds the pods back on one the engine refuses. The API rules on publicBaseURL and webhooks.hostname follow. The table mirrors ParseBoxURL, its host helpers and ReadBoxURLs. TestContractBoxURLShapes holds the render's verdict to what crewlet llm doctor reports.
  6. docs: follow the engine image's own git and the role it adds
    • Engine: the image builds git from the source release Crewlet pins and installs it under /usr/local, with no Debian git package. node.roles gains git, which /fleet counts as unmanned only for a company with code.native.
    • Here: the tool servers guide and the engine client's comments on a node's roles follow.
  7. fix(api): refuse code in engineConfig until the code host is offered
    • Engine: a new top-level code block declares the company's repositories, which only a node with the git role and a repository cache serves.
    • Here: no pod the operator runs has either, so the API refuses an engineConfig entry named code, with its own message. The overview and company-document pages list it as not offered.
  8. fix(controller): count the estate service's two lanes in the boot window
    • Engine: the change gives a push's proposals an estate lane of their own. Every node that serves the estate registers two lanes, the shared one and the proposals', one after the other, and each registration flushes its subscription within flushBudget, 10 s. A node with the git role also registers lanes for the git reads of its repositories.
    • Here: the data pod's window counted the estate service as one registration, as it was on Crewlet's main, so it fell 10 s short of the bounds it adds up. It now counts two. The git lanes add nothing, because the render gives no data pod the git role. A data pod's window becomes 1216 s, or 1356 s with S3 (1206 s and 1346 s after commit 3). A seat pod serves no estate and keeps 1137 s and 1187 s. The EngineClass's minimum data.startupSeconds, the largest volume whose derived window stays within a day (now 14197Gi), the test/crd cases, and the runtime and troubleshooting pages follow.
  9. test(render): read serveEstate again after the git lane
    • Engine: (*Engine).serveEstate hands the estate service a git half on a node with the git role, and estate.Serve registers the git lanes only when it has one.

    • Here: serveEstate is read again at that engine (crewlet/crewlet@adba06740) and its new digest recorded. Its decision text now says that it hands the service a git half only where the node has the git role, which no pod the operator runs has. The table now also reads how many registrations a boot step makes, down to the queue:

      • estate's Serve and lanes;
      • the queue's (*Queue).Serve and flush, which make each registration one flush;
      • eventfan's Serve, (*inboxRelay).serve and presence's (*Hub).Start, one call below the steps that arm the history answerer, the inbox relay and the presence answerer. Each registers one subject today, so the windows those steps take stand.

      The proposals' lane came in below serveEstate, whose own code did not change, so no digest moved for it. A registration added below a step now moves one. CONTRIBUTING.md says so where it explains the table.

  10. fix(controller): count a state log's boot reads at their new ceiling
    • Engine: the change now bounds every read it asks again while nobody answers it (crewlet/crewlet@a5bf06830). A state log's read of its state, and of its record at a sequence, ends at LookupBudget, 30 s, where its caller set no deadline. Before, it was asked for as long as its caller's context lived, and a boot's context has no deadline. A leader read on such a context keeps the client's 5 s DefaultTimeout. A certified listing's key index is now one page of the bucket's subjects from its leader, where it was two requests at that default.
    • Here: the data pod's window counted each of the ten reads of a state log on its boot at the 5 s of one request, for want of a bound to count: the join's read of every log's stats, eight one after the other, and the two reads that verify the last log's checkpoint past the consumers' window. It now counts each at the 30 s bound, and the join's listing of the nodes' positions at 20 s rather than 25. A data pod's window becomes 1461 s, or 1601 s with S3 (1216 s and 1356 s after commit 8): 245 s more. A seat pod reads no state log on its boot and keeps 1137 s and 1187 s. The EngineClass's minimum data.startupSeconds and its S3 rule, the largest volume whose derived window stays within a day (now 14156Gi), the test/crd cases, and the runtime and troubleshooting pages follow. The troubleshooting guide no longer says that only a restart ends a boot whose broker never answers.
  11. test(render): read the leader read and its bound again after the hedge
    • Engine: a leader read is now asked again while nobody answers it, as a state log's reads already were. (*Leader).get no longer applies the client's bound to its one request itself: it hands its reader's term and bound to ask and askTimed, which ask through ReadBoundedWith. The NATS bucket's lookup is held to Lookup by ReadWith rather than by a context of its own, its open gives a bucket's piece reads across a link a longer term, and a log's read of a record across a link is given its crossing beside the read term. None of these three moves a bound.

    • Here: five rows are read again at crewlet/crewlet@a5bf06830 and their new digests recorded: (*Leader).get, ReadWith, (*DomainLog).At, and the NATS bucket's open and lookupBucket. Their decision texts say what each decides now. Eleven rows join the table, so that every read the windows count is read down to the function that bounds it:

      • NewLeader, ask, askTimed and ReadBoundedWith, which hold a leader read to the client's 5 s;
      • (*Queue).budget, the broker's limit read, which is asked the same way and keeps 5 s;
      • (*Queue).provisioning and Clustered.Timing, whose Lookup bounds a state log's reads at 30 s;
      • keysUnder, (*Leader).Subjects, page and sorted, which make the listing's key index one page.

      LookupBudget's digest holds, and its text now names the reads it bounds. The table's own doc and CONTRIBUTING.md's account of it now read a step's reads down to the function that bounds them, and name the agreement of the object store's fleet record (casRetry) as the only wait the engine bounds by no time.

  12. test(render): read the positions listing again after the clock measure
    • Engine: each node's clock is now measured against its broker's, and clock_skew is raised where the two part (crewlet/crewlet@cbc031701). The measure reads the instant the broker stored a node's row in the fleet's positions register, which every row of the listing now carries: (*FleetStore).eachPositionKey, the walk of one class of the register, hands it on from the entry it already holds, and (*FleetStore).Positions sets it on the row. The listing makes no request it did not make before.

    • Here: (*FleetStore).Positions is read again at that engine and its new digest recorded. Its decision is unchanged, one certified listing, so the join's listing of the positions keeps its 20 s and no window moves. The change itself was made in the walk below Positions, which no row read: Positions' digest moved only because the walk's callback now takes the stored instant, and a request added there would have moved no digest at all. Ten rows join the table, so that the listing the windows count is read down to the functions that bound its waits:

      • from the walk to the certified listing: eachPositionKey, the fleet bucket's filtered walk ((*FleetStore).eachUnder and eachEntryUnder), and newLeaderReader, which addresses the leader without a request;
      • inside the listing: the pass's walk, stop, certify and close, and the read back of a key the pass did not deliver live, (*leaderReader).last and (*Leader).Last.

      The table's own doc and CONTRIBUTING.md's account of it now read the listing down to those functions.

  13. test(render): read the fleet store's key reads and create to the bounds
    • Engine: the clock measure moved nothing on this path. The windows' other reads of one key (a log's trim floor, its maintenance window and the object store's fleet record) cross the same bridge to the leader read as the listing's read back. Each round of the fleet record's create also writes before it reads: a settled update, which waits out the leader's answer that another write to the key is still in flight, but no longer than the client's 5 s DefaultTimeout where its caller set no deadline.

    • Here: eight rows join the table, read at the same engine:

      • (*FleetStore).get, getLatest and (*leaderReader).live, the store's read of a key's live value, which is one leader read at the client's 5 s;
      • (*FleetStore).create, which is one createKey (a row already) on the store's client;
      • settled, settledBucket.Update, settledBucket.settle and internal/jsinflight's Decide, which hold each round's write to that 5 s.

      Before them, a longer budget in Decide, or a second request in the update or the read, would have moved no digest, and left short the 85 s both pods count for the object store's fleet record. No window moves. That the objects bucket is settled is not read here, since Crewlet's own TestAConditionalWriteIsAnsweredByWhatTheLeaderDecided requires it. The table's own doc and CONTRIBUTING.md now name the leader read's bridge, and the settled write beside the create.

  14. feat(render): render the inbox horizon at the company's top level
    • Engine: the change makes a person's inbox the company's rather than the tracker's (crewlet/crewlet@7504ffc5e). Every part of the engine that writes a person notices keeps them under one set of rules, so the horizon that ages them out is the top-level inbox.retention_days (unset is 365, refused outside 30..3650), and the tracker's block, which held only that horizon, is gone. The engine refuses the old key as an unknown one, and no longer ties the horizon to the tracker a company runs.
    • Here: the Company's horizon moves from spec.tracker.worklet.inboxRetentionDays to a block of its own, spec.inbox.retentionSeconds, rendered as inbox.retention_days. The field is declared anew, so it counts seconds as this API's durations do: a rule refuses a count that is not whole days, and the render divides it by 86400. WorkletSettings, spec.tracker.worklet and the rule that admitted it on backend Worklet alone go, so a company on Jira, or on no tracker, keeps a horizon like any other. inbox joins the keys engineConfig may not set, and Diff's typedKeys, so an edit made to it through the engine is found as drift. A new render test, TestEngineConfigCannotSetAKeyAFieldRenders, holds three lists to one another, each both ways: the company document's top-level keys, typedKeys, and the keys the CRD's rule refuses in engineConfig, read off the rule as the CRD states it. The test/crd cases, the org-chart golden, the overview and company-document pages, the CRDs, the chart and the API reference follow.
  15. test(render): re-digest the engine's inbox relay registration
    • Engine: with the same change (crewlet/crewlet@1a61828c7), the inbox relay a data node arms serves every domain whose applier announces a person's movements. (*Engine).armInboxRelay hands it those domains (inboxSources) and the per-domain check of which of them the node's copy holds out of service ((*localEstate).outOfService), where it handed it the tracker copy's check alone, and the relay delivers the inbox's own movements, of any domain.
    • Here: armInboxRelay is read again at the engine change's branch (crewlet/crewlet@66e8ad593), against every file its row names, and its new digest recorded; the digest is the same from crewlet/crewlet@1a61828c7 on. Neither change is a request on the boot: the relay still serves every domain on its node's one subject, registered once through (*inboxRelay).serve, whose digest holds, and a node without data registers none. So a data pod's boot still counts the relay as one flush, no window moves, and the row's decision text stands.
  16. test(render): read the run_sandbox launch again after its code holds
    • Engine: a turn now holds each scratch repository of the company's code host that it names, so that the host's expiry of an idle one leaves it be while the turn may still use it (crewlet/crewlet@521bd300f). (*launcher).Launch, the run_sandbox launch, hands that set to the run row it opens. It seals the set from its read until the row exists, so that a hold another call of the turn takes meanwhile waits; then the row keeps it, and every later hold reaches the row before the request it is taken for. When the launch fails, it unseals the set or detaches it from the row (launchHolds). The one refusal the holds add is a set past the most one run may carry (sandbox.MaxCodeHolds, 1024). Every node also answers for its running turns' holds (armCodeHolds), one more registration on each pod's boot.
    • Here: Launch is read again at the engine change's pushed head (crewlet/crewlet@4ee3f06a5), against every file its row names, and its new digest recorded; the digest is the same from crewlet/crewlet@521bd300f on. The launch still refuses, before a box exists, what it refused before. Only the repositories a turn names reach the new refusal, and a company the operator runs hosts no code, since its engineConfig may not set code (commit 7), so its turns hold nothing. No verdict, report or page moves, and the row's decision text stands. The windows need not count armCodeHolds: it registers past the recovery boundary, after /health answers 200, beside the seats' memory reads and the coding runs' live output, which they do not count either.
  17. test(render): read the agent-mode launch again after the bridge's model
    • Engine: each code write a seat makes now names the model the call was made on, and a call an agent-mode run makes through the tool bridge has no round of the engine's tool loop to take its model from. So (*agentLauncher).LaunchExecutor resolves the run before it opens the run's session on the bridge, and the session carries the model the launch points the coding CLI at (mcpbridge.Session.Model, crewlet/crewlet@86267fe2c). It still refuses a node with no bridge before it assembles the run's environment, now by asking whether it has one, in the words the bridge's empty endpoint was refused in (errNoBridgeURL), and it opens no session for a run executorRun refuses. It also seals the turn's code holds to the run row it opens, as the run_sandbox launch does (crewlet/crewlet@521bd300f), so its digest moved with each of the two changes.
    • Here: LaunchExecutor is read again at the same head, against every file its row names: the render's executor verdict, its NoProvisioner report, the config reconciler's report of an empty start value, the LLMProvider API's rule on a CLI of mode Agent, the CLI agents guide and the CRD suite's reading of the engine's sign-in verdicts. Its new digest, the same from crewlet/crewlet@86267fe2c on, is recorded. An agent-mode turn on a node with no tool bridge is still refused, never run on the engine's own tool loop, and so is one whose CLI login cannot follow it into the remote sandbox. A value of the run's environment that resolves to nothing still leaves the run to start without it. The session's model is the one the run is handed, so one model still serves the executor's run and every other phase, as the guide says. The row's decision text stands. Both launch rows now record the engine they were read at as codeHoldsRead; standingInstructionsRead, which no row reads any more, goes, and codeHoldsRead's account carries what it said.
  18. fix(api): refuse the variables Crewlet sets in a coding run itself
    • Engine: a run_sandbox call that names repositories on the company's own code host gives the run its credential there, the base its clones and pushes reach, and the values its commit hook writes into each commit's trailers, in the launch's own environment and never on the run's row (ADR-0063). Crewlet now refuses a company document that sets any of those names (config.ReservedSandboxEnv: CREWLET_GIT_TOKEN, CREWLET_GIT_URL, CREWLET_GIT_TRAILER_SEAT, CREWLET_GIT_TRAILER_RUN and CREWLET_GIT_TRAILER_WORK_ITEM) in every env a run's environment is assembled from: a seat's sandbox.env, its setup steps' env, and providers.sandbox.setup's.
    • Here: the operator renders all three, so a Seat or a Company that set one was admitted and then held back. The API now refuses each name at the variable's own name: a rule on SeatEnvVar.name (a Seat's spec.sandbox.env) and on the new SandboxEnvVar.name, the item type of a setup step's env, which took SecretEnvVar before. SandboxEnvVar has SecretEnvVar's shape and is a type of its own because an LLMProvider's env, which SecretEnvVar types, is no env a run's environment is assembled from, so the engine reserves nothing there. Each rule sits on the item's name at constant cost (17 per name), never as a list-wide rule on the step: the setup block's rules already cost 6 582 608 of the 10 000 000 a CRD's rules may. test/crd tries each rule in each of the three places (TestReservedRunVariablesValidation: 15 refusals and 21 admissions), and TestReservedRunVariablesFollowTheEngine holds them to the engine both ways: the names the engine's source reserves, the names the rules state, and names beside them are refused by the API exactly where crewlet validate refuses the document the operator renders. The credentials guide, the CRDs, the chart and the API reference follow.
  19. test(render): read the box address reads again after the code host's
    • Engine: a boot reads a third address a box dials back to the engine, CREWLET_BOX_GIT_URL (sandbox.BoxGitURLVar), the base a coding box reaches the company's code host at. It reads it through ParseBoxURL, as it reads the tool bridge's and the telemetry receiver's, and refuses to start on a value that is set and unusable.
    • Here: ReadBoxURLs and BoxURLs.All are read again at the engine change's pushed head (crewlet/crewlet@2887e830c), against every file their rows name, and their new digests recorded. The operator hands its pods the bridge's and the receiver's addresses alone. It does not offer the code host, so it sets no third, and the API refuses every CREWLET_ variable a Company or a class gives the engine's pods. So no boot of its meets the third address, and no verdict moves. The rows' decision texts name it, and render's address constants say why the operator sets none. On review, the harness's BoxURLVerdicts doc, which still said the engine reads two addresses, now names the three, and the rows' list of operator copies (boxURLCopies) now names the harness, so the next re-read reaches it.
  20. test(render): read the run_sandbox launch again after its repositories
    • Engine: (*launcher).Launch resolves the repositories a run_sandbox call names on the company's own code host after every refusal a document foresees and before the turn's holds are sealed for the row, and hands a run given some its credential, its git base and git's configuration in the launch's own environment (ADR-0063).
    • Here: Launch is read again at the same head, against every file its row names, and its new digest recorded, read at runReposRead. A company the operator runs hosts no code (its engineConfig may not set code, commit 7), so no call of its seats names a repository. The head a launch runs on, its coding agent, every other refusal before a box exists, and the sign-in that must reach the box are as at codeHoldsRead. The variables that environment carries are the ones commit 18 refuses. No verdict, report or page moves.
  21. docs(api): say a stop also refuses a detached run's writes
    • Engine: a person's stop now reaches the coding runs a seat has detached, which outlive their turn and run on through a pause. Until the seat is resumed the engine refuses every change such a run would make, naming who stopped the seat: a push its box makes to the company's code host, and a call it makes through the tool bridge of any of the seat's tools not marked read-only (mcpbridge.Session.Stopped, crewlet/crewlet@0645a8eb5), answered paused. Its reads still answer, and a pause without stop_running stops new work only.
    • Here: Seat.spec.stopRunning said only that a stop ends the turn the seat is on. Its doc and the status field reporting it now say what else it refuses, and that this lasts until the seat is resumed, which is why unsetting the field changes nothing. The applying page's account of seat pauses says the same. The operator offers no code host, so for a company it runs the stop reaches the bridged writes; the doc names the pushes too, as the engine's own stop_running does. The Seat CRD, the chart's copy and the API reference are regenerated.
  22. test(render): read the agent-mode launch again after the bridge's stop
    • Engine: (*agentLauncher).LaunchExecutor now opens its run's session on the tool bridge through bridgeSession, which sets the fields it set before plus the seat's stop check, stoppedBy, the same check the code host refuses a run's box's pushes by (crewlet/crewlet@0645a8eb5). So its digest moved.
    • Here: the row is read again at the engine change's pushed head (crewlet/crewlet@ab6d97e67, bridgeStopRead), against every file it names: the render's executor verdict, its NoProvisioner report, the config reconciler's report of an empty start value, the LLMProvider API's rule on a CLI of mode Agent, the CLI agents guide and the CRD suite's reading of the engine's sign-in verdicts. The launch refuses what it refused before, in the same order: a node with no bridge, and a CLI login that cannot follow the run into the sandbox. bridgeSession always sets the check, so the bridge's new refusal of a session without one is never met. The row's decision text stands and its new digest is recorded. No row reads at codeHoldsRead any more, so it goes, and bridgeStopRead's account carries what it said.
  23. fix(controller): say a stopped seat's runs' writes are refused
    • Engine: the same stop as commit 21. The engine's own pause_seat says both halves: the turn ends, and the detached runs' writes are refused.
    • Here: the Paused condition's message said only that the turn was stopped, and the Drifted and PauseNotManaged texts asked for a seat "paused, its running turn stopped". The condition now adds that the seat's detached coding runs' writes are refused until it is resumed, and the asked-for state reads "paused, its running turn stopped and its runs' writes refused". TestAStoppedSeatSaysItsRunsWritesAreRefused holds both texts to the stop, and holds a pause without one to naming neither.
  24. docs(engine): say a seat's stop also refuses its runs' writes
    • Engine: the same stop.
    • Here: the engine client's SeatPause.StopRunning and PauseSeat docs said a stop only ends the turn the seat is on. They now name the refused writes too: a call of the seat's tools that writes, and a push from its runs' boxes.

How it was checked

What ran, in a container with Go 1.27.2.

At b4cc2c1, for commits 1 to 7:

  • go test -race -count=1 over the packages that need no envtest assets, cluster or engine image:

    • internal/render: the goldens, TestStreamFloorFiguresAreTheEngines, TestTheDocsStateTheBases, TestBootstrapRefusesABoxURLNoSandboxCanDial, TestAMirroredDeclarationThatMovedFails and TestEveryFindingHasABasis;
    • internal/controller, without the envtest suite TestControllers: the startup-window tests among them;
    • internal/engine and its harness, internal/secretsync, internal/secretref, test/docs, test/enginecommit and test/envtestbin.

    Every test in those packages that failed did so only because the engine binary was absent. A script checked each failing test's output for that.

  • Static checks: gofmt; go vet in both modules, and with -tags e2e over ./test/e2e/; go build ./...; make manifests generate chart api-docs, which leaves the tree unchanged; and the sign-off gate.

  • A mutation check: with the box grammar's refusal of a numeric last label turned off, TestBootstrapRefusesABoxURLNoSandboxCanDial fails.

  • A throwaway test, not committed, that ran the new engineConfig rule through the API server's CEL validator against the generated CRD. code is refused with its message, and nearby names are admitted.

  • A digest scan of all 162 mirror rows with enginetest.Fingerprint. Every row matches the engine: 159 at the engine change's head (15c2f1a96), and the other three at Crewlet's main (fix(engine): read secrets and build the company after a node recovers crewlet#311).

At 1306e5c, after commits 8 and 9:

  • go test -race -count=1 over internal/controller without the envtest suite TestControllers, and over internal/render and test/docs. In those packages 834 tests and subtests pass, and every one that fails does so only because the engine binary is absent: a script checked each failing test's output for that. Among those that pass:
    • TestDataPodStartupSeconds and TestStartupFailureThreshold;
    • TestStartupSecondsFloorIsTheBootWindow, which holds the CRD's data.startupSeconds floors to the derived window;
    • TestStartupWindowFiguresAreTheControllers, which holds every page and comment that quotes a window to it;
    • TestAMirroredDeclarationThatMovedFails, and the docs gate's TestRelativeLinksResolve and TestEveryPageIsInTheIndex.
  • Static checks: gofmt over the changed Go files; go vet over internal/controller, internal/render and test/crd, and over the api module; and make verify-generated, which leaves the tree unchanged.
  • A digest scan of all 169 mirror rows with enginetest.Fingerprint, over the engine's git objects:

At 35ed36e, after commits 10 and 11 (the tests ran on its tree; the two commit messages were reworded afterwards, which left the tree as it was):

  • go test -race -count=1 over:

    • internal/controller, without the envtest suite TestControllers and TestExecutorFollowsTheEngineSource, which reads the engine's source. Among the passes: TestDataPodStartupSeconds, TestSeatPodStartupSeconds, TestStartupFailureThreshold, TestStartupSecondsFloorIsTheBootWindow and TestStartupWindowFiguresAreTheControllers;
    • internal/render: 591 tests and subtests pass, and each of the 531 that fail does so only because the engine binary is absent (a script checked each failing test's output for that), TestMirroredEngineDeclarationsAreUnchanged and the contract tests among them. Among the passes: TestAMirroredDeclarationThatMovedFails, TestStreamFloorFiguresAreTheEngines, TestTheDocsStateTheBases, TestCompanyGolden, TestBootstrapGolden and TestEveryFindingHasABasis;
    • test/docs, without TestEngineLinksResolve, which needs the engine binary. The one engine heading it would check, "The embedded broker's own logs" in Crewlet's deployment guide, exists at the engine change's pushed head and on Crewlet's main;
    • internal/secretref, test/enginecommit and test/envtestbin.

    After the rewording, the window tests and TestAMirroredDeclarationThatMovedFails ran again and pass.

  • A mutation check: with the old terms restored in dataPodBootSeconds, TestDataPodStartupSeconds fails.

  • Static checks: go vet over internal/controller, internal/render, test/crd and test/docs, and over the api module, which compiles the edited envtest cases without running them; go build ./... in both modules; gofmt over the changed Go files; make manifests generate chart api-docs, which leaves the tree unchanged; and the sign-off gate over all eleven commits.

  • A digest scan of all 180 mirror rows with the table's own check, over the engine's git objects:

    • the 16 rows commit 11 reads at crewlet/crewlet@a5bf06830, five read again and eleven new, match it, and still match the engine change's pushed head, crewlet/crewlet@b5dd4bb53;
    • at that head, 175 of the 180 rows match. The other five are under "Expected red" below.

At ab3fcc8, after commits 12 and 13:

  • go test -race -count=1 over:
    • internal/render, at ab3fcc8: TestAMirroredDeclarationThatMovedFails, TestCompanyGolden, TestBootstrapGolden, TestEveryFindingHasABasis, TestTheDocsStateTheBases and TestStreamFloorFiguresAreTheEngines pass. The whole package ran on commit 13's first version, before its last four rows were added on review: 591 tests and subtests pass, and each of the 531 that fail does so only because the engine binary is absent (a script checked each failing test's output for that);
    • test/docs, without TestEngineLinksResolve, which needs the engine binary: 7 of 7 pass.
  • Static checks: gofmt and go vet over internal/render and test/docs, and the sign-off gate over all thirteen commits. No type, marker or generator input changed, so there was nothing to regenerate.
  • A digest scan of all 198 mirror rows with the table's own check, over the engine's git objects:
    • the 19 rows commits 12 and 13 read at crewlet/crewlet@cbc031701, Positions again and 18 new, match it, and still match the engine change's pushed head, crewlet/crewlet@b5dd4bb53;
    • at that head, 194 of the 198 rows match. The other four are under "Expected red" below.
  • Two mutation checks, on copies of that head's engine sources:
    • a second listing in (*FleetStore).eachUnder and a second read in getLatest moved no row the table held before these two commits (Positions, Floor, Maintenance and AgreeObjectBackend still matched), and the new rows for those two functions catch them;
    • the create's write bound lengthened in four places (settled's timeout doubled, a read back added in settledBucket.Update, a second decide in settle, Decide's fallback doubled) moved none of the other 194 rows, and each of the four rows for those functions catches its own.

At a7d9b93, after commits 14 and 15:

  • go test -race -count=1 over internal/render and test/docs: 599 tests and subtests pass, and each of the 556 that fail does so only because the engine binary is absent (a script checked each failing test's output for that), TestMirroredEngineDeclarationsAreUnchanged, TestCredentialPositionsAreTheEnginesMarks, the contract tests and TestEngineLinksResolve among them. Among the passes: TestEngineConfigCannotSetAKeyAFieldRenders, TestCompanyGolden, TestCompanyLeavesItsInputAlone, TestBootstrapGolden, TestAMirroredDeclarationThatMovedFails, TestTheDocsStateTheBases, TestEveryFindingHasABasis and TestStreamFloorFiguresAreTheEngines, and the docs gate's TestRelativeLinksResolve and TestEveryPageIsInTheIndex.
  • Static checks: gofmt over the changed Go files; go vet over internal/render, test/crd and test/docs, and over the api module, which compiles the edited envtest cases without running them; make verify-generated, which leaves the tree unchanged; and the sign-off gate over all fifteen commits.
  • A digest scan of all 198 mirror rows with the table's own check, over the engine's git objects: at the engine change's pushed head, crewlet/crewlet@92c5022f2, and at crewlet/crewlet@66e8ad593, where commit 15 reads armInboxRelay, 195 of the 198 rows match, armInboxRelay among them. The other three are under "Expected red" below.

At 4df7827, after commits 16 and 17:

  • go test -race -count=1 over internal/render, the whole package: 592 tests and subtests pass, and the 555 that fail do so only because the engine binary is absent, TestMirroredEngineDeclarationsAreUnchanged among them. Each of the 531 failing leaf tests says so in its own output (a script read each), and nothing fails for any other reason. TestAMirroredDeclarationThatMovedFails passes, and passed again on review.
  • Static checks: gofmt over the changed file at each commit, and over internal/render on review; go vet over internal/render; and the sign-off gate over all seventeen commits. No type, marker, generator input or page changed, so there was nothing to regenerate.
  • A digest scan of all 198 mirror rows with the table's own check, over the engine's git objects: at the engine change's pushed head, crewlet/crewlet@4ee3f06a5, 195 of the 198 rows match, Launch and LaunchExecutor among them. The other three are under "Expected red" below. Launch's new digest holds from crewlet/crewlet@521bd300f to that head, and LaunchExecutor's from crewlet/crewlet@86267fe2c. At crewlet/crewlet@a2634a307, where both rows were read before, the same check gives back their old digests.

At 2b1e20e, after commits 18 to 20 (the engine change's pushed head, crewlet/crewlet@2887e830c, read from git objects copied into a temporary directory and deleted after):

  • make manifests generate api-docs chart, whose output is committed; go build ./... and go vet in both modules, which compiles test/crd and internal/controller; and gofmt.
  • A throwaway check, not committed, that loaded all eight CRDs through the API server's ValidateCustomResourceDefinition: no errors. It also measured the new rule's cost: 17 per name, which is 8 704 on a setup step's env items and 544 on a Seat's spec.sandbox.env. The setup block's other rules stay at 6 582 608 of 10 000 000.
  • go test -race -count=1 ./internal/render/, at commit 20's first version: 78 top-level tests pass, and each of the 40 that fail does so only because the engine binary is absent. TestAMirroredDeclarationThatMovedFails passes. The three rows these commits record match the engine at that head: (*launcher).Launch, ReadBoxURLs and BoxURLs.All.
  • Read against that head: ReservedSandboxEnv names exactly the five variables. The engine's validateReservedSandboxEnv refuses them in providers.sandbox.setup[i].env, roles[i].sandbox.env and roles[i].sandbox.setup[j].env, the paths the test/crd cases expect. ADR-0063 and the heading the credentials guide links exist. Launch still assembles the run's environment, the coding run and the sandbox's credentials before it resolves the repositories and asks the coordinator to launch.
  • After the review's fix to commit 19 (a doc comment and one entry in the rows' list of operator copies): gofmt and go vet over internal/engine/enginetest and internal/render. TestAMirroredDeclarationThatMovedFails and go test ./internal/engine/enginetest/ did not run again: their run waited more than 30 minutes behind other jobs on the shared runner and was cancelled. The fix changes no digest, decision text or behaviour.
  • The sign-off gate over all twenty commits.

At 9341b49, after commits 21 to 24 (the engine change's pushed head, crewlet/crewlet@ab6d97e67, read from source extracted into a temporary directory and deleted after):

  • A throwaway test, not committed, ran all 198 mirror rows against that head: 191 match, LaunchExecutor among them, with the new digest commit 22 records. The other seven are under "Expected red" below. Between the previous read (crewlet/crewlet@4ee3f06a5) and that head, LaunchExecutor moved only with crewlet/crewlet@0645a8eb5.
  • make manifests generate chart api-docs, whose output is committed; gofmt over api, internal and test; go build ./...; go vet over internal/controller, internal/engine and internal/render, and in the api module.
  • go test -count=1 ./internal/render/, at commit 22: 78 top-level tests pass, and the 40 that fail do so because the engine binary is absent (21 checked directly; the other 19 are parents of engine-contract subtests).
  • go test -race -count=1 over the controller's pause unit tests at 9341b49: the new TestAStoppedSeatSaysItsRunsWritesAreRefused (a stop, and a pause only), TestAnOwnedPauseFollowsTheRecordNotTheRoster, TestAPauseThatCannotBeMadeIsAskedForAgainSoon and TestASeatTheEngineDoesNotRunHasNoPause pass.
  • A mutation check: with config_pause.go put back to its old wording, the new test fails on both the condition's message and the asked-for state.
  • Read against that head: the engine's agent-runtime page, "Pausing a seat", states what commits 21, 23 and 24 say a stop refuses. No mirror row names config_pause.go or the engine client's seats.go.
  • Every commit carries its sign-off, checked over all twenty-four.

What did not run here, left to CI: test/crd and the controller's envtest suite, TestReservedRunVariablesValidation and TestReservedRunVariablesFollowTheEngine among them; the engine-backed tests (the contract tests, TestMirroredEngineDeclarationsAreUnchanged, the engine client and secretsync); test/chart and test/e2e; make lint and helm-lint; and make check as a whole.

Expected red until the engine change lands

Checklist

  • make check passes. Not yet: its unit and static parts ran here (above). The envtest, engine and chart suites and lint run in CI, which needs feat(code): host the company's git repositories inside the engine crewlet#298 merged and the main image rebuilt first.
  • Tests added or updated; an API change comes with an envtest case that
    the API server enforces it (a CEL rule refuses what it must refuse). Every new rule has a test/crd case, which runs in CI.
  • Generated files regenerated and committed:
    make manifests generate chart api-docs
  • Docs updated (docs/) for any change to the API, behaviour, the
    deployment or a make target
  • Credentials stay references to Secrets; no field accepts a secret
    value, and no value reaches a pod template, a status, an event or a log. The engine change adds and removes no credential position.
  • A dependency, tool, image or action that was added or bumped is on its
    newest release and pinned exactly, or carries the reason it is held
    back at the pin (none was)
  • Every commit is signed off — git commit -s; CI's sign-off job fails
    without it
  • Commit subjects are type(scope): summary
  • The pull request title takes the same form and reads as a release note:
    it is what appears in the generated release notes

🤖 Generated with Claude Code

Crewlet moves a seat's code access tier out of its GitHub identity
(crewlet/crewlet#298): role.integrations.github.tier is gone, and the
tier is role.code.access, one setting for every code host the engine
drives, which a seat's GitHub App is created with and mints every token
at. The engine refuses the old key as an unknown field, so a Company
whose Seat set spec.integrations.github.tier would be held, and the
revision it keeps running reads every tier as read_only.

So the tier moves to the Seat's spec.code.access (ReadOnly, the default,
Review or FullAccess), rendered as role.code.access; GitHubAccessTier is
CodeAccessTier, and spec.integrations.github renders only for an app or
repositories. The API refuses spec.code on a Human seat, as Crewlet
does: a person acts as their own identity on every host, so a tier would
cap nothing.

Crewlet warns that a tier grants nothing where nothing applies it: a
company with no GitHub Integration, or a seat without
spec.integrations.github, since the operator never asks the engine to
host code of its own. The new code-access case holds the render to that
advisory, and the company-document page and the troubleshooting guide
say it. The goldens, the sample Seat and the credentials guide follow,
and the integrations guide points at the field.

Signed-off-by: Seena Fallah <seena.fallah@ionos.com>
Crewlet's code host adds an eighth state log, CREWLET_CODE_LOG, which
every node reserves at its 1 GiB floor whether or not the company hosts
code (crewlet/crewlet#298). A fresh node now reserves at least
8053063680 bytes for its logs, and 10737418239 with the object store's
bucket beside them, where it reserved 6979321856 and 9305762474, and it
refuses to boot on a stream smaller than that.

The stream is three quarters of the data volume, so the EngineClass
floors on data.storage.size move from 9Gi to 10Gi with the files in S3
(a share of 7.5Gi) and from 12Gi to 14Gi with them in the stream's
bucket (10.5Gi; 13Gi gives 10468982784 bytes, short of the floor). The
engine harness declares a broker of 10.5Gi, three quarters of 14Gi, the
e2e classes and the quickstart's class take 14Gi, the quickstart asks
for about 12 GiB free for each company where it asked for 10, and the
CRD suite's cases follow the floors.

A company made at the old floor stops booting on the engine that adds
the log: its existing logs keep their ceilings, and the new one finds no
room beside them. The troubleshooting guide names that refusal and has
its claim grown. An EngineClass stored below the new floor keeps its
size and hands it to every Company created under it, so the installing
and troubleshooting guides and DataStorage.size's doc tell a platform to
raise it, and say what the API does meanwhile, which a new CRD case
holds: it stores a class of each kind under lower floors and has the API
server refuse a change to the spec of the one in the stream's bucket, a
spec-wide rule, but spare the S3 one any change that leaves its size as
it was.

Signed-off-by: Seena Fallah <seena.fallah@ionos.com>
crewlet/crewlet#298 changes what a pod's boot waits on:

- The join reads every state log's trim floor once, each by its key and
  all at once, where it listed the fleet's floors once for the join and
  once more for each log. Its reads of where the fleet stands go from
  500 s to 70 s: the positions' listing, 25 s; the floors, 5 s; and each
  of the eight logs' stats, 5 s.
- The code host's log is the eighth state log, so a data pod's admission
  reads eight maintenance windows: 40 s becomes 45 s.
- The object store's fleet record is agreed in casRetry's attempts, a
  bound of attempts rather than time. An attempt is lost only when
  someone removes the record during the boot, so the windows count the
  one attempt a boot otherwise meets, 85 s, where they counted 90 s.
- A log's stats, and the reads that verify its checkpoint, are asked
  again while the broker answers none, on the boot's context, which has
  no deadline. Each is still counted at the 5 s term of one request.

A data pod's boot is now 1206 seconds with its objects on the event
stream and 1346 in S3 (was 1636 and 1776), and a seat pod's 1137 and
1187 (was 1142 and 1192). The EngineClass floors follow (a minimum
data.startupSeconds of 1206, and 1346 with objects.backend S3), so a
class may name a window 430 seconds shorter than before, and the
largest volume whose derived window is a day is now 14199Gi. The runtime
and troubleshooting pages quote the new figures and say how a state
log's read is counted. The mirrors the windows are read from follow in
the next commit.

Signed-off-by: Seena Fallah <seena.fallah@ionos.com>
crewlet/crewlet#298 moves sixteen declarations the boot windows are read
from; fifteen are read again here against the windows of the commit
before, at the head of its branch (codeHostRead), and mcpbridge.Build
follows with the box URLs.

Two are gone, and what decides in their place is named:

- (*FleetStore).Floors: the join reads every log's trim floor once, each
  by its key and all at once, through (*stateLog).readFloors,
  readFloorsOf and (*FleetStore).Floor. trimFloor, which listed the
  floors once more for each log, goes from the table: only a write's
  fence reads it now, so no boot waits on it.
- (*FleetStore).retryRaces: a fleet write is attempted again in
  casRetry's attempts, at most fleetCASAttempts of them, bounded by
  their number rather than by the client's 5 s default.

The decision texts that were no longer true say what the engine does
now: AgreeObjectBackend, fleetGenerations, replayable and
objectsReservation. The rest moved no bound: openObjects, start,
serveFleetBroker, armSteer, the queue's attach and the nats bucket's
open, lookup and create keep their decisions under new digests.

A log's stats and its record at a sequence used to be one request at
the client's 5 s default, and are now asked again while the broker
answers none, for as long as the caller's context lives, which a boot's
has no end of. No mirrored declaration showed that, since the change
was in the domain log's reads, so the table now names them:
(*DomainLog).Stats, stateInfo and At, and jsprovision's ReadWith and
hedge. The table's own doc and CONTRIBUTING's account of it say which
waits the engine bounds by no time and how the windows count them.

Read at this head, the join, startStateLog and bootSecrets are older
than their rows, which follow crewlet#311 on Crewlet's main: the branch
takes main before it merges, and they are unchanged on it.

Signed-off-by: Seena Fallah <seena.fallah@ionos.com>
Crewlet now reads the addresses a pod hands its sandboxes,
CREWLET_MCP_BRIDGE_URL and CREWLET_SANDBOX_OTEL_RECEIVER_URL, through a
grammar of their own at every boot (sandbox.ParseBoxURL, through
engine.ReadBoxURLs), and refuses to start on a value that is set and
that the URL parser of a sandbox's coding agent, the WHATWG URL
Standard's, cannot read (crewlet/crewlet#298): an IPv6 zone, a host
holding a '%', '<', '>' or ']', one that is not UTF-8 once its escapes
are decoded, or one whose last label is a number and that is no IPv4
address. The operator hands the Company's
status.endpoints.publicBaseURL as both, so such an address now
crash-loops the data pod, and a seat pod too while the class sends
telemetry.

The render judges each address the controller hands a pod's sandboxes
(BootstrapInput.BoxURLs) by that grammar beside the addresses of its
crewlet.yaml, and the controller holds the pods back on one Crewlet
refuses, with RuntimeReady=False AddressRefused naming the variable and
where the address came from. The API refuses what it can see: a
publicBaseURL whose host holds a '%' (the zone among them), a '<', a
'>' or a ']', or ends in a number and is not an IPv4 address written as
four decimal octets, and a class's webhooks.hostname whose last label is
a number. A last label a variable fills is a number only for some
Companies, which the render decides per Company.

The table mirrors ParseBoxURL and the helpers that judge its host, and
ReadBoxURLs, against the files that copy them; mcpbridge.Build follows
under a new digest, its parenthetical no longer true: the bridge's base
is ParseBoxURL's answer, not Configured's test. TestContractBoxURLShapes
holds the render's verdict to the engine's, which `crewlet llm doctor`
reports as its boot reads it, and test/crd's
TestAddressShapesFollowTheEngine holds the API's rules on a
publicBaseURL to the same verdict. ErrAddressRefused now says the
address may be in the pod's environment as well as its crewlet.yaml.

Signed-off-by: Seena Fallah <seena.fallah@ionos.com>
Crewlet's image no longer carries Debian's git package: it builds git
from the source release Crewlet pins (scripts/git-pin.env) and installs
it under /usr/local (crewlet/crewlet#298). The tool servers guide said
the image was Debian with git among its packages; it now says where the
image's git comes from, and still that it has no Python, Node.js, uv,
npm or curl, which is what a derived image is for.

The same change adds a fifth node role, git, which the engine counts as
unmanned only for a company that hosts its repositories in the engine
(code.native), since /fleet's unmanned_roles now lists only the roles
the company needs. The engine client's comments on a fleet node's roles
and on the unmanned roles named the four roles alone; they now name the
fifth and say that no pod the operator runs holds it.

Signed-off-by: Seena Fallah <seena.fallah@ionos.com>
Crewlet's code host keeps a company's git repositories in the engine,
declared under a new top-level block of the company document, code
(native, repos, limits, deleted_retention_days; crewlet/crewlet#298).
engineConfig passed it through, since it holds no credential and no
field of this API owns it, and the engine takes it. But the host serves
the repositories only from a node with the git role and a cache of them
on its volume, and no pod the operator runs has either: a Company that
set it would run a document declaring repositories nothing can clone or
push, with /fleet listing git as an unmanned role and nothing saying so
on the Company.

So the API refuses an engineConfig entry named code, with a message of
its own, until the operator offers the code host as a field of its own,
with the pods and volumes it needs. The CRD suite holds the rule, and
the overview and company-document pages say it is not offered.

Signed-off-by: Seena Fallah <seena.fallah@ionos.com>
A data pod's engine serves the replicated estate to the rest of the
fleet on two lanes since crewlet/crewlet#298 gave a push's proposals a
lane of their own: estate.Serve registers the shared lane and the
proposals' one after the other, and each registration flushes its
subscription within flushBudget, 10 s. The data pod's window counted
the estate service as one registration, as it was on Crewlet's main
when the window was first read, so it fell 10 seconds short of the
bounds it lays end to end. The second lane came in below
(*Engine).serveEstate, whose own code did not change, so no mirrored
digest moved with it.

The same branch now registers a third lane, for its git reads, on a
node with the git role. render.Bootstrap gives no data pod that role,
so it moves no window, and the window says so.

A data pod's boot is now 1216 seconds with its objects on the event
stream and 1356 in S3 (was 1206 and 1346); a seat pod serves no estate
and keeps its 1137 and 1187. The EngineClass floors follow (a minimum
data.startupSeconds of 1216, and 1356 with objects.backend S3), the
largest volume whose derived window is within a day is now 14197Gi,
and the runtime and troubleshooting pages quote the new figures. The
mirrors the count is read from follow in the next commit.

Signed-off-by: Seena Fallah <seena.fallah@ionos.com>
crewlet/crewlet#298 gives the estate service a lane for the git reads of
a company's repositories, which a node with the git role answers on:
serveEstate hands the service a git half on such a node, and
estate.Serve registers the git lanes beside its others only then. Read
at that engine (gitLaneRead), serveEstate takes its new digest, and its
decision text says what it decides now: a data node registers its
estate service, with a git half only where it has the git role, which
no pod the operator runs has.

Reading it against the windows found the count the previous commit
corrects: the service registers its lanes one after the other, each one
flush, and every data node registers two, the shared lane and the
proposals', where the windows counted one. The proposals' lane came in
below serveEstate, so its digest stayed put and nothing sent anybody to
read it. The table now names what decides that count: estate's Serve,
which registers each lane in turn, and lanes, which every node that
serves the estate registers; and the queue's Serve and flush, which make
every registration one flush within flushBudget.

The same gap was open under three of the other registrations the
windows count, a data pod's four and a seat pod's two. armHistory,
armInboxRelay and armPresence each leave their registration to a
function one call below them, eventfan's Serve, (*inboxRelay).serve and
presence's (*Hub).Start, which no row read, so a second subject
registered in any of them would have moved no digest. Each registers
one subject at this engine, so the windows' 40 and 20 seconds stand, and
the three are rows now, read at gitLaneRead. The steer answerer and the
fleet broker register in their own step's code, which their rows read
already. So every step that registers what a node answers is read down
to the queue, as the table's own doc and CONTRIBUTING's account of it
now say.

codeHostRead's note said that nothing else which moved at that engine
moved a bound. The change there that gave the registrations their
answer counts also brought the proposals' lane, so the note now says
that this one bound moved, outside the declarations it read.

Nine more rows differ at this engine, none of them read here. Six moved
with #298's changes to the leader read, its hedge and the fleet's
positions ((*Leader).get, ReadWith, (*DomainLog).At, the nats bucket's
open and lookupBucket, and (*FleetStore).Positions). Three read at
crewlet#311's engine ((*Engine).bootSecrets, (*Engine).join and
(*Engine).startStateLog) differ because #298's branch does not hold that
change of Crewlet's main yet.

Signed-off-by: Seena Fallah <seena.fallah@ionos.com>
crewlet/crewlet#298 now bounds every read Crewlet asks again while
nobody answers it (crewlet/crewlet@a5bf06830). A read asked that way
from the start, which a state log's read of its state and of its record
at a sequence are, is held to LookupBudget, 30 s, where its caller set
no deadline. Before, it was asked for as long as its caller's context
lived. A boot's context sets no deadline, so each such read on a data
pod's boot now ends at 30 s: the join's read of every log's stats,
eight one after the other, and the two reads that verify the last log's
checkpoint past the consumers' window. A join told nothing fails the
boot there, and an unread checkpoint lets it go on. The window counted
each of those reads at the 5 s term of one request, because no bound
existed to count. It now counts the bound the engine puts on them.

The same change reads a certified listing's key index from the bucket's
leader as one page of its subjects, inside the client's 5 s default.
Before, it was two requests at that default: the stream's lookup, then
its info filtered to the keys. So the listing of the nodes' positions on
the join is 5 s shorter: 15 s for the listing, and 20 with the read-back
of its one row.

Every other read the windows count keeps its bound. A leader read a step
makes on a context with no deadline is asked again inside the client's
5 s default, and so is the broker's limit read. So the admission, the
trim floors, the object store's fleet record and the ceilings keep their
counts, and a seat pod, whose boot reads no state log, keeps its window.

A data pod's boot is now 1461 seconds with its objects on the event
stream and 1601 in S3, where it was 1216 and 1356. That raises the least
window a data pod is given, and every window derived from a volume, by
245 s, about four minutes: 25 s more for each of the ten reads of a
state log, and 5 s less for the listing. A seat pod's stays 1137 and
1187. The EngineClass floors follow (a minimum data.startupSeconds of
1461, and 1601 with objects.backend S3), and the largest volume whose
derived window is within a day is now 14156Gi. The runtime and
troubleshooting pages quote the new figures. The troubleshooting page no
longer says that a boot whose broker never answers is one only a
restart ends: the engine gives such a read up and ends the boot itself,
and the last line of its log names the read. The mirrors the count is
read from follow in the next commit.

Signed-off-by: Seena Fallah <seena.fallah@ionos.com>
crewlet/crewlet#298 now asks a leader read again while nobody answers
it, as a state log's reads already were, and puts a ceiling on every
read asked that way (crewlet/crewlet@a5bf06830; #298's pushed head,
crewlet/crewlet@b5dd4bb53, reads the same). Read at a5bf06830
(hedgeCeilingRead), five rows take new digests: (*Leader).get,
ReadWith, (*DomainLog).At, and the nats bucket's open and lookupBucket.
Their decision texts say what each decides now:

- A leader read on a context with no deadline is still held to the
  client's DefaultTimeout, now asked again inside it while nobody
  answers. Before, get applied that bound to its one request itself.
- A read asked that way from the start, which a state log's read of its
  state and of its record are, is handed its timing's Lookup, 30 s.
  Before, it was asked for as long as its caller's context lived. The
  previous commit counts a boot's reads of a state log at that ceiling.
- A log's read of a record is given its crossing beside the read term
  only across a link, and the bucket's open gives its piece reads across
  a link a longer term. Neither moves a bound. The bucket's lookup is
  held to Lookup by ReadWith rather than by a context of its own.

Get no longer decides the bound a leader read keeps. It hands its
reader's term and bound to ask, ask hands them to askTimed, and askTimed
asks through ReadBoundedWith, which holds a read whose caller set no
deadline to the bound it is handed. The bound itself is NewLeader's,
the client's DefaultTimeout. No row read any of those, so a change
there would have moved no digest. They are rows now. So are the other
reads the windows count that ReadBoundedWith bounds: the broker's limit
read, (*Queue).budget, which is asked the same way and keeps the
client's 5 s; and the timing whose Lookup bounds a state log's reads,
(*Queue).provisioning and Clustered.Timing.

Reading the windows' listing the same way found the count the previous
commit corrects. The same change makes a certified listing's key index
a listing of the bucket's subjects from its leader, one page for a list
as short as the nodes' positions, where it was two requests at the
client's default. keysUnder, (*Leader).Subjects, page and sorted are
rows now too.

LookupBudget's digest holds, and its text now names the reads it bounds
as their ceiling. hedge, (*DomainLog).Stats and stateInfo did not move,
and they decide what their texts say. The table's own doc, and
CONTRIBUTING's account of it, now read a step's reads down to the
function that bounds them, as they read its registrations down to the
queue. They also take the state logs' reads out of the waits the engine
bounds by no time, which leaves casRetry's agreement as the only one.

Five rows differ at crewlet/crewlet@b5dd4bb53, and none of them is this
change's. (*FleetStore).Positions moved after it was read, in a change
that adds no request: its listing hands each row the time it was
stored. (*Engine).armInboxRelay moved with the change that keeps the
inbox relay's batches per domain (crewlet/crewlet@1a61828c7), and is
read again where the operator follows that change: it now hands the
relay the domains whose appliers announce and the check of which of the
node's copies are out of service, and still makes its one registration,
through (*inboxRelay).serve, whose digest holds, so no window moves.
The three rows read at crewlet#311's engine differ because #298's
branch does not hold that change of Crewlet's main yet:
(*Engine).bootSecrets is not on it at all, and (*Engine).join and
(*Engine).startStateLog differ from what that change made them.

Signed-off-by: Seena Fallah <seena.fallah@ionos.com>
crewlet/crewlet#298 now measures each node's clock against its broker's
and raises clock_skew where the two part (crewlet/crewlet@cbc031701).
The measure reads the instant the broker stored a node's row in the
fleet's positions register, which every row of the listing now carries:
the walk of the register's node class hands it on from the entry it
holds already, and (*FleetStore).Positions sets it on the row. Read at
that engine (clockSkewRead), Positions takes its new digest. Its
decision is unchanged: the fleet's positions are one certified listing,
and the measure adds no request to it, so the join's listing of the
positions keeps its 20 s and no window moves.

The change was made in the walk below Positions,
(*FleetStore).eachPositionKey, which no row read: Positions' digest
moved only because the walk's callback now takes the stored instant. A
request added there, or anywhere else between Positions and the bounds
the windows count for its listing, would have moved no digest at all.
Read down the way the table already reads a step's reads and its
registrations, the listing had two such stretches, and the functions on
them are rows now:

- from the walk to the certified listing it is: a fleet bucket's
  filtered walk, (*FleetStore).eachUnder and eachEntryUnder, and the
  leader reader it addresses without a request, newLeaderReader;
- inside the listing, between listUnder and the functions that bound
  its waits: the pass's walk, stop, certify and close, and the read back
  of a key the pass did not deliver live, (*leaderReader).last and
  (*Leader).Last.

Each makes no more requests than the windows count for a listing. The
table's own doc, and CONTRIBUTING's account of it, now read the listing
the windows count down to those functions.

Four rows differ at #298's pushed head, crewlet/crewlet@b5dd4bb53, and
none of them is this change's. (*Engine).armInboxRelay moved with
crewlet/crewlet@1a61828c7 and is read again where the operator follows
that change. The three rows read at crewlet#311's engine,
(*Engine).bootSecrets, (*Engine).join and (*Engine).startStateLog,
differ because #298's branch does not hold that change of Crewlet's
main yet.

Signed-off-by: Seena Fallah <seena.fallah@ionos.com>
Reading the positions listing's read back down to the leader read, in
the previous commit, went through the bridge every read of a key's
newest message in the coordination store crosses: (*leaderReader).last,
then (*Leader).Last, then the (*Leader).get the windows count at the
client's 5 s. The other reads of one key the windows count reach that
bridge through code no row read either, so a request added there would
have moved no digest:

- a log's trim floor, its maintenance window and the object store's
  fleet record are each the store's read of a key's live value,
  (*FleetStore).get, then getLatest and (*leaderReader).live;
- the object store's fleet record is created through
  (*FleetStore).create, whose createKey is a row already.

Each round of that create also writes before it reads, and no row read
what bounds that write either. It is a settled update, which waits out
the leader's answer that another write to the key is still in flight,
but no longer than the client's DefaultTimeout where its caller set no
deadline: settledBucket.Update is one settledBucket.settle, which is
one jsinflight Decide on the timeout settled gives the bucket, the
client's own DefaultTimeout, and Decide asks the publish again only
within it (internal/coord/kv/settle.go, internal/jsinflight). A longer
budget in Decide, or a second request in the update, would have moved
no digest, and left short the 85 s both pods count for the object
store's fleet record. The objects bucket is settled where the fleet
store opens it, which is not read: Crewlet's
TestAConditionalWriteIsAnsweredByWhatTheLeaderDecided requires it.

They are rows now, read at clockSkewRead, the engine the listing was
read at, whose doc says so. Each read is one leader read of the key,
as the Floor, Maintenance, createKey and AgreeObjectBackend rows
already say, and each round's write one settled update at the client's
5 s, as createKey's says, so no window moves. The table's own doc, and
CONTRIBUTING's account of it, now name the bridge beside the leader
read's own bound, and the settled write beside the create.

Signed-off-by: Seena Fallah <seena.fallah@ionos.com>
crewlet/crewlet#298 makes a person's inbox the company's rather than the
tracker's: every part of the engine that writes a person notices keeps
them under one set of rules, so the horizon that ages them out is the
top-level inbox.retention_days (unset is 365, refused outside 30..3650),
and the tracker's own block, which held only that horizon, is gone
(crewlet/crewlet@7504ffc5e). The engine refuses the old key as an
unknown one, and no longer ties the horizon to the tracker a company
runs.

So the Company's horizon moves from spec.tracker.worklet to a block of
its own, spec.inbox.retentionSeconds, rendered as inbox.retention_days.
WorkletSettings and spec.tracker.worklet go, and with them the rule that
admitted the block on backend Worklet alone: a company on Jira, or on no
tracker, keeps a horizon like any other. The API is v1alpha1 and no
release shipped the old field, so it moves with no alias.

The field is declared anew, so it takes this API's form for a duration,
an integer count of seconds, where the old inboxRetentionDays counted
days. Crewlet keeps the horizon in whole days, so the field holds
Crewlet's bounds in seconds, from a month (2592000) to ten years
(315360000), a rule refuses a count that is not whole days, naming the
multiple of 86400 it must be, and the render divides it by 86400.

inbox is now a top-level key a field renders, so engineConfig may not
set it. The render merges each engineConfig entry into the document as
a top-level key, trusting the API to refuse every key a field renders,
and an entry named inbox would have replaced what spec.inbox renders.
Diff's typedKeys lists it too, so an edit made to it through the
engine is found as drift rather than taken for a tuning block's
default. A new render test, TestEngineConfigCannotSetAKeyAFieldRenders,
holds the three lists to one another, each both ways: the company
document's top-level keys, typedKeys, and the keys the CRD's rule
refuses in engineConfig, read off the rule as the CRD states it. So a
key the rule refuses that no field renders fails it, as a key a field
renders that the rule lets through does.

The CRD suite holds the bounds, the whole-day rule, a horizon on a
company with no tracker, and inbox among engineConfig's reserved keys.
TestCompanyDefaults now sends the tracker block empty to see its backend
defaulted, since the block holds nothing else. The org-chart golden sets
the horizon, the overview and company-document pages list inbox among
the keys a Company renders, and the CRDs, the chart and the API
reference are regenerated.

Signed-off-by: Seena Fallah <seena.fallah@ionos.com>
crewlet/crewlet#298 lifts a person's inbox out of the tracker
(crewlet/crewlet@1a61828c7): every domain whose applier announces a
person's movements keeps its notices under one set of rules, and the
inbox relay a data node arms serves each of them. armInboxRelay, which
a data pod's boot window counts as one registration, now hands the
relay those domains (inboxSources) and the per-domain check of which of
them the node's copy holds out of service ((*localEstate).outOfService),
where it handed it the tracker copy's check alone, and the relay
delivers the inbox's own movements, of any domain. So its digest moved,
and the mirror table failed on it.

Read again at the head of #298's branch (inboxLiftRead), against every
file the row names: neither change is a request on the boot. The relay
still serves every domain on its node's one subject, registered once
through (*inboxRelay).serve, whose digest holds at that engine, and a
node without data registers none. So a data pod's boot still counts the
relay as one flush among its four registrations, no window moves, and
the row's decision text stands. Its new digest, the same from
crewlet/crewlet@1a61828c7 to that head, is recorded.

At that engine 195 of the table's 198 rows match. The other three,
(*Engine).bootSecrets, join and startStateLog, are recorded at
crewlet#311, which #298's branch has not taken from Crewlet's main yet.

Signed-off-by: Seena Fallah <seena.fallah@ionos.com>
@crewletbot crewletbot changed the title fix(api): move seat code tiers to spec.code.access, raise volume floors fix(api): move seat code tiers and inbox horizon, raise volume floors Oct 27, 2026
crewlet/crewlet#298 lets a turn hold each scratch repository of the
company's code host it names, so that the host's expiry of an idle one
leaves it be while the turn may still use it, and a launch hands that
set to the run row it opens (crewlet/crewlet@521bd300f).
(*launcher).Launch seals the set from its read until the row exists, so
that a hold another call of the turn takes meanwhile waits, then has
the row keep it, every later hold reaching the row before the request
it is taken for, and unseals the set, or detaches it from the row, when
the launch fails (launchHolds). So its digest moved, and the mirror
table failed on it.

Read again at the head of #298's branch (codeHoldsRead), against every
file the row names: the render's code-work verdicts
(internal/render/codework.go), its NoProvisioner report
(internal/render/company.go) and the config reconciler's report of an
empty start value (internal/controller/config_optional.go). The launch
runs the same head with the same coding agent, and refuses before a box
exists what newCodingRun and sandboxCredentials refuse, and a run whose
environment holds a value that resolves to nothing under a key nothing
declares optional, as before. The one refusal the holds add is the
row's, of a set past the most one run may carry (sandbox.MaxCodeHolds),
which only the repositories a turn names reach and no document
foresees; and a company the operator runs hosts no code, since its
engineConfig may not set code, so its turns hold nothing. No verdict,
report or page moves, and the row's decision text stands. Its new
digest, the same from crewlet/crewlet@521bd300f to that head, is
recorded.

The same change has every node answer for its running turns' holds
(armCodeHolds), one more registration on each pod's boot. The windows
need not count it: it is made past the recovery boundary, after /health
answers 200, beside the seats' memory reads and the coding runs' live
output, which they do not count either.

Signed-off-by: Seena Fallah <seena.fallah@ionos.com>
crewlet/crewlet#298's code writes name the model each was made on, and
a call an agent-mode run makes through the tool bridge has no round of
the engine's tool loop to take its model from. So
(*agentLauncher).LaunchExecutor now resolves the run before it opens
the run's session on the bridge, and the session carries the model the
launch points the coding CLI at, for every bridged call to name
(mcpbridge.Session.Model, crewlet/crewlet@86267fe2c). It still refuses
a node with no bridge before it assembles the run's environment, now by
asking whether it has one, in the words the bridge's empty endpoint was
refused in (errNoBridgeURL), and it opens no session for a run
executorRun refuses. The launch also hands the run row it opens the
turn's code holds, sealed from their read to the row, as the
run_sandbox launch does (crewlet/crewlet@521bd300f). So its digest
moved, with each of the two changes, and the mirror table failed on it.

Read again at the head of #298's branch (codeHoldsRead), against every
file the row names: the render's executor verdict
(internal/render/executor.go), its NoProvisioner report, the config
reconciler's report of an empty start value, the LLMProvider API's rule
on a CLI of mode Agent, the CLI agents guide and the CRD suite's
reading of the engine's sign-in verdicts. An agent-mode turn on a node
that serves no tool bridge is still refused, never run on the engine's
own tool loop, and so is one whose CLI login cannot follow it into the
remote sandbox (sandboxCredentials at phase Execute), the bridge first;
a run whose environment holds a value that resolves to nothing still
starts without it; and the model the session carries is the one the
run is handed, so one model still serves the executor's run and every
other phase, as the guide says. The holds' one refusal is reached by no
company the operator runs, as at the run_sandbox launch. So the row's
decision text stands, and its new digest, the same from
crewlet/crewlet@86267fe2c to that head, is recorded.

No row reads at standingInstructionsRead any more, so it goes, and
codeHoldsRead's account carries what it said: neither the standing
instructions, the run's label nor the wider task a run_sandbox brief
frames is a verdict.

At that engine 195 of the table's 198 rows match. The other three,
(*Engine).bootSecrets, join and startStateLog, are recorded at
crewlet#311, which #298's branch has not taken from Crewlet's main yet.

Signed-off-by: Seena Fallah <seena.fallah@ionos.com>
crewlet/crewlet#298 gives a coding run that names repositories on the
company's own code host its credential, the base its git reaches and the
values its commit hook writes, in the launch's own environment, and
Crewlet now refuses a company document that sets any of those names
(config.ReservedSandboxEnv: CREWLET_GIT_TOKEN, CREWLET_GIT_URL,
CREWLET_GIT_TRAILER_SEAT, CREWLET_GIT_TRAILER_RUN and
CREWLET_GIT_TRAILER_WORK_ITEM) in every env a run's environment is
assembled from: a seat's sandbox.env, a seat's setup steps' env and
providers.sandbox.setup's. The operator renders all three, so a Seat or a
Company setting one was admitted and then held back by crewlet validate.

The API now refuses each name at the variable's own name: a rule on
SeatEnvVar.name (a Seat's spec.sandbox.env) and on the new
SandboxEnvVar.name, the item type of a setup step's env, which took
SecretEnvVar before. SandboxEnvVar has its shape and is its own type
because an LLMProvider's env, which SecretEnvVar types, is no env a run's
environment is assembled from, so the engine reserves nothing there.
Each rule sits on the item's name, at constant cost, never as a
list-wide rule on the step: the setup block's rules already cost
6 582 608 of the 10 000 000 a CRD's rules may.

test/crd tries each rule in each of the three places, and holds them to
the engine both ways (TestReservedRunVariablesFollowTheEngine): the names
the engine's source reserves, the ones the rules state and names beside
them are refused by the API exactly where crewlet validate refuses the
document the operator renders. The credentials guide says so.

Signed-off-by: Seena Fallah <seena.fallah@ionos.com>
crewlet/crewlet#298's boot reads a third address a box dials back to the
engine, CREWLET_BOX_GIT_URL (sandbox.BoxGitURLVar), the base a coding box
reaches the company's code host at, through ParseBoxURL as it reads the
tool bridge's and the telemetry receiver's, and refuses to start on a
value that is set and unusable. ReadBoxURLs and BoxURLs.All are read
again at the head of its branch against the operator's copies: the
operator hands its pods the bridge's and the receiver's alone, does not
offer the code host and so sets no third, and the API refuses every
CREWLET_ variable a Company or a class gives the engine's pods, so no
boot of its meets the third address and no verdict moves. The rows'
decision texts name it and record the new digests; render's address
constants say why the operator sets none.

Signed-off-by: Seena Fallah <seena.fallah@ionos.com>
crewlet/crewlet#298's (*launcher).Launch resolves the repositories a
run_sandbox call names on the company's own code host, after every
refusal a document foresees and before the turn's holds are sealed for
the row, and hands a run given some its credential, its git base and
git's configuration in the launch's own environment, never on the row
(ADR-0063). Read again at the head of its branch against the operator's
copies: a company the operator runs hosts no code (its engineConfig may
not set code), so no call of its seats names a repository, and the head
a launch runs on, its coding agent, every other refusal before a box
exists and the sign-in that must reach the box are as at codeHoldsRead.
The variables that environment carries are the ones the API now refuses
in a Seat's spec.sandbox.env and a setup step's env. The row records the
new digest, read at runReposRead.

Signed-off-by: Seena Fallah <seena.fallah@ionos.com>
crewlet/crewlet#298 makes a person's stop reach the coding runs a seat
has detached, which outlive their turn and run on through a pause: until
the seat is resumed the engine refuses every change such a run would
make, naming who stopped the seat. That is a push its box makes to the
company's code host, and, since crewlet/crewlet@0645a8eb5, a call it
makes through the tool bridge of any of the seat's tools not marked
read-only (mcpbridge.Session.Stopped), answered paused. Its reads still
answer, and a pause without stop_running stops new work only.

Seat.spec.stopRunning said only that a stop ends the turn the seat is
on. Its doc, and the status field reporting it, now say what else it
refuses, and that it lasts until the seat is resumed, which is why
unsetting it changes nothing. The applying page's account of seat
pauses says the same. The operator offers no code host, so for a
company it runs the stop's reach is the bridged writes; the doc names
the pushes too, as the engine's own stop_running does. Regenerated: the
Seat CRD, the chart's copy and the API reference.

Signed-off-by: Seena Fallah <seena.fallah@ionos.com>
crewlet/crewlet#298's (*agentLauncher).LaunchExecutor now opens its
run's session on the tool bridge through bridgeSession, which sets the
fields it set before and the seat's stop check, stoppedBy, the one
reading the code host refuses a run's box's pushes by
(crewlet/crewlet@0645a8eb5). So its digest moved, and the mirror table
failed on it.

Read again at the head of #298's branch (bridgeStopRead,
crewlet/crewlet@ab6d97e67) against every file the row names: the
render's executor verdict, its NoProvisioner report, the config
reconciler's report of an empty start value, the LLMProvider API's rule
on a CLI of mode Agent, the CLI agents guide and the CRD suite's reading
of the engine's sign-in verdicts. The launch refuses what it refused
before, in the same order: a node with no bridge, and a CLI login that
cannot follow the run into the sandbox. bridgeSession always sets the
check, so the bridge's new refusal of a session without one is never
met. The row's decision text stands, and its new digest is recorded.
What a stop refuses a run is no verdict on a launch; the previous commit
says it where the operator says what a stop does.

No row reads at codeHoldsRead any more, so it goes, and bridgeStopRead's
account carries what it said.

At that engine 191 of the table's 198 rows match. Of the other seven,
three are recorded at crewlet#311, which #298's branch has not taken
from Crewlet's main yet ((*Engine).bootSecrets, join and
startStateLog); the other four moved with later #298 changes whose own
companions read them: estate's Serve (the git write bound and the git
lanes' doc), (*stateLog).start, and the embedded broker's connect and
awaitLeafReady (the in-process clients and the fleet's keys).

Signed-off-by: Seena Fallah <seena.fallah@ionos.com>
A pause with stopRunning ends the turn the seat was on and, since
crewlet/crewlet#298, refuses every change its detached coding runs would
make until the seat is resumed: a call through the tool bridge of any of
the seat's tools not marked read-only, and a push from their boxes.
Seat.spec.stopRunning and status.pause.stopRunning say so, but the
Paused condition's message still said only that the turn was stopped,
and the Drifted and PauseNotManaged texts asked for a seat "paused, its
running turn stopped". Someone reading a stopped Seat was told the
engine did less than it does.

The condition now adds that the seat's detached coding runs' writes are
refused until it is resumed, and the asked-for state reads "paused, its
running turn stopped and its runs' writes refused", as the engine's own
pause_seat says both halves. A test holds both texts to the stop, and
to a pause without one naming neither.

Signed-off-by: Seena Fallah <seena.fallah@ionos.com>
The client's SeatPause.StopRunning and PauseSeat docs said a stop only
ends the turn the seat is on. With crewlet/crewlet#298 it also refuses
every change the seat's detached coding runs would make, a call of its
tools that writes and a push from their boxes, until the seat is
resumed, as the engine's pause_seat tool describes it.

Signed-off-by: Seena Fallah <seena.fallah@ionos.com>

This branch has not been deployed

No deployments
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.

2 participants