archrails-mcp 0.3.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.
Files changed (48) hide show
  1. archrails_mcp-0.3.0/PKG-INFO +8 -0
  2. archrails_mcp-0.3.0/README.md +299 -0
  3. archrails_mcp-0.3.0/archrails_mcp/__init__.py +1 -0
  4. archrails_mcp-0.3.0/archrails_mcp/_config.py +107 -0
  5. archrails_mcp-0.3.0/archrails_mcp/cli.py +274 -0
  6. archrails_mcp-0.3.0/archrails_mcp/cmd/__init__.py +0 -0
  7. archrails_mcp-0.3.0/archrails_mcp/cmd/arch.py +64 -0
  8. archrails_mcp-0.3.0/archrails_mcp/cmd/bootstrap.py +197 -0
  9. archrails_mcp-0.3.0/archrails_mcp/cmd/calm.py +201 -0
  10. archrails_mcp-0.3.0/archrails_mcp/cmd/check.py +203 -0
  11. archrails_mcp-0.3.0/archrails_mcp/cmd/demo.py +229 -0
  12. archrails_mcp-0.3.0/archrails_mcp/cmd/login.py +303 -0
  13. archrails_mcp-0.3.0/archrails_mcp/cmd/node_install.py +204 -0
  14. archrails_mcp-0.3.0/archrails_mcp/credentials.py +157 -0
  15. archrails_mcp-0.3.0/archrails_mcp/diff_reader.py +72 -0
  16. archrails_mcp-0.3.0/archrails_mcp/examples/sample-payments-platform/README.md +81 -0
  17. archrails_mcp-0.3.0/archrails_mcp/examples/sample-payments-platform/architecture/system.calm.json +91 -0
  18. archrails_mcp-0.3.0/archrails_mcp/examples/sample-payments-platform/archrails.yml +28 -0
  19. archrails_mcp-0.3.0/archrails_mcp/examples/sample-payments-platform/services/card-data-service/src/Client.java +22 -0
  20. archrails_mcp-0.3.0/archrails_mcp/examples/sample-payments-platform/services/payment-api/src/Client.java +6 -0
  21. archrails_mcp-0.3.0/archrails_mcp/git_remote.py +99 -0
  22. archrails_mcp-0.3.0/archrails_mcp/graph_loader.py +166 -0
  23. archrails_mcp-0.3.0/archrails_mcp/rpc_client.py +92 -0
  24. archrails_mcp-0.3.0/archrails_mcp/server.py +409 -0
  25. archrails_mcp-0.3.0/archrails_mcp/tools/__init__.py +14 -0
  26. archrails_mcp-0.3.0/archrails_mcp/tools/bind_path.py +215 -0
  27. archrails_mcp-0.3.0/archrails_mcp/tools/validate_architecture.py +51 -0
  28. archrails_mcp-0.3.0/archrails_mcp.egg-info/PKG-INFO +8 -0
  29. archrails_mcp-0.3.0/archrails_mcp.egg-info/SOURCES.txt +46 -0
  30. archrails_mcp-0.3.0/archrails_mcp.egg-info/dependency_links.txt +1 -0
  31. archrails_mcp-0.3.0/archrails_mcp.egg-info/entry_points.txt +2 -0
  32. archrails_mcp-0.3.0/archrails_mcp.egg-info/requires.txt +2 -0
  33. archrails_mcp-0.3.0/archrails_mcp.egg-info/top_level.txt +1 -0
  34. archrails_mcp-0.3.0/pyproject.toml +60 -0
  35. archrails_mcp-0.3.0/setup.cfg +4 -0
  36. archrails_mcp-0.3.0/tests/test_access_store_admin_sentinel.py +140 -0
  37. archrails_mcp-0.3.0/tests/test_db_boundary_service_name_host.py +220 -0
  38. archrails_mcp-0.3.0/tests/test_diff_walker.py +141 -0
  39. archrails_mcp-0.3.0/tests/test_federated_graph_enrichment.py +267 -0
  40. archrails_mcp-0.3.0/tests/test_federation_classify_contract_repo.py +222 -0
  41. archrails_mcp-0.3.0/tests/test_graph_filter_to_owned.py +145 -0
  42. archrails_mcp-0.3.0/tests/test_mcp_drift_parity.py +130 -0
  43. archrails_mcp-0.3.0/tests/test_mcp_pr_time_parity.py +305 -0
  44. archrails_mcp-0.3.0/tests/test_mcp_validator_coverage.py +109 -0
  45. archrails_mcp-0.3.0/tests/test_orphan_skips_federation_stubs.py +70 -0
  46. archrails_mcp-0.3.0/tests/test_pipeline_e2e.py +646 -0
  47. archrails_mcp-0.3.0/tests/test_publisher_line_range_propagation.py +90 -0
  48. archrails_mcp-0.3.0/tests/test_resilience_validators.py +201 -0
@@ -0,0 +1,8 @@
1
+ Metadata-Version: 2.4
2
+ Name: archrails-mcp
3
+ Version: 0.3.0
4
+ Summary: ArchRails MCP — thin local CLI that proxies the cloud handler. The cloud runs the validators; the CLI ships only transport, auth, git-diff capture, and architect-mode YAML editing.
5
+ Author: ArchRails
6
+ Requires-Python: >=3.10
7
+ Requires-Dist: mcp>=1.0.0
8
+ Requires-Dist: pyyaml
@@ -0,0 +1,299 @@
1
+ # ArchRails AWS Backend
2
+
3
+ Architecture governance for monorepos — enforces [FINOS CALM](https://calm.finos.org) specifications on every pull request using a deterministic rule engine combined with LLM-grounded explanations.
4
+
5
+ ---
6
+
7
+ ## What It Does
8
+
9
+ ArchRails hooks into your PR workflow. When a developer opens or updates a pull request it:
10
+
11
+ 1. Fetches the diff and the repo's CALM architecture specification
12
+ 2. Runs a suite of deterministic preflight checks against declared nodes, edges, interfaces, and controls
13
+ 3. Calls AWS Bedrock (Claude) for grounded, plain-language explanations of any violations
14
+ 4. Posts the findings as a PR review comment with inline annotations and a blast-radius summary
15
+ 5. Approves or requests changes based on the deterministic verdict
16
+
17
+ ---
18
+
19
+ ## Supported Providers
20
+
21
+ | Provider | Auth Method | Inline Comments |
22
+ |---|---|---|
23
+ | GitHub App | GitHub App installation (HMAC) | Yes |
24
+ | GitHub Actions CI | Bearer token via workflow | Yes |
25
+ | GitLab (SaaS + Self-Hosted) | Bearer token (Secrets Manager) | Summary only |
26
+
27
+ ---
28
+
29
+ ## Quick Start
30
+
31
+ ### Prerequisites
32
+
33
+ - AWS account with DynamoDB, S3, Secrets Manager, Bedrock, Step Functions, Lambda, and ECR access
34
+ - Python 3.11
35
+ - Docker (for Lambda container builds)
36
+
37
+ ### 1. Configure Secrets Manager
38
+
39
+ Create one secret per tenant with these keys:
40
+
41
+ ```json
42
+ {
43
+ "GITHUB_TOKEN": "<GitHub App installation token>",
44
+ "GITHUB_WEBHOOK_SECRET": "<webhook HMAC secret>",
45
+ "GITLAB_TOKEN": "<GitLab personal/group access token>",
46
+ "GITLAB_WEBHOOK_SECRET": "<GitLab webhook secret token>"
47
+ }
48
+ ```
49
+
50
+ Secret ID pattern: `{PROVIDER_SECRET_ID_PREFIX}{tenant_pk}`
51
+
52
+ ### 2. Deploy DynamoDB Tables
53
+
54
+ Create the following tables (see [Technical Design](TECH_DESIGN.md) for full schemas):
55
+
56
+ | Table | PK | SK |
57
+ |---|---|---|
58
+ | `ArchRailsTenants` | `tenant_pk` | — |
59
+ | `ArchRailsTenantsProvisioning` | `tenant_pk` | — |
60
+ | `ArchRailsRepoLatestConfig` | `tenant_pk` | `repo_sk` |
61
+ | `ArchRailsPRValidations` | `tenant_pk` | `repo_sk#{pr_number}` |
62
+ | `ArchRailsMergedPRs` | `tenant_pk` | `repo_sk#{pr_number}` |
63
+ | `ArchRailsNodeHistory` | `tenant_pk` | `node_id#{created_at}` |
64
+ | `ArchRailsAccess` | `tenant_pk` | `repo_sk` |
65
+ | `ArchRailsAuditResults` | `tenant_pk` | `repo_sk#{timestamp}` |
66
+
67
+ ### 3. Build and Push Lambda Images
68
+
69
+ Each Lambda is packaged as a Docker container pushed to ECR:
70
+
71
+ ```bash
72
+ ACCOUNT=353468317406
73
+ REGION=us-east-2
74
+
75
+ for lambda in webhook_router fetch_pr_diff fetch_archrails_config \
76
+ bootstrap_governance process_calm_config validate_architecture \
77
+ publish_review_result update_pr_status run_full_audit; do
78
+ docker buildx build --platform linux/amd64 \
79
+ -t ${ACCOUNT}.dkr.ecr.${REGION}.amazonaws.com/${lambda}:latest \
80
+ ./${lambda}
81
+ docker push ${ACCOUNT}.dkr.ecr.${REGION}.amazonaws.com/${lambda}:latest
82
+ done
83
+ ```
84
+
85
+ ### 4. Deploy Step Functions State Machine
86
+
87
+ Deploy `stepfunction.json` as an Express Workflow with the Lambda ARNs substituted in.
88
+
89
+ ### 5. Add `.archrails/config.yml` to Your Repo
90
+
91
+ Open a PR with a config file — ArchRails will auto-generate the initial configuration:
92
+
93
+ ```yaml
94
+ version: 1
95
+ governance:
96
+ sources:
97
+ - id: api
98
+ file: services/api/architecture.json
99
+ provider: calm
100
+ owned_by: platform-team
101
+ mapping:
102
+ - path: "services/api/**"
103
+ node: api
104
+ - path: "services/payments/**"
105
+ node: payments
106
+ review:
107
+ enforce_governance: true
108
+ comment_mode: review
109
+ ```
110
+
111
+ ---
112
+
113
+ ## Architecture Overview
114
+
115
+ ```
116
+ GitHub / GitLab Webhook
117
+
118
+
119
+ WebhookRouter Lambda
120
+ (verify signature, normalize event)
121
+
122
+
123
+ AWS Step Functions
124
+
125
+ ┌──────▼──────┐
126
+ │FetchPRDiff │ → fetch diff, file list, detect config change
127
+ └──────┬──────┘
128
+ │ archrails_config_in_pr?
129
+ ├─ yes ──────────────────────────────────────┐
130
+ │ ▼
131
+ │ FetchArchRailsConfig
132
+ │ (fetch CALM sources,
133
+ │ check bootstrap state)
134
+ │ │
135
+ │ bootstrapped = false?
136
+ │ ├─ yes → BootstrapGovernance
137
+ │ │ (scan repo, infer
138
+ │ │ node→path bindings,
139
+ │ │ generate config.yml,
140
+ │ │ commit to PR branch)
141
+ │ │
142
+ │ └─ no → CalmParserLambda
143
+ │ (parse + merge CALM,
144
+ │ write to S3 staging)
145
+ │ │
146
+ └────────────────────────────────────────────┘
147
+
148
+ ┌───────▼────────┐
149
+ │ValidateArch │
150
+ │ · 8 preflight │
151
+ │ checks │
152
+ │ · Bedrock LLM │
153
+ │ · blast radius │
154
+ └───────┬────────┘
155
+
156
+ ┌───────▼────────┐
157
+ │PublishReview │
158
+ │ · PR comment │
159
+ │ · inline notes │
160
+ │ · APPROVE / │
161
+ │ REQUEST CHGS │
162
+ └────────────────┘
163
+
164
+ On PR merge / close:
165
+ UpdatePRStatus Lambda
166
+ (promote staging → live S3, archive record, activate tenant)
167
+ ```
168
+
169
+ ---
170
+
171
+ ## Validation Rules
172
+
173
+ ArchRails enforces 19 deterministic rules across 6 categories:
174
+
175
+ ### Diff vs Baseline (`AR-DIFF-*`)
176
+
177
+ | Rule | Severity | Trigger |
178
+ |---|---|---|
179
+ | AR-DIFF-001 | error | Diff calls a host:port not declared in CALM |
180
+ | AR-DIFF-002 | error | Diff uses a port not matching the declared interface |
181
+ | AR-DIFF-003 | error | Diff introduces a relationship not declared in CALM |
182
+ | AR-DIFF-004 | error | Direct DB access from code without a declared connection |
183
+ | AR-DIFF-005 | error | Source node not in CALM graph |
184
+ | AR-DIFF-007 | error | All files for a CALM node are deleted but node remains |
185
+ | AR-DIFF-008 | error | Hardcoded secret or high-entropy token detected in diff |
186
+
187
+ ### CALM Model Compliance (`AR-NODE-*`, `AR-REL-*`)
188
+
189
+ | Rule | Severity | Trigger |
190
+ |---|---|---|
191
+ | AR-NODE-001 | warning | Node missing `data-classification` field |
192
+ | AR-NODE-002 | warning | Node missing `controls` declaration |
193
+ | AR-REL-001 | error | HTTPS declared but diff shows HTTP call |
194
+ | AR-REL-002 | warning | Relationship missing `controls` declaration |
195
+ | AR-REL-003 | warning | Declared CALM relationship has no diff evidence |
196
+ | AR-REL-004 | error | SQL/DB access but relationship type is not `connects` |
197
+
198
+ ### Controls (`AR-CTRL-*`)
199
+
200
+ | Rule | Severity | Trigger |
201
+ |---|---|---|
202
+ | AR-CTRL-001 | error | Control config violates requirement JSON schema |
203
+ | AR-CTRL-002 | warning | Control requirement URL unreachable |
204
+ | AR-CTRL-003 | warning | Control requirement has no URLs to validate |
205
+
206
+ ### Import Boundaries (`AR-IMPORT-*`)
207
+
208
+ | Rule | Severity | Trigger |
209
+ |---|---|---|
210
+ | AR-IMPORT-001 | error | Import explicitly forbidden by CALM constraint |
211
+ | AR-IMPORT-002 | error | Cross-boundary import not declared in CALM graph |
212
+ | AR-IMPORT-003 | warning | Enforced import relationship not evidenced in diff |
213
+
214
+ Supports 10 languages: Kotlin/Java, Python, TypeScript/JavaScript, Go, Swift, Ruby, C/C++, Rust, C#, Dart.
215
+
216
+ ---
217
+
218
+ ## Repository Structure
219
+
220
+ ```
221
+ aws-backend/
222
+ ├── webhook_router/ # Entrypoint: normalize + dispatch webhooks
223
+ ├── fetch_pr_diff/ # Fetch PR diff and file list from SCM
224
+ ├── fetch_archrails_config/ # Fetch .archrails/config.yml, resolve bootstrap state
225
+ ├── bootstrap_governance/ # Auto-generate config.yml for new tenants
226
+ ├── process_calm_config/ # Parse + merge CALM, write to S3
227
+ ├── validate_architecture/ # Preflight checks + Bedrock LLM validation
228
+ │ ├── core/
229
+ │ │ ├── pattern.py # Path glob → node mapping
230
+ │ │ ├── control_validator.py # AR-CTRL-* rules
231
+ │ │ ├── relationship_diff_validator.py # AR-REL-003, AR-REL-004
232
+ │ │ ├── preflight_imports.py # AR-IMPORT-* rules (CALM-driven)
233
+ │ │ ├── forbidding_imports.py # AR-IMPORT-* rules (package-index-driven)
234
+ │ │ ├── driver_patterns.py # DB/queue/HTTP driver detection
235
+ │ │ └── secret_scanner.py # AR-DIFF-008 secret detection
236
+ │ ├── use_cases/
237
+ │ │ ├── system_prompt_use_case.py # Bedrock system prompt builder
238
+ │ │ ├── user_prompt_use_case.py # Bedrock user prompt builder
239
+ │ │ └── validation_use_case.py # Bedrock orchestration + fallback
240
+ │ └── test/ # 210 unit tests
241
+ ├── publish_review_result/ # Post PR comment + inline annotations
242
+ ├── update_pr_status/ # Handle merge/close, promote S3 staging → live
243
+ ├── run_full_audit/ # On-demand repo coverage audit (dashboard API)
244
+ └── shared/ # Shared modules across all Lambdas
245
+ ├── aws/
246
+ │ ├── dynamo/ # DynamoDB helpers + record stores
247
+ │ ├── s3/ # S3 read/write helpers
248
+ │ └── secrets/ # Secrets Manager token resolution
249
+ ├── calm/
250
+ │ ├── archrails_config.py # .archrails/config.yml parser + model
251
+ │ ├── mono/ # Monorepo merge logic
252
+ │ └── shared/ # CALM validators, errors, parsers
253
+ ├── http/ # HTTP response helpers
254
+ ├── exception/ # Standardized error responses
255
+ ├── identifiers.py # tenant_pk / repo_sk builders
256
+ └── storage_keys.py # S3 path builders
257
+ ```
258
+
259
+ ---
260
+
261
+ ## Running Tests
262
+
263
+ ```bash
264
+ # From repo root
265
+ python3.11 -m pytest validate_architecture/test/test_calm_validator_edge_cases.py # 65 tests
266
+ python3.11 -m pytest validate_architecture/test/test_calm_enforcer_edge_cases.py # 145 tests
267
+
268
+ # Run both (validator file must come first)
269
+ python3.11 -m pytest \
270
+ validate_architecture/test/test_calm_validator_edge_cases.py \
271
+ validate_architecture/test/test_calm_enforcer_edge_cases.py
272
+ ```
273
+
274
+ ---
275
+
276
+ ## Environment Variables
277
+
278
+ | Variable | Lambda(s) | Description |
279
+ |---|---|---|
280
+ | `GITHUB_WEBHOOK_SECRET_ID` | WebhookRouter | Secrets Manager prefix for GitHub webhook secrets |
281
+ | `GITLAB_WEBHOOK_SECRET_ID` | WebhookRouter, FetchPRDiff | Secrets Manager prefix for GitLab secrets |
282
+ | `GITHUB_CI_WEBHOOK_SECRET_ID` | WebhookRouter | Secrets Manager prefix for GitHub CI secrets |
283
+ | `GITHUB_TOKEN_SECRET_NAME` | FetchPRDiff, FetchArchRailsConfig | Secret key name for GitHub token |
284
+ | `GITLAB_TOKEN_SECRET_NAME` | FetchPRDiff, FetchArchRailsConfig | Secret key name for GitLab token |
285
+ | `GITLAB_TOKEN_BEARER` | FetchPRDiff | If `"true"`, use Bearer auth instead of PRIVATE-TOKEN |
286
+ | `ARCHRAILS_REPO_LATEST_CONFIG` | BootstrapGovernance, FetchArchRailsConfig | DynamoDB table for repo configs |
287
+ | `ARCHRAILS_TABLE` | CalmParser, ValidateArch | DynamoDB table for live tenant data |
288
+ | `PR_VALIDATION_TABLE` | ValidateArch, UpdatePRStatus | DynamoDB table for PR validation records |
289
+ | `TENANT_PROVISIONING_TABLE` | PublishReview | DynamoDB table for tenant provisioning |
290
+ | `INLINE_MIN_SEVERITY` | PublishReview | Minimum severity for inline comments (default: `minor`) |
291
+ | `MAX_INLINE_COMMENTS` | PublishReview | Max inline comments per PR (default: `50`) |
292
+ | `USE_REVIEW_COMMENTS_API` | PublishReview | If `"true"`, batch inline+summary into single review |
293
+ | `SCHEMA_VERSION` | ValidateArch | DynamoDB record schema version |
294
+
295
+ ---
296
+
297
+ ## License
298
+
299
+ See [LICENSE](LICENSE).
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,107 @@
1
+ """Minimal archrails.yml schema + loader, inlined into the local CLI.
2
+
3
+ The full config schema lives in `shared.calm.archrails_config` (server-
4
+ side), but we don't ship `shared/` in the CLI wheel — it would leak
5
+ validators, audit logic, and federation code. Architect-mode local
6
+ tools (`bind_path`, `validate_architecture`) need *just enough* of the
7
+ schema to read sources + mappings out of archrails.yml; that subset
8
+ lives here.
9
+
10
+ Kept tiny on purpose. If a field isn't accessed by `bind_path`,
11
+ `validate_architecture`, or the local `graph_loader.load_graph`, it's
12
+ not modelled here. Adding a field is cheap; bloating this with PR-time
13
+ config knobs reverses the IP boundary we set up.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ from dataclasses import dataclass, field
18
+ from typing import Any
19
+
20
+ import yaml
21
+
22
+
23
+ class ArchrailsConfigParseError(Exception):
24
+ """YAML syntax error or top-level shape error in archrails.yml."""
25
+
26
+
27
+ @dataclass
28
+ class MappingRule:
29
+ path: str
30
+ node: str
31
+
32
+
33
+ @dataclass
34
+ class MonorepoSource:
35
+ """One entry in governance.sources[] — maps to one CALM file."""
36
+ id: str
37
+ file: str
38
+ paths: list[str] = field(default_factory=list)
39
+
40
+
41
+ @dataclass
42
+ class MonorepoGovernanceConfig:
43
+ provider: str = "calm"
44
+ sources: list[MonorepoSource] = field(default_factory=list)
45
+
46
+
47
+ @dataclass
48
+ class MonorepoArchrailsConfig:
49
+ """Trimmed schema covering only the fields local architect-mode tools touch."""
50
+ version: int = 1
51
+ governance: MonorepoGovernanceConfig = field(default_factory=MonorepoGovernanceConfig)
52
+ mapping: list[MappingRule] = field(default_factory=list)
53
+
54
+
55
+ def load_archrails_config(raw_yaml: str) -> MonorepoArchrailsConfig:
56
+ """Parse archrails.yml into the trimmed MonorepoArchrailsConfig.
57
+
58
+ Raises:
59
+ ArchrailsConfigParseError: YAML invalid or top-level not a mapping.
60
+ """
61
+ if not raw_yaml or not raw_yaml.strip():
62
+ raise ArchrailsConfigParseError("archrails.yml is empty")
63
+
64
+ try:
65
+ data = yaml.safe_load(raw_yaml)
66
+ except yaml.YAMLError as exc:
67
+ mark = getattr(exc, "problem_mark", None)
68
+ location = f" (line {mark.line + 1}, col {mark.column + 1})" if mark else ""
69
+ problem = getattr(exc, "problem", str(exc))
70
+ raise ArchrailsConfigParseError(
71
+ f"Invalid YAML in archrails.yml{location}: {problem}"
72
+ ) from exc
73
+
74
+ if not isinstance(data, dict):
75
+ raise ArchrailsConfigParseError(
76
+ "archrails.yml must be a YAML mapping at the top level"
77
+ )
78
+
79
+ gov_raw = data.get("governance") or {}
80
+ sources_raw = gov_raw.get("sources") or []
81
+ sources: list[MonorepoSource] = []
82
+ for s in sources_raw if isinstance(sources_raw, list) else []:
83
+ if not isinstance(s, dict):
84
+ continue
85
+ src_id = s.get("id") or ""
86
+ src_file = s.get("file") or ""
87
+ if not src_id or not src_file:
88
+ continue
89
+ sources.append(MonorepoSource(
90
+ id=src_id,
91
+ file=src_file,
92
+ paths=s.get("paths") or [],
93
+ ))
94
+
95
+ mapping: list[MappingRule] = []
96
+ for entry in (data.get("mapping") or []):
97
+ if isinstance(entry, dict) and "path" in entry and "node" in entry:
98
+ mapping.append(MappingRule(path=entry["path"], node=entry["node"]))
99
+
100
+ return MonorepoArchrailsConfig(
101
+ version=int(data.get("version", 1)),
102
+ governance=MonorepoGovernanceConfig(
103
+ provider=gov_raw.get("provider", "calm"),
104
+ sources=sources,
105
+ ),
106
+ mapping=mapping,
107
+ )
@@ -0,0 +1,274 @@
1
+ """archrails CLI entry point.
2
+
3
+ Usage:
4
+ archrails bootstrap # one-time setup for a new dev
5
+ archrails login # cache credentials for cloud MCP
6
+ archrails logout # clear cached credentials
7
+ archrails mcp start --repo /path/to/repo # default code-mode server
8
+ archrails mcp start --repo . --mode architect # architect mode (CALM-editing)
9
+ archrails arch init # drop FINOS Claude skill
10
+ archrails calm <subcommand> # wraps FINOS CALM CLI
11
+ archrails calm install # install pinned FINOS CALM CLI
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import asyncio
17
+ import logging
18
+ import sys
19
+ from pathlib import Path
20
+
21
+ # CRITICAL for MCP stdio: stdout is reserved for JSON-RPC. shared.logger
22
+ # (and several other libraries) default to StreamHandler(sys.stdout),
23
+ # which would corrupt the MCP framing. Patch StreamHandler so anything
24
+ # that would target sys.stdout — or that defaults — lands on sys.stderr.
25
+ # This must run before any module that creates loggers is imported.
26
+ _orig_stream_handler_init = logging.StreamHandler.__init__
27
+
28
+
29
+ def _stderr_default_init(self, stream=None): # type: ignore[no-untyped-def]
30
+ if stream is None or stream is sys.stdout:
31
+ stream = sys.stderr
32
+ return _orig_stream_handler_init(self, stream)
33
+
34
+
35
+ logging.StreamHandler.__init__ = _stderr_default_init # type: ignore[assignment]
36
+
37
+
38
+ def _build_parser() -> argparse.ArgumentParser:
39
+ parser = argparse.ArgumentParser(
40
+ prog="archrails",
41
+ description="ArchRails CLI — MCP server, FINOS CALM tooling, and developer auth.",
42
+ )
43
+ sub = parser.add_subparsers(dest="cmd", required=True)
44
+
45
+ # bootstrap
46
+ boot = sub.add_parser(
47
+ "bootstrap",
48
+ help="One-time setup: install CALM CLI, sign in, wire IDE MCP config.",
49
+ )
50
+ boot.add_argument("--dashboard", default=None, help="Dashboard URL (default: env or built-in dev)")
51
+ boot.add_argument("--api-base", default=None, help="API Gateway base URL")
52
+ boot.add_argument("--mcp-url", default=None, help="Cloud MCP Function URL")
53
+ boot.add_argument("--force", action="store_true", help="Replace existing credentials")
54
+ boot.add_argument("--skip-login", action="store_true", help="Skip the sign-in step")
55
+ boot.add_argument(
56
+ "--no-calm-required",
57
+ action="store_true",
58
+ help="Continue bootstrap even if Node.js / CALM CLI install fails",
59
+ )
60
+
61
+ # login / logout
62
+ login = sub.add_parser("login", help="Sign in via the dashboard and cache credentials.")
63
+ login.add_argument("--dashboard", default=None)
64
+ login.add_argument("--api-base", default=None)
65
+ login.add_argument("--mcp-url", default=None)
66
+ login.add_argument(
67
+ "--paste",
68
+ action="store_true",
69
+ help="Use the manual paste flow instead of the browser callback.",
70
+ )
71
+ sub.add_parser("logout", help="Remove cached credentials.")
72
+
73
+ # demo — copies the sample-payments-platform to the user's machine
74
+ # and prints a refusal walk-through. No auth required.
75
+ demo = sub.add_parser(
76
+ "demo",
77
+ help=(
78
+ "Copy a tiny sample CALM architecture to your machine and "
79
+ "walk through what ArchRails does when an agent proposes a "
80
+ "forbidden change. No login required — kick the tires "
81
+ "before setting up your own repo."
82
+ ),
83
+ )
84
+ demo.add_argument(
85
+ "--dir",
86
+ default=None,
87
+ help="Where to copy the sample (default: ~/archrails-demo).",
88
+ )
89
+ demo.add_argument(
90
+ "--force",
91
+ action="store_true",
92
+ help="Overwrite the destination if it already exists.",
93
+ )
94
+
95
+ # check — direct CLI verb for "validate my local diff"
96
+ check = sub.add_parser(
97
+ "check",
98
+ help=(
99
+ "Run the ArchRails validator suite against your local git diff. "
100
+ "Same brain as the merge gate — a clean response here means "
101
+ "the PR would pass; a blocked verdict means the merge will block."
102
+ ),
103
+ )
104
+ check.add_argument(
105
+ "--repo",
106
+ default=".",
107
+ help="Path to the workspace (default: current dir).",
108
+ )
109
+ check.add_argument(
110
+ "--against",
111
+ default="HEAD",
112
+ help=(
113
+ "Git ref to diff against. Default 'HEAD' captures uncommitted + "
114
+ "staged work. Use 'main' to compare working-tree vs main, or "
115
+ "'main..HEAD' for branch-only changes (closest to 'what would "
116
+ "land in a PR opened against main')."
117
+ ),
118
+ )
119
+ check.add_argument(
120
+ "--json",
121
+ action="store_true",
122
+ help=(
123
+ "Emit the full server response as JSON instead of a human-"
124
+ "readable report. Exit code is 0 iff allowed=true."
125
+ ),
126
+ )
127
+ check.add_argument(
128
+ "paths",
129
+ nargs="*",
130
+ help=(
131
+ "Optional git pathspecs to scope the diff (e.g. "
132
+ "'services/payment/**'). Useful for focusing on the file(s) "
133
+ "you just edited."
134
+ ),
135
+ )
136
+
137
+ # mcp start
138
+ mcp = sub.add_parser("mcp", help="MCP server commands.")
139
+ mcp_sub = mcp.add_subparsers(dest="mcp_cmd", required=True)
140
+ start = mcp_sub.add_parser("start", help="Run the MCP stdio server.")
141
+ start.add_argument(
142
+ "--repo",
143
+ default=".",
144
+ help="Path to the workspace repo (default: current directory).",
145
+ )
146
+ start.add_argument(
147
+ "--mode",
148
+ choices=["code", "architect"],
149
+ default="code",
150
+ help=(
151
+ "Server mode. `code` (default) — agent writes code, architecture "
152
+ "files are read-only. `architect` — agent may edit CALM files / "
153
+ "archrails.yml; mutation tools (validate_architecture, bind_path) "
154
+ "are exposed and the read-only prohibition is lifted. The two "
155
+ "modes are mutually exclusive — never run both in one session."
156
+ ),
157
+ )
158
+ start.add_argument(
159
+ "--repo-url",
160
+ default=None,
161
+ help=(
162
+ "Override the repo URL the cloud uses to pick a graph. By default "
163
+ "we read it from `git remote get-url origin` on --repo. Use this "
164
+ "when auto-detection picks the wrong remote (forks, mirrors, "
165
+ "multi-remote setups), when --repo doesn't have an `origin` "
166
+ "remote, or when you want to pin the URL in your IDE's MCP "
167
+ "config so it's immune to which folder Cursor inherits at "
168
+ "launch. Also overridable via the ARCHRAILS_REPO_URL env var."
169
+ ),
170
+ )
171
+
172
+ # arch — architecture-authoring helpers (architect-mode adjacent).
173
+ arch = sub.add_parser(
174
+ "arch",
175
+ help="Architecture authoring helpers (drops the FINOS Claude skill, etc.).",
176
+ )
177
+ arch_sub = arch.add_subparsers(dest="arch_cmd", required=True)
178
+ arch_init = arch_sub.add_parser(
179
+ "init",
180
+ help=(
181
+ "Drop the FINOS CALM Claude skill into this repo "
182
+ "(.claude/skills/calm/). Wraps `calm init-ai --provider claude`."
183
+ ),
184
+ )
185
+ arch_init.add_argument(
186
+ "--directory",
187
+ default=".",
188
+ help="Target directory (default: current working directory).",
189
+ )
190
+
191
+ # calm passthrough — argparse REMAINDER lets us forward arbitrary args
192
+ calm = sub.add_parser(
193
+ "calm",
194
+ help="Wraps the FINOS CALM CLI. `archrails calm install` installs it.",
195
+ )
196
+ calm.add_argument("calm_args", nargs=argparse.REMAINDER)
197
+
198
+ return parser
199
+
200
+
201
+ def main() -> int:
202
+ # Special-case `archrails calm ...` so EVERYTHING after `calm` is
203
+ # passed through to the FINOS CALM CLI — including bare flags like
204
+ # `--version` that argparse would otherwise intercept.
205
+ if len(sys.argv) >= 2 and sys.argv[1] == "calm":
206
+ from archrails_mcp.cmd.calm import cmd_calm
207
+ return cmd_calm(sys.argv[2:])
208
+
209
+ parser = _build_parser()
210
+ args = parser.parse_args()
211
+
212
+ if args.cmd == "mcp" and args.mcp_cmd == "start":
213
+ repo_path = Path(args.repo).resolve()
214
+ if not repo_path.is_dir():
215
+ print(f"[archrails] --repo not a directory: {repo_path}", file=sys.stderr)
216
+ return 2
217
+ # --repo-url override: explicit flag wins, then env var, then None
218
+ # (let server.py auto-detect via git remote).
219
+ import os as _os
220
+ repo_url_override = args.repo_url or _os.environ.get("ARCHRAILS_REPO_URL") or None
221
+ # Gate: unprovisioned machines cannot run the local MCP server.
222
+ # See archrails_mcp.credentials.require_credentials for the env-var
223
+ # escape hatch used by our own CI.
224
+ from archrails_mcp import credentials
225
+ try:
226
+ credentials.require_credentials()
227
+ except credentials.NotProvisioned as e:
228
+ print(str(e), file=sys.stderr)
229
+ return 2
230
+ from archrails_mcp.server import main_async
231
+ try:
232
+ asyncio.run(main_async(
233
+ repo_path,
234
+ mode=args.mode,
235
+ repo_url_override=repo_url_override,
236
+ ))
237
+ except KeyboardInterrupt:
238
+ return 0
239
+ return 0
240
+
241
+ if args.cmd == "arch" and args.arch_cmd == "init":
242
+ from archrails_mcp.cmd.arch import cmd_arch_init
243
+ return cmd_arch_init(args)
244
+
245
+ if args.cmd == "calm":
246
+ from archrails_mcp.cmd.calm import cmd_calm
247
+ return cmd_calm(args.calm_args or [])
248
+
249
+ if args.cmd == "login":
250
+ from archrails_mcp.cmd.login import cmd_login
251
+ return cmd_login(args)
252
+
253
+ if args.cmd == "logout":
254
+ from archrails_mcp.cmd.login import cmd_logout
255
+ return cmd_logout(args)
256
+
257
+ if args.cmd == "bootstrap":
258
+ from archrails_mcp.cmd.bootstrap import cmd_bootstrap
259
+ return cmd_bootstrap(args)
260
+
261
+ if args.cmd == "check":
262
+ from archrails_mcp.cmd.check import cmd_check
263
+ return cmd_check(args)
264
+
265
+ if args.cmd == "demo":
266
+ from archrails_mcp.cmd.demo import cmd_demo
267
+ return cmd_demo(args)
268
+
269
+ parser.print_help()
270
+ return 1
271
+
272
+
273
+ if __name__ == "__main__":
274
+ sys.exit(main())
File without changes