Skip to content

feat(flow): induce shadow-mined episodes into candidate drafts - #1212

Merged
abrichr merged 18 commits into
mainfrom
flow/shadow-motif-mining
Oct 18, 2026
Merged

abrichr merged 18 commits into
mainfrom
flow/shadow-motif-mining

Conversation

@abrichr

@abrichr abrichr commented Oct 18, 2026 •

Copy link
Copy Markdown
Member

Ports the Flow part of Play 5 (openadapt-flow feat/shadow-motif-mining) into packages/flow.

What it adds: openadapt_flow.compiler.motif_induction reads a shadow-episodes export (v2) from openadapt-capture's on-device motif mining. It turns repeated work into a candidate draft workflow. The code is deterministic, makes no model calls and uses no network. Digests stay out of drafts. A person must admit a candidate before it runs. See docs/SHADOW_CANDIDATES.md.

Includes three shadow-episode fixtures, listed in public-artifacts.json, and 26 tests in tests/test_motif_induction.py.

Related: capture side merged in openadapt-capture#156 (released in 1.3.0). Desktop side is openadapt-desktop#205.

Invariants: mining only proposes a draft. Nothing is admitted or authorized automatically, and replay of an admitted program still makes no model calls.

Local: ruff, ruff format, release-consistency pass; test_motif_induction.py 26 passed. Full suite on CI.


One admission command (added after review)

  • openadapt-flow admit <recordings...> or admit --candidate ID compiles, induces and proposes checks. It then stops once with a short summary. accept applies the checks through accept_proposal, which re-runs the oracle gate. admit decide DIR decides later at a terminal.
  • MCP: openadapt-agent serve --admissions DIR adds prepare_admission, list_admissions and decide_admission. A person decides in the client's form. With --delegate-approvals ID --approver-grant FILE, the agent decides as that delegated approver, under a grant a person signed (see below).
  • admission-decision.json names who prepared and who decided. An agent can't decide what it prepared, and regulated needs a person. A decision binds the summary digest and the bundle digest. Shadow suggestions are never applied, and nothing is signed.
  • Tests: tests/test_admit_pipeline.py (15) and packages/agent/tests/test_admission_tools.py (9). Both pass locally with the qualification-proposal, CLI-hints, motif and agent contract/CLI/README suites.

Admission sets "only problems" under a person-signed grant (added)

  • An accepted admission admits the workflow to the problems_only approval mode. Its runs stop only for problems: a failed check, a changed screen, or a step outside what was qualified. They no longer stop before every save. openadapt-flow run reads this from the bundle's admission-decision.json and prints who admitted it. A --approval-mode flag or runtime.approval_mode still wins.
  • The admitter is a person, or a delegated agent acting under an approver grant a registered person signed (openadapt-flow/approver-grant/v1 from Flow: person-signed approver grants, no self-approval, one-tap phone approval on the decision portal #1234). This replaces the unsigned delegate decision from feat(approval): delegated approvers, read-back field checks, triage and eligibility (carry-over of openadapt-flow#534) #1190. The grant must verify against the signer registry, name the agent, cover the workflow, and let an agent approve consequential saves. Otherwise the agent's decision is refused. The decision record keeps the grant's digest (policy_ref), its signer (on_behalf_of) and the signed grant itself.
  • The mode applies only to the exact admitted bundle (any later change drops it). An agent's admission applies only while its grant still verifies (expired, swapped or narrowed grants drop it). problems_only keeps its own gates, so every save still waits for a person until the switch is earned.
  • Tests: tests/test_admit_pipeline.py (25) and packages/agent/tests/test_admission_tools.py (9).

This branch now includes main and #1234's branch (merged, not rebased). Once #1234 lands, its commits drop out of this diff.

abrichr and others added 13 commits October 17, 2026 19:46
Add compiler/motif_induction.py. It validates an openadapt.shadow.episodes/v1 export from openadapt-capture's shadow miner (strict schema: digests only, no unknown fields), maps each episode to a linear Workflow, and runs the existing induce_program over them. The result is a MotifCandidate whose status is always candidate and whose admitted and runnable fields are pinned to False.

Anchors are placeholders, so record-next always starts with recording the task once. Write-shaped clicks are irreversible with risk_review_required. Checks the episodes suggest (file written, record written, row added, page reached) are proposals and never become effects. write_candidate writes candidate.json and draft-workflow.json, never workflow.json. No model calls and no network access; tests cover both.

Signed-off-by: Richard Abrich <richard.abrich@mldsai.com>
Signed-off-by: Richard Abrich <richard.abrich@mldsai.com>
Capture's shadow export moves to openadapt.shadow.episodes/v2: each keyed digest becomes a reference local to the export (label-a, value-c), so a draft never carries a digest that could be checked against guesses for a short identifier after the fact. induce_candidate now reads v2 only. It refuses v1 with a clear message, refuses label_digest and value_digest fields, and refuses a token with an 8+ character hex part. A scrubbed chrome label (<dynamic_label>) is treated as no label.

The fixtures are regenerated with Capture's v2 exporter. New tests check that no induced candidate carries a hex run other than its own export id.

Signed-off-by: Richard Abrich <richard.abrich@mldsai.com>
…mat check

Signed-off-by: Richard Abrich <richard.abrich@mldsai.com>
….json

Regenerated with check_release_consistency.py --write-public-artifact-inventory
on a clean export. Three entries, all under tests/fixtures/shadow.

Signed-off-by: Richard Abrich <richard.abrich@mldsai.com>
…epted checks

`openadapt-flow admit <recordings...>` (or `--candidate ID`) compiles each
recording, induces one program from two or more, proposes the
qualification checks, and stops once with a short summary. On accept it
applies the checks through accept_proposal, which re-runs the oracle gate,
and reseals the bundle. `admit decide DIR` decides later at a terminal.

The decision record names who prepared and who decided (kind, id,
route). An agent can't decide an admission it prepared, the regulated
pack needs a person, and a decision binds the exact summary digest and
bundle digest. Induction refusals, HALTed proposals and missing
recordings stop with nothing to accept. Shadow-suggested checks are shown
but never applied, and no admission is signed.

Signed-off-by: Richard Abrich <richard.abrich@mldsai.com>
…MCP tools

`openadapt-agent serve --admissions DIR` exposes the admit command over
MCP. Recordings and candidates are named by folder under DIR, never by
path. Without --delegate-approvals, decide_admission asks a person in the
client's form (person:mcp-form). With it, the agent decides as the
delegated approver (agent:ID). Either way the self-approval, regulated-pack
and digest-binding rules in openadapt_flow.admit_pipeline apply.

Signed-off-by: Richard Abrich <richard.abrich@mldsai.com>
…legated agent

Signed-off-by: Richard Abrich <richard.abrich@mldsai.com>
An accepted admission now admits the workflow to the problems_only approval
mode: its runs stop only for problems (a failed check, a changed screen, a
step outside what was qualified) instead of before every save.
openadapt-flow run reads this from the bundle's admission-decision.json and
prints who admitted it. A flag or runtime.approval_mode still wins.

The admitter is a person, or a delegated agent acting under an approver
grant a registered person signed (#1234's openadapt-flow/approver-grant/v1),
replacing the unsigned delegate decision. The grant must verify, name the
agent, cover the workflow and let an agent approve consequential saves. The
decision records the grant's digest, its signer and the signed grant itself.

The mode applies only to the exact admitted bundle, and an agent's admission
only while its grant still verifies. problems_only keeps its own gates, so
every save still waits for a person until the switch is earned.

MCP: openadapt-agent serve --admissions DIR --delegate-approvals ID
--approver-grant FILE.
abrichr and others added 3 commits October 18, 2026 12:20
# Conflicts:
#	packages/flow/openadapt_flow/__main__.py
Review fixes for the admission pipeline before merge:

- Sign admission-pending.json and admission-decision.json with the local
  admission key (the JIT key directory), as JIT admissions on main are.
  admitted_approval_mode refuses an unsigned or edited decision record
  (admission_signature_invalid), so write access to a bundle can no longer
  set problems_only, and an edited pending file can't change who prepared
  an admission.
- An agent can't decide an admission an unidentified MCP client
  (agent:mcp-client) prepared; that may be the same agent. A person can.
- The summaries the MCP tools return are value-free: record-next codes, not
  induction's record-next lines, which can quote recorded labels, and no
  halt reason or shadow-suggestion text.
- Test that an admitted bundle's problems_only still sends each save to a
  person until the signed switch is on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y runs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@abrichr

abrichr commented Oct 18, 2026

Copy link
Copy Markdown
Member Author

Review before merge (main merged in; __main__.py conflict resolved by keeping both admit_default_form and main's no_outbound.enforce_from_env()). Commits 3e8eb85 and dc6a1e4 are still on the branch.

Fail-open paths found and fixed in 7a9aeb6, each with a test that fails without the fix:

Problem Fix Test
admission-decision.json was unsigned. Anyone who could write to the bundle could write a "person accepted" record and get problems_only Pending and decision records are signed with the local admission key (same key and helpers as JIT admissions). admitted_approval_mode refuses an unsigned or edited record (admission_signature_invalid) test_a_decision_file_written_without_the_admission_key_admits_nothing, test_a_signed_decision_edited_afterwards_admits_nothing
An edited admission-pending.json could relabel the preparer to get around the self-approval check load_pending verifies the signature, and decide uses it test_an_edited_pending_file_is_refused, agent test_a_tampered_pending_file_is_listed_but_never_shown
A server without --delegate-approvals records its preparer as mcp-client. The same agent could then decide on a delegate server An agent can't decide what mcp-client prepared. A person still can test_an_agent_cannot_decide_what_an_unidentified_client_prepared, agent test_a_delegate_cannot_decide_what_an_unidentified_client_prepared
MCP summaries carried induction's record-next lines and halt reasons, which can quote recorded labels ("Record each of X vs Y") to a client that may forward them to a hosted model render_summary(value_free=True) for every MCP view and form: codes, counts and step ids only test_the_value_free_summary_quotes_no_recorded_text

Checked and left as is:

Docs: new packages/flow/docs/LEARN_FROM_MANY_RUNS.md (status of induce, teach-branch and shadow mining), and SHADOW_CANDIDATES.md updated for the rules above.

Follow-ups, not done here:

  • There is no CLI to turn a Capture episodes export into a candidate folder. Only the Python write_candidate exists.
  • A person's MCP form decision is recorded as person:mcp-form. The engine can't tell who filled in the form.

@abrichr
abrichr merged commit 9f270f2 into main Oct 18, 2026
31 checks passed
@abrichr
abrichr deleted the flow/shadow-motif-mining branch October 18, 2026 17:20
abrichr added a commit that referenced this pull request Oct 19, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant