Skip to content

Commit 9b6c44e

Browse files
committed
Machine-auth: resolve a host-supplied caller ahead of grantGuard (CL-6286)
resolveCaller() now calls deps.callerResolver when the host configures one, seating the resolved {tenantId, principalId} as the request's principal/tenant before requirePrincipal/grantGuard run. An unresolved request gets 401 and never falls through to a browser principal. Grant checks apply through the exact same requireGrant("memory", action) path a browser caller gets, since it reads context principal/tenant either way. Identity from the resolver always wins: routes never read tenantId/ principalId from the request body, so a model's tool arguments can never name a different tenant.
1 parent 42e014a commit 9b6c44e

1 file changed

Lines changed: 65 additions & 6 deletions

File tree

‎src/routes/deps.ts‎

Lines changed: 65 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import type { Context, MiddlewareHandler } from "hono";
2-
import type { RequireGrant, TenantEnv } from "@intx/hub-api";
2+
import type {
3+
PrincipalRow,
4+
RequireGrant,
5+
TenantEnv,
6+
TenantRow,
7+
} from "@intx/hub-api";
38
import type { ConditionRegistry, GrantStore } from "@intx/authz";
49

510
import type { Memory } from "../memory.ts";
@@ -111,12 +116,66 @@ export function grantGuard(
111116
}
112117

113118
/**
114-
* TODO(CL-6286): resolve `deps.callerResolver` and seat the result on the
115-
* context ahead of `requirePrincipal`/`grantGuard`. Currently a no-op
116-
* passthrough — behavior is unchanged from before this ticket either way.
119+
* `requireGrant`/`authorize` only ever read `.id` off the tenant/principal
120+
* rows they're handed; the rest of `PrincipalRow`/`TenantRow` describes a
121+
* browser-session database row a bearer-token caller has none of. These
122+
* placeholders exist only to satisfy that shape.
117123
*/
118-
export function resolveCaller(_deps: RouteDeps): MiddlewareHandler<TenantEnv> {
119-
return async (_c, next) => {
124+
function principalRowFor(resolved: ResolvedCaller): PrincipalRow {
125+
return {
126+
id: resolved.principalId,
127+
tenantId: resolved.tenantId,
128+
kind: "agent",
129+
refId: resolved.principalId,
130+
status: "active",
131+
createdAt: new Date(0),
132+
updatedAt: new Date(0),
133+
};
134+
}
135+
136+
function tenantRowFor(resolved: ResolvedCaller): TenantRow {
137+
return {
138+
id: resolved.tenantId,
139+
name: resolved.tenantId,
140+
slug: resolved.tenantId,
141+
domain: "",
142+
parentId: null,
143+
config: null,
144+
createdAt: new Date(0),
145+
updatedAt: new Date(0),
146+
};
147+
}
148+
149+
/**
150+
* When `deps.callerResolver` is set, resolve the caller and seat it as the
151+
* context principal/tenant before `requirePrincipal`/`grantGuard` run — a
152+
* request the resolver rejects never reaches them. When unset, this is a
153+
* no-op passthrough; the host's own tenant-session middleware remains the
154+
* only thing that ever sets `principal`/`tenant`, exactly as before this
155+
* ticket.
156+
*/
157+
export function resolveCaller(deps: RouteDeps): MiddlewareHandler<TenantEnv> {
158+
return async (c, next) => {
159+
if (!deps.callerResolver) {
160+
await next();
161+
return;
162+
}
163+
const resolved = await deps.callerResolver(c);
164+
if (!resolved) {
165+
return c.json(
166+
{
167+
error: {
168+
code: "unauthorized",
169+
message:
170+
"The configured caller resolver could not identify this " +
171+
"request (missing or unrecognized credentials).",
172+
},
173+
},
174+
401,
175+
);
176+
}
177+
c.set("principal", principalRowFor(resolved));
178+
c.set("tenant", tenantRowFor(resolved));
120179
await next();
121180
};
122181
}

0 commit comments

Comments
 (0)