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.
Files changed (31) hide show
  1. {pixie_lab_dibs-0.2.0 → pixie_lab_dibs-0.2.1}/CHANGELOG.md +8 -0
  2. {pixie_lab_dibs-0.2.0 → pixie_lab_dibs-0.2.1}/PKG-INFO +8 -1
  3. {pixie_lab_dibs-0.2.0 → pixie_lab_dibs-0.2.1}/README.md +7 -0
  4. {pixie_lab_dibs-0.2.0 → pixie_lab_dibs-0.2.1}/src/dibs/__init__.py +1 -1
  5. pixie_lab_dibs-0.2.1/src/dibs/_skill.py +26 -0
  6. {pixie_lab_dibs-0.2.0 → pixie_lab_dibs-0.2.1}/src/dibs/app.py +37 -1
  7. {pixie_lab_dibs-0.2.0 → pixie_lab_dibs-0.2.1}/src/dibs/cli.py +3 -3
  8. {pixie_lab_dibs-0.2.0 → pixie_lab_dibs-0.2.1}/tests/test_app.py +15 -0
  9. {pixie_lab_dibs-0.2.0 → pixie_lab_dibs-0.2.1}/tests/test_cli.py +8 -0
  10. {pixie_lab_dibs-0.2.0 → pixie_lab_dibs-0.2.1}/.github/workflows/format.yml +0 -0
  11. {pixie_lab_dibs-0.2.0 → pixie_lab_dibs-0.2.1}/.github/workflows/publish.yml +0 -0
  12. {pixie_lab_dibs-0.2.0 → pixie_lab_dibs-0.2.1}/.github/workflows/test.yml +0 -0
  13. {pixie_lab_dibs-0.2.0 → pixie_lab_dibs-0.2.1}/.gitignore +0 -0
  14. {pixie_lab_dibs-0.2.0 → pixie_lab_dibs-0.2.1}/.pre-commit-config.yaml +0 -0
  15. {pixie_lab_dibs-0.2.0 → pixie_lab_dibs-0.2.1}/LICENSE +0 -0
  16. {pixie_lab_dibs-0.2.0 → pixie_lab_dibs-0.2.1}/LICENSES/BSD-3-Clause.txt +0 -0
  17. {pixie_lab_dibs-0.2.0 → pixie_lab_dibs-0.2.1}/Makefile +0 -0
  18. {pixie_lab_dibs-0.2.0 → pixie_lab_dibs-0.2.1}/REUSE.toml +0 -0
  19. {pixie_lab_dibs-0.2.0 → pixie_lab_dibs-0.2.1}/pyproject.toml +0 -0
  20. {pixie_lab_dibs-0.2.0 → pixie_lab_dibs-0.2.1}/scripts/service-smoke.sh +0 -0
  21. {pixie_lab_dibs-0.2.0 → pixie_lab_dibs-0.2.1}/src/dibs/_sources.py +0 -0
  22. {pixie_lab_dibs-0.2.0 → pixie_lab_dibs-0.2.1}/src/dibs/_sshsig.py +0 -0
  23. {pixie_lab_dibs-0.2.0 → pixie_lab_dibs-0.2.1}/src/dibs/_store.py +0 -0
  24. {pixie_lab_dibs-0.2.0 → pixie_lab_dibs-0.2.1}/src/dibs/_users.py +0 -0
  25. {pixie_lab_dibs-0.2.0 → pixie_lab_dibs-0.2.1}/src/dibs/skill/SKILL.md +0 -0
  26. {pixie_lab_dibs-0.2.0 → pixie_lab_dibs-0.2.1}/tests/__init__.py +0 -0
  27. {pixie_lab_dibs-0.2.0 → pixie_lab_dibs-0.2.1}/tests/conftest.py +0 -0
  28. {pixie_lab_dibs-0.2.0 → pixie_lab_dibs-0.2.1}/tests/test_sources.py +0 -0
  29. {pixie_lab_dibs-0.2.0 → pixie_lab_dibs-0.2.1}/tests/test_store.py +0 -0
  30. {pixie_lab_dibs-0.2.0 → pixie_lab_dibs-0.2.1}/tests/test_users.py +0 -0
  31. {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.0
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
@@ -12,4 +12,4 @@ everyone who is on a machine, and it never stops anyone from logging
12
12
  in.
13
13
  """
14
14
 
15
- __version__ = "0.2.0"
15
+ __version__ = "0.2.1"
@@ -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 &lt;name&gt; --for &lt;purpose&gt;</code>.</p>
385
+ <code>dibs claim &lt;name&gt; --for &lt;purpose&gt;</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
- text = SKILL.read_text()
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