From 5c713759bb74026433accfd283737547b4adc31c Mon Sep 17 00:00:00 2001 From: Omar Nahhas Sanchez Date: Wed, 29 Jul 2026 12:37:28 +0100 Subject: [PATCH] =?UTF-8?q?fix:=20open-source=20readiness=20=E2=80=94=20re?= =?UTF-8?q?store=20broken=20stub=20files,=20docs=20excellence,=20L1=20suit?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - LICENSE/README/pyproject/.gitignore contained literal unexpanded '§§include(...)' markers since the initial commit — restored with real content (LICENSE = full canonical Apache-2.0, 11.4KB; REQ-LIC-001) - README: value-prop, real Playwright-captured screenshots, mermaid architecture diagram, honest access model, install + dev/test guide - SECURITY.md: honest share-link threat model (ctxid entropy, exposure surface, clipboard, metadata leaks) + design mitigations - CONTRIBUTING.md, issue templates (bug/feature YAML forms), dependabot (github-actions + gitsubmodule) - Real 256x256 thumbnail (3.3KB, was a 79-byte stub) - L1 testkit shape suite: 9 component tests incl. surface wiring, static validator, version-sync, Apache-2.0 license guard - docs/marketplace-draft.md: staged index.yaml + flip-time checklist - Removed stale tests/e2e/behaviour.mjs (BDD harness retired it) --- .github/ISSUE_TEMPLATE/bug_report.yml | 47 +++++ .github/ISSUE_TEMPLATE/config.yml | 5 + .github/ISSUE_TEMPLATE/feature_request.yml | 23 +++ .github/dependabot.yml | 10 + .gitignore | 31 +++- CONTRIBUTING.md | 60 ++++++ LICENSE | 203 ++++++++++++++++++++- README.md | 118 +++++++++++- SECURITY.md | 58 ++++++ docs/marketplace-draft.md | 42 +++++ docs/screenshot-copied.png | Bin 0 -> 12931 bytes docs/screenshot-idle.png | Bin 0 -> 12412 bytes pyproject.toml | 27 ++- tests/component/test_plugin_shape.py | 78 ++++++++ tests/conftest.py | 11 ++ tests/e2e/behaviour.mjs | 42 ----- usr/plugins/share_chat/webui/thumbnail.png | Bin 79 -> 3341 bytes 17 files changed, 709 insertions(+), 46 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/dependabot.yml create mode 100644 CONTRIBUTING.md create mode 100644 SECURITY.md create mode 100644 docs/marketplace-draft.md create mode 100644 docs/screenshot-copied.png create mode 100644 docs/screenshot-idle.png create mode 100644 tests/component/test_plugin_shape.py create mode 100644 tests/conftest.py delete mode 100644 tests/e2e/behaviour.mjs 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 0000000000000000000000000000000000000000..60014424b3905699549baf148bb03167500dad07 GIT binary patch literal 12931 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 t0*0;H`H1hfQ`8W)`r?M6eykQ<$2dPKayE9 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 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 0de0c0d13677199c5d3bc2ae0ba4e98e47560003..608e5eca21d35b265bac9df193fdf283a03bd0b8 100644 GIT binary patch literal 3341 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