torb owner --step-up: approve owner changes with a passkey #280

Closed
opened 2026-10-09 20:26:36 +00:00 by orion · 5 comments
Collaborator

Goal

An account whose only second factor is a passkey cannot use torb owner add|remove (#22 sends Torb-Code from --code/$TORB_CODE, an authenticator app or recovery code). Add the step-up flow of #275 to the client: torb owner add|remove --step-up.

Scope

POST /api/1/step-up {call: "owners"}, print the userCode and the account page, wait for the person to approve it with the passkey and press Enter (standard input, refused when it is not a terminal), then send Torb-Code: step-up:<secret> on the one call. The secret is never printed or stored; a grant that expired (300 s) or was refused is reported in the registry's words.

Acceptance criteria

  • Against the registry over loopback, with a grant approved the way tools/registry/tests/passkey-step-up.test.trb approves one: owner add succeeds with --step-up, the grant is used up, a second call needs a new one
  • An unapproved or expired grant gives the registry's refusal and changes nothing
  • The secret appears in no argument, output or file
  • Docs: torb-owner.md section on passkeys

Design record point

RELEASE.md 7.6 ("a stolen token"), tools/registry/README.md (issue #275). Seed impact: none. One PR, one commit feat(package): step-up for torb owner.

Depends on #22.

## Goal An account whose only second factor is a passkey cannot use `torb owner add|remove` (#22 sends `Torb-Code` from `--code`/`$TORB_CODE`, an authenticator app or recovery code). Add the step-up flow of #275 to the client: `torb owner add|remove --step-up`. ## Scope `POST /api/1/step-up {call: "owners"}`, print the `userCode` and the account page, wait for the person to approve it with the passkey and press Enter (standard input, refused when it is not a terminal), then send `Torb-Code: step-up:<secret>` on the one call. The secret is never printed or stored; a grant that expired (300 s) or was refused is reported in the registry's words. ## Acceptance criteria - [ ] Against the registry over loopback, with a grant approved the way `tools/registry/tests/passkey-step-up.test.trb` approves one: `owner add` succeeds with `--step-up`, the grant is used up, a second call needs a new one - [ ] An unapproved or expired grant gives the registry's refusal and changes nothing - [ ] The secret appears in no argument, output or file - [ ] Docs: `torb-owner.md` section on passkeys ## Design record point RELEASE.md 7.6 ("a stolen token"), `tools/registry/README.md` (issue #275). Seed impact: none. One PR, one commit `feat(package): step-up for torb owner`. Depends on #22.
Author
Collaborator

[loop:torbscript] intake: in scope (author orion, label loop, split from #22 / #275), not a duplicate, no collision with a design record. Depends on #22 (the owner command) and is refined after it lands. next: waiting (dependency #22)

[loop:torbscript] intake: in scope (author orion, label loop, split from #22 / #275), not a duplicate, no collision with a design record. Depends on #22 (the owner command) and is refined after it lands. next: waiting (dependency #22)
Author
Collaborator

[loop:torbscript] claim refine by session:loop-torbscript-20261009 until 2026-10-09T23:40Z (dependency #22 landed in 6f9d340b)

[loop:torbscript] claim refine by session:loop-torbscript-20261009 until 2026-10-09T23:40Z (dependency #22 landed in 6f9d340b)
Author
Collaborator

[loop:torbscript] refine: ready, one PR of two commits (read against main at 6f9d340b, after #22).

Goal. torb owner add|remove --step-up lets an account whose only second factor is a passkey change an owner's members: the client starts a step-up (#275), the person approves it at the account page, and the one call carries Torb-Code: step-up:<secret>.

Acceptance criteria (testable)

  • Against the write service over loopback with a passkey account and a grant approved the way tools/registry/tests/passkey-step-up.test.trb approves one (recorded assertion, registry methods or the account page's form): torb owner add <owner> <account> --step-up prints the user code and the account page, waits for a line on standard input, then succeeds; the grant row is used; a second owner add --step-up starts a new grant and needs a new approval.
  • An unapproved grant (Enter pressed without approval) and an expired one (row moved into the past) print the registry's refusal in its words, exit 1, and change nothing; owner remove --step-up works the same.
  • The secret is in no argument of curl/wget, no line torb prints, and no file left under the work directory (the header file and the answer file are empty or gone afterwards).
  • Usage errors exit 2: --step-up with list, --step-up together with --code; a registry that answers 409 (no passkey) or 403 (operator) is reported in its words, exit 1.
  • Docs: torb-owner.md section "A passkey", regenerated skills, one sentence in RELEASE.md 7.13 and the registry README.

Design record point. RELEASE.md 7.6 ("A stolen token": the step-up approved at the account page within five minutes, single use), tools/registry/README.md (issue #275, POST /api/1/step-up). Seed impact: none (TorbScript only, no new native, syntax, driver flag or std name).

Size. One PR, two Conventional Commits (a finding below makes the first necessary):

  1. fix(package): keep the registry's answer out of the work directory
  2. feat(package): step-up for torb owner

Finding the issue did not have: the secret would be left in a file. request in compiler/src/package/upload.trb writes every answer to <cache>/upload/answer with the default mode and never clears it (only the header files are emptied). The answer of POST /api/1/step-up holds the secret, so the acceptance criterion "in no file" fails without a change there. Commit 1 creates the answer file private (writePrivate, mode 0600 before writing, as for the headers) and empties it after the reply is read, for every request; the test of the header file in tools/registry/tests/commands.test.trb grows an assertion for the answer file.

Files touched.

  • Commit 1: compiler/src/package/upload.trb, tools/registry/tests/commands.test.trb.
  • Commit 2: compiler/src/cli/registry.trb (--step-up flag and RegistryOptions.stepUp, ownerProblem, a stepUpCodeFor beside secondFactorCode, StepUpAnswer/StepUpBody types, runOwner), compiler/src/cli/usage.trb, compiler/tests/registry-commands.test.trb (option parsing), new tools/registry/tests/owner-step-up.test.trb, docs docs/tooling/torb-owner.md, docs/design/RELEASE.md (7.13 row), tools/registry/README.md, regenerated skills/** (torb-owner.md copies), .pdisc-sidecar.yaml if pdisc asks.

Test plan (forge CI, no local suites).

  • compiler/tests/registry-commands.test.trb: --step-up parsing and each usage error above.
  • New tools/registry/tests/owner-step-up.test.trb: registry in-process as in passkey-step-up.test.trb (reuse tests/passkey-vectors.trb; found the organisation before the passkey is registered so no code is needed for it), torb run as a child with open pipes (Process.start, as src/program.trb does), not through ran, which feeds all input at once: read its output until the user code appears, approve that grant (beginStepUpApproval, the recorded challenge put in place, finishStepUpApproval), then write the line. Cases: approved add, used-up grant and a second add needing a new one, remove, unapproved, expired, no passkey (409), --step-up with list and with --code. The curl stand-in of commands.test.trb additionally copies each header file and answer file at call time into the test's own log; that is how the test learns the secret (the registry keeps only its SHA-256) to assert it is absent from arguments, output and leftover files.
  • Tier A gates run by CI: format --check, docs skill --check, pdisc.

Implementer. opus / high (compiler/src and secret handling in the registry client).

Dependencies and overlap. #22 landed (6f9d340b); no dependency on #276. PR #282 touches only registry service files and step-up.test.trb, none of these (the new test is a separate file on purpose, passkey-step-up.test.trb stays untouched). Textual overlap with #279 in compiler/src/cli/registry.trb, cli/usage.trb, RELEASE.md 7.13 and the registry README; whichever lands second rebases.

[loop:torbscript] decided by the loop: standard input need not be a terminal. The issue said the Enter is refused when stdin is not a terminal. That protects nothing: the grant is redeemed by a single UPDATE ... WHERE approved = 1 AND used = 0, so an Enter pressed (or piped) before the approval gets the registry's "not approved" refusal and burns no grant. Requiring a terminal would only make the loopback test impossible and a wrapper script (an editor's terminal pane, expect) awkward. Instead an end of input without a line cancels: nothing is sent, exit 1, with a word saying the grant stays open until it expires.

[loop:torbscript] decided by the loop: --step-up excludes --code and the owner list (usage errors, exit 2). TORB_CODE in the environment is ignored when --step-up is given: the explicit flag over ambient state, and an exported TORB_CODE must not turn a passkey flow into a wrong-code attempt that counts against the account's five tries.

[loop:torbscript] decided by the loop: what is printed. The user code and the account page (the page the registry answers) on standard output, the prompt on standard error when it is a terminal (as login does); the id, secret and expires of the answer are never printed, only held in memory until the header is written. A refusal of the start (401, 403 operator, 409 without passkey, 429 at three live grants) and of the call is reported with refusalOf, in the registry's words. The client does not time the 300 seconds itself.

next: implement

[loop:torbscript] refine: ready, one PR of two commits (read against main at 6f9d340b, after #22). **Goal.** `torb owner add|remove --step-up` lets an account whose only second factor is a passkey change an owner's members: the client starts a step-up (#275), the person approves it at the account page, and the one call carries `Torb-Code: step-up:<secret>`. **Acceptance criteria** (testable) - [ ] Against the write service over loopback with a passkey account and a grant approved the way `tools/registry/tests/passkey-step-up.test.trb` approves one (recorded assertion, registry methods or the account page's form): `torb owner add <owner> <account> --step-up` prints the user code and the account page, waits for a line on standard input, then succeeds; the grant row is `used`; a second `owner add --step-up` starts a new grant and needs a new approval. - [ ] An unapproved grant (Enter pressed without approval) and an expired one (row moved into the past) print the registry's refusal in its words, exit 1, and change nothing; `owner remove --step-up` works the same. - [ ] The secret is in no argument of `curl`/`wget`, no line `torb` prints, and no file left under the work directory (the header file and the answer file are empty or gone afterwards). - [ ] Usage errors exit 2: `--step-up` with `list`, `--step-up` together with `--code`; a registry that answers `409` (no passkey) or `403` (operator) is reported in its words, exit 1. - [ ] Docs: `torb-owner.md` section "A passkey", regenerated skills, one sentence in RELEASE.md 7.13 and the registry README. **Design record point.** RELEASE.md 7.6 ("A stolen token": the step-up approved at the account page within five minutes, single use), `tools/registry/README.md` (issue #275, `POST /api/1/step-up`). **Seed impact: none** (TorbScript only, no new native, syntax, driver flag or std name). **Size.** One PR, two Conventional Commits (a finding below makes the first necessary): 1. `fix(package): keep the registry's answer out of the work directory` 2. `feat(package): step-up for torb owner` **Finding the issue did not have: the secret would be left in a file.** `request` in `compiler/src/package/upload.trb` writes every answer to `<cache>/upload/answer` with the default mode and never clears it (only the header files are emptied). The answer of `POST /api/1/step-up` holds the secret, so the acceptance criterion "in no file" fails without a change there. Commit 1 creates the answer file private (`writePrivate`, mode `0600` before writing, as for the headers) and empties it after the reply is read, for every request; the test of the header file in `tools/registry/tests/commands.test.trb` grows an assertion for the answer file. **Files touched.** - Commit 1: `compiler/src/package/upload.trb`, `tools/registry/tests/commands.test.trb`. - Commit 2: `compiler/src/cli/registry.trb` (`--step-up` flag and `RegistryOptions.stepUp`, `ownerProblem`, a `stepUpCodeFor` beside `secondFactorCode`, `StepUpAnswer`/`StepUpBody` types, `runOwner`), `compiler/src/cli/usage.trb`, `compiler/tests/registry-commands.test.trb` (option parsing), new `tools/registry/tests/owner-step-up.test.trb`, docs `docs/tooling/torb-owner.md`, `docs/design/RELEASE.md` (7.13 row), `tools/registry/README.md`, regenerated `skills/**` (`torb-owner.md` copies), `.pdisc-sidecar.yaml` if `pdisc` asks. **Test plan (forge CI, no local suites).** - `compiler/tests/registry-commands.test.trb`: `--step-up` parsing and each usage error above. - New `tools/registry/tests/owner-step-up.test.trb`: registry in-process as in `passkey-step-up.test.trb` (reuse `tests/passkey-vectors.trb`; found the organisation before the passkey is registered so no code is needed for it), `torb` run as a child with open pipes (`Process.start`, as `src/program.trb` does), not through `ran`, which feeds all input at once: read its output until the user code appears, approve that grant (`beginStepUpApproval`, the recorded challenge put in place, `finishStepUpApproval`), then write the line. Cases: approved add, used-up grant and a second add needing a new one, remove, unapproved, expired, no passkey (`409`), `--step-up` with `list` and with `--code`. The `curl` stand-in of `commands.test.trb` additionally copies each header file and answer file at call time into the test's own log; that is how the test learns the secret (the registry keeps only its SHA-256) to assert it is absent from arguments, output and leftover files. - Tier A gates run by CI: `format --check`, `docs skill --check`, pdisc. **Implementer.** opus / high (`compiler/src` and secret handling in the registry client). **Dependencies and overlap.** #22 landed (6f9d340b); no dependency on #276. PR #282 touches only registry service files and `step-up.test.trb`, none of these (the new test is a separate file on purpose, `passkey-step-up.test.trb` stays untouched). Textual overlap with #279 in `compiler/src/cli/registry.trb`, `cli/usage.trb`, RELEASE.md 7.13 and the registry README; whichever lands second rebases. [loop:torbscript] decided by the loop: **standard input need not be a terminal.** The issue said the Enter is refused when stdin is not a terminal. That protects nothing: the grant is redeemed by a single `UPDATE ... WHERE approved = 1 AND used = 0`, so an Enter pressed (or piped) before the approval gets the registry's "not approved" refusal and burns no grant. Requiring a terminal would only make the loopback test impossible and a wrapper script (an editor's terminal pane, `expect`) awkward. Instead an end of input without a line cancels: nothing is sent, exit 1, with a word saying the grant stays open until it expires. [loop:torbscript] decided by the loop: **`--step-up` excludes `--code` and the owner `list`** (usage errors, exit 2). `TORB_CODE` in the environment is ignored when `--step-up` is given: the explicit flag over ambient state, and an exported `TORB_CODE` must not turn a passkey flow into a wrong-code attempt that counts against the account's five tries. [loop:torbscript] decided by the loop: **what is printed.** The user code and the account page (the `page` the registry answers) on standard output, the prompt on standard error when it is a terminal (as `login` does); the `id`, `secret` and `expires` of the answer are never printed, only held in memory until the header is written. A refusal of the start (`401`, `403` operator, `409` without passkey, `429` at three live grants) and of the call is reported with `refusalOf`, in the registry's words. The client does not time the 300 seconds itself. next: implement
Author
Collaborator

[loop:torbscript] claim implement by session:loop-torbscript-20261009 until 2026-10-10T05:45Z

[loop:torbscript] claim implement by session:loop-torbscript-20261009 until 2026-10-10T05:45Z
argus closed this issue 2026-10-09 22:30:50 +00:00
Author
Collaborator

[loop:torbscript] implement: PR #287 open, CI green

Two commits: 37a958ec fix(package): keep the registry's answer out of the work directory; 18a24952 feat(package): step-up for torb owner. The architect review found no blockers; its fixes are in the PR (a private upload directory, files made anew, every file emptied, a --code value that is a flag refused, the docs on an early Enter). Its out-of-scope points are #288. The new owner-step-up.test.trb passes in CI, and locally against the patched compiler (10/10).

Tokens: one implement run of about 0.3M, within the loop's weekly budget.

next: waiting (merge gate review of PR #287)

[loop:torbscript] implement: PR #287 open, CI green Two commits: `37a958ec` fix(package): keep the registry's answer out of the work directory; `18a24952` feat(package): step-up for torb owner. The architect review found no blockers; its fixes are in the PR (a private `upload` directory, files made anew, every file emptied, a `--code` value that is a flag refused, the docs on an early Enter). Its out-of-scope points are #288. The new `owner-step-up.test.trb` passes in CI, and locally against the patched compiler (10/10). Tokens: one implement run of about 0.3M, within the loop's weekly budget. next: waiting (merge gate review of PR #287)
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.

Dependencies

No dependencies set

Reference
torbscript/language#280
No description provided.