From bc1943190448359d93b878508e3013b27743fd79 Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Thu, 3 Sep 2026 18:08:25 +0000 Subject: [PATCH 01/26] feat(io): add interactive terminal lifecycle --- src/io/index.ts | 7 + src/io/interactiveTerminal.test.ts | 214 +++++++++++++++++++++++++++++ src/io/interactiveTerminal.ts | 126 +++++++++++++++++ 3 files changed, 347 insertions(+) create mode 100644 src/io/interactiveTerminal.test.ts create mode 100644 src/io/interactiveTerminal.ts diff --git a/src/io/index.ts b/src/io/index.ts index 843c4da82..b63139a83 100644 --- a/src/io/index.ts +++ b/src/io/index.ts @@ -19,6 +19,13 @@ export { type StreamProcessOptions, } from "./exec"; export { FsReadWriteJson } from "./json"; +export { + InteractiveTerminal, + type InteractiveTerminalConfig, + type InteractiveTerminalPeer, + type InteractiveTerminalResult, + type TerminalFrame, +} from "./interactiveTerminal"; export { readTextFile, type ReadTextFileOptions } from "./fileRead"; export { readOptionalBytes, resolvePackageFileDir } from "./packagedAssets"; export { diff --git a/src/io/interactiveTerminal.test.ts b/src/io/interactiveTerminal.test.ts new file mode 100644 index 000000000..f3332c9e9 --- /dev/null +++ b/src/io/interactiveTerminal.test.ts @@ -0,0 +1,214 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import { PassThrough } from "node:stream"; +import type { AppIO } from "./types"; +import { + InteractiveTerminal, + type InteractiveTerminalPeer, + type TerminalFrame, +} from "./interactiveTerminal"; +import { StreamController } from "../testing"; + +type TestStdin = NodeJS.ReadStream & { + write(chunk: Uint8Array | string): boolean; + isRaw?: boolean; + setRawMode(mode: boolean): TestStdin; +}; + +function terminalIO(): { + io: AppIO; + stdin: TestStdin; + stdout: () => string; + stderr: () => string; + rawModes: boolean[]; +} { + const stdin = new PassThrough() as unknown as TestStdin; + const stdout = new PassThrough(); + const stderr = new PassThrough(); + let stdoutText = ""; + let stderrText = ""; + const rawModes: boolean[] = []; + stdin.isRaw = false; + stdin.setRawMode = (mode) => { + rawModes.push(mode); + stdin.isRaw = mode; + return stdin; + }; + Object.defineProperty(stdin, "isTTY", { value: true }); + Object.defineProperties(stdout, { + isTTY: { value: true }, + columns: { configurable: true, value: 120 }, + rows: { configurable: true, value: 35 }, + }); + stderr.on("data", (chunk) => { + stderrText += chunk.toString(); + }); + stdout.on("data", (chunk) => { + stdoutText += chunk.toString(); + }); + return { + io: { + stdin, + stdout: stdout as unknown as NodeJS.WriteStream, + stderr: stderr as unknown as NodeJS.WriteStream, + }, + stdin, + stdout: () => stdoutText, + stderr: () => stderrText, + rawModes, + }; +} + +class FakePeer implements InteractiveTerminalPeer { + readonly frames = new StreamController(); + readonly sent: Uint8Array[] = []; + readonly resizes: { columns: number; rows: number }[] = []; + detached = 0; + + send(data: Uint8Array): Promise { + this.sent.push(Uint8Array.from(data)); + return Promise.resolve(); + } + + resize(columns: number, rows: number): Promise { + this.resizes.push({ columns, rows }); + return Promise.resolve(); + } + + detach(): Promise { + this.detached += 1; + this.frames.end(); + return Promise.resolve(); + } + + [Symbol.asyncIterator](): AsyncIterator { + return this.frames[Symbol.asyncIterator](); + } +} + +const activeTerminals: InteractiveTerminal[] = []; + +afterEach(async () => { + await Promise.all(activeTerminals.splice(0).map((terminal) => terminal.stop())); +}); + +function subject() { + const streams = terminalIO(); + let resizeListener: (() => void) | undefined; + let removed = 0; + const terminal = new InteractiveTerminal({ + io: streams.io, + dimensions: () => ({ + columns: streams.io.stdout.columns ?? 80, + rows: streams.io.stdout.rows ?? 24, + }), + onResize: (listener) => { + resizeListener = listener; + return () => { + removed += 1; + resizeListener = undefined; + }; + }, + }); + activeTerminals.push(terminal); + return { + ...streams, + terminal, + resize: () => resizeListener?.(), + resizeRemoved: () => removed, + }; +} + +describe("InteractiveTerminal", () => { + test("forwards raw stdin and remote output, then restores terminal state", async () => { + const s = subject(); + const peer = new FakePeer(); + const running = s.terminal.run(peer); + await Bun.sleep(0); + + s.stdin.write(Uint8Array.from([0x1b, 0x5b, 0x41])); + peer.frames.emit({ type: "stdout", data: new TextEncoder().encode("out") }); + peer.frames.emit({ type: "stderr", data: new TextEncoder().encode("err") }); + peer.frames.end(); + + await expect(running).resolves.toEqual({ detached: false }); + expect(peer.sent).toEqual([Uint8Array.from([0x1b, 0x5b, 0x41])]); + expect(s.stdout()).toBe("out"); + expect(s.stderr()).toBe("err"); + expect(s.rawModes).toEqual([true, false]); + expect(s.resizeRemoved()).toBe(1); + }); + + test("sends initial and subsequent terminal dimensions", async () => { + const s = subject(); + const peer = new FakePeer(); + const running = s.terminal.run(peer); + await Bun.sleep(0); + + expect(peer.resizes).toEqual([{ columns: 120, rows: 35 }]); + Object.defineProperties(s.io.stdout, { + columns: { configurable: true, value: 90 }, + rows: { configurable: true, value: 20 }, + }); + s.resize(); + await Bun.sleep(0); + expect(peer.resizes).toEqual([ + { columns: 120, rows: 35 }, + { columns: 90, rows: 20 }, + ]); + + peer.frames.end(); + await running; + }); + + test("consumes Ctrl+] as detach and forwards Ctrl+C", async () => { + const s = subject(); + const peer = new FakePeer(); + const running = s.terminal.run(peer); + await Bun.sleep(0); + + s.stdin.write(Uint8Array.from([0x03])); + s.stdin.write(Uint8Array.from([0x1d])); + + await expect(running).resolves.toEqual({ detached: true }); + expect(peer.sent).toEqual([Uint8Array.from([0x03])]); + expect(peer.detached).toBe(1); + expect(s.rawModes).toEqual([true, false]); + }); + + test("detaches and restores terminal state when aborted", async () => { + const s = subject(); + const peer = new FakePeer(); + const controller = new AbortController(); + const interrupted = new Error("interrupted"); + const running = s.terminal.run(peer, controller.signal); + await Bun.sleep(0); + + controller.abort(interrupted); + + await expect(running).rejects.toBe(interrupted); + expect(peer.detached).toBe(1); + expect(s.rawModes).toEqual([true, false]); + expect(s.resizeRemoved()).toBe(1); + }); + + test("restores terminal state when the remote stream fails", async () => { + const s = subject(); + const failure = new Error("stream failed"); + const peer: InteractiveTerminalPeer = { + send: async () => {}, + resize: async () => {}, + detach: async () => {}, + [Symbol.asyncIterator]() { + return { + next: async (): Promise> => { + throw failure; + }, + }; + }, + }; + + await expect(s.terminal.run(peer)).rejects.toBe(failure); + expect(s.rawModes).toEqual([true, false]); + expect(s.resizeRemoved()).toBe(1); + }); +}); diff --git a/src/io/interactiveTerminal.ts b/src/io/interactiveTerminal.ts new file mode 100644 index 000000000..855504a87 --- /dev/null +++ b/src/io/interactiveTerminal.ts @@ -0,0 +1,126 @@ +import type { AppIO } from "./types"; + +const DETACH_BYTE = 0x1d; + +export type TerminalFrame = + { type: "stdout"; data: Uint8Array } | { type: "stderr"; data: Uint8Array }; + +export interface InteractiveTerminalPeer extends AsyncIterable { + send(data: Uint8Array): Promise; + resize(columns: number, rows: number): Promise; + detach(): Promise; +} + +export type InteractiveTerminalResult = { + detached: boolean; +}; + +export type InteractiveTerminalConfig = { + io: AppIO; + dimensions?: () => { columns: number; rows: number }; + onResize?: (listener: () => void) => () => void; +}; + +export class InteractiveTerminal { + private readonly dimensions: () => { columns: number; rows: number }; + private readonly onResize: (listener: () => void) => () => void; + private stopCurrent?: () => Promise; + + constructor(private readonly config: InteractiveTerminalConfig) { + this.dimensions = + config.dimensions ?? + (() => ({ + columns: config.io.stdout.columns ?? 80, + rows: config.io.stdout.rows ?? 24, + })); + this.onResize = + config.onResize ?? + ((listener) => { + process.on("SIGWINCH", listener); + return () => process.off("SIGWINCH", listener); + }); + } + + async run( + peer: InteractiveTerminalPeer, + signal?: AbortSignal, + ): Promise { + if (this.stopCurrent) throw new Error("InteractiveTerminal is already running"); + + const { stdin, stdout, stderr } = this.config.io; + const wasPaused = stdin.isPaused(); + const wasRaw = (stdin as NodeJS.ReadStream & { isRaw?: boolean }).isRaw ?? false; + let detached = false; + let fail: (error: unknown) => void = () => {}; + let queuedFailure: unknown; + let hasQueuedFailure = false; + const failure = new Promise((_resolve, reject) => { + fail = reject; + }); + let pending = Promise.resolve(); + const enqueue = (operation: () => Promise) => { + pending = pending.then(operation).catch((error) => { + queuedFailure = error; + hasQueuedFailure = true; + fail(error); + }); + }; + const detach = async () => { + if (detached) return; + detached = true; + await peer.detach(); + }; + this.stopCurrent = detach; + + const onData = (chunk: Buffer | string) => { + const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, "binary"); + if (bytes.length === 1 && bytes[0] === DETACH_BYTE) { + enqueue(detach); + return; + } + enqueue(() => peer.send(bytes)); + }; + const resize = () => { + const { columns, rows } = this.dimensions(); + enqueue(() => peer.resize(columns, rows)); + }; + const removeResize = this.onResize(resize); + const abort = () => { + enqueue(async () => { + await detach(); + throw signal?.reason ?? new Error("terminal interrupted"); + }); + }; + signal?.addEventListener("abort", abort, { once: true }); + if (signal?.aborted) abort(); + + try { + if (stdin.isTTY && stdin.setRawMode) stdin.setRawMode(true); + stdin.on("data", onData); + stdin.resume(); + resize(); + + const pump = async () => { + for await (const frame of peer) { + const output = frame.type === "stdout" ? stdout : stderr; + output.write(frame.data); + } + }; + await Promise.race([pump(), failure]); + await pending; + if (hasQueuedFailure) throw queuedFailure; + return { detached }; + } finally { + signal?.removeEventListener("abort", abort); + removeResize(); + stdin.off("data", onData); + if (wasPaused) stdin.pause(); + if (stdin.isTTY && stdin.setRawMode) stdin.setRawMode(wasRaw); + this.stopCurrent = undefined; + } + } + + async stop(): Promise { + await this.stopCurrent?.(); + } +} From 24e41e8b9032c19445aaab39b4e83281c7aba122 Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Thu, 3 Sep 2026 18:08:40 +0000 Subject: [PATCH 02/26] feat(tui): add post-render handoff --- src/tui/handoff.test.ts | 21 +++++++++++++++++++++ src/tui/handoff.ts | 22 ++++++++++++++++++++++ src/tui/index.tsx | 6 +++++- 3 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 src/tui/handoff.test.ts create mode 100644 src/tui/handoff.ts diff --git a/src/tui/handoff.test.ts b/src/tui/handoff.test.ts new file mode 100644 index 000000000..c04e92afd --- /dev/null +++ b/src/tui/handoff.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, test } from "bun:test"; +import { TuiHandoffController } from "./handoff"; + +describe("TuiHandoffController", () => { + test("stores one handoff and returns it exactly once", () => { + const controller = new TuiHandoffController(); + const handoff = async () => {}; + + controller.request(handoff); + + expect(controller.take()).toBe(handoff); + expect(controller.take()).toBeUndefined(); + }); + + test("rejects a second handoff request", () => { + const controller = new TuiHandoffController(); + controller.request(async () => {}); + + expect(() => controller.request(async () => {})).toThrow("TUI handoff already requested"); + }); +}); diff --git a/src/tui/handoff.ts b/src/tui/handoff.ts new file mode 100644 index 000000000..729d8d842 --- /dev/null +++ b/src/tui/handoff.ts @@ -0,0 +1,22 @@ +import type { Core } from "../handlers/types"; +import type { AppIO } from "../io"; +import { contextKey, type Context } from "../router"; + +export type TuiHandoff = (input: { ctx: Context; core: Core; io: AppIO }) => Promise; + +export class TuiHandoffController { + private handoff?: TuiHandoff; + + request(handoff: TuiHandoff): void { + if (this.handoff) throw new Error("TUI handoff already requested"); + this.handoff = handoff; + } + + take(): TuiHandoff | undefined { + const handoff = this.handoff; + this.handoff = undefined; + return handoff; + } +} + +export const TuiHandoffKey = contextKey("tui.handoff"); diff --git a/src/tui/index.tsx b/src/tui/index.tsx index 26ff12549..60fbe5b78 100644 --- a/src/tui/index.tsx +++ b/src/tui/index.tsx @@ -14,6 +14,7 @@ import type { Core } from "../handlers/types"; import { JsonKey } from "../handlers/keys"; import { InvalidEnvironmentError } from "../errors"; import { ExitCode } from "../runnable"; +import { TuiHandoffController, TuiHandoffKey } from "./handoff"; // renderJson pretty-prints a value as indented JSON. It is the output // counterpart to renderTui: handlers call it to emit machine-readable results @@ -56,7 +57,9 @@ export async function renderTuiAt( // alternateScreen switches the terminal to its alternate buffer so the TUI // takes over the screen and the prior scrollback is restored on exit (like Vim). - const { waitUntilExit } = render(, { + const handoffs = new TuiHandoffController(); + const tuiCtx = ctx.withValue(TuiHandoffKey, handoffs); + const { waitUntilExit } = render(, { stdin: io.stdin, stdout: io.stdout, stderr: io.stderr, @@ -64,6 +67,7 @@ export async function renderTuiAt( incrementalRendering: true, }); await waitUntilExit(); + await handoffs.take()?.({ ctx: tuiCtx, core, io }); } // renderTui builds the root DefaultHandle that mounts the Ink React tree. It From af429920e10e819a45b904474d63c555a1723ad1 Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Thu, 3 Sep 2026 18:08:54 +0000 Subject: [PATCH 03/26] feat(runtime): add interactive shell transport --- bun.lock | 151 +++++++++++++++++++++++- package.json | 1 + src/core/index.tsx | 9 +- src/core/runtime.tsx | 17 +++ src/core/runtimeShell.test.ts | 202 +++++++++++++++++++++++++++++++++ src/core/runtimeShell.ts | 157 +++++++++++++++++++++++++ src/handlers/runtime/types.tsx | 26 +++++ src/index.ts | 2 + src/testing/TestCoreClient.tsx | 26 +++++ 9 files changed, 589 insertions(+), 2 deletions(-) create mode 100644 src/core/runtimeShell.test.ts create mode 100644 src/core/runtimeShell.ts diff --git a/bun.lock b/bun.lock index 3aa5d4eea..b11791008 100644 --- a/bun.lock +++ b/bun.lock @@ -21,6 +21,7 @@ "@opentelemetry/sdk-metrics": "^2.10.0", "@smithy/core": "^3.33.3", "@tanstack/react-query": "^5.101.2", + "bedrock-agentcore": "^0.4.3", "cli-truncate": "^6.1.1", "commander": "^15.0.0", "handlebars": "^4.7.9", @@ -82,6 +83,12 @@ "@aws-cdk/toolkit-lib": ["@aws-cdk/toolkit-lib@1.38.2", "", { "dependencies": { "@aws-cdk/cdk-assets-lib": "^1", "@aws-cdk/cloud-assembly-api": "2.3.0", "@aws-cdk/cloud-assembly-schema": ">=54.18.0", "@aws-cdk/cloudformation-diff": "^2", "@aws-cdk/cx-api": "^2", "@aws-sdk/client-appsync": "^3", "@aws-sdk/client-bedrock-agentcore-control": "^3", "@aws-sdk/client-cloudcontrol": "^3", "@aws-sdk/client-cloudformation": "^3", "@aws-sdk/client-cloudtrail": "^3", "@aws-sdk/client-cloudwatch-logs": "^3", "@aws-sdk/client-codebuild": "^3", "@aws-sdk/client-ec2": "^3", "@aws-sdk/client-ecr": "^3", "@aws-sdk/client-ecs": "^3", "@aws-sdk/client-elastic-load-balancing-v2": "^3", "@aws-sdk/client-iam": "^3", "@aws-sdk/client-kms": "^3", "@aws-sdk/client-lambda": "^3", "@aws-sdk/client-route-53": "^3", "@aws-sdk/client-s3": "^3", "@aws-sdk/client-secrets-manager": "^3", "@aws-sdk/client-sfn": "^3", "@aws-sdk/client-ssm": "^3", "@aws-sdk/client-sts": "^3", "@aws-sdk/credential-providers": "^3", "@aws-sdk/ec2-metadata-service": "^3", "@aws-sdk/lib-storage": "^3", "@smithy/middleware-endpoint": "^4", "@smithy/property-provider": "^4", "@smithy/shared-ini-file-loader": "^4", "@smithy/util-retry": "^4", "@smithy/util-waiter": "^4", "cdk-from-cfn": "^0.321.0", "chalk": "^4", "chokidar": "^4", "fast-deep-equal": "^3.1.3", "fast-glob": "^3.3.3", "fs-extra": "^11", "p-limit": "^3", "picomatch": "^4", "semver": "^7.8.5", "split2": "^4.2.0", "wrap-ansi": "^7", "yaml": "^1", "yazl": "^3.3.1" }, "peerDependencies": { "@aws-cdk/cli-plugin-contract": "^2" } }, "sha512-WsyPjZnLr4zk16sp1tuPGXUKZ9elHJLqcpogxfQ+92cKolw9srbRJe57Z5whMZ0HNXarOYf0pOm5hWKqr8UAEw=="], + "@aws-crypto/crc32": ["@aws-crypto/crc32@3.0.0", "", { "dependencies": { "@aws-crypto/util": "^3.0.0", "@aws-sdk/types": "^3.222.0", "tslib": "^1.11.1" } }, "sha512-IzSgsrxUcsrejQbPVilIKy16kAT52EwB6zSaI+M3xxIhKh5+aldEyvI+z6erM7TCLB2BJsFrtHjp6/4/sr+3dA=="], + + "@aws-crypto/sha256-js": ["@aws-crypto/sha256-js@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA=="], + + "@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="], + "@aws-sdk/checksums": ["@aws-sdk/checksums@3.1000.29", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-Dtu0gr4dnATZAPwEYbpCsG+MpLM7OAliy2gTepEFQwl1vZ6DL3QMH2FveMa3HLvPsOdhJsPRB3KtxVhph9T75A=="], "@aws-sdk/client-appsync": ["@aws-sdk/client-appsync@3.1121.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/credential-provider-node": "^3.972.81", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/fetch-http-handler": "^5.7.2", "@smithy/node-http-handler": "^4.11.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-plJfd6SEdhUgKLQrwPwZ6547DY+jyN6wUCaIh2/XX4nzSwCqfrNjP4uOUL+h79ELX3oHY1D8GuGMgVx3J9C+hw=="], @@ -162,12 +169,18 @@ "@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.44", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/signature-v4-multi-region": "^3.996.46", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/fetch-http-handler": "^5.7.2", "@smithy/node-http-handler": "^4.11.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-NhEgryjlBF9w38ZXqGymQV28IhkYa1mKhlbYnqIis57AYwWGVYfUPgg/qC2rLRqOUfblxx++irvju10kVTa8Vw=="], + "@aws-sdk/protocol-http": ["@aws-sdk/protocol-http@3.374.0", "", { "dependencies": { "@smithy/protocol-http": "^1.1.0", "tslib": "^2.5.0" } }, "sha512-9WpRUbINdGroV3HiZZIBoJvL2ndoWk39OfwxWs2otxByppJZNN14bg/lvCx5e8ggHUti7IBk5rb0nqQZ4m05pg=="], + + "@aws-sdk/signature-v4": ["@aws-sdk/signature-v4@3.374.0", "", { "dependencies": { "@smithy/signature-v4": "^1.0.1", "tslib": "^2.5.0" } }, "sha512-2xLJvSdzcZZAg0lsDLUAuSQuihzK0dcxIK7WmfuJeF7DGKJFmp9czQmz5f3qiDz6IDQzvgK1M9vtJSVCslJbyQ=="], + "@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.46", "", { "dependencies": { "@aws-sdk/types": "^3.974.5", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-L+2xZTye/2T96f3lwCws0Zw6GG2JHZW9e8FpVgGBeeExSKyeoZ6CWRpBml/7DNiK/O26jrgPM9F+Ay8VkgzUWQ=="], "@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1116.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/nested-clients": "^3.997.44", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-ygIivKqh8aHzNkucOCXHyIBgBpLPfrSI0mCqXF+vLBsPTUKqj0VSqAY0GFPe7lQl4HntjOcQ+KSyS7oUV2C54Q=="], "@aws-sdk/types": ["@aws-sdk/types@3.974.5", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ=="], + "@aws-sdk/util-utf8-browser": ["@aws-sdk/util-utf8-browser@3.259.0", "", { "dependencies": { "tslib": "^2.3.1" } }, "sha512-UvFa/vR+e19XookZF8RzFZBrw2EUkQWxiBW0yYQAhvk3C+QVGl0H3ouca8LDBlBfQKXwmW3huo/59H8rwb1wJw=="], + "@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.40", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-wlFmCIGUlwF4zx/kncw+bmxTQh1HeSJq4mYV/V5cZUSJadDP3kXvGW8Rn21cimj/7y9ju+47oYWXi97vF7czaA=="], "@aws/agent-inspector": ["@aws/agent-inspector@0.6.1", "", { "dependencies": { "@ag-ui/core": "^0.0.52", "eslint-plugin-import": "^2.32.0", "eslint-plugin-prettier": "^5.5.5", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-testing-library": "^7.16.0", "lucide-react": "^0.575.0", "react": "^19.1.0", "react-dom": "^19.1.0", "react-markdown": "^10.1.0", "remark-gfm": "^4.0.1" } }, "sha512-+0KJRZe/mK1qenDOGdnCYZdSAt8jZK4tLb8Bc6vbpQqKc3MsRP2ICh0X9mtSAA6iT2KyG7crprsSXZJ3laEijQ=="], @@ -234,6 +247,22 @@ "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], + "@fastify/ajv-compiler": ["@fastify/ajv-compiler@4.0.6", "", { "dependencies": { "ajv": "^8.12.0", "ajv-formats": "^3.0.1", "fast-uri": "^4.0.0" } }, "sha512-NtuzM0SfaMJbGlnjr9LWQUN5LzgSrbB8tf/wRZNas+4E1O/Nmzl53e7ruT61HDZyRCJGC6FxIogmNZO1c5ETBA=="], + + "@fastify/error": ["@fastify/error@4.2.0", "", {}, "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ=="], + + "@fastify/fast-json-stringify-compiler": ["@fastify/fast-json-stringify-compiler@5.1.0", "", { "dependencies": { "fast-json-stringify": "^7.0.0" } }, "sha512-PxcYtKLbQ8Z+yApiqjK8FwxIwvEj38k2OiLc17u8dkJSlmfi2wHHPaSnaoqBPQqtvF8YVsDgDpP2snDCfFrpfw=="], + + "@fastify/forwarded": ["@fastify/forwarded@3.0.2", "", {}, "sha512-NE8HgKLgYejV9lDpqkEFaDKMLYelJBVfHekhB0UKvX0ghagXRJqg68feg8er1NPXxG4N9i6vPxzt8E+3wHfcmA=="], + + "@fastify/merge-json-schemas": ["@fastify/merge-json-schemas@0.2.1", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A=="], + + "@fastify/proxy-addr": ["@fastify/proxy-addr@5.1.0", "", { "dependencies": { "@fastify/forwarded": "^3.0.0", "ipaddr.js": "^2.1.0" } }, "sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw=="], + + "@fastify/sse": ["@fastify/sse@0.4.0", "", { "dependencies": { "fastify-plugin": "^5.0.0" }, "peerDependencies": { "fastify": "^5.x" } }, "sha512-bBV96iT2kHEw6h3i8IMkZGaqA7Gk81ugUzTNctXuE6N2BEC/qBnUuzlD/O17V43OkJP73h0/kf3Bp/asXlSuFA=="], + + "@fastify/websocket": ["@fastify/websocket@11.3.0", "", { "dependencies": { "duplexify": "^4.1.3", "fastify-plugin": "^6.0.0", "ws": "^8.16.0" } }, "sha512-g89ag4BCcD9YP5wBZXixzoLnuf5j89p/sXFcfpCiv2pdEkYYukBEoK3heVzqsp0EAtszVDc2BBZG0KZqeAShIA=="], + "@humanfs/core": ["@humanfs/core@0.19.2", "", { "dependencies": { "@humanfs/types": "^0.15.0" } }, "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA=="], "@humanfs/node": ["@humanfs/node@0.16.8", "", { "dependencies": { "@humanfs/core": "^0.19.2", "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ=="], @@ -322,6 +351,8 @@ "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.80.0", "", { "os": "win32", "cpu": "x64" }, "sha512-yAnO7lwBYQnz2pcfBPIGQQZWIX5zd5R/1aAKIF3oE+TVj7IhoHcROjOkz3sRDngzqhfPKfFaXqug5j5rE5dn6Q=="], + "@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="], + "@pkgr/core": ["@pkgr/core@0.3.6", "", {}, "sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA=="], "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], @@ -372,8 +403,12 @@ "@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.5.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-A9uSdn72ozbRUSit0eib0TW7nXuNPlaeM0zcGkJ+nE6tFcSDbnmtwoxbTCFBukVQcszDAyvsd7+rTduPTXpygg=="], + "@smithy/eventstream-codec": ["@smithy/eventstream-codec@1.1.0", "", { "dependencies": { "@aws-crypto/crc32": "3.0.0", "@smithy/types": "^1.2.0", "@smithy/util-hex-encoding": "^1.1.0", "tslib": "^2.5.0" } }, "sha512-3tEbUb8t8an226jKB6V/Q2XU/J53lCwCzULuBPEaF4JjSh+FlCMp7TmogE/Aij5J9DwlsZ4VAD/IRDuQ/0ZtMw=="], + "@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.7.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-nZyWTmSpJEXl6VtWVMBJve/7x12DZu6sIX1z1a+ZMaHlQQRs9Zpu6NbTe/gmxYXVRpkjxyDYpZ5gx2IM6f/Wkw=="], + "@smithy/is-array-buffer": ["@smithy/is-array-buffer@1.1.0", "", { "dependencies": { "tslib": "^2.5.0" } }, "sha512-twpQ/n+3OWZJ7Z+xu43MJErmhB/WO/mMTnqR6PwWQShvSJ/emx5d1N59LQZk6ZpTAeuRWrc+eHhkzTp9NFjNRQ=="], + "@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.7.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "tslib": "^2.6.2" } }, "sha512-7HgK3/pQQHcD5w9lOPtK53/eDMKDp324Nd4KZ8XvxlcKHiWytuM9VwVOB2iy3rutsz/N1WNEWBkaRBayrGnuog=="], "@smithy/node-config-provider": ["@smithy/node-config-provider@4.6.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "tslib": "^2.6.2" } }, "sha512-zMrXu/O5tPa7GLtra8L4wFG6DACcXT9QV4Ay+WEAjUhXm1dVq7c/q9Qv9gkJZNLY8hmQKg08778kDcxpKNMqOA=="], @@ -382,14 +417,26 @@ "@smithy/property-provider": ["@smithy/property-provider@4.5.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "tslib": "^2.6.2" } }, "sha512-SPJCSCCGpHf5g5b8244ig3WVKIdv2DS+X6cfmy4bpKEYo4VlLYM+bGHpHbfbmp76vegMKQBvs7DEpwQ5YuhKLA=="], + "@smithy/protocol-http": ["@smithy/protocol-http@5.6.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "tslib": "^2.6.2" } }, "sha512-Asd04MaxODN6FNY8EPTeCAM4kPNi3jDUAjZU0Y4F9rHvpLUrrUo7KLcxFgSthywFr6dZfIyDLIJda6jxmVTk5w=="], + "@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.7.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "tslib": "^2.6.2" } }, "sha512-XsIDj5gVG4YRxGS4n4TiBDAogPWRHXKZyor6JW/sEuaa/7BvKADe9j45jI2dPYCnYbKkLBmbZ4qN9ns0sH+kMQ=="], "@smithy/signature-v4": ["@smithy/signature-v4@5.7.3", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-7ImGm+FkHRLcBaRttIAMZ6bzJZWb2cJGoYjq46F2UjycujWzrL9GEN9h4w7eQyXJYnltrUhxbbieBAIRrdqpow=="], "@smithy/types": ["@smithy/types@4.17.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA=="], + "@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], + + "@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@1.1.0", "", { "dependencies": { "tslib": "^2.5.0" } }, "sha512-7UtIE9eH0u41zpB60Jzr0oNCQ3hMJUabMcKRUVjmyHTXiWDE4vjSqN6qlih7rCNeKGbioS7f/y2Jgym4QZcKFg=="], + + "@smithy/util-middleware": ["@smithy/util-middleware@1.1.0", "", { "dependencies": { "tslib": "^2.5.0" } }, "sha512-6hhckcBqVgjWAqLy2vqlPZ3rfxLDhFWEmM7oLh2POGvsi7j0tHkbN7w4DFhuBExVJAbJ/qqxqZdRY6Fu7/OezQ=="], + "@smithy/util-retry": ["@smithy/util-retry@4.6.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "tslib": "^2.6.2" } }, "sha512-q6MXFNu+W4ZCNdNKutzDLP/Hzumd1FU9CQX++P/7ylanYIYiGhgZwSzwZWAw+G1RNcfY+RBYv61XcGlSYhj+BA=="], + "@smithy/util-uri-escape": ["@smithy/util-uri-escape@1.1.0", "", { "dependencies": { "tslib": "^2.5.0" } }, "sha512-/jL/V1xdVRt5XppwiaEU8Etp5WHZj609n0xMTuehmCqdoOFbId1M+aEeDWZsQ+8JbEB/BJ6ynY2SlYmOaKtt8w=="], + + "@smithy/util-utf8": ["@smithy/util-utf8@4.5.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "tslib": "^2.6.2" } }, "sha512-7wNWV7SugHpcMA7uzEawJNpE0GrasXM7a9E+1+Wm6NxVuDClESac/AKt+G7jMZNUz5vBLWqKlqVV7Sv7AtUr6Q=="], + "@smithy/util-waiter": ["@smithy/util-waiter@4.6.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "tslib": "^2.6.2" } }, "sha512-TNNK01i1HiSAR6hBF94J7q338JgEaHPrP8QKRSeQBsbSbw5O4+YGdZYitmsTeEWvHGX+2R/oQ7BzxyAjeZxuWg=="], "@so-ric/colorspace": ["@so-ric/colorspace@1.1.6", "", { "dependencies": { "color": "^5.0.2", "text-hex": "1.0.x" } }, "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw=="], @@ -438,6 +485,8 @@ "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], + "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.68.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.68.0", "@typescript-eslint/types": "^8.68.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-5GQtWZCXFcFYux955pvoS02WLc49pXNlvIxocKjS0clvwo3in1RdlzVKyiqQH9vE5AKWFLTaUgeQkOrTS+0Qxw=="], "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.68.0", "", { "dependencies": { "@typescript-eslint/types": "8.68.0", "@typescript-eslint/visitor-keys": "8.68.0" } }, "sha512-T5eXpcaJNg8bhjHJ8Rjp68Vq/QBteYtTKY8TZqVNPaUbuz0f6jI9t6aDkylwvalpAB9XTTFeFOjrjXAZ3YvmVA=="], @@ -454,12 +503,16 @@ "@ungap/structured-clone": ["@ungap/structured-clone@1.4.0", "", {}, "sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ=="], + "abstract-logging": ["abstract-logging@2.0.1", "", {}, "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA=="], + "acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="], "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], "ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], + "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], + "ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="], "ansi-regex": ["ansi-regex@6.3.0", "", {}, "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ=="], @@ -490,10 +543,14 @@ "async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="], + "atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="], + "auto-bind": ["auto-bind@5.0.1", "", {}, "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg=="], "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], + "avvio": ["avvio@9.3.0", "", { "dependencies": { "@fastify/error": "^4.0.0", "fastq": "^1.17.1" } }, "sha512-g2tQ7LE7oOSqDfwEm3M+ZCMTJc7KiZCdJ4UwyZJb5ckTKyYu50OYmvv0mCFXPuYXoM4zkSt8zM9XQ9KCvxA74A=="], + "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], @@ -502,6 +559,8 @@ "baseline-browser-mapping": ["baseline-browser-mapping@2.11.20", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-H0ulySigv6icDJ1F7SjtdCD6PrhTpdYCmP0CactWy1+ekh0AFd0o1Wn5T8b+hnTmdBx19u9yhL6wvCylXMY7zw=="], + "bedrock-agentcore": ["bedrock-agentcore@0.4.3", "", { "dependencies": { "@aws-crypto/sha256-js": "^5.2.0", "@aws-sdk/client-bedrock-agentcore": "^3.1065.0", "@aws-sdk/client-bedrock-agentcore-control": "^3.996.0", "@aws-sdk/credential-providers": "^3.996.0", "@aws-sdk/protocol-http": "^3.370.0", "@aws-sdk/signature-v4": "^3.370.0", "@fastify/sse": "^0.4.0", "@fastify/websocket": "^11.0.1", "@smithy/protocol-http": "^5.3.5", "@smithy/signature-v4": "^5.3.5", "@smithy/util-utf8": "^4.2.0", "@types/ws": "^8.18.1", "fastify": "^5.7.1", "ws": "^8.18.3", "zod": "^4.1.13" }, "peerDependencies": { "@strands-agents/sdk": ">=1.5.0", "ai": ">=6.0.0-beta", "playwright": ">=1.56.0", "react": ">=18.0.0", "react-dom": ">=18.0.0" }, "optionalPeers": ["@strands-agents/sdk", "ai", "playwright", "react", "react-dom"] }, "sha512-Ft66jLjMKlsP3/efepYrhoGU1+d2ccbpLiBU0PXtn1BCI1Lj2ounmi+pJRyaNDrtdJm9zjbdy+aKzpGZ/QQRMQ=="], + "binaryextensions": ["binaryextensions@6.11.0", "", { "dependencies": { "editions": "^6.21.0" } }, "sha512-sXnYK/Ij80TO3lcqZVV2YgfKN5QjUWIRk/XSm2J/4bd/lPko3lvk0O4ZppH6m+6hB2/GTu+ptNwVFe1xh+QLQw=="], "boundary": ["boundary@2.0.0", "", {}, "sha512-rJKn5ooC9u8q13IMCrW0RSp31pxBCHE3y9V/tp3TdWSLf8Em3p6Di4NBpfzbJge9YjjFEsD0RtFEjtvHL5VyEA=="], @@ -572,6 +631,8 @@ "convert-to-spaces": ["convert-to-spaces@2.0.1", "", {}, "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ=="], + "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], + "cookie-es": ["cookie-es@3.1.1", "", {}, "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg=="], "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], @@ -604,6 +665,8 @@ "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + "duplexify": ["duplexify@4.1.3", "", { "dependencies": { "end-of-stream": "^1.4.1", "inherits": "^2.0.3", "readable-stream": "^3.1.1", "stream-shift": "^1.0.2" } }, "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA=="], + "editions": ["editions@6.22.0", "", { "dependencies": { "version-range": "^4.15.0" } }, "sha512-UgGlf8IW75je7HZjNDpJdCv4cGJWIi6yumFdZ0R7A8/CIhQiWUjyGLCxdHpd8bmyD1gnkfUNK0oeOXqUS2cpfQ=="], "electron-to-chromium": ["electron-to-chromium@1.5.416", "", {}, "sha512-K6bvB2BjnNrugtIih6ewlbBI9DXa976jIdiIlRLHhBoEI9a4JaQjjHyF+A1IQI543aQYR4LnmOrT/K5fZj0aPA=="], @@ -612,6 +675,8 @@ "enabled": ["enabled@2.0.0", "", {}, "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="], + "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], + "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], "es-abstract": ["es-abstract@1.24.2", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg=="], @@ -674,6 +739,8 @@ "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], + "fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], "fast-diff": ["fast-diff@1.3.0", "", {}, "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw=="], @@ -682,9 +749,17 @@ "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + "fast-json-stringify": ["fast-json-stringify@7.0.1", "", { "dependencies": { "@fastify/merge-json-schemas": "^0.2.0", "ajv": "^8.12.0", "ajv-formats": "^3.0.1", "fast-uri": "^4.0.0", "json-schema-ref-resolver": "^3.0.0", "rfdc": "^1.2.0" } }, "sha512-eRSayARSbbwlBjpP4vnTTIRD5QPcIrmihPxDeN1DtKnHPg66UuJLx+8hlK1kaFdjvzyQ/dzALoi4vwAQ+T+iZA=="], + "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], - "fast-uri": ["fast-uri@3.1.6", "", {}, "sha512-7Ical1vFEMr0onbVzEDIreM22I4khW+fzyQPwvAFWBp1iwdshSZRsL4jjRvPG9JP1uiqMHRto+YU6R2/CzDz5Q=="], + "fast-querystring": ["fast-querystring@1.1.2", "", { "dependencies": { "fast-decode-uri-component": "^1.0.1" } }, "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg=="], + + "fast-uri": ["fast-uri@4.1.4", "", {}, "sha512-dODXrIxlS9JSdgAnhIUKOosKV1oMtU2VtVw87QRaHzyl5jxO290Ii5tEZfCfzfWNHi3jKWwBSdQj0qIyshdZdQ=="], + + "fastify": ["fastify@5.12.1", "", { "dependencies": { "@fastify/ajv-compiler": "^4.0.5", "@fastify/error": "^4.0.0", "@fastify/fast-json-stringify-compiler": "^5.0.0", "@fastify/proxy-addr": "^5.0.0", "abstract-logging": "^2.0.1", "avvio": "^9.0.0", "fast-json-stringify": "^7.0.0", "find-my-way": "^9.6.0", "light-my-request": "^6.0.0", "pino": "^9.14.0 || ^10.1.0", "process-warning": "^5.1.0", "rfdc": "^1.3.1", "secure-json-parse": "^4.0.0", "semver": "^7.6.0", "toad-cache": "^3.7.0" } }, "sha512-FWi+tQvwxR/PeRX7Z2mhfEF5ozJ3jn9asiiclzKXNSzJRHAYcU924aIOKAdHFJ+YIKieh3cqr1IwCOvTr41B3Q=="], + + "fastify-plugin": ["fastify-plugin@5.1.0", "", {}, "sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw=="], "fastq": ["fastq@1.20.3", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw=="], @@ -698,6 +773,8 @@ "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + "find-my-way": ["find-my-way@9.9.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-querystring": "^1.0.0", "safe-regex2": "^5.0.0" } }, "sha512-sJsgZ1sQH2UDuowPuMKg8az7Qc8F0jnj+SKkFWU/+T0xcFlgV5skgXOGUqmQzOdmW6ALA7AhJINWx3qFBkbLHA=="], + "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], @@ -794,6 +871,8 @@ "internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="], + "ipaddr.js": ["ipaddr.js@2.5.0", "", {}, "sha512-aq+t5NAc+cS6rZQQVWC2x98CPqGtKKTMDd4Gaodv0wShnItdKg/51djkGJ1hqH+Oy0ivDftCbSLCQob8zso01w=="], + "is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="], "is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="], @@ -880,6 +959,8 @@ "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + "json-schema-ref-resolver": ["json-schema-ref-resolver@3.0.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A=="], + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], "json-source-map": ["json-source-map@0.6.1", "", {}, "sha512-1QoztHPsMQqhDq0hlXY5ZqcEdUzxQEIxgFkKl4WUp2pgShObl+9ovi4kRh2TfvAfxAoHOJ9vIMEqk3k4iex7tg=="], @@ -900,6 +981,8 @@ "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + "light-my-request": ["light-my-request@6.6.0", "", { "dependencies": { "cookie": "^1.0.1", "process-warning": "^4.0.0", "set-cookie-parser": "^2.6.0" } }, "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A=="], + "lint-staged": ["lint-staged@17.4.1", "", { "dependencies": { "picomatch": "^4.0.7", "string-argv": "^0.3.2", "tinyexec": "^1.3.0" }, "optionalDependencies": { "yaml": "^2.9.0" }, "bin": { "lint-staged": "bin/lint-staged.js" } }, "sha512-FmJeudcalbSfg1du+JCfvi5vS6Qt08KgbfLWiHinbef+2JJwUZwAWVoaO1AcJVUTWPfk0t30PMQNwPAeCzYQ+Q=="], "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], @@ -1056,6 +1139,10 @@ "object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="], + "on-exit-leak-free": ["on-exit-leak-free@2.1.2", "", {}, "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + "one-time": ["one-time@1.0.0", "", { "dependencies": { "fn.name": "1.x.x" } }, "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g=="], "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], @@ -1090,6 +1177,12 @@ "picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="], + "pino": ["pino@10.3.1", "", { "dependencies": { "@pinojs/redact": "^0.4.0", "atomic-sleep": "^1.0.0", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^3.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^4.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg=="], + + "pino-abstract-transport": ["pino-abstract-transport@3.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg=="], + + "pino-std-serializers": ["pino-std-serializers@7.1.0", "", {}, "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw=="], + "pluralize": ["pluralize@8.0.0", "", {}, "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA=="], "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], @@ -1100,6 +1193,8 @@ "prettier-linter-helpers": ["prettier-linter-helpers@1.0.1", "", { "dependencies": { "fast-diff": "^1.1.2" } }, "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg=="], + "process-warning": ["process-warning@5.1.0", "", {}, "sha512-jQSaVHsPgtyw60e1rQ/A+/ArPEj/S8pS/vFnyGa/gYFXrKk/6RuDkoqVDQ5NI5MmS01698ltlAk0NoDBNLujRw=="], + "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], "property-information": ["property-information@7.2.0", "", {}, "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg=="], @@ -1110,6 +1205,8 @@ "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + "quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="], + "rc-config-loader": ["rc-config-loader@4.1.4", "", { "dependencies": { "debug": "^4.4.3", "js-yaml": "^4.1.1", "json5": "^2.2.3", "require-from-string": "^2.0.2" } }, "sha512-3GiwEzklkbXTDp52UR5nT8iXgYAx1V9ZG/kDZT7p60u2GCv2XTwQq4NzinMoMpNtXhmt3WkhYXcj6HH8HdwCEQ=="], "react": ["react@19.2.8", "", {}, "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw=="], @@ -1132,6 +1229,8 @@ "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + "real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="], + "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="], "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="], @@ -1152,8 +1251,12 @@ "restore-cursor": ["restore-cursor@4.0.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg=="], + "ret": ["ret@0.5.0", "", {}, "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw=="], + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], "safe-array-concat": ["safe-array-concat@1.1.4", "", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "get-intrinsic": "^1.3.0", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg=="], @@ -1164,14 +1267,20 @@ "safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="], + "safe-regex2": ["safe-regex2@5.1.1", "", { "dependencies": { "ret": "~0.5.0" }, "bin": { "safe-regex2": "bin/safe-regex2.js" } }, "sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA=="], + "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="], "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], "secretlint": ["secretlint@12.3.1", "", { "dependencies": { "@secretlint/config-creator": "12.3.1", "@secretlint/formatter": "12.3.1", "@secretlint/node": "12.3.1", "@secretlint/profiler": "12.3.1", "@secretlint/resolver": "12.3.1", "debug": "^4.4.3", "globby": "^16.2.0", "read-pkg": "^10.1.0" }, "bin": { "secretlint": "bin/secretlint.js" } }, "sha512-wv8TKCjU5hbBxo5jKEX8wIE78VAoL0Ux7pu18+TxtbICMZ2OCbu6EmO3OJLbUbyfUXSPVryNLNmGVgvwY6Z0xw=="], + "secure-json-parse": ["secure-json-parse@4.1.0", "", {}, "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA=="], + "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + "set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="], + "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], "set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="], @@ -1198,6 +1307,8 @@ "slice-ansi": ["slice-ansi@9.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "is-fullwidth-code-point": "^5.1.0" } }, "sha512-SO/3iYL5S3W57LLEniscOGPZgOqZUPCx6d3dB+52B80yJ0XstzsC/eV8gnA4tM3MHDrKz+OCFSLNjswdSC+/bA=="], + "sonic-boom": ["sonic-boom@4.2.1", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q=="], + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], @@ -1220,6 +1331,8 @@ "stream-browserify": ["stream-browserify@3.0.0", "", { "dependencies": { "inherits": "~2.0.4", "readable-stream": "^3.5.0" } }, "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA=="], + "stream-shift": ["stream-shift@1.0.3", "", {}, "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ=="], + "string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="], "string-width": ["string-width@8.2.2", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg=="], @@ -1272,12 +1385,16 @@ "textextensions": ["textextensions@6.11.0", "", { "dependencies": { "editions": "^6.21.0" } }, "sha512-tXJwSr9355kFJI3lbCkPpUH5cP8/M0GGy2xLO34aZCjMXBaK3SoPnZwr/oWmo1FdCnELcs4npdCIOFtq9W3ruQ=="], + "thread-stream": ["thread-stream@4.2.0", "", { "dependencies": { "real-require": "^1.0.0" } }, "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ=="], + "tinyexec": ["tinyexec@1.3.0", "", {}, "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ=="], "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + "toad-cache": ["toad-cache@3.7.4", "", {}, "sha512-m1TdR/rvT7kgGJZhspNtXdsdYk0fddFpJJFlG5s+UkPFo6lkLoZ3YLOaovPYjq1R75NP5JfeTlSHaOsE09peCg=="], + "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], "triple-beam": ["triple-beam@1.4.1", "", {}, "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg=="], @@ -1364,6 +1481,8 @@ "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + "ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="], "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], @@ -1406,6 +1525,16 @@ "@aws-cdk/toolkit-lib/yaml": ["yaml@1.10.3", "", {}, "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA=="], + "@aws-crypto/crc32/@aws-crypto/util": ["@aws-crypto/util@3.0.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-utf8-browser": "^3.0.0", "tslib": "^1.11.1" } }, "sha512-2OJlpeJpCR48CC8r+uKVChzs9Iungj9wkZrl8Z041DWEWvyIHILYKCPNzJghKsivj+S3mLo6BVc7mBNzdxA46w=="], + + "@aws-crypto/crc32/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], + + "@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], + + "@aws-sdk/protocol-http/@smithy/protocol-http": ["@smithy/protocol-http@1.2.0", "", { "dependencies": { "@smithy/types": "^1.2.0", "tslib": "^2.5.0" } }, "sha512-GfGfruksi3nXdFok5RhgtOnWe5f6BndzYfmEXISD+5gAGdayFGpjWu5pIqIweTudMtse20bGbc+7MFZXT1Tb8Q=="], + + "@aws-sdk/signature-v4/@smithy/signature-v4": ["@smithy/signature-v4@1.1.0", "", { "dependencies": { "@smithy/eventstream-codec": "^1.1.0", "@smithy/is-array-buffer": "^1.1.0", "@smithy/types": "^1.2.0", "@smithy/util-hex-encoding": "^1.1.0", "@smithy/util-middleware": "^1.1.0", "@smithy/util-uri-escape": "^1.1.0", "@smithy/util-utf8": "^1.1.0", "tslib": "^2.5.0" } }, "sha512-fDo3m7YqXBs7neciOePPd/X9LPm5QLlDMdIC4m1H6dgNLnXfLMFNIxEfPyohGA8VW9Wn4X8lygnPSGxDZSmp0Q=="], + "@babel/core/json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], @@ -1420,6 +1549,8 @@ "@eslint/eslintrc/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + "@fastify/websocket/fastify-plugin": ["fastify-plugin@6.0.0", "", {}, "sha512-fZOty7z3O7vOliF6d8bHE3wiEh1KcNnKEQensSgTk9C1DvN6nRLS++XVd86v33Hw/8u9Un8A1zDrQ8ujcQDHEg=="], + "@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.221.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.221.0", "@opentelemetry/core": "2.10.0", "@opentelemetry/resources": "2.10.0", "@opentelemetry/sdk-logs": "0.221.0", "@opentelemetry/sdk-metrics": "2.10.0", "@opentelemetry/sdk-trace": "2.10.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-lg6lkOU08Az23jVcn/0Els9HP+V8PnR4Km6p0KgpTggS0n/WuhnmY64rSh83Of9iR9nD+dpWr6adlcX8KzAwjg=="], "@opentelemetry/otlp-exporter-base/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.221.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.221.0", "@opentelemetry/core": "2.10.0", "@opentelemetry/resources": "2.10.0", "@opentelemetry/sdk-logs": "0.221.0", "@opentelemetry/sdk-metrics": "2.10.0", "@opentelemetry/sdk-trace": "2.10.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-lg6lkOU08Az23jVcn/0Els9HP+V8PnR4Km6p0KgpTggS0n/WuhnmY64rSh83Of9iR9nD+dpWr6adlcX8KzAwjg=="], @@ -1434,6 +1565,10 @@ "@secretlint/formatter/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + "@smithy/eventstream-codec/@smithy/types": ["@smithy/types@1.2.0", "", { "dependencies": { "tslib": "^2.5.0" } }, "sha512-z1r00TvBqF3dh4aHhya7nz1HhvCg4TRmw51fjMrh5do3h+ngSstt/yKlNbHeb9QxJmFbmN8KEVSWgb1bRvfEoA=="], + + "@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], + "@textlint/linter-formatter/pluralize": ["pluralize@2.0.0", "", {}, "sha512-TqNZzQCD4S42De9IfnnBvILN7HAW7riLqsCyp8lgjXeysyPlX5HhqKAcJHHHb9XskE4/a+7VGC9zzx8Ls0jOAw=="], "@textlint/linter-formatter/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -1444,6 +1579,8 @@ "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], + "ajv/fast-uri": ["fast-uri@3.1.6", "", {}, "sha512-7Ical1vFEMr0onbVzEDIreM22I4khW+fzyQPwvAFWBp1iwdshSZRsL4jjRvPG9JP1uiqMHRto+YU6R2/CzDz5Q=="], + "chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "color/color-convert": ["color-convert@3.1.3", "", { "dependencies": { "color-name": "^2.0.0" } }, "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg=="], @@ -1472,6 +1609,8 @@ "ink/wrap-ansi": ["wrap-ansi@10.0.1", "", { "dependencies": { "ansi-styles": "^6.2.3", "string-width": "^8.2.0" } }, "sha512-M0N4xzyzosiIok3svYlEo1sdLZts/8FPgYH/GPC3wvlmPoRvnoManGMrE54waYj3tISA8w6lsdesfVv67qSr8Q=="], + "light-my-request/process-warning": ["process-warning@4.0.1", "", {}, "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q=="], + "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], "micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], @@ -1496,6 +1635,8 @@ "table/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "thread-stream/real-require": ["real-require@1.0.0", "", {}, "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g=="], + "wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "wrap-ansi/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -1510,6 +1651,12 @@ "@aws-cdk/cx-api/@aws-cdk/cloud-assembly-schema/semver": ["semver@7.8.5", "", { "bundled": true, "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + "@aws-sdk/protocol-http/@smithy/protocol-http/@smithy/types": ["@smithy/types@1.2.0", "", { "dependencies": { "tslib": "^2.5.0" } }, "sha512-z1r00TvBqF3dh4aHhya7nz1HhvCg4TRmw51fjMrh5do3h+ngSstt/yKlNbHeb9QxJmFbmN8KEVSWgb1bRvfEoA=="], + + "@aws-sdk/signature-v4/@smithy/signature-v4/@smithy/types": ["@smithy/types@1.2.0", "", { "dependencies": { "tslib": "^2.5.0" } }, "sha512-z1r00TvBqF3dh4aHhya7nz1HhvCg4TRmw51fjMrh5do3h+ngSstt/yKlNbHeb9QxJmFbmN8KEVSWgb1bRvfEoA=="], + + "@aws-sdk/signature-v4/@smithy/signature-v4/@smithy/util-utf8": ["@smithy/util-utf8@1.1.0", "", { "dependencies": { "@smithy/util-buffer-from": "^1.1.0", "tslib": "^2.5.0" } }, "sha512-p/MYV+JmqmPyjdgyN2UxAeYDj9cBqCjp0C/NsTWnnjoZUVqoeZ6IrW915L9CAKWVECgv9lVQGc4u/yz26/bI1A=="], + "@eslint/eslintrc/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], "@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.221.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-OlanaW1vv7ufTqQ3/fPLI4arGt5ZoM+P8abOMki6uEYnpRazepSWDwDnnw+la7kE26SHVC18//SMccrDvLKOXQ=="], @@ -1544,6 +1691,8 @@ "@aws-cdk/cloudformation-diff/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "@aws-sdk/signature-v4/@smithy/signature-v4/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@1.1.0", "", { "dependencies": { "@smithy/is-array-buffer": "^1.1.0", "tslib": "^2.5.0" } }, "sha512-9m6NXE0ww+ra5HKHCHig20T+FAwxBAm7DIdwc/767uGWbRcY720ybgPacQNB96JMOI7xVr/CDa3oMzKmW4a+kw=="], + "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], } } diff --git a/package.json b/package.json index 3a6fac2f4..d6b61eec4 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "@opentelemetry/sdk-metrics": "^2.10.0", "@smithy/core": "^3.33.3", "@tanstack/react-query": "^5.101.2", + "bedrock-agentcore": "^0.4.3", "cli-truncate": "^6.1.1", "commander": "^15.0.0", "handlebars": "^4.7.9", diff --git a/src/core/index.tsx b/src/core/index.tsx index 318073481..057c3cd03 100644 --- a/src/core/index.tsx +++ b/src/core/index.tsx @@ -10,6 +10,7 @@ import { MemoryClient } from "./memory"; import { PolicyClient } from "./policy"; import { ObservabilityClient } from "./observability"; import { RuntimeClient } from "./runtime"; +import type { OpenRuntimeShell } from "./runtime"; import { FsReadWriteJson } from "../io"; import type { AwsClients, @@ -49,6 +50,7 @@ type CoreClientConfig = { newSessionId?: () => string; now?: () => number; bedrockAgentImporter?: CoreBedrockAgentImporter; + openRuntimeShell?: OpenRuntimeShell; }; // CoreClient is the single entry point to the Bedrock AgentCore APIs. It owns the @@ -89,7 +91,12 @@ export class CoreClient implements AwsClients { this.logger = config.logger; const fetch = config.fetch ?? globalThis.fetch; this.fetch = fetch; - this.runtime = new RuntimeClient(this, fetch, this.logger.child({ module: "runtime" })); + this.runtime = new RuntimeClient( + this, + fetch, + this.logger.child({ module: "runtime" }), + config.openRuntimeShell, + ); this.gateway = new GatewayClient(this, fetch, this.logger.child({ module: "gateway" })); this.policy = new PolicyClient(this, this.logger.child({ module: "policy" })); // EvalClient shares the injected fetch: dataset content is served from a diff --git a/src/core/runtime.tsx b/src/core/runtime.tsx index a1d485e9a..00e1f34d3 100644 --- a/src/core/runtime.tsx +++ b/src/core/runtime.tsx @@ -14,17 +14,27 @@ import type { CoreRuntimeClient, RuntimeInvokeRequest, RuntimeInvokeResponse, + RuntimeShellRequest, + RuntimeShellSession, } from "../handlers/runtime/types"; import type { Logger } from "../logging"; import type { AwsClients, CoreFetch, CoreOptions } from "./types"; import { invokeRuntime } from "./invokeRuntime"; import { toClientConfig } from "./utils"; +export type OpenRuntimeShell = ( + request: RuntimeShellRequest, + options: CoreOptions, +) => Promise; + export class RuntimeClient implements CoreRuntimeClient { constructor( private readonly clients: AwsClients, private readonly fetch: CoreFetch, private readonly logger: Logger, + private readonly openShell: OpenRuntimeShell = async () => { + throw new Error("Runtime shell transport is not configured"); + }, ) {} invokeRuntime( @@ -40,6 +50,13 @@ export class RuntimeClient implements CoreRuntimeClient { ); } + openRuntimeShell( + request: RuntimeShellRequest, + options: CoreOptions, + ): Promise { + return this.openShell(request, options); + } + async getRuntime( id: string, options: CoreOptions, diff --git a/src/core/runtimeShell.test.ts b/src/core/runtimeShell.test.ts new file mode 100644 index 000000000..ce20d9d00 --- /dev/null +++ b/src/core/runtimeShell.test.ts @@ -0,0 +1,202 @@ +import { describe, expect, test } from "bun:test"; +import { ShellChannel } from "bedrock-agentcore/runtime"; +import type { RuntimeShellRequest } from "../handlers/runtime/types"; +import { + createRuntimeShellOpener, + type RuntimeShellSdkClient, + type RuntimeShellSdkSession, +} from "./runtimeShell"; + +const REQUEST: RuntimeShellRequest = { + runtimeArn: "arn:aws:bedrock-agentcore:us-west-2:123456789012:runtime/checkout-AbCdEf1234", + qualifier: "prod", + runtimeSessionId: "session-012345678901234567890123456789", + shellId: "shell-1", +}; + +function sdkSession( + frames: { channel: ShellChannel; payload: Uint8Array }[] = [], +): RuntimeShellSdkSession & { closed: number } { + return { + shellId: "server-shell", + sessionId: "server-session", + kicked: false, + exitCode: 0, + closed: 0, + send: async () => {}, + resize: async () => {}, + async close() { + this.closed += 1; + }, + async *[Symbol.asyncIterator]() { + yield* frames; + }, + }; +} + +describe("createRuntimeShellOpener", () => { + test("constructs a SigV4 client from Core options and opens the requested shell", async () => { + const clients: unknown[] = []; + const opens: unknown[] = []; + const session = sdkSession(); + const opener = createRuntimeShellOpener({ + createClient: (config) => { + clients.push(config); + return { + openShell: async (input) => { + opens.push(input); + return session; + }, + }; + }, + sleep: async () => {}, + }); + const credentials = { + accessKeyId: "access", + secretAccessKey: "secret", + sessionToken: "session", + }; + + const result = await opener(REQUEST, { + region: "us-west-2", + endpointUrl: "https://runtime.test", + credentials, + }); + + expect(clients).toEqual([ + { + region: "us-west-2", + credentialsProvider: expect.any(Function), + }, + ]); + await expect( + (clients[0] as { credentialsProvider: () => Promise }).credentialsProvider(), + ).resolves.toEqual(credentials); + expect(opens).toEqual([ + { + runtimeArn: REQUEST.runtimeArn, + endpointName: "prod", + sessionId: REQUEST.runtimeSessionId, + shellId: REQUEST.shellId, + auth: "sigv4", + reconnectConfig: {}, + }, + ]); + expect(result.runtimeSessionId).toBe("server-session"); + expect(result.shellId).toBe("server-shell"); + }); + + test("uses OAuth auth for a bearer token", async () => { + const opens: unknown[] = []; + const opener = createRuntimeShellOpener({ + createClient: () => ({ + openShell: async (input) => { + opens.push(input); + return sdkSession(); + }, + }), + sleep: async () => {}, + }); + + await opener({ ...REQUEST, bearerToken: "token" }, { region: "us-west-2" }); + + expect(opens).toEqual([ + expect.objectContaining({ auth: { type: "oauth", bearerToken: "token" } }), + ]); + }); + + test("translates stdout and stderr frames and delegates writes", async () => { + const sent: (string | Buffer)[] = []; + const resizes: unknown[] = []; + const session = sdkSession([ + { channel: ShellChannel.STDOUT, payload: new TextEncoder().encode("out") }, + { channel: ShellChannel.STATUS, payload: new Uint8Array() }, + { channel: ShellChannel.STDERR, payload: new TextEncoder().encode("err") }, + ]); + session.send = async (data) => { + sent.push(data); + }; + session.resize = async (columns, rows) => { + resizes.push({ columns, rows }); + }; + const opener = createRuntimeShellOpener({ + createClient: (): RuntimeShellSdkClient => ({ openShell: async () => session }), + sleep: async () => {}, + }); + + const result = await opener(REQUEST, { region: "us-west-2" }); + const frames = []; + for await (const frame of result) frames.push(frame); + await result.send(Uint8Array.from([1, 2])); + await result.resize(100, 40); + await result.detach(); + + expect(frames).toEqual([ + { type: "stdout", data: new TextEncoder().encode("out") }, + { type: "stderr", data: new TextEncoder().encode("err") }, + ]); + expect(sent).toEqual([Buffer.from([1, 2])]); + expect(Buffer.isBuffer(sent[0])).toBe(true); + expect(resizes).toEqual([{ columns: 100, rows: 40 }]); + expect(session.closed).toBe(1); + }); + + test("splits large terminal writes at the shell protocol frame limit", async () => { + const sent: Buffer[] = []; + const session = sdkSession(); + session.send = async (data) => { + if (typeof data === "string") throw new Error("expected Buffer"); + sent.push(Buffer.from(data)); + }; + const opener = createRuntimeShellOpener({ + createClient: () => ({ openShell: async () => session }), + sleep: async () => {}, + }); + const result = await opener(REQUEST, { region: "us-west-2" }); + const paste = Buffer.alloc(64 * 1024 + 1, 0x61); + + await result.send(paste); + + expect(sent.map((frame) => frame.byteLength)).toEqual([64 * 1024 - 1, 2]); + expect(Buffer.concat(sent)).toEqual(paste); + }); + + test("retries retryable initial upgrade failures", async () => { + let attempts = 0; + const delays: number[] = []; + const opener = createRuntimeShellOpener({ + createClient: () => ({ + openShell: async () => { + attempts += 1; + if (attempts < 3) throw new Error("Server rejected WebSocket connection: HTTP 424"); + return sdkSession(); + }, + }), + sleep: async (delay) => { + delays.push(delay); + }, + }); + + await opener(REQUEST, { region: "us-west-2" }); + + expect(attempts).toBe(3); + expect(delays).toEqual([250, 500]); + }); + + test("does not retry a non-retryable failure", async () => { + let attempts = 0; + const failure = new Error("Server rejected WebSocket connection: HTTP 403"); + const opener = createRuntimeShellOpener({ + createClient: () => ({ + openShell: async () => { + attempts += 1; + throw failure; + }, + }), + sleep: async () => {}, + }); + + await expect(opener(REQUEST, { region: "us-west-2" })).rejects.toBe(failure); + expect(attempts).toBe(1); + }); +}); diff --git a/src/core/runtimeShell.ts b/src/core/runtimeShell.ts new file mode 100644 index 000000000..6acf5c64c --- /dev/null +++ b/src/core/runtimeShell.ts @@ -0,0 +1,157 @@ +import { RuntimeClient as AgentCoreRuntimeClient, ShellChannel } from "bedrock-agentcore/runtime"; +import type { AwsCredentialIdentityProvider } from "@smithy/types"; +import { Buffer } from "node:buffer"; +import type { RuntimeShellFrame, RuntimeShellSession } from "../handlers/runtime/types"; +import type { OpenRuntimeShell } from "./runtime"; +import type { CoreOptions } from "./types"; + +export type RuntimeShellSdkOpenInput = { + runtimeArn: string; + endpointName: string; + sessionId?: string; + shellId?: string; + auth: "sigv4" | { type: "oauth"; bearerToken: string }; + reconnectConfig: { onReconnect?: () => void }; +}; + +export type RuntimeShellSdkFrame = { + channel: number; + payload: Uint8Array; +}; + +export interface RuntimeShellSdkSession extends AsyncIterable { + readonly shellId: string; + readonly sessionId: string; + readonly kicked: boolean; + readonly exitCode: number | null; + send(data: string | Buffer): Promise; + resize(columns: number, rows: number): Promise; + close(): Promise; +} + +export interface RuntimeShellSdkClient { + openShell(input: RuntimeShellSdkOpenInput): Promise; +} + +export type RuntimeShellSdkClientConfig = { + region: string; + credentialsProvider?: AwsCredentialIdentityProvider; +}; + +export type CreateRuntimeShellSdkClient = ( + config: RuntimeShellSdkClientConfig, +) => RuntimeShellSdkClient; + +export type RuntimeShellOpenerConfig = { + createClient?: CreateRuntimeShellSdkClient; + sleep?: (delayMs: number) => Promise; +}; + +const RETRYABLE_UPGRADE = /HTTP (409|424|429)\b/; +const MAX_ATTEMPTS = 5; +const MAX_STDIN_PAYLOAD_BYTES = 64 * 1024 - 1; + +export function createRuntimeShellOpener(config: RuntimeShellOpenerConfig = {}): OpenRuntimeShell { + const createClient = + config.createClient ?? + ((clientConfig) => + new AgentCoreRuntimeClient( + clientConfig as ConstructorParameters[0], + ) as unknown as RuntimeShellSdkClient); + const sleep = + config.sleep ?? ((delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs))); + + return async (request, options) => { + const client = createClient({ + region: options.region, + ...(options.credentials !== undefined && { + credentialsProvider: credentialProvider(options.credentials), + }), + }); + const input: RuntimeShellSdkOpenInput = { + runtimeArn: request.runtimeArn, + endpointName: request.qualifier, + ...(request.runtimeSessionId !== undefined && { sessionId: request.runtimeSessionId }), + ...(request.shellId !== undefined && { shellId: request.shellId }), + auth: + request.bearerToken === undefined + ? "sigv4" + : { type: "oauth", bearerToken: request.bearerToken }, + reconnectConfig: { + ...(request.onReconnect !== undefined && { onReconnect: request.onReconnect }), + }, + }; + + let delayMs = 250; + for (let attempt = 1; ; attempt += 1) { + try { + const session = await client.openShell(input); + return new RuntimeShellSessionAdapter(session); + } catch (error) { + if (attempt >= MAX_ATTEMPTS || !isRetryableUpgrade(error)) throw error; + await sleep(delayMs); + delayMs *= 2; + } + } + }; +} + +function credentialProvider( + credentials: NonNullable, +): AwsCredentialIdentityProvider { + return typeof credentials === "function" ? credentials : async () => credentials; +} + +function isRetryableUpgrade(error: unknown): boolean { + const reported = error instanceof Error ? error : new Error(String(error)); + return ( + RETRYABLE_UPGRADE.test(reported.message) || + reported.name === "TimeoutError" || + reported.name === "NetworkingError" || + reported.name === "WebSocketError" + ); +} + +class RuntimeShellSessionAdapter implements RuntimeShellSession { + constructor(private readonly session: RuntimeShellSdkSession) {} + + get runtimeSessionId(): string { + return this.session.sessionId; + } + + get shellId(): string { + return this.session.shellId; + } + + get kicked(): boolean { + return this.session.kicked; + } + + get exitCode(): number | null { + return this.session.exitCode; + } + + async send(data: Uint8Array): Promise { + for (let offset = 0; offset < data.length; offset += MAX_STDIN_PAYLOAD_BYTES) { + await this.session.send(Buffer.from(data.subarray(offset, offset + MAX_STDIN_PAYLOAD_BYTES))); + } + } + + resize(columns: number, rows: number): Promise { + return this.session.resize(columns, rows); + } + + detach(): Promise { + return this.session.close(); + } + + async *[Symbol.asyncIterator](): AsyncIterator { + for await (const frame of this.session) { + if (frame.channel === ShellChannel.STDOUT) { + yield { type: "stdout", data: Uint8Array.from(frame.payload) }; + } else if (frame.channel === ShellChannel.STDERR) { + yield { type: "stderr", data: Uint8Array.from(frame.payload) }; + } + } + } +} diff --git a/src/handlers/runtime/types.tsx b/src/handlers/runtime/types.tsx index d47d31cc5..2791b7a15 100644 --- a/src/handlers/runtime/types.tsx +++ b/src/handlers/runtime/types.tsx @@ -54,12 +54,38 @@ export type RuntimeInvokeResponse = { body: AsyncIterable; }; +export type RuntimeShellRequest = { + runtimeArn: string; + qualifier: string; + runtimeSessionId?: string; + shellId?: string; + bearerToken?: string; + onReconnect?: () => void; +}; + +export type RuntimeShellFrame = + { type: "stdout"; data: Uint8Array } | { type: "stderr"; data: Uint8Array }; + +export interface RuntimeShellSession extends AsyncIterable { + readonly runtimeSessionId: string; + readonly shellId: string; + readonly kicked: boolean; + readonly exitCode: number | null; + send(data: Uint8Array): Promise; + resize(columns: number, rows: number): Promise; + detach(): Promise; +} + export interface CoreRuntimeClient { invokeRuntime( request: RuntimeInvokeRequest, options: CoreOptions, signal?: AbortSignal, ): Promise; + openRuntimeShell( + request: RuntimeShellRequest, + options: CoreOptions, + ): Promise; getRuntime( id: string, options: CoreOptions, diff --git a/src/index.ts b/src/index.ts index ed6241af9..ce04a58e4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,7 @@ import { homedir } from "os"; import { join } from "path"; import { CoreClient } from "./core"; +import { createRuntimeShellOpener } from "./core/runtimeShell"; import { createCloudFormationClient, createControlClient, @@ -68,6 +69,7 @@ process.exit( createDataClient, createIamClient, createLogsClient, + openRuntimeShell: createRuntimeShellOpener(), logger: rootLogger.child({ module: "core" }), }); diff --git a/src/testing/TestCoreClient.tsx b/src/testing/TestCoreClient.tsx index 78638a2ea..32cd07d76 100644 --- a/src/testing/TestCoreClient.tsx +++ b/src/testing/TestCoreClient.tsx @@ -151,6 +151,8 @@ import type { DeployedRuntime, RuntimeInvokeRequest, RuntimeInvokeResponse, + RuntimeShellRequest, + RuntimeShellSession, } from "../handlers/runtime/types"; import type { BatchEvaluationDetail, @@ -670,6 +672,16 @@ export class TestRuntimeClient implements CoreRuntimeClient { private listEndpointResponses = new Map(); private invokeResponse: RuntimeInvokeResponse = DEFAULT_RUNTIME_INVOKE_RESPONSE; private invokeBodies: AsyncIterable[] = []; + private shellSession: RuntimeShellSession = { + runtimeSessionId: "runtime-session-012345678901234567890123", + shellId: "shell-1", + kicked: false, + exitCode: 0, + send: async () => {}, + resize: async () => {}, + detach: async () => {}, + async *[Symbol.asyncIterator]() {}, + }; private error?: Error; setGetResponse(response: GetAgentRuntimeResponse): this { @@ -710,6 +722,11 @@ export class TestRuntimeClient implements CoreRuntimeClient { return this; } + setShellSession(session: RuntimeShellSession): this { + this.shellSession = session; + return this; + } + queueInvokeBody(body: AsyncIterable): this { this.invokeBodies.push(body); return this; @@ -743,6 +760,15 @@ export class TestRuntimeClient implements CoreRuntimeClient { }; } + async openRuntimeShell( + request: RuntimeShellRequest, + options: CoreOptions, + ): Promise { + this.calls.push({ method: "openRuntimeShell", args: [request, options] }); + if (this.error) throw this.error; + return this.shellSession; + } + async getRuntimeVersion( id: string, version: string, From 9bbcf4e65f003dbe87ecd473170a8a1e1cbeb239 Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Thu, 3 Sep 2026 18:09:08 +0000 Subject: [PATCH 04/26] feat(runtime): add shell command and TUI flow --- src/components/Root.tsx | 13 ++ src/handlers/runtime/get/screen.tsx | 6 + src/handlers/runtime/index.tsx | 4 +- src/handlers/runtime/runtime.screen.test.tsx | 14 +- src/handlers/runtime/runtime.test.tsx | 1 + src/handlers/runtime/shell/index.tsx | 72 ++++++++ src/handlers/runtime/shell/launchContext.ts | 11 ++ src/handlers/runtime/shell/operation.ts | 78 +++++++++ src/handlers/runtime/shell/request.test.ts | 139 +++++++++++++++ src/handlers/runtime/shell/request.ts | 76 +++++++++ src/handlers/runtime/shell/screen.tsx | 84 +++++++++ .../runtime/shell/shell.screen.test.tsx | 112 ++++++++++++ src/handlers/runtime/shell/shell.test.tsx | 161 ++++++++++++++++++ 13 files changed, 765 insertions(+), 6 deletions(-) create mode 100644 src/handlers/runtime/shell/index.tsx create mode 100644 src/handlers/runtime/shell/launchContext.ts create mode 100644 src/handlers/runtime/shell/operation.ts create mode 100644 src/handlers/runtime/shell/request.test.ts create mode 100644 src/handlers/runtime/shell/request.ts create mode 100644 src/handlers/runtime/shell/screen.tsx create mode 100644 src/handlers/runtime/shell/shell.screen.test.tsx create mode 100644 src/handlers/runtime/shell/shell.test.tsx diff --git a/src/components/Root.tsx b/src/components/Root.tsx index d44b90b0c..83c573164 100644 --- a/src/components/Root.tsx +++ b/src/components/Root.tsx @@ -35,6 +35,7 @@ import { MemoryScreen } from "../handlers/memory/screen.tsx"; import { MemoryGetJsonScreen, MemoryGetScreen } from "../handlers/memory/get/screen.tsx"; import { MemoryListScreen } from "../handlers/memory/list/screen.tsx"; import { RuntimeInvokeScreen } from "../handlers/runtime/invoke/screen.tsx"; +import { RuntimeShellScreen } from "../handlers/runtime/shell/screen.tsx"; import { EvalScreen } from "../handlers/eval/screen.tsx"; import { EvaluatorScreen } from "../handlers/eval/evaluator/screen.tsx"; import { EvaluatorListScreen } from "../handlers/eval/evaluator/list/screen.tsx"; @@ -378,6 +379,18 @@ export function Root({ path, ctx, core, queryClient }: RootProps) { path="agentcore/runtime/invoke/:runtimeId/:qualifier" element={} /> + } + /> + } + /> + } + /> } /> `/agentcore/runtime/invoke/${encodeURIComponent(id)}`, returnsToDetails: true, }, + { + name: "shell", + description: "open an interactive terminal", + to: (id: string) => `/agentcore/runtime/shell/${encodeURIComponent(id)}`, + returnsToDetails: true, + }, { name: "endpoints", description: "list this Runtime's endpoints", diff --git a/src/handlers/runtime/index.tsx b/src/handlers/runtime/index.tsx index 4fdb1a982..469b6cd07 100644 --- a/src/handlers/runtime/index.tsx +++ b/src/handlers/runtime/index.tsx @@ -8,6 +8,7 @@ import { createGetRuntimeHandler } from "./get"; import { createInvokeRuntimeHandler } from "./invoke"; import { createListRuntimesHandler } from "./list"; import { createRuntimeLogsHandler } from "./logs"; +import { createRuntimeShellHandler } from "./shell"; import { createRuntimeTracesHandler } from "./traces"; import { createRuntimeVersionHandler } from "./version"; @@ -15,10 +16,11 @@ export function createRuntimeHandler(core: Core, io: AppIO): Router { return new Router("runtime", "inspect AgentCore Runtimes") .use(withTuiOnEmptyFlagsAndArgs(core, io)) .default(renderTui(core, io)) - .supportedTuiCommands("get", "list", "invoke", "version", "endpoint") + .supportedTuiCommands("get", "list", "invoke", "shell", "version", "endpoint") .handler(createGetRuntimeHandler(core)) .handler(createListRuntimesHandler(core)) .handler(createInvokeRuntimeHandler(core, io)) + .handler(createRuntimeShellHandler(core, io)) .handler(createRuntimeVersionHandler(core, io)) .handler(createRuntimeEndpointHandler(core, io)) .handler(createRuntimeLogsHandler(core, io)) diff --git a/src/handlers/runtime/runtime.screen.test.tsx b/src/handlers/runtime/runtime.screen.test.tsx index dff9eb0f5..874bdefd8 100644 --- a/src/handlers/runtime/runtime.screen.test.tsx +++ b/src/handlers/runtime/runtime.screen.test.tsx @@ -216,6 +216,7 @@ describe("runtime hub", () => { await waitForText(r.lastFrame, "show the full JSON definition"); const frame = r.lastFrame()!; expect(frame).toMatch(/❯ invoke\s+invoke this Runtime/); + expect(frame).toMatch(/shell\s+open an interactive terminal/); expect(frame).toContain("versions"); expect(frame).toContain("endpoints"); for (const excluded of ["exec", "update", "create", "delete"]) { @@ -245,8 +246,9 @@ describe("runtime hub", () => { }); test.each([ - ["endpoints", 1], - ["versions", 2], + ["shell", 1], + ["endpoints", 2], + ["versions", 3], ] as const)( "selecting %s opens its encoded Runtime-scoped route", async (action, downPresses) => { @@ -287,7 +289,9 @@ describe("runtime hub", () => { r.lastFrame, action === "versions" ? `agentcore → runtime → version → list → ${runtimeId}` - : `agentcore → runtime → endpoint → list → ${runtimeId}`, + : action === "shell" + ? `agentcore → runtime → shell → ${runtimeId}` + : `agentcore → runtime → endpoint → list → ${runtimeId}`, ); }, ); @@ -304,7 +308,7 @@ describe("runtime hub", () => { const r = renderScreen("/agentcore/runtime/get/runtime-123", { core }); await waitForText(r.lastFrame, "show the full JSON definition"); - for (let index = 0; index < 3; index += 1) await r.press("down"); + for (let index = 0; index < 4; index += 1) await r.press("down"); await r.press("return"); await waitForText(r.lastFrame, "agentcore → runtime → get → runtime-123 → json"); const frame = r.lastFrame()!; @@ -401,7 +405,7 @@ describe("runtime hub", () => { await waitForText(r.lastFrame, "checkout"); await r.press("return"); await waitForText(r.lastFrame, "show the full JSON definition"); - for (let index = 0; index < 3; index += 1) await r.press("down"); + for (let index = 0; index < 4; index += 1) await r.press("down"); await r.press("return"); await waitForText(r.lastFrame, '"agentRuntimeId"'); await r.press("escape"); diff --git a/src/handlers/runtime/runtime.test.tsx b/src/handlers/runtime/runtime.test.tsx index 6c856ca7d..01ca13a5f 100644 --- a/src/handlers/runtime/runtime.test.tsx +++ b/src/handlers/runtime/runtime.test.tsx @@ -97,6 +97,7 @@ describe("runtime command hierarchy", () => { "get", "list", "invoke", + "shell", "version", "endpoint", "logs", diff --git a/src/handlers/runtime/shell/index.tsx b/src/handlers/runtime/shell/index.tsx new file mode 100644 index 000000000..31932a30b --- /dev/null +++ b/src/handlers/runtime/shell/index.tsx @@ -0,0 +1,72 @@ +import z from "zod"; +import { InputValidationError } from "../../../errors"; +import type { AppIO } from "../../../io"; +import { createHandler, flag, PathKey } from "../../../router"; +import { renderTuiAt } from "../../../tui"; +import { JsonKey } from "../../keys"; +import type { Core } from "../../types"; +import { runtimeIdSchema } from "../invoke/request"; +import { RuntimeShellLaunchContextKey } from "./launchContext"; +import { runRuntimeShell } from "./operation"; +import { resolveRuntimeShellBearerToken, validateRuntimeShellIds } from "./request"; + +const shellIdSchema = z + .string() + .regex( + /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/, + "must start with an alphanumeric character and contain at most 128 alphanumeric, '-' or '_' characters", + ); + +export const createRuntimeShellHandler = (core: Core, io: AppIO) => + createHandler({ + name: "shell", + description: "open an interactive shell in a Runtime", + flags: [ + flag("id", "the ID of the Runtime", runtimeIdSchema.optional()), + flag("qualifier", "the Runtime endpoint qualifier", z.string().min(1).optional()), + flag( + "session-id", + "the Runtime session ID to resume", + z.string().min(33).max(256).optional(), + ), + flag("shell-id", "the shell ID to reattach", shellIdSchema.optional()), + flag("bearer-token", "the CUSTOM_JWT bearer token", z.string().optional(), { + sensitive: true, + }), + ], + handle: async (ctx, flags) => { + if (ctx.require(JsonKey)) { + throw new InputValidationError("--json cannot be used with runtime shell"); + } + if (flags.id === undefined) { + throw new InputValidationError("required option '--id ' not specified"); + } + validateRuntimeShellIds(flags["session-id"], flags["shell-id"]); + const bearerToken = await resolveRuntimeShellBearerToken(flags["bearer-token"], io.stdin); + const launchContext = { + runtimeId: flags.id, + runtimeSessionId: flags["session-id"], + shellId: flags["shell-id"], + bearerToken, + }; + if (flags.qualifier === undefined) { + await renderTuiAt( + `${ctx.require(PathKey)}/${encodeURIComponent(flags.id)}`, + ctx.withValue(RuntimeShellLaunchContextKey, launchContext), + core, + io, + ); + return; + } + await runRuntimeShell({ + ctx, + core, + io, + runtimeId: flags.id, + qualifier: flags.qualifier, + launchContext, + }); + }, + }); + +export { RuntimeShellScreen } from "./screen"; diff --git a/src/handlers/runtime/shell/launchContext.ts b/src/handlers/runtime/shell/launchContext.ts new file mode 100644 index 000000000..78c8228e3 --- /dev/null +++ b/src/handlers/runtime/shell/launchContext.ts @@ -0,0 +1,11 @@ +import { contextKey } from "../../../router"; + +export type RuntimeShellLaunchContext = { + runtimeId: string; + runtimeSessionId?: string; + shellId?: string; + bearerToken?: string; +}; + +export const RuntimeShellLaunchContextKey = + contextKey("runtime.shell.launch"); diff --git a/src/handlers/runtime/shell/operation.ts b/src/handlers/runtime/shell/operation.ts new file mode 100644 index 000000000..2a6ea1d19 --- /dev/null +++ b/src/handlers/runtime/shell/operation.ts @@ -0,0 +1,78 @@ +import { InputValidationError, InvalidEnvironmentError, SilentCLIError } from "../../../errors"; +import { InteractiveTerminal, type AppIO } from "../../../io"; +import type { Context } from "../../../router"; +import { ExitCode } from "../../../runnable"; +import type { Core } from "../../types"; +import { coreOptsFromCtx } from "../../utils"; +import type { RuntimeShellLaunchContext } from "./launchContext"; +import { normalizeRuntimeShellRequest } from "./request"; + +export type RunRuntimeShellInput = { + ctx: Context; + core: Core; + io: AppIO; + runtimeId: string; + qualifier: string; + launchContext?: RuntimeShellLaunchContext; +}; + +export async function runRuntimeShell(input: RunRuntimeShellInput): Promise { + const { ctx, core, io, runtimeId, qualifier, launchContext } = input; + if (!io.stdin.isTTY || !io.stdout.isTTY) { + throw new InvalidEnvironmentError("interactive mode requires a TTY on stdin and stdout", { + exitCode: ExitCode.USAGE, + }); + } + + const options = coreOptsFromCtx(ctx); + if (options.endpointUrl !== undefined) { + throw new InputValidationError("runtime shell does not support --endpoint-url"); + } + const detail = await core.runtime.getRuntime(runtimeId, options); + const request = normalizeRuntimeShellRequest(detail, { + qualifier, + runtimeSessionId: launchContext?.runtimeSessionId, + shellId: launchContext?.shellId, + bearerToken: launchContext?.bearerToken, + }); + request.onReconnect = () => io.stderr.write("\r\nReconnected to shell.\r\n"); + + io.stderr.write(`Connecting to Runtime ${runtimeId} (${qualifier})...\n`); + const session = await core.runtime.openRuntimeShell(request, options); + io.stderr.write( + `Connected · session ${session.runtimeSessionId} · shell ${session.shellId} · ` + + `Ctrl+D or 'exit' to quit · Ctrl+] to detach\n`, + ); + + const terminal = new InteractiveTerminal({ io }); + let result: { detached: boolean }; + try { + result = await terminal.run(session); + } finally { + await session.detach(); + } + + if (result.detached) { + io.stderr.write( + `\nDetached.\nTo reattach:\n` + + ` agentcore runtime shell --id ${runtimeId} --qualifier ${qualifier} \\\n` + + ` --session-id ${session.runtimeSessionId} --shell-id ${session.shellId}\n`, + ); + return; + } + if (session.kicked) { + io.stderr.write("\nShell attached from another client.\n"); + throw new SilentCLIError("shell attached from another client"); + } + if (session.exitCode === null) { + io.stderr.write("\nShell connection ended without an exit code.\n"); + throw new SilentCLIError("shell connection ended without an exit code"); + } + + io.stderr.write(`\nSession closed · exit ${session.exitCode}\n`); + if (session.exitCode !== 0) { + throw new SilentCLIError(`shell exited with code ${session.exitCode}`, { + exitCode: session.exitCode, + }); + } +} diff --git a/src/handlers/runtime/shell/request.test.ts b/src/handlers/runtime/shell/request.test.ts new file mode 100644 index 000000000..b0688f137 --- /dev/null +++ b/src/handlers/runtime/shell/request.test.ts @@ -0,0 +1,139 @@ +import { describe, expect, test } from "bun:test"; +import type { GetAgentRuntimeResponse } from "@aws-sdk/client-bedrock-agentcore-control"; +import { PassThrough } from "node:stream"; +import { rm } from "node:fs/promises"; +import { + normalizeRuntimeShellRequest, + resolveRuntimeShellBearerToken, + validateRuntimeShellIds, +} from "./request"; + +const RUNTIME_ARN = "arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/checkout-AbCdEf1234"; + +function runtime(overrides: Partial = {}): GetAgentRuntimeResponse { + return { + agentRuntimeArn: RUNTIME_ARN, + agentRuntimeId: "checkout-AbCdEf1234", + agentRuntimeName: "checkout", + agentRuntimeVersion: "1", + createdAt: new Date("2026-09-03T00:00:00Z"), + lastUpdatedAt: new Date("2026-09-03T00:00:00Z"), + status: "READY", + roleArn: "arn:aws:iam::123456789012:role/runtime", + networkConfiguration: { networkMode: "PUBLIC" }, + lifecycleConfiguration: { + idleRuntimeSessionTimeout: 900, + maxLifetime: 28_800, + }, + ...overrides, + }; +} + +describe("normalizeRuntimeShellRequest", () => { + test("builds an IAM shell request from Runtime detail", () => { + expect( + normalizeRuntimeShellRequest(runtime(), { + qualifier: "prod", + runtimeSessionId: "session-012345678901234567890123456789", + shellId: "shell-1", + }), + ).toEqual({ + runtimeArn: RUNTIME_ARN, + qualifier: "prod", + runtimeSessionId: "session-012345678901234567890123456789", + shellId: "shell-1", + }); + }); + + test("requires a bearer token for CUSTOM_JWT", () => { + expect(() => + normalizeRuntimeShellRequest( + runtime({ + authorizerConfiguration: { customJWTAuthorizer: { discoveryUrl: "https://idp" } }, + }), + { qualifier: "DEFAULT" }, + ), + ).toThrow("CUSTOM_JWT Runtime requires --bearer-token"); + }); + + test("passes a bearer token for CUSTOM_JWT", () => { + expect( + normalizeRuntimeShellRequest( + runtime({ + authorizerConfiguration: { customJWTAuthorizer: { discoveryUrl: "https://idp" } }, + }), + { qualifier: "DEFAULT", bearerToken: "token" }, + ), + ).toMatchObject({ bearerToken: "token" }); + }); + + test("rejects a bearer token for IAM", () => { + expect(() => + normalizeRuntimeShellRequest(runtime(), { + qualifier: "DEFAULT", + bearerToken: "token", + }), + ).toThrow("IAM Runtime does not accept --bearer-token"); + }); + + test("rejects a Runtime that is not READY", () => { + expect(() => + normalizeRuntimeShellRequest(runtime({ status: "UPDATING" }), { qualifier: "DEFAULT" }), + ).toThrow("Runtime is not ready"); + }); + + test("rejects a missing or malformed Runtime ARN", () => { + expect(() => + normalizeRuntimeShellRequest(runtime({ agentRuntimeArn: "not-an-arn" }), { + qualifier: "DEFAULT", + }), + ).toThrow("Runtime returned an invalid ARN"); + }); +}); + +describe("validateRuntimeShellIds", () => { + test("requires session ID when shell ID is supplied", () => { + expect(() => validateRuntimeShellIds(undefined, "shell-1")).toThrow( + "--shell-id requires --session-id", + ); + }); + + test("accepts both IDs or neither", () => { + expect(() => validateRuntimeShellIds(undefined, undefined)).not.toThrow(); + expect(() => + validateRuntimeShellIds("session-012345678901234567890123456789", "shell-1"), + ).not.toThrow(); + }); +}); + +describe("resolveRuntimeShellBearerToken", () => { + test("reads file:// tokens and strips one trailing newline", async () => { + const path = `${import.meta.dir}/token.test.txt`; + await Bun.write(path, "secret-token\n"); + try { + await expect( + resolveRuntimeShellBearerToken( + `file://${path}`, + new PassThrough() as unknown as NodeJS.ReadStream, + ), + ).resolves.toBe("secret-token"); + } finally { + await rm(path, { force: true }); + } + }); + + test("accepts an inline token", async () => { + await expect( + resolveRuntimeShellBearerToken( + "inline-token", + new PassThrough() as unknown as NodeJS.ReadStream, + ), + ).resolves.toBe("inline-token"); + }); + + test("rejects stdin because the PTY owns it", async () => { + await expect( + resolveRuntimeShellBearerToken("-", new PassThrough() as unknown as NodeJS.ReadStream), + ).rejects.toThrow("stdin bearer tokens are not available"); + }); +}); diff --git a/src/handlers/runtime/shell/request.ts b/src/handlers/runtime/shell/request.ts new file mode 100644 index 000000000..4d2ac4879 --- /dev/null +++ b/src/handlers/runtime/shell/request.ts @@ -0,0 +1,76 @@ +import type { GetAgentRuntimeResponse } from "@aws-sdk/client-bedrock-agentcore-control"; +import { InputValidationError, SourceResolutionError } from "../../../errors"; +import { SourceResolver } from "../../../io"; +import type { RuntimeShellRequest } from "../types"; + +export type RuntimeShellInput = Omit; + +export function validateRuntimeShellIds( + runtimeSessionId: string | undefined, + shellId: string | undefined, +): void { + if (shellId !== undefined && runtimeSessionId === undefined) { + throw new InputValidationError("--shell-id requires --session-id"); + } +} + +export async function resolveRuntimeShellBearerToken( + source: string | undefined, + stdin: NodeJS.ReadStream, +): Promise { + if (source === "-") { + throw new InputValidationError( + "stdin bearer tokens are not available when opening an interactive shell", + ); + } + try { + const value = await new SourceResolver({ stdin }).resolveText("bearer-token", source); + if (value === undefined) return undefined; + const normalized = value.replace(/\r?\n$/, ""); + if (normalized.includes("\n")) { + throw new InputValidationError("--bearer-token must be a single-line value"); + } + return normalized; + } catch (error) { + if (error instanceof SourceResolutionError) { + throw new InputValidationError(error.message, { cause: error }); + } + throw error; + } +} + +export function normalizeRuntimeShellRequest( + detail: GetAgentRuntimeResponse, + input: RuntimeShellInput, +): RuntimeShellRequest { + if (detail.status !== "READY") { + throw new InputValidationError(`Runtime is not ready (status: ${detail.status ?? "unknown"})`); + } + const runtimeArn = detail.agentRuntimeArn; + if (!runtimeArn?.match(/^arn:[^:]+:bedrock-agentcore:[^:]+:\d{12}:runtime\/.+$/)) { + throw new InputValidationError("Runtime returned an invalid ARN"); + } + + const authorizer = detail.authorizerConfiguration; + const customJwt = authorizer !== undefined && "customJWTAuthorizer" in authorizer; + if (authorizer && !customJwt) { + throw new InputValidationError("Runtime uses an unsupported authorizer"); + } + if (customJwt && !input.bearerToken) { + throw new InputValidationError("CUSTOM_JWT Runtime requires --bearer-token"); + } + if (!customJwt && input.bearerToken !== undefined) { + throw new InputValidationError("IAM Runtime does not accept --bearer-token"); + } + validateRuntimeShellIds(input.runtimeSessionId, input.shellId); + + return { + runtimeArn, + qualifier: input.qualifier, + ...(input.runtimeSessionId !== undefined && { + runtimeSessionId: input.runtimeSessionId, + }), + ...(input.shellId !== undefined && { shellId: input.shellId }), + ...(input.bearerToken !== undefined && { bearerToken: input.bearerToken }), + }; +} diff --git a/src/handlers/runtime/shell/screen.tsx b/src/handlers/runtime/shell/screen.tsx new file mode 100644 index 000000000..4fcc0ae05 --- /dev/null +++ b/src/handlers/runtime/shell/screen.tsx @@ -0,0 +1,84 @@ +import { useEffect, useRef } from "react"; +import { useApp } from "ink"; +import { useLocation, useNavigate, useParams } from "react-router"; +import { RuntimeEndpointPicker } from "../../../components/RuntimeEndpointPicker"; +import { RuntimePicker } from "../../../components/RuntimePicker"; +import { Spinner } from "../../../components/ui/spinner"; +import { TuiHandoffKey } from "../../../tui/handoff"; +import type { ScreenProps } from "../../types"; +import { RuntimeShellLaunchContextKey } from "./launchContext"; +import { runRuntimeShell } from "./operation"; + +type RuntimeShellLocationState = { + returnOnEscape?: boolean; +}; + +const shellPath = (...parts: string[]) => + ["/agentcore/runtime/shell", ...parts.map(encodeURIComponent)].join("/"); + +export function RuntimeShellScreen(props: ScreenProps) { + const { runtimeId, qualifier } = useParams(); + const location = useLocation(); + const navigate = useNavigate(); + const returnOnEscape = (location.state as RuntimeShellLocationState | null)?.returnOnEscape; + + if (!runtimeId) { + return ( + navigate(shellPath(id))} + /> + ); + } + if (!qualifier) { + return ( + + navigate(shellPath(runtimeId, selected), { + replace: returnOnEscape === true, + state: returnOnEscape ? { returnOnEscape } : undefined, + }) + } + onEscape={() => (returnOnEscape ? navigate(-1) : navigate(shellPath()))} + /> + ); + } + + return ; +} + +function RuntimeShellHandoff({ + ctx, + core, + runtimeId, + qualifier, +}: ScreenProps & { runtimeId: string; qualifier: string }) { + const { exit } = useApp(); + const requested = useRef(false); + const launchContext = ctx.value(RuntimeShellLaunchContextKey); + const initialContext = launchContext?.runtimeId === runtimeId ? launchContext : undefined; + + useEffect(() => { + if (requested.current) return; + requested.current = true; + ctx.require(TuiHandoffKey).request(({ ctx, core, io }) => + runRuntimeShell({ + ctx, + core, + io, + runtimeId, + qualifier, + launchContext: initialContext, + }), + ); + exit(); + }, [ctx, core, exit, initialContext, qualifier, runtimeId]); + + return ; +} diff --git a/src/handlers/runtime/shell/shell.screen.test.tsx b/src/handlers/runtime/shell/shell.screen.test.tsx new file mode 100644 index 000000000..ca99e3d7c --- /dev/null +++ b/src/handlers/runtime/shell/shell.screen.test.tsx @@ -0,0 +1,112 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import { TuiHandoffController, TuiHandoffKey } from "../../../tui/handoff"; +import { renderTuiAt } from "../../../tui"; +import { DebugKey, EndpointKey, JsonKey, RegionKey } from "../../keys"; +import { ValueContext } from "../../../router"; +import { + cleanupScreens, + renderScreen, + TestCoreClient, + ttyTestIO, + waitFor, + waitForText, +} from "../../../testing"; + +afterEach(cleanupScreens); + +function core() { + const value = new TestCoreClient(); + value.runtime.setListResponse({ + agentRuntimes: [ + { + agentRuntimeArn: + "arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/checkout-AbCdEf1234", + agentRuntimeId: "checkout-AbCdEf1234", + agentRuntimeName: "checkout", + agentRuntimeVersion: "1", + description: "Checkout Runtime", + status: "READY", + lastUpdatedAt: new Date("2026-09-03T00:00:00Z"), + }, + ], + }); + value.runtime.setListEndpointsResponse({ + runtimeEndpoints: [ + { + name: "prod", + id: "prod", + liveVersion: "1", + status: "READY", + agentRuntimeEndpointArn: + "arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime-endpoint/prod", + agentRuntimeArn: + "arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/checkout-AbCdEf1234", + createdAt: new Date("2026-09-03T00:00:00Z"), + lastUpdatedAt: new Date("2026-09-03T00:00:00Z"), + }, + ], + }); + value.runtime.setGetResponse({ + agentRuntimeArn: "arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/checkout-AbCdEf1234", + agentRuntimeId: "checkout-AbCdEf1234", + agentRuntimeName: "checkout", + agentRuntimeVersion: "1", + createdAt: new Date("2026-09-03T00:00:00Z"), + lastUpdatedAt: new Date("2026-09-03T00:00:00Z"), + status: "READY", + roleArn: "arn:aws:iam::123456789012:role/runtime", + networkConfiguration: { networkMode: "PUBLIC" }, + lifecycleConfiguration: { + idleRuntimeSessionTimeout: 900, + maxLifetime: 28_800, + }, + }); + return value; +} + +describe("RuntimeShellScreen", () => { + test("selects a Runtime and endpoint, then requests one post-Ink handoff", async () => { + const controller = new TuiHandoffController(); + const screen = renderScreen("/agentcore/runtime/shell", { + core: core(), + withContext: (ctx) => ctx.withValue(TuiHandoffKey, controller), + }); + + await waitForText(screen.lastFrame, "checkout"); + await screen.press("return"); + await waitForText(screen.lastFrame, "prod"); + await screen.press("return"); + await waitFor(() => controller.take() !== undefined); + + expect(screen.core.runtime.calls.some((call) => call.method === "listRuntimes")).toBe(true); + expect(screen.core.runtime.calls.some((call) => call.method === "listRuntimeEndpoints")).toBe( + true, + ); + }); + + test("a direct Runtime route skips the Runtime picker", async () => { + const controller = new TuiHandoffController(); + const screen = renderScreen("/agentcore/runtime/shell/checkout-AbCdEf1234", { + core: core(), + withContext: (ctx) => ctx.withValue(TuiHandoffKey, controller), + }); + + await waitForText(screen.lastFrame, "prod"); + expect(screen.core.runtime.calls.some((call) => call.method === "listRuntimes")).toBe(false); + }); + + test("renderTuiAt unmounts Ink before executing the shell handoff", async () => { + const value = core(); + const { streams } = ttyTestIO(); + const ctx = ValueContext.EmptyContext() + .withValue(RegionKey, "us-east-1") + .withValue(EndpointKey, undefined) + .withValue(JsonKey, false) + .withValue(DebugKey, false); + + await renderTuiAt("/agentcore/runtime/shell/checkout-AbCdEf1234/prod", ctx, value, streams.io); + + expect(value.runtime.calls.some((call) => call.method === "openRuntimeShell")).toBe(true); + expect(streams.stderr()).toContain("Connected"); + }); +}); diff --git a/src/handlers/runtime/shell/shell.test.tsx b/src/handlers/runtime/shell/shell.test.tsx new file mode 100644 index 000000000..ed8a83423 --- /dev/null +++ b/src/handlers/runtime/shell/shell.test.tsx @@ -0,0 +1,161 @@ +import { describe, expect, test } from "bun:test"; +import type { GetAgentRuntimeResponse } from "@aws-sdk/client-bedrock-agentcore-control"; +import { createRootHandler } from "../../index"; +import type { RuntimeShellSession } from "../types"; +import { + createSilentLogger, + TestCoreClient, + TestGlobalConfigAccessor, + testIO, +} from "../../../testing"; + +const RUNTIME_ID = "checkout-AbCdEf1234"; +const RUNTIME_ARN = "arn:aws:bedrock-agentcore:us-west-2:123456789012:runtime/checkout-AbCdEf1234"; + +function runtime(overrides: Partial = {}): GetAgentRuntimeResponse { + return { + agentRuntimeArn: RUNTIME_ARN, + agentRuntimeId: RUNTIME_ID, + agentRuntimeName: "checkout", + agentRuntimeVersion: "1", + createdAt: new Date("2026-09-03T00:00:00Z"), + lastUpdatedAt: new Date("2026-09-03T00:00:00Z"), + status: "READY", + roleArn: "arn:aws:iam::123456789012:role/runtime", + networkConfiguration: { networkMode: "PUBLIC" }, + lifecycleConfiguration: { + idleRuntimeSessionTimeout: 900, + maxLifetime: 28_800, + }, + ...overrides, + }; +} + +class CompletedShell implements RuntimeShellSession { + readonly runtimeSessionId = "session-012345678901234567890123456789"; + readonly shellId = "shell-1"; + readonly kicked = false; + readonly exitCode = 0; + detached = 0; + + send(): Promise { + return Promise.resolve(); + } + + resize(): Promise { + return Promise.resolve(); + } + + detach(): Promise { + this.detached += 1; + return Promise.resolve(); + } + + async *[Symbol.asyncIterator]() {} +} + +function harness(options: { isTTY?: boolean; runtime?: GetAgentRuntimeResponse } = {}) { + const core = new TestCoreClient(); + core.runtime.setGetResponse(options.runtime ?? runtime()); + const shell = new CompletedShell(); + core.runtime.setShellSession(shell); + const io = testIO({ isTTY: options.isTTY ?? true }); + const root = createRootHandler(core, { + io: io.io, + logger: createSilentLogger(), + globalConfigAccessor: new TestGlobalConfigAccessor(), + }); + return { + core, + shell, + io, + run: (...args: string[]) => root.route(["node", "agentcore", "runtime", "shell", ...args]), + }; +} + +describe("runtime shell command", () => { + test("opens a direct IAM shell and detaches after the remote stream ends", async () => { + const subject = harness(); + + await subject.run("--id", RUNTIME_ID, "--qualifier", "prod"); + + expect(subject.core.runtime.calls.find((call) => call.method === "getRuntime")?.args[0]).toBe( + RUNTIME_ID, + ); + expect(subject.core.runtime.calls.find((call) => call.method === "openRuntimeShell")).toEqual({ + method: "openRuntimeShell", + args: [ + { + runtimeArn: RUNTIME_ARN, + qualifier: "prod", + onReconnect: expect.any(Function), + }, + { region: "us-west-2", endpointUrl: undefined }, + ], + }); + expect(subject.shell.detached).toBe(1); + expect(subject.io.stderr()).toContain("Connected"); + expect(subject.io.stderr()).toContain("exit 0"); + }); + + test("passes CUSTOM_JWT bearer auth to Core", async () => { + const subject = harness({ + runtime: runtime({ + authorizerConfiguration: { + customJWTAuthorizer: { + discoveryUrl: "https://idp.example/.well-known/openid-configuration", + }, + }, + }), + }); + + await subject.run("--id", RUNTIME_ID, "--qualifier", "DEFAULT", "--bearer-token", "token"); + + expect( + subject.core.runtime.calls.find((call) => call.method === "openRuntimeShell")?.args[0], + ).toMatchObject({ bearerToken: "token" }); + }); + + test("rejects JSON mode", async () => { + const subject = harness(); + + await expect( + subject.run("--id", RUNTIME_ID, "--qualifier", "DEFAULT", "--json"), + ).rejects.toThrow("--json cannot be used with runtime shell"); + expect(subject.core.runtime.calls.some((call) => call.method === "openRuntimeShell")).toBe( + false, + ); + }); + + test("requires a TTY for direct shell", async () => { + const subject = harness({ isTTY: false }); + + await expect(subject.run("--id", RUNTIME_ID, "--qualifier", "DEFAULT")).rejects.toThrow( + "interactive mode requires a TTY", + ); + }); + + test("rejects an endpoint URL override the shell SDK cannot honor", async () => { + const subject = harness(); + + await expect( + subject.run( + "--id", + RUNTIME_ID, + "--qualifier", + "DEFAULT", + "--endpoint-url", + "https://runtime.test", + ), + ).rejects.toThrow("runtime shell does not support --endpoint-url"); + expect(subject.core.runtime.calls.some((call) => call.method === "getRuntime")).toBe(false); + }); + + test("requires session ID when shell ID is supplied", async () => { + const subject = harness(); + + await expect( + subject.run("--id", RUNTIME_ID, "--qualifier", "DEFAULT", "--shell-id", "shell-1"), + ).rejects.toThrow("--shell-id requires --session-id"); + }); +}); From 6cbfaa485ebcdb6309a6a44cef315c6d8529fa7a Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Thu, 3 Sep 2026 18:09:18 +0000 Subject: [PATCH 05/26] docs(runtime): document interactive shell --- README.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/README.md b/README.md index 53f426695..29ce57045 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,7 @@ agentcore # interactive TUI │ ├── get # fetch a Runtime by id │ ├── list # list Runtimes (server-side paginated) │ ├── invoke # invoke a Runtime headlessly or in a persistent console +│ ├── shell # open a persistent interactive terminal in a Runtime │ ├── logs # follow a Runtime's logs live, or search a time window │ ├── traces │ │ ├── list # list a Runtime's recent traces @@ -517,6 +518,40 @@ accept ARNs, `--version`, `--interactive`, cross-account targets, or custom request paths. All requests use the Runtime `/invocations` route, including MCP Runtimes. +### Open a Runtime shell + +Runtime Shell opens a persistent interactive terminal in a Runtime session. +Bare shell opens the Runtime and endpoint pickers. `--id` skips the Runtime +picker, and `--id` plus `--qualifier` connects directly. + +```bash +agentcore runtime shell +agentcore runtime shell --id +agentcore runtime shell --id --qualifier DEFAULT +``` + +Use both IDs to reattach to the same shell: + +```bash +agentcore runtime shell \ + --id \ + --qualifier DEFAULT \ + --session-id \ + --shell-id +``` + +CUSTOM_JWT Runtimes require `--bearer-token`. Interactive bearer tokens may be +inline or `file://` sources, but not stdin. + +The shell forwards terminal input byte-for-byte, including `Ctrl+C`, `Ctrl+D`, +escape sequences, and full-screen terminal applications. Terminal resize events +update the remote PTY. `Ctrl+]` detaches the client while leaving the shell +available for reattachment. Running `exit` or sending `Ctrl+D` terminates the +remote shell. + +Runtime Shell requires TTY stdin and stdout and does not support `--json` or +`--endpoint-url`. + Bare Runtime branches and leaves, plus `memory`, `memory get`, and `memory list`, require a TTY on stdin and stdout. For Runtime Invoke, supplying a payload or headless-only request or output flags From 18a74ba283a42bdfa63152bc4296e74928838d2c Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Thu, 3 Sep 2026 18:22:51 +0000 Subject: [PATCH 06/26] fix(build): bundle runtime shell client only --- scripts/build.ts | 17 ++++++++++++++++- src/core/runtimeShell.ts | 8 +++++--- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/scripts/build.ts b/scripts/build.ts index e35cf0f3e..f11ef620e 100644 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -78,6 +78,20 @@ function assetLoaderPlugin(): Bun.BunPlugin { }; } +function runtimeShellSdkPlugin(): Bun.BunPlugin { + const runtimeEntry = Bun.resolveSync("bedrock-agentcore/runtime", REPO_ROOT); + const runtimeClient = join(resolve(runtimeEntry, ".."), "client.js"); + + return { + name: "runtime-shell-sdk-client", + setup(build) { + build.onResolve({ filter: /^bedrock-agentcore\/runtime$/ }, () => ({ + path: runtimeClient, + })); + }, + }; +} + function bootstrapTemplate(): string { const manifest = Bun.resolveSync("@aws-cdk/toolkit-lib/package.json", REPO_ROOT); const template = join(resolve(manifest, ".."), ...BOOTSTRAP_TEMPLATE); @@ -133,6 +147,7 @@ async function bundle(): Promise { minify: MINIFY, define: DEFINE, external: EXTERNAL, + plugins: [runtimeShellSdkPlugin()], }); const bin = join(DIST, "index.js"); await Bun.write(bin, BIN_LOADER); @@ -165,7 +180,7 @@ async function compile(target: string): Promise { define: DEFINE, root: REPO_ROOT, naming: { asset: ASSET_NAMING }, - plugins: [assetLoaderPlugin()], + plugins: [assetLoaderPlugin(), runtimeShellSdkPlugin()], }); await assertTemplateIsEmbedded(outfile, template); console.log( diff --git a/src/core/runtimeShell.ts b/src/core/runtimeShell.ts index 6acf5c64c..cc14efe0a 100644 --- a/src/core/runtimeShell.ts +++ b/src/core/runtimeShell.ts @@ -1,4 +1,4 @@ -import { RuntimeClient as AgentCoreRuntimeClient, ShellChannel } from "bedrock-agentcore/runtime"; +import { RuntimeClient as AgentCoreRuntimeClient } from "bedrock-agentcore/runtime"; import type { AwsCredentialIdentityProvider } from "@smithy/types"; import { Buffer } from "node:buffer"; import type { RuntimeShellFrame, RuntimeShellSession } from "../handlers/runtime/types"; @@ -50,6 +50,8 @@ export type RuntimeShellOpenerConfig = { const RETRYABLE_UPGRADE = /HTTP (409|424|429)\b/; const MAX_ATTEMPTS = 5; const MAX_STDIN_PAYLOAD_BYTES = 64 * 1024 - 1; +const STDOUT_CHANNEL = 1; +const STDERR_CHANNEL = 2; export function createRuntimeShellOpener(config: RuntimeShellOpenerConfig = {}): OpenRuntimeShell { const createClient = @@ -147,9 +149,9 @@ class RuntimeShellSessionAdapter implements RuntimeShellSession { async *[Symbol.asyncIterator](): AsyncIterator { for await (const frame of this.session) { - if (frame.channel === ShellChannel.STDOUT) { + if (frame.channel === STDOUT_CHANNEL) { yield { type: "stdout", data: Uint8Array.from(frame.payload) }; - } else if (frame.channel === ShellChannel.STDERR) { + } else if (frame.channel === STDERR_CHANNEL) { yield { type: "stderr", data: Uint8Array.from(frame.payload) }; } } From f40348f55f74ad0b76e0cb82d92bb9eb5cc21ab9 Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Thu, 3 Sep 2026 18:27:04 +0000 Subject: [PATCH 07/26] test(runtime): pin shell command region --- src/handlers/runtime/shell/shell.test.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/handlers/runtime/shell/shell.test.tsx b/src/handlers/runtime/shell/shell.test.tsx index ed8a83423..e2fb9310a 100644 --- a/src/handlers/runtime/shell/shell.test.tsx +++ b/src/handlers/runtime/shell/shell.test.tsx @@ -9,8 +9,9 @@ import { testIO, } from "../../../testing"; +const REGION = "us-west-2"; const RUNTIME_ID = "checkout-AbCdEf1234"; -const RUNTIME_ARN = "arn:aws:bedrock-agentcore:us-west-2:123456789012:runtime/checkout-AbCdEf1234"; +const RUNTIME_ARN = `arn:aws:bedrock-agentcore:${REGION}:123456789012:runtime/checkout-AbCdEf1234`; function runtime(overrides: Partial = {}): GetAgentRuntimeResponse { return { @@ -69,7 +70,8 @@ function harness(options: { isTTY?: boolean; runtime?: GetAgentRuntimeResponse } core, shell, io, - run: (...args: string[]) => root.route(["node", "agentcore", "runtime", "shell", ...args]), + run: (...args: string[]) => + root.route(["node", "agentcore", "runtime", "shell", ...args, "--region", REGION]), }; } From f895e833cfe3c8dbdf759f329ac442ce6323317a Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Thu, 3 Sep 2026 20:07:53 +0000 Subject: [PATCH 08/26] feat(tui): return results from handoffs --- src/tui/handoff.test.ts | 9 +++++++++ src/tui/handoff.ts | 10 +++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/tui/handoff.test.ts b/src/tui/handoff.test.ts index c04e92afd..e23184d4d 100644 --- a/src/tui/handoff.test.ts +++ b/src/tui/handoff.test.ts @@ -18,4 +18,13 @@ describe("TuiHandoffController", () => { expect(() => controller.request(async () => {})).toThrow("TUI handoff already requested"); }); + + test("preserves the handoff result", async () => { + const controller = new TuiHandoffController(); + controller.request(async () => ({ resumePath: "/agentcore/runtime/shell" })); + + await expect(controller.take()!({} as never)).resolves.toEqual({ + resumePath: "/agentcore/runtime/shell", + }); + }); }); diff --git a/src/tui/handoff.ts b/src/tui/handoff.ts index 729d8d842..1f6830d9b 100644 --- a/src/tui/handoff.ts +++ b/src/tui/handoff.ts @@ -2,7 +2,15 @@ import type { Core } from "../handlers/types"; import type { AppIO } from "../io"; import { contextKey, type Context } from "../router"; -export type TuiHandoff = (input: { ctx: Context; core: Core; io: AppIO }) => Promise; +export type TuiHandoffResult = { + resumePath?: string; +}; + +export type TuiHandoff = (input: { + ctx: Context; + core: Core; + io: AppIO; +}) => Promise; export class TuiHandoffController { private handoff?: TuiHandoff; From 77dc231ab693ead208363c9c4e4bfe5200a67a7f Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Thu, 3 Sep 2026 20:08:04 +0000 Subject: [PATCH 09/26] feat(tui): remount after resumable handoffs --- src/tui/index.tsx | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/tui/index.tsx b/src/tui/index.tsx index 60fbe5b78..9f277623f 100644 --- a/src/tui/index.tsx +++ b/src/tui/index.tsx @@ -57,17 +57,21 @@ export async function renderTuiAt( // alternateScreen switches the terminal to its alternate buffer so the TUI // takes over the screen and the prior scrollback is restored on exit (like Vim). - const handoffs = new TuiHandoffController(); - const tuiCtx = ctx.withValue(TuiHandoffKey, handoffs); - const { waitUntilExit } = render(, { - stdin: io.stdin, - stdout: io.stdout, - stderr: io.stderr, - alternateScreen: true, - incrementalRendering: true, - }); - await waitUntilExit(); - await handoffs.take()?.({ ctx: tuiCtx, core, io }); + let nextPath: string | undefined = path; + while (nextPath !== undefined) { + const handoffs = new TuiHandoffController(); + const tuiCtx = ctx.withValue(TuiHandoffKey, handoffs); + const { waitUntilExit } = render(, { + stdin: io.stdin, + stdout: io.stdout, + stderr: io.stderr, + alternateScreen: true, + incrementalRendering: true, + }); + await waitUntilExit(); + const result = await handoffs.take()?.({ ctx: tuiCtx, core, io }); + nextPath = result?.resumePath; + } } // renderTui builds the root DefaultHandle that mounts the Ink React tree. It From 9160e64db52cee8e151cdec2edf3704bd01eb1cc Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Thu, 3 Sep 2026 20:08:19 +0000 Subject: [PATCH 10/26] feat(runtime): return shell TUI to its origin --- src/handlers/runtime/shell/screen.tsx | 60 ++++++++---- .../runtime/shell/shell.screen.test.tsx | 91 ++++++++++++++++++- 2 files changed, 131 insertions(+), 20 deletions(-) diff --git a/src/handlers/runtime/shell/screen.tsx b/src/handlers/runtime/shell/screen.tsx index 4fcc0ae05..f0f781af1 100644 --- a/src/handlers/runtime/shell/screen.tsx +++ b/src/handlers/runtime/shell/screen.tsx @@ -4,6 +4,7 @@ import { useLocation, useNavigate, useParams } from "react-router"; import { RuntimeEndpointPicker } from "../../../components/RuntimeEndpointPicker"; import { RuntimePicker } from "../../../components/RuntimePicker"; import { Spinner } from "../../../components/ui/spinner"; +import { SilentCLIError } from "../../../errors"; import { TuiHandoffKey } from "../../../tui/handoff"; import type { ScreenProps } from "../../types"; import { RuntimeShellLaunchContextKey } from "./launchContext"; @@ -11,6 +12,7 @@ import { runRuntimeShell } from "./operation"; type RuntimeShellLocationState = { returnOnEscape?: boolean; + returnPath?: string; }; const shellPath = (...parts: string[]) => @@ -20,7 +22,8 @@ export function RuntimeShellScreen(props: ScreenProps) { const { runtimeId, qualifier } = useParams(); const location = useLocation(); const navigate = useNavigate(); - const returnOnEscape = (location.state as RuntimeShellLocationState | null)?.returnOnEscape; + const locationState = location.state as RuntimeShellLocationState | null; + const returnOnEscape = locationState?.returnOnEscape; if (!runtimeId) { return ( @@ -28,11 +31,20 @@ export function RuntimeShellScreen(props: ScreenProps) { {...props} breadcrumb={["agentcore", "runtime", "shell"]} description="choose a Runtime to open a shell" - onSelect={(id) => navigate(shellPath(id))} + onSelect={(id) => + navigate(shellPath(id), { + state: { returnPath: locationState?.returnPath ?? location.pathname }, + }) + } /> ); } if (!qualifier) { + const returnPath = + locationState?.returnPath ?? + (returnOnEscape + ? `/agentcore/runtime/get/${encodeURIComponent(runtimeId)}` + : location.pathname); return ( navigate(shellPath(runtimeId, selected), { replace: returnOnEscape === true, - state: returnOnEscape ? { returnOnEscape } : undefined, + state: { + ...locationState, + returnPath, + }, }) } onEscape={() => (returnOnEscape ? navigate(-1) : navigate(shellPath()))} @@ -50,7 +65,14 @@ export function RuntimeShellScreen(props: ScreenProps) { ); } - return ; + return ( + + ); } function RuntimeShellHandoff({ @@ -58,7 +80,8 @@ function RuntimeShellHandoff({ core, runtimeId, qualifier, -}: ScreenProps & { runtimeId: string; qualifier: string }) { + returnPath, +}: ScreenProps & { runtimeId: string; qualifier: string; returnPath?: string }) { const { exit } = useApp(); const requested = useRef(false); const launchContext = ctx.value(RuntimeShellLaunchContextKey); @@ -67,18 +90,23 @@ function RuntimeShellHandoff({ useEffect(() => { if (requested.current) return; requested.current = true; - ctx.require(TuiHandoffKey).request(({ ctx, core, io }) => - runRuntimeShell({ - ctx, - core, - io, - runtimeId, - qualifier, - launchContext: initialContext, - }), - ); + ctx.require(TuiHandoffKey).request(async ({ ctx, core, io }) => { + try { + await runRuntimeShell({ + ctx, + core, + io, + runtimeId, + qualifier, + launchContext: initialContext, + }); + } catch (error) { + if (returnPath === undefined || !(error instanceof SilentCLIError)) throw error; + } + return returnPath === undefined ? undefined : { resumePath: returnPath }; + }); exit(); - }, [ctx, core, exit, initialContext, qualifier, runtimeId]); + }, [ctx, core, exit, initialContext, qualifier, returnPath, runtimeId]); return ; } diff --git a/src/handlers/runtime/shell/shell.screen.test.tsx b/src/handlers/runtime/shell/shell.screen.test.tsx index ca99e3d7c..71470fe45 100644 --- a/src/handlers/runtime/shell/shell.screen.test.tsx +++ b/src/handlers/runtime/shell/shell.screen.test.tsx @@ -2,7 +2,8 @@ import { afterEach, describe, expect, test } from "bun:test"; import { TuiHandoffController, TuiHandoffKey } from "../../../tui/handoff"; import { renderTuiAt } from "../../../tui"; import { DebugKey, EndpointKey, JsonKey, RegionKey } from "../../keys"; -import { ValueContext } from "../../../router"; +import { type Context, ValueContext } from "../../../router"; +import type { RuntimeShellSession } from "../types"; import { cleanupScreens, renderScreen, @@ -65,23 +66,37 @@ function core() { } describe("RuntimeShellScreen", () => { - test("selects a Runtime and endpoint, then requests one post-Ink handoff", async () => { + test("a shell selected from the bare picker returns to that picker", async () => { const controller = new TuiHandoffController(); + let handoffContext!: Context; const screen = renderScreen("/agentcore/runtime/shell", { core: core(), - withContext: (ctx) => ctx.withValue(TuiHandoffKey, controller), + withContext: (ctx) => { + handoffContext = ctx.withValue(TuiHandoffKey, controller); + return handoffContext; + }, }); await waitForText(screen.lastFrame, "checkout"); await screen.press("return"); await waitForText(screen.lastFrame, "prod"); await screen.press("return"); - await waitFor(() => controller.take() !== undefined); + let handoff = controller.take(); + await waitFor(() => { + handoff ??= controller.take(); + return handoff !== undefined; + }); + const { streams } = ttyTestIO(); expect(screen.core.runtime.calls.some((call) => call.method === "listRuntimes")).toBe(true); expect(screen.core.runtime.calls.some((call) => call.method === "listRuntimeEndpoints")).toBe( true, ); + await expect( + handoff!({ ctx: handoffContext, core: screen.core, io: streams.io }), + ).resolves.toEqual({ + resumePath: "/agentcore/runtime/shell", + }); }); test("a direct Runtime route skips the Runtime picker", async () => { @@ -95,6 +110,49 @@ describe("RuntimeShellScreen", () => { expect(screen.core.runtime.calls.some((call) => call.method === "listRuntimes")).toBe(false); }); + test("a shell selected from Runtime details returns there after a nonzero exit", async () => { + const controller = new TuiHandoffController(); + const value = core(); + const failedSession: RuntimeShellSession = { + runtimeSessionId: "session-012345678901234567890123456789", + shellId: "shell-1", + kicked: false, + exitCode: 42, + send: async () => {}, + resize: async () => {}, + detach: async () => {}, + async *[Symbol.asyncIterator]() {}, + }; + value.runtime.setShellSession(failedSession); + let handoffContext!: Context; + const screen = renderScreen("/agentcore/runtime/get/checkout-AbCdEf1234", { + core: value, + withContext: (ctx) => { + handoffContext = ctx.withValue(TuiHandoffKey, controller); + return handoffContext; + }, + }); + + await waitForText(screen.lastFrame, "show the full JSON definition"); + await screen.press("down"); + await screen.press("return"); + await waitForText(screen.lastFrame, "prod"); + await screen.press("return"); + let handoff = controller.take(); + await waitFor(() => { + handoff ??= controller.take(); + return handoff !== undefined; + }); + const { streams } = ttyTestIO(); + + await expect( + handoff!({ ctx: handoffContext, core: screen.core, io: streams.io }), + ).resolves.toEqual({ + resumePath: "/agentcore/runtime/get/checkout-AbCdEf1234", + }); + expect(streams.stderr()).toContain("Session closed · exit 42"); + }); + test("renderTuiAt unmounts Ink before executing the shell handoff", async () => { const value = core(); const { streams } = ttyTestIO(); @@ -109,4 +167,29 @@ describe("RuntimeShellScreen", () => { expect(value.runtime.calls.some((call) => call.method === "openRuntimeShell")).toBe(true); expect(streams.stderr()).toContain("Connected"); }); + + test("renderTuiAt remounts a requested origin after the shell ends", async () => { + const value = core(); + const { streams, stdin } = ttyTestIO(); + const ctx = ValueContext.EmptyContext() + .withValue(RegionKey, "us-east-1") + .withValue(EndpointKey, undefined) + .withValue(JsonKey, false) + .withValue(DebugKey, false); + const rendering = renderTuiAt("/agentcore/runtime/shell", ctx, value, streams.io); + + await waitFor(() => streams.stdout().includes("checkout")); + stdin.write("\r"); + await waitFor(() => streams.stdout().includes("prod")); + stdin.write("\r"); + await waitFor( + () => value.runtime.calls.filter((call) => call.method === "listRuntimes").length === 2, + ); + stdin.write("\x03"); + + await rendering; + expect(value.runtime.calls.filter((call) => call.method === "openRuntimeShell")).toHaveLength( + 1, + ); + }); }); From d37ad2afe612aeae605be3061c922a5db7d14f0c Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Thu, 3 Sep 2026 20:44:57 +0000 Subject: [PATCH 11/26] test(runtime): allow TUI remount under coverage --- src/handlers/runtime/shell/shell.screen.test.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/handlers/runtime/shell/shell.screen.test.tsx b/src/handlers/runtime/shell/shell.screen.test.tsx index 71470fe45..cf9e9d173 100644 --- a/src/handlers/runtime/shell/shell.screen.test.tsx +++ b/src/handlers/runtime/shell/shell.screen.test.tsx @@ -184,6 +184,7 @@ describe("RuntimeShellScreen", () => { stdin.write("\r"); await waitFor( () => value.runtime.calls.filter((call) => call.method === "listRuntimes").length === 2, + 5000, ); stdin.write("\x03"); From 6ea324a0b131c6bdf79ab035734719c925796d37 Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Thu, 3 Sep 2026 20:51:57 +0000 Subject: [PATCH 12/26] refactor(io): remove explicit terminal detach --- src/io/index.ts | 1 - src/io/interactiveTerminal.test.ts | 24 ++++++++++++---------- src/io/interactiveTerminal.ts | 32 +++++++++--------------------- 3 files changed, 22 insertions(+), 35 deletions(-) diff --git a/src/io/index.ts b/src/io/index.ts index b63139a83..8341746d3 100644 --- a/src/io/index.ts +++ b/src/io/index.ts @@ -23,7 +23,6 @@ export { InteractiveTerminal, type InteractiveTerminalConfig, type InteractiveTerminalPeer, - type InteractiveTerminalResult, type TerminalFrame, } from "./interactiveTerminal"; export { readTextFile, type ReadTextFileOptions } from "./fileRead"; diff --git a/src/io/interactiveTerminal.test.ts b/src/io/interactiveTerminal.test.ts index f3332c9e9..b140288b4 100644 --- a/src/io/interactiveTerminal.test.ts +++ b/src/io/interactiveTerminal.test.ts @@ -62,7 +62,7 @@ class FakePeer implements InteractiveTerminalPeer { readonly frames = new StreamController(); readonly sent: Uint8Array[] = []; readonly resizes: { columns: number; rows: number }[] = []; - detached = 0; + closed = 0; send(data: Uint8Array): Promise { this.sent.push(Uint8Array.from(data)); @@ -74,8 +74,8 @@ class FakePeer implements InteractiveTerminalPeer { return Promise.resolve(); } - detach(): Promise { - this.detached += 1; + close(): Promise { + this.closed += 1; this.frames.end(); return Promise.resolve(); } @@ -130,7 +130,7 @@ describe("InteractiveTerminal", () => { peer.frames.emit({ type: "stderr", data: new TextEncoder().encode("err") }); peer.frames.end(); - await expect(running).resolves.toEqual({ detached: false }); + await running; expect(peer.sent).toEqual([Uint8Array.from([0x1b, 0x5b, 0x41])]); expect(s.stdout()).toBe("out"); expect(s.stderr()).toBe("err"); @@ -160,7 +160,7 @@ describe("InteractiveTerminal", () => { await running; }); - test("consumes Ctrl+] as detach and forwards Ctrl+C", async () => { + test("forwards Ctrl+C and Ctrl+] as raw input", async () => { const s = subject(); const peer = new FakePeer(); const running = s.terminal.run(peer); @@ -168,14 +168,16 @@ describe("InteractiveTerminal", () => { s.stdin.write(Uint8Array.from([0x03])); s.stdin.write(Uint8Array.from([0x1d])); + await Bun.sleep(0); + peer.frames.end(); - await expect(running).resolves.toEqual({ detached: true }); - expect(peer.sent).toEqual([Uint8Array.from([0x03])]); - expect(peer.detached).toBe(1); + await running; + expect(peer.sent).toEqual([Uint8Array.from([0x03]), Uint8Array.from([0x1d])]); + expect(peer.closed).toBe(0); expect(s.rawModes).toEqual([true, false]); }); - test("detaches and restores terminal state when aborted", async () => { + test("closes the peer and restores terminal state when aborted", async () => { const s = subject(); const peer = new FakePeer(); const controller = new AbortController(); @@ -186,7 +188,7 @@ describe("InteractiveTerminal", () => { controller.abort(interrupted); await expect(running).rejects.toBe(interrupted); - expect(peer.detached).toBe(1); + expect(peer.closed).toBe(1); expect(s.rawModes).toEqual([true, false]); expect(s.resizeRemoved()).toBe(1); }); @@ -197,7 +199,7 @@ describe("InteractiveTerminal", () => { const peer: InteractiveTerminalPeer = { send: async () => {}, resize: async () => {}, - detach: async () => {}, + close: async () => {}, [Symbol.asyncIterator]() { return { next: async (): Promise> => { diff --git a/src/io/interactiveTerminal.ts b/src/io/interactiveTerminal.ts index 855504a87..27bfd4d54 100644 --- a/src/io/interactiveTerminal.ts +++ b/src/io/interactiveTerminal.ts @@ -1,20 +1,14 @@ import type { AppIO } from "./types"; -const DETACH_BYTE = 0x1d; - export type TerminalFrame = { type: "stdout"; data: Uint8Array } | { type: "stderr"; data: Uint8Array }; export interface InteractiveTerminalPeer extends AsyncIterable { send(data: Uint8Array): Promise; resize(columns: number, rows: number): Promise; - detach(): Promise; + close(): Promise; } -export type InteractiveTerminalResult = { - detached: boolean; -}; - export type InteractiveTerminalConfig = { io: AppIO; dimensions?: () => { columns: number; rows: number }; @@ -41,16 +35,13 @@ export class InteractiveTerminal { }); } - async run( - peer: InteractiveTerminalPeer, - signal?: AbortSignal, - ): Promise { + async run(peer: InteractiveTerminalPeer, signal?: AbortSignal): Promise { if (this.stopCurrent) throw new Error("InteractiveTerminal is already running"); const { stdin, stdout, stderr } = this.config.io; const wasPaused = stdin.isPaused(); const wasRaw = (stdin as NodeJS.ReadStream & { isRaw?: boolean }).isRaw ?? false; - let detached = false; + let closed = false; let fail: (error: unknown) => void = () => {}; let queuedFailure: unknown; let hasQueuedFailure = false; @@ -65,19 +56,15 @@ export class InteractiveTerminal { fail(error); }); }; - const detach = async () => { - if (detached) return; - detached = true; - await peer.detach(); + const close = async () => { + if (closed) return; + closed = true; + await peer.close(); }; - this.stopCurrent = detach; + this.stopCurrent = close; const onData = (chunk: Buffer | string) => { const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, "binary"); - if (bytes.length === 1 && bytes[0] === DETACH_BYTE) { - enqueue(detach); - return; - } enqueue(() => peer.send(bytes)); }; const resize = () => { @@ -87,7 +74,7 @@ export class InteractiveTerminal { const removeResize = this.onResize(resize); const abort = () => { enqueue(async () => { - await detach(); + await close(); throw signal?.reason ?? new Error("terminal interrupted"); }); }; @@ -109,7 +96,6 @@ export class InteractiveTerminal { await Promise.race([pump(), failure]); await pending; if (hasQueuedFailure) throw queuedFailure; - return { detached }; } finally { signal?.removeEventListener("abort", abort); removeResize(); From f176bff399bcdf62af557ce21158a60cba026e22 Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Thu, 3 Sep 2026 20:52:11 +0000 Subject: [PATCH 13/26] refactor(runtime): target published shell session API --- src/core/runtimeShell.test.ts | 6 +----- src/core/runtimeShell.ts | 11 ++--------- src/handlers/runtime/types.tsx | 4 +--- src/testing/TestCoreClient.tsx | 3 +-- 4 files changed, 5 insertions(+), 19 deletions(-) diff --git a/src/core/runtimeShell.test.ts b/src/core/runtimeShell.test.ts index ce20d9d00..621ab6012 100644 --- a/src/core/runtimeShell.test.ts +++ b/src/core/runtimeShell.test.ts @@ -11,14 +11,12 @@ const REQUEST: RuntimeShellRequest = { runtimeArn: "arn:aws:bedrock-agentcore:us-west-2:123456789012:runtime/checkout-AbCdEf1234", qualifier: "prod", runtimeSessionId: "session-012345678901234567890123456789", - shellId: "shell-1", }; function sdkSession( frames: { channel: ShellChannel; payload: Uint8Array }[] = [], ): RuntimeShellSdkSession & { closed: number } { return { - shellId: "server-shell", sessionId: "server-session", kicked: false, exitCode: 0, @@ -77,13 +75,11 @@ describe("createRuntimeShellOpener", () => { runtimeArn: REQUEST.runtimeArn, endpointName: "prod", sessionId: REQUEST.runtimeSessionId, - shellId: REQUEST.shellId, auth: "sigv4", reconnectConfig: {}, }, ]); expect(result.runtimeSessionId).toBe("server-session"); - expect(result.shellId).toBe("server-shell"); }); test("uses OAuth auth for a bearer token", async () => { @@ -129,7 +125,7 @@ describe("createRuntimeShellOpener", () => { for await (const frame of result) frames.push(frame); await result.send(Uint8Array.from([1, 2])); await result.resize(100, 40); - await result.detach(); + await result.close(); expect(frames).toEqual([ { type: "stdout", data: new TextEncoder().encode("out") }, diff --git a/src/core/runtimeShell.ts b/src/core/runtimeShell.ts index cc14efe0a..7dbac610f 100644 --- a/src/core/runtimeShell.ts +++ b/src/core/runtimeShell.ts @@ -9,9 +9,8 @@ export type RuntimeShellSdkOpenInput = { runtimeArn: string; endpointName: string; sessionId?: string; - shellId?: string; auth: "sigv4" | { type: "oauth"; bearerToken: string }; - reconnectConfig: { onReconnect?: () => void }; + reconnectConfig: { onReconnect?: (reconnected: boolean) => void }; }; export type RuntimeShellSdkFrame = { @@ -20,7 +19,6 @@ export type RuntimeShellSdkFrame = { }; export interface RuntimeShellSdkSession extends AsyncIterable { - readonly shellId: string; readonly sessionId: string; readonly kicked: boolean; readonly exitCode: number | null; @@ -74,7 +72,6 @@ export function createRuntimeShellOpener(config: RuntimeShellOpenerConfig = {}): runtimeArn: request.runtimeArn, endpointName: request.qualifier, ...(request.runtimeSessionId !== undefined && { sessionId: request.runtimeSessionId }), - ...(request.shellId !== undefined && { shellId: request.shellId }), auth: request.bearerToken === undefined ? "sigv4" @@ -121,10 +118,6 @@ class RuntimeShellSessionAdapter implements RuntimeShellSession { return this.session.sessionId; } - get shellId(): string { - return this.session.shellId; - } - get kicked(): boolean { return this.session.kicked; } @@ -143,7 +136,7 @@ class RuntimeShellSessionAdapter implements RuntimeShellSession { return this.session.resize(columns, rows); } - detach(): Promise { + close(): Promise { return this.session.close(); } diff --git a/src/handlers/runtime/types.tsx b/src/handlers/runtime/types.tsx index 2791b7a15..be5f6cac0 100644 --- a/src/handlers/runtime/types.tsx +++ b/src/handlers/runtime/types.tsx @@ -58,7 +58,6 @@ export type RuntimeShellRequest = { runtimeArn: string; qualifier: string; runtimeSessionId?: string; - shellId?: string; bearerToken?: string; onReconnect?: () => void; }; @@ -68,12 +67,11 @@ export type RuntimeShellFrame = export interface RuntimeShellSession extends AsyncIterable { readonly runtimeSessionId: string; - readonly shellId: string; readonly kicked: boolean; readonly exitCode: number | null; send(data: Uint8Array): Promise; resize(columns: number, rows: number): Promise; - detach(): Promise; + close(): Promise; } export interface CoreRuntimeClient { diff --git a/src/testing/TestCoreClient.tsx b/src/testing/TestCoreClient.tsx index 32cd07d76..cca896254 100644 --- a/src/testing/TestCoreClient.tsx +++ b/src/testing/TestCoreClient.tsx @@ -674,12 +674,11 @@ export class TestRuntimeClient implements CoreRuntimeClient { private invokeBodies: AsyncIterable[] = []; private shellSession: RuntimeShellSession = { runtimeSessionId: "runtime-session-012345678901234567890123", - shellId: "shell-1", kicked: false, exitCode: 0, send: async () => {}, resize: async () => {}, - detach: async () => {}, + close: async () => {}, async *[Symbol.asyncIterator]() {}, }; private error?: Error; From 4d7d0737441032a39f03f382187ac35753f2600f Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Thu, 3 Sep 2026 20:52:37 +0000 Subject: [PATCH 14/26] refactor(runtime): remove shell reattach inputs --- src/handlers/runtime/shell/index.tsx | 18 ++-------------- src/handlers/runtime/shell/launchContext.ts | 1 - src/handlers/runtime/shell/request.test.ts | 23 +-------------------- src/handlers/runtime/shell/request.ts | 12 ----------- 4 files changed, 3 insertions(+), 51 deletions(-) diff --git a/src/handlers/runtime/shell/index.tsx b/src/handlers/runtime/shell/index.tsx index 31932a30b..04cd6ef0c 100644 --- a/src/handlers/runtime/shell/index.tsx +++ b/src/handlers/runtime/shell/index.tsx @@ -8,14 +8,7 @@ import type { Core } from "../../types"; import { runtimeIdSchema } from "../invoke/request"; import { RuntimeShellLaunchContextKey } from "./launchContext"; import { runRuntimeShell } from "./operation"; -import { resolveRuntimeShellBearerToken, validateRuntimeShellIds } from "./request"; - -const shellIdSchema = z - .string() - .regex( - /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/, - "must start with an alphanumeric character and contain at most 128 alphanumeric, '-' or '_' characters", - ); +import { resolveRuntimeShellBearerToken } from "./request"; export const createRuntimeShellHandler = (core: Core, io: AppIO) => createHandler({ @@ -24,12 +17,7 @@ export const createRuntimeShellHandler = (core: Core, io: AppIO) => flags: [ flag("id", "the ID of the Runtime", runtimeIdSchema.optional()), flag("qualifier", "the Runtime endpoint qualifier", z.string().min(1).optional()), - flag( - "session-id", - "the Runtime session ID to resume", - z.string().min(33).max(256).optional(), - ), - flag("shell-id", "the shell ID to reattach", shellIdSchema.optional()), + flag("session-id", "the Runtime session ID to use", z.string().min(33).max(256).optional()), flag("bearer-token", "the CUSTOM_JWT bearer token", z.string().optional(), { sensitive: true, }), @@ -41,12 +29,10 @@ export const createRuntimeShellHandler = (core: Core, io: AppIO) => if (flags.id === undefined) { throw new InputValidationError("required option '--id ' not specified"); } - validateRuntimeShellIds(flags["session-id"], flags["shell-id"]); const bearerToken = await resolveRuntimeShellBearerToken(flags["bearer-token"], io.stdin); const launchContext = { runtimeId: flags.id, runtimeSessionId: flags["session-id"], - shellId: flags["shell-id"], bearerToken, }; if (flags.qualifier === undefined) { diff --git a/src/handlers/runtime/shell/launchContext.ts b/src/handlers/runtime/shell/launchContext.ts index 78c8228e3..f084c51f6 100644 --- a/src/handlers/runtime/shell/launchContext.ts +++ b/src/handlers/runtime/shell/launchContext.ts @@ -3,7 +3,6 @@ import { contextKey } from "../../../router"; export type RuntimeShellLaunchContext = { runtimeId: string; runtimeSessionId?: string; - shellId?: string; bearerToken?: string; }; diff --git a/src/handlers/runtime/shell/request.test.ts b/src/handlers/runtime/shell/request.test.ts index b0688f137..53c12cf9f 100644 --- a/src/handlers/runtime/shell/request.test.ts +++ b/src/handlers/runtime/shell/request.test.ts @@ -2,11 +2,7 @@ import { describe, expect, test } from "bun:test"; import type { GetAgentRuntimeResponse } from "@aws-sdk/client-bedrock-agentcore-control"; import { PassThrough } from "node:stream"; import { rm } from "node:fs/promises"; -import { - normalizeRuntimeShellRequest, - resolveRuntimeShellBearerToken, - validateRuntimeShellIds, -} from "./request"; +import { normalizeRuntimeShellRequest, resolveRuntimeShellBearerToken } from "./request"; const RUNTIME_ARN = "arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/checkout-AbCdEf1234"; @@ -35,13 +31,11 @@ describe("normalizeRuntimeShellRequest", () => { normalizeRuntimeShellRequest(runtime(), { qualifier: "prod", runtimeSessionId: "session-012345678901234567890123456789", - shellId: "shell-1", }), ).toEqual({ runtimeArn: RUNTIME_ARN, qualifier: "prod", runtimeSessionId: "session-012345678901234567890123456789", - shellId: "shell-1", }); }); @@ -91,21 +85,6 @@ describe("normalizeRuntimeShellRequest", () => { }); }); -describe("validateRuntimeShellIds", () => { - test("requires session ID when shell ID is supplied", () => { - expect(() => validateRuntimeShellIds(undefined, "shell-1")).toThrow( - "--shell-id requires --session-id", - ); - }); - - test("accepts both IDs or neither", () => { - expect(() => validateRuntimeShellIds(undefined, undefined)).not.toThrow(); - expect(() => - validateRuntimeShellIds("session-012345678901234567890123456789", "shell-1"), - ).not.toThrow(); - }); -}); - describe("resolveRuntimeShellBearerToken", () => { test("reads file:// tokens and strips one trailing newline", async () => { const path = `${import.meta.dir}/token.test.txt`; diff --git a/src/handlers/runtime/shell/request.ts b/src/handlers/runtime/shell/request.ts index 4d2ac4879..9621249f2 100644 --- a/src/handlers/runtime/shell/request.ts +++ b/src/handlers/runtime/shell/request.ts @@ -5,15 +5,6 @@ import type { RuntimeShellRequest } from "../types"; export type RuntimeShellInput = Omit; -export function validateRuntimeShellIds( - runtimeSessionId: string | undefined, - shellId: string | undefined, -): void { - if (shellId !== undefined && runtimeSessionId === undefined) { - throw new InputValidationError("--shell-id requires --session-id"); - } -} - export async function resolveRuntimeShellBearerToken( source: string | undefined, stdin: NodeJS.ReadStream, @@ -62,15 +53,12 @@ export function normalizeRuntimeShellRequest( if (!customJwt && input.bearerToken !== undefined) { throw new InputValidationError("IAM Runtime does not accept --bearer-token"); } - validateRuntimeShellIds(input.runtimeSessionId, input.shellId); - return { runtimeArn, qualifier: input.qualifier, ...(input.runtimeSessionId !== undefined && { runtimeSessionId: input.runtimeSessionId, }), - ...(input.shellId !== undefined && { shellId: input.shellId }), ...(input.bearerToken !== undefined && { bearerToken: input.bearerToken }), }; } From eeb14ebc079c3b9c9ce65474b89c67150993a5f8 Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Thu, 3 Sep 2026 20:52:55 +0000 Subject: [PATCH 15/26] refactor(runtime): remove detach lifecycle messaging --- src/handlers/runtime/shell/operation.ts | 20 +++---------------- .../runtime/shell/shell.screen.test.tsx | 3 +-- src/handlers/runtime/shell/shell.test.tsx | 19 +++++------------- 3 files changed, 9 insertions(+), 33 deletions(-) diff --git a/src/handlers/runtime/shell/operation.ts b/src/handlers/runtime/shell/operation.ts index 2a6ea1d19..ebb59c95e 100644 --- a/src/handlers/runtime/shell/operation.ts +++ b/src/handlers/runtime/shell/operation.ts @@ -32,33 +32,19 @@ export async function runRuntimeShell(input: RunRuntimeShellInput): Promise io.stderr.write("\r\nReconnected to shell.\r\n"); io.stderr.write(`Connecting to Runtime ${runtimeId} (${qualifier})...\n`); const session = await core.runtime.openRuntimeShell(request, options); - io.stderr.write( - `Connected · session ${session.runtimeSessionId} · shell ${session.shellId} · ` + - `Ctrl+D or 'exit' to quit · Ctrl+] to detach\n`, - ); + io.stderr.write(`Connected · session ${session.runtimeSessionId} · Ctrl+D or 'exit' to quit\n`); const terminal = new InteractiveTerminal({ io }); - let result: { detached: boolean }; try { - result = await terminal.run(session); + await terminal.run(session); } finally { - await session.detach(); - } - - if (result.detached) { - io.stderr.write( - `\nDetached.\nTo reattach:\n` + - ` agentcore runtime shell --id ${runtimeId} --qualifier ${qualifier} \\\n` + - ` --session-id ${session.runtimeSessionId} --shell-id ${session.shellId}\n`, - ); - return; + await session.close(); } if (session.kicked) { io.stderr.write("\nShell attached from another client.\n"); diff --git a/src/handlers/runtime/shell/shell.screen.test.tsx b/src/handlers/runtime/shell/shell.screen.test.tsx index cf9e9d173..beabb22a9 100644 --- a/src/handlers/runtime/shell/shell.screen.test.tsx +++ b/src/handlers/runtime/shell/shell.screen.test.tsx @@ -115,12 +115,11 @@ describe("RuntimeShellScreen", () => { const value = core(); const failedSession: RuntimeShellSession = { runtimeSessionId: "session-012345678901234567890123456789", - shellId: "shell-1", kicked: false, exitCode: 42, send: async () => {}, resize: async () => {}, - detach: async () => {}, + close: async () => {}, async *[Symbol.asyncIterator]() {}, }; value.runtime.setShellSession(failedSession); diff --git a/src/handlers/runtime/shell/shell.test.tsx b/src/handlers/runtime/shell/shell.test.tsx index e2fb9310a..7c7157cb4 100644 --- a/src/handlers/runtime/shell/shell.test.tsx +++ b/src/handlers/runtime/shell/shell.test.tsx @@ -34,10 +34,9 @@ function runtime(overrides: Partial = {}): GetAgentRunt class CompletedShell implements RuntimeShellSession { readonly runtimeSessionId = "session-012345678901234567890123456789"; - readonly shellId = "shell-1"; readonly kicked = false; readonly exitCode = 0; - detached = 0; + closed = 0; send(): Promise { return Promise.resolve(); @@ -47,8 +46,8 @@ class CompletedShell implements RuntimeShellSession { return Promise.resolve(); } - detach(): Promise { - this.detached += 1; + close(): Promise { + this.closed += 1; return Promise.resolve(); } @@ -76,7 +75,7 @@ function harness(options: { isTTY?: boolean; runtime?: GetAgentRuntimeResponse } } describe("runtime shell command", () => { - test("opens a direct IAM shell and detaches after the remote stream ends", async () => { + test("opens a direct IAM shell and closes after the remote stream ends", async () => { const subject = harness(); await subject.run("--id", RUNTIME_ID, "--qualifier", "prod"); @@ -95,7 +94,7 @@ describe("runtime shell command", () => { { region: "us-west-2", endpointUrl: undefined }, ], }); - expect(subject.shell.detached).toBe(1); + expect(subject.shell.closed).toBe(1); expect(subject.io.stderr()).toContain("Connected"); expect(subject.io.stderr()).toContain("exit 0"); }); @@ -152,12 +151,4 @@ describe("runtime shell command", () => { ).rejects.toThrow("runtime shell does not support --endpoint-url"); expect(subject.core.runtime.calls.some((call) => call.method === "getRuntime")).toBe(false); }); - - test("requires session ID when shell ID is supplied", async () => { - const subject = harness(); - - await expect( - subject.run("--id", RUNTIME_ID, "--qualifier", "DEFAULT", "--shell-id", "shell-1"), - ).rejects.toThrow("--shell-id requires --session-id"); - }); }); From 087d24ef68bbb6da5d411747836b9b64da06dfb9 Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Thu, 3 Sep 2026 20:53:09 +0000 Subject: [PATCH 16/26] docs(runtime): defer shell reattach workflow --- README.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 29ce57045..ca175928f 100644 --- a/README.md +++ b/README.md @@ -530,14 +530,13 @@ agentcore runtime shell --id agentcore runtime shell --id --qualifier DEFAULT ``` -Use both IDs to reattach to the same shell: +Use `--session-id` to open the shell in a specific Runtime session/VM: ```bash agentcore runtime shell \ --id \ --qualifier DEFAULT \ - --session-id \ - --shell-id + --session-id ``` CUSTOM_JWT Runtimes require `--bearer-token`. Interactive bearer tokens may be @@ -545,9 +544,8 @@ inline or `file://` sources, but not stdin. The shell forwards terminal input byte-for-byte, including `Ctrl+C`, `Ctrl+D`, escape sequences, and full-screen terminal applications. Terminal resize events -update the remote PTY. `Ctrl+]` detaches the client while leaving the shell -available for reattachment. Running `exit` or sending `Ctrl+D` terminates the -remote shell. +update the remote PTY. Running `exit` or sending `Ctrl+D` terminates the remote +shell. Runtime Shell requires TTY stdin and stdout and does not support `--json` or `--endpoint-url`. From 47a4bb853f2c5ac9f07b14cea767ff697de6c743 Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Thu, 3 Sep 2026 21:20:22 +0000 Subject: [PATCH 17/26] test(runtime): allow shell picker rendering under coverage --- src/handlers/runtime/shell/shell.screen.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/handlers/runtime/shell/shell.screen.test.tsx b/src/handlers/runtime/shell/shell.screen.test.tsx index beabb22a9..24f907c9a 100644 --- a/src/handlers/runtime/shell/shell.screen.test.tsx +++ b/src/handlers/runtime/shell/shell.screen.test.tsx @@ -177,9 +177,9 @@ describe("RuntimeShellScreen", () => { .withValue(DebugKey, false); const rendering = renderTuiAt("/agentcore/runtime/shell", ctx, value, streams.io); - await waitFor(() => streams.stdout().includes("checkout")); + await waitFor(() => streams.stdout().includes("checkout"), 5000); stdin.write("\r"); - await waitFor(() => streams.stdout().includes("prod")); + await waitFor(() => streams.stdout().includes("prod"), 5000); stdin.write("\r"); await waitFor( () => value.runtime.calls.filter((call) => call.method === "listRuntimes").length === 2, From d67550bdba75e08961b3d8a14ec66e4ce15ba1bf Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Thu, 3 Sep 2026 21:33:21 +0000 Subject: [PATCH 18/26] fix(tui): force interactive rendering for TTY sessions Ink treats CI environments as non-interactive even when stdin and stdout are TTYs. renderTuiAt already requires a TTY, so force interactive mode to preserve live rendering and input. --- src/handlers/runtime/shell/shell.screen.test.tsx | 4 ++-- src/tui/index.tsx | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/handlers/runtime/shell/shell.screen.test.tsx b/src/handlers/runtime/shell/shell.screen.test.tsx index 24f907c9a..beabb22a9 100644 --- a/src/handlers/runtime/shell/shell.screen.test.tsx +++ b/src/handlers/runtime/shell/shell.screen.test.tsx @@ -177,9 +177,9 @@ describe("RuntimeShellScreen", () => { .withValue(DebugKey, false); const rendering = renderTuiAt("/agentcore/runtime/shell", ctx, value, streams.io); - await waitFor(() => streams.stdout().includes("checkout"), 5000); + await waitFor(() => streams.stdout().includes("checkout")); stdin.write("\r"); - await waitFor(() => streams.stdout().includes("prod"), 5000); + await waitFor(() => streams.stdout().includes("prod")); stdin.write("\r"); await waitFor( () => value.runtime.calls.filter((call) => call.method === "listRuntimes").length === 2, diff --git a/src/tui/index.tsx b/src/tui/index.tsx index 9f277623f..6e1daa93d 100644 --- a/src/tui/index.tsx +++ b/src/tui/index.tsx @@ -65,6 +65,7 @@ export async function renderTuiAt( stdin: io.stdin, stdout: io.stdout, stderr: io.stderr, + interactive: true, alternateScreen: true, incrementalRendering: true, }); From 6888d1fc57584e934a7aea39bab2eb8baddd0567 Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Thu, 3 Sep 2026 23:40:25 +0000 Subject: [PATCH 19/26] fix(io): preserve UTF-8 terminal input --- src/io/interactiveTerminal.test.ts | 15 +++++++++++++++ src/io/interactiveTerminal.ts | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/io/interactiveTerminal.test.ts b/src/io/interactiveTerminal.test.ts index b140288b4..9baf87aaf 100644 --- a/src/io/interactiveTerminal.test.ts +++ b/src/io/interactiveTerminal.test.ts @@ -138,6 +138,21 @@ describe("InteractiveTerminal", () => { expect(s.resizeRemoved()).toBe(1); }); + test("encodes string input as UTF-8", async () => { + const s = subject(); + const peer = new FakePeer(); + s.stdin.setEncoding("utf8"); + const running = s.terminal.run(peer); + await Bun.sleep(0); + + s.stdin.write("café 🙂"); + await Bun.sleep(0); + peer.frames.end(); + + await running; + expect(peer.sent).toEqual([new TextEncoder().encode("café 🙂")]); + }); + test("sends initial and subsequent terminal dimensions", async () => { const s = subject(); const peer = new FakePeer(); diff --git a/src/io/interactiveTerminal.ts b/src/io/interactiveTerminal.ts index 27bfd4d54..e9204c569 100644 --- a/src/io/interactiveTerminal.ts +++ b/src/io/interactiveTerminal.ts @@ -64,7 +64,7 @@ export class InteractiveTerminal { this.stopCurrent = close; const onData = (chunk: Buffer | string) => { - const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, "binary"); + const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, "utf8"); enqueue(() => peer.send(bytes)); }; const resize = () => { From b1ff89d6682e80d8a837a6f722de7027363d75bf Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Thu, 3 Sep 2026 23:40:32 +0000 Subject: [PATCH 20/26] build: expose shell protocol SDK exports --- scripts/build.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/scripts/build.ts b/scripts/build.ts index f11ef620e..24b3b4e5a 100644 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -80,14 +80,31 @@ function assetLoaderPlugin(): Bun.BunPlugin { function runtimeShellSdkPlugin(): Bun.BunPlugin { const runtimeEntry = Bun.resolveSync("bedrock-agentcore/runtime", REPO_ROOT); - const runtimeClient = join(resolve(runtimeEntry, ".."), "client.js"); + const runtimeDirectory = resolve(runtimeEntry, ".."); + const runtimeClient = join(runtimeDirectory, "client.js"); + const shellProtocol = join(runtimeDirectory, "shell", "protocol.js"); + const namespace = "runtime-shell-sdk"; return { name: "runtime-shell-sdk-client", setup(build) { build.onResolve({ filter: /^bedrock-agentcore\/runtime$/ }, () => ({ + path: "runtime", + namespace, + })); + build.onResolve({ filter: /^runtime-shell-sdk\/client$/ }, () => ({ path: runtimeClient, })); + build.onResolve({ filter: /^runtime-shell-sdk\/protocol$/ }, () => ({ + path: shellProtocol, + })); + build.onLoad({ filter: /^runtime$/, namespace }, () => ({ + contents: [ + 'export { RuntimeClient } from "runtime-shell-sdk/client";', + 'export { ShellChannel, MAX_FRAME_SIZE } from "runtime-shell-sdk/protocol";', + ].join("\n"), + loader: "js", + })); }, }; } From 92df6e419fed95b9b4a46f4f99ee99078f3c7937 Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Thu, 3 Sep 2026 23:40:48 +0000 Subject: [PATCH 21/26] refactor(runtime): derive shell transport from SDK --- src/core/runtimeShell.test.ts | 15 ++++----- src/core/runtimeShell.ts | 59 ++++++++++++++--------------------- 2 files changed, 31 insertions(+), 43 deletions(-) diff --git a/src/core/runtimeShell.test.ts b/src/core/runtimeShell.test.ts index 621ab6012..d3ba4bace 100644 --- a/src/core/runtimeShell.test.ts +++ b/src/core/runtimeShell.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test"; -import { ShellChannel } from "bedrock-agentcore/runtime"; +import { MAX_FRAME_SIZE, ShellChannel } from "bedrock-agentcore/runtime"; import type { RuntimeShellRequest } from "../handlers/runtime/types"; import { createRuntimeShellOpener, @@ -14,10 +14,11 @@ const REQUEST: RuntimeShellRequest = { }; function sdkSession( - frames: { channel: ShellChannel; payload: Uint8Array }[] = [], + frames: { channel: ShellChannel; payload: Buffer }[] = [], ): RuntimeShellSdkSession & { closed: number } { return { sessionId: "server-session", + reconnected: false, kicked: false, exitCode: 0, closed: 0, @@ -105,9 +106,9 @@ describe("createRuntimeShellOpener", () => { const sent: (string | Buffer)[] = []; const resizes: unknown[] = []; const session = sdkSession([ - { channel: ShellChannel.STDOUT, payload: new TextEncoder().encode("out") }, - { channel: ShellChannel.STATUS, payload: new Uint8Array() }, - { channel: ShellChannel.STDERR, payload: new TextEncoder().encode("err") }, + { channel: ShellChannel.STDOUT, payload: Buffer.from("out") }, + { channel: ShellChannel.STATUS, payload: Buffer.alloc(0) }, + { channel: ShellChannel.STDERR, payload: Buffer.from("err") }, ]); session.send = async (data) => { sent.push(data); @@ -149,11 +150,11 @@ describe("createRuntimeShellOpener", () => { sleep: async () => {}, }); const result = await opener(REQUEST, { region: "us-west-2" }); - const paste = Buffer.alloc(64 * 1024 + 1, 0x61); + const paste = Buffer.alloc(MAX_FRAME_SIZE + 1, 0x61); await result.send(paste); - expect(sent.map((frame) => frame.byteLength)).toEqual([64 * 1024 - 1, 2]); + expect(sent.map((frame) => frame.byteLength)).toEqual([MAX_FRAME_SIZE - 1, 2]); expect(Buffer.concat(sent)).toEqual(paste); }); diff --git a/src/core/runtimeShell.ts b/src/core/runtimeShell.ts index 7dbac610f..6271753ea 100644 --- a/src/core/runtimeShell.ts +++ b/src/core/runtimeShell.ts @@ -1,40 +1,33 @@ -import { RuntimeClient as AgentCoreRuntimeClient } from "bedrock-agentcore/runtime"; -import type { AwsCredentialIdentityProvider } from "@smithy/types"; +import { + MAX_FRAME_SIZE, + RuntimeClient as AgentCoreRuntimeClient, + ShellChannel, + type OpenShellParams, + type ShellFrame, + type ShellSession, +} from "bedrock-agentcore/runtime"; import { Buffer } from "node:buffer"; import type { RuntimeShellFrame, RuntimeShellSession } from "../handlers/runtime/types"; import type { OpenRuntimeShell } from "./runtime"; import type { CoreOptions } from "./types"; -export type RuntimeShellSdkOpenInput = { - runtimeArn: string; - endpointName: string; - sessionId?: string; - auth: "sigv4" | { type: "oauth"; bearerToken: string }; - reconnectConfig: { onReconnect?: (reconnected: boolean) => void }; -}; +export type RuntimeShellSdkOpenInput = OpenShellParams; -export type RuntimeShellSdkFrame = { - channel: number; - payload: Uint8Array; -}; +export type RuntimeShellSdkFrame = Pick; -export interface RuntimeShellSdkSession extends AsyncIterable { - readonly sessionId: string; - readonly kicked: boolean; - readonly exitCode: number | null; - send(data: string | Buffer): Promise; - resize(columns: number, rows: number): Promise; - close(): Promise; -} +export type RuntimeShellSdkSession = Pick< + ShellSession, + "sessionId" | "reconnected" | "kicked" | "exitCode" | "send" | "resize" | "close" +> & + AsyncIterable; export interface RuntimeShellSdkClient { openShell(input: RuntimeShellSdkOpenInput): Promise; } -export type RuntimeShellSdkClientConfig = { - region: string; - credentialsProvider?: AwsCredentialIdentityProvider; -}; +export type RuntimeShellSdkClientConfig = NonNullable< + ConstructorParameters[0] +>; export type CreateRuntimeShellSdkClient = ( config: RuntimeShellSdkClientConfig, @@ -47,17 +40,11 @@ export type RuntimeShellOpenerConfig = { const RETRYABLE_UPGRADE = /HTTP (409|424|429)\b/; const MAX_ATTEMPTS = 5; -const MAX_STDIN_PAYLOAD_BYTES = 64 * 1024 - 1; -const STDOUT_CHANNEL = 1; -const STDERR_CHANNEL = 2; +const MAX_STDIN_PAYLOAD_BYTES = MAX_FRAME_SIZE - 1; export function createRuntimeShellOpener(config: RuntimeShellOpenerConfig = {}): OpenRuntimeShell { const createClient = - config.createClient ?? - ((clientConfig) => - new AgentCoreRuntimeClient( - clientConfig as ConstructorParameters[0], - ) as unknown as RuntimeShellSdkClient); + config.createClient ?? ((clientConfig) => new AgentCoreRuntimeClient(clientConfig)); const sleep = config.sleep ?? ((delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs))); @@ -97,7 +84,7 @@ export function createRuntimeShellOpener(config: RuntimeShellOpenerConfig = {}): function credentialProvider( credentials: NonNullable, -): AwsCredentialIdentityProvider { +): NonNullable { return typeof credentials === "function" ? credentials : async () => credentials; } @@ -142,9 +129,9 @@ class RuntimeShellSessionAdapter implements RuntimeShellSession { async *[Symbol.asyncIterator](): AsyncIterator { for await (const frame of this.session) { - if (frame.channel === STDOUT_CHANNEL) { + if (frame.channel === ShellChannel.STDOUT) { yield { type: "stdout", data: Uint8Array.from(frame.payload) }; - } else if (frame.channel === STDERR_CHANNEL) { + } else if (frame.channel === ShellChannel.STDERR) { yield { type: "stderr", data: Uint8Array.from(frame.payload) }; } } From df026ce134e15150e8b12d71a67b721a03ae8aab Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Thu, 3 Sep 2026 23:41:07 +0000 Subject: [PATCH 22/26] fix(runtime): report shell reconnect outcome --- src/core/runtimeShell.test.ts | 24 +++++++++++++++++++++++ src/handlers/runtime/shell/operation.ts | 8 +++++++- src/handlers/runtime/shell/shell.test.tsx | 15 +++++++++++++- src/handlers/runtime/types.tsx | 2 +- 4 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/core/runtimeShell.test.ts b/src/core/runtimeShell.test.ts index d3ba4bace..42274f356 100644 --- a/src/core/runtimeShell.test.ts +++ b/src/core/runtimeShell.test.ts @@ -4,6 +4,7 @@ import type { RuntimeShellRequest } from "../handlers/runtime/types"; import { createRuntimeShellOpener, type RuntimeShellSdkClient, + type RuntimeShellSdkOpenInput, type RuntimeShellSdkSession, } from "./runtimeShell"; @@ -102,6 +103,29 @@ describe("createRuntimeShellOpener", () => { ]); }); + test("preserves the SDK reconnect outcome callback", async () => { + const opens: RuntimeShellSdkOpenInput[] = []; + const outcomes: boolean[] = []; + const onReconnect = async (reconnected: boolean) => { + outcomes.push(reconnected); + }; + const opener = createRuntimeShellOpener({ + createClient: () => ({ + openShell: async (input) => { + opens.push(input); + return sdkSession(); + }, + }), + sleep: async () => {}, + }); + + await opener({ ...REQUEST, onReconnect }, { region: "us-west-2" }); + await opens[0]?.reconnectConfig?.onReconnect?.(false); + + expect(opens[0]?.reconnectConfig?.onReconnect).toBe(onReconnect); + expect(outcomes).toEqual([false]); + }); + test("translates stdout and stderr frames and delegates writes", async () => { const sent: (string | Buffer)[] = []; const resizes: unknown[] = []; diff --git a/src/handlers/runtime/shell/operation.ts b/src/handlers/runtime/shell/operation.ts index ebb59c95e..30615ed74 100644 --- a/src/handlers/runtime/shell/operation.ts +++ b/src/handlers/runtime/shell/operation.ts @@ -34,7 +34,13 @@ export async function runRuntimeShell(input: RunRuntimeShellInput): Promise io.stderr.write("\r\nReconnected to shell.\r\n"); + request.onReconnect = (reconnected) => { + io.stderr.write( + reconnected + ? "\r\nReattached to existing shell.\r\n" + : "\r\nPrevious shell unavailable; started a new shell.\r\n", + ); + }; io.stderr.write(`Connecting to Runtime ${runtimeId} (${qualifier})...\n`); const session = await core.runtime.openRuntimeShell(request, options); diff --git a/src/handlers/runtime/shell/shell.test.tsx b/src/handlers/runtime/shell/shell.test.tsx index 7c7157cb4..6b51f7db5 100644 --- a/src/handlers/runtime/shell/shell.test.tsx +++ b/src/handlers/runtime/shell/shell.test.tsx @@ -1,7 +1,7 @@ import { describe, expect, test } from "bun:test"; import type { GetAgentRuntimeResponse } from "@aws-sdk/client-bedrock-agentcore-control"; import { createRootHandler } from "../../index"; -import type { RuntimeShellSession } from "../types"; +import type { RuntimeShellRequest, RuntimeShellSession } from "../types"; import { createSilentLogger, TestCoreClient, @@ -117,6 +117,19 @@ describe("runtime shell command", () => { ).toMatchObject({ bearerToken: "token" }); }); + test("reports whether reconnect preserved the existing shell", async () => { + const subject = harness(); + + await subject.run("--id", RUNTIME_ID, "--qualifier", "prod"); + const request = subject.core.runtime.calls.find((call) => call.method === "openRuntimeShell") + ?.args[0] as RuntimeShellRequest; + await request.onReconnect?.(true); + await request.onReconnect?.(false); + + expect(subject.io.stderr()).toContain("Reattached to existing shell."); + expect(subject.io.stderr()).toContain("Previous shell unavailable; started a new shell."); + }); + test("rejects JSON mode", async () => { const subject = harness(); diff --git a/src/handlers/runtime/types.tsx b/src/handlers/runtime/types.tsx index be5f6cac0..273afe7a6 100644 --- a/src/handlers/runtime/types.tsx +++ b/src/handlers/runtime/types.tsx @@ -59,7 +59,7 @@ export type RuntimeShellRequest = { qualifier: string; runtimeSessionId?: string; bearerToken?: string; - onReconnect?: () => void; + onReconnect?: (reconnected: boolean) => void | Promise; }; export type RuntimeShellFrame = From 7c206df45ab2c71aa83eebce6aa39a0becda3738 Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Thu, 3 Sep 2026 23:41:24 +0000 Subject: [PATCH 23/26] refactor(runtime): suspend Ink during shell sessions --- src/handlers/runtime/shell/screen.tsx | 58 ++++++--- .../runtime/shell/shell.screen.test.tsx | 113 +++++++----------- 2 files changed, 87 insertions(+), 84 deletions(-) diff --git a/src/handlers/runtime/shell/screen.tsx b/src/handlers/runtime/shell/screen.tsx index f0f781af1..6687b3c46 100644 --- a/src/handlers/runtime/shell/screen.tsx +++ b/src/handlers/runtime/shell/screen.tsx @@ -1,11 +1,10 @@ import { useEffect, useRef } from "react"; -import { useApp } from "ink"; +import { useApp, useStderr, useStdin, useStdout } from "ink"; import { useLocation, useNavigate, useParams } from "react-router"; import { RuntimeEndpointPicker } from "../../../components/RuntimeEndpointPicker"; import { RuntimePicker } from "../../../components/RuntimePicker"; import { Spinner } from "../../../components/ui/spinner"; import { SilentCLIError } from "../../../errors"; -import { TuiHandoffKey } from "../../../tui/handoff"; import type { ScreenProps } from "../../types"; import { RuntimeShellLaunchContextKey } from "./launchContext"; import { runRuntimeShell } from "./operation"; @@ -82,7 +81,11 @@ function RuntimeShellHandoff({ qualifier, returnPath, }: ScreenProps & { runtimeId: string; qualifier: string; returnPath?: string }) { - const { exit } = useApp(); + const { exit, suspendTerminal } = useApp(); + const { stdin } = useStdin(); + const { stdout } = useStdout(); + const { stderr } = useStderr(); + const navigate = useNavigate(); const requested = useRef(false); const launchContext = ctx.value(RuntimeShellLaunchContextKey); const initialContext = launchContext?.runtimeId === runtimeId ? launchContext : undefined; @@ -90,23 +93,44 @@ function RuntimeShellHandoff({ useEffect(() => { if (requested.current) return; requested.current = true; - ctx.require(TuiHandoffKey).request(async ({ ctx, core, io }) => { + void (async () => { try { - await runRuntimeShell({ - ctx, - core, - io, - runtimeId, - qualifier, - launchContext: initialContext, - }); + await suspendTerminal(() => + runRuntimeShell({ + ctx, + core, + io: { stdin, stdout, stderr }, + runtimeId, + qualifier, + launchContext: initialContext, + }), + ); } catch (error) { - if (returnPath === undefined || !(error instanceof SilentCLIError)) throw error; + if (returnPath === undefined || !(error instanceof SilentCLIError)) { + exit(error); + return; + } + } + if (returnPath === undefined) { + exit(); + } else { + navigate(returnPath, { replace: true }); } - return returnPath === undefined ? undefined : { resumePath: returnPath }; - }); - exit(); - }, [ctx, core, exit, initialContext, qualifier, returnPath, runtimeId]); + })(); + }, [ + core, + ctx, + exit, + initialContext, + navigate, + qualifier, + returnPath, + runtimeId, + stderr, + stdin, + stdout, + suspendTerminal, + ]); return ; } diff --git a/src/handlers/runtime/shell/shell.screen.test.tsx b/src/handlers/runtime/shell/shell.screen.test.tsx index beabb22a9..7ed64aa9a 100644 --- a/src/handlers/runtime/shell/shell.screen.test.tsx +++ b/src/handlers/runtime/shell/shell.screen.test.tsx @@ -1,8 +1,7 @@ import { afterEach, describe, expect, test } from "bun:test"; -import { TuiHandoffController, TuiHandoffKey } from "../../../tui/handoff"; import { renderTuiAt } from "../../../tui"; import { DebugKey, EndpointKey, JsonKey, RegionKey } from "../../keys"; -import { type Context, ValueContext } from "../../../router"; +import { ValueContext } from "../../../router"; import type { RuntimeShellSession } from "../types"; import { cleanupScreens, @@ -66,52 +65,16 @@ function core() { } describe("RuntimeShellScreen", () => { - test("a shell selected from the bare picker returns to that picker", async () => { - const controller = new TuiHandoffController(); - let handoffContext!: Context; - const screen = renderScreen("/agentcore/runtime/shell", { - core: core(), - withContext: (ctx) => { - handoffContext = ctx.withValue(TuiHandoffKey, controller); - return handoffContext; - }, - }); - - await waitForText(screen.lastFrame, "checkout"); - await screen.press("return"); - await waitForText(screen.lastFrame, "prod"); - await screen.press("return"); - let handoff = controller.take(); - await waitFor(() => { - handoff ??= controller.take(); - return handoff !== undefined; - }); - const { streams } = ttyTestIO(); - - expect(screen.core.runtime.calls.some((call) => call.method === "listRuntimes")).toBe(true); - expect(screen.core.runtime.calls.some((call) => call.method === "listRuntimeEndpoints")).toBe( - true, - ); - await expect( - handoff!({ ctx: handoffContext, core: screen.core, io: streams.io }), - ).resolves.toEqual({ - resumePath: "/agentcore/runtime/shell", - }); - }); - test("a direct Runtime route skips the Runtime picker", async () => { - const controller = new TuiHandoffController(); const screen = renderScreen("/agentcore/runtime/shell/checkout-AbCdEf1234", { core: core(), - withContext: (ctx) => ctx.withValue(TuiHandoffKey, controller), }); await waitForText(screen.lastFrame, "prod"); expect(screen.core.runtime.calls.some((call) => call.method === "listRuntimes")).toBe(false); }); - test("a shell selected from Runtime details returns there after a nonzero exit", async () => { - const controller = new TuiHandoffController(); + test("renderTuiAt returns to Runtime details after a nonzero shell exit", async () => { const value = core(); const failedSession: RuntimeShellSession = { runtimeSessionId: "session-012345678901234567890123456789", @@ -123,36 +86,38 @@ describe("RuntimeShellScreen", () => { async *[Symbol.asyncIterator]() {}, }; value.runtime.setShellSession(failedSession); - let handoffContext!: Context; - const screen = renderScreen("/agentcore/runtime/get/checkout-AbCdEf1234", { - core: value, - withContext: (ctx) => { - handoffContext = ctx.withValue(TuiHandoffKey, controller); - return handoffContext; - }, - }); + const { streams, stdin } = ttyTestIO(); + const ctx = ValueContext.EmptyContext() + .withValue(RegionKey, "us-east-1") + .withValue(EndpointKey, undefined) + .withValue(JsonKey, false) + .withValue(DebugKey, false); + const detailText = "show the full JSON definition"; + const rendering = renderTuiAt( + "/agentcore/runtime/get/checkout-AbCdEf1234", + ctx, + value, + streams.io, + ); - await waitForText(screen.lastFrame, "show the full JSON definition"); - await screen.press("down"); - await screen.press("return"); - await waitForText(screen.lastFrame, "prod"); - await screen.press("return"); - let handoff = controller.take(); - await waitFor(() => { - handoff ??= controller.take(); - return handoff !== undefined; - }); - const { streams } = ttyTestIO(); + await waitFor(() => streams.stdout().includes(detailText)); + const initialDetails = streams.stdout().split(detailText).length; + stdin.write("\x1b[B"); + await waitFor(() => streams.stdout().includes("❯ shell")); + stdin.write("\r"); + await waitFor(() => streams.stdout().includes("prod")); + stdin.write("\r"); + await waitFor(() => streams.stderr().includes("Session closed · exit 42")); + await waitFor(() => streams.stdout().split(detailText).length > initialDetails); + stdin.write("\x03"); - await expect( - handoff!({ ctx: handoffContext, core: screen.core, io: streams.io }), - ).resolves.toEqual({ - resumePath: "/agentcore/runtime/get/checkout-AbCdEf1234", - }); - expect(streams.stderr()).toContain("Session closed · exit 42"); + await rendering; + expect(value.runtime.calls.filter((call) => call.method === "openRuntimeShell")).toHaveLength( + 1, + ); }); - test("renderTuiAt unmounts Ink before executing the shell handoff", async () => { + test("renderTuiAt runs a direct shell to completion", async () => { const value = core(); const { streams } = ttyTestIO(); const ctx = ValueContext.EmptyContext() @@ -167,7 +132,22 @@ describe("RuntimeShellScreen", () => { expect(streams.stderr()).toContain("Connected"); }); - test("renderTuiAt remounts a requested origin after the shell ends", async () => { + test("renderTuiAt propagates unexpected shell failures", async () => { + const value = core(); + value.runtime.setError(new Error("shell lookup failed")); + const { streams } = ttyTestIO(); + const ctx = ValueContext.EmptyContext() + .withValue(RegionKey, "us-east-1") + .withValue(EndpointKey, undefined) + .withValue(JsonKey, false) + .withValue(DebugKey, false); + + await expect( + renderTuiAt("/agentcore/runtime/shell/checkout-AbCdEf1234/prod", ctx, value, streams.io), + ).rejects.toThrow("shell lookup failed"); + }); + + test("renderTuiAt returns to a requested origin after the shell ends", async () => { const value = core(); const { streams, stdin } = ttyTestIO(); const ctx = ValueContext.EmptyContext() @@ -183,7 +163,6 @@ describe("RuntimeShellScreen", () => { stdin.write("\r"); await waitFor( () => value.runtime.calls.filter((call) => call.method === "listRuntimes").length === 2, - 5000, ); stdin.write("\x03"); From 5c8294903a391d8da0a890e9be4f1bad1168b303 Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Thu, 3 Sep 2026 23:41:29 +0000 Subject: [PATCH 24/26] refactor(tui): remove custom shell handoff --- src/tui/handoff.test.ts | 30 ------------------------------ src/tui/handoff.ts | 30 ------------------------------ src/tui/index.tsx | 26 +++++++++----------------- 3 files changed, 9 insertions(+), 77 deletions(-) delete mode 100644 src/tui/handoff.test.ts delete mode 100644 src/tui/handoff.ts diff --git a/src/tui/handoff.test.ts b/src/tui/handoff.test.ts deleted file mode 100644 index e23184d4d..000000000 --- a/src/tui/handoff.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { TuiHandoffController } from "./handoff"; - -describe("TuiHandoffController", () => { - test("stores one handoff and returns it exactly once", () => { - const controller = new TuiHandoffController(); - const handoff = async () => {}; - - controller.request(handoff); - - expect(controller.take()).toBe(handoff); - expect(controller.take()).toBeUndefined(); - }); - - test("rejects a second handoff request", () => { - const controller = new TuiHandoffController(); - controller.request(async () => {}); - - expect(() => controller.request(async () => {})).toThrow("TUI handoff already requested"); - }); - - test("preserves the handoff result", async () => { - const controller = new TuiHandoffController(); - controller.request(async () => ({ resumePath: "/agentcore/runtime/shell" })); - - await expect(controller.take()!({} as never)).resolves.toEqual({ - resumePath: "/agentcore/runtime/shell", - }); - }); -}); diff --git a/src/tui/handoff.ts b/src/tui/handoff.ts deleted file mode 100644 index 1f6830d9b..000000000 --- a/src/tui/handoff.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { Core } from "../handlers/types"; -import type { AppIO } from "../io"; -import { contextKey, type Context } from "../router"; - -export type TuiHandoffResult = { - resumePath?: string; -}; - -export type TuiHandoff = (input: { - ctx: Context; - core: Core; - io: AppIO; -}) => Promise; - -export class TuiHandoffController { - private handoff?: TuiHandoff; - - request(handoff: TuiHandoff): void { - if (this.handoff) throw new Error("TUI handoff already requested"); - this.handoff = handoff; - } - - take(): TuiHandoff | undefined { - const handoff = this.handoff; - this.handoff = undefined; - return handoff; - } -} - -export const TuiHandoffKey = contextKey("tui.handoff"); diff --git a/src/tui/index.tsx b/src/tui/index.tsx index 6e1daa93d..6268f0c70 100644 --- a/src/tui/index.tsx +++ b/src/tui/index.tsx @@ -14,7 +14,6 @@ import type { Core } from "../handlers/types"; import { JsonKey } from "../handlers/keys"; import { InvalidEnvironmentError } from "../errors"; import { ExitCode } from "../runnable"; -import { TuiHandoffController, TuiHandoffKey } from "./handoff"; // renderJson pretty-prints a value as indented JSON. It is the output // counterpart to renderTui: handlers call it to emit machine-readable results @@ -57,22 +56,15 @@ export async function renderTuiAt( // alternateScreen switches the terminal to its alternate buffer so the TUI // takes over the screen and the prior scrollback is restored on exit (like Vim). - let nextPath: string | undefined = path; - while (nextPath !== undefined) { - const handoffs = new TuiHandoffController(); - const tuiCtx = ctx.withValue(TuiHandoffKey, handoffs); - const { waitUntilExit } = render(, { - stdin: io.stdin, - stdout: io.stdout, - stderr: io.stderr, - interactive: true, - alternateScreen: true, - incrementalRendering: true, - }); - await waitUntilExit(); - const result = await handoffs.take()?.({ ctx: tuiCtx, core, io }); - nextPath = result?.resumePath; - } + const { waitUntilExit } = render(, { + stdin: io.stdin, + stdout: io.stdout, + stderr: io.stderr, + interactive: true, + alternateScreen: true, + incrementalRendering: true, + }); + await waitUntilExit(); } // renderTui builds the root DefaultHandle that mounts the Ink React tree. It From 79858dcc622a7de07d07c86327447a291b1316c7 Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Thu, 3 Sep 2026 23:54:59 +0000 Subject: [PATCH 25/26] test(runtime): wait for returned TUI input handler --- src/handlers/runtime/shell/shell.screen.test.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/handlers/runtime/shell/shell.screen.test.tsx b/src/handlers/runtime/shell/shell.screen.test.tsx index 7ed64aa9a..ab94d43ff 100644 --- a/src/handlers/runtime/shell/shell.screen.test.tsx +++ b/src/handlers/runtime/shell/shell.screen.test.tsx @@ -7,6 +7,7 @@ import { cleanupScreens, renderScreen, TestCoreClient, + tick, ttyTestIO, waitFor, waitForText, @@ -109,6 +110,7 @@ describe("RuntimeShellScreen", () => { stdin.write("\r"); await waitFor(() => streams.stderr().includes("Session closed · exit 42")); await waitFor(() => streams.stdout().split(detailText).length > initialDetails); + await tick(); stdin.write("\x03"); await rendering; From afbfd617ce2629de4b83622d4ce3e04e81f422f1 Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Fri, 4 Sep 2026 00:00:25 +0000 Subject: [PATCH 26/26] test(runtime): retry TUI exit after route return --- .../runtime/shell/shell.screen.test.tsx | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/handlers/runtime/shell/shell.screen.test.tsx b/src/handlers/runtime/shell/shell.screen.test.tsx index ab94d43ff..4989db37a 100644 --- a/src/handlers/runtime/shell/shell.screen.test.tsx +++ b/src/handlers/runtime/shell/shell.screen.test.tsx @@ -8,6 +8,7 @@ import { renderScreen, TestCoreClient, tick, + type TtyInput, ttyTestIO, waitFor, waitForText, @@ -65,6 +66,18 @@ function core() { return value; } +async function interruptUntilExit(rendering: Promise, stdin: TtyInput): Promise { + let settled = false; + const tracked = rendering.finally(() => { + settled = true; + }); + while (!settled) { + stdin.write("\x03"); + await tick(); + } + await tracked; +} + describe("RuntimeShellScreen", () => { test("a direct Runtime route skips the Runtime picker", async () => { const screen = renderScreen("/agentcore/runtime/shell/checkout-AbCdEf1234", { @@ -110,10 +123,8 @@ describe("RuntimeShellScreen", () => { stdin.write("\r"); await waitFor(() => streams.stderr().includes("Session closed · exit 42")); await waitFor(() => streams.stdout().split(detailText).length > initialDetails); - await tick(); - stdin.write("\x03"); - await rendering; + await interruptUntilExit(rendering, stdin); expect(value.runtime.calls.filter((call) => call.method === "openRuntimeShell")).toHaveLength( 1, ); @@ -166,9 +177,8 @@ describe("RuntimeShellScreen", () => { await waitFor( () => value.runtime.calls.filter((call) => call.method === "listRuntimes").length === 2, ); - stdin.write("\x03"); - await rendering; + await interruptUntilExit(rendering, stdin); expect(value.runtime.calls.filter((call) => call.method === "openRuntimeShell")).toHaveLength( 1, );