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
2 changes: 1 addition & 1 deletion .devkit.yml
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
8 changes: 6 additions & 2 deletions .github/workflows/plugin-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
PLUGIN_DIR := usr/plugins/share_chat
PLUGIN_DIR := .
PLUGIN_DISPLAY_NAME := Share Chat
-include tests/_testkit/e2e/Makefile.devkit
151 changes: 41 additions & 110 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,160 +1,91 @@
# Share Chat — Agent Zero Plugin
# Share Chat — Agent Zero plugin

<!-- BADGES:START (generated by the devkit — `make badges`) -->

[![plugin-e2e](https://github.com/agent-zero-plugins/agent-zero-plugin-share-chat/actions/workflows/plugin-e2e.yml/badge.svg?branch=main)](https://github.com/agent-zero-plugins/agent-zero-plugin-share-chat/actions/workflows/plugin-e2e.yml) [![BDD scenarios](https://img.shields.io/badge/tests-BDD%20e2e-brightgreen)](https://github.com/agent-zero-plugins/agent-zero-plugin-share-chat/tree/main/tests/e2e/features) [![Release](https://img.shields.io/github/v/tag/agent-zero-plugins/agent-zero-plugin-share-chat?label=release&sort=semver)](https://github.com/agent-zero-plugins/agent-zero-plugin-share-chat/tags) [![License: Apache-2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://github.com/agent-zero-plugins/agent-zero-plugin-share-chat/blob/main/LICENSE) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/agent-zero-plugins/agent-zero-plugin-share-chat/blob/main/CONTRIBUTING.md)

<!-- BADGES:END -->

[![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.
<p align="center">
<img src="docs/share-chat.gif" alt="Share Chat in action">
</p>

![Share button in the chat top bar](docs/screenshot-idle.png)
## ✨ Features

## Why
- **One-click deep link** — copies `<your-a0-origin>/?ctxid=<chat-id>` 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
`<your-a0-origin>/?ctxid=<chat-id>`. 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<br/>(chat-top-end extension)"] -->|click| CTX["getContext()<br/>current chat id"]
CTX --> LINK["Compose deep link<br/>origin + ?ctxid=&lt;id&gt;"]
LINK --> CLIP["navigator.clipboard<br/>(textarea fallback)"]
CLIP --> CONFIRM["✓ Link copied!<br/>auto-reset 2s"]
end
subgraph Recipient ["Recipient's browser"]
URL["Opens link with ?ctxid"] --> STORE["A0 chats-store.init()<br/>reads &amp; 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)

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:

```
<your-a0-origin>/?ctxid=<chat-id>
```

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
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).
File renamed without changes.
File renamed without changes.
Binary file added docs/how-it-works.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/share-chat.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
File renamed without changes.
File renamed without changes.
3 changes: 2 additions & 1 deletion tests/component/test_plugin_shape.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
24 changes: 21 additions & 3 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -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
File renamed without changes.
File renamed without changes
Loading