create-aimarket-agent 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. create_aimarket_agent-0.1.0/.gitignore +5 -0
  2. create_aimarket_agent-0.1.0/LICENSE +9 -0
  3. create_aimarket_agent-0.1.0/PKG-INFO +128 -0
  4. create_aimarket_agent-0.1.0/README.md +109 -0
  5. create_aimarket_agent-0.1.0/pyproject.toml +49 -0
  6. create_aimarket_agent-0.1.0/src/create_aimarket_agent/__init__.py +4 -0
  7. create_aimarket_agent-0.1.0/src/create_aimarket_agent/cli.py +99 -0
  8. create_aimarket_agent-0.1.0/src/create_aimarket_agent/templates/base/.dockerignore +11 -0
  9. create_aimarket_agent-0.1.0/src/create_aimarket_agent/templates/base/.env.example +4 -0
  10. create_aimarket_agent-0.1.0/src/create_aimarket_agent/templates/base/.github/workflows/test.yml +18 -0
  11. create_aimarket_agent-0.1.0/src/create_aimarket_agent/templates/base/.gitignore +5 -0
  12. create_aimarket_agent-0.1.0/src/create_aimarket_agent/templates/base/Dockerfile +15 -0
  13. create_aimarket_agent-0.1.0/src/create_aimarket_agent/templates/base/Makefile +16 -0
  14. create_aimarket_agent-0.1.0/src/create_aimarket_agent/templates/base/README.md +27 -0
  15. create_aimarket_agent-0.1.0/src/create_aimarket_agent/templates/base/agent.py +81 -0
  16. create_aimarket_agent-0.1.0/src/create_aimarket_agent/templates/base/capability.json +13 -0
  17. create_aimarket_agent-0.1.0/src/create_aimarket_agent/templates/base/configure_provider.py +22 -0
  18. create_aimarket_agent-0.1.0/src/create_aimarket_agent/templates/base/provider_signing.py +77 -0
  19. create_aimarket_agent-0.1.0/src/create_aimarket_agent/templates/base/pyproject.toml +18 -0
  20. create_aimarket_agent-0.1.0/src/create_aimarket_agent/templates/base/test_agent.py +73 -0
  21. create_aimarket_agent-0.1.0/src/create_aimarket_agent/templates/base/validate_manifest.py +145 -0
  22. create_aimarket_agent-0.1.0/tests/test_cli.py +166 -0
  23. create_aimarket_agent-0.1.0/tests/test_docs_i18n.py +37 -0
  24. create_aimarket_agent-0.1.0/tests/test_generated_project.py +153 -0
@@ -0,0 +1,5 @@
1
+ .venv/
2
+ __pycache__/
3
+ .pytest_cache/
4
+ dist/
5
+ *.egg-info/
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AICOM Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
@@ -0,0 +1,128 @@
1
+ Metadata-Version: 2.4
2
+ Name: create-aimarket-agent
3
+ Version: 0.1.0
4
+ Summary: Scaffold a minimal AIMarket Protocol v2 capability provider
5
+ Project-URL: Documentation, https://github.com/alexar76/create-aimarket-agent#readme
6
+ Project-URL: Issues, https://github.com/alexar76/create-aimarket-agent/issues
7
+ Project-URL: Repository, https://github.com/alexar76/create-aimarket-agent
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Requires-Python: >=3.11
11
+ Provides-Extra: dev
12
+ Requires-Dist: build>=1.2; extra == 'dev'
13
+ Requires-Dist: cryptography>=44; extra == 'dev'
14
+ Requires-Dist: fastapi>=0.115; extra == 'dev'
15
+ Requires-Dist: httpx>=0.27; extra == 'dev'
16
+ Requires-Dist: pytest-cov>=5; extra == 'dev'
17
+ Requires-Dist: pytest>=8; extra == 'dev'
18
+ Description-Content-Type: text/markdown
19
+
20
+ # create-aimarket-agent
21
+
22
+ <p align="center">
23
+ <strong>From an empty directory to a signed AIMarket Protocol v2 provider.</strong><br>
24
+ A safe, deterministic project generator with tests, Docker packaging, and explicit publishing.
25
+ </p>
26
+
27
+ <!-- aicom-readme-badges -->
28
+ <p align="center">
29
+ <a href="https://github.com/alexar76/create-aimarket-agent/actions/workflows/ci.yml"><img src="https://github.com/alexar76/create-aimarket-agent/actions/workflows/ci.yml/badge.svg" alt="CI" /></a>
30
+ <img src="https://img.shields.io/badge/python-%3E%3D3.11-3776AB" alt="Python >=3.11" />
31
+ <img src="https://img.shields.io/badge/tests-45%20passing-4c1" alt="45 tests passing" />
32
+ <img src="https://img.shields.io/badge/branch%20coverage-100%25-4c1" alt="100% branch coverage" />
33
+ <img src="https://img.shields.io/badge/docs-EN%20RU%20ES%20FR%20ZH-9c70ff" alt="Documentation in 5 languages" />
34
+ <img src="https://img.shields.io/badge/AIMarket-Protocol%20v2-35e7ff" alt="AIMarket Protocol v2" />
35
+ <img src="https://img.shields.io/badge/signing-Ed25519-8d83ff" alt="Ed25519 signing" />
36
+ <a href="https://github.com/alexar76/create-aimarket-agent/blob/main/LICENSE"><img src="https://raw.githubusercontent.com/alexar76/create-aimarket-agent/main/docs/badges/license.svg" alt="License: MIT" /></a>
37
+ </p>
38
+ <!-- /aicom-readme-badges -->
39
+
40
+ <p align="center">
41
+ <a href="README.md"><b>English</b></a> ·
42
+ <a href="docs/README.ru.md">Русский</a> ·
43
+ <a href="docs/README.es.md">Español</a> ·
44
+ <a href="docs/README.fr.md">Français</a> ·
45
+ <a href="docs/README.zh.md">中文</a> ·
46
+ <a href="https://github.com/alexar76/aicom/blob/main/docs/localization-glossary.md">Localization glossary</a>
47
+ </p>
48
+
49
+ ## Quick start
50
+
51
+ ```bash
52
+ uvx create-aimarket-agent my-agent --kind tool --metis
53
+ cd my-agent
54
+ uv sync --extra dev
55
+ uv run python configure_provider.py
56
+ uv run pytest
57
+ uv run python validate_manifest.py
58
+ uv run python agent.py
59
+ ```
60
+
61
+ The command, identifiers, flags, and generated filenames are stable in every language.
62
+
63
+ ## Generated repository
64
+
65
+ | Path | Role |
66
+ |---|---|
67
+ | `agent.py` | FastAPI health and invoke endpoints |
68
+ | `capability.json` | AIMarket Protocol v2 capability manifest |
69
+ | `provider_signing.py` | Persistent Ed25519 identity and request-bound response signing |
70
+ | `configure_provider.py` | Writes the public provider key into the manifest atomically |
71
+ | `validate_manifest.py` | Fail-closed structural validation before publishing |
72
+ | `test_agent.py` | API, request-bound signature, and request-size tests |
73
+ | `Dockerfile` · `.dockerignore` | Non-root container packaging with persistent key volume |
74
+ | `.github/workflows/test.yml` | Generated-project CI |
75
+
76
+ ## Security model
77
+
78
+ - Scaffolding is atomic: a failed copy or replacement leaves no partial target repository.
79
+ - Project names are allow-listed before they enter source or JSON files.
80
+ - The provider key is a 32-byte Ed25519 seed stored with mode `0600`; symlinks and non-regular files are rejected.
81
+ - Responses sign a canonical request-bound envelope containing `capability_id`, `product_id`, the SHA-256 input digest, and the result. This prevents replay across requests.
82
+ - The generated service accepts only the product and capability identity declared in its manifest,
83
+ so an untrusted caller cannot make the provider key sign another identity.
84
+ - The manifest validator rejects duplicate JSON keys, malformed Ed25519 keys, non-finite prices,
85
+ malformed URLs, and public HTTP endpoints before the publish step.
86
+ - The default invoke endpoint rejects missing, malformed, or oversized request bodies.
87
+ - Publishing is never automatic. Stake, publisher identity, trust policy, and Hub registration remain explicit operator actions.
88
+
89
+ ## Project kinds
90
+
91
+ ```bash
92
+ create-aimarket-agent my-tool --kind tool
93
+ create-aimarket-agent my-data --kind data-provider
94
+ create-aimarket-agent my-orchestrator --kind orchestrator
95
+ create-aimarket-agent my-agent --no-metis
96
+ ```
97
+
98
+ ## Docker
99
+
100
+ The generated image runs as an unprivileged user, includes a health check, listens on
101
+ `0.0.0.0:8080`, and stores the provider key in `/data/provider.key`.
102
+
103
+ ```bash
104
+ docker build -t my-agent .
105
+ docker run --read-only --tmpfs /tmp -p 127.0.0.1:8080:8080 -v my-agent-key:/data my-agent
106
+ ```
107
+
108
+ Back up the provider key before publishing. Replacing it invalidates the `provider_pubkey`
109
+ registered with the Hub. Keep the port loopback-only until an HTTPS ingress provides production
110
+ traffic, concurrency, and rate limits; the response signature does not authorize direct callers.
111
+
112
+ ## Development of this generator
113
+
114
+ ```bash
115
+ uv sync --extra dev
116
+ uv run pytest
117
+ uv build --wheel
118
+ ```
119
+
120
+ ## Localization contract
121
+
122
+ Documentation is available in English, Russian, Spanish, French, and Chinese and follows the
123
+ canonical [AICOM localization glossary](https://github.com/alexar76/aicom/blob/main/docs/localization-glossary.md).
124
+ Code, commands, flags, filenames, API fields, env vars, brands, `LIVE`, and `SIM` are never translated.
125
+
126
+ ## License
127
+
128
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,109 @@
1
+ # create-aimarket-agent
2
+
3
+ <p align="center">
4
+ <strong>From an empty directory to a signed AIMarket Protocol v2 provider.</strong><br>
5
+ A safe, deterministic project generator with tests, Docker packaging, and explicit publishing.
6
+ </p>
7
+
8
+ <!-- aicom-readme-badges -->
9
+ <p align="center">
10
+ <a href="https://github.com/alexar76/create-aimarket-agent/actions/workflows/ci.yml"><img src="https://github.com/alexar76/create-aimarket-agent/actions/workflows/ci.yml/badge.svg" alt="CI" /></a>
11
+ <img src="https://img.shields.io/badge/python-%3E%3D3.11-3776AB" alt="Python >=3.11" />
12
+ <img src="https://img.shields.io/badge/tests-45%20passing-4c1" alt="45 tests passing" />
13
+ <img src="https://img.shields.io/badge/branch%20coverage-100%25-4c1" alt="100% branch coverage" />
14
+ <img src="https://img.shields.io/badge/docs-EN%20RU%20ES%20FR%20ZH-9c70ff" alt="Documentation in 5 languages" />
15
+ <img src="https://img.shields.io/badge/AIMarket-Protocol%20v2-35e7ff" alt="AIMarket Protocol v2" />
16
+ <img src="https://img.shields.io/badge/signing-Ed25519-8d83ff" alt="Ed25519 signing" />
17
+ <a href="https://github.com/alexar76/create-aimarket-agent/blob/main/LICENSE"><img src="https://raw.githubusercontent.com/alexar76/create-aimarket-agent/main/docs/badges/license.svg" alt="License: MIT" /></a>
18
+ </p>
19
+ <!-- /aicom-readme-badges -->
20
+
21
+ <p align="center">
22
+ <a href="README.md"><b>English</b></a> ·
23
+ <a href="docs/README.ru.md">Русский</a> ·
24
+ <a href="docs/README.es.md">Español</a> ·
25
+ <a href="docs/README.fr.md">Français</a> ·
26
+ <a href="docs/README.zh.md">中文</a> ·
27
+ <a href="https://github.com/alexar76/aicom/blob/main/docs/localization-glossary.md">Localization glossary</a>
28
+ </p>
29
+
30
+ ## Quick start
31
+
32
+ ```bash
33
+ uvx create-aimarket-agent my-agent --kind tool --metis
34
+ cd my-agent
35
+ uv sync --extra dev
36
+ uv run python configure_provider.py
37
+ uv run pytest
38
+ uv run python validate_manifest.py
39
+ uv run python agent.py
40
+ ```
41
+
42
+ The command, identifiers, flags, and generated filenames are stable in every language.
43
+
44
+ ## Generated repository
45
+
46
+ | Path | Role |
47
+ |---|---|
48
+ | `agent.py` | FastAPI health and invoke endpoints |
49
+ | `capability.json` | AIMarket Protocol v2 capability manifest |
50
+ | `provider_signing.py` | Persistent Ed25519 identity and request-bound response signing |
51
+ | `configure_provider.py` | Writes the public provider key into the manifest atomically |
52
+ | `validate_manifest.py` | Fail-closed structural validation before publishing |
53
+ | `test_agent.py` | API, request-bound signature, and request-size tests |
54
+ | `Dockerfile` · `.dockerignore` | Non-root container packaging with persistent key volume |
55
+ | `.github/workflows/test.yml` | Generated-project CI |
56
+
57
+ ## Security model
58
+
59
+ - Scaffolding is atomic: a failed copy or replacement leaves no partial target repository.
60
+ - Project names are allow-listed before they enter source or JSON files.
61
+ - The provider key is a 32-byte Ed25519 seed stored with mode `0600`; symlinks and non-regular files are rejected.
62
+ - Responses sign a canonical request-bound envelope containing `capability_id`, `product_id`, the SHA-256 input digest, and the result. This prevents replay across requests.
63
+ - The generated service accepts only the product and capability identity declared in its manifest,
64
+ so an untrusted caller cannot make the provider key sign another identity.
65
+ - The manifest validator rejects duplicate JSON keys, malformed Ed25519 keys, non-finite prices,
66
+ malformed URLs, and public HTTP endpoints before the publish step.
67
+ - The default invoke endpoint rejects missing, malformed, or oversized request bodies.
68
+ - Publishing is never automatic. Stake, publisher identity, trust policy, and Hub registration remain explicit operator actions.
69
+
70
+ ## Project kinds
71
+
72
+ ```bash
73
+ create-aimarket-agent my-tool --kind tool
74
+ create-aimarket-agent my-data --kind data-provider
75
+ create-aimarket-agent my-orchestrator --kind orchestrator
76
+ create-aimarket-agent my-agent --no-metis
77
+ ```
78
+
79
+ ## Docker
80
+
81
+ The generated image runs as an unprivileged user, includes a health check, listens on
82
+ `0.0.0.0:8080`, and stores the provider key in `/data/provider.key`.
83
+
84
+ ```bash
85
+ docker build -t my-agent .
86
+ docker run --read-only --tmpfs /tmp -p 127.0.0.1:8080:8080 -v my-agent-key:/data my-agent
87
+ ```
88
+
89
+ Back up the provider key before publishing. Replacing it invalidates the `provider_pubkey`
90
+ registered with the Hub. Keep the port loopback-only until an HTTPS ingress provides production
91
+ traffic, concurrency, and rate limits; the response signature does not authorize direct callers.
92
+
93
+ ## Development of this generator
94
+
95
+ ```bash
96
+ uv sync --extra dev
97
+ uv run pytest
98
+ uv build --wheel
99
+ ```
100
+
101
+ ## Localization contract
102
+
103
+ Documentation is available in English, Russian, Spanish, French, and Chinese and follows the
104
+ canonical [AICOM localization glossary](https://github.com/alexar76/aicom/blob/main/docs/localization-glossary.md).
105
+ Code, commands, flags, filenames, API fields, env vars, brands, `LIVE`, and `SIM` are never translated.
106
+
107
+ ## License
108
+
109
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,49 @@
1
+ [project]
2
+ name = "create-aimarket-agent"
3
+ version = "0.1.0"
4
+ description = "Scaffold a minimal AIMarket Protocol v2 capability provider"
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ license = { text = "MIT" }
8
+ dependencies = []
9
+
10
+ [project.urls]
11
+ Documentation = "https://github.com/alexar76/create-aimarket-agent#readme"
12
+ Issues = "https://github.com/alexar76/create-aimarket-agent/issues"
13
+ Repository = "https://github.com/alexar76/create-aimarket-agent"
14
+
15
+ [project.optional-dependencies]
16
+ dev = [
17
+ "build>=1.2",
18
+ "cryptography>=44",
19
+ "fastapi>=0.115",
20
+ "httpx>=0.27",
21
+ "pytest>=8",
22
+ "pytest-cov>=5",
23
+ ]
24
+
25
+ [project.scripts]
26
+ create-aimarket-agent = "create_aimarket_agent.cli:main"
27
+
28
+ [build-system]
29
+ requires = ["hatchling"]
30
+ build-backend = "hatchling.build"
31
+
32
+ [tool.hatch.build.targets.wheel]
33
+ packages = ["src/create_aimarket_agent"]
34
+
35
+ # Explicit sdist allow-list so hatchling does not walk up to the monorepo .gitignore
36
+ # (and so template `.env.example` / nested scaffold files stay intentional package content).
37
+ [tool.hatch.build.targets.sdist]
38
+ include = [
39
+ "/src",
40
+ "/tests",
41
+ "/README.md",
42
+ "/LICENSE",
43
+ "/pyproject.toml",
44
+ "/.gitignore",
45
+ ]
46
+
47
+ [tool.pytest.ini_options]
48
+ testpaths = ["tests"]
49
+ addopts = "--cov=create_aimarket_agent --cov-branch --cov-report=term-missing --cov-fail-under=95"
@@ -0,0 +1,4 @@
1
+ """AIMarket agent project scaffolder."""
2
+
3
+ __version__ = "0.1.0"
4
+
@@ -0,0 +1,99 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import re
5
+ import shutil
6
+ import sys
7
+ import tempfile
8
+ from importlib.resources import files
9
+ from pathlib import Path
10
+
11
+ KINDS = ("tool", "data-provider", "orchestrator")
12
+ _SAFE_NAME = re.compile(r"^[A-Za-z0-9][A-Za-z0-9 ._-]{0,63}$")
13
+
14
+
15
+ def slug(value: str) -> str:
16
+ clean = re.sub(r"[^a-z0-9]+", "-", value.strip().lower()).strip("-")
17
+ if not clean:
18
+ raise ValueError("project name must contain a letter or digit")
19
+ return clean
20
+
21
+
22
+ def validate_name(value: str) -> str:
23
+ value = value.strip()
24
+ if not _SAFE_NAME.fullmatch(value):
25
+ raise ValueError(
26
+ "project name must be 1-64 characters and contain only letters, digits, spaces, dots, dashes, or underscores"
27
+ )
28
+ return value
29
+
30
+
31
+ def scaffold(target: Path, *, name: str, kind: str, metis: bool) -> Path:
32
+ name = validate_name(name)
33
+ if kind not in KINDS:
34
+ raise ValueError(f"kind must be one of: {', '.join(KINDS)}")
35
+ target = target.resolve()
36
+ if target.exists():
37
+ raise FileExistsError(f"target already exists: {target}")
38
+ template = files("create_aimarket_agent").joinpath("templates/base")
39
+ target.parent.mkdir(parents=True, exist_ok=True)
40
+ stage_root = Path(tempfile.mkdtemp(prefix=f".{target.name}.tmp-", dir=target.parent))
41
+ staged = stage_root / target.name
42
+ replacements = {
43
+ "__PROJECT_NAME__": name,
44
+ "__PROJECT_SLUG__": slug(name),
45
+ "__AGENT_KIND__": kind,
46
+ "__METIS_ENABLED__": "true" if metis else "false",
47
+ "__METIS_PYTHON__": "True" if metis else "False",
48
+ }
49
+ try:
50
+ shutil.copytree(
51
+ str(template),
52
+ staged,
53
+ ignore=shutil.ignore_patterns("__pycache__", "*.pyc", "*.pyo"),
54
+ )
55
+ for path in staged.rglob("*"):
56
+ if path.is_file():
57
+ text = path.read_text(encoding="utf-8")
58
+ for old, new in replacements.items():
59
+ text = text.replace(old, new)
60
+ path.write_text(text, encoding="utf-8")
61
+ if target.exists():
62
+ raise FileExistsError(f"target appeared while scaffolding: {target}")
63
+ staged.rename(target)
64
+ finally:
65
+ shutil.rmtree(stage_root, ignore_errors=True)
66
+ return target
67
+
68
+
69
+ def parser() -> argparse.ArgumentParser:
70
+ p = argparse.ArgumentParser(prog="create-aimarket-agent")
71
+ p.add_argument("name", nargs="?", help="project name")
72
+ p.add_argument("--kind", choices=KINDS, default="tool")
73
+ p.add_argument("--metis", action=argparse.BooleanOptionalAction, default=True)
74
+ p.add_argument("--directory", type=Path, help="output directory")
75
+ return p
76
+
77
+
78
+ def main(argv: list[str] | None = None) -> int:
79
+ args = parser().parse_args(argv)
80
+ name = args.name
81
+ if not name and sys.stdin.isatty():
82
+ name = input("Agent name: ").strip()
83
+ if not name:
84
+ parser().error("name is required")
85
+ try:
86
+ name = validate_name(name)
87
+ target = args.directory or Path.cwd() / slug(name)
88
+ scaffold(target, name=name, kind=args.kind, metis=args.metis)
89
+ except (ValueError, FileExistsError, OSError) as exc:
90
+ print(f"error: {exc}", file=sys.stderr)
91
+ return 2
92
+ print(f"Created {name} in {target}")
93
+ print(f"Next: cd {target} && uv sync --extra dev")
94
+ print("Then: uv run python configure_provider.py && uv run pytest && uv run python agent.py")
95
+ return 0
96
+
97
+
98
+ if __name__ == "__main__":
99
+ raise SystemExit(main())
@@ -0,0 +1,11 @@
1
+ .env
2
+ .git
3
+ .github
4
+ .aimarket
5
+ .venv
6
+ __pycache__
7
+ .pytest_cache
8
+ .coverage
9
+ build
10
+ dist
11
+ *.egg-info
@@ -0,0 +1,4 @@
1
+ PORT=8080
2
+ AIMARKET_HUB_URL=https://modelmarket.dev
3
+ AIMARKET_METIS_URL=https://metis.modelmarket.dev
4
+ AIMARKET_PROVIDER_KEY_FILE=.aimarket/provider.key
@@ -0,0 +1,18 @@
1
+ name: test
2
+ on: [push, pull_request]
3
+ permissions:
4
+ contents: read
5
+ jobs:
6
+ test:
7
+ runs-on: ubuntu-latest
8
+ timeout-minutes: 10
9
+ steps:
10
+ - uses: actions/checkout@v4
11
+ with:
12
+ persist-credentials: false
13
+ - uses: actions/setup-python@v5
14
+ with: {python-version: "3.12"}
15
+ - run: pip install -e '.[dev]'
16
+ - run: python configure_provider.py
17
+ - run: pytest -q
18
+ - run: python validate_manifest.py
@@ -0,0 +1,5 @@
1
+ .env
2
+ .venv/
3
+ __pycache__/
4
+ .pytest_cache/
5
+ .aimarket/
@@ -0,0 +1,15 @@
1
+ FROM python:3.12-slim
2
+ WORKDIR /app
3
+ COPY . .
4
+ RUN pip install --no-cache-dir .
5
+ RUN mkdir -p /data && chown 65532:65532 /data
6
+ ENV HOST=0.0.0.0 \
7
+ AIMARKET_PROVIDER_IDENTITY_FILE=/data/provider.key \
8
+ PYTHONDONTWRITEBYTECODE=1 \
9
+ PYTHONUNBUFFERED=1
10
+ USER 65532:65532
11
+ VOLUME ["/data"]
12
+ EXPOSE 8080
13
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
14
+ CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/health', timeout=3).read()"
15
+ CMD ["python", "agent.py"]
@@ -0,0 +1,16 @@
1
+ .PHONY: configure dev test validate publish-dry-run
2
+
3
+ configure:
4
+ uv run python configure_provider.py
5
+
6
+ dev: configure
7
+ uv run python agent.py
8
+
9
+ test:
10
+ uv run pytest -q
11
+
12
+ validate: configure
13
+ uv run python validate_manifest.py
14
+
15
+ publish-dry-run: validate
16
+ @echo "Dry run only: aimarket publish capability.json --hub $${AIMARKET_HUB_URL:-http://127.0.0.1:9083}"
@@ -0,0 +1,27 @@
1
+ # __PROJECT_NAME__
2
+
3
+ Generated AIMarket Protocol v2 `__AGENT_KIND__` provider.
4
+
5
+ ```bash
6
+ uv sync --extra dev
7
+ uv run python configure_provider.py
8
+ uv run pytest
9
+ uv run python agent.py
10
+ make validate
11
+ make publish-dry-run
12
+ ```
13
+
14
+ Validate `capability.json`, then publish deliberately with the AIMarket Hub CLI. Never commit `.env`.
15
+
16
+ The Ed25519 provider key lives in `.aimarket/provider.key` and is gitignored. Back it up: changing
17
+ it after publish invalidates the `provider_pubkey` registered with the Hub. The provider accepts
18
+ only the `product_id` and `capability_id` declared in `capability.json`; never replace that check
19
+ with caller-selected signing identity. The validator rejects ambiguous JSON, malformed Ed25519
20
+ keys, non-finite prices, and public HTTP invoke URLs.
21
+
22
+ The Dockerfile is a development image; mount the same key as a secret before containerizing a
23
+ published provider. Keep the container port private until an HTTPS ingress adds production traffic,
24
+ concurrency, and rate limits. The generated endpoint authenticates its output with a signature; it
25
+ does not authenticate or bill clients that bypass Hub.
26
+ Responses use the replay-resistant AIMarket request-bound signature, covering the capability,
27
+ product, SHA-256 of the exact input, and result. Keep those request fields when extending `agent.py`.
@@ -0,0 +1,81 @@
1
+ """Minimal AIMarket capability provider generated by create-aimarket-agent."""
2
+ from __future__ import annotations
3
+
4
+ import hmac
5
+ import os
6
+ from fastapi import FastAPI, Request
7
+ from fastapi.responses import JSONResponse
8
+ from pydantic import BaseModel, Field
9
+
10
+ from provider_signing import ProviderSigner
11
+
12
+ app = FastAPI(
13
+ title="__PROJECT_NAME__",
14
+ docs_url=None,
15
+ redoc_url=None,
16
+ openapi_url=None,
17
+ )
18
+ METIS_ENABLED = __METIS_PYTHON__
19
+ SIGNER = ProviderSigner()
20
+ PRODUCT_ID = "__PROJECT_SLUG__"
21
+ CAPABILITY_ID = "__PROJECT_SLUG__.invoke@v1"
22
+
23
+
24
+ class InvokeInput(BaseModel):
25
+ input: dict = Field(default_factory=dict)
26
+ product_id: str = Field(default=PRODUCT_ID, max_length=128)
27
+ capability_id: str = Field(default=CAPABILITY_ID, max_length=192)
28
+
29
+
30
+ @app.middleware("http")
31
+ async def request_boundary(request: Request, call_next):
32
+ response = None
33
+ if request.method == "POST" and request.url.path == "/invoke":
34
+ raw_length = request.headers.get("content-length")
35
+ try:
36
+ length = int(raw_length) if raw_length is not None else -1
37
+ except ValueError:
38
+ length = -1
39
+ if length < 0 or length > 1_048_576:
40
+ response = JSONResponse({"detail": "request body must be 0-1048576 bytes"}, status_code=413)
41
+ if response is None:
42
+ response = await call_next(request)
43
+ response.headers["Cache-Control"] = "no-store"
44
+ response.headers["X-Content-Type-Options"] = "nosniff"
45
+ response.headers["Referrer-Policy"] = "no-referrer"
46
+ response.headers["X-Frame-Options"] = "DENY"
47
+ return response
48
+
49
+
50
+ @app.get("/health")
51
+ def health() -> dict:
52
+ return {"ok": True, "agent": "__PROJECT_SLUG__", "kind": "__AGENT_KIND__",
53
+ "provider_pubkey": SIGNER.public_key_b64}
54
+
55
+
56
+ @app.post("/invoke")
57
+ def invoke(request: InvokeInput) -> JSONResponse:
58
+ # Identity belongs to capability.json, not to an untrusted caller. Allowing
59
+ # arbitrary identifiers here would turn this key into a signing oracle for
60
+ # another product or capability that happens to reuse the provider key.
61
+ if not hmac.compare_digest(request.product_id, PRODUCT_ID):
62
+ return JSONResponse({"detail": "product_id does not match this provider"}, status_code=400)
63
+ if not hmac.compare_digest(request.capability_id, CAPABILITY_ID):
64
+ return JSONResponse({"detail": "capability_id does not match this provider"}, status_code=400)
65
+ # Replace this deterministic example with your capability logic.
66
+ result = {"message": "Hello from __PROJECT_NAME__", "received": request.input,
67
+ "verification_requested": METIS_ENABLED}
68
+ return JSONResponse(
69
+ {"success": True, "result": result},
70
+ headers={"X-Provider-Signature": SIGNER.sign_result(
71
+ result,
72
+ capability_id=CAPABILITY_ID,
73
+ product_id=PRODUCT_ID,
74
+ input_payload=request.input,
75
+ )},
76
+ )
77
+
78
+
79
+ if __name__ == "__main__":
80
+ import uvicorn
81
+ uvicorn.run(app, host=os.getenv("HOST", "127.0.0.1"), port=int(os.getenv("PORT", "8080")))
@@ -0,0 +1,13 @@
1
+ {
2
+ "product_id": "__PROJECT_SLUG__",
3
+ "capability_id": "__PROJECT_SLUG__.invoke@v1",
4
+ "name": "__PROJECT_NAME__",
5
+ "description": "A generated AIMarket __AGENT_KIND__ capability",
6
+ "invoke_url": "http://127.0.0.1:8080/invoke",
7
+ "publisher_id": "community",
8
+ "provider_pubkey": "",
9
+ "price_per_call_usd": 0.001,
10
+ "input_schema": {"type": "object", "additionalProperties": true},
11
+ "output_schema": {"type": "object", "additionalProperties": true},
12
+ "verification": {"metis": __METIS_ENABLED__}
13
+ }
@@ -0,0 +1,22 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ from provider_signing import ProviderSigner
7
+
8
+
9
+ def main() -> int:
10
+ signer = ProviderSigner()
11
+ path = Path("capability.json")
12
+ manifest = json.loads(path.read_text(encoding="utf-8"))
13
+ manifest["provider_pubkey"] = signer.public_key_b64
14
+ temporary = path.with_suffix(".json.tmp")
15
+ temporary.write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
16
+ temporary.replace(path)
17
+ print(f"provider identity ready: {signer.public_key_b64}")
18
+ return 0
19
+
20
+
21
+ if __name__ == "__main__":
22
+ raise SystemExit(main())