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.
- stepstitch_service-0.5.0/PKG-INFO +22 -0
- stepstitch_service-0.5.0/pyproject.toml +101 -0
- stepstitch_service-0.5.0/setup.cfg +4 -0
- stepstitch_service-0.5.0/stepstitch_service/__init__.py +48 -0
- stepstitch_service-0.5.0/stepstitch_service/compiler.py +226 -0
- stepstitch_service-0.5.0/stepstitch_service/compliance.py +231 -0
- stepstitch_service-0.5.0/stepstitch_service/delivery/__init__.py +43 -0
- stepstitch_service-0.5.0/stepstitch_service/delivery/base.py +113 -0
- stepstitch_service-0.5.0/stepstitch_service/delivery/clients.py +112 -0
- stepstitch_service-0.5.0/stepstitch_service/delivery/config.py +17 -0
- stepstitch_service-0.5.0/stepstitch_service/delivery/salesforce_writer.py +28 -0
- stepstitch_service-0.5.0/stepstitch_service/delivery/servicenow_writer.py +31 -0
- stepstitch_service-0.5.0/stepstitch_service/github_bridge/__init__.py +39 -0
- stepstitch_service-0.5.0/stepstitch_service/github_bridge/bridge.py +119 -0
- stepstitch_service-0.5.0/stepstitch_service/github_bridge/client.py +135 -0
- stepstitch_service-0.5.0/stepstitch_service/github_bridge/content.py +72 -0
- stepstitch_service-0.5.0/stepstitch_service/github_bridge/workflow.py +67 -0
- stepstitch_service-0.5.0/stepstitch_service/integrations/__init__.py +28 -0
- stepstitch_service-0.5.0/stepstitch_service/integrations/base.py +175 -0
- stepstitch_service-0.5.0/stepstitch_service/integrations/bundle.py +82 -0
- stepstitch_service-0.5.0/stepstitch_service/integrations/conformance.py +71 -0
- stepstitch_service-0.5.0/stepstitch_service/integrations/contrib/__init__.py +5 -0
- stepstitch_service-0.5.0/stepstitch_service/integrations/contrib/jira.py +39 -0
- stepstitch_service-0.5.0/stepstitch_service/integrations/contrib/zendesk.py +36 -0
- stepstitch_service-0.5.0/stepstitch_service/integrations/genesys.py +47 -0
- stepstitch_service-0.5.0/stepstitch_service/integrations/salesforce.py +59 -0
- stepstitch_service-0.5.0/stepstitch_service/integrations/servicenow.py +91 -0
- stepstitch_service-0.5.0/stepstitch_service/integrations/validation.py +52 -0
- stepstitch_service-0.5.0/stepstitch_service/mcp_cli.py +51 -0
- stepstitch_service-0.5.0/stepstitch_service/mcp_server.py +298 -0
- stepstitch_service-0.5.0/stepstitch_service/profiles.py +126 -0
- stepstitch_service-0.5.0/stepstitch_service/replayability.py +142 -0
- stepstitch_service-0.5.0/stepstitch_service/retention.py +64 -0
- stepstitch_service-0.5.0/stepstitch_service/router.py +784 -0
- stepstitch_service-0.5.0/stepstitch_service/scrubber.py +399 -0
- stepstitch_service-0.5.0/stepstitch_service/verification/__init__.py +20 -0
- stepstitch_service-0.5.0/stepstitch_service/verification/verdict.py +52 -0
- stepstitch_service-0.5.0/stepstitch_service.egg-info/PKG-INFO +22 -0
- stepstitch_service-0.5.0/stepstitch_service.egg-info/SOURCES.txt +66 -0
- stepstitch_service-0.5.0/stepstitch_service.egg-info/dependency_links.txt +1 -0
- stepstitch_service-0.5.0/stepstitch_service.egg-info/requires.txt +21 -0
- stepstitch_service-0.5.0/stepstitch_service.egg-info/top_level.txt +1 -0
- stepstitch_service-0.5.0/tests/test_audit_endpoint.py +99 -0
- stepstitch_service-0.5.0/tests/test_compiler.py +89 -0
- stepstitch_service-0.5.0/tests/test_compliance.py +62 -0
- stepstitch_service-0.5.0/tests/test_connector_platform.py +69 -0
- stepstitch_service-0.5.0/tests/test_copilot_pack.py +29 -0
- stepstitch_service-0.5.0/tests/test_copilot_surface.py +197 -0
- stepstitch_service-0.5.0/tests/test_delivery.py +162 -0
- stepstitch_service-0.5.0/tests/test_delivery_clients.py +110 -0
- stepstitch_service-0.5.0/tests/test_demo_bundle.py +140 -0
- stepstitch_service-0.5.0/tests/test_github_bridge.py +95 -0
- stepstitch_service-0.5.0/tests/test_github_client.py +65 -0
- stepstitch_service-0.5.0/tests/test_github_content.py +74 -0
- stepstitch_service-0.5.0/tests/test_github_endpoints.py +142 -0
- stepstitch_service-0.5.0/tests/test_golden_path.py +160 -0
- stepstitch_service-0.5.0/tests/test_integrations.py +154 -0
- stepstitch_service-0.5.0/tests/test_mcp_surface.py +237 -0
- stepstitch_service-0.5.0/tests/test_open_core_boundary.py +136 -0
- stepstitch_service-0.5.0/tests/test_profiles.py +65 -0
- stepstitch_service-0.5.0/tests/test_replayability.py +92 -0
- stepstitch_service-0.5.0/tests/test_repro_eval.py +88 -0
- stepstitch_service-0.5.0/tests/test_retention.py +66 -0
- stepstitch_service-0.5.0/tests/test_router_smoke.py +181 -0
- stepstitch_service-0.5.0/tests/test_scrub_overrides.py +76 -0
- stepstitch_service-0.5.0/tests/test_scrubber.py +284 -0
- stepstitch_service-0.5.0/tests/test_verdict.py +31 -0
- 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,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
|
+
]
|