Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
3d4ac35
feat: complete pluggable architecture with Signal-Learn-Evolve loop
wangxingjun778 Aug 18, 2026
2ea5e36
feat(engine): prefer registry handlers over ToolBridge dispatch (Land…
wangxingjun778 Aug 18, 2026
4947dc4
feat(perception): add TelegramBotSignalSource as active signal source
wangxingjun778 Aug 18, 2026
b0d3861
feat(marketplace): add Ed25519 signature verification for plugin inst…
wangxingjun778 Aug 18, 2026
fe8ea46
refactor: promote plugins to first-class module
wangxingjun778 Aug 19, 2026
2396387
feat: harden plugin lifecycle governance
wangxingjun778 Aug 19, 2026
a378910
Add governed plugin proposals and rollback
wangxingjun778 Aug 19, 2026
5f343fd
feat(learning): add Plugin Compatibility Assessment Engine (P0)
wangxingjun778 Aug 20, 2026
3ef6d82
feat(learning): complete Compatibility Assessment Engine P1
wangxingjun778 Aug 20, 2026
b8fc277
feat(learning): P2 adapter generator, manifest converter, file-path l…
wangxingjun778 Aug 20, 2026
68df756
feat(domain): Cordis P0 — extended fiber states + scope-bound EventBus
wangxingjun778 Aug 20, 2026
6e77a83
feat(plugins): Cordis P1 — dependency-driven fiber activation + topol…
wangxingjun778 Aug 20, 2026
60b1279
feat(domain): Cordis P2 — waterfall tool pipeline + async EffectScope…
wangxingjun778 Aug 20, 2026
9c572a6
feat: engine pipeline integration + adapter escaping + experiment upg…
wangxingjun778 Aug 20, 2026
cb33e0f
remove unused doc
wangxingjun778 Aug 20, 2026
0b855ec
bump version
wangxingjun778 Aug 20, 2026
c851538
feat(cli): /plugin generate slash command + review fixes + docs
wangxingjun778 Aug 20, 2026
206a3ca
config: enable plugin_generation_enabled by default
wangxingjun778 Aug 20, 2026
75976de
fix(daemon): heartbeat for long-running non-streaming RPCs
wangxingjun778 Aug 20, 2026
0781407
feat: add adaptive plugin capability workflow
wangxingjun778 Aug 24, 2026
9fbc75e
feat: advance adaptive plugin autonomy and runtime safety
wangxingjun778 Aug 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
23 changes: 22 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ This document is the LeapFlow engineering collaboration contract. It is not only
- **Cross-Session Fallbacks Must Be Named For What They Do**: a resolver that answers "whichever session was most recently active" ignores workspace and client identity, so it is valid only for genuinely aggregate views (a dashboard summarizing all activity). Such helpers must say so in their name (e.g. `most_recent_any_client()`), and no code path that describes one caller may use them. A friendly name like "the current session" invites exactly the misuse that leaked one client's identity to another.
- **Workspace Binding Is Part of Session Identity**: a session is bound to the workspace of its first request, and reuse from another workspace is refused. Because correct clients cannot trigger this, a mismatch means either an explicit `--resume` into another workspace (the only legitimate cause, and what the message must name) or a defect — never something to tell the user to work around.
- **Terminal Output Must Wrap at the Console Layer**: `LeapConsole` owns wrapping (`soft_wrap=False`), because prompt_toolkit's renderer clips at the window edge rather than reflowing. Never enable `soft_wrap` on the shared console or hand it pre-formatted long lines; long answers silently lose their tail. A standalone `Console` for fixed-width art (e.g. the banner) may opt out, but must set an explicit `width`.
- **Immutable Domain Types**: Use `@dataclass(frozen=True)` or `NamedTuple` for domain objects
- **Immutable Domain Types**: Use `@dataclass(frozen=True)` or `NamedTuple` for domain objects — but never for an exception type. CPython stores the traceback on the instance and every Python-level re-raise assigns `__traceback__` through `__setattr__` (`contextlib.__exit__`, asyncio, and pytest all do), so a frozen exception raises `FrozenInstanceError` there and *replaces* the real failure with that noise: a driver answering "Unknown tool" was reported for months as "cannot assign to field '__traceback__'".
- **Config-Driven Behavior**: Thresholds, intervals, feature flags, model budgets, platform capabilities, hub backends, gateway manifests, and paths must be configurable through Settings/env/config layers.
- **Graceful Degradation**: Every optional component (LLM, Hub) can be absent without crash
- **Single Source of Truth**: DuckDB for persistence, EventBus for communication, Settings for configuration
Expand Down Expand Up @@ -79,6 +79,22 @@ This document is the LeapFlow engineering collaboration contract. It is not only
- **Safety follows path semantics**: daemon sockets, pid/lock files, runtime state, DuckDB files, vault files, approval grants, audit logs, and memory stores must flow through layout descriptors, path sensitivity, risk, approval, and redaction gates.
- **No legacy aliases**: do not reintroduce global `.env` as persistent config, flat cache roots, profile-root gateway config, inline credential files, `.credential_key`, or `run/` runtime paths.

## Sensitive Capability and Approval Rules

Every capability that changes the world outside the current turn — shell execution, sensitive file read/write, config mutation, outbound sends, platform actions, network egress, desktop control, plugin self-modification — reaches the user through **one** approval chain. Wiring a new one is a fixed sequence, not a design exercise: each rule below is the residue of a defect that shipped with a green suite.

- **One orchestrator is the only entry point (MANDATORY)**: build an `ActionDescriptor` — its `kind`/`effect`/`resource`/`metadata` *is* the contract — and call `ApprovalOrchestrator.evaluate()`, which supplies risk classification, policy, existing grants, and the audit record for free. Never hand-roll a confirmation prompt, a per-tool approval allow-list, or a second gate implementation. Never call the orchestrator's legacy `check(command)`: it is the single-argument shell adapter, and a gate exposing only `check()` must be treated as unusable and denied rather than assumed permissive. `config_set` copied `file_write`'s four-argument gate call and failed on every invocation with `check() takes 2 positional arguments but 4 were given`, so no model-driven config change ever reached approval — invisibly, because the test fakes implemented the same wrong signature.
- **Register the gate in both installation sites**: gates are process-global and injected twice — in-process through `cli/context.py`, daemon-side through `ApprovalCoordinator.install_gate()`. A new sensitive capability must be wired in both, or it behaves differently depending on whether `leapd` is running. Where a mode legitimately cannot supply a gate (in-process CLI binds no `plugin_approval_gate`), the tool documents it and fails closed instead of proceeding unguarded.
- **Fail closed on absence and on exception (MANDATORY)**: no gate installed, no per-turn route, or a gate that raises all mean deny, with a message the model can act on — a broken gate must never become an open door. Keep the `except` narrow, and never raise from inside one `except` branch expecting a sibling `except` to catch it; that is how a `TypeError` fallback escaped its handler and surfaced raw instead of failing closed.
- **Feasibility precedes consent**: an action that cannot succeed must never reach a human. The order is fixed — dedup → payload validation → capability feasibility (`CapabilityHealthLedger`, `blocks_approval`) → resource provenance → approval → execute. Missing scopes, degraded capabilities, and admin-required failures return a deterministic repair instruction and hard-stop the turn, with `security/permission_failures.py` as the single authority both engine and TUI consult. Prompting for consent to a call that will be refused for lack of permission teaches users to click through prompts.
- **Gate the action that actually executes, at every hop**: classify once and the transport can still go somewhere else. `web_fetch`'s egress gate only saw the first hop while the transports followed redirects themselves, so any server answering `302` could bounce an approved public URL to loopback or a cloud metadata endpoint. Transports are single-hop and report `Location`; the caller re-classifies and re-gates each hop, and reachability asks `is_global` rather than `not is_private` so unenumerated non-routable ranges fail closed.
- **The decision vocabulary is one enum across the process boundary**: `ApprovalDecision` is the entire vocabulary, so adding or renaming a value means updating the enum, the orchestrator's `_choices` and scope mapping, `ApprovalCoordinator._normalize_decision`, the TUI modal, and the RPC together. The daemon normalizer kept a hardcoded allowed-set that predated `ALLOW_ALL_SESSION`, silently rewrote that choice to `deny`, and never armed the session bypass it was meant to grant — explicit user consent became a refusal with no error logged anywhere.
- **Scope is the grant's contract, and a bypass is resolved in one predicate**: grants persist only at `SESSION`/`PROFILE` scope via `ApprovalScope`, and `HIGH`/`CRITICAL` risk sets `allow_permanent=False` so "always allow" is never offered for actions that change the agent's own composition or reach internal addresses. A bypass — config `approval_bypass` or a session-level "allow all" — is answered by a single predicate every gate consults, so it cannot mean "approved" at one gate and "still ask" at the next. Hardline blocks sit above all of it and are never bypassable by any grant, scope, or bypass flag.
- **Prompt and audit text is redacted at the descriptor**: `ApprovalRequest.detail` is both rendered to the user and persisted to the JSONL audit log, so secrets are removed when the descriptor is built, not when it is displayed. URL query, fragment, and userinfo are stripped; config values are excluded entirely and only the key is shown. Approval details were carrying API keys and signed tokens out of query strings straight into the audit log.
- **Daemon approvals are turn-routed and terminally resolved**: prompts travel on the per-turn `approval_route` ContextVar `(queue, request_id)`, so a prompt never surfaces in a client that did not cause it. Every pending future needs a terminal path — `deny_for_request` when the turn ends, `deny_for_queue` when the stream closes, `prune_stale` on TTL — because an unresolved future blocks its tool call forever. The ContextVar must be set and reset within the *same* `contextvars.Context`; setting it in one per-chunk task and resetting it in another raised "was created in a different Context" and broke streaming for every gated action.
- **A denial is terminal and reaches the model verbatim**: `ApprovalResult.denial_message` states that the user did not consent and that the outcome must not be retried, rephrased, or pursued through another tool. An adapter wrapping a gate must capture that message and return it to the caller; substituting a generic tool error lets the agent reroute around the human's refusal.
- **Approval invariants are tested against the real orchestrator**: `tests/test_approval_layer.py` drives the production `ApprovalOrchestrator` — per-scope grant reuse, hardline deny without prompting, `allow_permanent` suppression, decision round-trips through the RPC vocabulary, denial-message pass-through. A hand-written fake is acceptable only as the *human surface*; a fake that reimplements the orchestrator's own interface will keep agreeing with the caller's mistake, which is precisely how a dead gate stayed green.

## Implementation Guidelines

- Define the Protocol first — the contract is the design
Expand Down Expand Up @@ -111,6 +127,7 @@ This document is the LeapFlow engineering collaboration contract. It is not only
- Equally in scope: dispatch and routing, prompts and confirmations, emitted output, browser/dashboard launches, background work the command triggers (watches, schedules, re-entries), and error/recovery messaging.
- Passing tests and a clean lint run are NOT a substitute for confirmation. Slash commands are the primary user-facing control plane; correctness of the visible behavior is only established by a human check.
- State the pending confirmation explicitly in the handoff, and name the behavior a human should exercise to verify it.
- **Human confirmation for approval-path changes**: any change to what reaches the approval chain — a newly gated capability, a new `ActionKind`, an `ApprovalDecision`/scope/bypass change, or gate registration — requires exercising the real prompt by hand in *both* in-process and daemon mode before it is considered ready. Gates are process-global and injected twice, so a green suite proves at most that one of the two wirings works; every approval defect recorded in this document passed its tests.
- **Design goal check**: Verify that the implementation actually achieves the intended design goal and is not just a local patch.
- **Optimality check**: Evaluate whether the solution is the simplest robust design, avoids unnecessary abstractions, and fits the existing architecture.
- **Regression impact check**: Inspect affected modules and user journeys for logic bugs, degraded UX, broken compatibility, slower feedback, weaker diagnostics, or worse failure recovery.
Expand Down Expand Up @@ -175,6 +192,10 @@ Each journey also declares two cost ceilings, both enforced at the proxy and rep
- Multiple parallel error-handling paths for the same failure domain (LLM errors in one handler, tool errors in another, security errors in a third); use a unified classification and coordination pipeline
- Widening a provider call's `try` block to cover bookkeeping, or classifying a Python-level defect by matching its message text against provider conditions
- Answering "the caller's session" with "the most recently active session", or letting a client adopt a session id the daemon happened to report
- Hand-rolled confirmation prompts, per-tool approval allow-lists, or any second gate beside `ApprovalOrchestrator`; a sensitive capability declares an `ActionDescriptor` and evaluates it
- Treating a missing, unbound, or raising approval gate as permission to proceed
- Putting a secret, token, or config value into `ApprovalRequest.detail` — it is rendered to the user *and* persisted to the audit log
- Extending `ApprovalDecision` without updating the daemon normalizer, TUI modal, and RPC in the same change
- Bare `except:` clauses — always specify the exception type
- `# TODO: implement` stubs — implement or don't commit

Expand Down
Loading
Loading