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.
- create_aimarket_agent-0.1.0/.gitignore +5 -0
- create_aimarket_agent-0.1.0/LICENSE +9 -0
- create_aimarket_agent-0.1.0/PKG-INFO +128 -0
- create_aimarket_agent-0.1.0/README.md +109 -0
- create_aimarket_agent-0.1.0/pyproject.toml +49 -0
- create_aimarket_agent-0.1.0/src/create_aimarket_agent/__init__.py +4 -0
- create_aimarket_agent-0.1.0/src/create_aimarket_agent/cli.py +99 -0
- create_aimarket_agent-0.1.0/src/create_aimarket_agent/templates/base/.dockerignore +11 -0
- create_aimarket_agent-0.1.0/src/create_aimarket_agent/templates/base/.env.example +4 -0
- create_aimarket_agent-0.1.0/src/create_aimarket_agent/templates/base/.github/workflows/test.yml +18 -0
- create_aimarket_agent-0.1.0/src/create_aimarket_agent/templates/base/.gitignore +5 -0
- create_aimarket_agent-0.1.0/src/create_aimarket_agent/templates/base/Dockerfile +15 -0
- create_aimarket_agent-0.1.0/src/create_aimarket_agent/templates/base/Makefile +16 -0
- create_aimarket_agent-0.1.0/src/create_aimarket_agent/templates/base/README.md +27 -0
- create_aimarket_agent-0.1.0/src/create_aimarket_agent/templates/base/agent.py +81 -0
- create_aimarket_agent-0.1.0/src/create_aimarket_agent/templates/base/capability.json +13 -0
- create_aimarket_agent-0.1.0/src/create_aimarket_agent/templates/base/configure_provider.py +22 -0
- create_aimarket_agent-0.1.0/src/create_aimarket_agent/templates/base/provider_signing.py +77 -0
- create_aimarket_agent-0.1.0/src/create_aimarket_agent/templates/base/pyproject.toml +18 -0
- create_aimarket_agent-0.1.0/src/create_aimarket_agent/templates/base/test_agent.py +73 -0
- create_aimarket_agent-0.1.0/src/create_aimarket_agent/templates/base/validate_manifest.py +145 -0
- create_aimarket_agent-0.1.0/tests/test_cli.py +166 -0
- create_aimarket_agent-0.1.0/tests/test_docs_i18n.py +37 -0
- create_aimarket_agent-0.1.0/tests/test_generated_project.py +153 -0
|
@@ -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,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())
|
create_aimarket_agent-0.1.0/src/create_aimarket_agent/templates/base/.github/workflows/test.yml
ADDED
|
@@ -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,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())
|