mirror of
https://github.com/usestrix/strix.git
synced 2026-10-08 03:08:08 +00:00
fix(cloud): align agent commands with API contracts
This commit is contained in:
parent
3580412178
commit
a02c56423c
8 changed files with 151 additions and 18 deletions
|
|
@ -44,11 +44,11 @@ Target-specific workflows built on the same engine:
|
|||
strix cloud vulns list --severity critical
|
||||
strix cloud billing topup --credits 20 # buy credits when a scan returns exit code 5
|
||||
```
|
||||
- Account setup runs from the CLI too: `strix cloud workspaces list|create|use`, `strix cloud org members invite`, `strix cloud billing subscribe --plan strix_pro`, `strix cloud billing portal`, `strix cloud integrations install github`, and `strix cloud domains verify <id>`. The last four end at a person: the command prints a link or a DNS record for the user to open or add, and it never completes the payment, the installation, or the DNS change for them.
|
||||
- Account setup runs from the CLI too: `strix cloud workspaces list|create|use`, `strix cloud org members invite`, `strix cloud billing subscribe --plan strix_cloud`, `strix cloud billing portal`, `strix cloud integrations install github`, and `strix cloud domains verify <id>`. The last four end at a person: the command prints a link or a DNS record for the user to open or add, and it never completes the payment, the installation, or the DNS change for them.
|
||||
- Every REST operation has a `strix cloud <resource> <verb>` command. Run `strix cloud` to list them. Output is JSON when stdout is not a terminal (or with `--json`), and there are no prompts without a TTY. Exit codes: `0` success, `1` error, `2` usage, `4` auth or plan limit, `5` payment required. `--token` or `STRIX_API_TOKEN` overrides the stored sign-in. `--data` adds extra request fields as JSON, and accepts `@file` or `-` for standard input.
|
||||
- The REST API works directly too: https://docs.app.strix.ai (OpenAPI: https://docs.app.strix.ai/openapi.json).
|
||||
|
||||
- CLI docs index for LLMs: https://docs.strix.ai/llms.txt (full: https://docs.strix.ai/llms-full.txt).
|
||||
- CLI docs index for LLMs: https://docs.strix.ai/llms.txt (full: https://docs.strix.ai/llms-full.txt). Managed API docs for LLMs: https://docs.app.strix.ai/llms.txt.
|
||||
- Only scan targets the user is authorized to test.
|
||||
|
||||
## Contributing to this repo
|
||||
|
|
|
|||
|
|
@ -350,7 +350,7 @@ Workspaces and account setup also work from the terminal:
|
|||
strix cloud workspaces list # your workspaces
|
||||
strix cloud workspaces create --name "My Team"
|
||||
strix cloud workspaces use "My Team" # store a token for another workspace
|
||||
strix cloud billing subscribe --plan strix_pro # opens the hosted checkout page
|
||||
strix cloud billing subscribe --plan strix_cloud # opens the hosted checkout page
|
||||
strix cloud billing portal # opens the billing portal
|
||||
strix cloud integrations install github # opens the app installation page
|
||||
strix cloud domains verify <domain-id> # prints the DNS record to add
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ Use this when you want Strix's autonomous pentesting **without running Docker or
|
|||
There are two equivalent interfaces. Prefer the CLI:
|
||||
|
||||
- **`strix cloud` CLI** — every REST operation has a command in the form `strix cloud <resource> <verb>`. Install with `curl -sSL https://strix.ai/install | bash`. Run `strix cloud` to list all resources and `strix cloud <resource>` to list its verbs.
|
||||
- **REST API** — base URL `https://app.strix.ai/api/v1`, `Authorization: Bearer <token>` on every request. Full reference: **[docs.app.strix.ai](https://docs.app.strix.ai)** · OpenAPI: `https://docs.app.strix.ai/openapi.json`.
|
||||
- **REST API** — base URL `https://app.strix.ai/api/v1`, `Authorization: Bearer <token>` on every request. Full reference: **[docs.app.strix.ai](https://docs.app.strix.ai)** · agent index: `https://docs.app.strix.ai/llms.txt` · OpenAPI: `https://docs.app.strix.ai/openapi.json`.
|
||||
|
||||
The CLI is agent friendly. Output is JSON when stdout is not a terminal, or when you pass `--json`. There are no interactive prompts when stdin is not a terminal. Exit codes: `0` success, `1` error, `2` invalid usage, `4` authentication required, `5` payment required.
|
||||
|
||||
|
|
@ -38,7 +38,7 @@ strix cloud login --scopes scans:read scans:write billing:read vulnerabilities:r
|
|||
|
||||
The user approves the sign-in in the browser. With `--scopes` (and optionally `--workspace <name-or-id>`) there are no prompts, so the command works from a non-interactive agent shell. In an interactive terminal without flags, the CLI offers a workspace picker and scope presets (Recommended, Full access, Minimal, Custom).
|
||||
|
||||
- `strix cloud whoami` shows the active sign-in. `strix cloud logout` removes it.
|
||||
- `strix cloud whoami` shows the active sign-in; add `--json` when another agent will parse it. `strix cloud logout` removes it.
|
||||
- Every other `strix cloud` command uses the stored token automatically. To use a different token (for example one created in the dashboard at **Settings → API Access**), pass `--token <token>` or set `STRIX_API_TOKEN`.
|
||||
- Never hardcode, log, or commit the token. Store it in an env var or the CI secret store.
|
||||
- **Scopes (least-privilege):** assign only what the integration needs and rotate regularly:
|
||||
|
|
@ -101,7 +101,7 @@ strix cloud org members invite --email dev@example.com --role analyst
|
|||
Four steps end at the user. The command creates the link or the record and prints it. Strix opens the browser only in an interactive terminal. Pass `--no-browser` to print the URL only.
|
||||
|
||||
```bash
|
||||
strix cloud billing subscribe --plan strix_pro # hosted checkout page for a plan
|
||||
strix cloud billing subscribe --plan strix_cloud # hosted checkout page for the Cloud plan
|
||||
strix cloud billing portal # billing portal for the card and the plan
|
||||
strix cloud integrations install github # GitHub App or Slack installation page
|
||||
strix cloud domains verify <domain-id> # DNS record to add, then run it again
|
||||
|
|
@ -109,6 +109,22 @@ strix cloud domains verify <domain-id> # DNS record to add, then run i
|
|||
|
||||
Give the printed URL or DNS record to the user and wait. Do not claim that the payment, the installation, or the DNS change is complete. Confirm the result afterwards with `strix cloud credits`, `strix cloud integrations list`, or `strix cloud domains list`. All four commands need an admin token, except `domains verify`, which needs `assets:write`.
|
||||
|
||||
### Organization knowledge
|
||||
|
||||
Agents can manage the organization knowledge base without the dashboard (`knowledge:read` / `knowledge:write`):
|
||||
|
||||
```bash
|
||||
strix cloud knowledge list --search authentication
|
||||
strix cloud knowledge add --title "Authentication" --content "Staging uses SSO."
|
||||
strix cloud knowledge update <document-id> --content "Staging uses SSO and TOTP."
|
||||
strix cloud knowledge delete <document-id>
|
||||
strix cloud knowledge policies add --key staging-only --content "Never test production."
|
||||
strix cloud knowledge policies delete staging-only
|
||||
strix cloud knowledge repos entries usestrix/strix
|
||||
```
|
||||
|
||||
Knowledge policy writes require an admin token. Repository names are passed as normal `owner/name` values; the CLI handles URL encoding. The `costs` and `llm-settings` commands target on-prem installations and return `404` on app.strix.ai.
|
||||
|
||||
## 1. Register the target as an asset
|
||||
|
||||
Scans run against **registered assets**, not raw URLs. Register once, then reuse the returned UUID.
|
||||
|
|
|
|||
|
|
@ -42,7 +42,11 @@ def run_cloud(argv: list[str]) -> int:
|
|||
|
||||
group, rest = argv[0], argv[1:]
|
||||
if group in ("login", "logout", "whoami"):
|
||||
session_argv = {"login": rest, "logout": ["logout"], "whoami": ["status"]}
|
||||
session_argv = {
|
||||
"login": rest,
|
||||
"logout": ["logout"],
|
||||
"whoami": ["status", *rest],
|
||||
}
|
||||
return run_login(session_argv[group])
|
||||
if group == "credits":
|
||||
group, rest = "billing", ["credits", *rest]
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import time
|
|||
import webbrowser
|
||||
from pathlib import Path
|
||||
from typing import Any, cast
|
||||
from urllib.parse import quote
|
||||
|
||||
from rich.console import Console
|
||||
|
||||
|
|
@ -77,7 +78,8 @@ def run(group: str, verb_label: str, cmd: Cmd, argv: list[str]) -> int:
|
|||
|
||||
path = cmd.path
|
||||
for name in _PLACEHOLDER.findall(cmd.path):
|
||||
path = path.replace("{" + name + "}", str(getattr(args, _dest(name))))
|
||||
value = quote(str(getattr(args, _dest(name))), safe="")
|
||||
path = path.replace("{" + name + "}", value)
|
||||
|
||||
as_json = json_mode(flag=bool(getattr(args, "json", False)))
|
||||
token = getattr(args, "token", None)
|
||||
|
|
@ -351,7 +353,11 @@ def _topup(
|
|||
|
||||
challenge = http.parsed(response)
|
||||
if getattr(args, "no_pay", False):
|
||||
emit(console, challenge, as_json=as_json)
|
||||
emit(
|
||||
console,
|
||||
{"error": "Payment required", "challenge": challenge},
|
||||
as_json=as_json,
|
||||
)
|
||||
return http.EXIT_PAYMENT
|
||||
|
||||
npx = shutil.which("npx")
|
||||
|
|
|
|||
|
|
@ -511,7 +511,7 @@ SPEC: dict[str, dict[str, Cmd]] = {
|
|||
"product",
|
||||
required=True,
|
||||
flag="plan",
|
||||
help="Product to buy, for example strix_pro, strix_cloud, or strix_top_up.",
|
||||
help="Product to buy: strix_cloud, strix_startup, or strix_top_up.",
|
||||
),
|
||||
P("success_url", help="Page to open after the payment."),
|
||||
),
|
||||
|
|
@ -655,9 +655,11 @@ SPEC: dict[str, dict[str, Cmd]] = {
|
|||
"/knowledge/policies",
|
||||
"Add a knowledge policy.",
|
||||
body=(
|
||||
P("key", required=True, help="Policy key."),
|
||||
P("content", help="Policy content."),
|
||||
P("enabled", "bool", help="Turn the policy on or off."),
|
||||
P("policy_key", required=True, flag="key", help="Policy key."),
|
||||
P("policy_value", required=True, flag="content", help="Policy content."),
|
||||
P("policy_type", help="Policy type. Defaults to constraint."),
|
||||
P("is_active", "bool", flag="enabled", help="Turn the policy on or off."),
|
||||
P("metadata", "json", help="JSON metadata for the policy."),
|
||||
),
|
||||
),
|
||||
"policies delete": Cmd(
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ def run_login(argv: list[str]) -> int:
|
|||
console.print(_LOGIN_USAGE)
|
||||
return 0
|
||||
if subcommand == "status":
|
||||
return _status(console)
|
||||
return _status(console, argv[1:])
|
||||
if subcommand == "logout":
|
||||
return _logout(console)
|
||||
return _login(console, argv)
|
||||
|
|
@ -468,14 +468,35 @@ def _print_success(console: Console, record: dict[str, Any]) -> None:
|
|||
)
|
||||
|
||||
|
||||
def _status(console: Console) -> int:
|
||||
def _status(console: Console, argv: list[str]) -> int:
|
||||
parser = argparse.ArgumentParser(prog="strix cloud whoami")
|
||||
parser.add_argument("--json", action="store_true", help="Print the session as JSON.")
|
||||
try:
|
||||
args = parser.parse_args(argv)
|
||||
except SystemExit as exc:
|
||||
return exc.code if isinstance(exc.code, int) else 2
|
||||
|
||||
record = read_record()
|
||||
if record is None:
|
||||
if args.json:
|
||||
sys.stdout.write(json.dumps({"signed_in": False, "error": "Not signed in"}) + "\n")
|
||||
return 1
|
||||
console.print("[yellow]Not signed in.[/] Run [bold]strix cloud login[/] to sign in.")
|
||||
return 1
|
||||
email = record.get("email", "unknown")
|
||||
organization = record.get("organization_name") or record.get("organization_id", "")
|
||||
expires_at = record.get("expires_at", "")
|
||||
if args.json:
|
||||
payload = {
|
||||
"signed_in": True,
|
||||
"email": email,
|
||||
"organization_id": record.get("organization_id"),
|
||||
"organization_name": record.get("organization_name"),
|
||||
"scopes": record.get("scopes", []),
|
||||
"expires_at": expires_at or None,
|
||||
}
|
||||
sys.stdout.write(json.dumps(payload, indent=2, default=str) + "\n")
|
||||
return 0
|
||||
console.print(f"[green]Signed in[/] as [bold]{email}[/]")
|
||||
if organization:
|
||||
console.print(f" Workspace: {organization}")
|
||||
|
|
|
|||
|
|
@ -102,6 +102,21 @@ def test_placeholder_substitution(monkeypatch: pytest.MonkeyPatch, capsys: Any)
|
|||
assert json.loads(capsys.readouterr().out) == {"id": "abc"}
|
||||
|
||||
|
||||
def test_placeholder_substitution_percent_encodes_path_segments(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
seen: dict[str, Any] = {}
|
||||
|
||||
def fake_request(_method: str, path: str, **_kwargs: Any) -> FakeResponse:
|
||||
seen["path"] = path
|
||||
return FakeResponse(payload={"entries": []})
|
||||
|
||||
monkeypatch.setattr(http, "request", fake_request)
|
||||
code = cloud.run_cloud(["knowledge", "repos", "entries", "usestrix/.github", "--json"])
|
||||
assert code == 0
|
||||
assert seen["path"] == "/knowledge/repos/usestrix%2F.github/entries"
|
||||
|
||||
|
||||
def test_query_and_body_collection(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
seen: dict[str, Any] = {}
|
||||
|
||||
|
|
@ -282,6 +297,35 @@ def test_credits_alias_routes_to_billing(monkeypatch: pytest.MonkeyPatch) -> Non
|
|||
assert seen["path"] == "/billing/credits"
|
||||
|
||||
|
||||
def test_whoami_json_is_machine_readable_and_omits_the_token(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: Any
|
||||
) -> None:
|
||||
monkeypatch.delenv("STRIX_API_TOKEN", raising=False)
|
||||
monkeypatch.setattr(platform_cli, "AUTH_PATH", tmp_path / "platform-auth.json")
|
||||
platform_cli.save_record(
|
||||
{
|
||||
"api_token": "strix_pat_secret",
|
||||
"email": "agent@example.test",
|
||||
"organization_id": "org_1",
|
||||
"organization_name": "Example",
|
||||
"scopes": ["scans:read"],
|
||||
"expires_at": "2026-09-01T00:00:00Z",
|
||||
}
|
||||
)
|
||||
|
||||
assert cloud.run_cloud(["whoami", "--json"]) == 0
|
||||
payload = json.loads(capsys.readouterr().out)
|
||||
assert payload == {
|
||||
"signed_in": True,
|
||||
"email": "agent@example.test",
|
||||
"organization_id": "org_1",
|
||||
"organization_name": "Example",
|
||||
"scopes": ["scans:read"],
|
||||
"expires_at": "2026-09-01T00:00:00Z",
|
||||
}
|
||||
assert "api_token" not in payload
|
||||
|
||||
|
||||
def test_topup_no_pay_prints_challenge(monkeypatch: pytest.MonkeyPatch, capsys: Any) -> None:
|
||||
challenge = {"payment_requirements": [{"amount": 500}]}
|
||||
monkeypatch.setattr(
|
||||
|
|
@ -289,7 +333,10 @@ def test_topup_no_pay_prints_challenge(monkeypatch: pytest.MonkeyPatch, capsys:
|
|||
)
|
||||
code = cloud.run_cloud(["billing", "topup", "--credits", "5", "--no-pay", "--json"])
|
||||
assert code == http.EXIT_PAYMENT
|
||||
assert json.loads(capsys.readouterr().out) == challenge
|
||||
assert json.loads(capsys.readouterr().out) == {
|
||||
"error": "Payment required",
|
||||
"challenge": challenge,
|
||||
}
|
||||
|
||||
|
||||
def test_topup_success_without_payment(monkeypatch: pytest.MonkeyPatch, capsys: Any) -> None:
|
||||
|
|
@ -393,14 +440,51 @@ def test_billing_subscribe_prints_checkout_url(
|
|||
return FakeResponse(status_code=200, payload={"checkout_url": "https://pay.test/session"})
|
||||
|
||||
monkeypatch.setattr(http, "request", fake_request)
|
||||
code = cloud.run_cloud(["billing", "subscribe", "--plan", "strix_pro", "--json"])
|
||||
code = cloud.run_cloud(["billing", "subscribe", "--plan", "strix_cloud", "--json"])
|
||||
assert code == 0
|
||||
assert seen["method"] == "POST"
|
||||
assert seen["path"] == "/billing/checkout"
|
||||
assert seen["body"] == {"product": "strix_pro"}
|
||||
assert seen["body"] == {"product": "strix_cloud"}
|
||||
assert "https://pay.test/session" in capsys.readouterr().out
|
||||
|
||||
|
||||
def test_knowledge_policy_flags_use_the_api_field_names(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
seen: dict[str, Any] = {}
|
||||
|
||||
def fake_request(_method: str, _path: str, **kwargs: Any) -> FakeResponse:
|
||||
seen["body"] = kwargs.get("body")
|
||||
return FakeResponse(payload={"success": True})
|
||||
|
||||
monkeypatch.setattr(http, "request", fake_request)
|
||||
code = cloud.run_cloud(
|
||||
[
|
||||
"knowledge",
|
||||
"policies",
|
||||
"add",
|
||||
"--key",
|
||||
"no-production-data",
|
||||
"--content",
|
||||
"Never test production data.",
|
||||
"--policy-type",
|
||||
"constraint",
|
||||
"--no-enabled",
|
||||
"--metadata",
|
||||
'{"owner":"security"}',
|
||||
"--json",
|
||||
]
|
||||
)
|
||||
assert code == 0
|
||||
assert seen["body"] == {
|
||||
"policy_key": "no-production-data",
|
||||
"policy_value": "Never test production data.",
|
||||
"policy_type": "constraint",
|
||||
"is_active": False,
|
||||
"metadata": {"owner": "security"},
|
||||
}
|
||||
|
||||
|
||||
def test_integration_install_url_does_not_open_browser(
|
||||
monkeypatch: pytest.MonkeyPatch, capsys: Any
|
||||
) -> None:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue