refactorai-cli 0.2.26__tar.gz → 0.2.28__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {refactorai_cli-0.2.26 → refactorai_cli-0.2.28}/PKG-INFO +35 -2
- refactorai_cli-0.2.28/README.md +80 -0
- {refactorai_cli-0.2.26 → refactorai_cli-0.2.28}/pyproject.toml +1 -1
- {refactorai_cli-0.2.26 → refactorai_cli-0.2.28}/refactorai_cli/__init__.py +1 -1
- refactorai_cli-0.2.28/refactorai_cli/commands/cloud_cmds.py +176 -0
- {refactorai_cli-0.2.26 → refactorai_cli-0.2.28}/refactorai_cli/commands/run_cmds.py +231 -7
- refactorai_cli-0.2.28/refactorai_cli/commands/runtime_proxy_cmds.py +224 -0
- {refactorai_cli-0.2.26 → refactorai_cli-0.2.28}/refactorai_cli/commands/setup_cmds.py +15 -0
- {refactorai_cli-0.2.26 → refactorai_cli-0.2.28}/refactorai_cli/main.py +12 -1
- {refactorai_cli-0.2.26 → refactorai_cli-0.2.28}/refactorai_cli/setup_flow.py +90 -9
- {refactorai_cli-0.2.26 → refactorai_cli-0.2.28}/refactorai_cli.egg-info/PKG-INFO +35 -2
- {refactorai_cli-0.2.26 → refactorai_cli-0.2.28}/refactorai_cli.egg-info/SOURCES.txt +1 -0
- refactorai_cli-0.2.26/README.md +0 -47
- refactorai_cli-0.2.26/refactorai_cli/commands/runtime_proxy_cmds.py +0 -143
- {refactorai_cli-0.2.26 → refactorai_cli-0.2.28}/refactorai_cli/auth.py +0 -0
- {refactorai_cli-0.2.26 → refactorai_cli-0.2.28}/refactorai_cli/client.py +0 -0
- {refactorai_cli-0.2.26 → refactorai_cli-0.2.28}/refactorai_cli/commands/__init__.py +0 -0
- {refactorai_cli-0.2.26 → refactorai_cli-0.2.28}/refactorai_cli/commands/auth_cmds.py +0 -0
- {refactorai_cli-0.2.26 → refactorai_cli-0.2.28}/refactorai_cli/commands/engine_cmds.py +0 -0
- {refactorai_cli-0.2.26 → refactorai_cli-0.2.28}/refactorai_cli/commands/model_cmds.py +0 -0
- {refactorai_cli-0.2.26 → refactorai_cli-0.2.28}/refactorai_cli/commands/rules_cmds.py +0 -0
- {refactorai_cli-0.2.26 → refactorai_cli-0.2.28}/refactorai_cli/commands/runtime_cmds.py +0 -0
- {refactorai_cli-0.2.26 → refactorai_cli-0.2.28}/refactorai_cli/control_plane.py +0 -0
- {refactorai_cli-0.2.26 → refactorai_cli-0.2.28}/refactorai_cli/credentials.py +0 -0
- {refactorai_cli-0.2.26 → refactorai_cli-0.2.28}/refactorai_cli/local_constitution.py +0 -0
- {refactorai_cli-0.2.26 → refactorai_cli-0.2.28}/refactorai_cli/local_engine_runtime.py +0 -0
- {refactorai_cli-0.2.26 → refactorai_cli-0.2.28}/refactorai_cli/local_paths.py +0 -0
- {refactorai_cli-0.2.26 → refactorai_cli-0.2.28}/refactorai_cli/model_policy.py +0 -0
- {refactorai_cli-0.2.26 → refactorai_cli-0.2.28}/refactorai_cli/runtime_manager.py +0 -0
- {refactorai_cli-0.2.26 → refactorai_cli-0.2.28}/refactorai_cli/settings.py +0 -0
- {refactorai_cli-0.2.26 → refactorai_cli-0.2.28}/refactorai_cli.egg-info/dependency_links.txt +0 -0
- {refactorai_cli-0.2.26 → refactorai_cli-0.2.28}/refactorai_cli.egg-info/entry_points.txt +0 -0
- {refactorai_cli-0.2.26 → refactorai_cli-0.2.28}/refactorai_cli.egg-info/requires.txt +0 -0
- {refactorai_cli-0.2.26 → refactorai_cli-0.2.28}/refactorai_cli.egg-info/top_level.txt +0 -0
- {refactorai_cli-0.2.26 → refactorai_cli-0.2.28}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: refactorai-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.28
|
|
4
4
|
Summary: Local-first CLI for the refactor platform
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -53,6 +53,39 @@ python -m twine upload ./refactorai-cli/dist/*
|
|
|
53
53
|
## Install test (local)
|
|
54
54
|
|
|
55
55
|
```bash
|
|
56
|
-
python -m pip install ./refactorai-cli/dist/refactorai_cli-0.2.
|
|
56
|
+
python -m pip install ./refactorai-cli/dist/refactorai_cli-0.2.28-py3-none-any.whl
|
|
57
57
|
refactor --version
|
|
58
58
|
```
|
|
59
|
+
|
|
60
|
+
## Cloud BYOK setup (bring your own provider key)
|
|
61
|
+
|
|
62
|
+
Run cloud inference with your own provider credentials (no local runtime
|
|
63
|
+
required). The secret is read from your environment and encrypted server-side;
|
|
64
|
+
it is never written to disk or printed.
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# 1. Authenticate (developer key with the run:byok entitlement).
|
|
68
|
+
refactor login
|
|
69
|
+
|
|
70
|
+
# 2. Register your provider secret from an env var.
|
|
71
|
+
export OPENAI_API_KEY=sk-...
|
|
72
|
+
refactor cloud credentials set --provider openai --from-env OPENAI_API_KEY --label "my key"
|
|
73
|
+
# -> prints a credential_ref, e.g. cred_01J...
|
|
74
|
+
|
|
75
|
+
# 3. Point your project at cloud_byok in refactor.config:
|
|
76
|
+
# mode: cloud_byok
|
|
77
|
+
# provider: openai
|
|
78
|
+
# credential_ref: cred_01J...
|
|
79
|
+
|
|
80
|
+
# 4. Verify readiness, then run.
|
|
81
|
+
refactor doctor
|
|
82
|
+
refactor review .
|
|
83
|
+
refactor code . --apply
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Manage credentials:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
refactor cloud credentials list
|
|
90
|
+
refactor cloud credentials remove --credential-ref cred_01J...
|
|
91
|
+
```
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# refactorai-cli
|
|
2
|
+
|
|
3
|
+
Public CLI package for Refactor.
|
|
4
|
+
|
|
5
|
+
- PyPI package name: `refactorai-cli`
|
|
6
|
+
- Installed command: `refactor`
|
|
7
|
+
- Python module package: `refactorai_cli`
|
|
8
|
+
|
|
9
|
+
By default, the CLI targets `https://api.refactorai.codes`.
|
|
10
|
+
Use `REFACTOR_PLATFORM_URL` only when you need to override the control-plane URL
|
|
11
|
+
(for self-hosted or local development environments).
|
|
12
|
+
|
|
13
|
+
## Local development install
|
|
14
|
+
|
|
15
|
+
From repository root:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install -e refactor_core -e refactorai-cli
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Build
|
|
22
|
+
|
|
23
|
+
From repository root:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
python -m pip install --upgrade build twine
|
|
27
|
+
python -m build "./refactorai-cli"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Artifacts are created in:
|
|
31
|
+
|
|
32
|
+
- `refactorai-cli/dist/*.whl`
|
|
33
|
+
- `refactorai-cli/dist/*.tar.gz`
|
|
34
|
+
|
|
35
|
+
## Publish
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
python -m twine check ./refactorai-cli/dist/*
|
|
39
|
+
python -m twine upload ./refactorai-cli/dist/*
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Install test (local)
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
python -m pip install ./refactorai-cli/dist/refactorai_cli-0.2.28-py3-none-any.whl
|
|
46
|
+
refactor --version
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Cloud BYOK setup (bring your own provider key)
|
|
50
|
+
|
|
51
|
+
Run cloud inference with your own provider credentials (no local runtime
|
|
52
|
+
required). The secret is read from your environment and encrypted server-side;
|
|
53
|
+
it is never written to disk or printed.
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
# 1. Authenticate (developer key with the run:byok entitlement).
|
|
57
|
+
refactor login
|
|
58
|
+
|
|
59
|
+
# 2. Register your provider secret from an env var.
|
|
60
|
+
export OPENAI_API_KEY=sk-...
|
|
61
|
+
refactor cloud credentials set --provider openai --from-env OPENAI_API_KEY --label "my key"
|
|
62
|
+
# -> prints a credential_ref, e.g. cred_01J...
|
|
63
|
+
|
|
64
|
+
# 3. Point your project at cloud_byok in refactor.config:
|
|
65
|
+
# mode: cloud_byok
|
|
66
|
+
# provider: openai
|
|
67
|
+
# credential_ref: cred_01J...
|
|
68
|
+
|
|
69
|
+
# 4. Verify readiness, then run.
|
|
70
|
+
refactor doctor
|
|
71
|
+
refactor review .
|
|
72
|
+
refactor code . --apply
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Manage credentials:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
refactor cloud credentials list
|
|
79
|
+
refactor cloud credentials remove --credential-ref cred_01J...
|
|
80
|
+
```
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""`refactor cloud` command group for BYOK cloud credential management (R20).
|
|
2
|
+
|
|
3
|
+
Lets developers self-serve register/list/remove provider credentials used by
|
|
4
|
+
``cloud_byok`` inference without hand-crafting API calls. Secrets are read from
|
|
5
|
+
the environment (never passed on the command line) and are envelope-encrypted
|
|
6
|
+
server-side; responses are always masked.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
|
|
13
|
+
import httpx
|
|
14
|
+
import typer
|
|
15
|
+
from rich.console import Console
|
|
16
|
+
from rich.table import Table
|
|
17
|
+
|
|
18
|
+
from refactorai_cli.auth import AuthError, ensure_authenticated
|
|
19
|
+
from refactorai_cli.settings import platform_url
|
|
20
|
+
|
|
21
|
+
console = Console()
|
|
22
|
+
|
|
23
|
+
app = typer.Typer(
|
|
24
|
+
name="cloud",
|
|
25
|
+
help="Cloud (BYOK) credential management and helpers.",
|
|
26
|
+
no_args_is_help=True,
|
|
27
|
+
add_completion=False,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
credentials_app = typer.Typer(
|
|
31
|
+
name="credentials",
|
|
32
|
+
help="Manage provider credentials for cloud_byok inference.",
|
|
33
|
+
no_args_is_help=True,
|
|
34
|
+
add_completion=False,
|
|
35
|
+
)
|
|
36
|
+
app.add_typer(credentials_app, name="credentials")
|
|
37
|
+
|
|
38
|
+
_CREDENTIALS_PATH = "/v1/inference/credentials"
|
|
39
|
+
_TIMEOUT = 20.0
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _auth_header() -> tuple[str, str]:
|
|
43
|
+
"""Return ``(base_url, bearer_key)`` or exit with a clear auth error."""
|
|
44
|
+
try:
|
|
45
|
+
auth_ctx = ensure_authenticated()
|
|
46
|
+
except AuthError as exc:
|
|
47
|
+
console.print(f"[red]Not authenticated:[/red] {exc}")
|
|
48
|
+
raise typer.Exit(code=1) from exc
|
|
49
|
+
return platform_url(), auth_ctx.key.key
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _handle_error_status(response: httpx.Response) -> None:
|
|
53
|
+
"""Map credential API error statuses to actionable CLI messages."""
|
|
54
|
+
if response.status_code < 400:
|
|
55
|
+
return
|
|
56
|
+
if response.status_code == 401:
|
|
57
|
+
console.print("[red]Unauthorized[/red]: developer key invalid or revoked. Run `refactor login`.")
|
|
58
|
+
elif response.status_code == 402:
|
|
59
|
+
console.print(
|
|
60
|
+
"[red]BYOK not enabled[/red] for this account (missing `run:byok`). "
|
|
61
|
+
"Contact your admin to enable BYOK cloud execution."
|
|
62
|
+
)
|
|
63
|
+
elif response.status_code == 404:
|
|
64
|
+
console.print("[red]Not found[/red]: credential does not exist or is not owned by this account.")
|
|
65
|
+
else:
|
|
66
|
+
detail = ""
|
|
67
|
+
try:
|
|
68
|
+
detail = str((response.json() or {}).get("detail", ""))
|
|
69
|
+
except Exception:
|
|
70
|
+
detail = response.text[:200]
|
|
71
|
+
console.print(f"[red]Request failed[/red] ({response.status_code}): {detail}")
|
|
72
|
+
raise typer.Exit(code=1)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@credentials_app.command("set")
|
|
76
|
+
def credentials_set(
|
|
77
|
+
provider: str = typer.Option(..., "--provider", help="Provider name, e.g. openai, anthropic."),
|
|
78
|
+
from_env: str = typer.Option(
|
|
79
|
+
...,
|
|
80
|
+
"--from-env",
|
|
81
|
+
help="Environment variable holding the provider secret (value is read from your shell).",
|
|
82
|
+
),
|
|
83
|
+
label: str = typer.Option("", "--label", help="Optional human-readable label for this credential."),
|
|
84
|
+
) -> None:
|
|
85
|
+
"""Register (or replace) a provider credential from an environment variable.
|
|
86
|
+
|
|
87
|
+
The secret value is read from ``--from-env`` and sent over TLS to be
|
|
88
|
+
encrypted server-side. It is never written to disk or shown on screen.
|
|
89
|
+
"""
|
|
90
|
+
secret_value = os.environ.get(from_env, "").strip()
|
|
91
|
+
if not secret_value:
|
|
92
|
+
console.print(
|
|
93
|
+
f"[red]Environment variable ${{{from_env}}} is empty or unset.[/red] "
|
|
94
|
+
"Export your provider secret first, e.g. "
|
|
95
|
+
f"`export {from_env}=sk-...` then re-run."
|
|
96
|
+
)
|
|
97
|
+
raise typer.Exit(code=1)
|
|
98
|
+
|
|
99
|
+
base_url, key = _auth_header()
|
|
100
|
+
try:
|
|
101
|
+
response = httpx.post(
|
|
102
|
+
f"{base_url}{_CREDENTIALS_PATH}",
|
|
103
|
+
headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"},
|
|
104
|
+
json={"provider": provider, "secret_value": secret_value, "credential_label": label},
|
|
105
|
+
timeout=_TIMEOUT,
|
|
106
|
+
)
|
|
107
|
+
except httpx.HTTPError as exc:
|
|
108
|
+
console.print(f"[red]Could not reach platform:[/red] {exc}")
|
|
109
|
+
raise typer.Exit(code=1) from exc
|
|
110
|
+
|
|
111
|
+
_handle_error_status(response)
|
|
112
|
+
data = response.json() if response.content else {}
|
|
113
|
+
console.print(
|
|
114
|
+
f"[green]Credential registered[/green] for provider [bold]{data.get('provider', provider)}[/bold].\n"
|
|
115
|
+
f" credential_ref: [bold]{data.get('credential_ref', '?')}[/bold]\n"
|
|
116
|
+
f" masked_secret: {data.get('masked_secret', '****')}\n"
|
|
117
|
+
"Add the credential_ref to refactor.config under `credential_ref:` to use cloud_byok."
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@credentials_app.command("list")
|
|
122
|
+
def credentials_list() -> None:
|
|
123
|
+
"""List the developer's active BYOK provider credentials (masked)."""
|
|
124
|
+
base_url, key = _auth_header()
|
|
125
|
+
try:
|
|
126
|
+
response = httpx.get(
|
|
127
|
+
f"{base_url}{_CREDENTIALS_PATH}",
|
|
128
|
+
headers={"Authorization": f"Bearer {key}"},
|
|
129
|
+
timeout=_TIMEOUT,
|
|
130
|
+
)
|
|
131
|
+
except httpx.HTTPError as exc:
|
|
132
|
+
console.print(f"[red]Could not reach platform:[/red] {exc}")
|
|
133
|
+
raise typer.Exit(code=1) from exc
|
|
134
|
+
|
|
135
|
+
_handle_error_status(response)
|
|
136
|
+
data = response.json() if response.content else {}
|
|
137
|
+
rows = data.get("credentials", []) if isinstance(data, dict) else []
|
|
138
|
+
if not rows:
|
|
139
|
+
console.print("[dim]No cloud credentials registered.[/dim] Use `refactor cloud credentials set`.")
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
table = Table(title="Cloud BYOK credentials")
|
|
143
|
+
table.add_column("provider")
|
|
144
|
+
table.add_column("credential_ref")
|
|
145
|
+
table.add_column("label")
|
|
146
|
+
table.add_column("secret")
|
|
147
|
+
table.add_column("created")
|
|
148
|
+
for row in rows:
|
|
149
|
+
table.add_row(
|
|
150
|
+
str(row.get("provider", "")),
|
|
151
|
+
str(row.get("credential_ref", "")),
|
|
152
|
+
str(row.get("credential_label", "") or ""),
|
|
153
|
+
str(row.get("masked_secret", "****")),
|
|
154
|
+
str(row.get("created_at", "") or ""),
|
|
155
|
+
)
|
|
156
|
+
console.print(table)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@credentials_app.command("remove")
|
|
160
|
+
def credentials_remove(
|
|
161
|
+
credential_ref: str = typer.Option(..., "--credential-ref", help="Credential reference to deactivate."),
|
|
162
|
+
) -> None:
|
|
163
|
+
"""Deactivate (remove) a BYOK provider credential by reference."""
|
|
164
|
+
base_url, key = _auth_header()
|
|
165
|
+
try:
|
|
166
|
+
response = httpx.delete(
|
|
167
|
+
f"{base_url}{_CREDENTIALS_PATH}/{credential_ref}",
|
|
168
|
+
headers={"Authorization": f"Bearer {key}"},
|
|
169
|
+
timeout=_TIMEOUT,
|
|
170
|
+
)
|
|
171
|
+
except httpx.HTTPError as exc:
|
|
172
|
+
console.print(f"[red]Could not reach platform:[/red] {exc}")
|
|
173
|
+
raise typer.Exit(code=1) from exc
|
|
174
|
+
|
|
175
|
+
_handle_error_status(response)
|
|
176
|
+
console.print(f"[green]Removed[/green] credential [bold]{credential_ref}[/bold].")
|
|
@@ -7,6 +7,7 @@ generation and application.
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
+
import hashlib
|
|
10
11
|
import os
|
|
11
12
|
import json
|
|
12
13
|
import re
|
|
@@ -578,8 +579,40 @@ def _require_auth() -> None:
|
|
|
578
579
|
raise typer.Exit(code=1) from exc
|
|
579
580
|
|
|
580
581
|
|
|
582
|
+
def _cloud_execution_mode(constitution) -> str:
|
|
583
|
+
"""Return the cloud execution mode for the project, or "" for local mode.
|
|
584
|
+
|
|
585
|
+
Recognizes both cloud modes (R20):
|
|
586
|
+
- ``cloud_managed``: platform-managed provider keys.
|
|
587
|
+
- ``cloud_byok``: developer-supplied provider credential (via credential_ref).
|
|
588
|
+
"""
|
|
589
|
+
mode = str(constitution.get_setting("mode", "local") or "local").strip().lower()
|
|
590
|
+
return mode if mode in {"cloud_managed", "cloud_byok"} else ""
|
|
591
|
+
|
|
592
|
+
|
|
581
593
|
def _is_cloud_mode(constitution) -> bool:
|
|
582
|
-
return
|
|
594
|
+
return bool(_cloud_execution_mode(constitution))
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
def _stable_idempotency_key(
|
|
598
|
+
*,
|
|
599
|
+
account_id: str,
|
|
600
|
+
stage: str,
|
|
601
|
+
constitution_hash: str,
|
|
602
|
+
target: str,
|
|
603
|
+
max_alerts: int,
|
|
604
|
+
execution_mode: str,
|
|
605
|
+
) -> str:
|
|
606
|
+
"""Deterministic idempotency key so retries replay instead of re-charging (C10).
|
|
607
|
+
|
|
608
|
+
The key MUST change when inputs change (target/constitution/stage/mode) so
|
|
609
|
+
distinct runs are not incorrectly deduplicated, and MUST be stable across
|
|
610
|
+
retries of the same logical run so managed quota is not double-charged.
|
|
611
|
+
"""
|
|
612
|
+
seed = "|".join(
|
|
613
|
+
[account_id, execution_mode, stage, constitution_hash, str(target), str(int(max_alerts))]
|
|
614
|
+
)
|
|
615
|
+
return hashlib.sha256(seed.encode("utf-8")).hexdigest()
|
|
583
616
|
|
|
584
617
|
|
|
585
618
|
def _cloud_inference(
|
|
@@ -589,6 +622,7 @@ def _cloud_inference(
|
|
|
589
622
|
constitution,
|
|
590
623
|
target: str,
|
|
591
624
|
model_hint: str | None = None,
|
|
625
|
+
findings: list | None = None,
|
|
592
626
|
) -> RunArtifact:
|
|
593
627
|
max_alerts = int(constitution.get_setting("max_alerts_per_run", 5) or 5)
|
|
594
628
|
gate_mode = str(constitution.get_setting("gate_mode", "strict") or "strict")
|
|
@@ -600,35 +634,72 @@ def _cloud_inference(
|
|
|
600
634
|
if stage == "review":
|
|
601
635
|
system_prompt, user_prompt = build_review_prompt(constitution, documents, max_alerts)
|
|
602
636
|
else:
|
|
603
|
-
|
|
637
|
+
# Feed prior review findings into the refactor prompt so cloud `code`
|
|
638
|
+
# generates patches targeting the same issues (parity with local RR).
|
|
639
|
+
system_prompt, user_prompt = build_refactor_prompt(
|
|
640
|
+
constitution, documents, list(findings or []), max_alerts
|
|
641
|
+
)
|
|
604
642
|
|
|
643
|
+
execution_mode = _cloud_execution_mode(constitution) or "cloud_managed"
|
|
605
644
|
auth_ctx = ensure_authenticated(project_root)
|
|
606
645
|
payload = {
|
|
607
646
|
"stage": stage,
|
|
647
|
+
"execution_mode": execution_mode,
|
|
608
648
|
"constitution_hash": constitution.content_hash,
|
|
609
649
|
"system_prompt": system_prompt,
|
|
610
650
|
"user_prompt": user_prompt,
|
|
611
651
|
"max_alerts": max_alerts,
|
|
612
652
|
"model_hint": model_hint or str(constitution.get_setting("model_id", "managed-default") or "managed-default"),
|
|
613
653
|
}
|
|
654
|
+
|
|
655
|
+
if execution_mode == "cloud_byok":
|
|
656
|
+
credential_ref = str(constitution.get_setting("credential_ref", "") or "").strip()
|
|
657
|
+
provider_name = str(constitution.get_setting("provider", "") or "").strip()
|
|
658
|
+
if not credential_ref:
|
|
659
|
+
raise RuntimeError(
|
|
660
|
+
"cloud_byok requires a `credential_ref` in refactor.config. "
|
|
661
|
+
"Register one with `refactor cloud credentials set` and copy its ref."
|
|
662
|
+
)
|
|
663
|
+
payload["credential_ref"] = credential_ref
|
|
664
|
+
if provider_name and provider_name.lower() != "cloud":
|
|
665
|
+
payload["provider"] = provider_name
|
|
666
|
+
|
|
667
|
+
idempotency_key = _stable_idempotency_key(
|
|
668
|
+
account_id=auth_ctx.account_id,
|
|
669
|
+
stage=stage,
|
|
670
|
+
constitution_hash=constitution.content_hash,
|
|
671
|
+
target=target,
|
|
672
|
+
max_alerts=max_alerts,
|
|
673
|
+
execution_mode=execution_mode,
|
|
674
|
+
)
|
|
614
675
|
try:
|
|
615
676
|
response = httpx.post(
|
|
616
677
|
f"{platform_url()}/v1/inference/refactor",
|
|
617
678
|
headers={
|
|
618
679
|
"Authorization": f"Bearer {auth_ctx.key.key}",
|
|
619
680
|
"Content-Type": "application/json",
|
|
620
|
-
"Idempotency-Key":
|
|
681
|
+
"Idempotency-Key": idempotency_key,
|
|
621
682
|
},
|
|
622
683
|
json=payload,
|
|
623
684
|
timeout=45.0,
|
|
624
685
|
)
|
|
625
686
|
except httpx.HTTPError as exc:
|
|
626
|
-
raise RuntimeError(f"Could not reach
|
|
687
|
+
raise RuntimeError(f"Could not reach cloud inference service: {exc}") from exc
|
|
627
688
|
|
|
628
689
|
if response.status_code == 401:
|
|
629
690
|
raise RuntimeError("Cloud inference unauthorized (developer key invalid or revoked).")
|
|
630
691
|
if response.status_code == 402:
|
|
692
|
+
if execution_mode == "cloud_byok":
|
|
693
|
+
raise RuntimeError(
|
|
694
|
+
"Cloud BYOK is not enabled for this account (missing `run:byok`). "
|
|
695
|
+
"Contact your admin to enable BYOK cloud execution."
|
|
696
|
+
)
|
|
631
697
|
raise RuntimeError("Cloud inference requires paid tier (`mode: cloud_managed`).")
|
|
698
|
+
if response.status_code == 404 and execution_mode == "cloud_byok":
|
|
699
|
+
raise RuntimeError(
|
|
700
|
+
"BYOK credential not found or not owned by this account. "
|
|
701
|
+
"Check `refactor cloud credentials list` and your `credential_ref`."
|
|
702
|
+
)
|
|
632
703
|
if response.status_code == 429:
|
|
633
704
|
raise RuntimeError("Cloud inference quota exhausted or rate-limited.")
|
|
634
705
|
if response.status_code >= 400:
|
|
@@ -1926,6 +1997,10 @@ def code(
|
|
|
1926
1997
|
constitution, project_root=project_root, provider_name=provider
|
|
1927
1998
|
)
|
|
1928
1999
|
|
|
2000
|
+
# Cloud refactor generation reuses the review findings as prompt context;
|
|
2001
|
+
# track them so they flow into the cloud refactor stage below (C2).
|
|
2002
|
+
cloud_review_findings: list[Finding] = []
|
|
2003
|
+
|
|
1929
2004
|
# If no pending refactor requests exist, run review first to create them.
|
|
1930
2005
|
requests = pending_requests(project_root)
|
|
1931
2006
|
if not requests:
|
|
@@ -1938,6 +2013,7 @@ def code(
|
|
|
1938
2013
|
target=target,
|
|
1939
2014
|
model_hint=provider,
|
|
1940
2015
|
)
|
|
2016
|
+
cloud_review_findings = list(review_artifact.findings or [])
|
|
1941
2017
|
else:
|
|
1942
2018
|
with console.status("[cyan]review[/cyan] analyzing...", spinner="dots") as status:
|
|
1943
2019
|
review_progress = _agent_progress_logger(
|
|
@@ -2006,11 +2082,60 @@ def code(
|
|
|
2006
2082
|
return
|
|
2007
2083
|
|
|
2008
2084
|
if cloud:
|
|
2085
|
+
# Cloud modes generate patches via the platform inference API rather
|
|
2086
|
+
# than the local RR interactive loop. Reuse prior review findings as
|
|
2087
|
+
# prompt context; fall back to the latest stored review findings when
|
|
2088
|
+
# requests were carried over from an earlier run.
|
|
2089
|
+
if not cloud_review_findings:
|
|
2090
|
+
existing_dir = head_run_dir(project_root)
|
|
2091
|
+
if existing_dir is not None:
|
|
2092
|
+
try:
|
|
2093
|
+
cloud_review_findings = [
|
|
2094
|
+
Finding.model_validate(item) for item in (read_findings(existing_dir) or [])
|
|
2095
|
+
]
|
|
2096
|
+
except Exception:
|
|
2097
|
+
cloud_review_findings = []
|
|
2098
|
+
with console.status("[cyan]code[/cyan] generating patches (cloud)...", spinner="dots"):
|
|
2099
|
+
artifact = _cloud_inference(
|
|
2100
|
+
stage="refactor",
|
|
2101
|
+
project_root=project_root,
|
|
2102
|
+
constitution=constitution,
|
|
2103
|
+
target=target,
|
|
2104
|
+
model_hint=provider,
|
|
2105
|
+
findings=cloud_review_findings,
|
|
2106
|
+
)
|
|
2107
|
+
cloud_store = create_run(project_root, run_id=artifact.run_id)
|
|
2108
|
+
cloud_store.write_artifact(artifact, target=target, status="completed")
|
|
2109
|
+
|
|
2110
|
+
_print_rr_summary(artifact)
|
|
2111
|
+
_print_patches(artifact.patches)
|
|
2112
|
+
gate_color = "green" if artifact.gate.status == "passed" else "red"
|
|
2009
2113
|
console.print(
|
|
2010
|
-
f"[
|
|
2011
|
-
"
|
|
2012
|
-
"
|
|
2114
|
+
f"\n[bold]result[/bold]: [{gate_color}]{artifact.gate.status}[/{gate_color}] "
|
|
2115
|
+
f"[dim]model={artifact.model_id} mode={_cloud_execution_mode(constitution)} "
|
|
2116
|
+
f"run={artifact.run_id}[/dim]"
|
|
2013
2117
|
)
|
|
2118
|
+
|
|
2119
|
+
if apply:
|
|
2120
|
+
gate_mode = str(constitution.get_setting("gate_mode", "strict") or "strict")
|
|
2121
|
+
diffs = [p.diff_text for p in (artifact.patches or [])]
|
|
2122
|
+
if not diffs:
|
|
2123
|
+
console.print("[dim]No patches to apply.[/dim]")
|
|
2124
|
+
else:
|
|
2125
|
+
_apply_diffs(
|
|
2126
|
+
project_root,
|
|
2127
|
+
cloud_store.run_dir,
|
|
2128
|
+
diffs,
|
|
2129
|
+
artifact.gate.status,
|
|
2130
|
+
gate_mode,
|
|
2131
|
+
force,
|
|
2132
|
+
project_config,
|
|
2133
|
+
)
|
|
2134
|
+
elif artifact.patches:
|
|
2135
|
+
console.print(
|
|
2136
|
+
"[dim]Review patches with `refactor diff`, then apply with "
|
|
2137
|
+
"`refactor code --apply` or `refactor apply`.[/dim]"
|
|
2138
|
+
)
|
|
2014
2139
|
return
|
|
2015
2140
|
|
|
2016
2141
|
with console.status("[cyan]code[/cyan] running...", spinner="dots") as status:
|
|
@@ -2030,6 +2155,9 @@ def code(
|
|
|
2030
2155
|
on_rr_failure=on_rr_failure,
|
|
2031
2156
|
equivalence_mode=equivalence,
|
|
2032
2157
|
)
|
|
2158
|
+
except typer.Exit:
|
|
2159
|
+
# Intentional exits (e.g. gate-blocked apply) keep their own code/message.
|
|
2160
|
+
raise
|
|
2033
2161
|
except Exception as exc:
|
|
2034
2162
|
console.print(f"[red]Refactor failed:[/red] {_sanitize_runtime_error_message(exc)}")
|
|
2035
2163
|
raise typer.Exit(code=1) from exc
|
|
@@ -2912,6 +3040,92 @@ def _missing_requirements(provider_name: str) -> list[str]:
|
|
|
2912
3040
|
return missing
|
|
2913
3041
|
|
|
2914
3042
|
|
|
3043
|
+
def _cloud_doctor(
|
|
3044
|
+
*,
|
|
3045
|
+
project_root: Path,
|
|
3046
|
+
constitution,
|
|
3047
|
+
execution_mode: str,
|
|
3048
|
+
skip_ping: bool,
|
|
3049
|
+
) -> None:
|
|
3050
|
+
"""Cloud-mode readiness checks (C7): auth, entitlement, endpoint, credential.
|
|
3051
|
+
|
|
3052
|
+
Cloud modes don't require a local provider or runtime, so this path checks
|
|
3053
|
+
the control-plane identity and inference endpoint reachability instead of
|
|
3054
|
+
resolving/pinging a local provider.
|
|
3055
|
+
"""
|
|
3056
|
+
console.print(f"[bold]project[/bold]: {project_root}")
|
|
3057
|
+
console.print(f"[bold]mode[/bold]: {execution_mode}")
|
|
3058
|
+
|
|
3059
|
+
try:
|
|
3060
|
+
auth_ctx = ensure_authenticated(project_root)
|
|
3061
|
+
cache_note = " (cached)" if auth_ctx.cached else ""
|
|
3062
|
+
console.print(f"[green]developer key[/green]: OK account={auth_ctx.account_id}{cache_note}")
|
|
3063
|
+
except AuthError as exc:
|
|
3064
|
+
console.print(f"[red]developer key[/red]: {exc}")
|
|
3065
|
+
console.print("[dim]Run `refactor login` or set REFACTOR_API_KEY.[/dim]")
|
|
3066
|
+
raise typer.Exit(code=1) from exc
|
|
3067
|
+
|
|
3068
|
+
# BYOK requires a credential_ref in config plus a matching server-side
|
|
3069
|
+
# credential owned by this account.
|
|
3070
|
+
credential_ref = ""
|
|
3071
|
+
if execution_mode == "cloud_byok":
|
|
3072
|
+
credential_ref = str(constitution.get_setting("credential_ref", "") or "").strip()
|
|
3073
|
+
if not credential_ref:
|
|
3074
|
+
console.print(
|
|
3075
|
+
"[red]cloud_byok[/red]: no `credential_ref` in refactor.config. "
|
|
3076
|
+
"Register one with `refactor cloud credentials set` and add its ref."
|
|
3077
|
+
)
|
|
3078
|
+
raise typer.Exit(code=1)
|
|
3079
|
+
console.print(f"[green]credential_ref[/green]: {credential_ref}")
|
|
3080
|
+
provider_name = str(constitution.get_setting("provider", "") or "").strip()
|
|
3081
|
+
if provider_name and provider_name.lower() != "cloud":
|
|
3082
|
+
console.print(f"[bold]provider[/bold]: {provider_name}")
|
|
3083
|
+
|
|
3084
|
+
if skip_ping:
|
|
3085
|
+
console.print("[dim]Skipping cloud endpoint reachability check (--skip-ping).[/dim]")
|
|
3086
|
+
return
|
|
3087
|
+
|
|
3088
|
+
# Verify the BYOK credential exists and is owned by this account, and that
|
|
3089
|
+
# the credential endpoint is reachable.
|
|
3090
|
+
if execution_mode == "cloud_byok":
|
|
3091
|
+
try:
|
|
3092
|
+
response = httpx.get(
|
|
3093
|
+
f"{platform_url()}/v1/inference/credentials",
|
|
3094
|
+
headers={"Authorization": f"Bearer {auth_ctx.key.key}"},
|
|
3095
|
+
timeout=20.0,
|
|
3096
|
+
)
|
|
3097
|
+
except httpx.HTTPError as exc:
|
|
3098
|
+
console.print(f"[red]cloud endpoint[/red]: unreachable ({exc}).")
|
|
3099
|
+
raise typer.Exit(code=1) from exc
|
|
3100
|
+
if response.status_code == 402:
|
|
3101
|
+
console.print(
|
|
3102
|
+
"[red]entitlement[/red]: BYOK not enabled for this account (missing `run:byok`). "
|
|
3103
|
+
"Contact your admin to enable BYOK cloud execution."
|
|
3104
|
+
)
|
|
3105
|
+
raise typer.Exit(code=1)
|
|
3106
|
+
if response.status_code == 401:
|
|
3107
|
+
console.print("[red]developer key[/red]: unauthorized (invalid or revoked). Run `refactor login`.")
|
|
3108
|
+
raise typer.Exit(code=1)
|
|
3109
|
+
if response.status_code >= 400:
|
|
3110
|
+
console.print(f"[red]cloud endpoint[/red]: credential check failed ({response.status_code}).")
|
|
3111
|
+
raise typer.Exit(code=1)
|
|
3112
|
+
data = response.json() if response.content else {}
|
|
3113
|
+
refs = {str(row.get("credential_ref", "")) for row in (data.get("credentials", []) or [])}
|
|
3114
|
+
if credential_ref not in refs:
|
|
3115
|
+
console.print(
|
|
3116
|
+
f"[red]credential[/red]: `{credential_ref}` not found for this account. "
|
|
3117
|
+
"Check `refactor cloud credentials list`."
|
|
3118
|
+
)
|
|
3119
|
+
raise typer.Exit(code=1)
|
|
3120
|
+
console.print("[green]credential[/green]: found and owned by this account.")
|
|
3121
|
+
console.print("[green]cloud endpoint[/green]: reachable.")
|
|
3122
|
+
else:
|
|
3123
|
+
console.print(
|
|
3124
|
+
"[dim]cloud_managed uses platform-managed provider credentials; "
|
|
3125
|
+
"run `refactor review` to exercise the endpoint.[/dim]"
|
|
3126
|
+
)
|
|
3127
|
+
|
|
3128
|
+
|
|
2915
3129
|
def doctor(
|
|
2916
3130
|
provider: str | None = typer.Option(
|
|
2917
3131
|
None,
|
|
@@ -2946,6 +3160,16 @@ def doctor(
|
|
|
2946
3160
|
_report_sandbox(project_root, project_config)
|
|
2947
3161
|
return
|
|
2948
3162
|
|
|
3163
|
+
execution_mode = _cloud_execution_mode(constitution)
|
|
3164
|
+
if execution_mode:
|
|
3165
|
+
_cloud_doctor(
|
|
3166
|
+
project_root=project_root,
|
|
3167
|
+
constitution=constitution,
|
|
3168
|
+
execution_mode=execution_mode,
|
|
3169
|
+
skip_ping=skip_ping,
|
|
3170
|
+
)
|
|
3171
|
+
return
|
|
3172
|
+
|
|
2949
3173
|
mode = str(constitution.get_setting("mode", "local") or "local")
|
|
2950
3174
|
provider_name = resolve_provider_name(constitution, provider_name=provider)
|
|
2951
3175
|
|