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.
- zu_shadow-0.1.13/.gitignore +66 -0
- zu_shadow-0.1.13/PKG-INFO +81 -0
- zu_shadow-0.1.13/README.md +58 -0
- zu_shadow-0.1.13/pyproject.toml +38 -0
- zu_shadow-0.1.13/src/zu_shadow/__init__.py +46 -0
- zu_shadow-0.1.13/src/zu_shadow/capture.py +119 -0
- zu_shadow-0.1.13/src/zu_shadow/executor.py +273 -0
- zu_shadow-0.1.13/src/zu_shadow/live.py +106 -0
- zu_shadow-0.1.13/src/zu_shadow/live_capture.py +340 -0
- zu_shadow-0.1.13/src/zu_shadow/live_executor.py +242 -0
- zu_shadow-0.1.13/src/zu_shadow/recorder.py +190 -0
- zu_shadow-0.1.13/src/zu_shadow/redaction.py +213 -0
- zu_shadow-0.1.13/src/zu_shadow/replay_gate.py +133 -0
- zu_shadow-0.1.13/src/zu_shadow/scale.py +87 -0
- zu_shadow-0.1.13/src/zu_shadow/synthesizer.py +346 -0
- zu_shadow-0.1.13/tests/__init__.py +0 -0
- zu_shadow-0.1.13/tests/test_conformance_audit4.py +64 -0
- zu_shadow-0.1.13/tests/test_executor.py +161 -0
- zu_shadow-0.1.13/tests/test_live.py +69 -0
- zu_shadow-0.1.13/tests/test_live_capture.py +69 -0
- zu_shadow-0.1.13/tests/test_recorder.py +75 -0
- zu_shadow-0.1.13/tests/test_redaction.py +106 -0
- zu_shadow-0.1.13/tests/test_replay_gate.py +80 -0
- zu_shadow-0.1.13/tests/test_scale.py +42 -0
- zu_shadow-0.1.13/tests/test_synthesizer.py +117 -0
|
@@ -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)}
|