registry: "elsewhere" owners and the signed owner file #19

Closed
opened 2026-10-05 21:07:11 +00:00 by orion · 9 comments
Collaborator

Context

RELEASE.md section 7.1 lists the owner kinds of the registry. This issue builds the "elsewhere" owner: an organisation claims its owner on the public registry and states the URL of its own registry; the public registry then serves nothing for that owner and says where it publishes. It is the registry's answer to dependency confusion (7.1, 7.6). The issue also introduces the signed owner file index/<owner>/owner.trb of 7.4, which nothing writes today; verified domains (#341) and the client (#342) build on it.

Verified domains were part of this issue's first version and are split off: see Dependencies.

What exists today (main 787f63f)

  • tools/registry: table owners has a column elsewhere TEXT (src/database.trb) that no code reads or writes; ownerView / OwnerView (src/registry.trb) show name, kind and members only; organisations are created by POST /api/1/owners, owner calls need the owners action and a Torb-Code (proved).
  • No code writes index/<owner>/owner.trb. The client never reads it; compiler/src/package/signing.trb and index.trb sign and check records of package files and config.trb only.
  • RELEASE.md 7.11 "Not built yet" names "elsewhere" owners.

Source

Design record point

7.1 table row Elsewhere and the paragraph "The 'elsewhere' owner is the answer to dependency confusion", plus 7.4's owner.trb line. The record gets a short "as built" note under 7.11 and the "Not built yet" list loses "elsewhere" owners.

Design (decided by the loop, see the comment of 2026-10-12)

  • Claim: PUT /api/1/owners/<owner>/elsewhere with {url}, undo with DELETE of the same path. Only an organisation, only by a member with role owner, token action owners, Torb-Code as for every owners call (proved). Operator token allowed like for members.

  • URL rules: https: only, a host name (no IP literal, no localhost), no user info, no query or fragment, at most 200 bytes, parsed with std/uri. The registry never fetches it.

  • Owner with packages: a claim on an owner that already has a package is a 409 (published releases are immutable, 7.3; the moderators can resolve it by hand). Publishing to an owner that claimed elsewhere is a 409 that names the URL; so is creating a package of it by any route (publish, trusted publishing exchange).

  • The owner file: one signed record, replaced whole (the one file of the index that is not append-only, because it is state, not history):

    // index/acme/owner.trb - written by packages.torb.dev. Replaced whole.
    owner "acme", kind: "organisation", created: 1760000000, elsewhere: "https://registry.acme.example", signature: "ed25519:..."
    

    elsewhere: and domain: are left out when unset. domain: is part of the record type, the writer, the reader and the signed text from the start (empty here; #341 sets it), so adding verified domains changes no format. It is written for every owner (person and organisation) when the owner is created, when a claim changes, and filled at start for an owner without one (like package pages); mirrored like index files.

  • Signature: over a text torb index owner 1, the owner, kind, created, elsewhere and domain, quoted the way recordMessage quotes, in compiler/src/package/signing.trb / index.trb shared by the write service and the client. A rolled-back file served by a mirror is the same as a withheld update (7.14) and accepted.

  • API: OwnerView gains elsewhere (empty if none) and domain (empty here); torb owner list is #342's.

Acceptance criteria

  • PUT/DELETE /api/1/owners/<owner>/elsewhere as designed: success returns the OwnerView with elsewhere; refusals tested: person owner (400), non-owner member (403), missing Torb-Code (401/403 as proved answers), bad URL (http, IP literal, user info, query, over length: 400), owner with a package (409), unknown owner (404)
  • A publish (upload and trusted-publishing exchange) to an owner with elsewhere is refused with a 409 naming the URL; after DELETE it works again
  • index/<owner>/owner.trb is written for every owner (created, claim, undo), filled at start for an owner without one, mirrored, and signed; compiler/src/package has the record text, a writer and a reader that rejects a changed, unsigned or other-owner record
  • Tests in tools/registry/tests (new elsewhere.test.trb, natively and in the VM) and compiler/tests/packages.test.trb (record text, signature roundtrip, tampering)
  • tools/registry/README.md (API list), docs/design/RELEASE.md 7.1/7.4/7.11 ("as built" note, "Not built yet" updated) and the generated skills/ are in step; sh tools/gates.sh a green in forge CI

Seed impact

No: TorbScript only, no new native, no driver flag, no std name looked up by string.

Size

One PR, two Conventional Commits: (1) feat(compiler): owner record text/writer/reader/signature with tests; (2) feat(registry): database access, endpoints, publish refusal, file writing, tests, docs. Worker: opus/high (security-relevant, tools/registry); architect review of the diff before the PR.

Dependencies

None. Followed by #341 (verified domains) and #342 (client), both of which wait for this issue.

Moved from the planning lists of the docs; the design record stays the specification.

## Context RELEASE.md section 7.1 lists the owner kinds of the registry. This issue builds the **"elsewhere" owner**: an organisation claims its owner on the public registry and states the URL of its own registry; the public registry then serves nothing for that owner and says where it publishes. It is the registry's answer to dependency confusion (7.1, 7.6). The issue also introduces the **signed owner file** `index/<owner>/owner.trb` of 7.4, which nothing writes today; verified domains (#341) and the client (#342) build on it. Verified domains were part of this issue's first version and are split off: see Dependencies. ## What exists today (main `787f63f`) - `tools/registry`: table `owners` has a column `elsewhere TEXT` (`src/database.trb`) that no code reads or writes; `ownerView` / `OwnerView` (`src/registry.trb`) show name, kind and members only; organisations are created by `POST /api/1/owners`, owner calls need the `owners` action and a `Torb-Code` (`proved`). - No code writes `index/<owner>/owner.trb`. The client never reads it; `compiler/src/package/signing.trb` and `index.trb` sign and check records of package files and `config.trb` only. - RELEASE.md 7.11 "Not built yet" names "elsewhere" owners. ## Source - [ROADMAP.md, the package manager](https://git.torb.dev/torbscript/language/src/commit/787f63f2548b3c1e71dfb15418a1df497408872e/docs/ROADMAP.md#L96) - [RELEASE.md section 7.1 (Owners, "elsewhere")](https://git.torb.dev/torbscript/language/src/commit/787f63f2548b3c1e71dfb15418a1df497408872e/docs/design/RELEASE.md#L866), [7.4 (owner.trb)](https://git.torb.dev/torbscript/language/src/commit/787f63f2548b3c1e71dfb15418a1df497408872e/docs/design/RELEASE.md#L975), [7.6 (dependency confusion)](https://git.torb.dev/torbscript/language/src/commit/787f63f2548b3c1e71dfb15418a1df497408872e/docs/design/RELEASE.md#L1070), [7.11](https://git.torb.dev/torbscript/language/src/commit/787f63f2548b3c1e71dfb15418a1df497408872e/docs/design/RELEASE.md#L1172) ## Design record point 7.1 table row **Elsewhere** and the paragraph "The 'elsewhere' owner is the answer to dependency confusion", plus 7.4's `owner.trb` line. The record gets a short "as built" note under 7.11 and the "Not built yet" list loses "elsewhere" owners. ## Design (decided by the loop, see the comment of 2026-10-12) - **Claim:** `PUT /api/1/owners/<owner>/elsewhere` with `{url}`, undo with `DELETE` of the same path. Only an **organisation**, only by a member with role `owner`, token action `owners`, `Torb-Code` as for every owners call (`proved`). Operator token allowed like for members. - **URL rules:** `https:` only, a host name (no IP literal, no `localhost`), no user info, no query or fragment, at most 200 bytes, parsed with `std/uri`. The registry never fetches it. - **Owner with packages:** a claim on an owner that already has a package is a `409` (published releases are immutable, 7.3; the moderators can resolve it by hand). Publishing to an owner that claimed elsewhere is a `409` that names the URL; so is creating a package of it by any route (publish, trusted publishing exchange). - **The owner file:** one signed record, replaced whole (the one file of the index that is not append-only, because it is state, not history): ```trb // index/acme/owner.trb - written by packages.torb.dev. Replaced whole. owner "acme", kind: "organisation", created: 1760000000, elsewhere: "https://registry.acme.example", signature: "ed25519:..." ``` `elsewhere:` and `domain:` are left out when unset. `domain:` is part of the record type, the writer, the reader and the signed text from the start (empty here; #341 sets it), so adding verified domains changes no format. It is written for **every** owner (person and organisation) when the owner is created, when a claim changes, and filled at start for an owner without one (like package pages); mirrored like index files. - **Signature:** over a text `torb index owner 1`, the owner, kind, created, elsewhere and domain, quoted the way `recordMessage` quotes, in `compiler/src/package/signing.trb` / `index.trb` shared by the write service and the client. A rolled-back file served by a mirror is the same as a withheld update (7.14) and accepted. - **API:** `OwnerView` gains `elsewhere` (empty if none) and `domain` (empty here); `torb owner list` is #342's. ## Acceptance criteria - [ ] `PUT`/`DELETE /api/1/owners/<owner>/elsewhere` as designed: success returns the `OwnerView` with `elsewhere`; refusals tested: person owner (`400`), non-owner member (`403`), missing `Torb-Code` (`401`/`403` as `proved` answers), bad URL (http, IP literal, user info, query, over length: `400`), owner with a package (`409`), unknown owner (`404`) - [ ] A publish (upload and trusted-publishing exchange) to an owner with `elsewhere` is refused with a `409` naming the URL; after `DELETE` it works again - [ ] `index/<owner>/owner.trb` is written for every owner (created, claim, undo), filled at start for an owner without one, mirrored, and **signed**; `compiler/src/package` has the record text, a writer and a reader that rejects a changed, unsigned or other-owner record - [ ] Tests in `tools/registry/tests` (new `elsewhere.test.trb`, natively and in the VM) and `compiler/tests/packages.test.trb` (record text, signature roundtrip, tampering) - [ ] `tools/registry/README.md` (API list), `docs/design/RELEASE.md` 7.1/7.4/7.11 ("as built" note, "Not built yet" updated) and the generated `skills/` are in step; `sh tools/gates.sh a` green in forge CI ## Seed impact No: TorbScript only, no new native, no driver flag, no std name looked up by string. ## Size One PR, two Conventional Commits: (1) `feat(compiler)`: owner record text/writer/reader/signature with tests; (2) `feat(registry)`: database access, endpoints, publish refusal, file writing, tests, docs. Worker: opus/high (security-relevant, `tools/registry`); architect review of the diff before the PR. ## Dependencies None. Followed by #341 (verified domains) and #342 (client), both of which wait for this issue. _Moved from the planning lists of the docs; the design record stays the specification._
Author
Collaborator

[loop:torbscript] intake: in scope (author orion, label loop). Registry domain verification and "elsewhere" owners per RELEASE.md section 7.11, tools/registry; security-relevant (opus/high worker, architect review); verification rules are specified in the design record, gaps become an owner decision in refine. Release freeze for 0.1.0 (2026-10-06) holds new PRs, next: refine (queued)

[loop:torbscript] intake: in scope (author orion, label loop). Registry domain verification and "elsewhere" owners per RELEASE.md section 7.11, `tools/registry`; security-relevant (opus/high worker, architect review); verification rules are specified in the design record, gaps become an owner decision in refine. Release freeze for 0.1.0 (2026-10-06) holds new PRs, next: refine (queued)
Author
Collaborator

[loop:torbscript] refine: still queued behind the intake order. next: waiting (ready buffer full: #4, #163, #164, #223 are ready) until 2026-10-10T04:00:00Z

[loop:torbscript] refine: still queued behind the intake order. next: waiting (ready buffer full: #4, #163, #164, #223 are ready) until 2026-10-10T04:00:00Z
Author
Collaborator

[loop:torbscript] refine: held back, the ready buffer is at its target of 4 ready issues (#23, #24, #312, #313), next: waiting (ready buffer full) until 2026-10-12T11:22:04Z

[loop:torbscript] refine: held back, the ready buffer is at its target of 4 `ready` issues (#23, #24, #312, #313), next: waiting (ready buffer full) until 2026-10-12T11:22:04Z
Author
Collaborator

[loop:torbscript] claim refine by session:loop-torbscript-20261012 until 2026-10-12T18:24Z

[loop:torbscript] claim refine by session:loop-torbscript-20261012 until 2026-10-12T18:24Z
orion changed title from registry: verified domains and "elsewhere" owners to registry: "elsewhere" owners and the signed owner file 2026-10-12 16:27:14 +00:00
Author
Collaborator

[loop:torbscript] decided by the loop: the issue was larger than one PR (owner file, elsewhere owners, DNS verification, re-check, client), so it is split along the design record's own seams; none of the decisions below is owner-reserved.

  • Split: #19 (this issue, narrowed in title and body) = "elsewhere" owners + the signed owner.trb; #341 = verified domains (DNS TXT, badge); #343 = re-check of verified domains; #342 = client output (torb add, torb owner list). Dependencies are set in the forge: #341 and #342 need #19, #343 needs #341. Only #19 carries ready now; the others meet the definition of ready in content and get the label when their dependency has landed.
  • Today: owners.elsewhere exists in the database and is unused; index/<owner>/owner.trb is described in 7.4 but written and read by nothing; the client has no owner knowledge; std/network/std/dns already do TXT lookups.
  • owner.trb format: one signed constructor line, replaced whole (state, not history; it is the one index file that is not append-only). domain: is in the type, writer, reader and signed text from the start so #341 changes no format. Why: a format change after the first signed files exist would invalidate signatures or need versions; signing keeps a mirror from inventing an "elsewhere" URL (7.14).
  • Elsewhere rules: organisations only, owner role, owners action and Torb-Code like every owners call; https: URL only, no IP literal/userinfo/query/fragment, <= 200 bytes, never fetched (no SSRF surface); refused with 409 for an owner that already has a package (releases are immutable, 7.3); publish to such an owner is a 409 naming the URL. The URL is only ever printed by the client, never bound automatically (a stranger's claim must not rebind anyone's build).
  • Domain TXT (#341): _torb.<domain> with torb-owner=<owner>; no nonce, because only the domain's DNS controller can create the record and the owner name binds it. Checked in the claim request, resolver = system default, REGISTRY_DNS_SERVERS to override; DNSSEC stays out (DNS.md). One owner per domain, at most 5 domains per owner.
  • Re-check (#343): daily, withdrawn after 3 consecutive days of an answered lookup without the record; timeouts and SERVFAIL never count. Why: RELEASE.md says "checked at creation"; a badge for a lost domain would be false, but a DNS outage must not strip a badge.
  • Open question 9 (dotted owner names) stays as the record recommends: not at launch; the domain is a badge on a plain owner. No breaking change to a published contract (nothing writes or reads owner.trb today).
  • Not owner-reserved: no cost, account or external service (the system resolver is the default; choosing a public resolver would be the operator's configuration), no public statement, no product direction beyond the record.
[loop:torbscript] decided by the loop: the issue was larger than one PR (owner file, elsewhere owners, DNS verification, re-check, client), so it is split along the design record's own seams; none of the decisions below is owner-reserved. - **Split:** #19 (this issue, narrowed in title and body) = "elsewhere" owners + the signed `owner.trb`; #341 = verified domains (DNS TXT, badge); #343 = re-check of verified domains; #342 = client output (`torb add`, `torb owner list`). Dependencies are set in the forge: #341 and #342 need #19, #343 needs #341. Only #19 carries `ready` now; the others meet the definition of ready in content and get the label when their dependency has landed. - **Today:** `owners.elsewhere` exists in the database and is unused; `index/<owner>/owner.trb` is described in 7.4 but written and read by nothing; the client has no owner knowledge; `std/network`/`std/dns` already do TXT lookups. - **owner.trb format:** one signed constructor line, replaced whole (state, not history; it is the one index file that is not append-only). `domain:` is in the type, writer, reader and signed text from the start so #341 changes no format. Why: a format change after the first signed files exist would invalidate signatures or need versions; signing keeps a mirror from inventing an "elsewhere" URL (7.14). - **Elsewhere rules:** organisations only, owner role, `owners` action and `Torb-Code` like every owners call; `https:` URL only, no IP literal/userinfo/query/fragment, <= 200 bytes, never fetched (no SSRF surface); refused with 409 for an owner that already has a package (releases are immutable, 7.3); publish to such an owner is a 409 naming the URL. The URL is only ever printed by the client, never bound automatically (a stranger's claim must not rebind anyone's build). - **Domain TXT (#341):** `_torb.<domain>` with `torb-owner=<owner>`; no nonce, because only the domain's DNS controller can create the record and the owner name binds it. Checked in the claim request, resolver = system default, `REGISTRY_DNS_SERVERS` to override; DNSSEC stays out (DNS.md). One owner per domain, at most 5 domains per owner. - **Re-check (#343):** daily, withdrawn after 3 consecutive days of an *answered* lookup without the record; timeouts and SERVFAIL never count. Why: RELEASE.md says "checked at creation"; a badge for a lost domain would be false, but a DNS outage must not strip a badge. - **Open question 9 (dotted owner names)** stays as the record recommends: not at launch; the domain is a badge on a plain owner. No breaking change to a published contract (nothing writes or reads `owner.trb` today). - Not owner-reserved: no cost, account or external service (the system resolver is the default; choosing a public resolver would be the operator's configuration), no public statement, no product direction beyond the record.
Author
Collaborator

[loop:torbscript] refine: Definition of ready met: body rewritten (goal, state of main 787f63f, design record points 7.1/7.4/7.6/7.11, testable acceptance criteria, seed impact: no, size: one PR / two commits), split into #341, #342, #343 (dependencies set), no owner decision needed. Label ready set. Worker usage about 110k tokens (estimate), next: implement

[loop:torbscript] refine: Definition of ready met: body rewritten (goal, state of main 787f63f, design record points 7.1/7.4/7.6/7.11, testable acceptance criteria, seed impact: no, size: one PR / two commits), split into #341, #342, #343 (dependencies set), no owner decision needed. Label `ready` set. Worker usage about 110k tokens (estimate), next: implement
Author
Collaborator

[loop:torbscript] claim implement by session:loop-torbscript-20261012 until 2026-10-12T22:28Z

[loop:torbscript] claim implement by session:loop-torbscript-20261012 until 2026-10-12T22:28Z
Author
Collaborator

[loop:torbscript] claim review-fix by session:loop-torbscript-20261012 until 2026-10-13T00:03Z (PR #347: pdisc check red; the first worker died on an output-size error after pushing)

[loop:torbscript] claim review-fix by session:loop-torbscript-20261012 until 2026-10-13T00:03Z (PR #347: pdisc check red; the first worker died on an output-size error after pushing)
Author
Collaborator

[loop:torbscript] review-fix: pdisc / pdisc was red because the tools/registry/README.md body changed (the elsewhere-owners rows and the owner-file paragraph) while its .pdisc-sidecar.yaml stamp (body_hash) still had the old value. Changed: the sidecar summary now names the elsewhere owners and the signed owner file (under 300 chars), the entry is restamped (pdisc.py stamp) and INDEX.md regenerated; the fix is folded into the second commit so the PR stays two Conventional Commits. main had moved, so the branch is rebased on it (linear, no merge commit; tools/check-commits.sh origin/main HEAD clean) and force-pushed with lease to loop/19-elsewhere-owners (PR #347, head aa9488d5). No other review findings were open on the PR or this issue. CI on aa9488d5: all checks green (pdisc, gates, tier A/B, registry, same-C, playground, VS Code). Worker usage about 110k tokens (estimate), next: review

[loop:torbscript] review-fix: `pdisc / pdisc` was red because the `tools/registry/README.md` body changed (the elsewhere-owners rows and the owner-file paragraph) while its `.pdisc-sidecar.yaml` stamp (`body_hash`) still had the old value. Changed: the sidecar summary now names the elsewhere owners and the signed owner file (under 300 chars), the entry is restamped (`pdisc.py stamp`) and `INDEX.md` regenerated; the fix is folded into the second commit so the PR stays two Conventional Commits. `main` had moved, so the branch is rebased on it (linear, no merge commit; `tools/check-commits.sh origin/main HEAD` clean) and force-pushed with lease to `loop/19-elsewhere-owners` (PR #347, head aa9488d5). No other review findings were open on the PR or this issue. CI on aa9488d5: all checks green (pdisc, gates, tier A/B, registry, same-C, playground, VS Code). Worker usage about 110k tokens (estimate), next: review
argus closed this issue 2026-10-12 21:32:33 +00:00
Sign in to join this conversation.
No labels
loop
ready
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
torbscript/language#19
No description provided.