diff --git a/.devkit.yml b/.devkit.yml index 0f5a5e2..1d37a4e 100644 --- a/.devkit.yml +++ b/.devkit.yml @@ -1,4 +1,4 @@ -plugin_dir: usr/plugins/share_chat +plugin_dir: . display_name: Share Chat # If the plugin has agent-driven behaviour needing an e2e seam, enable it here: # e2e_pod_env: diff --git a/.github/workflows/plugin-e2e.yml b/.github/workflows/plugin-e2e.yml index 587c7c9..27851ab 100644 --- a/.github/workflows/plugin-e2e.yml +++ b/.github/workflows/plugin-e2e.yml @@ -26,6 +26,10 @@ name: plugin-e2e on: pull_request: branches: ["main", "master"] + # push on main: proves the shipped code, and gives the README badge a status + # (PR-only runs leave branch=main with zero runs → "no status"). + push: + branches: ["main", "master"] workflow_dispatch: inputs: capture_all_traces: @@ -35,8 +39,8 @@ on: permissions: contents: read - pull-requests: write # merge-guard drafts a PR whose plugin-e2e is red - actions: write # DEC-074 count-based artifact retention: delete superseded e2e artifacts + pull-requests: write # merge-guard drafts a PR whose plugin-e2e is red + actions: write # DEC-074 count-based artifact retention: delete superseded e2e artifacts jobs: e2e: diff --git a/Makefile b/Makefile index c954001..cd9dedb 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,3 @@ -PLUGIN_DIR := usr/plugins/share_chat +PLUGIN_DIR := . PLUGIN_DISPLAY_NAME := Share Chat -include tests/_testkit/e2e/Makefile.devkit diff --git a/README.md b/README.md index 24bdc25..bba5ceb 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Share Chat — Agent Zero Plugin +# Share Chat — Agent Zero plugin @@ -6,46 +6,25 @@ -[![plugin-e2e](https://github.com/agent-zero-plugins/agent-zero-plugin-share-chat/actions/workflows/plugin-e2e.yml/badge.svg)](https://github.com/agent-zero-plugins/agent-zero-plugin-share-chat/actions/workflows/plugin-e2e.yml) -License: [Apache-2.0](LICENSE) +> One click to share the conversation you're looking at. **Share Chat** adds a small circular share button to the top bar of the Agent Zero chat area. Clicking it copies a deep link to the currently open conversation to your clipboard, paste it into a message, a ticket, or another browser tab, and Agent Zero opens straight to that chat. -One click to share the conversation you're looking at. **Share Chat** adds a small circular -share button to the top bar of the Agent Zero chat area. Clicking it copies a **deep link** -to the currently open conversation to your clipboard — paste it into a message, a ticket, or -another browser tab, and Agent Zero opens straight to that chat. +

+ Share Chat in action +

-![Share button in the chat top bar](docs/screenshot-idle.png) +## ✨ Features -## Why +- **One-click deep link** — copies `/?ctxid=` to the clipboard, ready to paste into a message, a ticket, or another browser tab. +- **Instant confirmation** — the button flips to a green ✓ _"Link copied!"_ and reverts after ~2 seconds, so you know it worked without dismissing anything. +- **Client-side only** — the link is composed entirely in your browser. No backend, no state, no tokens, no network calls. +- **Native feel** — a `chat-top-end` extension styled like the rest of A0's toolbar controls. +- **Safe by default** — no chat selected → the button is a no-op, and the link grants no access by itself (see the access model below). -- **Zero-friction hand-off** — point a teammate (or your future self on another device) at the - exact conversation, not "the chat somewhere in my sidebar". -- **No backend, no state, no tokens** — the link is composed entirely client-side as - `/?ctxid=`. Nothing is stored, published, or proxied. -- **Native feel** — the button matches A0's toolbar styling, confirms with a green check - ("Link copied!") and auto-reverts after ~2 seconds. +## ⚙️ How it works -![Copied confirmation state](docs/screenshot-copied.png) - -## How it works - -```mermaid -flowchart LR - subgraph Browser ["Your browser (client-side only)"] - BTN["Share button
(chat-top-end extension)"] -->|click| CTX["getContext()
current chat id"] - CTX --> LINK["Compose deep link
origin + ?ctxid=<id>"] - LINK --> CLIP["navigator.clipboard
(textarea fallback)"] - CLIP --> CONFIRM["✓ Link copied!
auto-reset 2s"] - end - subgraph Recipient ["Recipient's browser"] - URL["Opens link with ?ctxid"] --> STORE["A0 chats-store.init()
reads & strips ctxid"] - STORE --> OPEN["Selects that chat"] - end - LINK -.paste anywhere.-> URL -``` +![How it works](docs/how-it-works.png) -The plugin only **produces** the link. The **consume** half — opening `?ctxid=` links — is a -pre-existing Agent Zero capability the plugin piggybacks on. +The plugin only **produces** the link. The **consume** half — opening `?ctxid=` links — is a pre-existing Agent Zero capability it piggybacks on. ### Access model (honest edition) @@ -53,108 +32,60 @@ The deep link is **not** a public share page and grants **no access by itself**: - The recipient must be able to reach your Agent Zero instance **and** authenticate to it. An unauthenticated visitor hits the A0 login screen, not your conversation. -- The `ctxid` identifies a chat *within* your instance; it is not a bearer token. +- The `ctxid` identifies a chat _within_ your instance; it is not a bearer token. - If no chat is selected, the button is a safe no-op. See [SECURITY.md](SECURITY.md) for the full threat model. -## Usage - -Every behaviour below is covered by a BDD scenario in [`tests/e2e/features/`](tests/e2e/features/), -and [`docs/BEHAVIOUR.md`](docs/BEHAVIOUR.md) shows each one as a screenshot captured from a passing -run — so what you read here is what CI proves on every push. - -### Share the chat you are looking at +## 💬 Usage -Click the share control in the chat toolbar. The link to the current chat goes straight to your -clipboard, and the button confirms with a green *"Link copied!"* that reverts after ~2 seconds — so -you know it worked without a dialog to dismiss. - -Paste it to a teammate, or to yourself on another device, and they land on **that** conversation -rather than hunting the sidebar for "the one about the deploy". - -### What the link actually is +Click the share control in the chat toolbar. That's the whole feature — and that's on purpose: ``` /?ctxid= ``` -Composed entirely client-side. **Nothing is stored, published, proxied, or sent anywhere** — there is -no backend, no state and no token. The link is only as reachable as your A0 instance already is: if -the recipient cannot reach your origin, or is not authenticated to it, the link does nothing for them. - -That is the deliberate trade-off. This plugin is a fast hand-off *within* an environment people -already share, not a publishing tool. If you need a chat readable by someone with no access to your -instance, this is the wrong plugin — export it instead. - -### Scope, stated honestly - -The suite asserts three scenarios: the control is present, sharing copies a link to *this* chat, and -the copy is confirmed. That is the whole feature — it is small on purpose, and the coverage matches -the surface rather than being padded to look thorough. - ---- +![Copied confirmation state](docs/screenshot-copied.png) -## Install +Every behaviour has an asserted [BDD scenario](tests/e2e/features/), and [`docs/BEHAVIOUR.md`](docs/BEHAVIOUR.md) shows each as a screenshot from a passing run. -### From the Plugin Hub +## 📦 Installation -1. Open **Settings → Plugins** in Agent Zero. -2. Find **Share Chat** and click install, then enable it. -3. Open any chat — the share button appears at the right end of the top bar. +**Plugin Hub** — once listed in the [Plugin Index](https://github.com/agent0ai/a0-plugins): **Settings → Plugins → Share Chat → Install**. -### Manual install +**Manual** (Zip) — build it yourself, or grab the zip from the [latest release](https://github.com/agent-zero-plugins/agent-zero-plugin-share-chat/releases/latest): ```bash -cd /a0/usr/plugins -git clone https://github.com/agent-zero-plugins/agent-zero-plugin-share-chat.git _tmp_share_chat -cp -r _tmp_share_chat/usr/plugins/share_chat ./share_chat -rm -rf _tmp_share_chat +make package # → dist/share_chat.zip ``` -Then enable **Share Chat** in **Settings → Plugins**. - -## Configuration +Then **Settings → Plugins → Install from file** → pick the zip. Or copy the plugin into your instance (`usr/plugins/share_chat/`) and reload plugins. -There is none — by design. `default_config.yaml` declares no options, there is no config -screen, no per-project and no per-agent settings. The plugin is a single declarative HTML -extension (`extensions/webui/chat-top-end/share-chat.html`). +## 🔧 Configuration -| Manifest field | Value | -|---|---| -| `name` | `share_chat` | -| `version` | `0.1.0` | -| `license` | `Apache-2.0` | -| `always_enabled` | `false` (opt-in) | -| `per_project_config` / `per_agent_config` | `false` / `false` | +None — by design. `default_config.yaml` declares no options, there is no config screen, no per-project and no per-agent settings. The plugin is a single declarative HTML extension (`extensions/webui/chat-top-end/share-chat.html`). -## Development & testing - -The repo follows the [agent-zero-plugins devkit v2](https://github.com/agent-zero-plugins/agent-zero-plugin-development-testkit) -layout: plugin source under `usr/plugins/share_chat/`, devkit as the `tests/_testkit` submodule. +## 🛠️ Development ```bash -git clone --recursive https://github.com/agent-zero-plugins/agent-zero-plugin-share-chat.git +git clone --recurse-submodules https://github.com/agent-zero-plugins/agent-zero-plugin-share-chat cd agent-zero-plugin-share-chat -# L1 shape suite (fast, no A0 boot) -pip install pytest pyyaml -pytest tests/component -v - -# Tier-1 static BDD gates (feature purity, honesty, traceability) -make verify - -# Full local BDD e2e loop (boots a disposable nested A0) -make bdd-e2e +make verify # BDD static gates +python -m pytest tests -q # L1 shape suite (needs the tests/_testkit submodule) +make e2e # behaviour BDD on a nested disposable A0 (podman) ``` -Behaviour contract lives in [`docs/spec/behaviour-spec.md`](docs/spec/behaviour-spec.md) -(BEH-1…BEH-11) and is mirrored by the executable Gherkin in -[`tests/e2e/features/`](tests/e2e/features/). CI (`plugin-e2e`) runs lint → seam-off -red-proof → BDD e2e on every PR. +## 🧪 Test layout + +| Layer | Where | What | +| ------------- | -------------------------------------- | ---------------------------------------------------------- | +| L1 shape | `tests/component/test_plugin_shape.py` | static validator, deps/A0-API audits, dead hooks | +| Static gates | `make verify` | feature purity, honesty, traceability | +| Behaviour BDD | `tests/e2e/features/` + `steps/` | control present, link identifies this chat, copy confirmed | -See [CONTRIBUTING.md](CONTRIBUTING.md) for the workflow. +Specs live in [`docs/spec/`](docs/spec/) (BEH-1..11). CI runs the pyramid on every PR, including a seam-off red-proof — a scenario that passes without the plugin installed fails the build. -## License +## ⚖️ License -[Apache-2.0](LICENSE) © Interu \ No newline at end of file +Apache-2.0 — see [LICENSE](LICENSE), © Interu. Links are composed in your browser and copied to your clipboard; nothing is stored, published, or sent anywhere. Full threat model in [SECURITY.md](SECURITY.md). diff --git a/usr/plugins/share_chat/__init__.py b/__init__.py similarity index 100% rename from usr/plugins/share_chat/__init__.py rename to __init__.py diff --git a/usr/plugins/share_chat/default_config.yaml b/default_config.yaml similarity index 100% rename from usr/plugins/share_chat/default_config.yaml rename to default_config.yaml diff --git a/docs/how-it-works.png b/docs/how-it-works.png new file mode 100644 index 0000000..97ffc8e Binary files /dev/null and b/docs/how-it-works.png differ diff --git a/docs/share-chat.gif b/docs/share-chat.gif new file mode 100644 index 0000000..585c8dd Binary files /dev/null and b/docs/share-chat.gif differ diff --git a/usr/plugins/share_chat/extensions/webui/chat-top-end/share-chat.html b/extensions/webui/chat-top-end/share-chat.html similarity index 100% rename from usr/plugins/share_chat/extensions/webui/chat-top-end/share-chat.html rename to extensions/webui/chat-top-end/share-chat.html diff --git a/usr/plugins/share_chat/meta.yaml b/meta.yaml similarity index 100% rename from usr/plugins/share_chat/meta.yaml rename to meta.yaml diff --git a/usr/plugins/share_chat/plugin.yaml b/plugin.yaml similarity index 100% rename from usr/plugins/share_chat/plugin.yaml rename to plugin.yaml diff --git a/tests/_testkit b/tests/_testkit index c77ed9b..2f3e8ca 160000 --- a/tests/_testkit +++ b/tests/_testkit @@ -1 +1 @@ -Subproject commit c77ed9bd7ccb675b656caffabcf5c1e7aad77c91 +Subproject commit 2f3e8ca6f5f2a69e4a247beb27c1a6912980b9ef diff --git a/tests/component/test_plugin_shape.py b/tests/component/test_plugin_shape.py index e11b19e..1cce0e3 100644 --- a/tests/component/test_plugin_shape.py +++ b/tests/component/test_plugin_shape.py @@ -72,7 +72,8 @@ def test_license_declared_apache2(plugin_dir: Path) -> None: plugin = yaml.safe_load((plugin_dir / "plugin.yaml").read_text()) assert plugin.get("license") == "Apache-2.0" - license_file = plugin_dir.parent.parent.parent / "LICENSE" + # Root layout: the repo root IS the plugin dir, so LICENSE sits beside plugin.yaml. + license_file = plugin_dir / "LICENSE" text = license_file.read_text() assert "Apache License" in text and "Version 2.0" in text assert len(text) > 10_000, f"LICENSE is a stub ({len(text)} bytes)" diff --git a/tests/conftest.py b/tests/conftest.py index bfbb6f6..32b09da 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,11 +1,29 @@ -"""Shared fixtures for the share_chat test suite.""" -from __future__ import annotations +"""pytest conftest for an agent-zero-plugins plugin source repo. +Provides the `plugin_dir` fixture every a0_plugin_testkit assertion needs. +Point it at this plugin's source directory. +""" + +import sys from pathlib import Path import pytest +# Tests exec plugin modules straight from the plugin tree. Without this, the +# loader writes __pycache__ INTO the plugin dir (= repo root, root layout), +# which the static validator rightly flags as a packaging hazard. +sys.dont_write_bytecode = True + @pytest.fixture(scope="session") def plugin_dir() -> Path: - return Path(__file__).resolve().parent.parent / "usr" / "plugins" / "share_chat" + """Path to this plugin's source directory. + + The testkit's assertions take this as their first argument: + assert_extension_at_surface(plugin_dir, ...) + assert_no_dead_plugin_hooks(plugin_dir) + etc. + + Root layout: the repo root IS the plugin dir (.devkit.yml plugin_dir: .) + """ + return Path(__file__).resolve().parent.parent diff --git a/usr/plugins/share_chat/webui/.gitkeep b/webui/.gitkeep similarity index 100% rename from usr/plugins/share_chat/webui/.gitkeep rename to webui/.gitkeep diff --git a/usr/plugins/share_chat/webui/thumbnail.png b/webui/thumbnail.png similarity index 100% rename from usr/plugins/share_chat/webui/thumbnail.png rename to webui/thumbnail.png