Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

agent-auth.ts

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

agent-auth.tsBlame104 lines · 1 contributor
e75eddcClaude1/**
2 * Agent multiplayer — Bearer-token auth middleware.
3 *
4 * Sits in front of the v2 API. When the caller presents
5 * `Authorization: Bearer agt_<hex>`, we resolve the row from
6 * `agent_sessions` and stash it on the request context as
7 * `c.set("agent", session)`. Downstream handlers can then:
8 * - charge the session's budget,
9 * - enforce the branch namespace on ref updates,
10 * - rate-limit by agent rather than by user.
11 *
12 * If the header isn't an `agt_` token (or it doesn't validate), we
13 * call next() unchanged so the regular `apiAuth` middleware can pick
14 * up the PAT/OAuth/session flow.
15 *
16 * The middleware never rejects on a bad agent token — it just falls
17 * through and lets the canonical auth middleware decide. This keeps
18 * the matrix of "what the caller can do" centralised in `apiAuth`.
19 */
20
21import { createMiddleware } from "hono/factory";
22import {
23 AGENT_TOKEN_PREFIX,
24 authenticateAgent,
25 isAgentToken,
26 refIsInNamespace,
27} from "../lib/agent-multiplayer";
28import type { AgentSession } from "../db/schema";
29
30export type AgentAuthEnv = {
31 Variables: {
32 agent?: AgentSession | null;
33 };
34};
35
36/**
37 * Detect and resolve an agent Bearer token. Always calls next();
38 * downstream auth handles the non-agent paths.
39 */
40export const agentAuth = createMiddleware<AgentAuthEnv>(async (c, next) => {
41 const authHeader = c.req.header("Authorization");
42 if (!authHeader?.startsWith("Bearer ")) {
43 return next();
44 }
45 const token = authHeader.slice(7).trim();
46 if (!token.startsWith(AGENT_TOKEN_PREFIX) || !isAgentToken(token)) {
47 return next();
48 }
49
50 const session = await authenticateAgent(token);
51 if (session) {
52 c.set("agent", session);
53 }
54 return next();
55});
56
57/**
58 * Guard: require an authenticated agent on this route. Returns 401
59 * when no agent is on the context.
60 */
61export const requireAgentAuth = createMiddleware<AgentAuthEnv>(
62 async (c, next) => {
63 const agent = c.get("agent");
64 if (!agent) {
65 return c.json(
66 {
67 error: "Agent authentication required",
68 hint: "Use Authorization: Bearer agt_<token>",
69 },
70 401
71 );
72 }
73 return next();
74 }
75);
76
77/**
78 * Branch-namespace guard for PATCH /repos/:owner/:repo/git/refs/heads/:branch.
79 * When the caller authenticated as an agent, the target branch must sit
80 * under the agent's `branch_namespace` prefix. Non-agent callers
81 * (sessions, PATs) are passed through unchanged.
82 *
83 * This is mounted on the v2 git-refs path; it never blocks regular
84 * humans.
85 */
86export const enforceAgentBranchNamespace = createMiddleware<AgentAuthEnv>(
87 async (c, next) => {
88 const agent = c.get("agent");
89 if (!agent) return next();
90 const branch = c.req.param("branch");
91 if (!branch) return next();
92 if (!refIsInNamespace(branch, agent.branchNamespace)) {
93 return c.json(
94 {
95 error: "Branch is outside the agent's namespace",
96 namespace: agent.branchNamespace,
97 branch,
98 },
99 403
100 );
101 }
102 return next();
103 }
104);