stepstitch-service 0.5.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 (68) hide show
  1. stepstitch_service-0.5.0/PKG-INFO +22 -0
  2. stepstitch_service-0.5.0/pyproject.toml +101 -0
  3. stepstitch_service-0.5.0/setup.cfg +4 -0
  4. stepstitch_service-0.5.0/stepstitch_service/__init__.py +48 -0
  5. stepstitch_service-0.5.0/stepstitch_service/compiler.py +226 -0
  6. stepstitch_service-0.5.0/stepstitch_service/compliance.py +231 -0
  7. stepstitch_service-0.5.0/stepstitch_service/delivery/__init__.py +43 -0
  8. stepstitch_service-0.5.0/stepstitch_service/delivery/base.py +113 -0
  9. stepstitch_service-0.5.0/stepstitch_service/delivery/clients.py +112 -0
  10. stepstitch_service-0.5.0/stepstitch_service/delivery/config.py +17 -0
  11. stepstitch_service-0.5.0/stepstitch_service/delivery/salesforce_writer.py +28 -0
  12. stepstitch_service-0.5.0/stepstitch_service/delivery/servicenow_writer.py +31 -0
  13. stepstitch_service-0.5.0/stepstitch_service/github_bridge/__init__.py +39 -0
  14. stepstitch_service-0.5.0/stepstitch_service/github_bridge/bridge.py +119 -0
  15. stepstitch_service-0.5.0/stepstitch_service/github_bridge/client.py +135 -0
  16. stepstitch_service-0.5.0/stepstitch_service/github_bridge/content.py +72 -0
  17. stepstitch_service-0.5.0/stepstitch_service/github_bridge/workflow.py +67 -0
  18. stepstitch_service-0.5.0/stepstitch_service/integrations/__init__.py +28 -0
  19. stepstitch_service-0.5.0/stepstitch_service/integrations/base.py +175 -0
  20. stepstitch_service-0.5.0/stepstitch_service/integrations/bundle.py +82 -0
  21. stepstitch_service-0.5.0/stepstitch_service/integrations/conformance.py +71 -0
  22. stepstitch_service-0.5.0/stepstitch_service/integrations/contrib/__init__.py +5 -0
  23. stepstitch_service-0.5.0/stepstitch_service/integrations/contrib/jira.py +39 -0
  24. stepstitch_service-0.5.0/stepstitch_service/integrations/contrib/zendesk.py +36 -0
  25. stepstitch_service-0.5.0/stepstitch_service/integrations/genesys.py +47 -0
  26. stepstitch_service-0.5.0/stepstitch_service/integrations/salesforce.py +59 -0
  27. stepstitch_service-0.5.0/stepstitch_service/integrations/servicenow.py +91 -0
  28. stepstitch_service-0.5.0/stepstitch_service/integrations/validation.py +52 -0
  29. stepstitch_service-0.5.0/stepstitch_service/mcp_cli.py +51 -0
  30. stepstitch_service-0.5.0/stepstitch_service/mcp_server.py +298 -0
  31. stepstitch_service-0.5.0/stepstitch_service/profiles.py +126 -0
  32. stepstitch_service-0.5.0/stepstitch_service/replayability.py +142 -0
  33. stepstitch_service-0.5.0/stepstitch_service/retention.py +64 -0
  34. stepstitch_service-0.5.0/stepstitch_service/router.py +784 -0
  35. stepstitch_service-0.5.0/stepstitch_service/scrubber.py +399 -0
  36. stepstitch_service-0.5.0/stepstitch_service/verification/__init__.py +20 -0
  37. stepstitch_service-0.5.0/stepstitch_service/verification/verdict.py +52 -0
  38. stepstitch_service-0.5.0/stepstitch_service.egg-info/PKG-INFO +22 -0
  39. stepstitch_service-0.5.0/stepstitch_service.egg-info/SOURCES.txt +66 -0
  40. stepstitch_service-0.5.0/stepstitch_service.egg-info/dependency_links.txt +1 -0
  41. stepstitch_service-0.5.0/stepstitch_service.egg-info/requires.txt +21 -0
  42. stepstitch_service-0.5.0/stepstitch_service.egg-info/top_level.txt +1 -0
  43. stepstitch_service-0.5.0/tests/test_audit_endpoint.py +99 -0
  44. stepstitch_service-0.5.0/tests/test_compiler.py +89 -0
  45. stepstitch_service-0.5.0/tests/test_compliance.py +62 -0
  46. stepstitch_service-0.5.0/tests/test_connector_platform.py +69 -0
  47. stepstitch_service-0.5.0/tests/test_copilot_pack.py +29 -0
  48. stepstitch_service-0.5.0/tests/test_copilot_surface.py +197 -0
  49. stepstitch_service-0.5.0/tests/test_delivery.py +162 -0
  50. stepstitch_service-0.5.0/tests/test_delivery_clients.py +110 -0
  51. stepstitch_service-0.5.0/tests/test_demo_bundle.py +140 -0
  52. stepstitch_service-0.5.0/tests/test_github_bridge.py +95 -0
  53. stepstitch_service-0.5.0/tests/test_github_client.py +65 -0
  54. stepstitch_service-0.5.0/tests/test_github_content.py +74 -0
  55. stepstitch_service-0.5.0/tests/test_github_endpoints.py +142 -0
  56. stepstitch_service-0.5.0/tests/test_golden_path.py +160 -0
  57. stepstitch_service-0.5.0/tests/test_integrations.py +154 -0
  58. stepstitch_service-0.5.0/tests/test_mcp_surface.py +237 -0
  59. stepstitch_service-0.5.0/tests/test_open_core_boundary.py +136 -0
  60. stepstitch_service-0.5.0/tests/test_profiles.py +65 -0
  61. stepstitch_service-0.5.0/tests/test_replayability.py +92 -0
  62. stepstitch_service-0.5.0/tests/test_repro_eval.py +88 -0
  63. stepstitch_service-0.5.0/tests/test_retention.py +66 -0
  64. stepstitch_service-0.5.0/tests/test_router_smoke.py +181 -0
  65. stepstitch_service-0.5.0/tests/test_scrub_overrides.py +76 -0
  66. stepstitch_service-0.5.0/tests/test_scrubber.py +284 -0
  67. stepstitch_service-0.5.0/tests/test_verdict.py +31 -0
  68. stepstitch_service-0.5.0/tests/test_verification_endpoints.py +163 -0
@@ -0,0 +1,22 @@
1
+ Metadata-Version: 2.4
2
+ Name: stepstitch-service
3
+ Version: 0.5.0
4
+ Summary: StepStitch backend service: deterministic Playwright compiler + decoupled router factory + MCP connector.
5
+ License: Apache-2.0
6
+ Requires-Python: >=3.10
7
+ Requires-Dist: fastapi>=0.104
8
+ Requires-Dist: pydantic>=2.5
9
+ Provides-Extra: test
10
+ Requires-Dist: httpx>=0.27; extra == "test"
11
+ Requires-Dist: pytest>=8.0; extra == "test"
12
+ Provides-Extra: mcp
13
+ Requires-Dist: mcp>=1.0; extra == "mcp"
14
+ Requires-Dist: httpx>=0.27; extra == "mcp"
15
+ Provides-Extra: delivery
16
+ Requires-Dist: httpx>=0.27; extra == "delivery"
17
+ Provides-Extra: github
18
+ Requires-Dist: httpx>=0.27; extra == "github"
19
+ Provides-Extra: lint
20
+ Requires-Dist: import-linter>=2.0; extra == "lint"
21
+ Requires-Dist: ruff>=0.6; extra == "lint"
22
+ Requires-Dist: mypy>=1.8; extra == "lint"
@@ -0,0 +1,101 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "stepstitch-service"
7
+ version = "0.5.0" # x-release-please-version
8
+ description = "StepStitch backend service: deterministic Playwright compiler + decoupled router factory + MCP connector."
9
+ requires-python = ">=3.10"
10
+ license = { text = "Apache-2.0" }
11
+ dependencies = [
12
+ "fastapi>=0.104",
13
+ "pydantic>=2.5",
14
+ ]
15
+
16
+ [project.optional-dependencies]
17
+ test = ["httpx>=0.27", "pytest>=8.0"]
18
+ # The universal agentic connector (docs/PRODUCT-PLAN.md P1). Optional so the core
19
+ # service stays dependency-free; the MCP tool registry + dispatch are pure Python and
20
+ # need no extra — only `serve_stdio` (the stdio transport) requires this.
21
+ mcp = ["mcp>=1.0", "httpx>=0.27"]
22
+ # Reference HTTP clients for the optional governed direct-write (delivery/clients.py).
23
+ delivery = ["httpx>=0.27"]
24
+ # Reference GitHub client for the optional Repair Loop bridge (github_bridge/client.py).
25
+ github = ["httpx>=0.27"]
26
+ # Architecture/layering boundary enforcement (docs/PRODUCT-PLAN.md P3). The boundary is also
27
+ # proven dependency-free by tests/test_open_core_boundary.py; this is for CI `lint-imports`.
28
+ # ruff/mypy are pinned-floor here so CI installs blocking-gate versions (no ad-hoc installs).
29
+ lint = ["import-linter>=2.0", "ruff>=0.6", "mypy>=1.8"]
30
+
31
+ [tool.setuptools]
32
+ # All Apache-2.0. The concrete adapters (integrations.bundle + servicenow/salesforce/genesys)
33
+ # are injected by the host; the core never imports them (a layering rule, not a license one).
34
+ # See COMMERCIAL.md and tests/test_open_core_boundary.py.
35
+ packages = [
36
+ "stepstitch_service",
37
+ "stepstitch_service.integrations",
38
+ "stepstitch_service.integrations.contrib",
39
+ "stepstitch_service.delivery",
40
+ "stepstitch_service.github_bridge",
41
+ "stepstitch_service.verification",
42
+ ]
43
+
44
+ # Layering boundary: no core module may import a concrete adapter (keeps the privacy seam
45
+ # clean and the adapter set swappable — independent of licensing).
46
+ [tool.importlinter]
47
+ root_package = "stepstitch_service"
48
+
49
+ [[tool.importlinter.contracts]]
50
+ name = "Core must not import concrete adapters"
51
+ type = "forbidden"
52
+ source_modules = [
53
+ "stepstitch_service.router",
54
+ "stepstitch_service.scrubber",
55
+ "stepstitch_service.compiler",
56
+ "stepstitch_service.replayability",
57
+ "stepstitch_service.retention",
58
+ "stepstitch_service.profiles",
59
+ "stepstitch_service.compliance",
60
+ "stepstitch_service.mcp_server",
61
+ "stepstitch_service.mcp_cli",
62
+ "stepstitch_service.integrations.base",
63
+ "stepstitch_service.delivery.base",
64
+ "stepstitch_service.delivery.servicenow_writer",
65
+ "stepstitch_service.delivery.salesforce_writer",
66
+ "stepstitch_service.delivery.config",
67
+ "stepstitch_service.delivery.clients",
68
+ "stepstitch_service.github_bridge.content",
69
+ "stepstitch_service.github_bridge.client",
70
+ "stepstitch_service.github_bridge.bridge",
71
+ "stepstitch_service.github_bridge.workflow",
72
+ "stepstitch_service.verification.verdict",
73
+ ]
74
+ forbidden_modules = [
75
+ "stepstitch_service.integrations.servicenow",
76
+ "stepstitch_service.integrations.salesforce",
77
+ "stepstitch_service.integrations.genesys",
78
+ "stepstitch_service.integrations.bundle",
79
+ ]
80
+
81
+ # Lint gate (blocking in CI). Intentionally small + stable rule set: pycodestyle errors (E),
82
+ # pyflakes (F), and isort import ordering (I). Not a style crusade — just correctness + order.
83
+ [tool.ruff]
84
+ line-length = 100
85
+ target-version = "py310"
86
+
87
+ [tool.ruff.lint]
88
+ select = ["E", "F", "I"]
89
+
90
+ [tool.ruff.lint.per-file-ignores]
91
+ # workflow.py is a single raw-string literal: the verbatim GitHub Actions YAML we emit into
92
+ # customers' repos. Its long lines (curl/python one-liners) cannot be reflowed without changing
93
+ # the generated workflow, so E501 is intentionally not enforced on this template file.
94
+ "stepstitch_service/github_bridge/workflow.py" = ["E501"]
95
+
96
+ # Type gate (blocking in CI), scoped to the service package only. Non-strict baseline:
97
+ # we do NOT enable disallow_untyped_defs etc. — the goal is a real, green floor we can ratchet up.
98
+ [tool.mypy]
99
+ python_version = "3.10"
100
+ ignore_missing_imports = true
101
+ warn_unused_ignores = false
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,48 @@
1
+ """StepStitch backend service package (host-agnostic)."""
2
+ from .compiler import generate_playwright_test
3
+ from .compliance import build_evidence
4
+ from .mcp_server import (
5
+ COPILOT_SAFE_OPERATIONS,
6
+ assert_no_destructive_operation,
7
+ build_function_tool_specs,
8
+ build_tool_definitions,
9
+ dispatch_tool,
10
+ serve_stdio,
11
+ )
12
+ from .profiles import (
13
+ DEFAULT_PROFILE,
14
+ available_profiles,
15
+ load_profile,
16
+ policy_from_profile,
17
+ )
18
+ from .replayability import score_trace
19
+ from .retention import purge_expired_traces
20
+ from .router import create_stepstitch_router
21
+ from .scrubber import (
22
+ FINANCIAL_SERVICES_ENTERPRISE,
23
+ ScrubPolicy,
24
+ ScrubRejection,
25
+ scrub_trace_payload,
26
+ )
27
+
28
+ __all__ = [
29
+ "generate_playwright_test",
30
+ "score_trace",
31
+ "create_stepstitch_router",
32
+ "purge_expired_traces",
33
+ "scrub_trace_payload",
34
+ "ScrubPolicy",
35
+ "ScrubRejection",
36
+ "FINANCIAL_SERVICES_ENTERPRISE",
37
+ "load_profile",
38
+ "policy_from_profile",
39
+ "available_profiles",
40
+ "DEFAULT_PROFILE",
41
+ "build_evidence",
42
+ "COPILOT_SAFE_OPERATIONS",
43
+ "assert_no_destructive_operation",
44
+ "build_tool_definitions",
45
+ "build_function_tool_specs",
46
+ "dispatch_tool",
47
+ "serve_stdio",
48
+ ]
@@ -0,0 +1,226 @@
1
+ """Deterministic StepStitch -> Playwright compiler.
2
+
3
+ Pure functions, no I/O, no network, no embedded credentials. Given a trace's
4
+ structural footsteps (see contracts/stepstitch.md), emit an executable Playwright
5
+ TypeScript reproduction script. Output is fully determined by the inputs so it is
6
+ trivially unit-testable.
7
+
8
+ The compiled test is a real regression test: a captured API failure becomes an
9
+ armed ``page.waitForResponse`` (matched on URL + method, so it resolves whether or
10
+ not the bug is present) plus a status assertion, and a captured client exception
11
+ becomes a ``pageerror`` assertion. The test therefore FAILS while the bug is
12
+ present and PASSES once it is fixed (red -> green).
13
+ """
14
+ from __future__ import annotations
15
+
16
+ from typing import Any, Dict, List, Optional
17
+
18
+ from .replayability import score_trace
19
+
20
+ __all__ = ["generate_playwright_test"]
21
+
22
+
23
+ def _ts_str(value: str) -> str:
24
+ """Escape a string for a single-quoted TS literal."""
25
+ return value.replace("\\", "\\\\").replace("'", "\\'")
26
+
27
+
28
+ def _comment(text: str) -> str:
29
+ """Sanitize text for a single-line TS comment (no newlines/CR)."""
30
+ return text.replace("\r", " ").replace("\n", " ")
31
+
32
+
33
+ def _coerce_status(value: Any) -> Optional[int]:
34
+ try:
35
+ return int(value)
36
+ except (TypeError, ValueError):
37
+ return None
38
+
39
+
40
+ def _url_prefix(endpoint: str) -> str:
41
+ """Literal URL prefix before the first templated segment.
42
+
43
+ ``/api/accounts/:id/transfers`` -> ``/api/accounts/`` so ``url().includes(...)``
44
+ matches the concrete runtime URL (``/api/accounts/123/transfers``).
45
+ """
46
+ if not endpoint:
47
+ return ""
48
+ marker = endpoint.find("/:")
49
+ if marker != -1:
50
+ return endpoint[: marker + 1]
51
+ colon = endpoint.find(":")
52
+ if colon != -1:
53
+ return endpoint[:colon]
54
+ return endpoint
55
+
56
+
57
+ def _status_assertion(var: str, status: Optional[int], endpoint: str) -> str:
58
+ ep = _comment(endpoint or "the endpoint")
59
+ if status is not None and status >= 500:
60
+ return f" expect({var}.status(), 'no server error from {ep}').toBeLessThan(500);"
61
+ if status is not None:
62
+ return f" expect({var}.status(), '{ep} must not return {status}').not.toBe({status});"
63
+ return f" expect({var}.status(), '{ep} must succeed').toBeLessThan(400);"
64
+
65
+
66
+ def _arm_wait(var: str, prefix: str, method: str) -> List[str]:
67
+ method_clause = (
68
+ f" && r.request().method() === '{_ts_str(method.upper())}'" if method else ""
69
+ )
70
+ return [
71
+ f" const {var} = page.waitForResponse(",
72
+ f" (r) => r.url().includes('{_ts_str(prefix)}'){method_clause},",
73
+ " );",
74
+ ]
75
+
76
+
77
+ def generate_playwright_test(
78
+ trace_id: str,
79
+ footsteps: List[Dict[str, Any]],
80
+ base_url: str = "http://localhost:3000",
81
+ ) -> str:
82
+ """Compile footsteps into Playwright TS.
83
+
84
+ `base_url` is caller-supplied (no hardcoded host). Routes are templates
85
+ (e.g. ``/accounts/:id``); where a template contains ``:`` a TODO is emitted so the
86
+ engineer substitutes a concrete id. No credentials are ever embedded — auth is left
87
+ as a TODO for the operator to wire to a synthetic/test account.
88
+ """
89
+ base = base_url.rstrip("/")
90
+ replay = score_trace(footsteps)
91
+ has_exception = any(
92
+ str(s.get("type", "")).lower() == "exception" for s in footsteps
93
+ )
94
+
95
+ lines: List[str] = [
96
+ "import { test, expect } from '@playwright/test';",
97
+ "",
98
+ f"// StepStitch autogenerated reproduction (trace: {_comment(trace_id)})",
99
+ f"// Replayability: {replay['score']:.2f} (grade {replay['grade']})",
100
+ ]
101
+ for w in replay["warnings"]:
102
+ where = f" [step {w['step_index']}]" if "step_index" in w else ""
103
+ lines.append(f"// ⚠ {w['code']}{where}: {_comment(w['detail'])}")
104
+ lines += [
105
+ "// NOTE: no credentials are embedded. Wire authentication to a synthetic",
106
+ "// test account before running against a protected route.",
107
+ "test('StepStitch reproduction', async ({ page }) => {",
108
+ " // TODO: authenticate as a synthetic test user if the flow requires it.",
109
+ "",
110
+ ]
111
+ if has_exception:
112
+ lines += [
113
+ " // Capture uncaught client exceptions so we can assert they no longer occur.",
114
+ " const pageErrors: string[] = [];",
115
+ " page.on('pageerror', (e) => pageErrors.push(e.message));",
116
+ "",
117
+ ]
118
+
119
+ asserted = False
120
+ resp_n = 0
121
+
122
+ def emit_action(step_type: str, route: str, target: Optional[str], label: str) -> None:
123
+ if step_type == "navigation":
124
+ if ":" in route:
125
+ lines.append(
126
+ f" // TODO: substitute id(s) in templated route '{_comment(route)}'"
127
+ )
128
+ lines.append(f" await page.goto('{_ts_str(base + route)}');")
129
+ elif step_type == "click" and target:
130
+ if label and label != "[masked]":
131
+ lines.append(f" // label: {_comment(label)}")
132
+ lines.append(f" await page.locator('{_ts_str(target)}').click();")
133
+ elif step_type == "input" and target:
134
+ lines.append(" // value redacted by StepStitch; fill a test value:")
135
+ lines.append(
136
+ f" await page.locator('{_ts_str(target)}').fill('stepstitch-test-value');"
137
+ )
138
+
139
+ i = 0
140
+ n = len(footsteps)
141
+ while i < n:
142
+ step = footsteps[i]
143
+ step_type = str(step.get("type", "")).lower()
144
+ route = str(step.get("route", "/"))
145
+ target = step.get("target")
146
+ label = str(step.get("label", ""))
147
+ metadata = step.get("metadata") or {}
148
+
149
+ nxt = footsteps[i + 1] if i + 1 < n else None
150
+ nxt_is_api = nxt is not None and str(nxt.get("type", "")).lower() == "api_error"
151
+
152
+ lines.append(f" // [{step_type.upper()}] {_comment(route)}")
153
+
154
+ # An interaction immediately followed by an API error: arm the response
155
+ # wait BEFORE the action, then assert on the response after it.
156
+ if step_type in ("navigation", "click", "input") and nxt_is_api and nxt is not None:
157
+ api_meta = nxt.get("metadata") or {}
158
+ endpoint = str(api_meta.get("endpoint", ""))
159
+ status = _coerce_status(api_meta.get("status"))
160
+ method = str(api_meta.get("method", ""))
161
+ var = f"response{resp_n}"
162
+ lines += _arm_wait(var, _url_prefix(endpoint), method)
163
+ emit_action(step_type, route, target, label)
164
+ lines.append(
165
+ f" // expected API failure: {_comment(endpoint)} "
166
+ f"(HTTP {api_meta.get('status', '?')})"
167
+ )
168
+ lines.append(f" const res{resp_n} = await {var};")
169
+ lines.append(_status_assertion(f"res{resp_n}", status, endpoint))
170
+ asserted = True
171
+ resp_n += 1
172
+ lines.append("")
173
+ i += 2
174
+ continue
175
+
176
+ if step_type in ("navigation", "click", "input"):
177
+ emit_action(step_type, route, target, label)
178
+
179
+ elif step_type == "api_error":
180
+ # Standalone API error (no immediately preceding interaction): bind a
181
+ # short post-hoc wait so we still assert rather than just comment.
182
+ endpoint = str(metadata.get("endpoint", ""))
183
+ status = _coerce_status(metadata.get("status"))
184
+ method = str(metadata.get("method", ""))
185
+ method_clause = (
186
+ f" && r.request().method() === '{_ts_str(method.upper())}'"
187
+ if method
188
+ else ""
189
+ )
190
+ lines.append(
191
+ f" // expected API failure: {_comment(endpoint)} "
192
+ f"(HTTP {metadata.get('status', '?')})"
193
+ )
194
+ lines.append(
195
+ f" const res{resp_n} = await page.waitForResponse("
196
+ )
197
+ lines.append(
198
+ f" (r) => r.url().includes('{_ts_str(_url_prefix(endpoint))}'){method_clause},"
199
+ )
200
+ lines.append(" );")
201
+ lines.append(_status_assertion(f"res{resp_n}", status, endpoint))
202
+ asserted = True
203
+ resp_n += 1
204
+
205
+ elif step_type == "exception":
206
+ name = str(metadata.get("error_type") or metadata.get("name") or "Error")
207
+ lines.append(
208
+ f" expect(pageErrors.some((m) => m.includes('{_ts_str(_comment(name))}')), "
209
+ f"'the reported {_comment(name)} must not reproduce').toBe(false);"
210
+ )
211
+ asserted = True
212
+
213
+ lines.append("")
214
+ i += 1
215
+
216
+ if not asserted:
217
+ lines.append(
218
+ " // Navigation-only trace: no terminal failure was captured to assert on."
219
+ )
220
+ lines.append(
221
+ " // See the replayability warnings above; add a fixture/assertion to make this a"
222
+ )
223
+ lines.append(" // true regression test.")
224
+
225
+ lines.append("});")
226
+ return "\n".join(lines) + "\n"
@@ -0,0 +1,231 @@
1
+ """Compliance evidence — generated from the live ScrubPolicy, never hand-maintained.
2
+
3
+ A security reviewer's first question is "what do you capture, and what do you never
4
+ capture?". This module answers it from *code*: the capture/never-capture matrix is
5
+ derived directly from the active :class:`ScrubPolicy` (allowlists + forbidden keys), so
6
+ the evidence packet cannot drift from what the scrubber actually enforces. The drift
7
+ guard in ``service/tests/test_compliance.py`` fails CI if the committed
8
+ ``COMPLIANCE-EVIDENCE.md`` stops matching the policy.
9
+
10
+ Output is deterministic (no timestamps) precisely so it can be drift-guarded.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ from typing import List
15
+
16
+ from .profiles import PROFILES
17
+ from .scrubber import FINANCIAL_SERVICES_ENTERPRISE, ScrubPolicy
18
+
19
+ __all__ = ["build_evidence", "NEVER_CAPTURED_CATEGORIES", "ALWAYS_STRUCTURAL"]
20
+
21
+ # Human-facing categories the product never records (independent of key names).
22
+ NEVER_CAPTURED_CATEGORIES = (
23
+ "Screenshots / video",
24
+ "Input values (what a user typed)",
25
+ "Page text / DOM content",
26
+ "Raw URLs (templated to routes)",
27
+ "Request / response bodies",
28
+ "Raw frontend logs / console messages / stack traces",
29
+ "Network headers / cookies",
30
+ "SSNs, account/card numbers, emails, phone numbers (redacted from free text)",
31
+ )
32
+
33
+ ALWAYS_STRUCTURAL = (
34
+ "Route templates (e.g. /accounts/:id)",
35
+ "Stable selectors (data-testid preferred)",
36
+ "API status codes",
37
+ "Endpoint templates and source-path templates",
38
+ "Exception types",
39
+ "SDK/build/release metadata",
40
+ "Masked labels",
41
+ )
42
+
43
+ # --- Regulatory crosswalk (code-derived; cites the frameworks a reviewer applies) ----
44
+ _REGSP = "SEC Reg S-P (2024)"
45
+ _MRM = "2026 interagency MRM (supersedes SR 11-7)"
46
+ _HIPAA = "HIPAA"
47
+ _NIST = "NIST AI RMF"
48
+
49
+ # Each control maps to a citation per framework ("—" = not the controlling regime).
50
+ _CROSSWALK = (
51
+ ("Server-side scrub / NPI data-minimization (`scrubber.py`)", {
52
+ _REGSP: "Safeguards Rule — protect customer NPI",
53
+ _MRM: "Sound, controlled data inputs",
54
+ _HIPAA: "Minimum-necessary; no PHI stored",
55
+ _NIST: "MAP/MEASURE — data governance",
56
+ }),
57
+ ("Split retention + 5-yr audit clock (`retention.py`)", {
58
+ _REGSP: "Recordkeeping — incident records retained 5 yrs",
59
+ _MRM: "Auditability & traceability of model use",
60
+ _HIPAA: "Retain access/audit records",
61
+ _NIST: "GOVERN — documentation & records",
62
+ }),
63
+ ("Admin-only reads, audit on every read (`router.py`)", {
64
+ _REGSP: "Access controls; incident-response program",
65
+ _MRM: "Traceability / effective challenge",
66
+ _HIPAA: "Access controls & audit logging",
67
+ _NIST: "GOVERN/MANAGE — accountability",
68
+ }),
69
+ ("Org-wide kill switch, fail-safe (`router.py`)", {
70
+ _REGSP: "Incident-response containment",
71
+ _MRM: "Controls & human override",
72
+ _HIPAA: "Contingency / incident response",
73
+ _NIST: "MANAGE — incident response",
74
+ }),
75
+ ("Deterministic compiler + replayability + eval gate "
76
+ "(`compiler.py`, `test_repro_eval.py`)", {
77
+ _REGSP: "—",
78
+ _MRM: "Ongoing monitoring & output quality",
79
+ _HIPAA: "—",
80
+ _NIST: "MEASURE — validity & reliability",
81
+ }),
82
+ ("Draft-only, human-in-the-loop (`integrations/`, `copilot/action-policy.md`)", {
83
+ _REGSP: "—",
84
+ _MRM: "Human oversight — outputs support, not replace, decisions",
85
+ _HIPAA: "—",
86
+ _NIST: "GOVERN — human-AI configuration",
87
+ }),
88
+ )
89
+
90
+ # Release gates reframed as model-risk validation / ongoing-monitoring evidence.
91
+ _MRM_GATES = (
92
+ ("End-to-end golden path", "`test_golden_path.py`", "System validation"),
93
+ ("Server-side scrub boundary", "`test_scrubber.py`", "Data-control validation"),
94
+ ("Profile drift guard", "`test_profiles.py`", "Configuration control"),
95
+ ("Executable repro proof", "`scripts/prove-repro-executes.mjs`", "Output validity"),
96
+ ("Reproduction quality eval", "`test_repro_eval.py`", "Ongoing output-quality monitoring"),
97
+ ("Open-core import boundary", "`.importlinter` / `test_open_core_boundary.py`",
98
+ "Change control / segregation of duties"),
99
+ ("Compliance evidence drift guard", "`test_compliance.py`", "Documentation currency"),
100
+ )
101
+
102
+
103
+ def _bullets(items) -> List[str]:
104
+ return [f"- {i}" for i in items]
105
+
106
+
107
+ def _frameworks_for(policy: ScrubPolicy) -> List[str]:
108
+ """The framework columns that apply to a profile."""
109
+ if policy.name == "healthcare-strict":
110
+ return [_HIPAA, _NIST]
111
+ return [_REGSP, _MRM, _NIST]
112
+
113
+
114
+ def _crosswalk_section(policy: ScrubPolicy) -> List[str]:
115
+ cols = _frameworks_for(policy)
116
+ lines = [
117
+ "## Regulatory crosswalk",
118
+ "",
119
+ "The controls above mapped to the frameworks a regulated reviewer applies "
120
+ f"(columns selected for the `{policy.name}` profile).",
121
+ "",
122
+ "| Control | " + " | ".join(cols) + " |",
123
+ "|---|" + "|".join("---" for _ in cols) + "|",
124
+ ]
125
+ for control, mapping in _CROSSWALK:
126
+ cells = " | ".join(mapping.get(c, "—") for c in cols)
127
+ lines.append(f"| {control} | {cells} |")
128
+ lines.append("")
129
+ return lines
130
+
131
+
132
+ def _mrm_section() -> List[str]:
133
+ lines = [
134
+ "## Model risk management evidence",
135
+ "",
136
+ "Under the **April-2026 interagency model risk management guidance (superseding "
137
+ "SR 11-7)**, StepStitch's release gates are the validation & ongoing-monitoring "
138
+ "evidence — each a named, runnable check:",
139
+ "",
140
+ "| Gate | Check | MRM role |",
141
+ "|---|---|---|",
142
+ ]
143
+ for gate, check, role in _MRM_GATES:
144
+ lines.append(f"| {gate} | {check} | {role} |")
145
+ lines += [
146
+ "",
147
+ "StepStitch is a deterministic, **draft-only provider**: it produces evidence and "
148
+ "drafts for human decision-makers and never takes autonomous action, keeping AI "
149
+ "outputs in a \"support, not replace\" posture for fiduciary use.",
150
+ "",
151
+ ]
152
+ return lines
153
+
154
+
155
+ def build_evidence(policy: ScrubPolicy = FINANCIAL_SERVICES_ENTERPRISE) -> str:
156
+ """Return the compliance evidence packet (Markdown) for ``policy``."""
157
+ forbidden = sorted(policy.forbidden_keys)
158
+ meta_allow = sorted(policy.metadata_allowlist)
159
+ fs_meta_allow = sorted(policy.footstep_metadata_allowlist)
160
+
161
+ lines: List[str] = [
162
+ "# StepStitch — Compliance Evidence",
163
+ "",
164
+ "> Generated from the live `ScrubPolicy` by "
165
+ "`scripts/generate_compliance_evidence.py`. Do not edit by hand — the drift "
166
+ "guard in `service/tests/test_compliance.py` keeps this file equal to the code.",
167
+ "",
168
+ f"**Active policy:** `{policy.name}` ",
169
+ f"**Free-text handling:** `{policy.free_text}` (max {policy.max_text_len} chars) ",
170
+ f"**Forbidden key on payload →** "
171
+ f"{'reject (HTTP 422)' if policy.reject_on_forbidden else 'dropped + reported'}",
172
+ "",
173
+ "## What StepStitch never captures",
174
+ "",
175
+ *_bullets(NEVER_CAPTURED_CATEGORIES),
176
+ "",
177
+ "## What StepStitch captures (structural only)",
178
+ "",
179
+ *_bullets(ALWAYS_STRUCTURAL),
180
+ "",
181
+ "## Server-side enforcement (defense-in-depth)",
182
+ "",
183
+ "Every ingestion is scrubbed server-side before storage, independent of the SDK "
184
+ "(`service/stepstitch_service/scrubber.py`). The browser SDK also redacts, but "
185
+ "the server never trusts the client.",
186
+ "",
187
+ "### Metadata allowlist (everything else dropped)",
188
+ "",
189
+ f"- Top-level: {', '.join(f'`{k}`' for k in meta_allow)}",
190
+ f"- Footstep: {', '.join(f'`{k}`' for k in fs_meta_allow)}",
191
+ "",
192
+ "### Forbidden keys (dropped as a leak signal)",
193
+ "",
194
+ *_bullets(f"`{k}`" for k in forbidden),
195
+ "",
196
+ "## Operational controls",
197
+ "",
198
+ "- Consent required before capture; GPC and DNT respected (capture stays off).",
199
+ "- Admin-only operator reads; **every** read writes an audit event.",
200
+ "- Right-to-delete removes trace bodies; the deletion audit record is retained.",
201
+ "- Split retention: bodies purged on a short clock; audit records on a separate "
202
+ "5-year clock (SEC Reg S-P 2024).",
203
+ "- Org-wide kill switch refuses ingestion (HTTP 503) with no row written; a "
204
+ "broken flag fails safe (capture OFF).",
205
+ "- Per-trace scrub report stored at `trace_metadata._scrub` and returned on "
206
+ "ingestion.",
207
+ "",
208
+ "## Deployment profiles",
209
+ "",
210
+ "| Profile | free_text | forbidden-key handling |",
211
+ "|---|---|---|",
212
+ ]
213
+ for name in sorted(PROFILES):
214
+ scrub = PROFILES[name]["scrub"]
215
+ handling = "reject (422)" if scrub.get("reject_on_forbidden") else "drop"
216
+ lines.append(f"| `{name}` | {scrub.get('free_text')} | {handling} |")
217
+
218
+ lines.append("")
219
+ lines += _crosswalk_section(policy)
220
+ lines += _mrm_section()
221
+
222
+ lines += [
223
+ "## Verification",
224
+ "",
225
+ "- `pytest service/tests` — scrubber, replayability, profiles, integrations, "
226
+ "Copilot surface, retention, compiler, router.",
227
+ "- `ruff check service` — lint.",
228
+ "- `sbom.cdx.json` — supply-chain bill of materials.",
229
+ "",
230
+ ]
231
+ return "\n".join(lines) + "\n"
@@ -0,0 +1,43 @@
1
+ """StepStitch optional governed direct-write (off by default).
2
+
3
+ Delivers the already-sanitized export-preview draft to a system of record. Off unless a host
4
+ injects configured writers; human-approval-gated; audited; and never on the agent surface.
5
+ See ``base.py`` for the trust model.
6
+ """
7
+ from .base import (
8
+ DeliveryError,
9
+ DeliveryResult,
10
+ DeliveryService,
11
+ HttpPostFn,
12
+ RecordWriter,
13
+ )
14
+ from .config import enabled_targets_from_env
15
+ from .salesforce_writer import SalesforceWriter
16
+ from .servicenow_writer import ServiceNowWriter
17
+
18
+ # Reference HTTP clients require the optional [delivery] extra (httpx); import lazily so the
19
+ # core stays dependency-free.
20
+ try: # pragma: no cover - exercised via the [delivery] extra
21
+ from .clients import (
22
+ http_post_client,
23
+ salesforce_bearer,
24
+ servicenow_basic,
25
+ servicenow_bearer,
26
+ )
27
+ except Exception: # noqa: BLE001 - clients are optional
28
+ http_post_client = salesforce_bearer = servicenow_basic = servicenow_bearer = None # type: ignore
29
+
30
+ __all__ = [
31
+ "RecordWriter",
32
+ "DeliveryResult",
33
+ "DeliveryError",
34
+ "DeliveryService",
35
+ "HttpPostFn",
36
+ "ServiceNowWriter",
37
+ "SalesforceWriter",
38
+ "enabled_targets_from_env",
39
+ "http_post_client",
40
+ "servicenow_basic",
41
+ "servicenow_bearer",
42
+ "salesforce_bearer",
43
+ ]