Skip to content

feat: interactive asset selection and list-typed target handling#204

Open
DS-KoolAid wants to merge 2 commits intomainfrom
feat/interactive-asset-selection
Open

feat: interactive asset selection and list-typed target handling#204
DS-KoolAid wants to merge 2 commits intomainfrom
feat/interactive-asset-selection

Conversation

@DS-KoolAid
Copy link
Copy Markdown
Contributor

Summary

  • Add interactive asset picker for job/schedule commands — users can select an existing asset or enter a new network target (IP/CIDR) instead of always defaulting to the agent hostname
  • Handle target field being a list (ENG-1908) via new normalize_target helper in job_helpers.py
  • Apply the same selection logic in both job.py and schedule.py

Test plan

  • Updated test_aegis_job.py — monkeypatch _select_asset in 5 test cases
  • Updated test_aegis_schedule.py — mock select_asset for non-AD capabilities
  • Verify run command with an AD-domain capability (should skip asset selection)
  • Verify run command with a non-AD capability shows asset picker
  • Verify entering a new IP/CIDR creates the asset and runs the job

Add interactive asset picker for job/schedule commands so users can
select existing assets or enter new network targets (IP/CIDR) instead
of defaulting to the agent hostname. Handle target field being a list
(ENG-1908) via normalize_target helper.
Drop normalize_target in favor of main's extract_target_type helper
for handling list-typed capability targets (ENG-1908).
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 3, 2026

Walkthrough

Non-domain asset handling in job and schedule interactive commands now uses interactive selection via new helpers instead of constructing fixed asset keys. Added helpers: select_asset(menu, hostname), _create_asset_target(menu, hostname, target_input, colors), and extract_target_type(capability_info); capability target normalization was unified to use extract_target_type. run_job and add_schedule call select_asset and return early if selection is cancelled. Tests were updated to monkeypatch the asset-selection helpers to return fixed asset values for deterministic execution.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/interactive-asset-selection

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
praetorian_cli/sdk/test/ui/test_aegis_job.py (1)

49-51: Add one non-mocked test for the new target-handling path.

These monkeypatches keep the existing job-flow tests stable, but they also bypass all of the new behavior from this PR. There’s still no automated coverage for list-backed target values, manual IP/CIDR entry, or the 409 conflict path in select_asset().

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@praetorian_cli/sdk/test/ui/test_aegis_job.py` around lines 49 - 51, Add a new
test in praetorian_cli/sdk/test/ui/test_aegis_job.py that does not monkeypatch
Confirm.ask or _select_asset so the real target-handling logic and
select_asset() are exercised: construct a job invocation that provides a
list-backed target value, simulate manual IP/CIDR input (e.g., by supplying
input to the prompt mechanism used by Confirm.ask or the prompt toolkit), and
stub only the network/backend call that can return a 409 to trigger the conflict
path in select_asset(); assert the correct branch behavior (selected target,
conflict handling, and resulting job payload) to cover list-backed targets,
manual entry, and the 409 conflict branch.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@praetorian_cli/ui/aegis/commands/job_helpers.py`:
- Around line 381-390: Remove the unnecessary f-string prefixes on the two
menu.console.print calls that do not interpolate values: change the call
printing "\n  Select target asset:" and the call printing "     0. Enter target
manually (IP, CIDR, or hostname)" to plain string literals (leave the loop's
menu.console.print(f"    {i:2d}. {display}") intact since it interpolates i and
display). This fixes the F541 lint errors while keeping the enumerated line that
uses {i:2d} and {display} unchanged.
- Around line 367-370: The asset lookup uses key_prefix=f"#asset#{hostname}"
which can match hostnames with the same leading text (e.g. "agent01-old");
update the key_prefix passed to menu.sdk.assets.list to delimit the hostname
segment (e.g. append the segment separator used in your key schema after
{hostname}) so only exact-host matches are returned; change the call in
job_helpers.py where menu.sdk.assets.list(...) is invoked to use the delimited
prefix (keeping hostname variable and assets, _ = menu.sdk.assets.list(...)
unchanged).
- Around line 341-349: The current asset-creation try/except logs non-409 errors
but still returns a fabricated target_key; change the block so you call
menu.sdk.assets.add(...) and capture its return (use the key/value it returns to
set target_key/target_display on success), and in the except block if '409' is
not in str(e) log the error and return (None, None) immediately so callers
(run_job(), add_schedule()) don't proceed with a missing asset; if the exception
contains '409' treat it as a benign conflict and continue returning the existing
target_key/target_display as before.

---

Nitpick comments:
In `@praetorian_cli/sdk/test/ui/test_aegis_job.py`:
- Around line 49-51: Add a new test in
praetorian_cli/sdk/test/ui/test_aegis_job.py that does not monkeypatch
Confirm.ask or _select_asset so the real target-handling logic and
select_asset() are exercised: construct a job invocation that provides a
list-backed target value, simulate manual IP/CIDR input (e.g., by supplying
input to the prompt mechanism used by Confirm.ask or the prompt toolkit), and
stub only the network/backend call that can return a 409 to trigger the conflict
path in select_asset(); assert the correct branch behavior (selected target,
conflict handling, and resulting job payload) to cover list-backed targets,
manual entry, and the 409 conflict branch.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a0e61681-37ca-41fd-90c8-e513328bccc4

📥 Commits

Reviewing files that changed from the base of the PR and between 6990a97 and 1f9a6a4.

📒 Files selected for processing (5)
  • praetorian_cli/sdk/test/ui/test_aegis_job.py
  • praetorian_cli/sdk/test/ui/test_aegis_schedule.py
  • praetorian_cli/ui/aegis/commands/job.py
  • praetorian_cli/ui/aegis/commands/job_helpers.py
  • praetorian_cli/ui/aegis/commands/schedule.py

Comment on lines +341 to +349
try:
menu.console.print(f" [{colors['dim']}]Creating asset {target_input}...[/{colors['dim']}]")
menu.sdk.assets.add(group=hostname, identifier=target_input)
except Exception as e:
# Asset may already exist — 409 conflict is fine
if '409' not in str(e):
menu.console.print(f" [{colors['dim']}]Note: could not create asset: {e}[/{colors['dim']}]")

return target_key, target_display
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fail the flow when asset creation fails.

Non-409 errors are only logged here, but the helper still returns a fabricated target_key. That lets run_job() and add_schedule() continue against a target that was never created. Return (None, None) for non-conflict failures, and prefer the key returned by assets.add() on success.

Suggested fix
 def _create_asset_target(menu, hostname, target_input, colors):
     target_key = f"#asset#{hostname}#{target_input}"
     target_display = f"asset {target_input}"

     try:
         menu.console.print(f"  [{colors['dim']}]Creating asset {target_input}...[/{colors['dim']}]")
-        menu.sdk.assets.add(group=hostname, identifier=target_input)
+        asset = menu.sdk.assets.add(group=hostname, identifier=target_input)
+        target_key = asset.get('key', target_key)
     except Exception as e:
         # Asset may already exist — 409 conflict is fine
         if '409' not in str(e):
             menu.console.print(f"  [{colors['dim']}]Note: could not create asset: {e}[/{colors['dim']}]")
+            return None, None

     return target_key, target_display
🧰 Tools
🪛 Ruff (0.15.9)

[warning] 344-344: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@praetorian_cli/ui/aegis/commands/job_helpers.py` around lines 341 - 349, The
current asset-creation try/except logs non-409 errors but still returns a
fabricated target_key; change the block so you call menu.sdk.assets.add(...) and
capture its return (use the key/value it returns to set
target_key/target_display on success), and in the except block if '409' is not
in str(e) log the error and return (None, None) immediately so callers
(run_job(), add_schedule()) don't proceed with a missing asset; if the exception
contains '409' treat it as a benign conflict and continue returning the existing
target_key/target_display as before.

Comment on lines +367 to +370
menu.console.print(f" [{colors['dim']}]Fetching assets for {hostname}...[/{colors['dim']}]")

# Fetch assets associated with this agent's hostname
assets, _ = menu.sdk.assets.list(key_prefix=f"#asset#{hostname}", pages=1)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Delimit the hostname segment in the asset lookup prefix.

key_prefix=f"#asset#{hostname}" also matches groups like agent01-old or agent010. That can surface the wrong assets in the picker. Use the full segment prefix instead.

Suggested fix
-        assets, _ = menu.sdk.assets.list(key_prefix=f"#asset#{hostname}", pages=1)
+        assets, _ = menu.sdk.assets.list(key_prefix=f"#asset#{hostname}#", pages=1)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
menu.console.print(f" [{colors['dim']}]Fetching assets for {hostname}...[/{colors['dim']}]")
# Fetch assets associated with this agent's hostname
assets, _ = menu.sdk.assets.list(key_prefix=f"#asset#{hostname}", pages=1)
menu.console.print(f" [{colors['dim']}]Fetching assets for {hostname}...[/{colors['dim']}]")
# Fetch assets associated with this agent's hostname
assets, _ = menu.sdk.assets.list(key_prefix=f"#asset#{hostname}#", pages=1)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@praetorian_cli/ui/aegis/commands/job_helpers.py` around lines 367 - 370, The
asset lookup uses key_prefix=f"#asset#{hostname}" which can match hostnames with
the same leading text (e.g. "agent01-old"); update the key_prefix passed to
menu.sdk.assets.list to delimit the hostname segment (e.g. append the segment
separator used in your key schema after {hostname}) so only exact-host matches
are returned; change the call in job_helpers.py where menu.sdk.assets.list(...)
is invoked to use the delimited prefix (keeping hostname variable and assets, _
= menu.sdk.assets.list(...) unchanged).

Comment on lines +381 to +390
menu.console.print(f"\n Select target asset:")

for i, c in enumerate(choices, 1):
display = c['name']
if c['dns'] and c['dns'] != c['name']:
display = f"{c['dns']} / {c['name']}"
menu.console.print(f" {i:2d}. {display}")

# Option to enter manually
menu.console.print(f" 0. Enter target manually (IP, CIDR, or hostname)")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove the unused f prefixes here.

Ruff is already flagging both of these strings as F541, so this helper will stay red on lint until they become plain literals.

Suggested fix
-        menu.console.print(f"\n  Select target asset:")
+        menu.console.print("\n  Select target asset:")
@@
-        menu.console.print(f"     0. Enter target manually (IP, CIDR, or hostname)")
+        menu.console.print("     0. Enter target manually (IP, CIDR, or hostname)")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
menu.console.print(f"\n Select target asset:")
for i, c in enumerate(choices, 1):
display = c['name']
if c['dns'] and c['dns'] != c['name']:
display = f"{c['dns']} / {c['name']}"
menu.console.print(f" {i:2d}. {display}")
# Option to enter manually
menu.console.print(f" 0. Enter target manually (IP, CIDR, or hostname)")
menu.console.print("\n Select target asset:")
for i, c in enumerate(choices, 1):
display = c['name']
if c['dns'] and c['dns'] != c['name']:
display = f"{c['dns']} / {c['name']}"
menu.console.print(f" {i:2d}. {display}")
# Option to enter manually
menu.console.print(" 0. Enter target manually (IP, CIDR, or hostname)")
🧰 Tools
🪛 Ruff (0.15.9)

[error] 381-381: f-string without any placeholders

Remove extraneous f prefix

(F541)


[error] 390-390: f-string without any placeholders

Remove extraneous f prefix

(F541)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@praetorian_cli/ui/aegis/commands/job_helpers.py` around lines 381 - 390,
Remove the unnecessary f-string prefixes on the two menu.console.print calls
that do not interpolate values: change the call printing "\n  Select target
asset:" and the call printing "     0. Enter target manually (IP, CIDR, or
hostname)" to plain string literals (leave the loop's menu.console.print(f"   
{i:2d}. {display}") intact since it interpolates i and display). This fixes the
F541 lint errors while keeping the enumerated line that uses {i:2d} and
{display} unchanged.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (3)
praetorian_cli/ui/aegis/commands/job_helpers.py (3)

345-353: ⚠️ Potential issue | 🟠 Major

Still returns a fake asset key after failed creation.

On Lines 347-352, non-409 failures only log and then still return a truthy target_key. Callers treat that as success, so the flow can continue against an asset that was never created. Also, assets.add() already returns the canonical asset record, so use its key on success.

Proposed fix
     try:
         menu.console.print(f"  [{colors['dim']}]Creating asset {target_input}...[/{colors['dim']}]")
-        menu.sdk.assets.add(group=hostname, identifier=target_input)
+        asset = menu.sdk.assets.add(group=hostname, identifier=target_input)
+        target_key = asset.get('key', target_key)
     except Exception as e:
         # Asset may already exist — 409 conflict is fine
         if '409' not in str(e):
             menu.console.print(f"  [{colors['dim']}]Note: could not create asset: {e}[/{colors['dim']}]")
+            return None, None
 
     return target_key, target_display
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@praetorian_cli/ui/aegis/commands/job_helpers.py` around lines 345 - 353, The
code currently logs failures from menu.sdk.assets.add but still returns the
provisional target_key; instead, when calling
menu.sdk.assets.add(group=hostname, identifier=target_input) capture its return
value (the canonical asset record) and set target_key from that record on
success; on a 409 conflict call menu.sdk.assets.get(group=hostname,
identifier=target_input) to retrieve the existing asset's canonical key and set
target_key accordingly; on any non-409 error do not return the fake key (set
target_key to None or propagate the error) and only return a truthy target_key
when you have the actual key from the add/get calls. Ensure you reference
menu.sdk.assets.add, menu.sdk.assets.get, hostname, target_input, target_key,
and target_display when making the changes.

385-394: ⚠️ Potential issue | 🟡 Minor

Still tripping Ruff with literal f-strings.

These two prints do not interpolate anything, so Ruff will keep failing with F541 until they become plain string literals.

Proposed fix
-        menu.console.print(f"\n  Select target asset:")
+        menu.console.print("\n  Select target asset:")
@@
-        menu.console.print(f"     0. Enter target manually (IP, CIDR, or hostname)")
+        menu.console.print("     0. Enter target manually (IP, CIDR, or hostname)")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@praetorian_cli/ui/aegis/commands/job_helpers.py` around lines 385 - 394, The
two menu.console.print calls in job_helpers.py (the literal prints in the
selection UI: the string "\n  Select target asset:" and the option "     0.
Enter target manually (IP, CIDR, or hostname)") are using f-strings without
interpolation which triggers Ruff F541; remove the f prefix so they become plain
string literals (e.g., menu.console.print("\n  Select target asset:") and
menu.console.print("     0. Enter target manually (IP, CIDR, or hostname)")),
leaving other f-strings like the enumerated display formatting unchanged.

374-374: ⚠️ Potential issue | 🟠 Major

Still using an undelimited asset prefix.

Line 374 uses #asset#{hostname} as the prefix, which also matches groups like agent01-old or agent010. Add the segment delimiter so the picker only shows exact-host assets.

Proposed fix
-        assets, _ = menu.sdk.assets.list(key_prefix=f"#asset#{hostname}", pages=1)
+        assets, _ = menu.sdk.assets.list(key_prefix=f"#asset#{hostname}#", pages=1)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@praetorian_cli/ui/aegis/commands/job_helpers.py` at line 374, Replace the
undelimited prefix passed to menu.sdk.assets.list so it matches only exact-host
assets: change the key_prefix argument used where assets, _ =
menu.sdk.assets.list(...) is called (currently f"#asset#{hostname}") to include
a segment delimiter before (and ideally after) the hostname (for example use a
delimiter like "/" or "-" so the prefix becomes "#asset#/<hostname>/" or
similar) so the picker only returns assets for the exact hostname rather than
partial matches.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@praetorian_cli/ui/aegis/commands/job_helpers.py`:
- Around line 398-406: After reading the user's choice from Prompt.ask and
calling .strip(), explicitly reject empty string inputs before attempting
int(choice) so we don't treat blank input as a direct target; update the logic
in the block around Prompt.ask/choice handling in job_helpers.py (where
variables menu, hostname, choice and function _create_asset_target are used) to
check if choice == "" and if so re-prompt (or loop back to Prompt.ask) instead
of falling through to the non-numeric branch that calls _create_asset_target
with an empty identifier.

---

Duplicate comments:
In `@praetorian_cli/ui/aegis/commands/job_helpers.py`:
- Around line 345-353: The code currently logs failures from menu.sdk.assets.add
but still returns the provisional target_key; instead, when calling
menu.sdk.assets.add(group=hostname, identifier=target_input) capture its return
value (the canonical asset record) and set target_key from that record on
success; on a 409 conflict call menu.sdk.assets.get(group=hostname,
identifier=target_input) to retrieve the existing asset's canonical key and set
target_key accordingly; on any non-409 error do not return the fake key (set
target_key to None or propagate the error) and only return a truthy target_key
when you have the actual key from the add/get calls. Ensure you reference
menu.sdk.assets.add, menu.sdk.assets.get, hostname, target_input, target_key,
and target_display when making the changes.
- Around line 385-394: The two menu.console.print calls in job_helpers.py (the
literal prints in the selection UI: the string "\n  Select target asset:" and
the option "     0. Enter target manually (IP, CIDR, or hostname)") are using
f-strings without interpolation which triggers Ruff F541; remove the f prefix so
they become plain string literals (e.g., menu.console.print("\n  Select target
asset:") and menu.console.print("     0. Enter target manually (IP, CIDR, or
hostname)")), leaving other f-strings like the enumerated display formatting
unchanged.
- Line 374: Replace the undelimited prefix passed to menu.sdk.assets.list so it
matches only exact-host assets: change the key_prefix argument used where
assets, _ = menu.sdk.assets.list(...) is called (currently f"#asset#{hostname}")
to include a segment delimiter before (and ideally after) the hostname (for
example use a delimiter like "/" or "-" so the prefix becomes
"#asset#/<hostname>/" or similar) so the picker only returns assets for the
exact hostname rather than partial matches.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5d91d47a-96ab-49df-99d1-f7c53477a5b9

📥 Commits

Reviewing files that changed from the base of the PR and between 1f9a6a4 and d964804.

📒 Files selected for processing (3)
  • praetorian_cli/ui/aegis/commands/job.py
  • praetorian_cli/ui/aegis/commands/job_helpers.py
  • praetorian_cli/ui/aegis/commands/schedule.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • praetorian_cli/ui/aegis/commands/schedule.py
  • praetorian_cli/ui/aegis/commands/job.py

Comment on lines +398 to +406
default = "1" if choices else "0"
choice = Prompt.ask(" Choose target", default=default).strip()

# Allow direct target input (IP, CIDR, hostname) instead of a number
try:
choice_num = int(choice)
except ValueError:
# Treat non-numeric input as a direct target
return _create_asset_target(menu, hostname, choice, colors)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Reject blank direct input before creating an asset.

After .strip(), a whitespace-only response becomes '', falls into the non-numeric branch, and calls _create_asset_target(...) with an empty identifier. Handle the blank case before int(choice) and re-prompt.

Proposed fix
                 default = "1" if choices else "0"
                 choice = Prompt.ask("  Choose target", default=default).strip()
+                if not choice:
+                    menu.console.print("  Please enter a number or target value")
+                    continue
 
                 # Allow direct target input (IP, CIDR, hostname) instead of a number
                 try:
                     choice_num = int(choice)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@praetorian_cli/ui/aegis/commands/job_helpers.py` around lines 398 - 406,
After reading the user's choice from Prompt.ask and calling .strip(), explicitly
reject empty string inputs before attempting int(choice) so we don't treat blank
input as a direct target; update the logic in the block around Prompt.ask/choice
handling in job_helpers.py (where variables menu, hostname, choice and function
_create_asset_target are used) to check if choice == "" and if so re-prompt (or
loop back to Prompt.ask) instead of falling through to the non-numeric branch
that calls _create_asset_target with an empty identifier.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant