pixie-lab-dibs 0.2.0__tar.gz → 0.2.1__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.
- {pixie_lab_dibs-0.2.0 → pixie_lab_dibs-0.2.1}/CHANGELOG.md +8 -0
- {pixie_lab_dibs-0.2.0 → pixie_lab_dibs-0.2.1}/PKG-INFO +8 -1
- {pixie_lab_dibs-0.2.0 → pixie_lab_dibs-0.2.1}/README.md +7 -0
- {pixie_lab_dibs-0.2.0 → pixie_lab_dibs-0.2.1}/src/dibs/__init__.py +1 -1
- pixie_lab_dibs-0.2.1/src/dibs/_skill.py +26 -0
- {pixie_lab_dibs-0.2.0 → pixie_lab_dibs-0.2.1}/src/dibs/app.py +37 -1
- {pixie_lab_dibs-0.2.0 → pixie_lab_dibs-0.2.1}/src/dibs/cli.py +3 -3
- {pixie_lab_dibs-0.2.0 → pixie_lab_dibs-0.2.1}/tests/test_app.py +15 -0
- {pixie_lab_dibs-0.2.0 → pixie_lab_dibs-0.2.1}/tests/test_cli.py +8 -0
- {pixie_lab_dibs-0.2.0 → pixie_lab_dibs-0.2.1}/.github/workflows/format.yml +0 -0
- {pixie_lab_dibs-0.2.0 → pixie_lab_dibs-0.2.1}/.github/workflows/publish.yml +0 -0
- {pixie_lab_dibs-0.2.0 → pixie_lab_dibs-0.2.1}/.github/workflows/test.yml +0 -0
- {pixie_lab_dibs-0.2.0 → pixie_lab_dibs-0.2.1}/.gitignore +0 -0
- {pixie_lab_dibs-0.2.0 → pixie_lab_dibs-0.2.1}/.pre-commit-config.yaml +0 -0
- {pixie_lab_dibs-0.2.0 → pixie_lab_dibs-0.2.1}/LICENSE +0 -0
- {pixie_lab_dibs-0.2.0 → pixie_lab_dibs-0.2.1}/LICENSES/BSD-3-Clause.txt +0 -0
- {pixie_lab_dibs-0.2.0 → pixie_lab_dibs-0.2.1}/Makefile +0 -0
- {pixie_lab_dibs-0.2.0 → pixie_lab_dibs-0.2.1}/REUSE.toml +0 -0
- {pixie_lab_dibs-0.2.0 → pixie_lab_dibs-0.2.1}/pyproject.toml +0 -0
- {pixie_lab_dibs-0.2.0 → pixie_lab_dibs-0.2.1}/scripts/service-smoke.sh +0 -0
- {pixie_lab_dibs-0.2.0 → pixie_lab_dibs-0.2.1}/src/dibs/_sources.py +0 -0
- {pixie_lab_dibs-0.2.0 → pixie_lab_dibs-0.2.1}/src/dibs/_sshsig.py +0 -0
- {pixie_lab_dibs-0.2.0 → pixie_lab_dibs-0.2.1}/src/dibs/_store.py +0 -0
- {pixie_lab_dibs-0.2.0 → pixie_lab_dibs-0.2.1}/src/dibs/_users.py +0 -0
- {pixie_lab_dibs-0.2.0 → pixie_lab_dibs-0.2.1}/src/dibs/skill/SKILL.md +0 -0
- {pixie_lab_dibs-0.2.0 → pixie_lab_dibs-0.2.1}/tests/__init__.py +0 -0
- {pixie_lab_dibs-0.2.0 → pixie_lab_dibs-0.2.1}/tests/conftest.py +0 -0
- {pixie_lab_dibs-0.2.0 → pixie_lab_dibs-0.2.1}/tests/test_sources.py +0 -0
- {pixie_lab_dibs-0.2.0 → pixie_lab_dibs-0.2.1}/tests/test_store.py +0 -0
- {pixie_lab_dibs-0.2.0 → pixie_lab_dibs-0.2.1}/tests/test_users.py +0 -0
- {pixie_lab_dibs-0.2.0 → pixie_lab_dibs-0.2.1}/uv.lock +0 -0
|
@@ -10,6 +10,14 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
10
10
|
|
|
11
11
|
## [Unreleased]
|
|
12
12
|
|
|
13
|
+
## [0.2.1] - 2026-10-27
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- `GET /agents` explains the server to whoever is pointed at it: setup
|
|
18
|
+
with this server's URL filled in, what is claimed right now, and the
|
|
19
|
+
agent skill's rules. The web page links to it.
|
|
20
|
+
|
|
13
21
|
## [0.2.0] - 2026-10-27
|
|
14
22
|
|
|
15
23
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pixie-lab-dibs
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Call dibs on shared lab machines: see who is using what, claim it, release it
|
|
5
5
|
Project-URL: Homepage, https://github.com/safl/pixie-lab-dibs
|
|
6
6
|
Author-email: "Simon A. F. Lund" <os@safl.dk>
|
|
@@ -167,6 +167,12 @@ the thing (`bmc = "10.20.30.82"`) rather than a part.
|
|
|
167
167
|
|
|
168
168
|
### AI-agents
|
|
169
169
|
|
|
170
|
+
Every dibs server explains itself at `/agents`: how to set up for this
|
|
171
|
+
very server, with its URL filled in, what is claimed right now, and the
|
|
172
|
+
rules below. So the one thing to tell a person or an agent about a lab
|
|
173
|
+
is "before touching its machines, read `http://dibs:8090/agents`", in a
|
|
174
|
+
prompt, a project's `AGENTS.md`, or a message.
|
|
175
|
+
|
|
170
176
|
For Claude Code, install the skill that teaches an agent all of this,
|
|
171
177
|
and it applies whenever the agent is about to use a lab machine:
|
|
172
178
|
|
|
@@ -257,6 +263,7 @@ no login; claim and release take `Authorization: Bearer <token>`.
|
|
|
257
263
|
| `POST /auth/register` | `{"user": "...", "pubkey": "ssh-ed25519 ..."}` |
|
|
258
264
|
| `POST /auth/challenge` | `{"user": "..."}` gives a nonce and the message to sign |
|
|
259
265
|
| `POST /auth/login` | `{"user", "nonce", "signature"}` gives a token |
|
|
266
|
+
| `GET /agents` | how to use this server, for a person or an agent, as text |
|
|
260
267
|
| `GET /login.sh` | the login hook |
|
|
261
268
|
|
|
262
269
|
A claim or release blocked by someone else's claim answers 409 with
|
|
@@ -145,6 +145,12 @@ the thing (`bmc = "10.20.30.82"`) rather than a part.
|
|
|
145
145
|
|
|
146
146
|
### AI-agents
|
|
147
147
|
|
|
148
|
+
Every dibs server explains itself at `/agents`: how to set up for this
|
|
149
|
+
very server, with its URL filled in, what is claimed right now, and the
|
|
150
|
+
rules below. So the one thing to tell a person or an agent about a lab
|
|
151
|
+
is "before touching its machines, read `http://dibs:8090/agents`", in a
|
|
152
|
+
prompt, a project's `AGENTS.md`, or a message.
|
|
153
|
+
|
|
148
154
|
For Claude Code, install the skill that teaches an agent all of this,
|
|
149
155
|
and it applies whenever the agent is about to use a lab machine:
|
|
150
156
|
|
|
@@ -235,6 +241,7 @@ no login; claim and release take `Authorization: Bearer <token>`.
|
|
|
235
241
|
| `POST /auth/register` | `{"user": "...", "pubkey": "ssh-ed25519 ..."}` |
|
|
236
242
|
| `POST /auth/challenge` | `{"user": "..."}` gives a nonce and the message to sign |
|
|
237
243
|
| `POST /auth/login` | `{"user", "nonce", "signature"}` gives a token |
|
|
244
|
+
| `GET /agents` | how to use this server, for a person or an agent, as text |
|
|
238
245
|
| `GET /login.sh` | the login hook |
|
|
239
246
|
|
|
240
247
|
A claim or release blocked by someone else's claim answers 409 with
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Simon A. F. Lund <os@safl.dk>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
4
|
+
|
|
5
|
+
"""The agent skill, shared by ``dibs skill`` and the server's ``/agents``
|
|
6
|
+
page, so both say exactly the same thing."""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
SKILL_PATH = Path(__file__).parent / "skill" / "SKILL.md"
|
|
14
|
+
|
|
15
|
+
# The YAML frontmatter Claude Code reads, then the REUSE comment.
|
|
16
|
+
_PREAMBLE = re.compile(r"\A---\n.*?\n---\n\s*<!--.*?-->\s*", re.DOTALL)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def skill_text() -> str:
|
|
20
|
+
"""The skill as Claude Code installs it."""
|
|
21
|
+
return SKILL_PATH.read_text()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def skill_body() -> str:
|
|
25
|
+
"""The protocol alone, for readers that are not Claude Code."""
|
|
26
|
+
return _PREAMBLE.sub("", skill_text(), count=1)
|
|
@@ -30,6 +30,7 @@ from fastapi.responses import HTMLResponse, PlainTextResponse, Response
|
|
|
30
30
|
from pydantic import BaseModel, Field
|
|
31
31
|
|
|
32
32
|
import dibs
|
|
33
|
+
from dibs._skill import skill_body
|
|
33
34
|
from dibs._sources import Config, PixieSource, load_config, sync
|
|
34
35
|
from dibs._store import Claim, ClaimConflict, Store, Thing, covering, describe, sentence
|
|
35
36
|
from dibs._users import DuplicateUser, LoginFailed, NotApproved, UsersStore
|
|
@@ -297,6 +298,16 @@ def create_app(
|
|
|
297
298
|
login. It points back at whichever dibs URL fetched it."""
|
|
298
299
|
return PlainTextResponse(LOGIN_HOOK.format(base=str(request.base_url)))
|
|
299
300
|
|
|
301
|
+
@app.get("/agents", response_class=PlainTextResponse)
|
|
302
|
+
def agents(request: Request) -> PlainTextResponse:
|
|
303
|
+
"""How to use dibs on this lab, for a person or an agent pointed at
|
|
304
|
+
it: the setup with this server's URL filled in, what is claimed
|
|
305
|
+
right now, then the protocol from the agent skill."""
|
|
306
|
+
base = str(request.base_url).rstrip("/")
|
|
307
|
+
now_lines = "\n".join(f"- {t['sentence']}" for t in list_things()["things"])
|
|
308
|
+
page = AGENTS_PAGE.format(base=base, now=now_lines or "- nothing to claim yet")
|
|
309
|
+
return PlainTextResponse(page + "\n" + skill_body(), media_type="text/plain; charset=utf-8")
|
|
310
|
+
|
|
300
311
|
@app.get("/", response_class=HTMLResponse)
|
|
301
312
|
def index() -> HTMLResponse:
|
|
302
313
|
rows = list_things()["things"]
|
|
@@ -337,6 +348,30 @@ case $- in *i*)
|
|
|
337
348
|
esac
|
|
338
349
|
"""
|
|
339
350
|
|
|
351
|
+
AGENTS_PAGE = """\
|
|
352
|
+
# dibs on this lab
|
|
353
|
+
|
|
354
|
+
The machines on this lab are shared by people, ai-agents and
|
|
355
|
+
automation-runners, and claimed with dibs at {base}. Read this before you
|
|
356
|
+
touch any of them.
|
|
357
|
+
|
|
358
|
+
## Set up, once per machine you work from
|
|
359
|
+
|
|
360
|
+
```
|
|
361
|
+
uv tool install pixie-lab-dibs
|
|
362
|
+
export DIBS_URL={base}
|
|
363
|
+
dibs register <your name> # then ask the lab's admin to approve you
|
|
364
|
+
dibs login
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
An agent uses its human's login and needs none of its own. For Claude
|
|
368
|
+
Code, `dibs skill install` makes the rules below load by themselves.
|
|
369
|
+
|
|
370
|
+
## Right now
|
|
371
|
+
|
|
372
|
+
{now}
|
|
373
|
+
"""
|
|
374
|
+
|
|
340
375
|
PAGE = """<!doctype html>
|
|
341
376
|
<html lang="en"><head><meta charset="utf-8"><title>dibs</title>
|
|
342
377
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
@@ -347,7 +382,8 @@ td, th {{ text-align: left; padding: .4rem .6rem; border-bottom: 1px solid #ddd;
|
|
|
347
382
|
</style></head><body>
|
|
348
383
|
<h1>dibs</h1>
|
|
349
384
|
<p>Who is using which thing. Claim one with
|
|
350
|
-
<code>dibs claim <name> --for <purpose></code
|
|
385
|
+
<code>dibs claim <name> --for <purpose></code>.
|
|
386
|
+
New here, or an agent? Read <a href="/agents">/agents</a>.</p>
|
|
351
387
|
<table><thead><tr><th>Name</th><th>Kind</th><th>Claim</th></tr></thead>
|
|
352
388
|
<tbody>
|
|
353
389
|
{rows}
|
|
@@ -550,13 +550,13 @@ def cmd_admin_revoke(args: argparse.Namespace) -> None:
|
|
|
550
550
|
|
|
551
551
|
# ---- agent skill -----------------------------------------------------------
|
|
552
552
|
|
|
553
|
-
SKILL = Path(__file__).parent / "skill" / "SKILL.md"
|
|
554
|
-
|
|
555
553
|
|
|
556
554
|
def cmd_skill(args: argparse.Namespace) -> None:
|
|
557
555
|
"""Install the Claude Code skill that teaches an agent to claim
|
|
558
556
|
machines before using them, or print it for another agent's setup."""
|
|
559
|
-
|
|
557
|
+
from dibs._skill import skill_text
|
|
558
|
+
|
|
559
|
+
text = skill_text()
|
|
560
560
|
if args.action == "print":
|
|
561
561
|
sys.stdout.write(text)
|
|
562
562
|
return
|
|
@@ -176,3 +176,18 @@ def test_parts_over_the_api(client: TestClient, tokens, tmp_path: Path) -> None:
|
|
|
176
176
|
# The login hook on ryzen-01 (by MAC) sees the part's claim.
|
|
177
177
|
hook = client.get("/claim", params={"mac": "aa:bb:cc:dd:ee:60"}).text
|
|
178
178
|
assert hook.startswith("'bob' has claimed 'ryzen-01.bmc'")
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def test_agents_page_is_set_up_for_this_server(client: TestClient, tokens) -> None:
|
|
182
|
+
from dibs._skill import skill_body
|
|
183
|
+
|
|
184
|
+
_claim(client, tokens, "warp")
|
|
185
|
+
r = client.get("/agents")
|
|
186
|
+
assert r.headers["content-type"].startswith("text/plain")
|
|
187
|
+
page = r.text
|
|
188
|
+
assert "export DIBS_URL=http://testserver\n" in page
|
|
189
|
+
assert "- 'alice' has claimed 'warp' for 'xnvme' expires in: 'never'" in page
|
|
190
|
+
assert page.endswith(skill_body())
|
|
191
|
+
# The skill's Claude Code frontmatter and licence comment are not for this reader.
|
|
192
|
+
assert "name: dibs" not in page and "SPDX" not in page
|
|
193
|
+
assert '<a href="/agents">' in client.get("/").text
|
|
@@ -285,3 +285,11 @@ def test_skill_ships_in_the_wheel(tmp_path: Path) -> None:
|
|
|
285
285
|
)
|
|
286
286
|
wheel = next(tmp_path.glob("*.whl"))
|
|
287
287
|
assert "dibs/skill/SKILL.md" in zipfile.ZipFile(wheel).namelist()
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def test_skill_body_drops_only_the_preamble() -> None:
|
|
291
|
+
from dibs._skill import skill_body, skill_text
|
|
292
|
+
|
|
293
|
+
body = skill_body()
|
|
294
|
+
assert body.startswith("# Claim lab machines with dibs\n")
|
|
295
|
+
assert skill_text().endswith(body)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|