feat(invoke): overload invoke for project and existing Runtimes, harnesses, and Gateways - #2399
Conversation
…nesses, and Gateways BREAKING CHANGE: the invoke runtime, invoke harness, runtime invoke, harness invoke, and gateway invoke commands are removed. Use agentcore invoke with --runtime, --harness, or --gateway, which accept a project name, ID, or ARN.
|
Claude Security Review: no high-confidence findings. (run) |
There was a problem hiding this comment.
AgentCore Harness Review
Verdict: Looks good
Nice refactor. The unified invoke command reads well, the resource-selection logic (selectResource + REQUEST_FLAGS) cleanly enforces per-resource flag scoping, and the route/deep-link updates are consistent across screens (Root.tsx, runtime/invoke/screen.tsx, gateway/invoke/screen.tsx, harness/get/screen.tsx, etc.). Tests are thorough and stay on the right side of the mocking line (real TestCoreClient, real temp directories, renderTuiAt only spied at the TUI boundary; the new toResourceArn suite in utils.test.tsx is a good addition).
A couple of small things worth being aware of — not blockers:
- In
src/handlers/project/invoke/index.tsx,headlessis set whenever any flag is present (Object.values(flags).some(isSet)). Something benign likeagentcore invoke --target prodinside a multi-resource project will therefore fall through the picker and error with "Choose a resource to invoke: …" instead of opening the TUI on that target. Similarly--targetisn't threaded intoProjectInvokePickerScreenwhen the TUI does open, so it's silently ignored. Probably fine given the error message points users at the right flags, but worth confirming that's the intended UX. - Collapsing
invoke runtime|harness|gatewayinto a single command path means the auto-emittedcommand_pathtelemetry attribute no longer distinguishes the three resource kinds. If invoke-by-resource-type is a metric you care about, consider setting aresource_typeattribute onCommandRunMetricEventKeyfrom the handler.
The runtime, harness, and gateway invoke commands stay behind the imperative-commands setting with their routes unchanged. agentcore invoke resolves the resource ARN, then runs the matching standalone handler with the service ID and the ARN's region on the context.
|
Claude Security Review: no high-confidence findings. (run) |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## refactor #2399 +/- ##
============================================
+ Coverage 97.25% 97.28% +0.03%
============================================
Files 612 611 -1
Lines 41019 40987 -32
============================================
- Hits 39893 39875 -18
+ Misses 1126 1112 -14 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Merge the runtime, harness, and gateway invoke flag lists instead of copying them. An interactive bare invoke always opens the picker so the deployment target is chosen there. Simplify picker rows and update a stale comment.
|
Claude Security Review: no high-confidence findings. (run) |
…kups Project names resolve through resolveDeployedResource, now covering Gateways, so the delegated handler gets the target's region and account-verified credential provider. IDs and ARNs resolve locally and the existing handler does its single lookup. The deployment target is passed explicitly instead of through a router context key, and the picker keeps the target credentials and region.
|
Claude Security Review: no high-confidence findings. (run) |
| const resourceFlags = [ | ||
| flag( | ||
| "runtime", | ||
| "the Runtime to invoke: a project name, ID, or ARN", |
There was a problem hiding this comment.
a little confusing I would do : resource id in project or ARN
There was a problem hiding this comment.
Good call, it read a bit oddly. Changed it to "its name in this project, its ID, or its ARN". It's the project name that gets matched, not an ID, so I kept that part.
| return name.replace(/_/g, ""); | ||
| } | ||
|
|
||
| function findDeployedResourceId( |
There was a problem hiding this comment.
OOS, but doesn't this function fail if resourceType isn't a gateway, harness or runtime?
There was a problem hiding this comment.
It's typed to runtime, harness, and gateway only, so anything else won't compile. Those are the three types this function handles.
| import { InputValidationError, ResourceNotFoundError } from "../../errors"; | ||
| import type { Project, ProjectInvokableResource } from "./types"; | ||
|
|
||
| export const RESOURCE_LABELS: Record<ProjectInvokableResource, string> = { |
There was a problem hiding this comment.
nit: i feel like we have this same constant / mapping defined in a few places. Wondering if we should consolidate.
There was a problem hiding this comment.
Yeah, there were two copies. Now there's one RESOURCE_LABELS in handlers/project/types.ts, and ProjectManager uses it too.
| return `\nParameter details:\n\n${sections.join("\n\n")}\n`; | ||
| } | ||
|
|
||
| export function formatExamples(examples: HelpExample[]): string | undefined { |
There was a problem hiding this comment.
how is this related to the lifting of the command?
There was a problem hiding this comment.
It isn't really. I pulled it out into its own branch (feat/help-examples) and I'll open that PR once this one lands.
| children(): Handler[]; | ||
| } | ||
|
|
||
| export interface HelpExample { |
There was a problem hiding this comment.
this feels like a tangential change to the goal of this PR unless I'm missing a connection.
There was a problem hiding this comment.
Same here, moved it to feat/help-examples.
| const given = RESOURCE_TYPES.find((resourceType) => flags[resourceType] !== undefined); | ||
| if (given) return [given, flags[given]!]; | ||
|
|
||
| if (!project) { |
There was a problem hiding this comment.
I'm not fully following the logic here.
Its saying that when selecting the resource to invoke:
- If i'm not in a project and in headless, error with no project found.
- if I'm not in a project and i'm in a tui, return undefined.
Whats the intuition for this? I would have expected selectResource to be independent of TUI, and not to throw a project not found error?
There was a problem hiding this comment.
Fair, it was doing two jobs at once: picking the resource, and deciding whether the TUI opens. The function returning undefined meant the picker would handle selecting what to do.
I split it so neither function knows about the TUI:
flaggedResourcereturns whatever--runtime,--harness, or--gatewaynamed, or nothing.soleProjectResourcereturns the only resource in the project, or throws saying why (no project, no resources, or several to pick from).
The handler makes the TUI call:
const selection =
flaggedResource(flags) ?? (headless ? soleProjectResource(project) : undefined);
if (!selection) // open the pickerSo "no project found" only comes up on the headless path, where there's no picker to show it. In the TUI, the picker already has its own no-project screen.
There was a problem hiding this comment.
discussed in person, we can simplify by opening the TUI on missing selector rather than trying to be smart and detect if there is a single resource first.
| gateway: invokeGatewayFlags, | ||
| } as const; | ||
|
|
||
| type RequestFlag = Exclude<(typeof INVOKE_FLAGS)[ProjectInvokableResource][number], { name: "id" }>; |
There was a problem hiding this comment.
whats the intuition behind a request flag? what makes it different from a normal flag?
There was a problem hiding this comment.
Bad name on my part. They're the flags that belong to runtime/harness/gateway invoke, merged by name and passed through to whichever one runs. Renamed them to handlerFlags. The ones invoke owns itself (--runtime, --harness, --gateway, --target, --local, --port) are selectionFlags now.
| if (resolved.credentials) { | ||
| invokeCtx = invokeCtx.withValue(AwsCredentialProviderKey, resolved.credentials); | ||
| } | ||
| await invoker.handle(invokeCtx, request, {}); |
There was a problem hiding this comment.
out of curiosity, whats the advantage of calling the handlers v.s. calling the underlying functionality directly?
There was a problem hiding this comment.
Mostly so there's one copy of each request path. The handlers already do the flag checks, stdin payloads, output, progress, cancel on Ctrl-C, and the TUI deep link. My first pass called the operations directly and ended up copying about 150 lines per type. This way invoke and runtime/harness/gateway invoke can't drift apart.
| ...(resourceType === "runtime" ? ["local", "port"] : []), | ||
| ...invoker.flags().map(({ name }) => name), | ||
| ]; | ||
| const misplaced = Object.entries(flags).find( |
There was a problem hiding this comment.
nit: are there better names for these variables? invoker, misplaced, selected, etc. all feel a little ambiguous?
There was a problem hiding this comment.
Agreed. Renamed invoker to handler, selected to selection, misplaced to unsupported, allowed to acceptedFlags, and request to parsedHandlerFlags.
Split resource selection so it no longer depends on the TUI, rename the forwarded flags to handlerFlags, keep one RESOURCE_LABELS map in the project types, reword the resource flag help, and move the help examples feature to its own PR.
|
On the automated review: |
|
Claude Security Review: no high-confidence findings. (run) |
A bare interactive invoke always opens the picker, and a headless invoke needs exactly one of --runtime, --harness, or --gateway. Docs and template READMEs name the resource explicitly, and the minimal template now renders its README.
|
Claude Security Review: no high-confidence findings. (run) |
What
agentcore invokeis now one command that invokes a Runtime, harness, or Gateway. It replaces theinvoke runtimeandinvoke harnesssubcommands.--runtime,--harness, and--gatewayeach take a project name, an ID, or an ARN.resolveResourceinsrc/handlers/utils.tsxreturns{ id, region, credentials }.resolveDeployedResource, which now covers Gateways. It returns the target's region and account-verified credential provider.invokethen runs the existingruntime invoke,harness invoke, orgateway invokehandler with that ID, region, and credentials on the context. There is one copy of each request path, and the existing TUIs and routes are unchanged.invokeopens the picker, which now lists Gateways too. A headless call needs exactly one of--runtime,--harness, or--gateway.--localstays project Runtime only.The standalone
runtime|harness|gateway invokecommands are unchanged and stay behindimperative-commands.Why
The CLI is now project first (#2397, #2396). With the standalone families hidden by default, top-level
invokehas to reach both project resources and existing resources.How tested
bun testpasses, and typecheck and lint are clean.--region.invokepicker was used to open and use all three consoles.AWS_PROFILEfor a missing profile. This uses the target's verified credentials.imperative-commandson,runtime invoke,harness invoke, andgateway invokewere run headless and in the TUI. With it off, they stay hidden.