Skip to content

Commit f7c2ab7

Browse files
committed
feat: implement TUI for identity
1 parent 70d0752 commit f7c2ab7

26 files changed

Lines changed: 1304 additions & 60 deletions

File tree

README.md

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ It gives you two ways to work, from the same binary:
99
- **A scriptable CLI** — every operation is a flag-driven subcommand that emits
1010
JSON (`--json`), so it can be used by codeing agents and can drop cleanly into
1111
scripts, CI, and automation.
12-
- **An interactive TUI** — bare Harness and Runtime branches and leaves open
13-
their corresponding menus and selection flows.
12+
- **An interactive TUI** — bare Harness, Runtime, and Identity branches and
13+
leaves open their corresponding menus and selection flows.
1414

1515
```bash
1616
agentcore # launch the interactive TUI
@@ -26,8 +26,8 @@ responses. `agentcore` wraps all of that behind one ergonomic tool.
2626

2727
## Command surface
2828

29-
Commands with operation flags run headlessly. Bare Harness and Runtime branches
30-
and leaves open their interactive flows.
29+
Commands with operation flags run headlessly. Bare Harness, Runtime, and
30+
Identity branches and leaves open their interactive flows.
3131

3232
```
3333
agentcore # interactive TUI
@@ -49,12 +49,18 @@ agentcore # interactive TUI
4949
│ ├── update
5050
│ └── delete
5151
├── identity # manage AgentCore Identity resources
52-
│ └── api-key-credential-provider
53-
│ ├── create # create an API key credential provider
54-
│ ├── get # get an API key credential provider
55-
│ ├── list # list API key credential providers
56-
│ ├── update # update an API key credential provider
57-
│ └── delete # delete an API key credential provider
52+
│ ├── api-key-credential-provider
53+
│ │ ├── create # create an API key credential provider
54+
│ │ ├── get # get an API key credential provider
55+
│ │ ├── list # list API key credential providers
56+
│ │ ├── update # update an API key credential provider
57+
│ │ └── delete # delete an API key credential provider
58+
│ └── oauth2-credential-provider
59+
│ ├── create # create an OAuth2 credential provider
60+
│ ├── get # get an OAuth2 credential provider
61+
│ ├── list # list OAuth2 credential providers
62+
│ ├── update # update an OAuth2 credential provider
63+
│ └── delete # delete an OAuth2 credential provider
5864
├── runtime # inspect deployed AgentCore Runtimes
5965
│ ├── get # fetch a Runtime by id
6066
│ ├── list # list Runtimes (server-side paginated)
@@ -152,6 +158,17 @@ agentcore identity api-key-credential-provider list --max-results 10
152158
agentcore identity api-key-credential-provider update --name my-provider --api-key <new-key>
153159
agentcore identity api-key-credential-provider delete --name my-provider
154160

161+
# Manage OAuth2 credential providers (guided Custom OAuth2, or --provider-configuration for other vendors)
162+
agentcore identity oauth2-credential-provider create \
163+
--name my-oauth-provider \
164+
--vendor CustomOauth2 \
165+
--client-id <client-id> \
166+
--discovery-url https://issuer.example.com/.well-known/openid-configuration \
167+
--client-secret -
168+
agentcore identity oauth2-credential-provider get --name my-oauth-provider
169+
agentcore identity oauth2-credential-provider list --max-results 10
170+
agentcore identity oauth2-credential-provider delete --name my-oauth-provider
171+
155172
# Manage evaluators
156173
# Create an LLM-as-a-Judge evaluator with a rating-scale preset.
157174
agentcore eval evaluator llm-as-a-judge create \
@@ -271,6 +288,19 @@ agentcore runtime version list
271288
agentcore runtime endpoint list
272289
```
273290

291+
The Identity TUI is read-only: bare `identity` branches and the `get`/`list`
292+
leaves open interactive menus and detail views. Mutations (`create`, `update`,
293+
`delete`) are intentionally CLI-only — selecting one in the TUI points you at the
294+
equivalent CLI command rather than hosting a write flow.
295+
296+
```bash
297+
agentcore identity
298+
agentcore identity api-key-credential-provider list
299+
agentcore identity api-key-credential-provider get
300+
agentcore identity oauth2-credential-provider list
301+
agentcore identity oauth2-credential-provider get
302+
```
303+
274304
---
275305

276306
# Architecture & patterns

src/components/CliOnlyScreen.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { Box, Text, useInput } from "ink";
2+
import { useNavigate } from "react-router";
3+
import { Layout } from "./Layout";
4+
import { darkTheme } from "./ui/_core.js";
5+
import type { ScreenProps } from "../handlers/types";
6+
7+
export interface CliOnlyScreenProps extends ScreenProps {
8+
breadcrumb: string[];
9+
// command is the CLI invocation shown to the user
10+
// operation, e.g. "agentcore identity api-key-credential-provider create --help".
11+
command: string;
12+
}
13+
14+
// CliOnlyScreen is the supported TUI experience for operations that are
15+
// intentionally CLI-only
16+
export function CliOnlyScreen({ breadcrumb, command }: CliOnlyScreenProps) {
17+
const navigate = useNavigate();
18+
19+
useInput((_input, key) => {
20+
if (key.escape) navigate(-1);
21+
});
22+
23+
return (
24+
<Layout
25+
breadcrumb={breadcrumb}
26+
keyHints={[
27+
{ key: "esc", label: "back" },
28+
{ key: "ctl+c", label: "quit" },
29+
]}
30+
>
31+
<Box flexDirection="column" paddingLeft={1}>
32+
<Text color={darkTheme.colors.text}>This operation is available via the CLI:</Text>
33+
<Box paddingTop={1}>
34+
<Text color={darkTheme.colors.focus}>{command}</Text>
35+
</Box>
36+
</Box>
37+
</Layout>
38+
);
39+
}

src/components/PaginatedTablePicker.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export interface PaginatedTablePickerProps<TItem, TRow extends Record<string, un
2626
errorMessage: (error: Error) => string;
2727
emptyMessage: string;
2828
emptyPageMessage: string;
29+
maxPageSize?: number;
2930
}
3031

3132
export function PaginatedTablePicker<TItem, TRow extends Record<string, unknown>>({
@@ -43,8 +44,9 @@ export function PaginatedTablePicker<TItem, TRow extends Record<string, unknown>
4344
errorMessage,
4445
emptyMessage,
4546
emptyPageMessage,
47+
maxPageSize,
4648
}: PaginatedTablePickerProps<TItem, TRow>) {
47-
const paging = usePagedList();
49+
const paging = usePagedList(maxPageSize);
4850
const list = useQuery({
4951
queryKey: [...queryKey, paging.pageSize, paging.token],
5052
queryFn: () => loadPage(paging.token, paging.pageSize),

src/components/Root.tsx

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,25 @@ import { RuntimeListEndpointsScreen } from "../handlers/runtime/endpoint/list/sc
2828
import { RuntimeVersionScreen } from "../handlers/runtime/version/screen.tsx";
2929
import { RuntimeGetVersionScreen } from "../handlers/runtime/version/get/screen.tsx";
3030
import { RuntimeListVersionsScreen } from "../handlers/runtime/version/list/screen.tsx";
31+
import { IdentityScreen } from "../handlers/identity/screen.tsx";
32+
import { ApiKeyCredentialProviderScreen } from "../handlers/identity/api-key-credential-provider/screen.tsx";
33+
import { ApiKeyCredentialProviderListScreen } from "../handlers/identity/api-key-credential-provider/list/screen.tsx";
34+
import {
35+
ApiKeyCredentialProviderGetScreen,
36+
ApiKeyCredentialProviderGetJsonScreen,
37+
} from "../handlers/identity/api-key-credential-provider/get/screen.tsx";
38+
import { ApiKeyCredentialProviderCreateScreen } from "../handlers/identity/api-key-credential-provider/create/screen.tsx";
39+
import { ApiKeyCredentialProviderUpdateScreen } from "../handlers/identity/api-key-credential-provider/update/screen.tsx";
40+
import { ApiKeyCredentialProviderDeleteScreen } from "../handlers/identity/api-key-credential-provider/delete/screen.tsx";
41+
import { Oauth2CredentialProviderScreen } from "../handlers/identity/oauth2-credential-provider/screen.tsx";
42+
import { Oauth2CredentialProviderListScreen } from "../handlers/identity/oauth2-credential-provider/list/screen.tsx";
43+
import {
44+
Oauth2CredentialProviderGetScreen,
45+
Oauth2CredentialProviderGetJsonScreen,
46+
} from "../handlers/identity/oauth2-credential-provider/get/screen.tsx";
47+
import { Oauth2CredentialProviderCreateScreen } from "../handlers/identity/oauth2-credential-provider/create/screen.tsx";
48+
import { Oauth2CredentialProviderUpdateScreen } from "../handlers/identity/oauth2-credential-provider/update/screen.tsx";
49+
import { Oauth2CredentialProviderDeleteScreen } from "../handlers/identity/oauth2-credential-provider/delete/screen.tsx";
3150
import { RootScreen, HelpScreen } from "../handlers/screen.tsx";
3251
import type { Context } from "../router";
3352

@@ -255,6 +274,72 @@ export function Root({ path, ctx, core, queryClient }: RootProps) {
255274
path="agentcore/runtime/endpoint/list/:runtimeId"
256275
element={<RuntimeListEndpointsScreen ctx={ctx} core={core} />}
257276
/>
277+
<Route path="agentcore/identity" element={<IdentityScreen ctx={ctx} core={core} />} />
278+
<Route
279+
path="agentcore/identity/api-key-credential-provider"
280+
element={<ApiKeyCredentialProviderScreen ctx={ctx} core={core} />}
281+
/>
282+
{/* Bare `get` (no name) has nothing to show — send the user to the list. */}
283+
<Route
284+
path="agentcore/identity/api-key-credential-provider/get"
285+
element={<Navigate to="/agentcore/identity/api-key-credential-provider/list" replace />}
286+
/>
287+
<Route
288+
path="agentcore/identity/api-key-credential-provider/list"
289+
element={<ApiKeyCredentialProviderListScreen ctx={ctx} core={core} />}
290+
/>
291+
<Route
292+
path="agentcore/identity/api-key-credential-provider/get/:name"
293+
element={<ApiKeyCredentialProviderGetScreen ctx={ctx} core={core} />}
294+
/>
295+
<Route
296+
path="agentcore/identity/api-key-credential-provider/get/:name/json"
297+
element={<ApiKeyCredentialProviderGetJsonScreen ctx={ctx} core={core} />}
298+
/>
299+
<Route
300+
path="agentcore/identity/api-key-credential-provider/create"
301+
element={<ApiKeyCredentialProviderCreateScreen ctx={ctx} core={core} />}
302+
/>
303+
<Route
304+
path="agentcore/identity/api-key-credential-provider/update"
305+
element={<ApiKeyCredentialProviderUpdateScreen ctx={ctx} core={core} />}
306+
/>
307+
<Route
308+
path="agentcore/identity/api-key-credential-provider/delete"
309+
element={<ApiKeyCredentialProviderDeleteScreen ctx={ctx} core={core} />}
310+
/>
311+
<Route
312+
path="agentcore/identity/oauth2-credential-provider"
313+
element={<Oauth2CredentialProviderScreen ctx={ctx} core={core} />}
314+
/>
315+
<Route
316+
path="agentcore/identity/oauth2-credential-provider/get"
317+
element={<Navigate to="/agentcore/identity/oauth2-credential-provider/list" replace />}
318+
/>
319+
<Route
320+
path="agentcore/identity/oauth2-credential-provider/list"
321+
element={<Oauth2CredentialProviderListScreen ctx={ctx} core={core} />}
322+
/>
323+
<Route
324+
path="agentcore/identity/oauth2-credential-provider/get/:name"
325+
element={<Oauth2CredentialProviderGetScreen ctx={ctx} core={core} />}
326+
/>
327+
<Route
328+
path="agentcore/identity/oauth2-credential-provider/get/:name/json"
329+
element={<Oauth2CredentialProviderGetJsonScreen ctx={ctx} core={core} />}
330+
/>
331+
<Route
332+
path="agentcore/identity/oauth2-credential-provider/create"
333+
element={<Oauth2CredentialProviderCreateScreen ctx={ctx} core={core} />}
334+
/>
335+
<Route
336+
path="agentcore/identity/oauth2-credential-provider/update"
337+
element={<Oauth2CredentialProviderUpdateScreen ctx={ctx} core={core} />}
338+
/>
339+
<Route
340+
path="agentcore/identity/oauth2-credential-provider/delete"
341+
element={<Oauth2CredentialProviderDeleteScreen ctx={ctx} core={core} />}
342+
/>
258343
<Route path="*" element={<HelpScreen ctx={ctx} core={core} />} />
259344
</Routes>
260345
</MemoryRouter>

src/components/usePagedList.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,13 @@ function initialPagination(pageSize: number): PaginationState {
3737
// usePagedList holds the server-side pagination state shared by the picker
3838
// tables: a terminal-height-derived page size and the trail of nextTokens
3939
// leading to the current page, so ←/h can walk back through cached pages.
40-
export function usePagedList(): PagedList {
40+
//
41+
// maxPageSize caps the terminal-derived page size for APIs that constrain
42+
// maxResults (e.g. identity list operation capped at 20)
43+
export function usePagedList(maxPageSize?: number): PagedList {
4144
const { rows } = useWindowSize();
42-
const pageSize = Math.max(3, rows - CHROME_ROWS);
45+
const fitsTerminal = Math.max(3, rows - CHROME_ROWS);
46+
const pageSize = maxPageSize ? Math.min(fitsTerminal, maxPageSize) : fitsTerminal;
4347

4448
const [state, setState] = useState<PaginationState>(() => initialPagination(pageSize));
4549
const pageSizeChanged = state.pageSize !== pageSize;

0 commit comments

Comments
 (0)