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.
- archrails_mcp-0.3.0/PKG-INFO +8 -0
- archrails_mcp-0.3.0/README.md +299 -0
- archrails_mcp-0.3.0/archrails_mcp/__init__.py +1 -0
- archrails_mcp-0.3.0/archrails_mcp/_config.py +107 -0
- archrails_mcp-0.3.0/archrails_mcp/cli.py +274 -0
- archrails_mcp-0.3.0/archrails_mcp/cmd/__init__.py +0 -0
- archrails_mcp-0.3.0/archrails_mcp/cmd/arch.py +64 -0
- archrails_mcp-0.3.0/archrails_mcp/cmd/bootstrap.py +197 -0
- archrails_mcp-0.3.0/archrails_mcp/cmd/calm.py +201 -0
- archrails_mcp-0.3.0/archrails_mcp/cmd/check.py +203 -0
- archrails_mcp-0.3.0/archrails_mcp/cmd/demo.py +229 -0
- archrails_mcp-0.3.0/archrails_mcp/cmd/login.py +303 -0
- archrails_mcp-0.3.0/archrails_mcp/cmd/node_install.py +204 -0
- archrails_mcp-0.3.0/archrails_mcp/credentials.py +157 -0
- archrails_mcp-0.3.0/archrails_mcp/diff_reader.py +72 -0
- archrails_mcp-0.3.0/archrails_mcp/examples/sample-payments-platform/README.md +81 -0
- archrails_mcp-0.3.0/archrails_mcp/examples/sample-payments-platform/architecture/system.calm.json +91 -0
- archrails_mcp-0.3.0/archrails_mcp/examples/sample-payments-platform/archrails.yml +28 -0
- archrails_mcp-0.3.0/archrails_mcp/examples/sample-payments-platform/services/card-data-service/src/Client.java +22 -0
- archrails_mcp-0.3.0/archrails_mcp/examples/sample-payments-platform/services/payment-api/src/Client.java +6 -0
- archrails_mcp-0.3.0/archrails_mcp/git_remote.py +99 -0
- archrails_mcp-0.3.0/archrails_mcp/graph_loader.py +166 -0
- archrails_mcp-0.3.0/archrails_mcp/rpc_client.py +92 -0
- archrails_mcp-0.3.0/archrails_mcp/server.py +409 -0
- archrails_mcp-0.3.0/archrails_mcp/tools/__init__.py +14 -0
- archrails_mcp-0.3.0/archrails_mcp/tools/bind_path.py +215 -0
- archrails_mcp-0.3.0/archrails_mcp/tools/validate_architecture.py +51 -0
- archrails_mcp-0.3.0/archrails_mcp.egg-info/PKG-INFO +8 -0
- archrails_mcp-0.3.0/archrails_mcp.egg-info/SOURCES.txt +46 -0
- archrails_mcp-0.3.0/archrails_mcp.egg-info/dependency_links.txt +1 -0
- archrails_mcp-0.3.0/archrails_mcp.egg-info/entry_points.txt +2 -0
- archrails_mcp-0.3.0/archrails_mcp.egg-info/requires.txt +2 -0
- archrails_mcp-0.3.0/archrails_mcp.egg-info/top_level.txt +1 -0
- archrails_mcp-0.3.0/pyproject.toml +60 -0
- archrails_mcp-0.3.0/setup.cfg +4 -0
- archrails_mcp-0.3.0/tests/test_access_store_admin_sentinel.py +140 -0
- archrails_mcp-0.3.0/tests/test_db_boundary_service_name_host.py +220 -0
- archrails_mcp-0.3.0/tests/test_diff_walker.py +141 -0
- archrails_mcp-0.3.0/tests/test_federated_graph_enrichment.py +267 -0
- archrails_mcp-0.3.0/tests/test_federation_classify_contract_repo.py +222 -0
- archrails_mcp-0.3.0/tests/test_graph_filter_to_owned.py +145 -0
- archrails_mcp-0.3.0/tests/test_mcp_drift_parity.py +130 -0
- archrails_mcp-0.3.0/tests/test_mcp_pr_time_parity.py +305 -0
- archrails_mcp-0.3.0/tests/test_mcp_validator_coverage.py +109 -0
- archrails_mcp-0.3.0/tests/test_orphan_skips_federation_stubs.py +70 -0
- archrails_mcp-0.3.0/tests/test_pipeline_e2e.py +646 -0
- archrails_mcp-0.3.0/tests/test_publisher_line_range_propagation.py +90 -0
- 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
|