specky-sdd 3.4.0 → 3.5.0
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.
- package/CHANGELOG.md +22 -0
- package/README.md +24 -8
- package/SECURITY.md +15 -8
- package/apm.yml +1 -1
- package/config.yml +1 -1
- package/dist/cli/commands/serve.d.ts.map +1 -1
- package/dist/cli/commands/serve.js +3 -2
- package/dist/cli/commands/serve.js.map +1 -1
- package/dist/cli/index.js +3 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/config.d.ts +21 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +82 -6
- package/dist/config.js.map +1 -1
- package/dist/index.js +63 -14
- package/dist/index.js.map +1 -1
- package/dist/services/audit-logger.d.ts +54 -5
- package/dist/services/audit-logger.d.ts.map +1 -1
- package/dist/services/audit-logger.js +121 -9
- package/dist/services/audit-logger.js.map +1 -1
- package/dist/services/file-manager.d.ts.map +1 -1
- package/dist/services/file-manager.js +9 -3
- package/dist/services/file-manager.js.map +1 -1
- package/dist/tools/audit.js +1 -1
- package/dist/tools/audit.js.map +1 -1
- package/dist/tools/rbac.d.ts.map +1 -1
- package/dist/tools/rbac.js +18 -11
- package/dist/tools/rbac.js.map +1 -1
- package/dist/tools/tool-enforcement.d.ts +15 -1
- package/dist/tools/tool-enforcement.d.ts.map +1 -1
- package/dist/tools/tool-enforcement.js +70 -9
- package/dist/tools/tool-enforcement.js.map +1 -1
- package/dist/utils/token-table.d.ts +41 -0
- package/dist/utils/token-table.d.ts.map +1 -0
- package/dist/utils/token-table.js +103 -0
- package/dist/utils/token-table.js.map +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [3.5.0] - 2026-07-02
|
|
11
|
+
|
|
12
|
+
Enterprise mode — an **opt-in configuration profile** of the same 100%
|
|
13
|
+
open-source (MIT) package. Every control below ships in `specky-sdd` for
|
|
14
|
+
everyone, default-OFF, and the standard profile is byte-for-byte the same
|
|
15
|
+
experience as 3.4.0. See `docs/ENTERPRISE-DEPLOYMENT.md`.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- **Enterprise profile.** `profile: enterprise` in `.specky/config.yml` — or `SPECKY_PROFILE=enterprise`, the `SPECKY_ENTERPRISE=1` shorthand, or `specky serve --profile=enterprise` (flag > env > file) — flips the *defaults* of `audit_enabled`, `rbac.enabled`, `rate_limit.enabled`, and the new `audit.fail_closed` to ON. Explicit config values always win, so any control can still be switched off individually. The server logs the resolved posture at startup.
|
|
20
|
+
- **Identity-based RBAC over HTTP.** `SDD_HTTP_TOKENS_FILE` points at a YAML token table (kept outside the workspace) mapping bearer tokens — plaintext or `token_sha256` — to a named `principal` and RBAC `role`. Tokens are constant-time compared; loading is fail-closed (a malformed table aborts startup instead of accepting everyone). Role precedence is now **authenticated token role > `SDD_ROLE` > `rbac.default_role`** — an authenticated request ignores `SDD_ROLE`, so a remote caller cannot out-vote its token. `sdd_check_access` and `access_denied` responses report the principal and role source. The legacy shared `SDD_HTTP_TOKEN` continues to work unchanged when no table is configured.
|
|
21
|
+
- **Tamper-evident audit trail.** With `SDD_AUDIT_HMAC_KEY` (or `SDD_AUDIT_HMAC_KEY_FILE`, key held outside the workspace) every audit entry is signed with HMAC-SHA256 over its serialized form — `previous_hash` included, so signatures chain. `sdd_verify_audit` now verifies both the hash chain and the signatures and reports `hmac_checked` / `signed_entries`; a workspace writer who rewrites the log and recomputes the plain chain is detected. Audit entries also record the authenticated `principal`. (Documented limit: tail truncation needs external anchoring of `current_hash`.)
|
|
22
|
+
- **Fail-closed auditing.** `audit.fail_closed: true` (enterprise default) refuses tool execution with `audit_unavailable` when the pre-execution audit entry cannot be written — no unaudited actions. Post-execution audit failures are surfaced on stderr without masking the tool result. Standard profile keeps the historical fail-open behavior.
|
|
23
|
+
- **`docs/ENTERPRISE-DEPLOYMENT.md`** — profiles, token table setup (with token/hash generation commands), HMAC audit + verification, hosted `serve --http` behind TLS (systemd/container examples), air-gapped tarball + private-registry installs, CI enforcement, the full enterprise env-var/config reference, and an honest out-of-scope list (no in-process TLS, no SSO yet, stdio has no auth layer).
|
|
24
|
+
- Tests: `config-profile` (profile precedence + explicit-wins), `audit-hmac` (signing, forged-rewrite detection, fail-closed, key resolution), `token-table` (load validation + identity resolution), and identity/fail-closed cases in `tool-enforcement`.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- `AuditLogger` accepts an options object (`{ exportFormat, maxFileSizeMb, hmacKey, failClosed }`); the old positional signature still works.
|
|
29
|
+
- RBAC denial responses and audit entries include the caller's `principal` when authenticated.
|
|
30
|
+
- README, SECURITY.md, CLI.md, INSTALL.md, and ENTERPRISE-CONTROLS.md updated for the profile, token table, and HMAC audit; API reference regenerated.
|
|
31
|
+
|
|
10
32
|
## [3.4.0] - 2026-07-02
|
|
11
33
|
|
|
12
34
|
First stable `3.4.0` (supersedes the `3.4.0-rc.*` prereleases). Consolidates the
|
package/README.md
CHANGED
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
| **Enterprise** | [Compliance Frameworks](#compliance-frameworks) | HIPAA, SOC2, GDPR, PCI-DSS, ISO 27001 |
|
|
58
58
|
| | [Enterprise Ready](#enterprise-ready) | Security, audit trail, quality gates |
|
|
59
59
|
| **Platform** | [The SDD Platform](#the-spec-driven-development-platform) | Built on Spec-Kit, everything included |
|
|
60
|
-
| | [Roadmap](#roadmap) | v3.
|
|
60
|
+
| | [Roadmap](#roadmap) | v3.5 current, v3.6+ planned |
|
|
61
61
|
|
|
62
62
|
|
|
63
63
|
## What is Specky?
|
|
@@ -199,7 +199,7 @@ cd your-project
|
|
|
199
199
|
specky install
|
|
200
200
|
```
|
|
201
201
|
|
|
202
|
-
That's it. The CLI auto-detects Claude Code and/or GitHub Copilot and installs the 13 agents, 22 prompts, 8 skills, 16 hooks, the MCP server registration, and pre-authorizes
|
|
202
|
+
That's it. The CLI auto-detects Claude Code and/or GitHub Copilot and installs the 13 agents, 22 prompts, 8 skills, 16 hooks, the MCP server registration, and pre-authorizes a least-privilege set of tools (the Specky MCP tools plus scoped `git`/`npm`/`npx` and file editing — not arbitrary shell or network).
|
|
203
203
|
|
|
204
204
|
**Other install modes:**
|
|
205
205
|
|
|
@@ -887,13 +887,19 @@ Create `.specky/config.yml` in your project root to customize Specky:
|
|
|
887
887
|
|
|
888
888
|
```yaml
|
|
889
889
|
# .specky/config.yml
|
|
890
|
+
profile: standard # standard | enterprise (flips security defaults ON)
|
|
890
891
|
templates_path: ./my-templates # Override built-in templates
|
|
891
892
|
default_framework: vitest # Default test framework
|
|
892
893
|
compliance_frameworks: [hipaa, soc2] # Frameworks to check
|
|
893
894
|
audit_enabled: true # Enable audit trail
|
|
895
|
+
rbac:
|
|
896
|
+
enabled: false # Role checks (viewer/contributor/admin)
|
|
897
|
+
default_role: contributor
|
|
898
|
+
rate_limit:
|
|
899
|
+
enabled: false # HTTP token bucket (60 rpm, burst 10)
|
|
894
900
|
```
|
|
895
901
|
|
|
896
|
-
When `templates_path` is set, Specky uses your custom templates instead of the built-in ones. When `audit_enabled` is true, tool invocations are logged locally.
|
|
902
|
+
When `templates_path` is set, Specky uses your custom templates instead of the built-in ones. When `audit_enabled` is true, tool invocations are logged locally. `profile: enterprise` turns audit, RBAC, rate limiting, and fail-closed auditing on by default (explicit values win) — see [docs/ENTERPRISE-DEPLOYMENT.md](docs/ENTERPRISE-DEPLOYMENT.md).
|
|
897
903
|
|
|
898
904
|
|
|
899
905
|
## MCP Integration Architecture
|
|
@@ -982,6 +988,12 @@ From any [input](#input-methods-6-ways-to-start) to production -- fully automate
|
|
|
982
988
|
|
|
983
989
|
Specky is built with enterprise adoption in mind.
|
|
984
990
|
|
|
991
|
+
### Enterprise profile (opt-in)
|
|
992
|
+
|
|
993
|
+
Specky is 100% open source (MIT) — enterprise mode is just an opt-in configuration profile of the same package: `profile: enterprise` (or `SPECKY_PROFILE=enterprise`, or `specky serve --profile=enterprise`) flips the governance defaults ON — hash-chained **audit trail** (fail-closed), **RBAC**, and HTTP **rate limiting** — while explicit config values still win. Add `SDD_HTTP_TOKENS_FILE` for **identity-based roles** (each bearer token maps to a named principal + role; audit entries record who did what) and `SDD_AUDIT_HMAC_KEY[_FILE]` for a **tamper-evident audit log** signed with a key the workspace never sees. The standard profile is untouched — all of this stays off unless you opt in.
|
|
994
|
+
|
|
995
|
+
→ Full guide: [docs/ENTERPRISE-DEPLOYMENT.md](docs/ENTERPRISE-DEPLOYMENT.md) (hosted HTTP, tokens, HMAC audit, air-gapped installs, containers, CI gates)
|
|
996
|
+
|
|
985
997
|
### Security Posture
|
|
986
998
|
|
|
987
999
|
- **3 runtime dependencies** — minimal attack surface (`@modelcontextprotocol/sdk`, `zod`, `yaml`)
|
|
@@ -993,6 +1005,7 @@ Specky is built with enterprise adoption in mind.
|
|
|
993
1005
|
- See [SECURITY.md](SECURITY.md) for full OWASP Top 10 coverage
|
|
994
1006
|
- See [docs/SYSTEM-DESIGN.md](docs/SYSTEM-DESIGN.md) for complete security architecture
|
|
995
1007
|
- See [docs/ENTERPRISE-CONTROLS.md](docs/ENTERPRISE-CONTROLS.md) for RBAC, audit trail, and tool-enforcement controls
|
|
1008
|
+
- See [docs/ENTERPRISE-DEPLOYMENT.md](docs/ENTERPRISE-DEPLOYMENT.md) for the enterprise profile, identity tokens, HMAC audit, and hosted/air-gapped deployment
|
|
996
1009
|
- See [docs/DETERMINISM.md](docs/DETERMINISM.md) for reproducible-output guarantees
|
|
997
1010
|
- See [docs/BRANCH-GOVERNANCE.md](docs/BRANCH-GOVERNANCE.md) for branch and release governance
|
|
998
1011
|
- See [docs/EVIDENCE.md](docs/EVIDENCE.md) for the validation evidence pack
|
|
@@ -1004,7 +1017,7 @@ When using Specky, follow these practices to protect your data:
|
|
|
1004
1017
|
| Practice | Why | How |
|
|
1005
1018
|
|----------|-----|-----|
|
|
1006
1019
|
| **Use stdio mode for local development** | No network exposure | `npx specky-sdd` (default) |
|
|
1007
|
-
| **Never expose HTTP mode to public networks without TLS** | HTTP has optional bearer-token auth but no TLS | `--http` binds to `127.0.0.1` by default; set `SDD_HTTP_TOKEN` for bearer auth. For remote access, add a reverse proxy (nginx, Caddy) terminating TLS |
|
|
1020
|
+
| **Never expose HTTP mode to public networks without TLS** | HTTP has optional bearer-token auth but no TLS | `--http` binds to `127.0.0.1` by default; set `SDD_HTTP_TOKEN` (shared) or `SDD_HTTP_TOKENS_FILE` (per-user identity + role) for bearer auth. For remote access, add a reverse proxy (nginx, Caddy) terminating TLS |
|
|
1008
1021
|
| **Protect the `.specs/` directory** | Contains your specification artifacts (architecture, API contracts, business logic) | Add `.specs/` to `.gitignore` if specs contain sensitive IP, or use a private repo |
|
|
1009
1022
|
| **Protect checkpoints** | `.specs/{feature}/.checkpoints/` stores full artifact snapshots | Same as above — treat checkpoints like source code |
|
|
1010
1023
|
| **Review auto-generated specs before committing** | Turnkey and auto-pipeline generate from natural language — may capture sensitive details | Review SPECIFICATION.md and DESIGN.md before `git add` |
|
|
@@ -1086,7 +1099,7 @@ curl http://localhost:3200/health
|
|
|
1086
1099
|
|
|
1087
1100
|
## Roadmap
|
|
1088
1101
|
|
|
1089
|
-
### v3.
|
|
1102
|
+
### v3.5 (current)
|
|
1090
1103
|
|
|
1091
1104
|
| Capability | Status |
|
|
1092
1105
|
|------------|--------|
|
|
@@ -1118,17 +1131,20 @@ curl http://localhost:3200/health
|
|
|
1118
1131
|
| RBAC foundation (opt-in role-based access control) | Stable |
|
|
1119
1132
|
| Rate limiting for HTTP transport (opt-in) | Stable |
|
|
1120
1133
|
| HTTP transport: loopback bind by default, bearer-token auth (`SDD_HTTP_TOKEN`), DNS-rebinding protection | Stable |
|
|
1134
|
+
| Enterprise profile (`profile: enterprise` — audit/RBAC/rate-limit defaults ON, opt-in) | Stable |
|
|
1135
|
+
| Identity-based RBAC over HTTP (`SDD_HTTP_TOKENS_FILE`: token → principal + role) | Stable |
|
|
1136
|
+
| Tamper-evident audit trail (HMAC-signed entries, fail-closed mode, `sdd_verify_audit`) | Stable |
|
|
1121
1137
|
|
|
1122
|
-
### v3.
|
|
1138
|
+
### v3.6+ (planned)
|
|
1123
1139
|
|
|
1124
1140
|
| Feature | Description |
|
|
1125
1141
|
|---------|-------------|
|
|
1126
1142
|
| Observability | OpenTelemetry metrics and structured logging |
|
|
1127
1143
|
| Internationalization | Spec templates in PT-BR, ES, FR, DE, JA |
|
|
1128
1144
|
| Automated shrinking | fast-check/Hypothesis shrinking feedback into spec refinement |
|
|
1129
|
-
| Centralized audit log | SIEM
|
|
1145
|
+
| Centralized audit log | SIEM export (syslog shipping, OTLP) of the tamper-evident audit trail |
|
|
1130
1146
|
| Multi-tenant | Isolated workspaces for multiple teams |
|
|
1131
|
-
| SSO / SAML | Federated identity for enterprise auth |
|
|
1147
|
+
| SSO / SAML | Federated identity for enterprise auth (beyond the static token table) |
|
|
1132
1148
|
|
|
1133
1149
|
Have a feature request? [Open an issue](https://github.com/paulasilvatech/specky/issues).
|
|
1134
1150
|
|
package/SECURITY.md
CHANGED
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
| Version | Supported |
|
|
6
6
|
| --- | --- |
|
|
7
|
-
| 3.
|
|
8
|
-
| 3.
|
|
9
|
-
|
|
|
10
|
-
| 2.
|
|
7
|
+
| 3.5.x | ✅ Active |
|
|
8
|
+
| 3.4.x | ✅ Security fixes only |
|
|
9
|
+
| 3.0.x–3.3.x | ❌ End of life |
|
|
10
|
+
| 2.x | ❌ End of life |
|
|
11
11
|
| 1.0.x | ❌ End of life |
|
|
12
12
|
|
|
13
13
|
## Reporting a Vulnerability
|
|
@@ -78,7 +78,7 @@ Runtime dependencies are kept intentionally small and are audited in CI.
|
|
|
78
78
|
| A04 Insecure Design | State machine enforces phase ordering; thin tools / fat services separation |
|
|
79
79
|
| A05 Security Misconfiguration | Minimal config surface; no default credentials; no admin endpoints |
|
|
80
80
|
| A06 Vulnerable Components | 3 runtime deps only; Dependabot enabled; regular audits |
|
|
81
|
-
| A07 Authentication Failures | stdio mode is process-isolated (no network). HTTP mode binds to `127.0.0.1` by default and supports optional bearer-token auth (`SDD_HTTP_TOKEN`, constant-time compared
|
|
81
|
+
| A07 Authentication Failures | stdio mode is process-isolated (no network). HTTP mode binds to `127.0.0.1` by default and supports optional bearer-token auth — a shared token (`SDD_HTTP_TOKEN`) or a named token table (`SDD_HTTP_TOKENS_FILE`, principal + RBAC role per token, sha256 storage supported) — all constant-time compared, plus DNS-rebinding protection |
|
|
82
82
|
| A08 Data Integrity Failures | Atomic file writes via FileManager; Zod schema enforcement |
|
|
83
83
|
| A09 Logging Failures | Structured stderr logging; no stdout pollution |
|
|
84
84
|
| A10 SSRF | Zero outbound network requests |
|
|
@@ -101,8 +101,15 @@ We run `npm audit` in CI on every pull request. Any `high` or `critical` vulnera
|
|
|
101
101
|
| --- | --- | --- |
|
|
102
102
|
| `SDD_WORKSPACE` | Restricts file operations to this directory | Current working directory |
|
|
103
103
|
| `PORT` | HTTP transport port (when using `--http` mode) | 3200 |
|
|
104
|
+
| `SPECKY_PROFILE` | `enterprise` flips audit/RBAC/rate-limit defaults to ON (explicit config wins) | `standard` |
|
|
105
|
+
| `SDD_HTTP_TOKEN` | Shared bearer token for `--http` (no identity) | unset (auth off) |
|
|
106
|
+
| `SDD_HTTP_TOKENS_FILE` | Named token table → principal + RBAC role (identity-based auth) | unset |
|
|
107
|
+
| `SDD_AUDIT_HMAC_KEY` / `SDD_AUDIT_HMAC_KEY_FILE` | Sign audit entries (tamper evidence); keep the key outside the workspace | unset (chain only) |
|
|
108
|
+
| `SDD_ROLE` | Local RBAC role for stdio use — ignored on authenticated HTTP requests | `rbac.default_role` |
|
|
104
109
|
|
|
105
|
-
HTTP transport (`--http`) binds to `127.0.0.1` by default. Binding to a non-loopback address requires an explicit `--host` and prints a warning. Set `SDD_HTTP_TOKEN` to require an `Authorization: Bearer <token>` header on every `/mcp` request (the `/health` probe stays open). Even so, do not expose Specky to public networks without a TLS-terminating reverse proxy.
|
|
110
|
+
HTTP transport (`--http`) binds to `127.0.0.1` by default. Binding to a non-loopback address requires an explicit `--host` and prints a warning. Set `SDD_HTTP_TOKEN` (shared token) or `SDD_HTTP_TOKENS_FILE` (named tokens mapping to principal + role) to require an `Authorization: Bearer <token>` header on every `/mcp` request (the `/health` probe stays open). Even so, do not expose Specky to public networks without a TLS-terminating reverse proxy. Hosted, air-gapped, and container deployment patterns: [docs/ENTERPRISE-DEPLOYMENT.md](docs/ENTERPRISE-DEPLOYMENT.md).
|
|
111
|
+
|
|
112
|
+
The audit trail's plain hash chain detects corruption but not deliberate rewriting by a workspace writer; configure an HMAC key (held outside the workspace) to make entries tamper-evident, and anchor the `current_hash` from `sdd_verify_audit` externally to detect tail truncation.
|
|
106
113
|
|
|
107
114
|
## Secure Development Practices
|
|
108
115
|
|
|
@@ -119,7 +126,7 @@ HTTP transport (`--http`) binds to `127.0.0.1` by default. Binding to a non-loop
|
|
|
119
126
|
| Practice | Details |
|
|
120
127
|
| --- | --- |
|
|
121
128
|
| **Use stdio mode by default** | `specky-sdd` (global install) — no network exposure, process-level isolation |
|
|
122
|
-
| **Never expose HTTP mode publicly without TLS** | `--http` supports bearer-token auth (`SDD_HTTP_TOKEN`) and binds to `127.0.0.1` by default, but has no TLS. For remote access, set
|
|
129
|
+
| **Never expose HTTP mode publicly without TLS** | `--http` supports bearer-token auth (`SDD_HTTP_TOKEN`, or `SDD_HTTP_TOKENS_FILE` for per-user identity + role) and binds to `127.0.0.1` by default, but has no TLS. For remote access, set tokens AND place it behind a reverse proxy (nginx, Caddy, Traefik) terminating TLS |
|
|
123
130
|
| **Protect `.specs/` directory** | Contains architecture details, API contracts, security models. Add to `.gitignore` for sensitive projects, or use a private repository |
|
|
124
131
|
| **Protect `.checkpoints/`** | Contains full copies of all spec artifacts. Treat like source code |
|
|
125
132
|
| **Keep security-scan hook active** | `.claude/hooks/security-scan.sh` scans for hardcoded secrets and blocks commits (exit 2). Do not disable |
|
|
@@ -205,7 +212,7 @@ Running `npx -y specky-sdd@latest` without a pinned version downloads the latest
|
|
|
205
212
|
|
|
206
213
|
```bash
|
|
207
214
|
# Install into a local vendor directory — no global write permissions needed
|
|
208
|
-
npm install specky-sdd@3.
|
|
215
|
+
npm install specky-sdd@3.5.0 --prefix ./vendor --ignore-scripts
|
|
209
216
|
./vendor/node_modules/.bin/specky install
|
|
210
217
|
./vendor/node_modules/.bin/specky doctor # integrity check
|
|
211
218
|
```
|
package/apm.yml
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
name: specky-sdd
|
|
2
|
-
version: 3.
|
|
2
|
+
version: 3.5.0
|
|
3
3
|
description: "Agentic Spec-Driven Development — 10-phase pipeline with 58 MCP tools, 13 agents, 22 prompts, 8 skills, 16 hooks, EARS notation, model routing, and enterprise security"
|
|
4
4
|
author: Paula Silva
|
|
5
5
|
license: MIT
|
package/config.yml
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/serve.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,OAAO,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,wBAAsB,QAAQ,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,
|
|
1
|
+
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/serve.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,OAAO,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,wBAAsB,QAAQ,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,CAclE"}
|
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
*/
|
|
8
8
|
export async function runServe(opts) {
|
|
9
9
|
// We simply import the existing entry point which starts the MCP server.
|
|
10
|
-
// Flags like --http are read from process.argv by
|
|
11
|
-
// so we just set them back into argv for
|
|
10
|
+
// Flags like --http, --host= and --profile= are read from process.argv by
|
|
11
|
+
// the server module itself, so we just set them back into argv for
|
|
12
|
+
// compatibility.
|
|
12
13
|
const extra = [];
|
|
13
14
|
if (opts.http)
|
|
14
15
|
extra.push("--http");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serve.js","sourceRoot":"","sources":["../../../src/cli/commands/serve.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAOH,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,IAAkB;IAC/C,yEAAyE;IACzE,
|
|
1
|
+
{"version":3,"file":"serve.js","sourceRoot":"","sources":["../../../src/cli/commands/serve.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAOH,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,IAAkB;IAC/C,yEAAyE;IACzE,0EAA0E;IAC1E,mEAAmE;IACnE,iBAAiB;IACjB,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,IAAI,IAAI,CAAC,IAAI;QAAE,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACpC,IAAI,IAAI,CAAC,IAAI;QAAE,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACvD,6CAA6C;IAC7C,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,CAAC;IAE5B,MAAM,MAAM,CAAC,gBAAgB,CAAC,CAAC;IAC/B,2CAA2C;IAC3C,OAAO,CAAC,CAAC;AACX,CAAC"}
|
package/dist/cli/index.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* specky status
|
|
9
9
|
* specky upgrade
|
|
10
10
|
* specky hooks <list|test|run NAME>
|
|
11
|
-
* specky serve [--http] [--port=N]
|
|
11
|
+
* specky serve [--http] [--port=N] [--profile=enterprise]
|
|
12
12
|
* specky --version | -v
|
|
13
13
|
* specky --help | -h
|
|
14
14
|
*
|
|
@@ -70,6 +70,8 @@ Commands:
|
|
|
70
70
|
--port=<N> HTTP port (default 3200)
|
|
71
71
|
--host=<addr> HTTP bind address (default 127.0.0.1;
|
|
72
72
|
set 0.0.0.0 only behind an auth proxy)
|
|
73
|
+
--profile=<standard|enterprise> Config profile (enterprise defaults
|
|
74
|
+
audit/RBAC/rate-limit to ON)
|
|
73
75
|
|
|
74
76
|
--version, -v Print version
|
|
75
77
|
--help, -h Print this help
|
package/dist/cli/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAI1C,SAAS,UAAU,CAAC,IAAc;IAChC,MAAM,UAAU,GAAa,EAAE,CAAC;IAChC,MAAM,KAAK,GAAW,EAAE,CAAC;IACzB,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;QACrB,IAAI,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;YACvB,MAAM,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;YAC1B,IAAI,EAAE,GAAG,CAAC,EAAE,CAAC;gBACX,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;YAC1C,CAAC;iBAAM,CAAC;gBACN,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;YAC3B,CAAC;QACH,CAAC;aAAM,IAAI,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC7C,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;QAC3B,CAAC;aAAM,CAAC;YACN,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACrB,CAAC;IACH,CAAC;IACD,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC;AAC/B,CAAC;AAED,SAAS,SAAS;IAChB,OAAO,CAAC,GAAG,CAAC,WAAW,OAAO
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"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"}
|
package/dist/config.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
declare const configSchema: z.ZodObject<{
|
|
3
|
+
profile: z.ZodDefault<z.ZodEnum<{
|
|
4
|
+
standard: "standard";
|
|
5
|
+
enterprise: "enterprise";
|
|
6
|
+
}>>;
|
|
3
7
|
templates_path: z.ZodDefault<z.ZodString>;
|
|
4
8
|
default_framework: z.ZodDefault<z.ZodString>;
|
|
5
9
|
compliance_frameworks: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
@@ -16,6 +20,7 @@ declare const configSchema: z.ZodObject<{
|
|
|
16
20
|
otlp: "otlp";
|
|
17
21
|
}>>;
|
|
18
22
|
max_file_size_mb: z.ZodDefault<z.ZodNumber>;
|
|
23
|
+
fail_closed: z.ZodDefault<z.ZodBoolean>;
|
|
19
24
|
}, z.core.$strip>>;
|
|
20
25
|
rbac: z.ZodDefault<z.ZodObject<{
|
|
21
26
|
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
@@ -27,11 +32,26 @@ declare const configSchema: z.ZodObject<{
|
|
|
27
32
|
}, z.core.$strip>>;
|
|
28
33
|
}, z.core.$strip>;
|
|
29
34
|
export type SpeckyConfig = z.infer<typeof configSchema>;
|
|
35
|
+
export type SpeckyProfile = SpeckyConfig["profile"];
|
|
36
|
+
/** Inputs that can force the profile from outside config.yml (flag > env). */
|
|
37
|
+
export interface ProfileOverrides {
|
|
38
|
+
argv?: readonly string[];
|
|
39
|
+
env?: Record<string, string | undefined>;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Resolve the active profile. Precedence: `--profile=` flag > `SPECKY_PROFILE`
|
|
43
|
+
* env > `SPECKY_ENTERPRISE=1` shorthand > config.yml `profile:` > standard.
|
|
44
|
+
* Unknown values are ignored with a stderr warning (never crash the server).
|
|
45
|
+
*/
|
|
46
|
+
export declare function resolveProfile(configProfile: SpeckyProfile, overrides?: ProfileOverrides): SpeckyProfile;
|
|
30
47
|
/**
|
|
31
48
|
* Load `.specky/config.yml` from workspace root. Returns defaults if the file
|
|
32
49
|
* is absent, unparseable, or fails validation (a bad value never crashes the
|
|
33
50
|
* server — it falls back to the safe defaults, with a stderr warning).
|
|
51
|
+
*
|
|
52
|
+
* `overrides` lets the server entry point (and tests) inject argv/env for
|
|
53
|
+
* profile resolution instead of reading globals.
|
|
34
54
|
*/
|
|
35
|
-
export declare function loadConfig(workspaceRoot: string): SpeckyConfig;
|
|
55
|
+
export declare function loadConfig(workspaceRoot: string, overrides?: ProfileOverrides): SpeckyConfig;
|
|
36
56
|
export {};
|
|
37
57
|
//# sourceMappingURL=config.d.ts.map
|
package/dist/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAoBA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAcxB,QAAA,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA4BR,CAAC;AAEX,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AACxD,MAAM,MAAM,aAAa,GAAG,YAAY,CAAC,SAAS,CAAC,CAAC;AAIpD,8EAA8E;AAC9E,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;CAC1C;AAMD;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,aAAa,EAAE,aAAa,EAC5B,SAAS,GAAE,gBAAqB,GAC/B,aAAa,CAsBf;AAgCD;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,aAAa,EAAE,MAAM,EAAE,SAAS,GAAE,gBAAqB,GAAG,YAAY,CAyBhG"}
|
package/dist/config.js
CHANGED
|
@@ -5,6 +5,15 @@
|
|
|
5
5
|
* Parsing uses the `yaml` library (safe by default — no code execution) and a
|
|
6
6
|
* Zod schema for validation, replacing a hand-rolled line parser that could not
|
|
7
7
|
* handle nested lists/quoting and did no validation.
|
|
8
|
+
*
|
|
9
|
+
* Profiles: `standard` (default) keeps every enterprise control opt-in and
|
|
10
|
+
* off. `enterprise` flips the *defaults* of audit_enabled, rbac.enabled,
|
|
11
|
+
* rate_limit.enabled, and audit.fail_closed to true — an explicit value in
|
|
12
|
+
* config.yml always wins over the profile default, so an enterprise deployment
|
|
13
|
+
* can still switch an individual control off. The profile itself can come from
|
|
14
|
+
* config.yml (`profile: enterprise`), the `SPECKY_PROFILE` env var, the
|
|
15
|
+
* `SPECKY_ENTERPRISE=1` shorthand, or the server's `--profile=enterprise` flag
|
|
16
|
+
* (flag > env > config file).
|
|
8
17
|
*/
|
|
9
18
|
import { readFileSync } from "node:fs";
|
|
10
19
|
import { join } from "node:path";
|
|
@@ -19,6 +28,7 @@ const safeRelativePath = z.string().refine((p) => p === "" ||
|
|
|
19
28
|
!p.includes("\0")), { message: "templates_path must be a workspace-relative path (no absolute paths, no '..')." });
|
|
20
29
|
const configSchema = z
|
|
21
30
|
.object({
|
|
31
|
+
profile: z.enum(["standard", "enterprise"]).default("standard"),
|
|
22
32
|
templates_path: safeRelativePath.default(""),
|
|
23
33
|
default_framework: z.string().default("vitest"),
|
|
24
34
|
compliance_frameworks: z.array(z.string()).default(["general"]),
|
|
@@ -34,8 +44,9 @@ const configSchema = z
|
|
|
34
44
|
.object({
|
|
35
45
|
export_format: z.enum(["jsonl", "syslog", "otlp"]).default("jsonl"),
|
|
36
46
|
max_file_size_mb: z.number().positive().default(10),
|
|
47
|
+
fail_closed: z.boolean().default(false),
|
|
37
48
|
})
|
|
38
|
-
.default({ export_format: "jsonl", max_file_size_mb: 10 }),
|
|
49
|
+
.default({ export_format: "jsonl", max_file_size_mb: 10, fail_closed: false }),
|
|
39
50
|
rbac: z
|
|
40
51
|
.object({
|
|
41
52
|
enabled: z.boolean().default(false),
|
|
@@ -45,19 +56,79 @@ const configSchema = z
|
|
|
45
56
|
})
|
|
46
57
|
.strip();
|
|
47
58
|
const DEFAULT_CONFIG = configSchema.parse({});
|
|
59
|
+
function isRecord(value) {
|
|
60
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Resolve the active profile. Precedence: `--profile=` flag > `SPECKY_PROFILE`
|
|
64
|
+
* env > `SPECKY_ENTERPRISE=1` shorthand > config.yml `profile:` > standard.
|
|
65
|
+
* Unknown values are ignored with a stderr warning (never crash the server).
|
|
66
|
+
*/
|
|
67
|
+
export function resolveProfile(configProfile, overrides = {}) {
|
|
68
|
+
const argv = overrides.argv ?? process.argv;
|
|
69
|
+
const env = overrides.env ?? process.env;
|
|
70
|
+
const flagValue = argv
|
|
71
|
+
.filter((a) => a.startsWith("--profile="))
|
|
72
|
+
.at(-1)
|
|
73
|
+
?.slice("--profile=".length);
|
|
74
|
+
const candidates = [
|
|
75
|
+
flagValue,
|
|
76
|
+
env["SPECKY_PROFILE"],
|
|
77
|
+
env["SPECKY_ENTERPRISE"] === "1" ? "enterprise" : undefined,
|
|
78
|
+
];
|
|
79
|
+
for (const candidate of candidates) {
|
|
80
|
+
if (candidate === undefined)
|
|
81
|
+
continue;
|
|
82
|
+
if (candidate === "standard" || candidate === "enterprise")
|
|
83
|
+
return candidate;
|
|
84
|
+
console.error(`[specky] Ignoring unknown profile "${candidate}" (valid: standard, enterprise).`);
|
|
85
|
+
}
|
|
86
|
+
return configProfile;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Enterprise profile flips the *defaults* of the security controls to ON.
|
|
90
|
+
* A value explicitly present in config.yml always wins, so `rawConfig` (the
|
|
91
|
+
* parsed YAML before Zod defaults) is consulted to tell "user said false"
|
|
92
|
+
* apart from "user said nothing".
|
|
93
|
+
*/
|
|
94
|
+
function applyEnterpriseDefaults(config, rawConfig) {
|
|
95
|
+
const raw = isRecord(rawConfig) ? rawConfig : {};
|
|
96
|
+
const rawRbac = isRecord(raw["rbac"]) ? raw["rbac"] : {};
|
|
97
|
+
const rawRateLimit = isRecord(raw["rate_limit"]) ? raw["rate_limit"] : {};
|
|
98
|
+
const rawAudit = isRecord(raw["audit"]) ? raw["audit"] : {};
|
|
99
|
+
return {
|
|
100
|
+
...config,
|
|
101
|
+
audit_enabled: "audit_enabled" in raw ? config.audit_enabled : true,
|
|
102
|
+
rbac: {
|
|
103
|
+
...config.rbac,
|
|
104
|
+
enabled: "enabled" in rawRbac ? config.rbac.enabled : true,
|
|
105
|
+
},
|
|
106
|
+
rate_limit: {
|
|
107
|
+
...config.rate_limit,
|
|
108
|
+
enabled: "enabled" in rawRateLimit ? config.rate_limit.enabled : true,
|
|
109
|
+
},
|
|
110
|
+
audit: {
|
|
111
|
+
...config.audit,
|
|
112
|
+
fail_closed: "fail_closed" in rawAudit ? config.audit.fail_closed : true,
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
48
116
|
/**
|
|
49
117
|
* Load `.specky/config.yml` from workspace root. Returns defaults if the file
|
|
50
118
|
* is absent, unparseable, or fails validation (a bad value never crashes the
|
|
51
119
|
* server — it falls back to the safe defaults, with a stderr warning).
|
|
120
|
+
*
|
|
121
|
+
* `overrides` lets the server entry point (and tests) inject argv/env for
|
|
122
|
+
* profile resolution instead of reading globals.
|
|
52
123
|
*/
|
|
53
|
-
export function loadConfig(workspaceRoot) {
|
|
124
|
+
export function loadConfig(workspaceRoot, overrides = {}) {
|
|
54
125
|
const configPath = join(workspaceRoot, ".specky", "config.yml");
|
|
55
126
|
let raw;
|
|
56
127
|
try {
|
|
57
128
|
raw = readFileSync(configPath, "utf-8");
|
|
58
129
|
}
|
|
59
130
|
catch {
|
|
60
|
-
return { ...DEFAULT_CONFIG };
|
|
131
|
+
return finalizeConfig({ ...DEFAULT_CONFIG }, {}, overrides);
|
|
61
132
|
}
|
|
62
133
|
let parsed;
|
|
63
134
|
try {
|
|
@@ -65,13 +136,18 @@ export function loadConfig(workspaceRoot) {
|
|
|
65
136
|
}
|
|
66
137
|
catch (err) {
|
|
67
138
|
console.error(`[specky] Ignoring malformed .specky/config.yml: ${err.message}`);
|
|
68
|
-
return { ...DEFAULT_CONFIG };
|
|
139
|
+
return finalizeConfig({ ...DEFAULT_CONFIG }, {}, overrides);
|
|
69
140
|
}
|
|
70
141
|
const result = configSchema.safeParse(parsed);
|
|
71
142
|
if (!result.success) {
|
|
72
143
|
console.error(`[specky] Ignoring invalid .specky/config.yml: ${result.error.issues.map((i) => i.message).join("; ")}`);
|
|
73
|
-
return { ...DEFAULT_CONFIG };
|
|
144
|
+
return finalizeConfig({ ...DEFAULT_CONFIG }, {}, overrides);
|
|
74
145
|
}
|
|
75
|
-
return result.data;
|
|
146
|
+
return finalizeConfig(result.data, parsed, overrides);
|
|
147
|
+
}
|
|
148
|
+
function finalizeConfig(config, rawConfig, overrides) {
|
|
149
|
+
const profile = resolveProfile(config.profile, overrides);
|
|
150
|
+
const resolved = { ...config, profile };
|
|
151
|
+
return profile === "enterprise" ? applyEnterpriseDefaults(resolved, rawConfig) : resolved;
|
|
76
152
|
}
|
|
77
153
|
//# sourceMappingURL=config.js.map
|
package/dist/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
10
10
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
11
11
|
import { randomUUID } from "node:crypto";
|
|
12
|
-
import { isBearerAuthorized } from "./utils/http-auth.js";
|
|
13
12
|
import { VERSION, SERVER_NAME, DEFAULT_HTTP_PORT } from "./constants.js";
|
|
14
13
|
import { FileManager } from "./services/file-manager.js";
|
|
15
14
|
import { StateMachine } from "./services/state-machine.js";
|
|
@@ -45,7 +44,8 @@ import { registerTurnkeyTools } from "./tools/turnkey.js";
|
|
|
45
44
|
import { PbtGenerator } from "./services/pbt-generator.js";
|
|
46
45
|
import { registerPbtTools } from "./tools/pbt.js";
|
|
47
46
|
import { loadConfig } from "./config.js";
|
|
48
|
-
import { AuditLogger } from "./services/audit-logger.js";
|
|
47
|
+
import { AuditLogger, resolveAuditHmacKey } from "./services/audit-logger.js";
|
|
48
|
+
import { loadTokenTable, resolveBearerIdentity, sha256Hex } from "./utils/token-table.js";
|
|
49
49
|
import { MetricsGenerator } from "./services/metrics-generator.js";
|
|
50
50
|
import { registerMetricsTools } from "./tools/metrics.js";
|
|
51
51
|
import { ModelRoutingEngine } from "./services/model-routing-engine.js";
|
|
@@ -64,12 +64,26 @@ import { registerAuditTools } from "./tools/audit.js";
|
|
|
64
64
|
// Resolve workspace root
|
|
65
65
|
const workspaceRoot = process.env["SDD_WORKSPACE"] || process.cwd();
|
|
66
66
|
console.error(`[specky] Workspace root: ${workspaceRoot}`);
|
|
67
|
-
// Load optional project config (.specky/config.yml)
|
|
67
|
+
// Load optional project config (.specky/config.yml). The profile may be
|
|
68
|
+
// forced from outside the workspace: --profile=enterprise flag, SPECKY_PROFILE
|
|
69
|
+
// env, or SPECKY_ENTERPRISE=1 shorthand.
|
|
68
70
|
const config = loadConfig(workspaceRoot);
|
|
71
|
+
const auditHmacKey = resolveAuditHmacKey();
|
|
69
72
|
if (config.templates_path)
|
|
70
73
|
console.error(`[specky] Custom templates: ${config.templates_path}`);
|
|
71
|
-
if (config.
|
|
72
|
-
console.error(`[specky]
|
|
74
|
+
if (config.profile === "enterprise") {
|
|
75
|
+
console.error(`[specky] Profile: enterprise — audit=${config.audit_enabled ? "on" : "off"}` +
|
|
76
|
+
` (fail_closed=${config.audit.fail_closed ? "on" : "off"}, hmac=${auditHmacKey ? "on" : "off"})` +
|
|
77
|
+
`, rbac=${config.rbac.enabled ? "on" : "off"} (default_role=${config.rbac.default_role})` +
|
|
78
|
+
`, rate_limit=${config.rate_limit.enabled ? "on" : "off"}`);
|
|
79
|
+
if (config.audit_enabled && !auditHmacKey) {
|
|
80
|
+
console.error("[specky] WARNING: enterprise audit is hash-chained but NOT tamper-evident — " +
|
|
81
|
+
"set SDD_AUDIT_HMAC_KEY or SDD_AUDIT_HMAC_KEY_FILE (key stored outside the workspace) to sign entries.");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
else if (config.audit_enabled) {
|
|
85
|
+
console.error(`[specky] Audit trail: enabled${auditHmacKey ? " (HMAC-signed)" : ""}`);
|
|
86
|
+
}
|
|
73
87
|
// Initialize MCP server
|
|
74
88
|
const server = new McpServer({
|
|
75
89
|
name: SERVER_NAME,
|
|
@@ -109,7 +123,12 @@ const docGenerator = new DocGenerator(fileManager, stateMachine);
|
|
|
109
123
|
const gitManager = new GitManager(fileManager);
|
|
110
124
|
const testGenerator = new TestGenerator(fileManager);
|
|
111
125
|
const pbtGenerator = new PbtGenerator(fileManager);
|
|
112
|
-
const auditLogger = new AuditLogger(workspaceRoot, config.audit_enabled,
|
|
126
|
+
const auditLogger = new AuditLogger(workspaceRoot, config.audit_enabled, {
|
|
127
|
+
exportFormat: config.audit.export_format,
|
|
128
|
+
maxFileSizeMb: config.audit.max_file_size_mb,
|
|
129
|
+
hmacKey: auditHmacKey,
|
|
130
|
+
failClosed: config.audit.fail_closed,
|
|
131
|
+
});
|
|
113
132
|
const rbacEngine = new RbacEngine(config.rbac.enabled ?? false, config.rbac.default_role ?? "contributor");
|
|
114
133
|
const metricsGenerator = new MetricsGenerator(fileManager);
|
|
115
134
|
const modelRoutingEngine = new ModelRoutingEngine();
|
|
@@ -174,18 +193,36 @@ async function main() {
|
|
|
174
193
|
const hostArg = args.find((a) => a.startsWith("--host="))?.slice("--host=".length);
|
|
175
194
|
const host = hostArg || process.env["SDD_HTTP_HOST"] || "127.0.0.1";
|
|
176
195
|
const isLoopback = host === "127.0.0.1" || host === "::1" || host === "localhost";
|
|
177
|
-
// Optional bearer-token auth
|
|
178
|
-
//
|
|
196
|
+
// Optional bearer-token auth, two flavors:
|
|
197
|
+
// - SDD_HTTP_TOKEN: one shared token (no identity — RBAC falls back to
|
|
198
|
+
// SDD_ROLE / default_role).
|
|
199
|
+
// - SDD_HTTP_TOKENS_FILE: named tokens mapping to principal + role
|
|
200
|
+
// (identity-based RBAC). Fail-closed: a broken tokens file aborts startup.
|
|
179
201
|
const authToken = process.env["SDD_HTTP_TOKEN"] || "";
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
202
|
+
const tokensFile = process.env["SDD_HTTP_TOKENS_FILE"] || "";
|
|
203
|
+
let tokenTable = [];
|
|
204
|
+
if (tokensFile) {
|
|
205
|
+
try {
|
|
206
|
+
tokenTable = loadTokenTable(tokensFile);
|
|
207
|
+
console.error(`[specky] HTTP token table loaded: ${tokenTable.length} principal(s) from ${tokensFile}`);
|
|
208
|
+
}
|
|
209
|
+
catch (err) {
|
|
210
|
+
console.error(`[specky] FATAL: ${err.message}`);
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
else if (authToken) {
|
|
215
|
+
console.error("[specky] HTTP bearer-token authentication enabled (shared token).");
|
|
183
216
|
}
|
|
217
|
+
const requireAuth = tokenTable.length > 0 || authToken.length > 0;
|
|
184
218
|
if (!isLoopback && !requireAuth) {
|
|
185
219
|
console.error(`[specky] WARNING: HTTP transport bound to non-loopback host "${host}" WITHOUT authentication. ` +
|
|
186
|
-
"Set SDD_HTTP_TOKEN and put it behind a TLS-terminating reverse proxy, or bind to 127.0.0.1.");
|
|
220
|
+
"Set SDD_HTTP_TOKEN or SDD_HTTP_TOKENS_FILE and put it behind a TLS-terminating reverse proxy, or bind to 127.0.0.1.");
|
|
221
|
+
}
|
|
222
|
+
if (config.profile === "enterprise" && config.rbac.enabled && tokenTable.length === 0) {
|
|
223
|
+
console.error("[specky] NOTE: RBAC is on but no SDD_HTTP_TOKENS_FILE is configured — roles come from " +
|
|
224
|
+
"SDD_ROLE/default_role (self-asserted). Configure a token table for identity-based roles.");
|
|
187
225
|
}
|
|
188
|
-
const isAuthorized = (header) => isBearerAuthorized(header, authToken);
|
|
189
226
|
const { StreamableHTTPServerTransport } = await import("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
|
190
227
|
const http = await import("node:http");
|
|
191
228
|
const transport = new StreamableHTTPServerTransport({
|
|
@@ -206,7 +243,8 @@ async function main() {
|
|
|
206
243
|
const httpServer = http.createServer(async (req, res) => {
|
|
207
244
|
if (req.url === "/mcp") {
|
|
208
245
|
// Authenticate before doing any work.
|
|
209
|
-
|
|
246
|
+
const identity = resolveBearerIdentity(req.headers["authorization"], tokenTable, authToken);
|
|
247
|
+
if (!identity.authorized) {
|
|
210
248
|
res.writeHead(401, {
|
|
211
249
|
"Content-Type": "application/json",
|
|
212
250
|
"WWW-Authenticate": "Bearer",
|
|
@@ -214,6 +252,17 @@ async function main() {
|
|
|
214
252
|
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
215
253
|
return;
|
|
216
254
|
}
|
|
255
|
+
// Attach the authenticated identity so the transport propagates it to
|
|
256
|
+
// tool handlers as extra.authInfo (consumed by the RBAC enforcement
|
|
257
|
+
// wrapper). `token` carries a fingerprint, never the secret itself.
|
|
258
|
+
if (identity.principal) {
|
|
259
|
+
req.auth = {
|
|
260
|
+
token: sha256Hex(req.headers["authorization"] ?? ""),
|
|
261
|
+
clientId: identity.principal,
|
|
262
|
+
scopes: identity.role ? [`role:${identity.role}`] : [],
|
|
263
|
+
extra: { principal: identity.principal, role: identity.role },
|
|
264
|
+
};
|
|
265
|
+
}
|
|
217
266
|
// Apply rate limiting before forwarding to MCP handler
|
|
218
267
|
if (rateLimiter) {
|
|
219
268
|
const clientId = req.socket.remoteAddress ?? "unknown";
|