ream-artifacts 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.
- ream_artifacts-0.1.0/PKG-INFO +137 -0
- ream_artifacts-0.1.0/README.md +117 -0
- ream_artifacts-0.1.0/pyproject.toml +80 -0
- ream_artifacts-0.1.0/pyproject.toml.orig +109 -0
- ream_artifacts-0.1.0/src/artifacts/__init__.py +91 -0
- ream_artifacts-0.1.0/src/artifacts/py.typed +0 -0
- ream_artifacts-0.1.0/src/ream/__init__.py +0 -0
- ream_artifacts-0.1.0/src/ream/admin.py +698 -0
- ream_artifacts-0.1.0/src/ream/anchors.py +672 -0
- ream_artifacts-0.1.0/src/ream/api.py +3779 -0
- ream_artifacts-0.1.0/src/ream/app.py +332 -0
- ream_artifacts-0.1.0/src/ream/brand/rooted-lg-dk.svg +12 -0
- ream_artifacts-0.1.0/src/ream/brand/rooted-lg-lt.svg +12 -0
- ream_artifacts-0.1.0/src/ream/brand/rooted-lg-mono.svg +12 -0
- ream_artifacts-0.1.0/src/ream/brand/rooted-sm-dk.svg +11 -0
- ream_artifacts-0.1.0/src/ream/brand/rooted-sm-lt.svg +11 -0
- ream_artifacts-0.1.0/src/ream/brand/rooted-sm-mono.svg +11 -0
- ream_artifacts-0.1.0/src/ream/cli.py +185 -0
- ream_artifacts-0.1.0/src/ream/client.py +2574 -0
- ream_artifacts-0.1.0/src/ream/clientlevel.py +46 -0
- ream_artifacts-0.1.0/src/ream/comments.py +2073 -0
- ream_artifacts-0.1.0/src/ream/connect.py +347 -0
- ream_artifacts-0.1.0/src/ream/connect_cli.py +222 -0
- ream_artifacts-0.1.0/src/ream/content.py +1322 -0
- ream_artifacts-0.1.0/src/ream/csp.py +234 -0
- ream_artifacts-0.1.0/src/ream/db.py +1190 -0
- ream_artifacts-0.1.0/src/ream/decide.py +2727 -0
- ream_artifacts-0.1.0/src/ream/decisions.py +2901 -0
- ream_artifacts-0.1.0/src/ream/design.py +196 -0
- ream_artifacts-0.1.0/src/ream/email_share.py +202 -0
- ream_artifacts-0.1.0/src/ream/envnames.py +51 -0
- ream_artifacts-0.1.0/src/ream/events.py +694 -0
- ream_artifacts-0.1.0/src/ream/folders.py +252 -0
- ream_artifacts-0.1.0/src/ream/formanswer.py +537 -0
- ream_artifacts-0.1.0/src/ream/formrecord.py +313 -0
- ream_artifacts-0.1.0/src/ream/forms.py +830 -0
- ream_artifacts-0.1.0/src/ream/forms_ui.py +396 -0
- ream_artifacts-0.1.0/src/ream/frame_pass.py +106 -0
- ream_artifacts-0.1.0/src/ream/gallery.py +7769 -0
- ream_artifacts-0.1.0/src/ream/ggscan.py +243 -0
- ream_artifacts-0.1.0/src/ream/grants.py +90 -0
- ream_artifacts-0.1.0/src/ream/holds.py +175 -0
- ream_artifacts-0.1.0/src/ream/hosts.py +103 -0
- ream_artifacts-0.1.0/src/ream/idempotency.py +110 -0
- ream_artifacts-0.1.0/src/ream/ids.py +7 -0
- ream_artifacts-0.1.0/src/ream/image_store.py +343 -0
- ream_artifacts-0.1.0/src/ream/images.py +439 -0
- ream_artifacts-0.1.0/src/ream/inbox.py +246 -0
- ream_artifacts-0.1.0/src/ream/keys.py +373 -0
- ream_artifacts-0.1.0/src/ream/listening.py +373 -0
- ream_artifacts-0.1.0/src/ream/logo.py +56 -0
- ream_artifacts-0.1.0/src/ream/mail.py +119 -0
- ream_artifacts-0.1.0/src/ream/maintenance.py +158 -0
- ream_artifacts-0.1.0/src/ream/markdown.py +165 -0
- ream_artifacts-0.1.0/src/ream/mcp.py +1193 -0
- ream_artifacts-0.1.0/src/ream/mcp_apps.py +371 -0
- ream_artifacts-0.1.0/src/ream/mcp_clients.py +508 -0
- ream_artifacts-0.1.0/src/ream/mcp_headers.py +108 -0
- ream_artifacts-0.1.0/src/ream/mcp_tools.py +1220 -0
- ream_artifacts-0.1.0/src/ream/migrate.py +515 -0
- ream_artifacts-0.1.0/src/ream/news.py +295 -0
- ream_artifacts-0.1.0/src/ream/nudge.py +251 -0
- ream_artifacts-0.1.0/src/ream/oauth.py +803 -0
- ream_artifacts-0.1.0/src/ream/oidc.py +555 -0
- ream_artifacts-0.1.0/src/ream/onboarding.py +1142 -0
- ream_artifacts-0.1.0/src/ream/owner.py +132 -0
- ream_artifacts-0.1.0/src/ream/packing.py +124 -0
- ream_artifacts-0.1.0/src/ream/pagediff.py +1257 -0
- ream_artifacts-0.1.0/src/ream/partner.py +680 -0
- ream_artifacts-0.1.0/src/ream/partner_in.py +977 -0
- ream_artifacts-0.1.0/src/ream/pickup.py +10 -0
- ream_artifacts-0.1.0/src/ream/plugin_api.py +547 -0
- ream_artifacts-0.1.0/src/ream/plugin_cli.py +414 -0
- ream_artifacts-0.1.0/src/ream/py.typed +0 -0
- ream_artifacts-0.1.0/src/ream/ratelimit.py +137 -0
- ream_artifacts-0.1.0/src/ream/reach.py +75 -0
- ream_artifacts-0.1.0/src/ream/receipts.py +332 -0
- ream_artifacts-0.1.0/src/ream/rename.py +343 -0
- ream_artifacts-0.1.0/src/ream/repack.py +224 -0
- ream_artifacts-0.1.0/src/ream/republish.py +250 -0
- ream_artifacts-0.1.0/src/ream/review.py +213 -0
- ream_artifacts-0.1.0/src/ream/scan.py +924 -0
- ream_artifacts-0.1.0/src/ream/scanners.py +183 -0
- ream_artifacts-0.1.0/src/ream/schema.sql +1329 -0
- ream_artifacts-0.1.0/src/ream/schema_sqlite.sql +287 -0
- ream_artifacts-0.1.0/src/ream/secret_shapes.txt +129 -0
- ream_artifacts-0.1.0/src/ream/sections.py +679 -0
- ream_artifacts-0.1.0/src/ream/selfupdate.py +721 -0
- ream_artifacts-0.1.0/src/ream/serve.py +196 -0
- ream_artifacts-0.1.0/src/ream/settings.py +719 -0
- ream_artifacts-0.1.0/src/ream/sinks.py +716 -0
- ream_artifacts-0.1.0/src/ream/stamp.py +92 -0
- ream_artifacts-0.1.0/src/ream/store.py +3000 -0
- ream_artifacts-0.1.0/src/ream/templates/_shared.css +136 -0
- ream_artifacts-0.1.0/src/ream/templates/comparison.html +299 -0
- ream_artifacts-0.1.0/src/ream/templates/decision-request.html +309 -0
- ream_artifacts-0.1.0/src/ream/templates/design.html +329 -0
- ream_artifacts-0.1.0/src/ream/templates/explainer.html +323 -0
- ream_artifacts-0.1.0/src/ream/templates/implementation-plan.html +288 -0
- ream_artifacts-0.1.0/src/ream/templates/incident.html +321 -0
- ream_artifacts-0.1.0/src/ream/templates/intent.html +354 -0
- ream_artifacts-0.1.0/src/ream/templates/project-status.html +328 -0
- ream_artifacts-0.1.0/src/ream/templates.py +127 -0
- ream_artifacts-0.1.0/src/ream/text.py +335 -0
- ream_artifacts-0.1.0/src/ream/textvars.py +281 -0
- ream_artifacts-0.1.0/src/ream/thumbs.py +496 -0
- ream_artifacts-0.1.0/src/ream/traps.py +229 -0
- ream_artifacts-0.1.0/src/ream/webhook.py +513 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: ream-artifacts
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A self-hosted artifacts publisher: agents publish HTML pages over a keyed API; Jeremy views them in an Authelia-gated gallery.
|
|
5
|
+
Author: Jeremy Brown
|
|
6
|
+
Author-email: Jeremy Brown <jeremy@tenfourty.com>
|
|
7
|
+
Requires-Dist: cryptography>=46 ; extra == 'server'
|
|
8
|
+
Requires-Dist: fastapi>=0.141.1 ; extra == 'server'
|
|
9
|
+
Requires-Dist: html5lib>=1.1 ; extra == 'server'
|
|
10
|
+
Requires-Dist: markdown-it-py>=4 ; extra == 'server'
|
|
11
|
+
Requires-Dist: markupsafe>=3.0.3 ; extra == 'server'
|
|
12
|
+
Requires-Dist: mdit-py-plugins>=0.5 ; extra == 'server'
|
|
13
|
+
Requires-Dist: psycopg[binary]>=3.3.6 ; extra == 'server'
|
|
14
|
+
Requires-Dist: python-multipart>=0.0.32 ; extra == 'server'
|
|
15
|
+
Requires-Dist: uvicorn>=0.53.0 ; extra == 'server'
|
|
16
|
+
Requires-Dist: zstandard>=0.25 ; extra == 'server'
|
|
17
|
+
Requires-Python: >=3.12
|
|
18
|
+
Provides-Extra: server
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# Ream (formerly artifacts)
|
|
22
|
+
|
|
23
|
+
A self-hosted page publisher: agents publish an HTML
|
|
24
|
+
page over a keyed API and get back a stable URL. A person views published pages
|
|
25
|
+
in a gallery behind a sign-in; each page is served from its own
|
|
26
|
+
`<id>.<content domain>` hostname behind a capability token in the URL.
|
|
27
|
+
|
|
28
|
+
## Development
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
uv run pytest # run tests
|
|
32
|
+
uv run ruff check src tests # lint
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Requires Python 3.12 exactly (pinned in `.python-version`) — a later task's
|
|
36
|
+
test dependency (`pgserver`) has no wheels for 3.13+.
|
|
37
|
+
|
|
38
|
+
## Run locally
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
export DATABASE_URL=postgresql://... # a real Postgres; migrate() runs on startup
|
|
42
|
+
uv run uvicorn ream.serve:app_from_env --factory --reload
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The router is fail-closed: `/healthz` only answers 200 with the gallery's own
|
|
46
|
+
`Host` (the host of `REAM_GALLERY_ORIGIN`; `localhost:8000` on a fresh install)
|
|
47
|
+
and 404 on anything else, so curl it with that Host header, not the
|
|
48
|
+
bare address.
|
|
49
|
+
|
|
50
|
+
## Mint a key for an agent
|
|
51
|
+
|
|
52
|
+
A token is generated where it will be used and never crosses a terminal, a
|
|
53
|
+
transcript, or a network hop — only its SHA-256 is ever registered:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
# On the machine (or pod) that will hold the key:
|
|
57
|
+
artifacts-admin new-key --agent my-agent --out <your key file>
|
|
58
|
+
|
|
59
|
+
# Then, with the sha256 that command just printed, on the running pod:
|
|
60
|
+
kubectl -n artifacts exec deploy/artifacts -- artifacts-admin register-key \
|
|
61
|
+
--agent my-agent --sha256 <the printed sha256>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`artifacts-admin revoke-key <key_id>` and `artifacts-admin list-keys` manage
|
|
65
|
+
keys from there; neither ever prints a token.
|
|
66
|
+
|
|
67
|
+
## The skill
|
|
68
|
+
|
|
69
|
+
One skill, `GET /api/v1/skill.md` (open, no key), in the open SKILL.md format: save it wherever
|
|
70
|
+
your agent keeps skills. It is named `ream` and fires whenever the agent shows or shares something
|
|
71
|
+
beyond a short chat answer (a report, a plan, a chart, a diagram, a comparison), not only on the
|
|
72
|
+
words "page" or "publish". With a key set up it publishes to Ream instead of a host's built-in
|
|
73
|
+
artifacts or canvas panel; with none it falls back to that panel and says why. A page carries a
|
|
74
|
+
question only when the agent is blocked on the person or needs their choice. It names no house, so
|
|
75
|
+
it works for any agent on any host; a deployment's own skill adds only its look and keys.
|
|
76
|
+
|
|
77
|
+
## Publish a page
|
|
78
|
+
|
|
79
|
+
The command is `ream`, with a verb for each job (`ream publish`, `ream ask`,
|
|
80
|
+
`ream wait`; `ream --help` lists them). The older `artifacts-publish`,
|
|
81
|
+
`artifacts-decide` and the other `artifacts-*` commands keep working for good,
|
|
82
|
+
as do the `ARTIFACTS_*` settings, and `/api/v1/artifacts` answers exactly as
|
|
83
|
+
`/api/v1/pages` does (the same handlers, so a POST to either works).
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
ream publish page.html --title "My page"
|
|
87
|
+
ream publish page.html --update <id> # after editing page.html
|
|
88
|
+
ream publish --list
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Where the key comes from, in the order to prefer:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
# A vault hands it over on a descriptor; no cleartext copy exists. Where keys are
|
|
95
|
+
# vaulted, a wrapper does it (here `artifacts-key`, with your seat's name):
|
|
96
|
+
ARTIFACTS_SEAT=<seat> artifacts-key -- ream publish page.html --title "My page"
|
|
97
|
+
|
|
98
|
+
# Or any store of your own that can open one:
|
|
99
|
+
ream publish --key-fd 3 page.html --title "My page" 3< <(pass show artifacts/key)
|
|
100
|
+
|
|
101
|
+
# Or a file, mode 600 -- it refuses anything looser:
|
|
102
|
+
REAM_KEY_FILE=~/.config/artifacts/key ream publish --list
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Tidy one up
|
|
106
|
+
|
|
107
|
+
Everything Jeremy can do to a page in the gallery, an agent can do to its own
|
|
108
|
+
pages — his rule, 20 Sep 2026: *"ideally agents can take all the same actions a
|
|
109
|
+
human can."* None of these send the body, so none of them needs the file:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
ream publish --rename <id> --title "Its real name"
|
|
113
|
+
ream publish --archive <id> # off the wall; nothing is deleted
|
|
114
|
+
ream publish --unarchive <id>
|
|
115
|
+
ream publish --pin <id> # leads the gallery
|
|
116
|
+
ream publish --unpin <id>
|
|
117
|
+
ream publish --duplicate <id> # a copy at a new address
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Over HTTP these are `POST /api/v1/pages/{id}/{archive,rename,pin}`, each
|
|
121
|
+
scoped to the calling key's own agent: another seat's page answers 404, and so
|
|
122
|
+
does a page that does not exist — the same 404, so a key cannot be used to
|
|
123
|
+
learn which ids are real.
|
|
124
|
+
|
|
125
|
+
`$REAM_API` (default `https://artifacts.tenfourty.site/api/v1`) and
|
|
126
|
+
`$REAM_KEY_FILE` (default `~/.config/artifacts/key`) point it elsewhere;
|
|
127
|
+
`--key-file` overrides both for one call.
|
|
128
|
+
|
|
129
|
+
## How deploys happen
|
|
130
|
+
|
|
131
|
+
Every push to `main` runs `.forgejo/workflows/ci.yml`: tests, then an image
|
|
132
|
+
build pushed to the forge's registry as `<repo>:<sha12>`, then a pull request on
|
|
133
|
+
the deployment's own repo that moves its `core.lock` to that image
|
|
134
|
+
(`.forgejo/scripts/core-lock-pr.sh`). That repo builds the deployed image and opens
|
|
135
|
+
the pin PR; production moves when that PR is merged. The runbook is
|
|
136
|
+
`docs/agent/runbooks/image-from-ream-homelab.md`. This repo keeps no deployment
|
|
137
|
+
values: they are in the deployment's own repo.
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Ream (formerly artifacts)
|
|
2
|
+
|
|
3
|
+
A self-hosted page publisher: agents publish an HTML
|
|
4
|
+
page over a keyed API and get back a stable URL. A person views published pages
|
|
5
|
+
in a gallery behind a sign-in; each page is served from its own
|
|
6
|
+
`<id>.<content domain>` hostname behind a capability token in the URL.
|
|
7
|
+
|
|
8
|
+
## Development
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
uv run pytest # run tests
|
|
12
|
+
uv run ruff check src tests # lint
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Requires Python 3.12 exactly (pinned in `.python-version`) — a later task's
|
|
16
|
+
test dependency (`pgserver`) has no wheels for 3.13+.
|
|
17
|
+
|
|
18
|
+
## Run locally
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
export DATABASE_URL=postgresql://... # a real Postgres; migrate() runs on startup
|
|
22
|
+
uv run uvicorn ream.serve:app_from_env --factory --reload
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The router is fail-closed: `/healthz` only answers 200 with the gallery's own
|
|
26
|
+
`Host` (the host of `REAM_GALLERY_ORIGIN`; `localhost:8000` on a fresh install)
|
|
27
|
+
and 404 on anything else, so curl it with that Host header, not the
|
|
28
|
+
bare address.
|
|
29
|
+
|
|
30
|
+
## Mint a key for an agent
|
|
31
|
+
|
|
32
|
+
A token is generated where it will be used and never crosses a terminal, a
|
|
33
|
+
transcript, or a network hop — only its SHA-256 is ever registered:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# On the machine (or pod) that will hold the key:
|
|
37
|
+
artifacts-admin new-key --agent my-agent --out <your key file>
|
|
38
|
+
|
|
39
|
+
# Then, with the sha256 that command just printed, on the running pod:
|
|
40
|
+
kubectl -n artifacts exec deploy/artifacts -- artifacts-admin register-key \
|
|
41
|
+
--agent my-agent --sha256 <the printed sha256>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`artifacts-admin revoke-key <key_id>` and `artifacts-admin list-keys` manage
|
|
45
|
+
keys from there; neither ever prints a token.
|
|
46
|
+
|
|
47
|
+
## The skill
|
|
48
|
+
|
|
49
|
+
One skill, `GET /api/v1/skill.md` (open, no key), in the open SKILL.md format: save it wherever
|
|
50
|
+
your agent keeps skills. It is named `ream` and fires whenever the agent shows or shares something
|
|
51
|
+
beyond a short chat answer (a report, a plan, a chart, a diagram, a comparison), not only on the
|
|
52
|
+
words "page" or "publish". With a key set up it publishes to Ream instead of a host's built-in
|
|
53
|
+
artifacts or canvas panel; with none it falls back to that panel and says why. A page carries a
|
|
54
|
+
question only when the agent is blocked on the person or needs their choice. It names no house, so
|
|
55
|
+
it works for any agent on any host; a deployment's own skill adds only its look and keys.
|
|
56
|
+
|
|
57
|
+
## Publish a page
|
|
58
|
+
|
|
59
|
+
The command is `ream`, with a verb for each job (`ream publish`, `ream ask`,
|
|
60
|
+
`ream wait`; `ream --help` lists them). The older `artifacts-publish`,
|
|
61
|
+
`artifacts-decide` and the other `artifacts-*` commands keep working for good,
|
|
62
|
+
as do the `ARTIFACTS_*` settings, and `/api/v1/artifacts` answers exactly as
|
|
63
|
+
`/api/v1/pages` does (the same handlers, so a POST to either works).
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
ream publish page.html --title "My page"
|
|
67
|
+
ream publish page.html --update <id> # after editing page.html
|
|
68
|
+
ream publish --list
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Where the key comes from, in the order to prefer:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
# A vault hands it over on a descriptor; no cleartext copy exists. Where keys are
|
|
75
|
+
# vaulted, a wrapper does it (here `artifacts-key`, with your seat's name):
|
|
76
|
+
ARTIFACTS_SEAT=<seat> artifacts-key -- ream publish page.html --title "My page"
|
|
77
|
+
|
|
78
|
+
# Or any store of your own that can open one:
|
|
79
|
+
ream publish --key-fd 3 page.html --title "My page" 3< <(pass show artifacts/key)
|
|
80
|
+
|
|
81
|
+
# Or a file, mode 600 -- it refuses anything looser:
|
|
82
|
+
REAM_KEY_FILE=~/.config/artifacts/key ream publish --list
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Tidy one up
|
|
86
|
+
|
|
87
|
+
Everything Jeremy can do to a page in the gallery, an agent can do to its own
|
|
88
|
+
pages — his rule, 20 Sep 2026: *"ideally agents can take all the same actions a
|
|
89
|
+
human can."* None of these send the body, so none of them needs the file:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
ream publish --rename <id> --title "Its real name"
|
|
93
|
+
ream publish --archive <id> # off the wall; nothing is deleted
|
|
94
|
+
ream publish --unarchive <id>
|
|
95
|
+
ream publish --pin <id> # leads the gallery
|
|
96
|
+
ream publish --unpin <id>
|
|
97
|
+
ream publish --duplicate <id> # a copy at a new address
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Over HTTP these are `POST /api/v1/pages/{id}/{archive,rename,pin}`, each
|
|
101
|
+
scoped to the calling key's own agent: another seat's page answers 404, and so
|
|
102
|
+
does a page that does not exist — the same 404, so a key cannot be used to
|
|
103
|
+
learn which ids are real.
|
|
104
|
+
|
|
105
|
+
`$REAM_API` (default `https://artifacts.tenfourty.site/api/v1`) and
|
|
106
|
+
`$REAM_KEY_FILE` (default `~/.config/artifacts/key`) point it elsewhere;
|
|
107
|
+
`--key-file` overrides both for one call.
|
|
108
|
+
|
|
109
|
+
## How deploys happen
|
|
110
|
+
|
|
111
|
+
Every push to `main` runs `.forgejo/workflows/ci.yml`: tests, then an image
|
|
112
|
+
build pushed to the forge's registry as `<repo>:<sha12>`, then a pull request on
|
|
113
|
+
the deployment's own repo that moves its `core.lock` to that image
|
|
114
|
+
(`.forgejo/scripts/core-lock-pr.sh`). That repo builds the deployed image and opens
|
|
115
|
+
the pin PR; production moves when that PR is merged. The runbook is
|
|
116
|
+
`docs/agent/runbooks/image-from-ream-homelab.md`. This repo keeps no deployment
|
|
117
|
+
values: they are in the deployment's own repo.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "ream-artifacts"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "A self-hosted artifacts publisher: agents publish HTML pages over a keyed API; Jeremy views them in an Authelia-gated gallery."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
dependencies = []
|
|
8
|
+
|
|
9
|
+
[[project.authors]]
|
|
10
|
+
name = "Jeremy Brown"
|
|
11
|
+
email = "jeremy@tenfourty.com"
|
|
12
|
+
|
|
13
|
+
[project.optional-dependencies]
|
|
14
|
+
server = [
|
|
15
|
+
"cryptography>=46",
|
|
16
|
+
"fastapi>=0.141.1",
|
|
17
|
+
"html5lib>=1.1",
|
|
18
|
+
"markdown-it-py>=4",
|
|
19
|
+
"markupsafe>=3.0.3",
|
|
20
|
+
"mdit-py-plugins>=0.5",
|
|
21
|
+
"psycopg[binary]>=3.3.6",
|
|
22
|
+
"python-multipart>=0.0.32",
|
|
23
|
+
"uvicorn>=0.53.0",
|
|
24
|
+
"zstandard>=0.25",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.scripts]
|
|
28
|
+
ream = "ream.cli:main"
|
|
29
|
+
artifacts-admin = "ream.admin:main"
|
|
30
|
+
artifacts-publish = "ream.client:run"
|
|
31
|
+
artifacts-decide = "ream.decide:run"
|
|
32
|
+
artifacts-republish = "ream.republish:run"
|
|
33
|
+
artifacts-connect = "ream.connect_cli:run"
|
|
34
|
+
|
|
35
|
+
[tool.uv.build-backend]
|
|
36
|
+
module-name = [
|
|
37
|
+
"ream",
|
|
38
|
+
"artifacts",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
[tool.pytest.ini_options]
|
|
42
|
+
addopts = "--dist=worksteal -rs"
|
|
43
|
+
timeout = 300
|
|
44
|
+
timeout_method = "signal"
|
|
45
|
+
markers = [
|
|
46
|
+
"slow(reason): genuinely needs more than a second; says why",
|
|
47
|
+
"postgres_only: reads Postgres's catalogue or runs Postgres's own SQL; skipped when REAM_TEST_DB=sqlite",
|
|
48
|
+
"plugin_loader: drives the plug-in loader with entry points it fakes; REAM_PLUGINS is unset for it (the core-bare CI job sets it to none for everything else)",
|
|
49
|
+
"no_default_comments: leave a publish exactly as the test sends it (the suite otherwise adds comments on to a new page's publish, which the service now requires)",
|
|
50
|
+
"background_thumbnails: publishes in this test start the real background thumbnail render (off by default in tests; see tests/conftest.py)",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
[tool.ruff.lint]
|
|
54
|
+
extend-select = [
|
|
55
|
+
"S",
|
|
56
|
+
"B",
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
[tool.ruff.lint.per-file-ignores]
|
|
60
|
+
"tests/**" = [
|
|
61
|
+
"S101",
|
|
62
|
+
"S311",
|
|
63
|
+
"F811",
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
[build-system]
|
|
67
|
+
requires = ["uv_build>=0.12.9,<0.13.0"]
|
|
68
|
+
build-backend = "uv_build"
|
|
69
|
+
|
|
70
|
+
[dependency-groups]
|
|
71
|
+
dev = [
|
|
72
|
+
"httpx>=0.28.1",
|
|
73
|
+
"pgserver>=0.1.4",
|
|
74
|
+
"playwright>=1.63.0",
|
|
75
|
+
"pytest>=9.1.1",
|
|
76
|
+
"pytest-asyncio>=1.4.0",
|
|
77
|
+
"pytest-timeout>=2.4.0",
|
|
78
|
+
"pytest-xdist>=3.8.0",
|
|
79
|
+
"ruff>=0.16.8",
|
|
80
|
+
]
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
# The install name, which `uv tool install` takes, is `ream-artifacts` (docs/decisions.md, 10 Nov
|
|
3
|
+
# 2026): `ream` on public PyPI is somebody else's package. Nothing else is renamed: the `ream`
|
|
4
|
+
# command, the `artifacts-*` commands, the import packages `ream` and `artifacts`.
|
|
5
|
+
name = "ream-artifacts"
|
|
6
|
+
version = "0.1.0"
|
|
7
|
+
description = "A self-hosted artifacts publisher: agents publish HTML pages over a keyed API; Jeremy views them in an Authelia-gated gallery."
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
authors = [
|
|
10
|
+
{ name = "Jeremy Brown", email = "jeremy@tenfourty.com" }
|
|
11
|
+
]
|
|
12
|
+
requires-python = ">=3.12"
|
|
13
|
+
# NOTHING by default, on purpose (homelab-ij8.68, 23 Sep 2026). The agent tools
|
|
14
|
+
# -- artifacts-publish, -decide, -republish -- use only the standard library, and
|
|
15
|
+
# the service hands this package to agents on other machines from an open
|
|
16
|
+
# address. An agent that wants to publish a page must not have to install a web
|
|
17
|
+
# framework and a database driver to do it. The service itself installs the
|
|
18
|
+
# `server` extra (Dockerfile, CI); for local work: `uv sync --all-extras`.
|
|
19
|
+
dependencies = []
|
|
20
|
+
|
|
21
|
+
[project.optional-dependencies]
|
|
22
|
+
server = [
|
|
23
|
+
"cryptography>=46",
|
|
24
|
+
"fastapi>=0.141.1",
|
|
25
|
+
"html5lib>=1.1",
|
|
26
|
+
"markdown-it-py>=4",
|
|
27
|
+
"markupsafe>=3.0.3",
|
|
28
|
+
"mdit-py-plugins>=0.5",
|
|
29
|
+
"psycopg[binary]>=3.3.6",
|
|
30
|
+
"python-multipart>=0.0.32",
|
|
31
|
+
"uvicorn>=0.53.0",
|
|
32
|
+
"zstandard>=0.25",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.scripts]
|
|
36
|
+
ream = "ream.cli:main"
|
|
37
|
+
# The old command names work for good, beside the verbs of `ream`.
|
|
38
|
+
artifacts-admin = "ream.admin:main"
|
|
39
|
+
artifacts-publish = "ream.client:run"
|
|
40
|
+
artifacts-decide = "ream.decide:run"
|
|
41
|
+
artifacts-republish = "ream.republish:run"
|
|
42
|
+
artifacts-connect = "ream.connect_cli:run"
|
|
43
|
+
|
|
44
|
+
# Two top-level packages ship in the one wheel: `ream`, and `artifacts`, the old import
|
|
45
|
+
# name kept for good (a shim whose modules ARE the ream modules).
|
|
46
|
+
[tool.uv.build-backend]
|
|
47
|
+
module-name = ["ream", "artifacts"]
|
|
48
|
+
|
|
49
|
+
[build-system]
|
|
50
|
+
requires = ["uv_build>=0.12.9,<0.13.0"]
|
|
51
|
+
build-backend = "uv_build"
|
|
52
|
+
|
|
53
|
+
[dependency-groups]
|
|
54
|
+
dev = [
|
|
55
|
+
"httpx>=0.28.1",
|
|
56
|
+
"pgserver>=0.1.4",
|
|
57
|
+
"playwright>=1.63.0",
|
|
58
|
+
"pytest>=9.1.1",
|
|
59
|
+
"pytest-asyncio>=1.4.0",
|
|
60
|
+
"pytest-timeout>=2.4.0",
|
|
61
|
+
"pytest-xdist>=3.8.0",
|
|
62
|
+
"ruff>=0.16.8",
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
[tool.pytest.ini_options]
|
|
66
|
+
# worksteal, not xdist's default `load`: idle workers take tests from the
|
|
67
|
+
# busiest one's tail, so the run ends when the work does, not when the unluckiest
|
|
68
|
+
# worker's last chunk does. Inert without -n. See pytest_collection_modifyitems
|
|
69
|
+
# in tests/conftest.py for where the slow tests go.
|
|
70
|
+
# -rs: the summary names every skipped test and why. Browser tests skip where
|
|
71
|
+
# there is no headless browser (a laptop without one; CI installs chromium and sets
|
|
72
|
+
# REAM_REQUIRE_BROWSER=1, so there a missing browser fails), and a bare "13 skipped"
|
|
73
|
+
# hid that for weeks (runcove-5t3oj.77).
|
|
74
|
+
addopts = "--dist=worksteal -rs"
|
|
75
|
+
# A test that hangs fails after 5 minutes with every thread's stack printed,
|
|
76
|
+
# naming itself, instead of holding a CI builder until the job's own limit
|
|
77
|
+
# (6 Oct 2026: a test job sat idle for 35 minutes and left no clue). The
|
|
78
|
+
# slowest test takes about 9 s; this only ever catches a hang.
|
|
79
|
+
timeout = 300
|
|
80
|
+
# "signal", not "thread": the thread method dumps stacks and kills the worker,
|
|
81
|
+
# and xdist drops a dead worker's stderr -- on 7 Oct 2026 a CI hang left only
|
|
82
|
+
# "node down: Not properly terminated". The signal method fails the hung test
|
|
83
|
+
# in place with its traceback, and the worker carries on.
|
|
84
|
+
timeout_method = "signal"
|
|
85
|
+
# `slow`: a test that genuinely needs more than a second, each with a one-line
|
|
86
|
+
# reason at the mark. At most ten may carry it; everything else stays under 1 s.
|
|
87
|
+
markers = [
|
|
88
|
+
"slow(reason): genuinely needs more than a second; says why",
|
|
89
|
+
"postgres_only: reads Postgres's catalogue or runs Postgres's own SQL; skipped when REAM_TEST_DB=sqlite",
|
|
90
|
+
"plugin_loader: drives the plug-in loader with entry points it fakes; REAM_PLUGINS is unset for it (the core-bare CI job sets it to none for everything else)",
|
|
91
|
+
"no_default_comments: leave a publish exactly as the test sends it (the suite otherwise adds comments on to a new page's publish, which the service now requires)",
|
|
92
|
+
"background_thumbnails: publishes in this test start the real background thumbnail render (off by default in tests; see tests/conftest.py)",
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
[tool.ruff.lint]
|
|
96
|
+
# S (bandit) and B (bugbear) are worth enabling deliberately on a service whose
|
|
97
|
+
# whole job is handling credentials and publishing agent-authored HTML -- on
|
|
98
|
+
# top of ruff's E4/E7/E9/F + isort defaults, not replacing them.
|
|
99
|
+
extend-select = ["S", "B"]
|
|
100
|
+
|
|
101
|
+
[tool.ruff.lint.per-file-ignores]
|
|
102
|
+
# S101 flags `assert` -- pytest's idiom for every assertion, not a risk here.
|
|
103
|
+
# S311 flags `random.Random` as unfit for cryptographic use -- true, and
|
|
104
|
+
# irrelevant to a seeded fuzz corpus for a property test, which wants
|
|
105
|
+
# reproducible pseudo-randomness, never secrecy.
|
|
106
|
+
# F811 flags a pytest fixture imported from test_client and then taken as a
|
|
107
|
+
# parameter -- how pytest works, not a mistake. Per-line noqa stopped covering
|
|
108
|
+
# it once the formatter wrapped long signatures (homelab-ij8.25).
|
|
109
|
+
"tests/**" = ["S101", "S311", "F811"]
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""The old import name, kept for good: `import artifacts` is `import ream`.
|
|
2
|
+
|
|
3
|
+
The package was renamed to `ream` (docs/decisions.md, "Ream inside the code too"). Callers
|
|
4
|
+
outside this repository still write `import artifacts`, `from artifacts.settings import ...`,
|
|
5
|
+
`artifacts.plugin_api`, or name `artifacts.client:run` as an entry point, and all of it keeps
|
|
6
|
+
working. Nothing here is a copy:
|
|
7
|
+
|
|
8
|
+
* `artifacts` itself becomes the `ream` module object (so `resources.files("artifacts")` finds
|
|
9
|
+
the schema and the templates in `ream`);
|
|
10
|
+
* `artifacts.X` is the SAME module object as `ream.X`, found by the finder below, so
|
|
11
|
+
`settings.override(...)` or a monkeypatch through either name acts on one module, never on
|
|
12
|
+
two copies that each hold their own state.
|
|
13
|
+
|
|
14
|
+
This file reads no environment and holds no logic of the service.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import importlib
|
|
18
|
+
import importlib.abc
|
|
19
|
+
import importlib.util
|
|
20
|
+
import sys
|
|
21
|
+
from importlib.machinery import ModuleSpec
|
|
22
|
+
|
|
23
|
+
import ream
|
|
24
|
+
|
|
25
|
+
_OLD = "artifacts"
|
|
26
|
+
_NEW = "ream"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class _AliasFinder(importlib.abc.MetaPathFinder, importlib.abc.Loader):
|
|
30
|
+
"""Answers `artifacts.<x>` with the already-imported `ream.<x>`."""
|
|
31
|
+
|
|
32
|
+
def __init__(self) -> None:
|
|
33
|
+
self._real_specs: dict[str, ModuleSpec] = {}
|
|
34
|
+
|
|
35
|
+
def find_spec(self, fullname, path=None, target=None) -> ModuleSpec | None:
|
|
36
|
+
if not fullname.startswith(_OLD + "."):
|
|
37
|
+
return None
|
|
38
|
+
real = _NEW + fullname[len(_OLD) :]
|
|
39
|
+
try:
|
|
40
|
+
found = importlib.util.find_spec(real)
|
|
41
|
+
except ModuleNotFoundError:
|
|
42
|
+
return None
|
|
43
|
+
if found is None:
|
|
44
|
+
return None
|
|
45
|
+
self._real_spec(fullname)
|
|
46
|
+
return importlib.util.spec_from_loader(fullname, self)
|
|
47
|
+
|
|
48
|
+
def create_module(self, spec: ModuleSpec):
|
|
49
|
+
return importlib.import_module(_NEW + spec.name[len(_OLD) :])
|
|
50
|
+
|
|
51
|
+
def _real_spec(self, fullname: str) -> ModuleSpec | None:
|
|
52
|
+
"""The spec `ream.<x>` was really imported with. Once `artifacts.<x>` has been
|
|
53
|
+
imported the import system overwrites `module.__spec__` with the alias spec, so
|
|
54
|
+
the first real one seen is kept."""
|
|
55
|
+
real = _NEW + fullname[len(_OLD) :]
|
|
56
|
+
if real not in self._real_specs:
|
|
57
|
+
found = importlib.util.find_spec(real)
|
|
58
|
+
if found is None or found.name != real:
|
|
59
|
+
return found
|
|
60
|
+
self._real_specs[real] = found
|
|
61
|
+
return self._real_specs[real]
|
|
62
|
+
|
|
63
|
+
def exec_module(self, module) -> None:
|
|
64
|
+
"""Nothing to run: the module was executed under its real name. The import
|
|
65
|
+
system has just set `module.__spec__` to the alias spec; put the real one back."""
|
|
66
|
+
real = self._real_spec(_OLD + module.__name__[len(_NEW) :])
|
|
67
|
+
if real is not None:
|
|
68
|
+
module.__spec__ = real
|
|
69
|
+
|
|
70
|
+
def _real_loader(self, fullname: str):
|
|
71
|
+
real = self._real_spec(fullname)
|
|
72
|
+
if real is None or real.loader is None:
|
|
73
|
+
raise ImportError(f"no loader for {fullname}", name=fullname)
|
|
74
|
+
return real.loader
|
|
75
|
+
|
|
76
|
+
# `python -m artifacts.<x>` (runpy) asks the alias spec's loader for the code.
|
|
77
|
+
def get_code(self, fullname: str):
|
|
78
|
+
return self._real_loader(fullname).get_code(_NEW + fullname[len(_OLD) :])
|
|
79
|
+
|
|
80
|
+
def get_source(self, fullname: str):
|
|
81
|
+
return self._real_loader(fullname).get_source(_NEW + fullname[len(_OLD) :])
|
|
82
|
+
|
|
83
|
+
def is_package(self, fullname: str) -> bool:
|
|
84
|
+
return self._real_loader(fullname).is_package(_NEW + fullname[len(_OLD) :])
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
if not any(isinstance(f, _AliasFinder) for f in sys.meta_path):
|
|
88
|
+
sys.meta_path.insert(0, _AliasFinder())
|
|
89
|
+
|
|
90
|
+
# `import artifacts` hands back the ream package itself.
|
|
91
|
+
sys.modules[__name__] = ream
|
|
File without changes
|
|
File without changes
|