diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..ba9bd5f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,47 @@ +name: Bug report +description: Something in the Share Chat plugin doesn't work as documented +title: "[bug] " +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Thanks for the report! For security issues, use private vulnerability + reporting instead (see SECURITY.md) — do not file them here. + - type: textarea + id: what-happened + attributes: + label: What happened? + description: What did you do, what did you expect, what happened instead? + placeholder: Clicked the share button while ... expected ... but ... + validations: + required: true + - type: input + id: a0-version + attributes: + label: Agent Zero version + placeholder: e.g. v0.9.x / fork build tag + validations: + required: true + - type: input + id: plugin-version + attributes: + label: Plugin version + placeholder: e.g. 0.1.0 + validations: + required: true + - type: dropdown + id: context + attributes: + label: Browser context + options: + - HTTPS (secure context) + - HTTP / insecure context (clipboard fallback path) + - Not sure + validations: + required: true + - type: textarea + id: console + attributes: + label: Browser console output + render: text diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..a59aa97 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: true +contact_links: + - name: Security vulnerability + url: https://github.com/agent-zero-plugins/agent-zero-plugin-share-chat/security/advisories/new + about: Report security issues privately — never in a public issue. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..0852d9e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,23 @@ +name: Feature request +description: Suggest an improvement to the Share Chat plugin +title: "[feat] " +labels: ["enhancement"] +body: + - type: textarea + id: problem + attributes: + label: Problem / motivation + description: What are you trying to do that the plugin doesn't support? + validations: + required: true + - type: textarea + id: proposal + attributes: + label: Proposed behaviour + description: Describe the behaviour you'd like (user-visible, not implementation). + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: Alternatives considered diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..864697c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + - package-ecosystem: gitsubmodule + directory: / + schedule: + interval: weekly diff --git a/.gitignore b/.gitignore index 6262f87..b9430fb 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,30 @@ -§§include(/a0/src/github.com/agent-zero-plugins/agent-zero-plugin-share-chat/.gitignore) \ No newline at end of file +# Python +__pycache__/ +*.py[cod] +*.egg-info/ +.venv/ +.mypy_cache/ +.pytest_cache/ +.ruff_cache/ + +# Node / Playwright (e2e) +node_modules/ +test-results/ +playwright-report/ +blob-report/ +.playwright-mcp/ +tests/e2e/.e2e/ + +# Build output +dist/ +build/ + +# OS / editor +.DS_Store +Thumbs.db +*.swp +*~ + +# Temp / backups +*.tmp +*.bak diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..2216a16 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,60 @@ +# Contributing to Share Chat + +Thanks for helping improve the plugin! This repo follows the +[agent-zero-plugins devkit v2](https://github.com/agent-zero-plugins/agent-zero-plugin-development-testkit) +conventions — most of the tooling below ships from the `tests/_testkit` submodule. + +## Getting set up + +```bash +git clone --recursive https://github.com/agent-zero-plugins/agent-zero-plugin-share-chat.git +cd agent-zero-plugin-share-chat +pip install pytest pyyaml +``` + +If you cloned without `--recursive`, run `git submodule update --init --recursive`. + +## Repo layout + +| Path | What | +|---|---| +| `usr/plugins/share_chat/` | The plugin itself (manifests + one HTML extension) | +| `docs/spec/` | Behaviour contract (BEH-N), implementation plan, e2e spec docs | +| `tests/component/` | L1 shape suite (testkit assertions, fast, no A0 boot) | +| `tests/e2e/features/` + `tests/e2e/steps/` | Behaviour-first BDD (Gherkin + Playwright steps) | +| `tests/_testkit/` | Devkit submodule — runner, gates, shared steps. Do not edit here; PR the devkit repo. | + +## Workflow + +1. **Branch** from `main`. +2. **Change behaviour? Update the contract.** Any user-visible change must update + `docs/spec/behaviour-spec.md` (add/modify a `BEH-N`) and the matching scenario in + `tests/e2e/features/` — the traceability gate enforces this. +3. **Keep Gherkin pure.** Feature files describe behaviour in domain language — no selectors, + DOM ids, store names, or API calls (the feature-purity gate rejects them). Implementation + detail lives in `tests/e2e/steps/`. +4. **Run the gates locally before pushing:** + + ```bash + pytest tests/component -v # L1 shape suite + make verify # Tier-1 static BDD gates + make bdd-e2e # full local e2e (boots a disposable nested A0) + ``` + +5. **Open a PR.** CI runs `plugin-e2e` (lint → seam-off red-proof → BDD e2e). Green CI + + review is required; the publish gate refuses to ship a commit whose `plugin-e2e` was red. + +## Rules of the road + +- **No fake green.** Never wrap an assertion in `try/catch` to pass, never add a bare `@skip` + — skips need a tracked issue link. +- **Version bumps:** keep `usr/plugins/share_chat/plugin.yaml` and `meta.yaml` versions in + sync. +- **License:** contributions are accepted under [Apache-2.0](LICENSE). +- **No secrets** — this plugin has no credentials and it must stay that way. CI and the + static validator scan for committed secrets. + +## Reporting bugs / requesting features + +Use the issue templates. For security problems see [SECURITY.md](SECURITY.md) — do not open +a public issue. diff --git a/LICENSE b/LICENSE index ec6cfdb..d645695 100644 --- a/LICENSE +++ b/LICENSE @@ -1 +1,202 @@ -§§include(/a0/src/github.com/agent-zero-plugins/agent-zero-plugin-share-chat/LICENSE) \ No newline at end of file + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index e08008e..ea87a5f 100644 --- a/README.md +++ b/README.md @@ -1 +1,117 @@ -§§include(/a0/src/github.com/agent-zero-plugins/agent-zero-plugin-share-chat/README.md) \ No newline at end of file +# Share Chat — Agent Zero Plugin + +[![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. + +![Share button in the chat top bar](docs/screenshot-idle.png) + +## Why + +- **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. + +![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 +``` + +The plugin only **produces** the link. The **consume** half — opening `?ctxid=` links — is a +pre-existing Agent Zero capability the plugin 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. +- If no chat is selected, the button is a safe no-op. + +See [SECURITY.md](SECURITY.md) for the full threat model. + +## Install + +### From the Plugin Hub + +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. + +### Manual install + +```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 +``` + +Then enable **Share Chat** in **Settings → Plugins**. + +## Configuration + +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`). + +| 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` | + +## 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. + +```bash +git clone --recursive https://github.com/agent-zero-plugins/agent-zero-plugin-share-chat.git +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 +``` + +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. + +See [CONTRIBUTING.md](CONTRIBUTING.md) for the workflow. + +## License + +[Apache-2.0](LICENSE) © Interu diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..1c901b2 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,58 @@ +# Security Policy + +## Reporting a vulnerability + +Please **do not** open a public issue for security problems. Instead use +[GitHub private vulnerability reporting](https://github.com/agent-zero-plugins/agent-zero-plugin-share-chat/security/advisories/new) +on this repository, or contact the maintainers at `hello@interu.ai`. We aim to acknowledge +reports within 7 days. + +## Threat model — what a "share link" actually is + +This plugin composes a URL of the form: + +``` +/?ctxid= +``` + +and copies it to the clipboard. Being honest about the security properties: + +### What the link does NOT do + +- **It is not a capability/bearer token.** The `ctxid` grants no access by itself. A recipient + must (a) have network reachability to your Agent Zero instance and (b) pass its + authentication (login / auth proxy). Unauthenticated visitors land on the login screen. +- **It is not a public share page.** There is no anonymous read-only render, no proxy, no + third-party service. The plugin has zero backend and stores nothing. +- **Nothing leaves your machine on click.** Link composition and the clipboard write are + entirely client-side (`navigator.clipboard`, with a hidden-textarea `execCommand` fallback). + +### What you SHOULD consider before pasting a link somewhere + +- **Token entropy / guessability of `ctxid`:** Agent Zero chat context ids are short + (~8 char) random ids generated client- or server-side. They are identifiers, **not** + secrets — Agent Zero's security boundary is its authentication layer, not ctxid secrecy. + Anyone already authenticated to the *same* instance could enumerate or guess chat ids; + the plugin does not change that pre-existing property. +- **Exposure surface = your instance's exposure.** If your A0 instance is exposed on a LAN or + the internet without auth, the link points anyone straight at your conversation. That risk + exists with or without this plugin — the plugin just makes the URL easier to produce. + Run A0 behind authentication (built-in login, reverse-proxy auth, VPN). +- **The URL itself leaks metadata.** A pasted link reveals your instance origin (hostname/port) + and one chat id to whoever can read the paste destination (chat logs, issue trackers). + Treat links like you treat internal URLs. +- **Clipboard is a shared surface.** Anything on the clipboard can be read by other local + applications with clipboard access. The confirmation state ("Link copied!") auto-reverts + after ~2s but the clipboard content persists until overwritten. + +### Design mitigations in this plugin + +- No secrets embedded in links; existing query params are stripped before `ctxid` is set + (prevents accidentally re-sharing tokens that were in your current URL). +- No-op when no chat is selected (`getContext()` falsy → return). +- No network calls, no storage, no server-side component — the audit surface is a single + ~150-line declarative HTML file: `usr/plugins/share_chat/extensions/webui/chat-top-end/share-chat.html`. + +## Supported versions + +Only the latest released version receives security fixes. diff --git a/docs/marketplace-draft.md b/docs/marketplace-draft.md new file mode 100644 index 0000000..fbc9ae6 --- /dev/null +++ b/docs/marketplace-draft.md @@ -0,0 +1,42 @@ +# Marketplace submission draft — Share Chat + +Staged assets for the a0-plugins index PR. **Do not submit while the repo is private** — +the raw screenshot URLs below will 404 until the repo is flipped public. + +## `plugins/share_chat/index.yaml` + +```yaml +title: Share Chat +description: >- + One-click sharing for Agent Zero conversations. Adds a share button to the + chat top bar that copies a deep link to the current chat — paste it anywhere + and Agent Zero opens straight to that conversation. Entirely client-side: + no backend, no storage, no tokens. Confirms with a green check and + auto-resets. Recipients still need access to your instance — the link is a + pointer, not a bypass. +github: https://github.com/agent-zero-plugins/agent-zero-plugin-share-chat +tags: + - workflow + - tools + - integration +screenshots: + - https://raw.githubusercontent.com/agent-zero-plugins/agent-zero-plugin-share-chat/main/docs/screenshot-idle.png + - https://raw.githubusercontent.com/agent-zero-plugins/agent-zero-plugin-share-chat/main/docs/screenshot-copied.png +``` + +- Description length: ~430 chars (< 500 ✓) +- Tags: 3 (≤ 5 ✓), from TAGS.md +- Title: 10 chars (≤ 50 ✓) + +## Thumbnail + +Copy `usr/plugins/share_chat/webui/thumbnail.png` (256×256 square, ~3.3 KB < 20 KB ✓) +to `plugins/share_chat/thumbnail.png` in the index PR. + +## Pre-submission checklist (flip-time) + +- [ ] Repo public (screenshot URLs resolve) +- [ ] `plugin.yaml` at expected path with `name: share_chat` (matches index folder, `^[a-z0-9_]+$` ✓) +- [ ] `LICENSE` present at repo root (full Apache-2.0 ✓) +- [ ] `plugin-e2e` green on the submitted commit +- [ ] One plugin per PR; only `index.yaml` + `thumbnail.png` in the index folder diff --git a/docs/screenshot-copied.png b/docs/screenshot-copied.png new file mode 100644 index 0000000..6001442 Binary files /dev/null and b/docs/screenshot-copied.png differ diff --git a/docs/screenshot-idle.png b/docs/screenshot-idle.png new file mode 100644 index 0000000..c4a3bdd Binary files /dev/null and b/docs/screenshot-idle.png differ diff --git a/pyproject.toml b/pyproject.toml index 126a304..9bf7a8d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1 +1,26 @@ -§§include(/a0/src/github.com/agent-zero-plugins/agent-zero-plugin-share-chat/pyproject.toml) \ No newline at end of file +[project] +name = "agent-zero-plugin-share-chat" +description = "Agent Zero plugin: share button that copies a deep link to the current chat" +requires-python = ">=3.11" +license = { text = "Apache-2.0" } + +[tool.ruff] +target-version = "py311" +line-length = 99 + +[tool.ruff.lint] +select = ["E", "W", "F", "I", "UP", "B", "SIM", "RUF"] + +[tool.ruff.format] +quote-style = "double" + +[tool.pytest.ini_options] +testpaths = ["tests"] +pythonpath = [".", "tests/_testkit/src"] +# Exclude the devkit submodule's own self-tests (and its nested .agent-zero) +# from consumer collection — they run in the devkit repo's CI, not here. +norecursedirs = ["tests/_testkit", ".git", "node_modules", "tests/e2e"] +markers = [ + "unit: fast L0 tests, no subprocess, no network", + "component: L1 tests, A0 in-process or fixtures", +] diff --git a/tests/component/test_plugin_shape.py b/tests/component/test_plugin_shape.py new file mode 100644 index 0000000..e11b19e --- /dev/null +++ b/tests/component/test_plugin_shape.py @@ -0,0 +1,78 @@ +"""L1 shape suite — testkit assertions against the share_chat plugin. + +Fast (<1s), no A0 boot. Catches typo'd extension points, dead hooks, +blank thumbnails, undeclared deps, fabricated A0 API calls, and +committed secrets between commits. +""" +from __future__ import annotations + +from pathlib import Path + +import pytest + +from a0_plugin_testkit.assertions import ( + assert_extension_at_surface, + assert_no_dead_plugin_hooks, + assert_no_stray_extension_folders, + assert_plugin_has_thumbnail, +) +from a0_plugin_testkit.real.a0_api import audit_a0_api_usage, assert_a0_api_usage_ok +from a0_plugin_testkit.real.deps import audit_dependencies, assert_dependencies_declared +from a0_plugin_testkit.real.validator import assert_validator_clean, static_validate + +pytestmark = pytest.mark.component + + +def test_no_typo_extension_points(plugin_dir: Path) -> None: + assert_no_stray_extension_folders(plugin_dir) + + +def test_share_button_wired_to_real_surface(plugin_dir: Path) -> None: + """The whole plugin is one HTML extension — prove it targets a real surface.""" + assert_extension_at_surface(plugin_dir, "chat-top-end", pattern="*.html") + + +def test_no_dead_hooks(plugin_dir: Path) -> None: + assert_no_dead_plugin_hooks(plugin_dir) + + +def test_thumbnail(plugin_dir: Path) -> None: + assert_plugin_has_thumbnail(plugin_dir) + + +def test_static_validator(plugin_dir: Path) -> None: + assert_validator_clean(static_validate(plugin_dir), allow_warnings=False) + + +def test_dependencies_declared(plugin_dir: Path) -> None: + assert_dependencies_declared(audit_dependencies(plugin_dir)) + + +def test_a0_api_usage_valid(plugin_dir: Path) -> None: + assert_a0_api_usage_ok(audit_a0_api_usage(plugin_dir)) + + +def test_manifest_versions_in_sync(plugin_dir: Path) -> None: + """plugin.yaml and meta.yaml versions must match (REQ from sibling repos: + version drift between the two fails the publish gate).""" + import yaml + + plugin = yaml.safe_load((plugin_dir / "plugin.yaml").read_text()) + meta = yaml.safe_load((plugin_dir / "meta.yaml").read_text()) + assert plugin["version"] == meta["version"], ( + f"version drift: plugin.yaml={plugin['version']} meta.yaml={meta['version']}" + ) + + +def test_license_declared_apache2(plugin_dir: Path) -> None: + """REQ-LIC-001: org law — Apache-2.0 declared in the manifest and the repo + LICENSE is the full canonical text, not a stub.""" + import yaml + + 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" + 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 new file mode 100644 index 0000000..bfbb6f6 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,11 @@ +"""Shared fixtures for the share_chat test suite.""" +from __future__ import annotations + +from pathlib import Path + +import pytest + + +@pytest.fixture(scope="session") +def plugin_dir() -> Path: + return Path(__file__).resolve().parent.parent / "usr" / "plugins" / "share_chat" diff --git a/tests/e2e/behaviour.mjs b/tests/e2e/behaviour.mjs deleted file mode 100644 index 2bddbf2..0000000 --- a/tests/e2e/behaviour.mjs +++ /dev/null @@ -1,42 +0,0 @@ -// Behaviour test (SPEC DEC-056) — agent-zero-plugin-share-chat. -// The plugin injects a share button into the chat-top-end region. Clicking it -// copies a deep link and toggles a "copied" visual state (class + icon swap), -// which auto-resets after ~2000ms. -// -// No config screen: the plugin is a pure UI extension (per_project_config and -// per_agent_config are false; webui/ ships no config.html, default_config.yaml -// explicitly declares no options). So has_config_screen is false and there is -// no config panel to open — the over-the-wire surface is the injected button -// and its click-driven state machine. -export default async function behaviour({ page, expect, baseURL }) { - await page.goto(baseURL + "/", { waitUntil: "domcontentloaded" }); - - // 1. The injected share button appears in the live UI (extension loaded async). - const btn = page.locator(".share-chat-btn"); - await expect(btn).toBeVisible({ timeout: 20_000 }); - - // 1a. It is injected into the CORRECT region. The chat-top-end extension point - // is rendered by , whose innerHTML is - // replaced with the plugin's markup — so the button must be a descendant - // of that host, proving the plugin targeted the right extension point. - const inRegion = page.locator('x-extension#chat-top-end .share-chat-btn'); - await expect(inRegion).toBeVisible({ timeout: 20_000 }); - - // The icon starts as "share". - const icon = btn.locator(".material-symbols-outlined"); - await expect(icon).toHaveText("share", { timeout: 5_000 }); - - // 2. (best-effort) Clicking SHOULD toggle the copied state (.share-copied + icon - // "check" + title swap), but that depends on the clipboard/execCommand path - // in the headless context and is transient (2s auto-reset) — flaky to gate - // on. The injected button + "share" icon (above) are the deterministic - // plugin-specific signal; the title/copied flow is informational. - try { - await btn.click(); - await expect(btn).toHaveClass(/share-copied/, { timeout: 2_000 }); - await expect(icon).toHaveText("check", { timeout: 2_000 }); - console.log("[behaviour] share_chat: button injected + click toggled copied state ✓"); - } catch { - console.log("[behaviour] share_chat: button + 'share' icon injected into chat-top-end (copied-state flow not observed headless) — informational"); - } -} diff --git a/usr/plugins/share_chat/webui/thumbnail.png b/usr/plugins/share_chat/webui/thumbnail.png index 0de0c0d..608e5ec 100644 Binary files a/usr/plugins/share_chat/webui/thumbnail.png and b/usr/plugins/share_chat/webui/thumbnail.png differ