feat: namespace plugin identity

Adopt canonical plugin id/slug manifest fields, vendor-prefixed OTP app naming, and fully-qualified capability ids for Aether.
This commit is contained in:
2026-05-27 19:05:51 +02:00
parent 44b9c6caba
commit b4b8c83ddb
12 changed files with 51 additions and 34 deletions
+4 -4
View File
@@ -620,8 +620,8 @@ checkout during development. For the full contract, see
`version`, `entry_module`, `host_api`, `otp_app`, `provides`, `requires`, and
`enhances_with` aligned with the runtime spec returned by `register/1`.
- `entry_module` must be a valid module ending in `.Plugin` under an owner-controlled namespace.
- Capability versions are discrete breaking-change markers such as `ui@1` or
`social@1`, not semver. Framework APIs are part of the `host_api` foundation, not separate manifest capabilities.
- Capability versions are discrete breaking-change markers such as `org.tribe-one.caps.ui@1` or
`org.tribe-one.caps.social@1`, not semver. Framework APIs are part of the `host_api` foundation, not separate manifest capabilities.
- Use `requires` for hard dependencies and `enhances_with` for optional
integrations that the plugin can run without.
- Run `scripts/plugin validate` after changing `manifest.json` or the runtime
@@ -630,8 +630,8 @@ checkout during development. For the full contract, see
## UI And Assets
- Plugin LiveViews that use host chrome should render with
`Tribes.Plugin.Layouts.app` and keep `ui@1` in `manifest.json` `requires`.
- Consumers should target the `ui@1` facade with `use Tribes.UI` or
`Tribes.Plugin.Layouts.app` and keep `org.tribe-one.caps.ui@1` in `manifest.json` `requires`.
- Consumers should target the `org.tribe-one.caps.ui@1` facade with `use Tribes.UI` or
`import Tribes.UI.Components`, not a concrete provider module.
- Declare browser assets in `manifest.json` under `assets.global_js` and
`assets.global_css`; the host serves them through the plugin asset surface.
+10 -8
View File
@@ -5,8 +5,8 @@ Tribes node.
It provides:
- `social@1` — a local tribe feed backed by Nostr kind `1` notes.
- `chat@1` — reusable chat surfaces for public rooms, embeddable context chat,
- `org.tribe-one.caps.social@1` — a local tribe feed backed by Nostr kind `1` notes.
- `org.tribe-one.caps.chat@1` — reusable chat surfaces for public rooms, embeddable context chat,
NIP-17 direct messages, and future chat backends.
## Features
@@ -52,13 +52,15 @@ The package pin is kept at `@internet-privacy/marmot-ts@0.5.1`.
```json
{
"name": "aether",
"id": "org.tribe-one.plugins.aether",
"slug": "aether",
"display_name": "Aether",
"version": "0.2.0",
"entry_module": "TribeOne.TribesPlugin.Aether.Plugin",
"host_api": "1",
"otp_app": "aether",
"provides": ["social@1", "chat@1"],
"requires": ["ui@1"],
"otp_app": "tribe_one_aether",
"provides": ["org.tribe-one.caps.social@1", "org.tribe-one.caps.chat@1"],
"requires": ["org.tribe-one.caps.ui@1"],
"assets": {
"global_js": ["aether.js"],
"global_css": ["aether.css"]
@@ -75,7 +77,7 @@ Runtime contributions from `TribeOne.TribesPlugin.Aether.Plugin` include:
- plugin config schema for chat backend defaults and Marmot UI enablement,
- global JS/CSS assets.
The `chat@1` provider surface is intentionally small and reusable:
The `org.tribe-one.caps.chat@1` provider surface is intentionally small and reusable:
- `TribeOne.TribesPlugin.Aether.Chat.chat_panel_component/0` returns the reusable chat panel component.
- `TribeOne.TribesPlugin.Aether.Chat.recipient_picker_component/0` returns the recipient picker.
@@ -143,7 +145,7 @@ colocated hooks from external plugin OTP apps are not auto-imported by the host.
## Runtime requirements
- A Tribes host checkout/runtime with `ui@1`.
- A Tribes host checkout/runtime with `org.tribe-one.caps.ui@1`.
- Parrhesia available through the host for Nostr event storage and streaming.
- `nak` available in the runtime path for the current NIP-17/NIP-59/NIP-04
protocol implementation.
+1 -1
View File
@@ -1,5 +1,5 @@
import Config
config :aether, ash_domains: [TribeOne.TribesPlugin.Aether.Chat]
config :tribe_one_aether, ash_domains: [TribeOne.TribesPlugin.Aether.Chat]
import_config "#{config_env()}.exs"
+1 -1
View File
@@ -4,7 +4,7 @@ defmodule TribeOne.TribesPlugin.Aether.Chat do
"""
use Ash.Domain,
otp_app: :aether
otp_app: :tribe_one_aether
require Ash.Query
+1 -1
View File
@@ -4,7 +4,7 @@ defmodule TribeOne.TribesPlugin.Aether.Chat.Channel do
"""
use Ash.Resource,
otp_app: :aether,
otp_app: :tribe_one_aether,
domain: TribeOne.TribesPlugin.Aether.Chat,
data_layer: AshPostgres.DataLayer,
extensions: [AshNostrSync]
+1 -1
View File
@@ -4,7 +4,7 @@ defmodule TribeOne.TribesPlugin.Aether.Chat.Message do
"""
use Ash.Resource,
otp_app: :aether,
otp_app: :tribe_one_aether,
domain: TribeOne.TribesPlugin.Aether.Chat,
data_layer: AshPostgres.DataLayer,
extensions: [AshNostrSync]
+1 -1
View File
@@ -4,7 +4,7 @@ defmodule TribeOne.TribesPlugin.Aether.Chat.Participant do
"""
use Ash.Resource,
otp_app: :aether,
otp_app: :tribe_one_aether,
domain: TribeOne.TribesPlugin.Aether.Chat,
data_layer: AshPostgres.DataLayer,
extensions: [AshNostrSync]
+2 -2
View File
@@ -3,7 +3,7 @@ defmodule TribeOne.TribesPlugin.Aether.Plugin do
Tribes plugin entry point.
"""
use Tribes.Plugin.Base, otp_app: :aether
use Tribes.Plugin.Base, otp_app: :tribe_one_aether
@behaviour Tribes.Capabilities.Chat.V1
@@ -94,7 +94,7 @@ defmodule TribeOne.TribesPlugin.Aether.Plugin do
TribeOne.TribesPlugin.AetherWeb.ChatPanelComponent,
Keyword.get(opts, :id, "aether-chat-panel-#{slug}"),
assigns,
capability: "chat@1",
capability: "org.tribe-one.caps.chat@1",
provider: __MODULE__
)}
end
@@ -1,6 +1,6 @@
defmodule TribeOne.TribesPlugin.AetherWeb.ChatRecipientPickerComponent do
@moduledoc """
Recipient picker exposed as part of Aether's `chat@1` UI contract.
Recipient picker exposed as part of Aether's `org.tribe-one.caps.chat@1` UI contract.
"""
use Phoenix.LiveComponent
+17 -6
View File
@@ -1,16 +1,27 @@
{
"name": "aether",
"id": "org.tribe-one.plugins.aether",
"slug": "aether",
"display_name": "Aether",
"version": "0.2.0",
"description": "Local social feed and chat for Tribes, including NIP-17 direct messages.",
"entry_module": "TribeOne.TribesPlugin.Aether.Plugin",
"host_api": "1",
"otp_app": "aether",
"provides": ["social@1", "chat@1"],
"requires": ["ui@1"],
"otp_app": "tribe_one_aether",
"provides": [
"org.tribe-one.caps.social@1",
"org.tribe-one.caps.chat@1"
],
"requires": [
"org.tribe-one.caps.ui@1"
],
"enhances_with": [],
"assets": {
"global_js": ["aether.js"],
"global_css": ["aether.css"]
"global_js": [
"aether.js"
],
"global_css": [
"aether.css"
]
},
"migrations": true,
"children": false
+1 -1
View File
@@ -3,7 +3,7 @@ defmodule TribeOne.TribesPlugin.Aether.MixProject do
def project do
[
app: :aether,
app: :tribe_one_aether,
version: "0.2.0",
elixir: "~> 1.18",
elixirc_paths: elixirc_paths(Mix.env()),
+11 -7
View File
@@ -2,6 +2,7 @@ defmodule TribeOne.TribesPlugin.Aether.ManifestTest do
use ExUnit.Case, async: true
@manifest_path Path.join(__DIR__, "../../manifest.json") |> Path.expand()
@capability_regex ~r/^[a-z][a-z0-9_-]*(\.[a-z][a-z0-9_-]*)+@[1-9][0-9]*$/
setup_all do
content = File.read!(@manifest_path)
@@ -16,7 +17,9 @@ defmodule TribeOne.TribesPlugin.Aether.ManifestTest do
test "has required fields", %{manifest: manifest} do
required = [
"name",
"id",
"slug",
"display_name",
"version",
"entry_module",
"host_api",
@@ -31,9 +34,10 @@ defmodule TribeOne.TribesPlugin.Aether.ManifestTest do
end
end
test "name matches OTP app name", %{manifest: manifest} do
assert manifest["name"] == "aether"
assert manifest["otp_app"] == manifest["name"]
test "identity fields are namespaced", %{manifest: manifest} do
assert manifest["id"] == "org.tribe-one.plugins.aether"
assert manifest["slug"] == "aether"
assert manifest["otp_app"] == "tribe_one_aether"
end
test "entry_module uses the TribeOne namespace and Plugin suffix", %{manifest: manifest} do
@@ -48,20 +52,20 @@ defmodule TribeOne.TribesPlugin.Aether.ManifestTest do
test "provides contains valid capability identifiers", %{manifest: manifest} do
for cap <- manifest["provides"] do
assert Regex.match?(~r/^[a-z][a-z0-9_]*(@\d+)?$/, cap),
assert Regex.match?(@capability_regex, cap),
"invalid capability identifier: #{inspect(cap)}"
end
end
test "requires contains valid capability identifiers", %{manifest: manifest} do
for cap <- manifest["requires"] do
assert Regex.match?(~r/^[a-z][a-z0-9_]*(@\d+)?$/, cap),
assert Regex.match?(@capability_regex, cap),
"invalid capability identifier: #{inspect(cap)}"
end
end
test "declares the chat capability", %{manifest: manifest} do
assert "chat@1" in manifest["provides"]
assert "org.tribe-one.caps.chat@1" in manifest["provides"]
end
test "host_api is a string version", %{manifest: manifest} do