Skip to content
Closed
104 changes: 104 additions & 0 deletions src/components/CommandGate.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { afterEach, describe, expect, test } from "bun:test";
import { DEFAULT_GLOBAL_CONFIG } from "../globalConfig";
import { CommandKey } from "../router";
import {
cleanupScreens,
compiledRootCommand,
renderScreen,
TestCoreClient,
waitForText,
} from "../testing";

afterEach(cleanupScreens);

function expectNoResourceCalls(core: TestCoreClient) {
expect([
...core.harness.calls,
...core.runtime.calls,
...core.gateway.calls,
...core.policy.calls,
]).toEqual([]);
}

const MUTATION_ROUTES = [
"harness/create",
"harness/update",
"harness/update/update",
"harness/delete",
"harness/delete/delete",
"harness/endpoint/create",
"harness/endpoint/create/update",
"harness/endpoint/update",
"harness/endpoint/update/update",
"harness/endpoint/update/update/delete",
"harness/endpoint/delete",
"harness/endpoint/delete/delete",
"harness/endpoint/delete/delete/update",
"harness/invoke",
"harness/invoke/update",
"harness/invoke/update/delete",
"harness/exec",
"harness/exec/update",
"harness/exec/update/delete",
"runtime/invoke",
"runtime/invoke/update",
"runtime/invoke/update/delete",
"runtime/shell",
"runtime/shell/update",
"runtime/shell/update/delete",
"gateway/invoke",
"gateway/invoke/update",
"gateway/policy",
"gateway/policy/generate",
"gateway/policy/generate/delete",
];

describe("disabled imperative routes", () => {
test.each(MUTATION_ROUTES)("%s redirects before mounting its screen", async (path) => {
const core = new TestCoreClient();
const screen = renderScreen(`/agentcore/${path}`, {
core,
globalConfig: DEFAULT_GLOBAL_CONFIG,
});

await waitForText(screen.lastFrame, "the platform for production AI agents");
expect(screen.lastFrame()).toContain("type to choose a command");
expectNoResourceCalls(core);
});

test("unavailable Gateway creation shows project guidance, not another command's help", async () => {
const core = new TestCoreClient();
const screen = renderScreen("/agentcore/gateway/create", {
core,
globalConfig: DEFAULT_GLOBAL_CONFIG,
});
await waitForText(screen.lastFrame, "Create an AgentCore Gateway");
expect(screen.lastFrame()).not.toContain("this command runs from the command line");
expectNoResourceCalls(core);
});
});

describe("public project invocation does not enable other commands", () => {
test.each([
["harness", "harness/invoke"],
["runtime", "runtime/invoke"],
["harness", "runtime/invoke/update/DEFAULT"],
["runtime", "harness/invoke/update"],
["harness", "harness/exec/update"],
["runtime", "runtime/shell/update/DEFAULT"],
["harness", "harness/update/update"],
["runtime", "gateway/invoke/update"],
] as const)("project invoke %s does not authorize %s", async (family, path) => {
const core = new TestCoreClient();
const launch = compiledRootCommand(core, DEFAULT_GLOBAL_CONFIG)
.commands.find((command) => command.name() === "invoke")!
.commands.find((command) => command.name() === family)!;
const screen = renderScreen(`/agentcore/${path}`, {
core,
globalConfig: DEFAULT_GLOBAL_CONFIG,
withContext: (ctx) => ctx.withValue(CommandKey, launch),
});
await waitForText(screen.lastFrame, "the platform for production AI agents");
expectNoResourceCalls(core);
});
});
30 changes: 30 additions & 0 deletions src/components/CommandGate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Navigate, Outlet, useLocation, useResolvedPath } from "react-router";
import { CommandKey, type Context } from "../router";
import { commandPath, resolveCommand } from "./RouterScreen";

export function isCommandAvailable(ctx: Context, path: string[]): boolean {
const command = resolveCommand(ctx.require(CommandKey), path);
return commandPath(command).join("/") === path.join("/");
}

// Mount at the command's route, above its index and resource-ID routes, so an
// unavailable command cannot mount a screen that fetches or mutates resources.
export function CommandGate({ ctx }: { ctx: Context }) {
const path = useResolvedPath(".").pathname.split("/").filter(Boolean);
const locationPath = useLocation().pathname.split("/").filter(Boolean);
const launchPath = commandPath(ctx.require(CommandKey));
// Project invoke resolves a project resource before launching these shared
// consoles. It does not grant access to exec, shell, or other resource actions.
const isProjectInvoke =
path.length === 3 &&
locationPath.length > path.length &&
path[2] === "invoke" &&
(path[1] === "harness" || path[1] === "runtime") &&
launchPath.join("/") === `agentcore/invoke/${path[1]}`;

return isCommandAvailable(ctx, path) || isProjectInvoke ? (
<Outlet />
) : (
<Navigate to="/agentcore" replace />
);
}
184 changes: 70 additions & 114 deletions src/components/Root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ import { HelpScreen, RootScreen } from "../handlers/screen.tsx";
import { RegionKey } from "../handlers/keys.tsx";
import { RegionPinContext } from "../handlers/utils.tsx";
import type { Context } from "../router";
import { CommandGate } from "./CommandGate";

export interface RootProps {
// path is the command path to the executing node (e.g. "/agentcore").
Expand Down Expand Up @@ -260,60 +261,39 @@ function RouteTable({ ctx, core }: ScreenProps) {
element={<HarnessGetJsonScreen ctx={ctx} core={core} />}
/>
<Route path="agentcore/harness/list" element={<HarnessListScreen ctx={ctx} core={core} />} />
<Route
path="agentcore/harness/create"
element={<HarnessCreateScreen ctx={ctx} core={core} />}
/>
<Route
path="agentcore/harness/update"
element={<HarnessUpdateScreen ctx={ctx} core={core} />}
/>
<Route
path="agentcore/harness/update/:harnessId"
element={<HarnessUpdateScreen ctx={ctx} core={core} />}
/>
<Route
path="agentcore/harness/delete"
element={<HarnessDeleteScreen ctx={ctx} core={core} />}
/>
<Route
path="agentcore/harness/delete/:harnessId"
element={<HarnessDeleteScreen ctx={ctx} core={core} />}
/>
<Route
path="agentcore/harness/invoke"
element={<HarnessInvokeScreen ctx={ctx} core={core} />}
/>
<Route
path="agentcore/harness/invoke/:harnessId"
element={<HarnessInvokeScreen ctx={ctx} core={core} />}
/>
{/* Deep link that resumes an existing runtime session in the chat. */}
<Route
path="agentcore/harness/invoke/:harnessId/:sessionId"
element={<HarnessInvokeScreen ctx={ctx} core={core} />}
/>
<Route path="agentcore/harness/exec" element={<HarnessExecScreen ctx={ctx} core={core} />} />
<Route
path="agentcore/harness/exec/:harnessId"
element={<HarnessExecScreen ctx={ctx} core={core} />}
/>
<Route
path="agentcore/harness/exec/:harnessId/:sessionId"
element={<HarnessExecScreen ctx={ctx} core={core} />}
/>
<Route path="agentcore/harness/create" element={<CommandGate ctx={ctx} />}>
<Route index element={<HarnessCreateScreen ctx={ctx} core={core} />} />
</Route>
<Route path="agentcore/harness/update" element={<CommandGate ctx={ctx} />}>
<Route index element={<HarnessUpdateScreen ctx={ctx} core={core} />} />
<Route path=":harnessId" element={<HarnessUpdateScreen ctx={ctx} core={core} />} />
</Route>
<Route path="agentcore/harness/delete" element={<CommandGate ctx={ctx} />}>
<Route index element={<HarnessDeleteScreen ctx={ctx} core={core} />} />
<Route path=":harnessId" element={<HarnessDeleteScreen ctx={ctx} core={core} />} />
</Route>
<Route path="agentcore/harness/invoke" element={<CommandGate ctx={ctx} />}>
<Route index element={<HarnessInvokeScreen ctx={ctx} core={core} />} />
<Route path=":harnessId" element={<HarnessInvokeScreen ctx={ctx} core={core} />} />
{/* Deep link that resumes an existing runtime session in the chat. */}
<Route
path=":harnessId/:sessionId"
element={<HarnessInvokeScreen ctx={ctx} core={core} />}
/>
</Route>
<Route path="agentcore/harness/exec" element={<CommandGate ctx={ctx} />}>
<Route index element={<HarnessExecScreen ctx={ctx} core={core} />} />
<Route path=":harnessId" element={<HarnessExecScreen ctx={ctx} core={core} />} />
<Route path=":harnessId/:sessionId" element={<HarnessExecScreen ctx={ctx} core={core} />} />
</Route>
<Route
path="agentcore/harness/endpoint"
element={<HarnessEndpointScreen ctx={ctx} core={core} />}
/>
<Route
path="agentcore/harness/endpoint/create"
element={<HarnessCreateEndpointScreen ctx={ctx} core={core} />}
/>
<Route
path="agentcore/harness/endpoint/create/:harnessId"
element={<HarnessCreateEndpointScreen ctx={ctx} core={core} />}
/>
<Route path="agentcore/harness/endpoint/create" element={<CommandGate ctx={ctx} />}>
<Route index element={<HarnessCreateEndpointScreen ctx={ctx} core={core} />} />
<Route path=":harnessId" element={<HarnessCreateEndpointScreen ctx={ctx} core={core} />} />
</Route>
{/* Bare `endpoint get` (no target) has nothing to show — send the
user to the endpoint listing (same idea for `version get`). */}
<Route
Expand All @@ -332,30 +312,22 @@ function RouteTable({ ctx, core }: ScreenProps) {
path="agentcore/harness/endpoint/list/:harnessId"
element={<HarnessListEndpointsScreen ctx={ctx} core={core} />}
/>
<Route
path="agentcore/harness/endpoint/update"
element={<HarnessUpdateEndpointScreen ctx={ctx} core={core} />}
/>
<Route
path="agentcore/harness/endpoint/update/:harnessId"
element={<HarnessUpdateEndpointScreen ctx={ctx} core={core} />}
/>
<Route
path="agentcore/harness/endpoint/update/:harnessId/:endpointName"
element={<HarnessUpdateEndpointScreen ctx={ctx} core={core} />}
/>
<Route
path="agentcore/harness/endpoint/delete"
element={<HarnessDeleteEndpointScreen ctx={ctx} core={core} />}
/>
<Route
path="agentcore/harness/endpoint/delete/:harnessId"
element={<HarnessDeleteEndpointScreen ctx={ctx} core={core} />}
/>
<Route
path="agentcore/harness/endpoint/delete/:harnessId/:endpointName"
element={<HarnessDeleteEndpointScreen ctx={ctx} core={core} />}
/>
<Route path="agentcore/harness/endpoint/update" element={<CommandGate ctx={ctx} />}>
<Route index element={<HarnessUpdateEndpointScreen ctx={ctx} core={core} />} />
<Route path=":harnessId" element={<HarnessUpdateEndpointScreen ctx={ctx} core={core} />} />
<Route
path=":harnessId/:endpointName"
element={<HarnessUpdateEndpointScreen ctx={ctx} core={core} />}
/>
</Route>
<Route path="agentcore/harness/endpoint/delete" element={<CommandGate ctx={ctx} />}>
<Route index element={<HarnessDeleteEndpointScreen ctx={ctx} core={core} />} />
<Route path=":harnessId" element={<HarnessDeleteEndpointScreen ctx={ctx} core={core} />} />
<Route
path=":harnessId/:endpointName"
element={<HarnessDeleteEndpointScreen ctx={ctx} core={core} />}
/>
</Route>
<Route
path="agentcore/harness/version"
element={<HarnessVersionScreen ctx={ctx} core={core} />}
Expand Down Expand Up @@ -456,30 +428,22 @@ function RouteTable({ ctx, core }: ScreenProps) {
path="agentcore/memory/get/:memoryId/json"
element={<MemoryGetJsonScreen ctx={ctx} core={core} />}
/>
<Route
path="agentcore/runtime/invoke"
element={<RuntimeInvokeScreen ctx={ctx} core={core} />}
/>
<Route
path="agentcore/runtime/invoke/:runtimeId"
element={<RuntimeInvokeScreen ctx={ctx} core={core} />}
/>
<Route
path="agentcore/runtime/invoke/:runtimeId/:qualifier"
element={<RuntimeInvokeScreen ctx={ctx} core={core} />}
/>
<Route
path="agentcore/runtime/shell"
element={<RuntimeShellScreen ctx={ctx} core={core} />}
/>
<Route
path="agentcore/runtime/shell/:runtimeId"
element={<RuntimeShellScreen ctx={ctx} core={core} />}
/>
<Route
path="agentcore/runtime/shell/:runtimeId/:qualifier"
element={<RuntimeShellScreen ctx={ctx} core={core} />}
/>
<Route path="agentcore/runtime/invoke" element={<CommandGate ctx={ctx} />}>
<Route index element={<RuntimeInvokeScreen ctx={ctx} core={core} />} />
<Route path=":runtimeId" element={<RuntimeInvokeScreen ctx={ctx} core={core} />} />
<Route
path=":runtimeId/:qualifier"
element={<RuntimeInvokeScreen ctx={ctx} core={core} />}
/>
</Route>
<Route path="agentcore/runtime/shell" element={<CommandGate ctx={ctx} />}>
<Route index element={<RuntimeShellScreen ctx={ctx} core={core} />} />
<Route path=":runtimeId" element={<RuntimeShellScreen ctx={ctx} core={core} />} />
<Route
path=":runtimeId/:qualifier"
element={<RuntimeShellScreen ctx={ctx} core={core} />}
/>
</Route>
<Route path="agentcore/gateway" element={<GatewayScreen ctx={ctx} core={core} />} />
<Route
path="agentcore/gateway/create"
Expand All @@ -498,14 +462,10 @@ function RouteTable({ ctx, core }: ScreenProps) {
path="agentcore/gateway/get/:gatewayId/json"
element={<GatewayGetJsonScreen ctx={ctx} core={core} />}
/>
<Route
path="agentcore/gateway/invoke"
element={<GatewayInvokeScreen ctx={ctx} core={core} />}
/>
<Route
path="agentcore/gateway/invoke/:gatewayId"
element={<GatewayInvokeScreen ctx={ctx} core={core} />}
/>
<Route path="agentcore/gateway/invoke" element={<CommandGate ctx={ctx} />}>
<Route index element={<GatewayInvokeScreen ctx={ctx} core={core} />} />
<Route path=":gatewayId" element={<GatewayInvokeScreen ctx={ctx} core={core} />} />
</Route>
<Route
path="agentcore/gateway/target"
element={<GatewayTargetScreen ctx={ctx} core={core} />}
Expand Down Expand Up @@ -567,14 +527,10 @@ function RouteTable({ ctx, core }: ScreenProps) {
path="agentcore/gateway/policy"
element={<Navigate to="/agentcore/gateway/policy/generate" replace />}
/>
<Route
path="agentcore/gateway/policy/generate"
element={<GatewayPolicyGenerateScreen ctx={ctx} core={core} />}
/>
<Route
path="agentcore/gateway/policy/generate/:gatewayId"
element={<GatewayPolicyGenerateScreen ctx={ctx} core={core} />}
/>
<Route path="agentcore/gateway/policy/generate" element={<CommandGate ctx={ctx} />}>
<Route index element={<GatewayPolicyGenerateScreen ctx={ctx} core={core} />} />
<Route path=":gatewayId" element={<GatewayPolicyGenerateScreen ctx={ctx} core={core} />} />
</Route>
<Route path="agentcore/eval" element={<EvalScreen ctx={ctx} core={core} />} />
<Route path="agentcore/eval/evaluator" element={<EvaluatorScreen ctx={ctx} core={core} />} />
<Route
Expand Down
5 changes: 4 additions & 1 deletion src/handlers/gateway/create/screen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ import type { ScreenProps } from "../../types";
export function GatewayCreateScreen(props: ScreenProps) {
const gateway = resolveCommand(props.ctx.require(CommandKey), ["agentcore", "gateway"]);
const path = ["agentcore", "gateway", "create"];
if (gateway.commands.some((command) => command.name() === "create")) {
if (
gateway.name() === "gateway" &&
gateway.commands.some((command) => command.name() === "create")
) {
return <CliOnlyScreen {...props} path={path} />;
}

Expand Down
Loading
Loading