opencomplai-cli 0.5.0__tar.gz → 0.7.0__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.
- {opencomplai_cli-0.5.0 → opencomplai_cli-0.7.0}/.gitignore +2 -0
- {opencomplai_cli-0.5.0 → opencomplai_cli-0.7.0}/PKG-INFO +40 -4
- opencomplai_cli-0.7.0/README.md +117 -0
- {opencomplai_cli-0.5.0 → opencomplai_cli-0.7.0}/pyproject.toml +1 -1
- {opencomplai_cli-0.5.0 → opencomplai_cli-0.7.0}/src/opencomplai_cli/__init__.py +1 -1
- {opencomplai_cli-0.5.0 → opencomplai_cli-0.7.0}/src/opencomplai_cli/commands/dashboard.py +129 -60
- {opencomplai_cli-0.5.0 → opencomplai_cli-0.7.0}/src/opencomplai_cli/commands/push.py +104 -1
- opencomplai_cli-0.7.0/src/opencomplai_cli/commands/qms.py +177 -0
- {opencomplai_cli-0.5.0 → opencomplai_cli-0.7.0}/src/opencomplai_cli/commands/serve.py +0 -1
- {opencomplai_cli-0.5.0 → opencomplai_cli-0.7.0}/src/opencomplai_cli/connectors/github_actions.py +91 -21
- {opencomplai_cli-0.5.0 → opencomplai_cli-0.7.0}/src/opencomplai_cli/connectors/gitlab_ci.py +95 -21
- {opencomplai_cli-0.5.0 → opencomplai_cli-0.7.0}/src/opencomplai_cli/main.py +385 -10
- {opencomplai_cli-0.5.0 → opencomplai_cli-0.7.0}/src/opencomplai_cli/oidc_client.py +4 -2
- {opencomplai_cli-0.5.0 → opencomplai_cli-0.7.0}/src/opencomplai_cli/publish.py +136 -0
- {opencomplai_cli-0.5.0 → opencomplai_cli-0.7.0}/tests/test_checker_cmd.py +8 -2
- opencomplai_cli-0.7.0/tests/test_ci_integration_guide.py +108 -0
- {opencomplai_cli-0.5.0 → opencomplai_cli-0.7.0}/tests/test_connectors.py +5 -4
- {opencomplai_cli-0.5.0 → opencomplai_cli-0.7.0}/tests/test_dashboard_cmd.py +116 -5
- opencomplai_cli-0.7.0/tests/test_docs_generate_push.py +170 -0
- {opencomplai_cli-0.5.0 → opencomplai_cli-0.7.0}/tests/test_docs_generate_wiring.py +68 -1
- opencomplai_cli-0.7.0/tests/test_fria_generate.py +153 -0
- opencomplai_cli-0.7.0/tests/test_gaps_mapped_column.py +54 -0
- opencomplai_cli-0.7.0/tests/test_gaps_nist_rmf_target.py +211 -0
- {opencomplai_cli-0.5.0 → opencomplai_cli-0.7.0}/tests/test_oidc_client.py +8 -2
- {opencomplai_cli-0.5.0 → opencomplai_cli-0.7.0}/tests/test_publish.py +265 -0
- opencomplai_cli-0.7.0/tests/test_push_cmd_dossier.py +263 -0
- opencomplai_cli-0.7.0/tests/test_qms_generate_cmd.py +96 -0
- opencomplai_cli-0.7.0/tests/test_readme_command_table.py +80 -0
- opencomplai_cli-0.7.0/tests/test_readme_quickstart_pin.py +94 -0
- opencomplai_cli-0.7.0/tests/test_recommend_qms_clauses.py +77 -0
- opencomplai_cli-0.5.0/README.md +0 -81
- {opencomplai_cli-0.5.0 → opencomplai_cli-0.7.0}/src/opencomplai_cli/_encoding.py +0 -0
- {opencomplai_cli-0.5.0 → opencomplai_cli-0.7.0}/src/opencomplai_cli/commands/__init__.py +0 -0
- {opencomplai_cli-0.5.0 → opencomplai_cli-0.7.0}/src/opencomplai_cli/commands/checker.py +0 -0
- {opencomplai_cli-0.5.0 → opencomplai_cli-0.7.0}/src/opencomplai_cli/commands/controls.py +0 -0
- {opencomplai_cli-0.5.0 → opencomplai_cli-0.7.0}/src/opencomplai_cli/commands/halt.py +0 -0
- {opencomplai_cli-0.5.0 → opencomplai_cli-0.7.0}/src/opencomplai_cli/commands/interactive_init.py +0 -0
- {opencomplai_cli-0.5.0 → opencomplai_cli-0.7.0}/src/opencomplai_cli/connectors/__init__.py +0 -0
- {opencomplai_cli-0.5.0 → opencomplai_cli-0.7.0}/src/opencomplai_cli/data/checker-local.html +0 -0
- {opencomplai_cli-0.5.0 → opencomplai_cli-0.7.0}/src/opencomplai_cli/exit_codes.py +0 -0
- {opencomplai_cli-0.5.0 → opencomplai_cli-0.7.0}/tests/__init__.py +0 -0
- {opencomplai_cli-0.5.0 → opencomplai_cli-0.7.0}/tests/fixtures/trap/trap-manifest.json +0 -0
- {opencomplai_cli-0.5.0 → opencomplai_cli-0.7.0}/tests/test_ai_preload.py +0 -0
- {opencomplai_cli-0.5.0 → opencomplai_cli-0.7.0}/tests/test_cli.py +0 -0
- {opencomplai_cli-0.5.0 → opencomplai_cli-0.7.0}/tests/test_controls_cmd.py +0 -0
- {opencomplai_cli-0.5.0 → opencomplai_cli-0.7.0}/tests/test_controls_sync.py +0 -0
- {opencomplai_cli-0.5.0 → opencomplai_cli-0.7.0}/tests/test_encoding.py +0 -0
- {opencomplai_cli-0.5.0 → opencomplai_cli-0.7.0}/tests/test_eu_ai_scan_golden.py +0 -0
- {opencomplai_cli-0.5.0 → opencomplai_cli-0.7.0}/tests/test_evidence_provenance.py +0 -0
- {opencomplai_cli-0.5.0 → opencomplai_cli-0.7.0}/tests/test_exit_codes.py +0 -0
- {opencomplai_cli-0.5.0 → opencomplai_cli-0.7.0}/tests/test_halt_wire.py +0 -0
- {opencomplai_cli-0.5.0 → opencomplai_cli-0.7.0}/tests/test_push_cmd.py +0 -0
- {opencomplai_cli-0.5.0 → opencomplai_cli-0.7.0}/tests/test_scan_cmd.py +0 -0
- {opencomplai_cli-0.5.0 → opencomplai_cli-0.7.0}/tests/test_scan_quick.py +0 -0
- {opencomplai_cli-0.5.0 → opencomplai_cli-0.7.0}/tests/test_service_check.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: opencomplai-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: Opencomplai command-line interface for EU AI Act compliance assessment
|
|
5
5
|
Project-URL: Homepage, https://opencomplai.com
|
|
6
6
|
Project-URL: Documentation, https://docs.opencomplai.com
|
|
@@ -50,13 +50,32 @@ meta-package instead.
|
|
|
50
50
|
| `opencomplai init` | Scaffold a `system-manifest.json` for your project |
|
|
51
51
|
| `opencomplai scan` | Corroborate the manifest against your code and report discrepancies |
|
|
52
52
|
| `opencomplai check` | Run the compliance gate and write `compliance-artifact.json` |
|
|
53
|
-
| `opencomplai
|
|
53
|
+
| `opencomplai push` | Publish a signed artifact (scan status or Annex IV dossier) to the Premium Dashboard |
|
|
54
|
+
| `opencomplai checker` | Run the interactive EU AI Act applicability checker |
|
|
55
|
+
| `opencomplai gaps` | Print a per-article EU AI Act gap report (informational — never gates CI) |
|
|
56
|
+
| `opencomplai recommend` | Write copy-paste remediation templates for Missing/Partial gap-report rows |
|
|
57
|
+
| `opencomplai report` | Render a single shareable HTML/PDF compliance report |
|
|
58
|
+
| `opencomplai eval` | Run safety, bias, and data-leakage pipeline evaluators |
|
|
59
|
+
| `opencomplai validate-manifest` | Validate a `system-manifest.json` against the required schema |
|
|
60
|
+
| `opencomplai serve` | Start a localhost-only scan dashboard (not Pro/SaaS) |
|
|
61
|
+
| `opencomplai approve` | Mint a signed HITL approval token for a `HALTED_PENDING_REVIEW` system |
|
|
62
|
+
| `opencomplai resume` | Resume a `HALTED_PENDING_REVIEW` system with a signed approval token |
|
|
63
|
+
| `opencomplai verify-output` | Verify an AI output claim against ground-truth sources |
|
|
64
|
+
| `opencomplai version` | Show the installed Opencomplai version |
|
|
65
|
+
| `opencomplai info` | Show full package metadata (`pip show`-style, across the whole suite) |
|
|
66
|
+
|
|
67
|
+
Also available as command groups (`opencomplai <group> --help` for their own subcommands):
|
|
68
|
+
`docs` (Annex IV dossier generation), `risk` (risk classification), `sync` (metadata sync),
|
|
69
|
+
`keys` (signing-key rotation), `ai` (optional AI-intent plugin configuration), `controls`
|
|
70
|
+
(control-register status).
|
|
54
71
|
|
|
55
72
|
Run `opencomplai --help` for the full command list, or `opencomplai <command> --help` for
|
|
56
73
|
options.
|
|
57
74
|
|
|
58
75
|
## Quick start
|
|
59
76
|
|
|
77
|
+
The self-serve path: mint a key on the dashboard, export two env vars, sign a scan, push it.
|
|
78
|
+
|
|
60
79
|
```bash
|
|
61
80
|
# 1. Scaffold a manifest for your project
|
|
62
81
|
opencomplai init
|
|
@@ -64,10 +83,23 @@ opencomplai init
|
|
|
64
83
|
# 2. Cross-check the manifest against your source tree
|
|
65
84
|
opencomplai scan --manifest system-manifest.json --repo-root .
|
|
66
85
|
|
|
67
|
-
# 3.
|
|
68
|
-
|
|
86
|
+
# 3. Get an API key from the dashboard's /connect page (Projects -> your
|
|
87
|
+
# project -> Connect), then export it alongside the dashboard's ingest URL
|
|
88
|
+
export OPENCOMPLAI_API_KEY=ock_...
|
|
89
|
+
export OPENCOMPLAI_DASHBOARD_URL=https://your-dashboard-host/api/ingest
|
|
90
|
+
|
|
91
|
+
# 4. Run the compliance gate and sign the artifact (writes compliance-artifact.json)
|
|
92
|
+
opencomplai check --sign
|
|
93
|
+
|
|
94
|
+
# 5. Push the signed artifact to the dashboard
|
|
95
|
+
opencomplai push
|
|
69
96
|
```
|
|
70
97
|
|
|
98
|
+
Following this start-to-finish lands the scan on the dashboard's `/systems` page for that
|
|
99
|
+
project. `/connect` also generates ready-to-paste GitHub Actions / GitLab CI snippets that
|
|
100
|
+
wire the same two env vars into a pipeline — see
|
|
101
|
+
[CI integration](https://docs.opencomplai.com/guides/ci-integration/).
|
|
102
|
+
|
|
71
103
|
`opencomplai check` is the canonical CI gate. Its exit code is contractual:
|
|
72
104
|
|
|
73
105
|
| Exit code | Meaning |
|
|
@@ -84,6 +116,10 @@ So you can wire it straight into CI:
|
|
|
84
116
|
opencomplai check || exit $?
|
|
85
117
|
```
|
|
86
118
|
|
|
119
|
+
To publish an Annex IV dossier instead of (or in addition to) a scan-status artifact, run
|
|
120
|
+
`opencomplai docs generate --system-id ... --push` — same `OPENCOMPLAI_API_KEY` /
|
|
121
|
+
`OPENCOMPLAI_DASHBOARD_URL` as `opencomplai push` above.
|
|
122
|
+
|
|
87
123
|
## Optional: AI intent analysis
|
|
88
124
|
|
|
89
125
|
Install the [`opencomplai-ai`](https://pypi.org/project/opencomplai-ai/) plugin to unlock
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# opencomplai-cli
|
|
2
|
+
|
|
3
|
+
[](https://www.gnu.org/licenses/agpl-3.0)
|
|
4
|
+
[](https://pypi.org/project/opencomplai-cli/)
|
|
5
|
+
[](https://www.python.org/)
|
|
6
|
+
|
|
7
|
+
The `opencomplai` command-line tool for EU AI Act compliance assessment. It scans your
|
|
8
|
+
repository, classifies your AI system against the EU AI Act, and produces an auditable,
|
|
9
|
+
CI-gateable compliance artifact.
|
|
10
|
+
|
|
11
|
+
Built on [`opencomplai-core`](https://pypi.org/project/opencomplai-core/) — the same
|
|
12
|
+
deterministic, rule-based risk engine, with a rich terminal UX.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install opencomplai-cli
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
This pulls in `opencomplai-core[reports]` automatically. To install the full suite
|
|
21
|
+
(engine + CLI) in one step, use the [`opencomplai`](https://pypi.org/project/opencomplai/)
|
|
22
|
+
meta-package instead.
|
|
23
|
+
|
|
24
|
+
## Core commands
|
|
25
|
+
|
|
26
|
+
| Command | What it does |
|
|
27
|
+
|---|---|
|
|
28
|
+
| `opencomplai init` | Scaffold a `system-manifest.json` for your project |
|
|
29
|
+
| `opencomplai scan` | Corroborate the manifest against your code and report discrepancies |
|
|
30
|
+
| `opencomplai check` | Run the compliance gate and write `compliance-artifact.json` |
|
|
31
|
+
| `opencomplai push` | Publish a signed artifact (scan status or Annex IV dossier) to the Premium Dashboard |
|
|
32
|
+
| `opencomplai checker` | Run the interactive EU AI Act applicability checker |
|
|
33
|
+
| `opencomplai gaps` | Print a per-article EU AI Act gap report (informational — never gates CI) |
|
|
34
|
+
| `opencomplai recommend` | Write copy-paste remediation templates for Missing/Partial gap-report rows |
|
|
35
|
+
| `opencomplai report` | Render a single shareable HTML/PDF compliance report |
|
|
36
|
+
| `opencomplai eval` | Run safety, bias, and data-leakage pipeline evaluators |
|
|
37
|
+
| `opencomplai validate-manifest` | Validate a `system-manifest.json` against the required schema |
|
|
38
|
+
| `opencomplai serve` | Start a localhost-only scan dashboard (not Pro/SaaS) |
|
|
39
|
+
| `opencomplai approve` | Mint a signed HITL approval token for a `HALTED_PENDING_REVIEW` system |
|
|
40
|
+
| `opencomplai resume` | Resume a `HALTED_PENDING_REVIEW` system with a signed approval token |
|
|
41
|
+
| `opencomplai verify-output` | Verify an AI output claim against ground-truth sources |
|
|
42
|
+
| `opencomplai version` | Show the installed Opencomplai version |
|
|
43
|
+
| `opencomplai info` | Show full package metadata (`pip show`-style, across the whole suite) |
|
|
44
|
+
|
|
45
|
+
Also available as command groups (`opencomplai <group> --help` for their own subcommands):
|
|
46
|
+
`docs` (Annex IV dossier generation), `risk` (risk classification), `sync` (metadata sync),
|
|
47
|
+
`keys` (signing-key rotation), `ai` (optional AI-intent plugin configuration), `controls`
|
|
48
|
+
(control-register status).
|
|
49
|
+
|
|
50
|
+
Run `opencomplai --help` for the full command list, or `opencomplai <command> --help` for
|
|
51
|
+
options.
|
|
52
|
+
|
|
53
|
+
## Quick start
|
|
54
|
+
|
|
55
|
+
The self-serve path: mint a key on the dashboard, export two env vars, sign a scan, push it.
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# 1. Scaffold a manifest for your project
|
|
59
|
+
opencomplai init
|
|
60
|
+
|
|
61
|
+
# 2. Cross-check the manifest against your source tree
|
|
62
|
+
opencomplai scan --manifest system-manifest.json --repo-root .
|
|
63
|
+
|
|
64
|
+
# 3. Get an API key from the dashboard's /connect page (Projects -> your
|
|
65
|
+
# project -> Connect), then export it alongside the dashboard's ingest URL
|
|
66
|
+
export OPENCOMPLAI_API_KEY=ock_...
|
|
67
|
+
export OPENCOMPLAI_DASHBOARD_URL=https://your-dashboard-host/api/ingest
|
|
68
|
+
|
|
69
|
+
# 4. Run the compliance gate and sign the artifact (writes compliance-artifact.json)
|
|
70
|
+
opencomplai check --sign
|
|
71
|
+
|
|
72
|
+
# 5. Push the signed artifact to the dashboard
|
|
73
|
+
opencomplai push
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Following this start-to-finish lands the scan on the dashboard's `/systems` page for that
|
|
77
|
+
project. `/connect` also generates ready-to-paste GitHub Actions / GitLab CI snippets that
|
|
78
|
+
wire the same two env vars into a pipeline — see
|
|
79
|
+
[CI integration](https://docs.opencomplai.com/guides/ci-integration/).
|
|
80
|
+
|
|
81
|
+
`opencomplai check` is the canonical CI gate. Its exit code is contractual:
|
|
82
|
+
|
|
83
|
+
| Exit code | Meaning |
|
|
84
|
+
|---|---|
|
|
85
|
+
| `0` | PASS |
|
|
86
|
+
| `1` | CONTROL_FAIL |
|
|
87
|
+
| `2` | VALIDATION_FAIL |
|
|
88
|
+
| `3` | POLICY_BLOCK |
|
|
89
|
+
| `4` | TRAP_DETECTED |
|
|
90
|
+
|
|
91
|
+
So you can wire it straight into CI:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
opencomplai check || exit $?
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
To publish an Annex IV dossier instead of (or in addition to) a scan-status artifact, run
|
|
98
|
+
`opencomplai docs generate --system-id ... --push` — same `OPENCOMPLAI_API_KEY` /
|
|
99
|
+
`OPENCOMPLAI_DASHBOARD_URL` as `opencomplai push` above.
|
|
100
|
+
|
|
101
|
+
## Optional: AI intent analysis
|
|
102
|
+
|
|
103
|
+
Install the [`opencomplai-ai`](https://pypi.org/project/opencomplai-ai/) plugin to unlock
|
|
104
|
+
the `--ai-intent` flag, which classifies how each AI callsite is actually used:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
pip install opencomplai-ai
|
|
108
|
+
opencomplai scan --ai-intent
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Documentation
|
|
112
|
+
|
|
113
|
+
Full CLI reference and guides at **[docs.opencomplai.com](https://docs.opencomplai.com)**.
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
AGPL-3.0-only. See [LICENSE](https://www.gnu.org/licenses/agpl-3.0).
|
|
@@ -1,26 +1,25 @@
|
|
|
1
1
|
"""
|
|
2
2
|
CLI dashboard sub-commands (C0.8).
|
|
3
3
|
|
|
4
|
-
Subcommand: ``opencomplai dashboard enroll``
|
|
5
|
-
Opt an OSS install into the Premium Dashboard
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
4. Adds the dashboard endpoint to the local egress-proxy allowlist with a
|
|
13
|
-
per-endpoint policy reference.
|
|
14
|
-
5. Prints the dashboard URL and an audit-event hash the operator can verify.
|
|
15
|
-
|
|
16
|
-
Idempotent: re-running with the same tenant returns the existing enrollment
|
|
17
|
-
and emits no new ledger event.
|
|
4
|
+
Subcommand: ``opencomplai dashboard enroll`` (hidden — see its own docstring)
|
|
5
|
+
Opt an OSS install into the Premium Dashboard via a one-time bootstrap
|
|
6
|
+
token. This is an operator path: the token has to come from somewhere,
|
|
7
|
+
and as of DG-11 nothing in this repo issues one outside a test (admin-api's
|
|
8
|
+
``issue_bootstrap_token`` has zero non-test callers — no route, no web UI).
|
|
9
|
+
The self-serve onboarding path is the ``/connect`` page + an
|
|
10
|
+
``OPENCOMPLAI_API_KEY`` (see ``opencomplai push`` / ``opencomplai docs
|
|
11
|
+
generate --push``), which needs none of this.
|
|
18
12
|
|
|
19
13
|
Subcommand: ``opencomplai dashboard withdraw``
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
14
|
+
Clear this install's local enrollment state (egress-allowlist entry +
|
|
15
|
+
a ``consent_revoked`` ledger event) unconditionally. Also best-effort
|
|
16
|
+
notifies the dashboard's ``POST /v1/admin/withdraw`` when this install
|
|
17
|
+
can actually authenticate to it — that endpoint requires a tenant-scoped
|
|
18
|
+
service JWT (SEC-2) that a plain OSS install typically never has (it is
|
|
19
|
+
minted from ``enroll``'s OIDC client-credentials pair, and ``enroll`` is
|
|
20
|
+
the hidden/effectively-dead path described above). When no such token
|
|
21
|
+
can be produced, the remote call is skipped rather than sent bare and
|
|
22
|
+
guaranteed a 401 — see ``withdraw()``'s own docstring.
|
|
24
23
|
"""
|
|
25
24
|
|
|
26
25
|
from __future__ import annotations
|
|
@@ -53,9 +52,7 @@ except ImportError:
|
|
|
53
52
|
_console = _FallbackConsole() # type: ignore[assignment]
|
|
54
53
|
_err_console = _FallbackConsole() # type: ignore[assignment]
|
|
55
54
|
|
|
56
|
-
app = typer.Typer(
|
|
57
|
-
help="Premium Dashboard enrollment and withdrawal.", add_completion=False
|
|
58
|
-
)
|
|
55
|
+
app = typer.Typer(help="Premium Dashboard connection management.", add_completion=False)
|
|
59
56
|
|
|
60
57
|
_OPENCOMPLAI_DIR = Path.home() / ".opencomplai"
|
|
61
58
|
_CONFIG_FILE = _OPENCOMPLAI_DIR / "config.yaml"
|
|
@@ -177,7 +174,7 @@ def _post(url: str, payload: dict, token: str | None = None) -> tuple[int, dict]
|
|
|
177
174
|
# ---------------------------------------------------------------------------
|
|
178
175
|
|
|
179
176
|
|
|
180
|
-
@app.command("enroll")
|
|
177
|
+
@app.command("enroll", hidden=True)
|
|
181
178
|
def enroll(
|
|
182
179
|
tenant: str = typer.Option(
|
|
183
180
|
..., "--tenant", help="Tenant ID from the dashboard signup"
|
|
@@ -192,10 +189,19 @@ def enroll(
|
|
|
192
189
|
),
|
|
193
190
|
) -> None:
|
|
194
191
|
"""
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
192
|
+
Operator path pending a UI to mint bootstrap tokens.
|
|
193
|
+
|
|
194
|
+
Enrolls this install in the Opencomplai Premium Dashboard by validating a
|
|
195
|
+
one-time bootstrap token, registering signing keys, writing EgressConsent
|
|
196
|
+
to the local ledger, and adding the dashboard endpoint to the egress
|
|
197
|
+
allowlist. Hidden from --help as of DG-11: nothing in this product issues
|
|
198
|
+
a bootstrap token outside a test today (admin-api's
|
|
199
|
+
``issue_bootstrap_token`` has zero non-test callers), so this command has
|
|
200
|
+
no working entry point for a real customer to reach. It is kept, not
|
|
201
|
+
deleted, for the operator who already has -- or will eventually get a
|
|
202
|
+
console to mint -- a token. Everyone else wants the self-serve path:
|
|
203
|
+
an API key from ``/connect`` plus ``opencomplai push`` /
|
|
204
|
+
``opencomplai docs generate --push``.
|
|
199
205
|
"""
|
|
200
206
|
cfg = _load_config()
|
|
201
207
|
install_id = cfg.get("install_id", "")
|
|
@@ -318,6 +324,41 @@ def enroll(
|
|
|
318
324
|
# ---------------------------------------------------------------------------
|
|
319
325
|
|
|
320
326
|
|
|
327
|
+
def _resolve_withdraw_token() -> str | None:
|
|
328
|
+
"""
|
|
329
|
+
Best-effort bearer token for admin-api's ``POST /v1/admin/withdraw``
|
|
330
|
+
(SEC-2's ``PrincipalDep`` requires a tenant-scoped service JWT), mirroring
|
|
331
|
+
the CI connectors' own auth precedence
|
|
332
|
+
(``connectors/github_actions.py``/``gitlab_ci.py``): an operator-set
|
|
333
|
+
legacy bearer-token env var wins outright; otherwise this install's own
|
|
334
|
+
OIDC client-credentials pair (persisted locally by ``enroll`` — see
|
|
335
|
+
module docstring) is exchanged for a short-lived JWT.
|
|
336
|
+
|
|
337
|
+
Returns ``None`` — never raises — when neither path produces a token.
|
|
338
|
+
That is the *ordinary* outcome for the vast majority of installs, which
|
|
339
|
+
never went through ``enroll`` (it is hidden and has no working entry
|
|
340
|
+
point today): callers must treat it as "there is nothing to
|
|
341
|
+
authenticate a remote withdraw with," not as an error.
|
|
342
|
+
"""
|
|
343
|
+
token = os.environ.get("OPENCOMPLAI_AUTH_TOKEN", "")
|
|
344
|
+
if token:
|
|
345
|
+
return token
|
|
346
|
+
|
|
347
|
+
client_id = _load_config().get("client_id", "")
|
|
348
|
+
client_secret = _read_client_secret() or ""
|
|
349
|
+
token_endpoint = os.environ.get("OPENCOMPLAI_TOKEN_ENDPOINT", "")
|
|
350
|
+
if not (client_id and client_secret and token_endpoint):
|
|
351
|
+
return None
|
|
352
|
+
|
|
353
|
+
from opencomplai_cli.oidc_client import OidcTokenError, acquire_token
|
|
354
|
+
|
|
355
|
+
try:
|
|
356
|
+
token, _ = acquire_token(token_endpoint, client_id, client_secret)
|
|
357
|
+
except OidcTokenError:
|
|
358
|
+
return None
|
|
359
|
+
return token
|
|
360
|
+
|
|
361
|
+
|
|
321
362
|
@app.command("withdraw")
|
|
322
363
|
def withdraw(
|
|
323
364
|
tenant: str = typer.Option(..., "--tenant", help="Tenant ID to withdraw from"),
|
|
@@ -326,11 +367,28 @@ def withdraw(
|
|
|
326
367
|
"--dashboard-url",
|
|
327
368
|
help="Dashboard base URL (defaults to OPENCOMPLAI_DASHBOARD_URL env var)",
|
|
328
369
|
),
|
|
370
|
+
local_only: bool = typer.Option(
|
|
371
|
+
False,
|
|
372
|
+
"--local-only",
|
|
373
|
+
help=(
|
|
374
|
+
"Clear local state only — never attempt the remote "
|
|
375
|
+
"/v1/admin/withdraw call, even if a token is available."
|
|
376
|
+
),
|
|
377
|
+
),
|
|
329
378
|
) -> None:
|
|
330
379
|
"""
|
|
331
380
|
Withdraw dashboard enrollment for this install.
|
|
332
381
|
|
|
333
|
-
|
|
382
|
+
Always clears local state: emits a consent_revoked ledger event and
|
|
383
|
+
removes the egress allowlist entry, in this same run. Also best-effort
|
|
384
|
+
notifies the dashboard, but only when a bearer token can actually be
|
|
385
|
+
produced for it (see _resolve_withdraw_token) — admin-api's
|
|
386
|
+
/v1/admin/withdraw requires an authenticated, tenant-scoped service JWT
|
|
387
|
+
(SEC-2), which most OSS installs have no way to mint. Sending the
|
|
388
|
+
request anyway would just guarantee a 401 that looks like a real
|
|
389
|
+
failure; skipping it prints a direct next step instead. A genuine
|
|
390
|
+
>=400 from the dashboard on a request that *did* carry a token is a
|
|
391
|
+
real rejection and still gets the loud error message.
|
|
334
392
|
"""
|
|
335
393
|
cfg = _load_config()
|
|
336
394
|
install_id = cfg.get("install_id", "")
|
|
@@ -344,37 +402,39 @@ def withdraw(
|
|
|
344
402
|
"OPENCOMPLAI_DASHBOARD_URL", ""
|
|
345
403
|
).rstrip("/")
|
|
346
404
|
|
|
347
|
-
# --- Notify dashboard if
|
|
405
|
+
# --- Notify dashboard, only if we can actually authenticate to it ---
|
|
348
406
|
# Best-effort and non-fatal by design (an offline install can still clear
|
|
349
|
-
# its own local state)
|
|
350
|
-
#
|
|
351
|
-
#
|
|
352
|
-
#
|
|
353
|
-
#
|
|
354
|
-
#
|
|
355
|
-
#
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
407
|
+
# its own local state). Three distinct outcomes below, each with its own
|
|
408
|
+
# message: (1) skipped because no token could be minted, or --local-only
|
|
409
|
+
# was passed, or no dashboard URL is configured at all -- the common
|
|
410
|
+
# case, not an error; (2) skipped because of an actual bug/note aside;
|
|
411
|
+
# (3) attempted with a real token and the dashboard rejected it (>=400)
|
|
412
|
+
# or was unreachable -- a genuine failure, kept loud so it isn't
|
|
413
|
+
# confused with (1)'s "there was never anything to try" case.
|
|
414
|
+
dashboard_notified = False
|
|
415
|
+
skipped_no_token = True
|
|
416
|
+
if base_url and not local_only:
|
|
417
|
+
token = _resolve_withdraw_token()
|
|
418
|
+
if token is not None:
|
|
419
|
+
skipped_no_token = False
|
|
420
|
+
try:
|
|
421
|
+
status, data = _post(
|
|
422
|
+
f"{base_url}/v1/admin/withdraw",
|
|
423
|
+
{"tenant_id": tenant, "install_id": install_id},
|
|
424
|
+
token=token,
|
|
425
|
+
)
|
|
426
|
+
if status >= 400:
|
|
427
|
+
_err_console.print(
|
|
428
|
+
f"[red]Error:[/red] dashboard rejected the withdraw request "
|
|
429
|
+
f"({status}: {data.get('message', data)}) — egress consent was "
|
|
430
|
+
"NOT revoked server-side. Clearing local state only."
|
|
431
|
+
)
|
|
432
|
+
else:
|
|
433
|
+
dashboard_notified = True
|
|
434
|
+
except ConnectionError as exc:
|
|
367
435
|
_err_console.print(
|
|
368
|
-
f"[
|
|
369
|
-
f"({status}: {data.get('message', data)}) — egress consent was "
|
|
370
|
-
"NOT revoked server-side. Clearing local state only."
|
|
436
|
+
f"[yellow]Warning:[/yellow] could not reach dashboard: {exc}"
|
|
371
437
|
)
|
|
372
|
-
else:
|
|
373
|
-
dashboard_notified = True
|
|
374
|
-
except ConnectionError as exc:
|
|
375
|
-
_err_console.print(
|
|
376
|
-
f"[yellow]Warning:[/yellow] could not reach dashboard: {exc}"
|
|
377
|
-
)
|
|
378
438
|
|
|
379
439
|
# --- Emit consent_revoked ledger event ---
|
|
380
440
|
from datetime import UTC, datetime
|
|
@@ -394,17 +454,26 @@ def withdraw(
|
|
|
394
454
|
removed = allowlist.pop(key, None)
|
|
395
455
|
_write_egress_allowlist(allowlist)
|
|
396
456
|
|
|
397
|
-
if
|
|
398
|
-
if
|
|
457
|
+
if dashboard_notified:
|
|
458
|
+
if removed:
|
|
399
459
|
_console.print(
|
|
400
460
|
f"[green]Withdrawal complete.[/green] Egress allowlist entry removed for tenant {tenant}."
|
|
401
461
|
)
|
|
402
462
|
else:
|
|
403
463
|
_console.print(
|
|
404
|
-
f"[
|
|
405
|
-
f"for tenant {tenant}, but the dashboard was not notified (see error above). "
|
|
406
|
-
"Server-side egress consent likely remains active."
|
|
464
|
+
f"[green]Withdrawal complete.[/green] No local egress allowlist entry was present for tenant {tenant}."
|
|
407
465
|
)
|
|
466
|
+
elif skipped_no_token:
|
|
467
|
+
_console.print(
|
|
468
|
+
"[green]Local state cleared.[/green] Remote egress consent must be "
|
|
469
|
+
"revoked from the dashboard (Projects → Archive project)."
|
|
470
|
+
)
|
|
471
|
+
elif removed:
|
|
472
|
+
_console.print(
|
|
473
|
+
f"[yellow]Local withdrawal complete[/yellow] — egress allowlist entry removed "
|
|
474
|
+
f"for tenant {tenant}, but the dashboard was not notified (see error above). "
|
|
475
|
+
"Server-side egress consent likely remains active."
|
|
476
|
+
)
|
|
408
477
|
else:
|
|
409
478
|
_console.print(
|
|
410
479
|
f"[dim]No egress allowlist entry found for tenant {tenant} — nothing to remove.[/dim]"
|
|
@@ -31,7 +31,9 @@ from rich.markup import escape
|
|
|
31
31
|
|
|
32
32
|
from opencomplai_cli.publish import (
|
|
33
33
|
envelope_signature,
|
|
34
|
+
prepare_dossier_envelope,
|
|
34
35
|
prepare_scan_status_artifact,
|
|
36
|
+
publish_dossier_envelope,
|
|
35
37
|
publish_scan_status,
|
|
36
38
|
)
|
|
37
39
|
|
|
@@ -150,4 +152,105 @@ def run_push(artifact_file: Path, *, env: dict[str, str] | None = None) -> int:
|
|
|
150
152
|
return EXIT_PUBLISH_FAILED
|
|
151
153
|
|
|
152
154
|
|
|
153
|
-
|
|
155
|
+
def run_push_dossier(dossier_file: Path, *, env: dict[str, str] | None = None) -> int:
|
|
156
|
+
"""
|
|
157
|
+
Read ``dossier_file`` (an ``AnnexIVDossier`` JSON -- e.g. the
|
|
158
|
+
``dossier_<id>.json`` ``opencomplai docs generate`` writes locally),
|
|
159
|
+
shape it into a ``dossier_envelope``, and POST it to the dashboard
|
|
160
|
+
(DG-10).
|
|
161
|
+
|
|
162
|
+
Transliterated from ``run_push`` above: same file/env validation, same
|
|
163
|
+
``http``-non-local warning, same auth model (``ock_`` API key, no
|
|
164
|
+
``install_id``), same ``(0 success / 3 otherwise)`` return contract.
|
|
165
|
+
Two differences beyond the obvious (path, mapper, transport function):
|
|
166
|
+
|
|
167
|
+
* The envelope's own ``signature`` is always ``""``. Unlike a
|
|
168
|
+
``ScanStatusArtifact``, an ``AnnexIVDossier``'s own ``signature``
|
|
169
|
+
field (when present) signs the *dossier bundle* under a scheme
|
|
170
|
+
unrelated to the ingest envelope's G-5 byte-identity check
|
|
171
|
+
(``envelope_signature``'s docstring) -- forwarding it would not
|
|
172
|
+
"sort of" verify, it would deterministically fail closed. A
|
|
173
|
+
key-authed push already attests via the API key itself, same as
|
|
174
|
+
scan status's own key-authed path (``dashboard_ingest.routes``).
|
|
175
|
+
* The error message on a missing file points at ``docs generate``
|
|
176
|
+
instead of ``check --sign``.
|
|
177
|
+
"""
|
|
178
|
+
_env = env if env is not None else os.environ
|
|
179
|
+
|
|
180
|
+
if not dossier_file.exists():
|
|
181
|
+
err_console.print(f"[red]Error:[/red] dossier file not found: {dossier_file}")
|
|
182
|
+
err_console.print(
|
|
183
|
+
"Run [bold]opencomplai docs generate[/bold] first to produce one."
|
|
184
|
+
)
|
|
185
|
+
return EXIT_PUBLISH_FAILED
|
|
186
|
+
|
|
187
|
+
try:
|
|
188
|
+
raw_text = dossier_file.read_text(encoding="utf-8")
|
|
189
|
+
except (OSError, UnicodeDecodeError) as exc:
|
|
190
|
+
err_console.print(f"[red]Error:[/red] could not read {dossier_file}: {exc}")
|
|
191
|
+
return EXIT_PUBLISH_FAILED
|
|
192
|
+
|
|
193
|
+
try:
|
|
194
|
+
dossier: Any = json.loads(raw_text)
|
|
195
|
+
except json.JSONDecodeError as exc:
|
|
196
|
+
err_console.print(f"[red]Error:[/red] {dossier_file} is not valid JSON: {exc}")
|
|
197
|
+
return EXIT_PUBLISH_FAILED
|
|
198
|
+
|
|
199
|
+
if not isinstance(dossier, dict):
|
|
200
|
+
err_console.print(
|
|
201
|
+
f"[red]Error:[/red] {dossier_file} must contain a JSON object "
|
|
202
|
+
"(an AnnexIVDossier), not a "
|
|
203
|
+
f"{type(dossier).__name__}"
|
|
204
|
+
)
|
|
205
|
+
return EXIT_PUBLISH_FAILED
|
|
206
|
+
|
|
207
|
+
api_key = _env.get("OPENCOMPLAI_API_KEY", "")
|
|
208
|
+
dashboard_url = _env.get("OPENCOMPLAI_DASHBOARD_URL", "").rstrip("/")
|
|
209
|
+
if not api_key or not dashboard_url:
|
|
210
|
+
err_console.print(
|
|
211
|
+
"[red]Error:[/red] OPENCOMPLAI_API_KEY and OPENCOMPLAI_DASHBOARD_URL "
|
|
212
|
+
"must both be set."
|
|
213
|
+
)
|
|
214
|
+
return EXIT_PUBLISH_FAILED
|
|
215
|
+
|
|
216
|
+
parsed_url = urlparse(dashboard_url)
|
|
217
|
+
if parsed_url.scheme == "http" and parsed_url.hostname not in _LOCAL_HOSTNAMES:
|
|
218
|
+
err_console.print(
|
|
219
|
+
f"[yellow]Warning:[/yellow] OPENCOMPLAI_DASHBOARD_URL ({dashboard_url}) "
|
|
220
|
+
"uses plain http -- the API key will be sent unencrypted over the network. "
|
|
221
|
+
"Use https unless this is local development."
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
prepared = prepare_dossier_envelope(
|
|
225
|
+
dossier, commit_env=_env, repo_dir=dossier_file.resolve().parent
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
envelope = {
|
|
229
|
+
"system_id": prepared.get("system_id", "unknown"),
|
|
230
|
+
"artifact": prepared,
|
|
231
|
+
"signature": "",
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
status, data = publish_dossier_envelope(dashboard_url, api_key, envelope)
|
|
235
|
+
|
|
236
|
+
if status in (200, 201):
|
|
237
|
+
outcome = escape(
|
|
238
|
+
str(data.get("outcome", "accepted" if status == 201 else "replayed"))
|
|
239
|
+
)
|
|
240
|
+
content_hash = escape(str(data.get("content_hash", "")))
|
|
241
|
+
console.print(f"[green]Push {outcome}.[/green]")
|
|
242
|
+
console.print(f" system_id: {envelope['system_id']}")
|
|
243
|
+
console.print(f" content_hash: {content_hash}")
|
|
244
|
+
console.print(f"\nDashboard: {dashboard_url}")
|
|
245
|
+
return EXIT_OK
|
|
246
|
+
|
|
247
|
+
err_console.print(f"[red]Push failed ({status}):[/red] {escape(str(data))}")
|
|
248
|
+
return EXIT_PUBLISH_FAILED
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
__all__ = [
|
|
252
|
+
"EXIT_OK",
|
|
253
|
+
"EXIT_PUBLISH_FAILED",
|
|
254
|
+
"run_push",
|
|
255
|
+
"run_push_dossier",
|
|
256
|
+
]
|