Use HttpApi for Environment APIs & standardize authn/authz#2858
Use HttpApi for Environment APIs & standardize authn/authz#2858juliusmarminge wants to merge 19 commits into
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
28f8946 to
a9767d3
Compare
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
597e56d to
f9c9f4d
Compare
ApprovabilityVerdict: Needs human review Diff is too large for automated approval analysis. A human reviewer should evaluate this PR. You can customize Macroscope's approvability policy. Learn more. |
Co-authored-by: codex <codex@users.noreply.github.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
There are 3 total unresolved issues (including 1 from previous review).
Autofix Details
Bugbot Autofix prepared a fix for 1 of the 2 issues found in the latest run.
- ✅ Fixed: Local
requireEnvironmentScopeshadows exported function with different behavior- Renamed the local function from
requireEnvironmentScopetoauthenticateAndRequireScopeto clearly distinguish it from the exported function inauth/http.tsand reflect that it performs both authentication and scope checking.
- Renamed the local function from
Or push these changes by commenting:
@cursor push c2980e6b47
Preview (c2980e6b47)
diff --git a/apps/server/src/http.ts b/apps/server/src/http.ts
--- a/apps/server/src/http.ts
+++ b/apps/server/src/http.ts
@@ -81,7 +81,7 @@
return redirectUrl.toString();
}
-const requireEnvironmentScope = (
+const authenticateAndRequireScope = (
scope: typeof AuthOrchestrationReadScope | typeof AuthOrchestrationOperateScope,
) =>
Effect.gen(function* () {
@@ -123,7 +123,7 @@
"POST",
OTLP_TRACES_PROXY_PATH,
Effect.gen(function* () {
- yield* requireEnvironmentScope(AuthOrchestrationOperateScope);
+ yield* authenticateAndRequireScope(AuthOrchestrationOperateScope);
const request = yield* HttpServerRequest.HttpServerRequest;
const config = yield* ServerConfig;
const otlpTracesUrl = config.otlpTracesUrl;
@@ -177,7 +177,7 @@
"GET",
`${ATTACHMENTS_ROUTE_PREFIX}/*`,
Effect.gen(function* () {
- yield* requireEnvironmentScope(AuthOrchestrationReadScope);
+ yield* authenticateAndRequireScope(AuthOrchestrationReadScope);
const request = yield* HttpServerRequest.HttpServerRequest;
const url = HttpServerRequest.toURL(request);
if (Option.isNone(url)) {
@@ -238,7 +238,7 @@
"GET",
"/api/project-favicon",
Effect.gen(function* () {
- yield* requireEnvironmentScope(AuthOrchestrationReadScope);
+ yield* authenticateAndRequireScope(AuthOrchestrationReadScope);
const request = yield* HttpServerRequest.HttpServerRequest;
const url = HttpServerRequest.toURL(request);
if (Option.isNone(url)) {You can send follow-ups to the cloud agent here.
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
|
Bugbot Autofix prepared a fix for the issue found in the latest run.
Or push these changes by commenting: Preview (7c9551256f)diff --git a/apps/server/src/http.ts b/apps/server/src/http.ts
--- a/apps/server/src/http.ts
+++ b/apps/server/src/http.ts
@@ -15,6 +15,7 @@
HttpBody,
HttpClient,
HttpClientResponse,
+ HttpEffect,
HttpRouter,
HttpServerResponse,
HttpServerRequest,
@@ -59,8 +60,14 @@
},
});
}
- const response = yield* httpEffect;
- return HttpServerResponse.setHeaders(response, browserApiCorsHeaders);
+ HttpEffect.appendPreResponseHandlerUnsafe(
+ request,
+ (
+ _req: HttpServerRequest.HttpServerRequest,
+ response: HttpServerResponse.HttpServerResponse,
+ ) => Effect.succeed(HttpServerResponse.setHeaders(response, browserApiCorsHeaders)),
+ );
+ return yield* httpEffect;
}),
{ global: true },
);You can send follow-ups to the cloud agent here. |
Co-authored-by: codex <codex@users.noreply.github.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 4 total unresolved issues (including 3 from previous reviews).
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Pairing links with admin scope hidden from listings
- Decoupled the subject assignment so issuePairingCredential always uses 'one-time-token' (keeping user-created pairings visible), while issueStartupPairingUrl directly assigns 'administrative-bootstrap' (keeping startup links hidden).
Or push these changes by commenting:
@cursor push a9cff672ee
Preview (a9cff672ee)
diff --git a/apps/server/src/auth/Layers/ServerAuth.ts b/apps/server/src/auth/Layers/ServerAuth.ts
--- a/apps/server/src/auth/Layers/ServerAuth.ts
+++ b/apps/server/src/auth/Layers/ServerAuth.ts
@@ -1,5 +1,4 @@
import {
- AuthAccessManageScope,
AuthAccessTokenType,
AuthAdministrativeScopes,
AuthStandardClientScopes,
@@ -229,9 +228,7 @@
authControlPlane
.createPairingLink({
scopes: input?.scopes ?? AuthStandardClientScopes,
- subject: input?.scopes?.includes(AuthAccessManageScope)
- ? "administrative-bootstrap"
- : "one-time-token",
+ subject: "one-time-token",
...(input?.label ? { label: input.label } : {}),
})
.pipe(
@@ -329,15 +326,27 @@
);
const issueStartupPairingUrl: ServerAuthShape["issueStartupPairingUrl"] = (baseUrl) =>
- issuePairingCredential({ scopes: AuthAdministrativeScopes }).pipe(
- Effect.map((issued) => {
- const url = new URL(baseUrl);
- url.pathname = "/pair";
- url.searchParams.delete("token");
- url.hash = new URLSearchParams([["token", issued.credential]]).toString();
- return url.toString();
- }),
- );
+ authControlPlane
+ .createPairingLink({
+ scopes: AuthAdministrativeScopes,
+ subject: "administrative-bootstrap",
+ })
+ .pipe(
+ Effect.mapError(
+ (cause) =>
+ new ServerAuthInternalError({
+ message: "Failed to issue startup pairing credential.",
+ cause,
+ }),
+ ),
+ Effect.map((issued) => {
+ const url = new URL(baseUrl);
+ url.pathname = "/pair";
+ url.searchParams.delete("token");
+ url.hash = new URLSearchParams([["token", issued.credential]]).toString();
+ return url.toString();
+ }),
+ );
const issueWebSocketTicket: ServerAuthShape["issueWebSocketTicket"] = (session) =>
sessions.issueWebSocketToken(session.sessionId).pipe(You can send follow-ups to the cloud agent here.
Co-authored-by: codex <codex@users.noreply.github.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 5 total unresolved issues (including 4 from previous reviews).
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: CLI pairing list omits administrative-bootstrap exclusion filter
- Added { excludeSubjects: ["administrative-bootstrap"] } to the CLI's listPairingLinks() call to match the HTTP API behavior in ServerAuth.
Or push these changes by commenting:
@cursor push 33d826e224
Preview (33d826e224)
diff --git a/apps/server/src/cli/auth.ts b/apps/server/src/cli/auth.ts
--- a/apps/server/src/cli/auth.ts
+++ b/apps/server/src/cli/auth.ts
@@ -124,7 +124,9 @@
flags,
(authControlPlane) =>
Effect.gen(function* () {
- const pairingLinks = yield* authControlPlane.listPairingLinks();
+ const pairingLinks = yield* authControlPlane.listPairingLinks({
+ excludeSubjects: ["administrative-bootstrap"],
+ });
yield* Console.log(formatPairingCredentialList(pairingLinks, { json: flags.json }));
}),
{You can send follow-ups to the cloud agent here.
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Missing
scopesfield inAuthenticatedSessionreturned from auth- Added catchTag for ServerAuthInternalError in getSessionState to gracefully degrade to unauthenticated state on internal errors (e.g. database failures), restoring the never-fail contract and updated the type signature and caller accordingly.
Or push these changes by commenting:
@cursor push 9614dc8fc3
Preview (9614dc8fc3)
diff --git a/apps/server/src/auth/EnvironmentAuth.ts b/apps/server/src/auth/EnvironmentAuth.ts
--- a/apps/server/src/auth/EnvironmentAuth.ts
+++ b/apps/server/src/auth/EnvironmentAuth.ts
@@ -91,7 +91,7 @@
readonly getDescriptor: () => Effect.Effect<ServerAuthDescriptor>;
readonly getSessionState: (
request: HttpServerRequest.HttpServerRequest,
- ) => Effect.Effect<AuthSessionState, ServerAuthInternalError>;
+ ) => Effect.Effect<AuthSessionState>;
readonly createBrowserSession: (
credential: string,
requestMetadata: AuthClientMetadata,
@@ -296,12 +296,18 @@
...(session.expiresAt ? { expiresAt: DateTime.toUtc(session.expiresAt) } : {}),
}) satisfies AuthSessionState,
),
- Effect.catchTag("ServerAuthInvalidCredentialError", () =>
- Effect.succeed({
- authenticated: false,
- auth: descriptor,
- } satisfies AuthSessionState),
- ),
+ Effect.catchTags({
+ ServerAuthInvalidCredentialError: () =>
+ Effect.succeed({
+ authenticated: false,
+ auth: descriptor,
+ } satisfies AuthSessionState),
+ ServerAuthInternalError: () =>
+ Effect.succeed({
+ authenticated: false,
+ auth: descriptor,
+ } satisfies AuthSessionState),
+ }),
);
const createBrowserSession: EnvironmentAuthShape["createBrowserSession"] = (
diff --git a/apps/server/src/auth/http.ts b/apps/server/src/auth/http.ts
--- a/apps/server/src/auth/http.ts
+++ b/apps/server/src/auth/http.ts
@@ -167,13 +167,7 @@
Effect.fn("environment.auth.session")(function* (args) {
yield* annotateEnvironmentRequest(args.endpoint.name);
const request = yield* HttpServerRequest.HttpServerRequest;
- return yield* serverAuth
- .getSessionState(request)
- .pipe(
- Effect.catchTag("ServerAuthInternalError", (error) =>
- failEnvironmentInternal("internal_error", error),
- ),
- );
+ return yield* serverAuth.getSessionState(request);
}),
)
.handle(You can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit 295145c. Configure here.
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>


Summary
EnvironmentHttpApicontractVerification
bun fmtbun lint(passes with existing warnings in mobile/web base)bun lint:mobilebun typecheckcd apps/server && bun run test -- src/server.test.ts src/bin.test.tscd packages/client-runtime && bun run test -- src/remote.test.tsNote
Replace role-based auth with OAuth-style scopes and HttpApi for environment endpoints
owner/client) with fine-grained OAuth scopes (e.g.orchestration:read,orchestration:operate,access:manage) across sessions, pairing links, JWT claims, and all RPC endpoints.HttpRouterto a typedEnvironmentHttpApicontract inpackages/contracts/src/environmentHttp.ts, replacing/api/auth/bootstrap,/api/auth/ws-token, and related routes with/api/auth/browser-session,/oauth/token, and/api/auth/websocket-ticket.SessionCredentialServicetoSessionStoreand splitsSessionCredentialErrorintoSessionCredentialInvalidErrorandSessionCredentialInternalError; all RPC methods now declareEnvironmentAuthorizationErroras a possible failure.AuthAuthorizationScopes) that drops and recreatesauth_pairing_linksandauth_sessionswithscopescolumns, removing therolecolumn.ServerSecretStoreas a standalone DI service for filesystem-backed secrets with secure permissions and atomic writes.issueSshWebSocketToken→issueSshWebSocketTicketacross the desktop bridge, IPC handlers, and WebSocket URL construction (wsToken→wsTicket).Macroscope summarized 67cd500.
Note
High Risk
Large auth refactor (pairing, sessions, token exchange, WebSocket tickets) with client and persistence contract changes; active sessions/pairing data may be invalidated depending on migrations bundled with the full PR.
Overview
This PR consolidates server authentication into
EnvironmentAuth,PairingGrantStore, andSessionStore, and drops the olderServerAuth/AuthControlPlane/BootstrapCredentialService/SessionCredentialServicelayer split (including emptyServices/*stubs).Authorization model: pairing grants and sessions are keyed by OAuth-style scope lists instead of owner/client roles. Bootstrap exchange for remote clients returns
AuthAccessTokenResult(access_token,scope, etc.); session methods usebearer-access-token. WebSocket upgrades use awsTicketquery param andAuthWebSocketTicketResultinstead of awsToken/ token result.Clients: Desktop SSH IPC calls
@t3tools/client-runtimeremote helpers afterresolveLoopbackSshHttpBaseUrl, removesDesktopSshRemoteApi, and renames IPC toissueSshWebSocketTicket. Mobile pairing sendsmobileAuthClientMetadata()on bootstrap and persistsbootstrap.access_token.Tests move to the new modules (e.g.
sshEnvironment.test.ts,EnvironmentAuth*.test.ts) with updated expectations for scopes and error tags.Reviewed by Cursor Bugbot for commit 67cd500. Bugbot is set up for automated code reviews on this repo. Configure here.