fix: guard resolveUserPath against undefined input (#10176)

* fix: guard resolveUserPath against undefined input

When subagent spawner omits workspaceDir, resolveUserPath receives
undefined and crashes on .trim().  Add a falsy guard that falls back
to process.cwd(), matching the behavior callers already expect.

Closes #10089

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: harden runner workspace fallback (#10176) (thanks @Yida-Dev)

* fix: harden workspace fallback scoping (#10176) (thanks @Yida-Dev)

* refactor: centralize workspace fallback classification and redaction (#10176) (thanks @Yida-Dev)

* test: remove explicit any from utils mock (#10176) (thanks @Yida-Dev)

* security: reject malformed agent session keys for workspace resolution (#10176) (thanks @Yida-Dev)

---------

Co-authored-by: Yida-Dev <reyifeijun@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Gustavo Madeira Santana <gumadeiras@gmail.com>
This commit is contained in:
Yida-Dev
2026-02-07 01:16:58 +07:00
committed by GitHub
parent 5842bcaaf7
commit 4216449405
22 changed files with 522 additions and 24 deletions

View File

@@ -0,0 +1,25 @@
import { describe, expect, it } from "vitest";
import { classifySessionKeyShape } from "./session-key.js";
describe("classifySessionKeyShape", () => {
it("classifies empty keys as missing", () => {
expect(classifySessionKeyShape(undefined)).toBe("missing");
expect(classifySessionKeyShape(" ")).toBe("missing");
});
it("classifies valid agent keys", () => {
expect(classifySessionKeyShape("agent:main:main")).toBe("agent");
expect(classifySessionKeyShape("agent:research:subagent:worker")).toBe("agent");
});
it("classifies malformed agent keys", () => {
expect(classifySessionKeyShape("agent::broken")).toBe("malformed_agent");
expect(classifySessionKeyShape("agent:main")).toBe("malformed_agent");
});
it("treats non-agent legacy or alias keys as non-malformed", () => {
expect(classifySessionKeyShape("main")).toBe("legacy_or_alias");
expect(classifySessionKeyShape("custom-main")).toBe("legacy_or_alias");
expect(classifySessionKeyShape("subagent:worker")).toBe("legacy_or_alias");
});
});

View File

@@ -10,6 +10,7 @@ export {
export const DEFAULT_AGENT_ID = "main";
export const DEFAULT_MAIN_KEY = "main";
export const DEFAULT_ACCOUNT_ID = "default";
export type SessionKeyShape = "missing" | "agent" | "legacy_or_alias" | "malformed_agent";
// Pre-compiled regex
const VALID_ID_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/i;
@@ -58,6 +59,17 @@ export function resolveAgentIdFromSessionKey(sessionKey: string | undefined | nu
return normalizeAgentId(parsed?.agentId ?? DEFAULT_AGENT_ID);
}
export function classifySessionKeyShape(sessionKey: string | undefined | null): SessionKeyShape {
const raw = (sessionKey ?? "").trim();
if (!raw) {
return "missing";
}
if (parseAgentSessionKey(raw)) {
return "agent";
}
return raw.toLowerCase().startsWith("agent:") ? "malformed_agent" : "legacy_or_alias";
}
export function normalizeAgentId(value: string | undefined | null): string {
const trimmed = (value ?? "").trim();
if (!trimmed) {