zu-shadow 0.1.13__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.
@@ -0,0 +1,66 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .eggs/
6
+ build/
7
+ dist/
8
+
9
+ # uv / venv
10
+ .venv/
11
+ uv.lock.bak
12
+
13
+ # Test / type caches
14
+ .pytest_cache/
15
+ .mypy_cache/
16
+ .ruff_cache/
17
+ .coverage
18
+ htmlcov/
19
+
20
+ # Zu runtime artifacts
21
+ *.db
22
+ zu.db
23
+ zu.yaml.local
24
+ zu_review.jsonl
25
+ *.review.jsonl
26
+ # Per-agent cost telemetry ledger — machine-local run history, not source.
27
+ cost.jsonl
28
+ # A recorded replay path is learned per-run and machine-local — regenerated on
29
+ # every successful run, not source. The agent ships; its track does not.
30
+ track.json
31
+ # …except the flagship example ships its track on purpose, as a demo of the
32
+ # record/replay convergence (committed; re-runs show as ordinary modifications).
33
+ !examples/agents/vet-appointment/track.json
34
+
35
+ # Editor / OS
36
+ .idea/
37
+ .vscode/
38
+ .DS_Store
39
+
40
+ # Claude Code local session state
41
+ .claude/
42
+
43
+ # Secrets
44
+ .env
45
+ .env.*
46
+ !.env.example
47
+
48
+ # Microsoft Office temp/lock files
49
+ ~$*
50
+
51
+ # Internal design / strategy docs — kept local, never in the public repo
52
+ *.docx
53
+ *.pdf
54
+ # BUILD.md is the internal build-sequence / deferred-gaps ledger — kept local.
55
+ # (ARCHITECTURE.md is public: an onboarding agent needs the structural map.)
56
+ docs/BUILD.md
57
+
58
+ # Local secret — API key for live validation, never commit
59
+ zu_demo_key.md
60
+ *_key.md
61
+
62
+ # Local PyPI publish token — never commit
63
+ /pypi
64
+
65
+ # Local Discord credentials (bot token / app secrets) — never commit
66
+ /discord
@@ -0,0 +1,81 @@
1
+ Metadata-Version: 2.4
2
+ Name: zu-shadow
3
+ Version: 0.1.13
4
+ Summary: Zu Shadow: author a production agent by demonstration — record a human session, redact at capture, synthesize an agent + rail (§2.8)
5
+ Project-URL: Homepage, https://github.com/k3-mt/zu
6
+ Project-URL: Repository, https://github.com/k3-mt/zu
7
+ License-Expression: Apache-2.0
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: Apache Software License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
15
+ Classifier: Typing :: Typed
16
+ Requires-Python: >=3.11
17
+ Requires-Dist: zu-cli==0.2.6
18
+ Requires-Dist: zu-core==0.2.11
19
+ Provides-Extra: live
20
+ Requires-Dist: playwright>=1.40; extra == 'live'
21
+ Requires-Dist: zu-tools==0.2.7; extra == 'live'
22
+ Description-Content-Type: text/markdown
23
+
24
+ # zu-shadow — author an agent by demonstration (§2.8)
25
+
26
+ A Shadow recording **is** the event bus run over a *human* session: the human is
27
+ the policy for that one run, so recording costs almost nothing architecturally.
28
+ You drive the task once, by hand; Shadow folds your clicks, types, navigations and
29
+ the page/network metadata into `data.shadow.*` events on the same append-only log
30
+ everything else in Zu uses, then synthesizes a production agent + a rail from it.
31
+
32
+ ```
33
+ record (human session) ─▶ redact-at-capture ─▶ data.shadow.* on the log
34
+
35
+ synthesize (a Zu agent, ScriptedProvider offline)
36
+
37
+ agent spec + induced Fsm + Invariants + self-writing egress
38
+
39
+ verification-replay GATE (reuses zu-cli offline.py/build.py)
40
+
41
+ promote only if the recorded outcome reproduces
42
+ ```
43
+
44
+ ## Four load-bearing disciplines
45
+
46
+ - **Redaction is DEFAULT-ON and runs BEFORE append** (`redaction.py`). Passwords,
47
+ `Authorization`/`Cookie`/`Set-Cookie` headers, token/API-key shapes, and
48
+ consumer-configured PII are stripped — *including the "why" intent text* — before
49
+ any event reaches `EventSink.append`. The secret is gone before the event is
50
+ hashed into the audit chain. This is conformance requirement **ZU-AUDIT-4**.
51
+ - **Capture is SEMANTIC** (`capture.py`). Every action is named by its target's
52
+ `{role, name, label}` (the core `zu_core.surface` currency, shared with the §4
53
+ locator and §5 `SurfaceView`) — never a CSS selector or pixel coordinate, so the
54
+ synthesized agent re-resolves on a changed page instead of breaking.
55
+ - **The synthesizer is a Zu agent** (`synthesizer.py`). It is *driven by a*
56
+ `ModelProvider` (offline-tested with `ScriptedProvider`). The model writes only
57
+ the policy prompt + goal; the egress allowlist, the induced `Fsm`, and the
58
+ `Invariant`s are **derived deterministically** from the log — the egress allowlist
59
+ *writes itself* from the recorded `network.response` hosts. No new FSM/invariant
60
+ types: it emits `zu_core.reachability.Fsm` and `zu_core.invariants.Invariant`.
61
+ - **Promotion is GATED by reproduced outcome** (`replay_gate.py`). A synthesized
62
+ agent does not run on real data until it reproduces the recorded outcome, reusing
63
+ zu-cli's `offline.py`/`build.py`. The "why" resolutions are surfaced for **review**,
64
+ never auto-promoted.
65
+
66
+ ## The honest scope
67
+
68
+ Robustness comes from the runtime machinery — semantic re-resolution, detectors,
69
+ replay, the rail — not from a single recording. On a structurally different site
70
+ the honest behaviour is to **escalate**, not silently err. The live human recorder
71
+ (real Chromium + a real human over CDP) is demo/manual, behind the `live` extra and
72
+ a manual entrypoint (`live.py`); the offline core is fully tested against a
73
+ synthetic input/CDP stream at $0.
74
+
75
+ ## CLI
76
+
77
+ ```
78
+ zu shadow record <stream.json> --site <url> -o recording.json # synthetic/live stream → recording
79
+ zu shadow synthesize <recording.json> --instruction "…" # recording → agent + rail proposal
80
+ zu shadow scale <agent> --rows rows.csv --var <name> # one governed run per CSV row
81
+ ```
@@ -0,0 +1,58 @@
1
+ # zu-shadow — author an agent by demonstration (§2.8)
2
+
3
+ A Shadow recording **is** the event bus run over a *human* session: the human is
4
+ the policy for that one run, so recording costs almost nothing architecturally.
5
+ You drive the task once, by hand; Shadow folds your clicks, types, navigations and
6
+ the page/network metadata into `data.shadow.*` events on the same append-only log
7
+ everything else in Zu uses, then synthesizes a production agent + a rail from it.
8
+
9
+ ```
10
+ record (human session) ─▶ redact-at-capture ─▶ data.shadow.* on the log
11
+
12
+ synthesize (a Zu agent, ScriptedProvider offline)
13
+
14
+ agent spec + induced Fsm + Invariants + self-writing egress
15
+
16
+ verification-replay GATE (reuses zu-cli offline.py/build.py)
17
+
18
+ promote only if the recorded outcome reproduces
19
+ ```
20
+
21
+ ## Four load-bearing disciplines
22
+
23
+ - **Redaction is DEFAULT-ON and runs BEFORE append** (`redaction.py`). Passwords,
24
+ `Authorization`/`Cookie`/`Set-Cookie` headers, token/API-key shapes, and
25
+ consumer-configured PII are stripped — *including the "why" intent text* — before
26
+ any event reaches `EventSink.append`. The secret is gone before the event is
27
+ hashed into the audit chain. This is conformance requirement **ZU-AUDIT-4**.
28
+ - **Capture is SEMANTIC** (`capture.py`). Every action is named by its target's
29
+ `{role, name, label}` (the core `zu_core.surface` currency, shared with the §4
30
+ locator and §5 `SurfaceView`) — never a CSS selector or pixel coordinate, so the
31
+ synthesized agent re-resolves on a changed page instead of breaking.
32
+ - **The synthesizer is a Zu agent** (`synthesizer.py`). It is *driven by a*
33
+ `ModelProvider` (offline-tested with `ScriptedProvider`). The model writes only
34
+ the policy prompt + goal; the egress allowlist, the induced `Fsm`, and the
35
+ `Invariant`s are **derived deterministically** from the log — the egress allowlist
36
+ *writes itself* from the recorded `network.response` hosts. No new FSM/invariant
37
+ types: it emits `zu_core.reachability.Fsm` and `zu_core.invariants.Invariant`.
38
+ - **Promotion is GATED by reproduced outcome** (`replay_gate.py`). A synthesized
39
+ agent does not run on real data until it reproduces the recorded outcome, reusing
40
+ zu-cli's `offline.py`/`build.py`. The "why" resolutions are surfaced for **review**,
41
+ never auto-promoted.
42
+
43
+ ## The honest scope
44
+
45
+ Robustness comes from the runtime machinery — semantic re-resolution, detectors,
46
+ replay, the rail — not from a single recording. On a structurally different site
47
+ the honest behaviour is to **escalate**, not silently err. The live human recorder
48
+ (real Chromium + a real human over CDP) is demo/manual, behind the `live` extra and
49
+ a manual entrypoint (`live.py`); the offline core is fully tested against a
50
+ synthetic input/CDP stream at $0.
51
+
52
+ ## CLI
53
+
54
+ ```
55
+ zu shadow record <stream.json> --site <url> -o recording.json # synthetic/live stream → recording
56
+ zu shadow synthesize <recording.json> --instruction "…" # recording → agent + rail proposal
57
+ zu shadow scale <agent> --rows rows.csv --var <name> # one governed run per CSV row
58
+ ```
@@ -0,0 +1,38 @@
1
+ [project]
2
+ name = "zu-shadow"
3
+ version = "0.1.13"
4
+ description = "Zu Shadow: author a production agent by demonstration — record a human session, redact at capture, synthesize an agent + rail (§2.8)"
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ license = "Apache-2.0"
8
+ classifiers = [
9
+ "Development Status :: 4 - Beta",
10
+ "Intended Audience :: Developers",
11
+ "License :: OSI Approved :: Apache Software License",
12
+ "Programming Language :: Python :: 3",
13
+ "Programming Language :: Python :: 3.11",
14
+ "Programming Language :: Python :: 3.12",
15
+ "Topic :: Software Development :: Libraries :: Application Frameworks",
16
+ "Typing :: Typed",
17
+ ]
18
+ # A Shadow recording IS the event bus run over a HUMAN session, so the recorder
19
+ # only needs zu-core (events + surface types + the sink seam). The synthesizer is
20
+ # itself a Zu agent (a ModelProvider), and the verification-replay promotion gate
21
+ # REUSES zu-cli's offline.py/build.py — so zu-cli is a dependency too.
22
+ dependencies = ["zu-core==0.2.11", "zu-cli==0.2.6"]
23
+
24
+ [project.optional-dependencies]
25
+ # The LIVE recorder/capture binds a real Chrome over CDP (Playwright connects to it;
26
+ # no extra browser download) — opt-in, never needed for the offline core.
27
+ live = ["zu-tools==0.2.7", "playwright>=1.40"]
28
+
29
+ [project.urls]
30
+ Homepage = "https://github.com/k3-mt/zu"
31
+ Repository = "https://github.com/k3-mt/zu"
32
+
33
+ [build-system]
34
+ requires = ["hatchling"]
35
+ build-backend = "hatchling.build"
36
+
37
+ [tool.hatch.build.targets.wheel]
38
+ packages = ["src/zu_shadow"]
@@ -0,0 +1,46 @@
1
+ """zu-shadow — author a production agent by DEMONSTRATION (§2.8).
2
+
3
+ A Shadow recording *is* the event bus run over a HUMAN session: the human is the
4
+ policy for that one run, so recording costs almost nothing architecturally — the
5
+ recorder folds an abstract input/CDP stream into ``data.shadow.*`` events on the
6
+ same append-only log everything else uses. Four disciplines are load-bearing:
7
+
8
+ * **Redaction is DEFAULT-ON and runs BEFORE append** (``redaction``): secrets —
9
+ passwords, ``Authorization``/``Cookie`` headers, tokens/API keys, configured PII
10
+ — never reach :meth:`EventSink.append`. The "why" intent text is redacted too.
11
+ * **Capture is SEMANTIC** (``capture``): a user action is named by its target's
12
+ ``{role, name, label}`` (the core ``surface`` currency, shared with §4 handles /
13
+ §5 SurfaceView) — never a CSS selector or pixel coordinate.
14
+ * **The synthesizer is itself a Zu agent** (``synthesizer``): driven by a
15
+ ``ModelProvider`` (offline-tested with ``ScriptedProvider``), it PROPOSES an
16
+ agent spec + an induced ``Fsm`` + ``Invariant``s; the egress allowlist writes
17
+ itself from the recorded ``network.response`` hosts.
18
+ * **Promotion is GATED by reproduced outcome** (``replay_gate``): a synthesized
19
+ agent does not run on real data until it reproduces the recorded outcome, reusing
20
+ zu-cli's ``offline.py``/``build.py``. The "why" resolutions are reviewed, never
21
+ auto-promoted.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ from .capture import SemanticTarget, capture_click, capture_navigate, capture_type
27
+ from .recorder import RecordedSession, Recorder
28
+ from .redaction import RedactionPolicy, redact_event, redact_text
29
+ from .replay_gate import PromotionVerdict, verify_and_gate
30
+ from .synthesizer import SynthesisResult, Synthesizer
31
+
32
+ __all__ = [
33
+ "PromotionVerdict",
34
+ "RecordedSession",
35
+ "Recorder",
36
+ "RedactionPolicy",
37
+ "SemanticTarget",
38
+ "SynthesisResult",
39
+ "Synthesizer",
40
+ "capture_click",
41
+ "capture_navigate",
42
+ "capture_type",
43
+ "redact_event",
44
+ "redact_text",
45
+ "verify_and_gate",
46
+ ]
@@ -0,0 +1,119 @@
1
+ """SEMANTIC-TARGET capture — name an action by WHAT it acts on, not WHERE.
2
+
3
+ Every captured user action identifies its target by ``{role, name, label}`` — the
4
+ same accessibility-grounded currency the core ``surface`` types speak (§4 handles /
5
+ §5 ``SurfaceView``). NEVER a CSS selector, an XPath, or a pixel coordinate: those
6
+ are brittle (a redesign breaks them) and untransferable (they cannot feed the §4
7
+ locator / §5 recognizer). A semantic target re-resolves on a changed page, which is
8
+ the whole reason a synthesized agent can be *resilient* rather than pixel-frozen.
9
+
10
+ ``SemanticTarget`` is a thin, frozen value object that reuses ``role``/``label``
11
+ exactly as :class:`zu_core.surface.SurfaceAffordance` does, plus the accessible
12
+ ``name`` (the click target's accessible name). The capture helpers turn a raw
13
+ abstract-stream event into a redaction-ready ``data.shadow.*`` payload.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from pydantic import BaseModel
19
+
20
+ from zu_core import events as ev
21
+ from zu_core.surface import SurfaceAffordance
22
+
23
+ # Target role/name/label tokens that mark an input as a CREDENTIAL field, so the
24
+ # recorder records its typed value under a credential-named key the redaction stage
25
+ # blanks wholesale — a password is never recorded verbatim, even pre-redaction-sweep.
26
+ _CREDENTIAL_TARGET_HINTS: tuple[str, ...] = ("password", "passwd", "secret", "token",
27
+ "api key", "api_key", "apikey", "otp",
28
+ "cvv", "cvc", "pin", "security code",
29
+ # payment-card secrets — the agent must NEVER hold
30
+ # these; a real payment goes through the §8 broker.
31
+ "card number", "cardnumber", "card no",
32
+ "credit card", "debit card", "expiration", "expiry",
33
+ "iban", "sort code", "account number")
34
+
35
+
36
+ class SemanticTarget(BaseModel):
37
+ """A user-action target, identified the way the core surface currency does:
38
+ ``role`` (a free string, e.g. ``button``/``link``/``textbox``), the accessible
39
+ ``name``, and a human ``label``. NO selector, NO coordinates — re-resolvable on
40
+ a changed page. Frozen so it is a stable value on the log."""
41
+
42
+ model_config = {"frozen": True}
43
+
44
+ role: str
45
+ name: str = ""
46
+ label: str = ""
47
+
48
+ @classmethod
49
+ def from_affordance(cls, a: SurfaceAffordance, *, name: str = "") -> SemanticTarget:
50
+ """Build a target from a core ``SurfaceAffordance`` — the bridge from a §5
51
+ SurfaceView the live recorder reduced to a recorded action target. The
52
+ affordance's ``label`` carries through; ``name`` is the accessible name the
53
+ CDP locate step resolved (the affordance has no separate name field)."""
54
+ return cls(role=a.role, name=name or a.label, label=a.label)
55
+
56
+ def to_payload(self) -> dict:
57
+ return {"role": self.role, "name": self.name, "label": self.label}
58
+
59
+
60
+ def capture_click(target: SemanticTarget, *, intent: str | None = None) -> tuple[str, dict]:
61
+ """A ``data.shadow.user.click`` (type, payload). ``intent`` is the OPTIONAL,
62
+ reviewed "why" narration — carried but NEVER auto-promoted into the agent."""
63
+ payload: dict = {"target": target.to_payload()}
64
+ if intent is not None:
65
+ payload["intent"] = intent
66
+ return ev.SHADOW_USER_CLICK, payload
67
+
68
+
69
+ def _is_credential_target(target: SemanticTarget) -> bool:
70
+ """A type target whose role/name/label marks it as a credential input — so its
71
+ value is recorded under a credential-named key the redaction stage blanks."""
72
+ blob = f"{target.role} {target.name} {target.label}".lower()
73
+ return any(h in blob for h in _CREDENTIAL_TARGET_HINTS)
74
+
75
+
76
+ def capture_type(target: SemanticTarget, value: str, *,
77
+ intent: str | None = None) -> tuple[str, dict]:
78
+ """A ``data.shadow.user.type`` (type, payload). The recorder MARKS a credential
79
+ target: a password/secret field's value goes under a ``password`` key that the
80
+ redaction stage (run before append) blanks wholesale, so a credential is never
81
+ recorded verbatim. A non-credential value rides under ``value`` and is still
82
+ swept for token shapes by redaction. Capture marks; redaction enforces the floor."""
83
+ payload: dict = {"target": target.to_payload()}
84
+ if _is_credential_target(target):
85
+ payload["password"] = value # credential-named ⇒ redaction blanks it wholesale
86
+ else:
87
+ payload["value"] = value
88
+ if intent is not None:
89
+ payload["intent"] = intent
90
+ return ev.SHADOW_USER_TYPE, payload
91
+
92
+
93
+ def capture_navigate(url: str, *, intent: str | None = None) -> tuple[str, dict]:
94
+ """A ``data.shadow.user.navigate`` (type, payload). The URL is redaction-swept
95
+ (credentials/tokens in the query stripped) before it reaches the log."""
96
+ payload: dict = {"url": url}
97
+ if intent is not None:
98
+ payload["intent"] = intent
99
+ return ev.SHADOW_USER_NAVIGATE, payload
100
+
101
+
102
+ def capture_page_loaded(url: str, title: str) -> tuple[str, dict]:
103
+ """A ``data.shadow.page.loaded`` (type, payload) — a settled page; the locus a
104
+ subsequent action's semantic target re-resolves against."""
105
+ return ev.SHADOW_PAGE_LOADED, {"url": url, "title": title}
106
+
107
+
108
+ def capture_network_response(url: str, status: int, host: str) -> tuple[str, dict]:
109
+ """A ``data.shadow.network.response`` (type, payload) — METADATA only (no body,
110
+ no headers beyond the host). The synthesized agent's egress allowlist is induced
111
+ from the ``host`` values across these events."""
112
+ return ev.SHADOW_NETWORK_RESPONSE, {"url": url, "status": status, "host": host}
113
+
114
+
115
+ def capture_scroll(direction: str, y: int = 0) -> tuple[str, dict]:
116
+ """A ``data.shadow.user.scroll`` (type, payload) — a settled scroll up/down. Context,
117
+ not an action step: it records that the human had to scroll to reach the next thing."""
118
+ d = direction if direction in ("up", "down") else "down"
119
+ return ev.SHADOW_USER_SCROLL, {"direction": d, "y": int(y)}