Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 30 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,36 @@ openadapt-flow replay bundle # replay: local, $0
openadapt-flow replay bundle --drift theme # drift the UI, watch it heal
```

The replay commands serve the bundled MockMed demo app and write an
illustrated `REPORT.md` per run. Pass `--url` to replay against your own app;
recorded parameter values are the defaults and `--param` overrides them.
The last two commands serve the bundled MockMed demo app and write an
illustrated `REPORT.md` per run.

### Record your own app

`record --url` opens a headed browser on YOUR app and watches what you do —
real clicks, typing, key presses and scrolls — writing the same recording
format `compile` consumes. Perform the workflow, then press Ctrl-C (or close
the window) to finish:

```bash
openadapt-flow record --url https://your.app --out rec # do the task, Ctrl-C
openadapt-flow compile rec --out bundle --name my-task
openadapt-flow replay bundle --url https://your.app # replay it
```

Pass `--url` to `replay` to run against your own app; recorded parameter values
are the defaults and `--param` overrides them.

**Secrets never get recorded.** A `input[type=password]` field (or any field
named with `--secret <name>`) is a secret parameter: its value is never written
to the recording, the events log, the compiled bundle, or the saved frames (its
region is redacted). At replay it is injected from the environment and a missing
one fails fast:

```bash
openadapt-flow record --url https://your.app --out rec --secret password
export OPENADAPT_FLOW_SECRET_PASSWORD='…' # supplied at replay
openadapt-flow replay bundle --url https://your.app
```

**Compiled is not the same as certified safe.** `lint` reports a bundle's
coverage gaps (clicks that act with no identity check, steps that assert
Expand Down
62 changes: 62 additions & 0 deletions openadapt_flow/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
Subcommands (thin wrappers over the module APIs; sibling modules are
imported lazily inside each handler so ``--help`` always works):

- ``record`` — open a headed browser on your OWN app (``--url``) and
record what you do into the format ``compile`` consumes.
- ``demo-record`` — serve MockMed locally and record the canonical demo.
- ``compile`` — compile a recording directory into a workflow bundle.
- ``replay`` — replay a bundle; serves the bundled MockMed demo app when no
Expand Down Expand Up @@ -42,6 +44,28 @@ def _with_drift(url: str, drift: str | None) -> str:
return f"{url.rstrip('/')}/?drift={drift}"


def _cmd_record(args: argparse.Namespace) -> int:
from openadapt_flow.interactive_recorder import record_interactive

out = record_interactive(
args.url,
Path(args.out),
secret_fields=tuple(args.secret or ()),
param_fields=tuple(args.param or ()),
headless=args.headless,
)
print(f"Recording written to {out}")
secrets = sorted(args.secret or ())
if secrets:
print(
"Secret field(s) recorded (values NOT stored): "
+ ", ".join(secrets)
+ ". At replay, export "
+ ", ".join(f"OPENADAPT_FLOW_SECRET_{name.upper()}" for name in secrets)
)
return 0


def _cmd_demo_record(args: argparse.Namespace) -> int:
from openadapt_flow.demo_driver import record_triage_demo
from openadapt_flow.mockmed.server import serve
Expand Down Expand Up @@ -306,6 +330,44 @@ def build_parser() -> argparse.ArgumentParser:
)
sub = parser.add_subparsers(dest="command", required=True)

p = sub.add_parser(
"record",
help="Record YOUR app interactively in a headed browser (--url)",
)
p.add_argument(
"--url", required=True, help="URL of the app to record against"
)
p.add_argument("--out", required=True, help="Recording output directory")
p.add_argument(
"--secret",
action="append",
default=[],
metavar="FIELD",
help=(
"Mark a typed field (by name or id) as a SECRET; its value is "
"never persisted and is injected at replay from "
"OPENADAPT_FLOW_SECRET_<FIELD>. input[type=password] is always "
"treated as secret. Repeatable."
),
)
p.add_argument(
"--param",
action="append",
default=[],
metavar="FIELD",
help=(
"Record a typed field (by name or id) as a PARAMETER; its "
"demonstrated value becomes the default, overridable at replay "
"with --param. Repeatable."
),
)
p.add_argument(
"--headless",
action="store_true",
help="Run the browser headless (scripted/CI recording)",
)
p.set_defaults(func=_cmd_record)

p = sub.add_parser(
"demo-record",
help="Serve MockMed and record the canonical triage demo",
Expand Down
15 changes: 13 additions & 2 deletions openadapt_flow/compiler/compile.py
Original file line number Diff line number Diff line change
Expand Up @@ -929,7 +929,15 @@ def cached_lines(i: int, suffix: str, png: bytes) -> list[OcrLine]:
elif kind == "type":
param = event.get("param")
text = event.get("text")
if param:
secret = bool(event.get("secret"))
if secret:
# A secret's literal value is never in the recording, so it
# is never in the bundle either: the step carries only the
# param name, and the value is injected from the environment
# at replay (see ir.Step.secret / runtime.Replayer).
text = None
intent = f"type <{param}> (secret)"
elif param:
intent = f"type <{param}>"
else:
intent = f"type '{_text_preview(text or '')}'"
Expand All @@ -941,6 +949,7 @@ def cached_lines(i: int, suffix: str, png: bytes) -> list[OcrLine]:
action=ActionKind.TYPE,
text=text,
param=param,
secret=secret,
),
before_png,
after_png,
Expand Down Expand Up @@ -1023,7 +1032,8 @@ def cached_lines(i: int, suffix: str, png: bytes) -> list[OcrLine]:
# A parameterized TYPE step's changed region is the typed
# value's own pixels — never assert it (it varies per run).
include_region_stable=not (
step.action is ActionKind.TYPE and step.param is not None
step.action is ActionKind.TYPE
and (step.param is not None or step.secret)
),
before_lines=(
cached_lines(i, "before", step_before)
Expand Down Expand Up @@ -1079,6 +1089,7 @@ def cached_lines(i: int, suffix: str, png: bytes) -> list[OcrLine]:
recording_id=meta.get("id"),
viewport=tuple(viewport) if viewport else None,
params=params,
secret_params=list(meta.get("secret_params") or []),
steps=steps,
)

Expand Down
Loading
Loading