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
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Public design direction and discussion for the Amadeus extension system.

> **Status: Draft specification v0.1**
>
> A first draft of the extension protocol is available for discussion. An unmerged Amadeus experimental branch implements a restart-based TTS/MCP/Skill subset; no stable SDK has been released. Interfaces and compatibility guarantees may change until 1.0.
> A first draft of the extension protocol is available for discussion. Unmerged Amadeus experiments implement TTS/MCP/Skill lifecycle and data-only desktop themes; no stable SDK has been released. Interfaces and compatibility guarantees may change until 1.0.

## Why this repository exists

Expand All @@ -18,17 +18,18 @@ Our current focus is how extensions fit into Amadeus capability discovery, routi
| --- | --- |
| [Extension Protocol v0.1](spec/extension-protocol-v0.1.md) | Manifest, lifecycle, permissions, Runner protocol, packaging |
| [Voice backends](spec/contracts/voice-v0.md) | TTS backends (provisional) and ASR backends (provisional) |
| [Desktop themes](spec/contracts/theme-v0.md) | Data-only palettes, installed fonts, bounded images and package lifecycle (provisional) |
| [Context sources](spec/contracts/context-source-v0.md) | Reference material such as memories for Main Chat turns (provisional) |
| [Observers](spec/contracts/observer-v0.md) | Projected Host events (provisional) |
| [Manifest schema](schemas/amadeus-extension.v0.schema.json) | JSON Schema for `amadeus-extension.json` |
| [Examples](examples/) | Five example packages covering every contribution kind |
| [Examples](examples/) | Six manifest examples; the experimental Host supports only its documented subset |

In short:

- An extension is a package with one `amadeus-extension.json`. It never imports Amadeus internals.
- Extension code runs in its own process and Python environment and reaches Amadeus only through the protocol.
- Process contributions run in their own process and Python environment and reach Amadeus only through the protocol. Resource-only packages do not execute code during preparation.
- The first experimental profile rejects all Host capability requests and stores no Grants; broader permission design is provisional.
- In the Amadeus experiment, installation, configuration, explicit version updates, enable/disable, selection and removal take effect after restarting the backend, consistently with built-in settings. Runner crash recovery stays inside the proxy.
- Pure frontend packages apply immediately. Packages with backend contributions retain one loaded version until backend restart. Disabling or removing a mixed package closes its frontend contributions immediately while clearly reporting the old backend awaiting shutdown. Runner crash recovery stays inside the proxy.

Agent Skills can be installed once and bound to selected Codex, ACP and Pi
Agents in the experimental Host. Unchanged native sessions resume; changing
Expand Down
44 changes: 44 additions & 0 deletions examples/desktop-theme/amadeus-extension.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"license": "MIT-0",
"schema": "amadeus.extension/v0",
"id": "org.example.desktop-theme",
"version": "0.1.0",
"display_name": {
"en": "Quiet desktop",
"zh-CN": "静谧桌面"
},
"description": "A declarative desktop palette; no package code runs.",
"author": {
"name": "Amadeus contributors"
},
"engines": {
"amadeus_host_api": ">=0.1,<0.2"
},
"runtime": {
"kind": "none"
},
"contributes": {
"theme": [
{
"id": "night",
"contract": "amadeus.ui.theme/v0",
"base": "wallpaper-slice",
"title": {
"en": "Quiet night",
"zh-CN": "静谧夜色"
},
"tokens": {
"--bg": "rgba(6,17,22,.85)",
"--surface": "#0A1D22",
"--surface-alt": "#0D252A",
"--text": "#E0F8F1",
"--muted": "#A3CFC4",
"--accent": "#7DE0CC",
"--accent-hover": "#62BDA8",
"--faint": "#A3CFC4",
"--selection-bg": "rgba(84,255,221,.10)"
}
}
]
}
}
34 changes: 34 additions & 0 deletions schemas/amadeus-extension.v0.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,12 @@
"additionalProperties": false,
"minProperties": 1,
"properties": {
"theme": {
"type": "array",
"minItems": 1,
"maxItems": 16,
"items": { "$ref": "#/$defs/theme" }
},
"voice_backend": {
"type": "array",
"minItems": 1,
Expand Down Expand Up @@ -205,6 +211,34 @@
}
}
},
"theme": {
"type": "object",
"additionalProperties": false,
"required": ["id", "contract", "base", "tokens"],
"properties": {
"id": { "$ref": "#/$defs/contributionId" },
"contract": { "const": "amadeus.ui.theme/v0" },
"title": { "$ref": "#/$defs/text80" },
"summary": { "$ref": "#/$defs/text500" },
"base": { "enum": ["classic", "wallpaper-slice"] },
"tokens": {
"type": "object",
"maxProperties": 32,
"propertyNames": { "pattern": "^--[a-z][a-z-]{0,63}$" },
"additionalProperties": { "type": "string", "minLength": 1, "maxLength": 256 }
},
"background": {
"type": "object",
"additionalProperties": false,
"required": ["image", "fit", "dim"],
"properties": {
"image": { "$ref": "#/$defs/relativePath" },
"fit": { "enum": ["cover", "contain"] },
"dim": { "type": "number", "minimum": 0, "maximum": 1 }
}
}
}
},
"voiceBackend": {
"type": "object",
"required": ["role"],
Expand Down
27 changes: 27 additions & 0 deletions schemas/theme-tokens.v0.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"--bg": "color",
"--surface": "color",
"--surface-alt": "color",
"--text": "color",
"--muted": "color",
"--faint": "color",
"--border": "color",
"--border-strong": "color",
"--accent": "color",
"--accent-hover": "color",
"--hover": "color",
"--pressed": "color",
"--selection-bg": "color",
"--scrollbar": "color",
"--scrollbar-hover": "color",
"--shadow-color": "color",
"--card-border": "color",
"--divider": "color",
"--subtle-fill": "color",
"--selected-fill": "color",
"--focus-fill": "color",
"--neutral-pill": "color",
"--neutral-pill-bg": "color",
"--font": "font",
"--font-cjk": "font"
}
58 changes: 58 additions & 0 deletions spec/contracts/theme-v0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Theme contract v0

Status: **provisional**. Contract identifier: `amadeus.ui.theme/v0`.
The local P1/P2 Host implementation and its acceptance evidence are reviewed
separately; this document does not claim stable conformance.

A theme is data. It does not execute code, request Host capabilities, inject
stylesheets or alter the Host's base component identity. `base` selects the
Host's `classic` or `wallpaper-slice` styling; the qualified extension selection
is recorded separately. Title-bar style and `colorScheme` follow that base.

`tokens` override only the 25 names in the canonical
[theme token allowlist](../../schemas/theme-tokens.v0.json). Host tooling generates
its token names from a pinned copy of this file; adding an internal CSS variable
does not automatically expose it to packages. Status
colors (`--danger*`, `--warning*`, `--success*`) are reserved. Colors accept hex,
rgb/rgba or hsl/hsla literals; fonts reference installed system families only.
Functional literals use either comma-separated components (optional fourth alpha)
or three space-separated components followed by optional `/ alpha`. The forms
cannot be mixed; legacy comma RGB components are all numbers or all percentages.
Shared accepted/rejected cases live in `tests/fixtures/theme-color-literals.json`.
URLs, variables, declarations, comments and executable CSS are rejected. The
reference validator checks the allowlist, literal syntax/ranges, single-family
font syntax, contained image paths, size and file signatures. Installed fonts,
full native image decoding and rendered palette validation remain Host checks.
The Host validates and renders the same final palette. Normal text must retain
contrast of at least 4.5 and muted/faint text at least 3 on its actual surfaces.
Status and accent foreground text must also be checked on the surfaces where
it is displayed. Palettes keep the light/dark identity of their base; surface
colors are opaque to bound nested compositing. The Host provides a native or
otherwise theme-independent way to restore a built-in theme, even if a selected
font renders unreadable glyphs.

An optional `background` supplies a package-relative PNG or JPEG image,
`fit` (`cover` or `contain`) and black-overlay `dim` from 0 to 1. WebP is excluded
from v0 because the current native Host decoder cannot validate it. Renaming a
WebP file does not make it a PNG/JPEG. The Host checks decoded dimensions
(each at most 4096), size (8 MiB), path containment and absence
of links. Image contrast is evaluated over the entire possible luminance range
after dim/surface compositing, including interior minima. Looking only at a
black and white endpoint is insufficient when text lies between them.
Host-validated image bytes cross into the renderer; package filesystem URLs do
not. Old object URLs/styles are released on switching, disabling or removal.

New `runtime.kind=none` wheels cannot declare dependencies; the inspected package
resources are extracted without a virtual environment, pip or package imports.
The existing wheel inspection and ownership rules still apply.

Pure frontend packages apply immediately. If either the old or new package has
backend contributions, its version/configuration changes apply together after
backend restart. Frontend resources use the actual loaded snapshot, never new
desired metadata with old code. Disabling/removing closes frontend admission
immediately; the interface must still disclose a backend awaiting shutdown.

When a selected theme becomes unavailable, keep its saved identity and report
the reason while falling back to its last verified base (otherwise the Host
default). Reinstalling never silently enables a package. Resource cleanup follows
actual references and preserves retryable ownership after a deletion failure.
25 changes: 18 additions & 7 deletions spec/extension-protocol-v0.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Companion documents:
| [Voice backend contracts](contracts/voice-v0.md) (`amadeus.voice.tts/v0`, `amadeus.voice.asr/v0`) | TTS and ASR provisional |
| [Context source contract](contracts/context-source-v0.md) (`amadeus.context.source/v0`) | Provisional |
| [Observer contract](contracts/observer-v0.md) (`amadeus.events.observer/v0`) | Provisional |
| [Theme contract](contracts/theme-v0.md) (`amadeus.ui.theme/v0`) | Provisional; local P1/P2 implementation under review |
| [Manifest JSON Schema](../schemas/amadeus-extension.v0.schema.json) | Normative |
| [Reference manifest validator](../tools/validate_manifest.py) | Informative |
| [Example packages](../examples/) | Informative |
Expand Down Expand Up @@ -75,21 +76,29 @@ uses them and passes the acceptance checks listed with each pilot:
| A memory recall extension | `context_source`, scoped source access and memory governance; `observer` only when needed | A late result never delays a turn and is recorded as dropped. Forgotten items never return. Uninstalling handles data as the user chose, and earlier provenance shows the source as removed. |
| A task companion | `observer`, `presentation:notify` | A revoked capability stops the next delivery or call, reports the capability name, and affects no other extension. |

### 1.3.1 Amadeus experimental restart profile (informative)
### 1.3.1 Amadeus experimental Host profile (informative)

The 2026-10-11 experimental branch implements a local-wheel subset with three
native owners: TTS (MiMo English and Fish Audio Japanese/English) and declarative
The 2026-10-12 local pilot implements a local-wheel subset with backend
owners: TTS (MiMo English and Fish Audio Japanese/English) and declarative
MCP (a package-owned Notes service bound to compatible Work Providers), and
Agent Skills (one installed package bound to selected Codex, ACP or Pi Agents).
A data-only theme owner applies literal palettes and bounded package images in
the desktop. The theme contract remains provisional pending pilot acceptance.
MiMo does not support Japanese, confirmed by the project owner; no Japanese
MiMo audition is pending. Real MiMo English acceptance awaits a configured key.

All install, configuration, enable/disable, selection and removal changes apply
after backend restart, including built-in/extension transitions. A stable TTS
Backend install, configuration, enable/disable, selection and removal changes
apply after backend restart, including built-in/extension TTS transitions. A stable TTS
proxy lives for the backend process. Runner crash recovery occurs inside it and
never replays a failed synthesis. The pipeline does not hot-swap runtimes.
For this profile, manifest live/reactivate labels do not override restart
semantics: saved values are delivered in the next initialize snapshot.
Manifest live/reactivate labels do not override backend restart semantics:
saved values are delivered in the next initialize snapshot. Pure frontend theme
packages apply without restarting the backend. A version switch involving an
old or new backend contribution uses the backend boundary for the whole package;
frontend contributions keep the actual loaded snapshot until then. Disabling
or removing immediately closes frontend admission; a still-running backend is
shown separately. Re-enabling a mixed package requires restart before reopening
its frontend contributions.
General live configuration/generation handover below remains future design,
not a requirement of this experimental subset.

Expand Down Expand Up @@ -322,6 +331,7 @@ Kinds in v0.1:
| `voice_backend` | TTS and ASR provisional | yes | `amadeus.voice.tts/v0`, `amadeus.voice.asr/v0` | TTS and ASR registries | [voice-v0](contracts/voice-v0.md) |
| `skill` | provisional | no | `amadeus.skill/v0` | Work Provider skill projection | [§3.6.1](#361-skill) |
| `mcp_server` | provisional | no | `amadeus.mcp-server/v0` | MCP connection registry | [§3.6.2](#362-mcp_server) |
| `theme` | provisional | no | `amadeus.ui.theme/v0` | Desktop theme owner; not projected into Work execution | [Theme contract](contracts/theme-v0.md) |
| `context_source` | provisional | yes | `amadeus.context.source/v0` | Main Chat context assembly | [context-source-v0](contracts/context-source-v0.md) |
| `observer` | provisional | yes | `amadeus.events.observer/v0` | Host event projection | [observer-v0](contracts/observer-v0.md) |

Expand Down Expand Up @@ -497,6 +507,7 @@ When a Host or validator has the package contents, it also checks:
| --- | --- |
| P1 | The manifest exists, is at most 256 KiB, and parses as JSON. |
| P2 | Each skill `path` is a directory inside the package that contains `SKILL.md`, contains and passes through no links, and is at most 16 MiB. |
| P4 | Theme background resources are contained, unlinked PNG/JPEG files of at most 8 MiB. The Host also validates decoded size, token literals and rendered contrast before activation. |
| P3 | Each `${package}/…` command exists inside the package without passing through links. |

## 4. Lifecycle
Expand Down
4 changes: 4 additions & 0 deletions tests/fixtures/theme-color-literals.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"accepted": ["#123", "#1234", "#123456", "#12345678", "rgb(1, 2, 3)", "rgba(1, 2, 3, .5)", "rgb(10%,20%,30%)", "rgb(1 2 3)", "rgba(1 2 3 / 50%)", "rgb(10% 20 30% / .5)", "hsl(-30deg,80%,20%)", "hsla(360, 50%, 20%, 0.5)", "hsl(-30deg 80% 20% / 50%)"],
"rejected": ["rgb(1/2/3)", "rgb(10, 20 30)", "rgb(1,2,3 / .5)", "rgb(1 2 3 .5)", "rgb(1 2 3 / .5 / .4)", "rgb(1 2 3 /)", "rgb(10%,20,30%)", "rgba(1,,2,3)", "hsl(10,20% 30%)", "hsl(10/20%/30%)", "hsl(10 20 30)", "rgb(256 0 0)", "rgba(1,2,3,1.1)", "red", "image-set(x.png)"]
}
67 changes: 66 additions & 1 deletion tests/test_manifests.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ def test_examples_cover_every_contribution_kind():
kinds = set()
for package in EXAMPLES:
kinds.update(example(package.name)["contributes"])
assert kinds == {"voice_backend", "skill", "mcp_server", "context_source", "observer"}
assert kinds == set(json.loads(vm.SCHEMA_PATH.read_text(encoding='utf-8'))['$defs']['contributes']['properties'])


SCHEMA_REJECTIONS = {
Expand Down Expand Up @@ -271,3 +271,68 @@ def test_command_line_exit_status(tmp_path, capsys):
broken.write_text(json.dumps({"schema": "amadeus.extension/v0"}), encoding="utf-8")
assert vm.main([str(broken)]) == 1
assert "invalid" in capsys.readouterr().out


@pytest.mark.parametrize('name,value,code', [
('--danger', '#111111', 'T1.reserved_token'),
('--bg', 'url(https://example.invalid)', 'T2.css_input'),
('--font', 'Arial; color:red', 'T2.css_input'),
])
def test_theme_cannot_override_status_or_inject_css(name, value, code):
manifest = example('desktop-theme')
manifest['contributes']['theme'][0]['tokens'][name] = value
assert code in codes(manifest)


def test_theme_background_requires_a_contained_package_file(tmp_path):
manifest = example('desktop-theme')
manifest['contributes']['theme'][0]['background'] = {'image': 'bg.png', 'fit': 'cover', 'dim': .4}
assert not codes(manifest)
assert {issue.code for issue in vm.package_issues(manifest, tmp_path)} == {'P3.missing_file'}
(tmp_path / 'bg.png').write_bytes(b'Host separately validates image encoding and decoded dimensions')
assert 'P4.theme_image' in {issue.code for issue in vm.package_issues(manifest, tmp_path)}
(tmp_path / 'bg.png').write_bytes(b'\x89PNG\r\n\x1a\n' + b'header-only fixture; full decoding remains Host-owned')
assert not vm.package_issues(manifest, tmp_path)
manifest['contributes']['theme'][0]['background']['image'] = '../bg.png'
assert 'schema' in codes(manifest)


@pytest.mark.parametrize('name,value', [
('--unknown', '#123456'), ('--bg', 'image-set("x.png" 1x)'),
('--bg', 'expression(alert(1))'), ('--bg', 'red'), ('--bg', 'rgb(256 0 0)'),
('--bg', 'hsl(10 101% 20%)'), ('--bg', '#12345'), ('--bg', 'rgba(1,2,3,1.1)'),
('--font', 'Arial, serif'), ('--font', '</style>'),
])
def test_theme_rejects_unknown_tokens_and_non_literal_values(name, value):
manifest = example('desktop-theme')
manifest['contributes']['theme'][0]['tokens'][name] = value
assert codes(manifest)


@pytest.mark.parametrize('color', ['#123', '#1234', '#123456', '#12345678',
'rgb(10 20 30 / 50%)', 'rgba(10, 20, 30, .5)', 'hsl(-30deg 80% 20%)'])
def test_theme_color_literals_match_host_grammar(color):
manifest=example('desktop-theme')
manifest['contributes']['theme'][0]['tokens']['--bg']=color
assert not codes(manifest)


@pytest.mark.parametrize('font', ['"Segoe UI"', "'Noto Sans'", '微软雅黑'])
def test_single_quoted_or_unicode_font_family_syntax(font):
manifest = example('desktop-theme')
manifest['contributes']['theme'][0]['tokens']['--font'] = font
assert not codes(manifest)

@pytest.mark.parametrize('group', ['accepted', 'rejected'])
def test_shared_css_literal_cases(group):
cases = json.loads((ROOT / 'tests/fixtures/theme-color-literals.json').read_text(encoding='utf-8-sig'))
for color in cases[group]:
assert vm._theme_color_literal(color) is (group == 'accepted'), color


def test_theme_v0_rejects_webp_declaration_and_bytes(tmp_path):
manifest = example('desktop-theme')
manifest['contributes']['theme'][0]['background'] = {'image': 'bg.webp', 'fit': 'cover', 'dim': .4}
(tmp_path / 'bg.webp').write_bytes(b'RIFF' + b'\0' * 4 + b'WEBP' + b'fixture')
assert codes(manifest)
assert 'P4.theme_image' in {issue.code for issue in vm.package_issues(manifest, tmp_path)}
Loading