v0.2.3 - #3
Merged
Merged
Conversation
Flips ci.go.module_tags from verify to push, so a release is one action: push the root `vX.Y.Z` tag. publish-go gates on the Go tests and then creates and pushes the api/, sdk/go/ and server/ tags that Go resolves nested modules by. Earned rather than assumed: v0.2.2 ran the same job in verify mode against tags cut by hand and confirmed all four were present on the release commit, so the only new behaviour here is creating them instead of checking them. The job keeps the mismatch guard — a module tag that already exists on a different commit still fails rather than being moved. Pushing only the root tag also removes the failure that made v0.2.2 look inert: GitHub creates no push event when more than three tags arrive at once, and tag-release.sh pushed four together, so no workflow ran until the root tag was re-pushed alone. With CI creating the module tags, a release can never exceed one tag per push. scripts/tag-release.sh is deleted — publish-go supersedes it, and keeping a second way to cut tags invites exactly the four-at-once push that just failed. The CHANGELOG reference to it is left alone as an accurate record of how 0.1.58 was released. The release process is now documented in CLAUDE.md, including the three-tag limit, since that is not obvious and the consequence is silent. One incidental change: the module tags are now lightweight rather than annotated. Go resolves either, and nothing in this repo reads tag metadata. contents: write is scoped to the module-tags job alone; actions/checkout keeps its credentials by default, so the push authenticates with GITHUB_TOKEN.
The Python admin client exposed only list_agents/get_agent; the proto + Go gateway (agent_handler.go) already support REGISTER/UPDATE/DELETE via AgentOperation. Add register_agent/update_agent/delete_agent (async + sync) that build an AgentRegistrationInfo (shared _build_agent_registration_info helper — implementation/orchestrator_profile/description/launch_params/capabilities/ extensions/resource_schema; registered_at/updated_at stay server-owned) and send via the existing agent_op transport. Enables superadmin agent-registry CRUD.
Adds WithHandlerMiddleware(func(http.Handler) http.Handler) plus Server.WrapHandler. Motivation: the internal plane serves /auth/verify — the ext_authz call on the path of EVERY request through the gateway — and there was no way to instrument it from outside this package. In the Scitrera multi-tenant binary that made auth-go invisible in tracing: its TracerProvider and exporter were wired and metrics were flowing, but nothing on this plane ever created a span. The middleware is a plain func rather than an OTel dependency here, so tracing libraries stay in whichever binary wants them and this package's dependency graph is unchanged. Applied after AttachLogin so login routes are covered. The wrapped chain ends at the mux POINTER, so routes registered later are still served through it. WrapHandler is a no-op on nil, letting Run pass an unset option straight through.
handleIncrement set a counter's expiry only when some caller observed counterVal == 1, through a SEPARATE Set after the atomic Increment. Any key that came into existence another way never received a TTL at all: that Set failing, two first-increments racing so neither observed 1, or a key written by some other path. Such a key incremented forever and pinned its principal at "limit exceeded" permanently, with no self-healing — the state lives in KV, so restarting the client, the server, or anything between them changed nothing. Observed in production 2026-08-05: a MemoryLayer per-user rate-limit counter stuck at 10078 against a limit of 10000, returning 429 indefinitely across restarts of MemoryLayer, platform-server and platform-bridge. A freshly-keyed principal was unaffected, which is what isolated it to the key rather than the limiter or the traffic. Notably the count was barely over the limit — it had crept past gradually, not been driven there by a flood. Now SetNX writes the key with its TTL BEFORE the increment, so a counter cannot exist without an expiry, and it is atomic rather than a read-modify-write. The old post-increment Set is kept purely as a fallback for SetNX failing, where counterVal == 1 proves the key was absent and writing "1" cannot lose a concurrent update. Also fixes silent counter loss: the old Set clobbered the value back to "1", discarding increments that landed between the Increment and the Set.
Orchestrators were denied KV outright, so an orchestrator could not read the tenant ProvisionSpec or per-tenant launcher credentials it needs to build a worker's environment. The worker was launched anyway, with no environment, and the agent then died resolving its provider secrets. The failure was also hard to see: the gateway logged the real cause (PermissionDenied) while the orchestrator only ever received a generic "[KV_ERROR] internal error processing KV operation". This contradicted acl_seed.py, which already seeds NARROW orchestrator grants (orc::<impl>::* -> kv_key/provision/*, *ikv:provision:*, *ikv:api_key:MODAL_*). Those grants were unreachable: the type gate rejected orchestrators before checkKeyPermission was ever consulted, so they could never take effect. Follows the WorkflowEngine and MetricsBridge precedent directly — this ONLY opens the type gate; every key an orchestrator touches still requires an explicit ACL grant, so the reachable surface stays exactly what acl_seed.py defines. The existing test asserted the old denial deliberately, so it is rewritten to assert the new intent rather than deleted: orchestrators get a private KV space for passing init args to the tasks they create, still ACL-scoped per key.
Publish could lose a message with NO observable trace. When subs[topic] is empty the fan-out loop simply does not execute and Publish returns nil, so the gateway's routeMessage -- which only logs on publish *error* -- records a fully successful send while nothing was delivered. An agent that is connected but whose subscription never registered is therefore indistinguishable from a healthy one. This is not hypothetical: an app-open tool call is published to an application-specific agent identity topic, the agent is connected with a live session and a successful setupClientSubscriptions, and the message never arrives. The two drop paths that DO log (the full-channel warn here, and handleDeliverShed's "delivery shed by backpressure" in the session layer) both stayed silent, which leaves the zero-subscriber case as the only remaining explanation -- and it was unobservable. Adds: - Warn when publishing to a topic with zero live subscribers. For an identity topic this is always a fault, never a normal state. - Debug fan-out line (topic, seq, subscriber count) as its positive counterpart. - Debug on subscriber registration (topic, consumer, exclusive, policy, start_seq, resulting subscriber count) so "was anyone listening?" is answerable directly rather than by inference. - Debug on replay completion (start_seq -> replayed_up_to). A large replay is itself a suspect: replay pushes straight at the handler, which enqueues non-blocking into the client's delivery buffer, so a backlog can shed the messages that follow it. - Warn when an exclusive subscription is REJECTED because the consumer is already active. consumerName is the identity string, identical across every incarnation of an agent, so a leaked lock blocks all future subscriptions to that topic until the gateway restarts -- and the error was previously only returned, never logged. - Warn (was: silent skip) when a subscriber is already done. Observability only; no behavioural change.
Two rate limits gate a client's outbound messages: the per-client limiter and the per-workspace limiter (fed by the per-identity quota). In lite mode BOTH ignored configuration, so message throughput was pinned at 100/s no matter what the config said — and the drop is invisible to the sender, since the rejection comes back asynchronously as ERR_RATE_LIMITED rather than from Send. - gateway.message_rate_limit (+ message_rate_burst) was never applied: cmd/gateway appends gateway.WithMessageRateLimit, cmd/aetherlite did not, so the gateway's built-in newQuotaEnforcer(100, 200) stood regardless of the key. - The quotas: block was hardcoded, so max_message_rate_per_identity — which feeds the workspace limiter — was likewise stuck at 100. Both now read config with the same fallbacks cmd/gateway uses, so an unconfigured deployment behaves exactly as before. Measured against a 1500-message burst on one connection: 100/1500 delivered before, 1500/1500 in order after.
ScopeSpec has two independent axes: Sharing decides which AGENTS rendezvous on a key, Identity decides WHOSE data it is. buildJSKey is explicit that omitting the agent segments for shared scopes exists "so that all agents in the tenant rendezvous on the same storage key" -- the sharing axis was never meant to relax the user boundary. The user boundary had nothing enforcing it. op.UserId is client-supplied and ValidateScopeSpec only checks it is non-empty, so any caller could name another user and address their namespace directly. What stood in for it was an ACL default-deny on the shared user scopes: a mitigation at the wrong layer, and one that cannot tell a legitimate same-user read from a cross-user one. It blocks both, which is why the shared scopes could not be opened for durable per-user tool approvals without also permitting cross-user access. Under an on-behalf-of grant the subject IS the user, so the axis is derivable rather than assertable: user_id is filled in when omitted and must match the subject when supplied. A mismatch is denied and logged with both ids. Deliberately narrow. Callers acting under their OWN authority are untouched -- platform-server writes per-user session state for the browser's user and is bounded by its explicit kv_scope grants -- and direct user principals cannot reach KV at all (the type gate in HandleKVOperation), so OBO is the only path that could ever assert a foreign user id. Tests cover both shared and exclusive user scopes, derivation, non-user scopes, non-user subjects, and the own-authority carve-out; verified to fail without the pin. gateway/kv/acl suites pass.
A successful automatic reconnect returns nil from the receive-error handler. Continue the loop so Run services the replacement stream instead of reporting a false graceful exit. Cover both transport errors and graceful-disconnect signals.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This pull request introduces significant enhancements to the Aether protocol, focusing on portable runtime authorization, workflow schedule authority, and improved release and CI processes. The changes add new protocol messages and fields to support granular, gateway-enforced resource access checks, authority continuations, and workflow authority lifetimes. Additionally, the release documentation and CI workflows are updated to streamline and automate module tagging and Docker image builds.
Protocol and Authorization Enhancements:
ResourceAccessRequest,AccessDecisionReceipt,AccessCheckOperation,AccessCheckResponse,BatchAccessCheckOperation, andBatchAccessCheckResponsemessages, enabling gateway-evaluated access checks and receipts throughout the protocol (api/proto/aether.proto).SendMessage,IncomingMessage,MessageEnvelope,ProxyHttpRequest,TaskAssignment) with fields for checked access requests, gateway-authored access receipts, and authority continuations, allowing for fine-grained, auditable authorization flows (api/proto/aether.proto). [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11] [12]AuthorityContinuationScopeandAuthorityContinuationRequestfor explicit, attenuated authority delegation in message delivery, andForwardedAuthorizationfor securely transporting derived authority context (api/proto/aether.proto). [1] [2]Workflow Engine and Task Management:
WorkflowScheduleAuthorityScope,WorkflowRequestContext, andWorkflowAuthorityLifetimeMode, allowing the gateway to mint and manage schedule-scoped authority for workflow executions. ExtendedWorkflowOperationwith fields for caller OBO authority, requested schedule authority, and trusted request context (api/proto/aether.proto). [1] [2]api/proto/aether.proto). [1] [2] [3] [4]Release and CI Process Improvements:
CLAUDE.mdto clarify that a single root tag push triggers all publish workflows and that module tags are now managed automatically by CI, replacing previous manual or scripted processes (CLAUDE.md)..github/workflows/publish-go.yml). [1] [2]Dockerfile.aetherliteby default, aligning container builds with production standards (.github/workflows/build-docker.yml).Documentation Updates:
docs/aetherlite.md). [1] [2]