feat(project): add policy-engine and policy TUI wizards - #2462
Conversation
A bare `agentcore add policy-engine` on a TTY asks for a name, bounded live by the deployed 48-character cap, and — when the project has Gateways — which to attach the engine to, with the enforcement mode revealed beneath the checklist once one is checked, which is the --attach-mode-requires-gateways rule. A bare `agentcore add policy` picks the engine from the project, names the policy, takes the Cedar statement either pasted into a multi-line editor or from a file whose path opens in place under that row and is recorded as sourceFile, asks active or log-only, and shows the inferred authorization phase on the review so the heuristic's guess is seen before it is written. Both handlers gain a shared builder the flags and the wizard call, so the two paths bound names, attach Gateways, map modes and infer the phase identically. --description, --encryption-key-arn and --tags on the engine and --description, --validation-mode and --authorization-phase on the policy stay flag-only.
|
Claude Security Review: no high-confidence findings. (run) |
There was a problem hiding this comment.
AgentCore Harness Review
Verdict: Looks good
Consistent with the earlier project add … TUI wizards in this series. The two new screens are cleanly factored, and refactoring both handlers to funnel through shared toAddPolicyEngineInput / toAddPolicyInput builders is a nice change — it guarantees the wizard and the flag path share deployed-name budgeting, gateway attachment shape, mode mapping, and phase inference.
Notes from the review (all minor / non-blocking):
statementOfinpolicy/screen.tsxcallsreadFileSyncon every render of the review step (viasummaryOf). The file was already validated byreadableFileSchemaon the source step and is read again asynchronously on submit, so the sync read on render is a small unnecessary cost. AuseMemo(or reading once when leaving the source step) would tidy this up, but the file is small/local and the schema keeps unreadable files from getting here, so it's fine as is.- The wizard shows the inferred authorization phase computed from the
readFileSyncsnapshot on the review, but the submit path re-reads the file async and re-infers before writing. If the file changes between review and submit the value written can differ from the value shown; the "inferred from the statement" label makes this reasonable, and--authorization-phasein the flag path lets users override it. Worth being aware of, not worth changing. PolicyEngineInput.description/encryptionKeyArn/tagsare declared on the shared type but the wizard never populates them (they are flag-only by design). Consider a shorter wizard-facing type if you want to make that intent explicit, but the current shape is fine and keeps the two callsites symmetric.
Tests exercise real project state via temp directories rather than mocks, cover the interesting compound-field navigation (list ↔ mode focus, unchecking everything, up-from-first-mode-row), the deployed-name budget against the longest target, both file/inline source paths, the empty-engine empty state, and TTY/flag/--json dispatch. LGTM to merge.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## refactor #2462 +/- ##
============================================
+ Coverage 97.24% 97.26% +0.02%
============================================
Files 617 619 +2
Lines 43848 44334 +486
============================================
+ Hits 42639 43121 +482
- Misses 1209 1213 +4 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
| if (values.source === "file") { | ||
| try { | ||
| statement = await readFile(values.statementFile, "utf8"); |
There was a problem hiding this comment.
[P2] Could we use SourceResolver.resolveText for strict UTF-8 decoding in both submission and preview? A Latin-1 Cedar literal "café" is silently saved as "caf�" here, changing what the policy matches, while the review only shows the filename.
There was a problem hiding this comment.
Done. Both the review and the submit now go through SourceResolver.resolveText("statement", "file://…"), so a Latin-1 file is refused with the resolver's own must contain valid UTF-8 message instead of being decoded lossily. That needed SourceResolverConfig.stdin to become optional (a screen has no injected IO and never resolves -); with it omitted, - is refused. Covered by a new test that writes Latin-1 bytes and checks both the review row and the submit.
| function statementOf(values: PolicyFormValues): string | undefined { | ||
| if (values.source === "inline") return values.statement; | ||
| try { | ||
| return readFileSync(values.statementFile, "utf8"); |
There was a problem hiding this comment.
[P2] Could we validate the path first, then load the statement asynchronously and keep it for the summary? summaryOf(values) runs on every render, so typing a named-pipe path freezes the TUI before Enter or readableFileSchema can reject it.
There was a problem hiding this comment.
Done. The sync read in render is gone. The review step is its own component now, PolicyReview, which loads the statement asynchronously when it mounts — so nothing reads while the path is being typed, and only after readableFileSchema has already accepted it (a FIFO fails isFile() there). The row reads reading <path>… until it resolves, then the inferred phase, or the resolver's error if it fails.
| ? `attached to ${values.attached.length} ${values.attached.length === 1 ? "Gateway" : "Gateways"} in ${values.mode} mode` | ||
| : undefined | ||
| } | ||
| successNextSteps={[`agentcore add policy --engine ${values.name}`, "agentcore deploy"]} |
There was a problem hiding this comment.
[P2] Could we suggest bare agentcore add policy here so it opens the wizard and asks for the engine? Passing --engine selects headless mode, so copying this command fails with required option '--name' not specified, even in a TTY.
There was a problem hiding this comment.
Done: the next step is bare agentcore add policy now, with a comment on why --engine would defeat it. The test asserts the flag is absent.
… path A file-sourced statement is read through SourceResolver, as `--statement file://…` reads it, so a file that is not valid UTF-8 is refused instead of decoded with replacement characters that would change what the policy matches. The review loads it asynchronously when the step mounts rather than synchronously on every render, so typing a path never blocks the TUI and only a path the source step has already accepted is read. The engine wizard's next step is bare `agentcore add policy`, which opens the policy wizard; naming the engine with --engine would select the headless path. SourceResolverConfig.stdin is optional now, for callers that only ever resolve inline values and files.
…d-policy-tui # Conflicts: # src/components/Root.tsx # src/handlers/project/add/add.screen.test.tsx # src/handlers/project/add/index.ts
|
Claude Security Review: no high-confidence findings. (run) |
Description
Adds interactive
agentcore add policy-engineandagentcore add policywizards, the next two screens in theproject addwizard series (design doc: https://artifactory.amazon.dev/view/agentcore-project-add-wizard-designs). Built directly onrefactor; no stacking.policy-engine (
name → gateways → review)PolicyEngineNameSchemaand the deployed<project>_<target>_<name>48-character cap, checked against the longest declared target, so the step reports the real budget rather than the schema's 48.enforcedefault /log-only) beneath the list, which is exactly the--attach-mode requires --attach-to-gatewaysrule; enter moves from the list into the mode rows, enter again continues. Enter with nothing checked continues without attaching.(none) · attach from a Gateway laterwhen nothing was picked. Success suggests bareagentcore add policyas the next step, which opens the policy wizard (naming the engine with--enginewould select the headless path).policy (
engine → name → source → statement → enforcement → review)agentcore add policy-engine.sourceFile, as--statement file://…does.active(default) /log-only.INITIATEorRETURN_OUTPUT) with a note that it was inferred, sinceinferAuthorizationPhaseis a substring heuristic and the user should see the guess before it is written. A file-sourced statement is loaded when the review mounts, asynchronously and throughSourceResolver(the same strict UTF-8 path as--statement file://…), so a Latin-1 file is refused rather than decoded lossily and nothing reads while the path is typed.SourceResolverConfig.stdinbecame optional for this file-only use.Both handlers gain a shared builder (
toAddPolicyEngineInput,toAddPolicyInput) that the flags and the wizard call, so both paths bound names, attach Gateways, map modes and infer the phase identically. Flag-only, as designed:--description,--encryption-key-arn,--tagson the engine;--description,--validation-mode,--authorization-phaseon the policy.Related Issue
Closes #
Documentation PR
Not applicable: no flags or help text change.
Type of Change
Testing
bun testbun run test:e2e, or explained why they are not applicable — TUI-only change, no deployed resourcesbun run typecheckbun run lint:checkbun run format:checkbun run buildsrc/assets/, I updated affected snapshots withbun test <test-file> --update-snapshotsand committed themNew screen tests for both wizards: the bare engine matching the flag path byte for byte; attaching to checked Gateways in the revealed mode and the resulting
policyEngineConfiguration; enter with nothing checked; up from the mode rows back to the list; name pattern and deployed-name budget; the inline policy with the inferredINITIATEon review; a file-sourced policy recordingsourceFileand inferringRETURN_OUTPUT; a missing file kept on the step; a non-UTF-8 file refused on review and on submit; an empty statement refused; a multi-line statement kept as typed; both empty states; rejected adds handing the form back; esc to the menu; and TTY/non-TTY/flag/--jsondispatch for each command. Also walked the built bundle in a scratch project.Checklist