Skip to content

Commit d4ed089

Browse files
authored
fix(onboard): validate reset preflight (#111348)
1 parent a868d34 commit d4ed089

8 files changed

Lines changed: 999 additions & 34 deletions

File tree

extensions/anthropic/index.test.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1272,6 +1272,78 @@ describe("anthropic provider replay hooks", () => {
12721272
}
12731273
});
12741274

1275+
it("preflights non-interactive setup-token input without writing credentials", async () => {
1276+
const provider = await registerSingleProviderPlugin(anthropicPlugin);
1277+
const setupTokenAuth = provider.auth.find((entry) => entry.id === "setup-token");
1278+
if (!setupTokenAuth?.validateNonInteractive) {
1279+
throw new Error("expected setup-token reset preflight");
1280+
}
1281+
const runtime = { log: vi.fn(), error: vi.fn(), exit: vi.fn() };
1282+
1283+
const valid = await setupTokenAuth.validateNonInteractive({
1284+
authChoice: "setup-token",
1285+
config: {},
1286+
baseConfig: {},
1287+
opts: { token: ANTHROPIC_SETUP_TOKEN },
1288+
runtime,
1289+
resolveApiKey: vi.fn(async () => null),
1290+
});
1291+
1292+
expect(valid).toBe(true);
1293+
expect(runtime.error).not.toHaveBeenCalled();
1294+
});
1295+
1296+
it("rejects setup-token ref storage during non-interactive preflight", async () => {
1297+
const provider = await registerSingleProviderPlugin(anthropicPlugin);
1298+
const setupTokenAuth = provider.auth.find((entry) => entry.id === "setup-token");
1299+
if (!setupTokenAuth?.validateNonInteractive) {
1300+
throw new Error("expected setup-token reset preflight");
1301+
}
1302+
const runtime = { log: vi.fn(), error: vi.fn(), exit: vi.fn() };
1303+
1304+
const valid = await setupTokenAuth.validateNonInteractive({
1305+
authChoice: "setup-token",
1306+
config: {},
1307+
baseConfig: {},
1308+
opts: {
1309+
token: ANTHROPIC_SETUP_TOKEN,
1310+
secretInputMode: "ref", // pragma: allowlist secret
1311+
},
1312+
runtime,
1313+
resolveApiKey: vi.fn(async () => null),
1314+
});
1315+
1316+
expect(valid).toBe(false);
1317+
expect(runtime.error).toHaveBeenCalledWith(
1318+
"Anthropic setup-token input cannot be stored with --secret-input-mode ref. Use --secret-input-mode plaintext.",
1319+
);
1320+
expect(runtime.exit).toHaveBeenCalledWith(1);
1321+
});
1322+
1323+
it("rejects invalid setup-token expiry during non-interactive preflight", async () => {
1324+
const provider = await registerSingleProviderPlugin(anthropicPlugin);
1325+
const setupTokenAuth = provider.auth.find((entry) => entry.id === "setup-token");
1326+
if (!setupTokenAuth?.validateNonInteractive) {
1327+
throw new Error("expected setup-token reset preflight");
1328+
}
1329+
const runtime = { log: vi.fn(), error: vi.fn(), exit: vi.fn() };
1330+
1331+
const valid = await setupTokenAuth.validateNonInteractive({
1332+
authChoice: "setup-token",
1333+
config: {},
1334+
baseConfig: {},
1335+
opts: { token: ANTHROPIC_SETUP_TOKEN, tokenExpiresIn: "nope" },
1336+
runtime,
1337+
resolveApiKey: vi.fn(async () => null),
1338+
});
1339+
1340+
expect(valid).toBe(false);
1341+
expect(runtime.error).toHaveBeenCalledWith(
1342+
expect.stringContaining("Invalid --token-expires-in"),
1343+
);
1344+
expect(runtime.exit).toHaveBeenCalledWith(1);
1345+
});
1346+
12751347
it("omits setup-token expiry when duration overflows the Date range", async () => {
12761348
vi.useFakeTimers();
12771349
vi.setSystemTime(8_640_000_000_000_000);

extensions/anthropic/register.runtime.ts

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { resolveExpiresAtMsFromDurationMs } from "openclaw/plugin-sdk/number-run
77
import type {
88
OpenClawPluginApi,
99
ProviderAuthContext,
10+
ProviderAuthMethod,
1011
ProviderAuthMethodNonInteractiveContext,
1112
ProviderResolveDynamicModelContext,
1213
ProviderNormalizeResolvedModelContext,
@@ -60,6 +61,10 @@ import { registerClaudeSessionDiscovery } from "./session-catalog-registration.j
6061
import { wrapAnthropicProviderStream } from "./stream-wrappers.js";
6162
import { fetchAnthropicUsage, resolveAnthropicUsageAuth } from "./usage.js";
6263

64+
type ProviderAuthMethodNonInteractiveValidationContext = Parameters<
65+
NonNullable<ProviderAuthMethod["validateNonInteractive"]>
66+
>[0];
67+
6368
const PROVIDER_ID = "anthropic";
6469

6570
// Anthropic-native error descriptors stay with the Anthropic provider hook.
@@ -194,9 +199,16 @@ async function runAnthropicSetupTokenAuth(ctx: ProviderAuthContext): Promise<Pro
194199
};
195200
}
196201

197-
async function runAnthropicSetupTokenNonInteractive(
198-
ctx: ProviderAuthMethodNonInteractiveContext,
199-
): Promise<ProviderAuthConfig | null> {
202+
function validateAnthropicSetupTokenNonInteractive(
203+
ctx: ProviderAuthMethodNonInteractiveValidationContext,
204+
): string | null {
205+
if (ctx.opts.secretInputMode === "ref") {
206+
ctx.runtime.error(
207+
"Anthropic setup-token input cannot be stored with --secret-input-mode ref. Use --secret-input-mode plaintext.",
208+
);
209+
ctx.runtime.exit(1);
210+
return null;
211+
}
200212
const rawToken =
201213
typeof ctx.opts.token === "string" ? normalizeAnthropicSetupTokenInput(ctx.opts.token) : "";
202214
const tokenError = validateAnthropicSetupToken(rawToken);
@@ -209,6 +221,25 @@ async function runAnthropicSetupTokenNonInteractive(
209221
ctx.runtime.exit(1);
210222
return null;
211223
}
224+
try {
225+
resolveAnthropicSetupTokenExpiry(ctx.opts.tokenExpiresIn);
226+
} catch (error) {
227+
ctx.runtime.error(
228+
`Invalid --token-expires-in: ${error instanceof Error ? error.message : String(error)}`,
229+
);
230+
ctx.runtime.exit(1);
231+
return null;
232+
}
233+
return rawToken;
234+
}
235+
236+
async function runAnthropicSetupTokenNonInteractive(
237+
ctx: ProviderAuthMethodNonInteractiveContext,
238+
): Promise<ProviderAuthConfig | null> {
239+
const rawToken = validateAnthropicSetupTokenNonInteractive(ctx);
240+
if (!rawToken) {
241+
return null;
242+
}
212243

213244
const profileId = resolveAnthropicSetupTokenProfileId(ctx.opts.tokenProfileId);
214245
const expires = resolveAnthropicSetupTokenExpiry(ctx.opts.tokenExpiresIn);
@@ -836,6 +867,8 @@ export function buildAnthropicProvider(): ProviderPlugin {
836867
groupHint: "Claude CLI + API key + token",
837868
},
838869
run: async (ctx: ProviderAuthContext) => await runAnthropicSetupTokenAuth(ctx),
870+
validateNonInteractive: async (ctx) =>
871+
Boolean(validateAnthropicSetupTokenNonInteractive(ctx)),
839872
runNonInteractive: async (ctx: ProviderAuthMethodNonInteractiveContext) =>
840873
await runAnthropicSetupTokenNonInteractive(ctx),
841874
},

src/commands/onboard-remote.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ function ensureWsUrl(value: string): string {
3636
return trimmed;
3737
}
3838

39-
function validateGatewayWebSocketUrl(value: string): string | undefined {
39+
export function validateGatewayWebSocketUrl(value: string): string | undefined {
4040
const trimmed = value.trim();
4141
if (!trimmed.startsWith("ws://") && !trimmed.startsWith("wss://")) {
4242
return t("wizard.remote.validWebSocketUrl");

0 commit comments

Comments
 (0)