Skip to content

Commit 14defa0

Browse files
committed
fix(gateway): enforce the runtime-token connect cap at the mint boundary
The 2048-char schema cap on auth.agentRuntimeIdentityToken embeds a caller sessionKey that nothing bounded before minting, so a degenerate key could mint a token the gateway would silently reject at connect. mintAgentRuntimeIdentityToken now fails fast with a descriptive error when the serialized token exceeds HANDSHAKE_RUNTIME_TOKEN_MAX_LENGTH, where the oversized agentId/sessionKey is actionable. Regression tests mint from the longest chat-send-supported session key (512 chars, ~900-char token) and prove it verifies and passes validateConnectParams, plus the mint-time failure path.
1 parent 1e63b37 commit 14defa0

3 files changed

Lines changed: 61 additions & 4 deletions

File tree

packages/gateway-protocol/src/schema/primitives.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,13 @@ export const HandshakeBootstrapTokenString = Type.String({
4444
// (`auth.approvalRuntimeToken`, `auth.agentRuntimeIdentityToken`). The
4545
// approval token is a 43-char base64url HMAC; the agent runtime identity
4646
// token is `<base64url JSON payload>.<base64url HMAC>` whose payload carries
47-
// an agentId and sessionKey, so it needs headroom above the bootstrap cap.
48-
// Both are minted locally from base64url alphabets, so the printable-ASCII
49-
// shape keeps the cap a true byte bound with zero compatibility risk.
47+
// an agentId and sessionKey, so it needs headroom above the bootstrap cap:
48+
// the longest chat-send-supported session key (CHAT_SEND_SESSION_KEY_MAX_LENGTH,
49+
// 512) serializes to a ~900-char token. `mintAgentRuntimeIdentityToken`
50+
// enforces this same cap at the mint boundary, so a token that validates
51+
// there can never be rejected here. Both tokens are minted locally from
52+
// base64url alphabets, so the printable-ASCII shape keeps the cap a true
53+
// byte bound with zero compatibility risk.
5054
export const HANDSHAKE_RUNTIME_TOKEN_MAX_LENGTH = 2048;
5155
export const HandshakeRuntimeTokenString = Type.String({
5256
maxLength: HANDSHAKE_RUNTIME_TOKEN_MAX_LENGTH,

src/gateway/agent-runtime-identity-token.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@ import fs from "node:fs";
22
import os from "node:os";
33
import path from "node:path";
44
import { afterEach, describe, expect, it, vi } from "vitest";
5+
import { validateConnectParams } from "../../packages/gateway-protocol/src/index.js";
6+
import {
7+
CHAT_SEND_SESSION_KEY_MAX_LENGTH,
8+
HANDSHAKE_RUNTIME_TOKEN_MAX_LENGTH,
9+
} from "../../packages/gateway-protocol/src/schema.js";
510
import { captureEnv, setTestEnvValue } from "../test-utils/env.js";
611

712
const envSnapshot = captureEnv(["HOME", "OPENCLAW_HOME", "OPENCLAW_STATE_DIR"]);
@@ -74,6 +79,43 @@ describe("agent runtime identity token", () => {
7479
expect(fs.existsSync(execApprovalsPath(home))).toBe(false);
7580
});
7681

82+
it("mints a token from the longest chat-send-supported session key that passes the connect schema cap", async () => {
83+
useTempHome();
84+
const runtimeToken = await importRuntimeTokenModule();
85+
86+
const sessionKey = "k".repeat(CHAT_SEND_SESSION_KEY_MAX_LENGTH);
87+
const token = runtimeToken.mintAgentRuntimeIdentityToken({
88+
agentId: "agent-with-a-fairly-long-identifier-for-headroom-proof",
89+
sessionKey,
90+
});
91+
92+
expect(token.length).toBeLessThanOrEqual(HANDSHAKE_RUNTIME_TOKEN_MAX_LENGTH);
93+
expect(runtimeToken.verifyAgentRuntimeIdentityToken(token)?.sessionKey).toBe(sessionKey);
94+
const ok = validateConnectParams({
95+
minProtocol: 1,
96+
maxProtocol: 1,
97+
client: { id: "test", version: "1.0.0", platform: "test", mode: "test" },
98+
caps: [],
99+
commands: [],
100+
role: "operator",
101+
scopes: ["operator.read"],
102+
auth: { agentRuntimeIdentityToken: token },
103+
});
104+
expect(ok).toBe(true);
105+
});
106+
107+
it("fails at mint time instead of silently exceeding the connect schema cap", async () => {
108+
useTempHome();
109+
const runtimeToken = await importRuntimeTokenModule();
110+
111+
expect(() =>
112+
runtimeToken.mintAgentRuntimeIdentityToken({
113+
agentId: "main",
114+
sessionKey: "k".repeat(HANDSHAKE_RUNTIME_TOKEN_MAX_LENGTH),
115+
}),
116+
).toThrow(/exceeds the \d+-char connect protocol cap/);
117+
});
118+
77119
it("rejects tokens minted from a different local state directory", async () => {
78120
const firstHome = useTempHome();
79121
const firstProcess = await importRuntimeTokenModule();

src/gateway/agent-runtime-identity-token.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Purpose-scoped local agent runtime identity token for Gateway clients.
22
import { createHmac, timingSafeEqual } from "node:crypto";
3+
import { HANDSHAKE_RUNTIME_TOKEN_MAX_LENGTH } from "../../packages/gateway-protocol/src/schema.js";
34
import { ensureExecApprovals, loadExecApprovals } from "../infra/exec-approvals.js";
45
import { normalizeAgentId } from "../routing/session-key.js";
56

@@ -90,7 +91,17 @@ export function mintAgentRuntimeIdentityToken(params: {
9091
sessionKey: params.sessionKey.trim(),
9192
});
9293
const signature = signPayload(requireSharedAgentRuntimeIdentitySecret(), payload);
93-
return `${payload}.${signature}`;
94+
const token = `${payload}.${signature}`;
95+
// The connect protocol schema caps auth.agentRuntimeIdentityToken at
96+
// HANDSHAKE_RUNTIME_TOKEN_MAX_LENGTH, so an over-long token would be
97+
// silently rejected at the gateway handshake. Fail at the mint boundary
98+
// instead, where the oversized agentId/sessionKey is actionable.
99+
if (token.length > HANDSHAKE_RUNTIME_TOKEN_MAX_LENGTH) {
100+
throw new Error(
101+
`Unable to mint agent runtime identity token: serialized length ${token.length} exceeds the ${HANDSHAKE_RUNTIME_TOKEN_MAX_LENGTH}-char connect protocol cap (agentId/sessionKey too long).`,
102+
);
103+
}
104+
return token;
94105
}
95106

96107
/** Validate a presented agent runtime token and return the internal caller identity. */

0 commit comments

Comments
 (0)