refactorai-cli 0.7.1__tar.gz → 0.7.3__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.7.1 → refactorai_cli-0.7.3}/PKG-INFO +2 -2
- {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/pyproject.toml +2 -2
- {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/__init__.py +1 -1
- {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/client.py +25 -0
- {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/commands/auth_cmds.py +39 -2
- refactorai_cli-0.7.3/refactorai_cli/commands/policy_cmds.py +168 -0
- {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/commands/run_cmds.py +180 -13
- {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/main.py +5 -0
- {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli.egg-info/PKG-INFO +2 -2
- {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli.egg-info/SOURCES.txt +1 -0
- {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli.egg-info/requires.txt +1 -1
- {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/README.md +0 -0
- {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/auth.py +0 -0
- {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/cloud_rr.py +0 -0
- {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/commands/__init__.py +0 -0
- {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/commands/account_cmds.py +0 -0
- {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/commands/branch_cmds.py +0 -0
- {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/commands/cloud_cmds.py +0 -0
- {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/commands/engine_cmds.py +0 -0
- {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/commands/hook_cmds.py +0 -0
- {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/commands/model_cmds.py +0 -0
- {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/commands/pre_push_cmds.py +0 -0
- {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/commands/request_cmds.py +0 -0
- {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/commands/rules_cmds.py +0 -0
- {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/commands/runtime_cmds.py +0 -0
- {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/commands/runtime_proxy_cmds.py +0 -0
- {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/commands/setup_cmds.py +0 -0
- {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/commands/toolchains_cmds.py +0 -0
- {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/commands/watch_cmds.py +0 -0
- {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/commands/workspace_cmds.py +0 -0
- {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/commit_queue.py +0 -0
- {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/commit_telemetry.py +0 -0
- {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/control_plane.py +0 -0
- {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/credentials.py +0 -0
- {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/dotenv_loader.py +0 -0
- {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/git_scope.py +0 -0
- {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/local_constitution.py +0 -0
- {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/local_engine_runtime.py +0 -0
- {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/local_paths.py +0 -0
- {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/model_policy.py +0 -0
- {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/pre_push_gate.py +0 -0
- {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/refactor_branch_store.py +0 -0
- {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/review_runner.py +0 -0
- {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/runtime_manager.py +0 -0
- {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/settings.py +0 -0
- {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/setup_flow.py +0 -0
- {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/watch_ledger.py +0 -0
- {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/watch_state.py +0 -0
- {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/watch_supervisor.py +0 -0
- {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli.egg-info/dependency_links.txt +0 -0
- {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli.egg-info/entry_points.txt +0 -0
- {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli.egg-info/top_level.txt +0 -0
- {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: refactorai-cli
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.3
|
|
4
4
|
Summary: Local-first CLI for the refactor platform
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -8,7 +8,7 @@ Requires-Dist: typer>=0.12.0
|
|
|
8
8
|
Requires-Dist: httpx>=0.27.0
|
|
9
9
|
Requires-Dist: rich>=13.7.0
|
|
10
10
|
Requires-Dist: PyYAML>=6.0.1
|
|
11
|
-
Requires-Dist: refactorai-core>=3.2.
|
|
11
|
+
Requires-Dist: refactorai-core>=3.2.15
|
|
12
12
|
|
|
13
13
|
# refactorai-cli
|
|
14
14
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "refactorai-cli"
|
|
3
|
-
version = "0.7.
|
|
3
|
+
version = "0.7.3"
|
|
4
4
|
description = "Local-first CLI for the refactor platform"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -12,7 +12,7 @@ dependencies = [
|
|
|
12
12
|
"httpx>=0.27.0",
|
|
13
13
|
"rich>=13.7.0",
|
|
14
14
|
"PyYAML>=6.0.1",
|
|
15
|
-
"refactorai-core>=3.2.
|
|
15
|
+
"refactorai-core>=3.2.15",
|
|
16
16
|
]
|
|
17
17
|
|
|
18
18
|
[project.scripts]
|
|
@@ -118,3 +118,28 @@ class PlatformClient:
|
|
|
118
118
|
)
|
|
119
119
|
payload = response.json()
|
|
120
120
|
return payload if isinstance(payload, list) else []
|
|
121
|
+
|
|
122
|
+
def resolve_org_bootstrap(self, developer_key: str) -> dict:
|
|
123
|
+
"""Resolve the org governance bootstrap context for the key's account.
|
|
124
|
+
|
|
125
|
+
Returns ``{account_context, org_policy_profile, org_config_profile}``.
|
|
126
|
+
Org fields are ``None`` for personal accounts. Used by
|
|
127
|
+
``refactor policy sync`` to refresh the local read-only org cache.
|
|
128
|
+
"""
|
|
129
|
+
try:
|
|
130
|
+
response = httpx.get(
|
|
131
|
+
f"{self.base_url}/v1/keys/org-bootstrap",
|
|
132
|
+
headers={"Authorization": f"Bearer {developer_key}"},
|
|
133
|
+
timeout=self.timeout,
|
|
134
|
+
)
|
|
135
|
+
except httpx.HTTPError as exc:
|
|
136
|
+
raise PlatformError(f"Could not reach platform at {self.base_url}: {exc}") from exc
|
|
137
|
+
if response.status_code == 401:
|
|
138
|
+
raise PlatformError("Developer key is invalid or revoked", status_code=401)
|
|
139
|
+
if response.status_code >= 400:
|
|
140
|
+
raise PlatformError(
|
|
141
|
+
f"Org policy resolution failed ({response.status_code})",
|
|
142
|
+
status_code=response.status_code,
|
|
143
|
+
)
|
|
144
|
+
payload = response.json()
|
|
145
|
+
return payload if isinstance(payload, dict) else {}
|
|
@@ -1,14 +1,17 @@
|
|
|
1
|
-
"""`refactor login` and `refactor whoami`."""
|
|
1
|
+
"""`refactor login`, `refactor logout`, and `refactor whoami`."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import shutil
|
|
6
|
+
|
|
5
7
|
import typer
|
|
6
8
|
from rich.console import Console
|
|
7
9
|
|
|
8
10
|
from refactorai_cli.auth import AuthError, ensure_authenticated
|
|
9
11
|
from refactorai_cli.client import PlatformClient, PlatformError
|
|
10
12
|
from refactorai_cli.credentials import load_credentials, save_credentials
|
|
11
|
-
from refactorai_cli.
|
|
13
|
+
from refactorai_cli.local_paths import credentials_path, validation_cache_dir
|
|
14
|
+
from refactorai_cli.settings import env_api_key, mask_key, platform_url
|
|
12
15
|
|
|
13
16
|
console = Console()
|
|
14
17
|
|
|
@@ -86,3 +89,37 @@ def whoami() -> None:
|
|
|
86
89
|
f"[bold]key[/bold]: {mask_key(ctx.key.key)} (source: {source})\n"
|
|
87
90
|
f"[bold]quota_remaining[/bold]: {quota}{suffix}"
|
|
88
91
|
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def logout() -> None:
|
|
95
|
+
"""Clear central credentials and cached key validation state."""
|
|
96
|
+
creds_path = credentials_path()
|
|
97
|
+
cache_dir = validation_cache_dir()
|
|
98
|
+
removed_credentials = False
|
|
99
|
+
removed_cache = False
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
if creds_path.exists():
|
|
103
|
+
creds_path.unlink()
|
|
104
|
+
removed_credentials = True
|
|
105
|
+
except OSError as exc:
|
|
106
|
+
console.print(f"[red]Logout failed:[/red] Could not remove {creds_path}: {exc}")
|
|
107
|
+
raise typer.Exit(code=1) from exc
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
if cache_dir.exists():
|
|
111
|
+
shutil.rmtree(cache_dir)
|
|
112
|
+
removed_cache = True
|
|
113
|
+
except OSError as exc:
|
|
114
|
+
console.print(f"[red]Logout failed:[/red] Could not remove {cache_dir}: {exc}")
|
|
115
|
+
raise typer.Exit(code=1) from exc
|
|
116
|
+
|
|
117
|
+
if removed_credentials or removed_cache:
|
|
118
|
+
console.print("[green]Logged out.[/green] Cleared saved credentials and validation cache.")
|
|
119
|
+
else:
|
|
120
|
+
console.print("[dim]No saved credentials were found.[/dim]")
|
|
121
|
+
if env_api_key():
|
|
122
|
+
console.print(
|
|
123
|
+
"[yellow]Note:[/yellow] REFACTOR_API_KEY is set in your environment; "
|
|
124
|
+
"that key still takes precedence."
|
|
125
|
+
)
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""``refactor policy`` — inspect and sync organization governance.
|
|
2
|
+
|
|
3
|
+
Organization policy is authoritative on the cloud and cached read-only under
|
|
4
|
+
``~/.refactor`` (never in the repo). These commands let a developer:
|
|
5
|
+
|
|
6
|
+
* ``refactor policy show`` — view the resolved org policy + config guardrails
|
|
7
|
+
and their integrity/version status (offline, read-only).
|
|
8
|
+
* ``refactor policy sync`` — refresh the local cache from the cloud (resolves
|
|
9
|
+
``ORG_POLICY_STALE`` after an admin publishes a new version, or restores a
|
|
10
|
+
locally tampered cache).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
import typer
|
|
18
|
+
from rich.console import Console
|
|
19
|
+
|
|
20
|
+
from refactor_core.org_policy import verify_policy_integrity
|
|
21
|
+
from refactor_core.store import (
|
|
22
|
+
read_governance_events,
|
|
23
|
+
read_org_policy_cache,
|
|
24
|
+
resolve_project_id as resolve_store_project_id,
|
|
25
|
+
summarize_governance_events,
|
|
26
|
+
write_org_policy_cache,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
from refactorai_cli.auth import AuthError, ensure_authenticated
|
|
30
|
+
from refactorai_cli.client import PlatformClient, PlatformError
|
|
31
|
+
|
|
32
|
+
app = typer.Typer(help="Inspect and sync organization governance policy.")
|
|
33
|
+
console = Console()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _project_root() -> Path:
|
|
37
|
+
try:
|
|
38
|
+
return Path.cwd()
|
|
39
|
+
except OSError as exc:
|
|
40
|
+
console.print("[red]Could not resolve the current project directory.[/red]")
|
|
41
|
+
raise typer.Exit(code=1) from exc
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@app.command("show")
|
|
45
|
+
def show() -> None:
|
|
46
|
+
"""Show the resolved organization policy and config guardrails (read-only)."""
|
|
47
|
+
project_root = _project_root()
|
|
48
|
+
pid = resolve_store_project_id(project_root)
|
|
49
|
+
cached = read_org_policy_cache(project_root, project_id=pid or None)
|
|
50
|
+
if not cached:
|
|
51
|
+
console.print(
|
|
52
|
+
"[dim]This project is not organization-governed "
|
|
53
|
+
"(personal account, or no org policy resolved at init).[/dim]"
|
|
54
|
+
)
|
|
55
|
+
raise typer.Exit(code=0)
|
|
56
|
+
|
|
57
|
+
markdown, meta = cached
|
|
58
|
+
policy = meta.get("policy", {}) if isinstance(meta, dict) else {}
|
|
59
|
+
config = meta.get("config", {}) if isinstance(meta, dict) else {}
|
|
60
|
+
account_slug = meta.get("account_slug") if isinstance(meta, dict) else ""
|
|
61
|
+
|
|
62
|
+
source = str(policy.get("source") or "system_default")
|
|
63
|
+
version = policy.get("version")
|
|
64
|
+
integrity_ok = verify_policy_integrity(markdown, str(policy.get("hash") or ""))
|
|
65
|
+
|
|
66
|
+
console.print(f"[bold]Organization[/bold]: {account_slug or '?'}")
|
|
67
|
+
console.print(f"[bold]Policy source[/bold]: {source} (v{version})")
|
|
68
|
+
console.print(
|
|
69
|
+
f"[bold]Integrity[/bold]: "
|
|
70
|
+
+ ("[green]OK[/green]" if integrity_ok else "[red]TAMPERED — run `refactor policy sync`[/red]")
|
|
71
|
+
)
|
|
72
|
+
console.print(f"[bold]Resolved at[/bold]: {meta.get('resolved_at') if isinstance(meta, dict) else '?'}")
|
|
73
|
+
|
|
74
|
+
locked = list(config.get("locked_paths") or [])
|
|
75
|
+
required = list(config.get("required_paths") or [])
|
|
76
|
+
forbidden = list(config.get("forbidden_paths") or [])
|
|
77
|
+
console.print(
|
|
78
|
+
"[bold]Config guardrails[/bold]: "
|
|
79
|
+
f"locked={locked or '—'} required={required or '—'} forbidden={forbidden or '—'}"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
summary = summarize_governance_events(
|
|
83
|
+
read_governance_events(project_root, project_id=pid or None)
|
|
84
|
+
)
|
|
85
|
+
if summary.get("total"):
|
|
86
|
+
counts = summary.get("counts", {})
|
|
87
|
+
console.print(
|
|
88
|
+
"[bold]Enforcement history[/bold]: "
|
|
89
|
+
f"ok={counts.get('ok', 0)} "
|
|
90
|
+
f"tamper={counts.get('tamper', 0)} "
|
|
91
|
+
f"guardrail_violation={counts.get('guardrail_violation', 0)} "
|
|
92
|
+
f"(last {summary.get('last_ts') or '—'})"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
console.print("\n[bold]Organization Policy[/bold]:")
|
|
96
|
+
if markdown.strip():
|
|
97
|
+
console.print(markdown.strip())
|
|
98
|
+
else:
|
|
99
|
+
console.print("[dim](none — organization has not published a policy; system defaults apply)[/dim]")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@app.command("sync")
|
|
103
|
+
def sync() -> None:
|
|
104
|
+
"""Refresh the local org policy/config cache from the cloud."""
|
|
105
|
+
project_root = _project_root()
|
|
106
|
+
pid = resolve_store_project_id(project_root)
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
auth_ctx = ensure_authenticated(project_root, force_remote=True)
|
|
110
|
+
except AuthError as exc:
|
|
111
|
+
console.print(f"[red]Authentication required to sync org policy:[/red] {exc}")
|
|
112
|
+
raise typer.Exit(code=1) from exc
|
|
113
|
+
|
|
114
|
+
client = PlatformClient()
|
|
115
|
+
try:
|
|
116
|
+
data = client.resolve_org_bootstrap(auth_ctx.key.key)
|
|
117
|
+
except PlatformError as exc:
|
|
118
|
+
console.print(f"[red]Could not resolve organization policy:[/red] {exc}")
|
|
119
|
+
raise typer.Exit(code=1) from exc
|
|
120
|
+
|
|
121
|
+
account_ctx = data.get("account_context") or {}
|
|
122
|
+
policy_ctx = data.get("org_policy_profile")
|
|
123
|
+
config_ctx = data.get("org_config_profile")
|
|
124
|
+
|
|
125
|
+
if not policy_ctx and not config_ctx:
|
|
126
|
+
console.print(
|
|
127
|
+
"[dim]Account is personal / not organization-governed; nothing to sync.[/dim]"
|
|
128
|
+
)
|
|
129
|
+
raise typer.Exit(code=0)
|
|
130
|
+
|
|
131
|
+
policy_ctx = policy_ctx or {}
|
|
132
|
+
config_ctx = config_ctx or {}
|
|
133
|
+
|
|
134
|
+
previous = read_org_policy_cache(project_root, project_id=pid or None)
|
|
135
|
+
old_version = None
|
|
136
|
+
if previous is not None:
|
|
137
|
+
old_version = (previous[1].get("policy", {}) or {}).get("version")
|
|
138
|
+
|
|
139
|
+
write_org_policy_cache(
|
|
140
|
+
project_root,
|
|
141
|
+
project_id=pid or None,
|
|
142
|
+
account_slug=str(account_ctx.get("slug") or auth_ctx.account_slug or ""),
|
|
143
|
+
policy_source=str(policy_ctx.get("source") or "system_default"),
|
|
144
|
+
policy_version=int(policy_ctx.get("version") or 0),
|
|
145
|
+
policy_hash=str(policy_ctx.get("org_policies_hash") or ""),
|
|
146
|
+
policy_markdown=str(policy_ctx.get("org_policies_markdown") or ""),
|
|
147
|
+
policy_profile_id=policy_ctx.get("profile_id"),
|
|
148
|
+
config_source=str(config_ctx.get("source") or "system_default"),
|
|
149
|
+
config_version=int(config_ctx.get("version") or 0),
|
|
150
|
+
config_profile_id=config_ctx.get("profile_id"),
|
|
151
|
+
config_template_yaml=str(config_ctx.get("config_template_yaml") or ""),
|
|
152
|
+
locked_paths=list(config_ctx.get("locked_paths") or []),
|
|
153
|
+
required_paths=list(config_ctx.get("required_paths") or []),
|
|
154
|
+
forbidden_paths=list(config_ctx.get("forbidden_paths") or []),
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
new_version = int(policy_ctx.get("version") or 0)
|
|
158
|
+
slug = account_ctx.get("slug") or auth_ctx.account_slug or "?"
|
|
159
|
+
if old_version is not None and new_version != old_version:
|
|
160
|
+
console.print(
|
|
161
|
+
f"[green]Synced[/green] organization policy for [bold]{slug}[/bold]: "
|
|
162
|
+
f"v{old_version} → v{new_version}."
|
|
163
|
+
)
|
|
164
|
+
else:
|
|
165
|
+
console.print(
|
|
166
|
+
f"[green]Synced[/green] organization policy for [bold]{slug}[/bold] (v{new_version})."
|
|
167
|
+
)
|
|
168
|
+
console.print("[dim]Cache refreshed under ~/.refactor (never written into your repo).[/dim]")
|
|
@@ -33,9 +33,16 @@ from refactor_core.constitution import (
|
|
|
33
33
|
has_raw_secret_literal,
|
|
34
34
|
load_config,
|
|
35
35
|
load_constitution,
|
|
36
|
+
parse_config,
|
|
36
37
|
raw_secret_matches,
|
|
37
38
|
raw_secret_matches_config,
|
|
38
39
|
)
|
|
40
|
+
from refactor_core.org_policy import (
|
|
41
|
+
ORG_POLICY_TAMPERED,
|
|
42
|
+
assemble_effective_body,
|
|
43
|
+
check_config_guardrails,
|
|
44
|
+
verify_policy_integrity,
|
|
45
|
+
)
|
|
39
46
|
from refactor_core.compliance_settings import (
|
|
40
47
|
ComplianceSelectionError,
|
|
41
48
|
resolve_compliance_selection,
|
|
@@ -91,8 +98,11 @@ from refactor_core.store import (
|
|
|
91
98
|
head_run_dir,
|
|
92
99
|
list_runs,
|
|
93
100
|
prune_runs,
|
|
101
|
+
read_org_policy_cache,
|
|
102
|
+
record_governance_event,
|
|
94
103
|
resolve_project_id as resolve_store_project_id,
|
|
95
104
|
resolve_store_dir,
|
|
105
|
+
write_org_policy_cache,
|
|
96
106
|
read_findings,
|
|
97
107
|
read_report,
|
|
98
108
|
read_sandbox_install_history,
|
|
@@ -911,9 +921,115 @@ def _load_project() -> tuple[Path, object, object]:
|
|
|
911
921
|
constitution = load_constitution(consti_path)
|
|
912
922
|
config = load_config(config_path)
|
|
913
923
|
_warn_raw_key(constitution, config)
|
|
924
|
+
_apply_org_governance(consti_path.parent, constitution, config)
|
|
914
925
|
return consti_path.parent, constitution, config
|
|
915
926
|
|
|
916
927
|
|
|
928
|
+
def _apply_org_governance(project_root: Path, constitution, config) -> None:
|
|
929
|
+
"""Enforce org governance and assemble the effective constitution.
|
|
930
|
+
|
|
931
|
+
For org-governed projects (a home-level org cache exists) this runs three
|
|
932
|
+
deterministic, offline checks before any command touches the pipeline:
|
|
933
|
+
|
|
934
|
+
1. Tamper detection — the cached org policy must still match its pinned hash.
|
|
935
|
+
2. Config guardrails — locked/required/forbidden config paths.
|
|
936
|
+
3. Precedence assembly — org policy is prepended to the constitution body so
|
|
937
|
+
the engine always sees it first ("org wins").
|
|
938
|
+
|
|
939
|
+
Personal accounts (no cache) are a no-op. ``REFACTOR_ORG_POLICY_ENFORCEMENT
|
|
940
|
+
=warn`` downgrades blocking to warnings for emergency use.
|
|
941
|
+
"""
|
|
942
|
+
try:
|
|
943
|
+
pid = resolve_store_project_id(project_root)
|
|
944
|
+
cached = read_org_policy_cache(project_root, project_id=pid or None)
|
|
945
|
+
except Exception:
|
|
946
|
+
cached = None
|
|
947
|
+
if not cached:
|
|
948
|
+
return
|
|
949
|
+
|
|
950
|
+
markdown, meta = cached
|
|
951
|
+
policy_meta = meta.get("policy", {}) if isinstance(meta, dict) else {}
|
|
952
|
+
config_meta = meta.get("config", {}) if isinstance(meta, dict) else {}
|
|
953
|
+
account_slug = str(meta.get("account_slug") or "") if isinstance(meta, dict) else ""
|
|
954
|
+
warn_only = (
|
|
955
|
+
str(os.environ.get("REFACTOR_ORG_POLICY_ENFORCEMENT", "block")).strip().lower() == "warn"
|
|
956
|
+
)
|
|
957
|
+
|
|
958
|
+
pid_for_ledger = None
|
|
959
|
+
try:
|
|
960
|
+
pid_for_ledger = resolve_store_project_id(project_root)
|
|
961
|
+
except Exception:
|
|
962
|
+
pid_for_ledger = None
|
|
963
|
+
|
|
964
|
+
def _telemetry(event: str, *, detail: str = "", codes: list[str] | None = None) -> None:
|
|
965
|
+
# Local-only observability; never let it break the command.
|
|
966
|
+
try:
|
|
967
|
+
record_governance_event(
|
|
968
|
+
project_root,
|
|
969
|
+
project_id=pid_for_ledger or None,
|
|
970
|
+
event=event,
|
|
971
|
+
detail=detail,
|
|
972
|
+
codes=codes,
|
|
973
|
+
)
|
|
974
|
+
except Exception:
|
|
975
|
+
pass
|
|
976
|
+
|
|
977
|
+
def _block(message: str) -> None:
|
|
978
|
+
if warn_only:
|
|
979
|
+
console.print(f"[yellow]Warning (enforcement bypassed):[/yellow] {message}")
|
|
980
|
+
return
|
|
981
|
+
console.print(message)
|
|
982
|
+
raise typer.Exit(code=1)
|
|
983
|
+
|
|
984
|
+
# 1) Tamper detection.
|
|
985
|
+
if not verify_policy_integrity(markdown, str(policy_meta.get("hash") or "")):
|
|
986
|
+
_telemetry("tamper", detail=account_slug, codes=[ORG_POLICY_TAMPERED])
|
|
987
|
+
_block(
|
|
988
|
+
f"[red]{ORG_POLICY_TAMPERED}[/red]: the cached organization policy for account "
|
|
989
|
+
f"[bold]{account_slug or '?'}[/bold] was modified locally and no longer matches the "
|
|
990
|
+
"version published by your organization. Run `refactor policy sync` to restore it."
|
|
991
|
+
)
|
|
992
|
+
|
|
993
|
+
# 2) Config guardrails.
|
|
994
|
+
org_settings: dict = {}
|
|
995
|
+
template_yaml = str(config_meta.get("template_yaml") or "")
|
|
996
|
+
if template_yaml.strip():
|
|
997
|
+
try:
|
|
998
|
+
org_settings = dict(parse_config(template_yaml).settings or {})
|
|
999
|
+
except Exception:
|
|
1000
|
+
org_settings = {}
|
|
1001
|
+
violations = check_config_guardrails(
|
|
1002
|
+
dict(getattr(config, "settings", {}) or {}),
|
|
1003
|
+
org_settings,
|
|
1004
|
+
locked_paths=list(config_meta.get("locked_paths") or []),
|
|
1005
|
+
required_paths=list(config_meta.get("required_paths") or []),
|
|
1006
|
+
forbidden_paths=list(config_meta.get("forbidden_paths") or []),
|
|
1007
|
+
)
|
|
1008
|
+
if violations:
|
|
1009
|
+
_telemetry(
|
|
1010
|
+
"guardrail_violation",
|
|
1011
|
+
detail=account_slug,
|
|
1012
|
+
codes=[v.code for v in violations],
|
|
1013
|
+
)
|
|
1014
|
+
for violation in violations:
|
|
1015
|
+
console.print(f"[red]{violation.code}[/red]: {violation.detail}")
|
|
1016
|
+
console.print(
|
|
1017
|
+
"[dim]These settings are governed by your organization. Update "
|
|
1018
|
+
f"{CONFIG_FILENAME} to comply, or ask an org admin to change the policy.[/dim]"
|
|
1019
|
+
)
|
|
1020
|
+
if not warn_only:
|
|
1021
|
+
raise typer.Exit(code=1)
|
|
1022
|
+
|
|
1023
|
+
# 3) Effective-constitution assembly (org policy first; charter underneath).
|
|
1024
|
+
if markdown.strip():
|
|
1025
|
+
constitution.body = assemble_effective_body(
|
|
1026
|
+
getattr(constitution, "body", "") or "", markdown
|
|
1027
|
+
)
|
|
1028
|
+
|
|
1029
|
+
# Clean pass: record a compliant outcome for observability.
|
|
1030
|
+
_telemetry("ok", detail=account_slug)
|
|
1031
|
+
|
|
1032
|
+
|
|
917
1033
|
def _print_store_context(project_root) -> None:
|
|
918
1034
|
"""Surface the resolved project id + central store dir for this run.
|
|
919
1035
|
|
|
@@ -1150,16 +1266,54 @@ def init(
|
|
|
1150
1266
|
config_target.write_text(config_text, encoding="utf-8")
|
|
1151
1267
|
|
|
1152
1268
|
constitution = load_constitution(consti_target)
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1269
|
+
register_project(project_root, constitution_hash=constitution.content_hash, project_id=project_id)
|
|
1270
|
+
|
|
1271
|
+
# Org-governed projects: materialize the resolved org policy + config
|
|
1272
|
+
# guardrails at HOME level only (never in the repo). The committed
|
|
1273
|
+
# refactor.consti stays charter-only; the org policy is authoritative on the
|
|
1274
|
+
# cloud and cached read-only under ~/.refactor for offline/runtime assembly.
|
|
1275
|
+
org_policy_ctx = created.get("org_policy_profile") if isinstance(created, dict) else None
|
|
1276
|
+
org_config_ctx = created.get("org_config_profile") if isinstance(created, dict) else None
|
|
1277
|
+
account_ctx = created.get("account_context") if isinstance(created, dict) else None
|
|
1278
|
+
org_cached_meta: dict | None = None
|
|
1279
|
+
if isinstance(org_policy_ctx, dict) or isinstance(org_config_ctx, dict):
|
|
1280
|
+
policy_ctx = org_policy_ctx or {}
|
|
1281
|
+
config_ctx = org_config_ctx or {}
|
|
1282
|
+
org_cached_meta = write_org_policy_cache(
|
|
1283
|
+
project_root,
|
|
1284
|
+
project_id=project_id,
|
|
1285
|
+
account_slug=str((account_ctx or {}).get("slug") or account_slug or ""),
|
|
1286
|
+
policy_source=str(policy_ctx.get("source") or "system_default"),
|
|
1287
|
+
policy_version=int(policy_ctx.get("version") or 0),
|
|
1288
|
+
policy_hash=str(policy_ctx.get("org_policies_hash") or ""),
|
|
1289
|
+
policy_markdown=str(policy_ctx.get("org_policies_markdown") or ""),
|
|
1290
|
+
policy_profile_id=policy_ctx.get("profile_id"),
|
|
1291
|
+
config_source=str(config_ctx.get("source") or "system_default"),
|
|
1292
|
+
config_version=int(config_ctx.get("version") or 0),
|
|
1293
|
+
config_profile_id=config_ctx.get("profile_id"),
|
|
1294
|
+
config_template_yaml=str(config_ctx.get("config_template_yaml") or ""),
|
|
1295
|
+
locked_paths=list(config_ctx.get("locked_paths") or []),
|
|
1296
|
+
required_paths=list(config_ctx.get("required_paths") or []),
|
|
1297
|
+
forbidden_paths=list(config_ctx.get("forbidden_paths") or []),
|
|
1298
|
+
)
|
|
1299
|
+
|
|
1156
1300
|
console.print(f"[green]Created[/green] {consti_target}")
|
|
1157
1301
|
console.print(f"[green]Created[/green] {config_target}")
|
|
1158
|
-
|
|
1302
|
+
account_type = str((account_ctx or {}).get("type") or "").strip().lower()
|
|
1303
|
+
account_name = str((account_ctx or {}).get("display_name") or "").strip()
|
|
1304
|
+
account_slug = str((account_ctx or {}).get("slug") or account_slug or "").strip()
|
|
1305
|
+
if account_type == "organization":
|
|
1306
|
+
account_label = "organization"
|
|
1307
|
+
elif account_type:
|
|
1308
|
+
account_label = account_type
|
|
1309
|
+
else:
|
|
1310
|
+
account_label = "account"
|
|
1311
|
+
account_parts = [part for part in (account_name, account_slug) if part]
|
|
1312
|
+
account_value = " / ".join(account_parts) if account_parts else auth_ctx.account_id
|
|
1159
1313
|
console.print(
|
|
1160
|
-
f"[green]Linked[/green]
|
|
1161
|
-
f"{
|
|
1162
|
-
"
|
|
1314
|
+
f"[green]Linked[/green] cloud project [bold]{project_name}[/bold]. "
|
|
1315
|
+
f"{account_label.title()}: [bold]{account_value}[/bold]. "
|
|
1316
|
+
f"Project id saved in {CONFIG_FILENAME}."
|
|
1163
1317
|
)
|
|
1164
1318
|
if config_profile == "local_server":
|
|
1165
1319
|
console.print(
|
|
@@ -1172,13 +1326,26 @@ def init(
|
|
|
1172
1326
|
"(mode=cloud_byok, env-key-first via provider_key; credential_ref is optional fallback)."
|
|
1173
1327
|
)
|
|
1174
1328
|
console.print(
|
|
1175
|
-
f"
|
|
1176
|
-
|
|
1177
|
-
console.print(
|
|
1178
|
-
f"Only {CONSTITUTION_FILENAME} and {CONFIG_FILENAME} are added to your repo; "
|
|
1179
|
-
"all run state stays under ~/.refactor "
|
|
1180
|
-
f"(encoded: {record['encoded']})."
|
|
1329
|
+
f"Only {CONSTITUTION_FILENAME} and {CONFIG_FILENAME} are added to your repo. "
|
|
1330
|
+
"Runtime state stays outside the repository."
|
|
1181
1331
|
)
|
|
1332
|
+
if org_cached_meta is not None:
|
|
1333
|
+
policy_meta = org_cached_meta.get("policy", {}) if isinstance(org_cached_meta, dict) else {}
|
|
1334
|
+
source = str(policy_meta.get("source") or "system_default")
|
|
1335
|
+
version = policy_meta.get("version")
|
|
1336
|
+
if source == "active":
|
|
1337
|
+
console.print(
|
|
1338
|
+
f"[green]Applied[/green] organization policy (v{version}) from account "
|
|
1339
|
+
f"[bold]{org_cached_meta.get('account_slug') or account_slug}[/bold]. "
|
|
1340
|
+
f"It is cached locally and layered above your "
|
|
1341
|
+
f"editable {CONSTITUTION_FILENAME} charter at run time; it is never "
|
|
1342
|
+
"written into your repo."
|
|
1343
|
+
)
|
|
1344
|
+
else:
|
|
1345
|
+
console.print(
|
|
1346
|
+
"[dim]Organization has no published policy yet; using system defaults. "
|
|
1347
|
+
f"Your {CONSTITUTION_FILENAME} charter is fully in effect.[/dim]"
|
|
1348
|
+
)
|
|
1182
1349
|
_offer_commit_hook(project_root)
|
|
1183
1350
|
|
|
1184
1351
|
|
|
@@ -16,6 +16,7 @@ from refactorai_cli.commands import (
|
|
|
16
16
|
engine_cmds,
|
|
17
17
|
hook_cmds,
|
|
18
18
|
model_cmds,
|
|
19
|
+
policy_cmds,
|
|
19
20
|
pre_push_cmds,
|
|
20
21
|
request_cmds,
|
|
21
22
|
runtime_cmds,
|
|
@@ -118,6 +119,7 @@ def main(
|
|
|
118
119
|
|
|
119
120
|
# Auth (R7.1)
|
|
120
121
|
app.command()(auth_cmds.login)
|
|
122
|
+
app.command()(auth_cmds.logout)
|
|
121
123
|
app.command()(auth_cmds.whoami)
|
|
122
124
|
app.add_typer(account_cmds.app, name="account")
|
|
123
125
|
|
|
@@ -172,6 +174,9 @@ app.add_typer(watch_cmds.watch_app, name="watch")
|
|
|
172
174
|
app.add_typer(branch_cmds.app, name="branch")
|
|
173
175
|
app.add_typer(request_cmds.app, name="request")
|
|
174
176
|
|
|
177
|
+
# Organization governance: inspect/sync org policy + config guardrails (docs/45).
|
|
178
|
+
app.add_typer(policy_cmds.app, name="policy")
|
|
179
|
+
|
|
175
180
|
|
|
176
181
|
if __name__ == "__main__":
|
|
177
182
|
app()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: refactorai-cli
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.3
|
|
4
4
|
Summary: Local-first CLI for the refactor platform
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -8,7 +8,7 @@ Requires-Dist: typer>=0.12.0
|
|
|
8
8
|
Requires-Dist: httpx>=0.27.0
|
|
9
9
|
Requires-Dist: rich>=13.7.0
|
|
10
10
|
Requires-Dist: PyYAML>=6.0.1
|
|
11
|
-
Requires-Dist: refactorai-core>=3.2.
|
|
11
|
+
Requires-Dist: refactorai-core>=3.2.15
|
|
12
12
|
|
|
13
13
|
# refactorai-cli
|
|
14
14
|
|
|
@@ -38,6 +38,7 @@ refactorai_cli/commands/cloud_cmds.py
|
|
|
38
38
|
refactorai_cli/commands/engine_cmds.py
|
|
39
39
|
refactorai_cli/commands/hook_cmds.py
|
|
40
40
|
refactorai_cli/commands/model_cmds.py
|
|
41
|
+
refactorai_cli/commands/policy_cmds.py
|
|
41
42
|
refactorai_cli/commands/pre_push_cmds.py
|
|
42
43
|
refactorai_cli/commands/request_cmds.py
|
|
43
44
|
refactorai_cli/commands/rules_cmds.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|