|
48 | 48 | from legis.governance.signoff_binding import bind_signoff_to_issue |
49 | 49 | from legis.identity.entity_key import EntityKey |
50 | 50 | from legis.identity.resolver import IdentityResolver |
51 | | -from legis.service.errors import AuditIntegrityError, InvalidArgumentError, NotEnabledError |
| 51 | +from legis.service.errors import ( |
| 52 | + AuditIntegrityError, |
| 53 | + InvalidArgumentError, |
| 54 | + NotEnabledError, |
| 55 | + WardlineRoutingError, |
| 56 | +) |
52 | 57 | from legis.service.governance import compute_override_rate as _compute_override_rate |
53 | 58 | from legis.service.governance import evaluate_policy as _evaluate_policy |
54 | 59 | from legis.service.governance import request_signoff as _request_signoff |
|
58 | 63 | from legis.service.governance import submit_override as _submit_override |
59 | 64 | from legis.service.governance import submit_protected_override as _submit_protected_override |
60 | 65 | from legis.service.governance import verified_records as _verified_records |
61 | | -from legis.service.wardline import route_wardline_scan as _route_wardline_scan |
| 66 | +from legis.service.wardline import ( |
| 67 | + resolve_scan_routing, |
| 68 | + route_wardline_scan as _route_wardline_scan, |
| 69 | +) |
62 | 70 | from legis.policy.grammar import PolicyGrammar, default_grammar |
63 | 71 | from legis.pulls.models import PullRequest, PullRequestState |
64 | 72 | from legis.pulls.surface import PullSurface |
|
67 | 75 | ScanOutcome, |
68 | 76 | WardlineDirtyTreeError, |
69 | 77 | WardlinePayloadError, |
70 | | - WardlineSeverity, |
71 | 78 | ) |
72 | 79 |
|
73 | 80 | security = HTTPBearer(auto_error=False) |
@@ -247,20 +254,13 @@ class CheckRunIn(BaseModel): |
247 | 254 | finished_at: str | None = None |
248 | 255 |
|
249 | 256 |
|
250 | | -def _parse_wardline_cell_map(raw: str) -> dict[WardlineSeverity, WardlineCellPolicy]: |
251 | | - mapping: dict[WardlineSeverity, WardlineCellPolicy] = {} |
252 | | - for part in raw.split(","): |
253 | | - if not part.strip(): |
254 | | - continue |
255 | | - severity_raw, sep, cell_raw = part.partition("=") |
256 | | - if not sep: |
257 | | - raise ValueError("cell map entries must be SEVERITY=cell") |
258 | | - mapping[WardlineSeverity[severity_raw.strip()]] = WardlineCellPolicy( |
259 | | - cell_raw.strip() |
260 | | - ) |
261 | | - if not mapping: |
262 | | - raise ValueError("cell map must not be empty") |
263 | | - return mapping |
| 257 | +# Wardline scan-routing rejections (raised by service.resolve_scan_routing) map |
| 258 | +# to HTTP status by kind; the MCP adapter collapses the same kinds to one code. |
| 259 | +_WARDLINE_ROUTING_STATUS = { |
| 260 | + WardlineRoutingError.SERVER_MISCONFIGURED: 500, |
| 261 | + WardlineRoutingError.SERVER_OWNED: 403, |
| 262 | + WardlineRoutingError.MALFORMED: 422, |
| 263 | +} |
264 | 264 |
|
265 | 265 |
|
266 | 266 | def _check_to_dict(run: CheckRun) -> dict: |
@@ -772,72 +772,37 @@ def policy_evaluate(body: PolicyEvalIn, actor: str = Depends(verify_writer)) -> |
772 | 772 |
|
773 | 773 | @app.post("/wardline/scan-results") |
774 | 774 | def wardline_scan_results(body: ScanResultsIn, actor: str = Depends(verify_writer)) -> dict: |
775 | | - server_cell = os.environ.get("LEGIS_WARDLINE_CELL") |
776 | | - server_cell_by_severity = os.environ.get("LEGIS_WARDLINE_CELL_BY_SEVERITY") |
777 | | - if server_cell and server_cell_by_severity: |
778 | | - raise HTTPException(status_code=500, detail="server Wardline routing is misconfigured") |
779 | | - server_routing = server_cell is not None or server_cell_by_severity is not None |
780 | | - if server_routing and ( |
781 | | - body.cell is not None or body.cell_by_severity is not None or body.fail_on is not None |
782 | | - ): |
783 | | - raise HTTPException(status_code=403, detail="Wardline routing is server-owned") |
784 | | - if not server_routing: |
785 | | - if os.environ.get("LEGIS_UNSAFE_WARDLINE_REQUEST_ROUTING") != "1": |
786 | | - raise HTTPException( |
787 | | - status_code=403, |
788 | | - detail="Wardline routing is server-owned; configure LEGIS_WARDLINE_CELL or LEGIS_WARDLINE_CELL_BY_SEVERITY", |
789 | | - ) |
790 | | - if body.fail_on is not None: |
791 | | - if body.cell is None or body.cell_by_severity is not None: |
792 | | - raise HTTPException( |
793 | | - status_code=422, |
794 | | - detail="fail_on routing requires cell and forbids cell_by_severity", |
795 | | - ) |
796 | | - elif (body.cell is None) == (body.cell_by_severity is None): |
797 | | - raise HTTPException(status_code=422, |
798 | | - detail="provide exactly one of cell or cell_by_severity") |
799 | | - if body.cell_by_severity is not None and not body.cell_by_severity: |
800 | | - raise HTTPException(status_code=422, detail="cell_by_severity must not be empty") |
801 | | - |
802 | | - policy: WardlineCellPolicy | None = None |
803 | | - cell_map: dict[WardlineSeverity, WardlineCellPolicy] | None = None |
804 | | - fail_on: WardlineSeverity | None = None |
805 | 775 | try: |
806 | | - if server_cell_by_severity is not None: |
807 | | - cell_map = _parse_wardline_cell_map(server_cell_by_severity) |
808 | | - cells = set(cell_map.values()) |
809 | | - elif server_cell is not None: |
810 | | - policy = WardlineCellPolicy(server_cell) |
811 | | - cells = {policy} |
812 | | - elif body.cell_by_severity is not None: |
813 | | - cell_map = {WardlineSeverity[sev]: WardlineCellPolicy(cell) |
814 | | - for sev, cell in body.cell_by_severity.items()} |
815 | | - cells = set(cell_map.values()) |
816 | | - else: |
817 | | - policy = WardlineCellPolicy(body.cell) |
818 | | - if body.fail_on is not None: |
819 | | - fail_on = WardlineSeverity[body.fail_on] |
820 | | - cells = {policy, WardlineCellPolicy.SURFACE_ONLY} |
821 | | - else: |
822 | | - cells = {policy} |
823 | | - except (KeyError, ValueError) as exc: |
824 | | - raise HTTPException(status_code=422, detail=f"unknown cell/severity: {exc}") |
| 776 | + routing = resolve_scan_routing( |
| 777 | + server_cell=os.environ.get("LEGIS_WARDLINE_CELL"), |
| 778 | + server_cell_by_severity=os.environ.get("LEGIS_WARDLINE_CELL_BY_SEVERITY"), |
| 779 | + request_cell=body.cell, |
| 780 | + request_severity_map=body.cell_by_severity, |
| 781 | + request_fail_on=body.fail_on, |
| 782 | + allow_request_routing=( |
| 783 | + os.environ.get("LEGIS_UNSAFE_WARDLINE_REQUEST_ROUTING") == "1" |
| 784 | + ), |
| 785 | + ) |
| 786 | + except WardlineRoutingError as exc: |
| 787 | + raise HTTPException( |
| 788 | + status_code=_WARDLINE_ROUTING_STATUS[exc.kind], detail=str(exc) |
| 789 | + ) from exc |
825 | 790 |
|
826 | 791 | # Only provision the governance store when a surface cell can actually run: |
827 | 792 | # engine() lazily creates .weft/legis/legis-governance.db, so a pure block_escalate scan |
828 | 793 | # must not touch it. signoff_gate is an injected param (no side effect). |
829 | | - needs_engine = bool(cells & {WardlineCellPolicy.SURFACE_OVERRIDE, |
830 | | - WardlineCellPolicy.SURFACE_ONLY}) |
| 794 | + needs_engine = bool(routing.cells & {WardlineCellPolicy.SURFACE_OVERRIDE, |
| 795 | + WardlineCellPolicy.SURFACE_ONLY}) |
831 | 796 | try: |
832 | 797 | routed = _route_wardline_scan( |
833 | 798 | body.scan, |
834 | 799 | agent_id=_recorded_actor(actor, body.agent_id), |
835 | 800 | identity=identity, |
836 | 801 | engine=engine() if needs_engine else None, |
837 | 802 | signoff=signoff_gate, |
838 | | - policy=policy, |
839 | | - cell_map=cell_map, |
840 | | - fail_on=fail_on, |
| 803 | + policy=routing.policy, |
| 804 | + cell_map=routing.cell_map, |
| 805 | + fail_on=routing.fail_on, |
841 | 806 | artifact_key=( |
842 | 807 | os.environ["LEGIS_WARDLINE_ARTIFACT_KEY"].encode("utf-8") |
843 | 808 | if os.environ.get("LEGIS_WARDLINE_ARTIFACT_KEY") |
|
0 commit comments