remote signing help

This commit is contained in:
Ian Gulliver
2026-10-19 11:38:19 -07:00
parent fad610d5e7
commit 7965e9025a
4 changed files with 49 additions and 1 deletions
+3 -1
View File
@@ -20,13 +20,15 @@ func main() {
checkCmd() checkCmd()
case "run": case "run":
runCmd(os.Args[2:]) runCmd(os.Args[2:])
case "signing":
signingCmd(os.Args[2:])
default: default:
usage() usage()
} }
} }
func usage() { func usage() {
fmt.Fprintln(os.Stderr, "usage: byre serve | byre install | byre check | byre run [--resume ID]") fmt.Fprintln(os.Stderr, "usage: byre serve | byre install | byre check | byre run [--resume ID] | byre signing CHECKOUT")
os.Exit(2) os.Exit(2)
} }
+41
View File
@@ -0,0 +1,41 @@
package main
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
)
func signingCmd(args []string) {
if len(args) != 1 {
usage()
}
checkout := args[0]
common := gitOut(checkout, "rev-parse", "--path-format=absolute", "--git-common-dir")
if gitOut(checkout, "rev-parse", "--path-format=absolute", "--git-dir") != common {
fatal("checkout is a linked worktree; run byre signing on its main checkout", "checkout", checkout)
}
unsigned := filepath.Join(common, "unsigned.gitconfig")
gitOut(checkout, "config", "commit.gpgsign", "true")
gitOut(checkout, "config", "--file", unsigned, "commit.gpgsign", "false")
gitOut(checkout, "config", "includeIf.gitdir:"+filepath.Join(common, "worktrees")+"/**.path", "unsigned.gitconfig")
signing := gitOut(checkout, "config", "--type=bool", "--get", "commit.gpgsign")
if signing != "true" {
fatal("checkout's commit.gpgsign is not true after setting it", "checkout", checkout, "value", signing)
}
fmt.Printf("%s: commit.gpgsign %s\n", checkout, signing)
fmt.Printf("%s/worktrees/*: commit.gpgsign %s\n", common, gitOut(checkout, "config", "--file", unsigned, "--type=bool", "--get", "commit.gpgsign"))
}
func gitOut(dir string, args ...string) string {
cmd := exec.Command("git", args...)
cmd.Dir = dir
cmd.Stderr = os.Stderr
out, err := cmd.Output()
if err != nil {
fatal("git failed", "args", strings.Join(args, " "), "err", err)
}
return strings.TrimSpace(string(out))
}
+4
View File
@@ -366,6 +366,10 @@ A byre restart drops every connection; each app reconnects 2s later, and calls i
byre never signs with a key on the machine it runs on. When a project checkout's `commit.gpgsign` is true (`git -C <checkout> config --type=bool --get commit.gpgsign` prints it), the squash commit of a Merge or `merge_to_main` is signed on the merging user's Mac, so a key that never leaves that Mac's Secure Enclave and needs a fingerprint works. `SquashCommit` (`internal/git/git.go`) makes the unsigned commit with `git commit-tree`, reads its object with `git cat-file commit`, and hands those bytes to the signer (`internal/agents/sign.go`). The signer sends `op: sign` with the bytes in `data`, `ident` (`Name <email>` from the user's git identity) and `timeout_ms` 120000 to the user's most recently active connected client, the same order `list_clients` uses, and fails the merge when the user has none connected. The app writes the bytes to a temporary file and runs a script under `/bin/zsh -l` in the home directory that reads that Mac's own git config: `gpg.format` (default `openpgp`), its program (`gpg.openpgp.program` or `gpg.program`, default `gpg`, for `openpgp`; `gpg.x509.program`, default `gpgsm`, for `x509`; any other format fails), and `user.signingkey`, or `ident` when unset. It runs the program as git does, `<program> --status-fd=2 -bsau <key>` with the commit on stdin, with the signature written to a second temporary file, since login shell startup files can print to stdout. The reply carries the exit status, the program's stderr in `output` and the signature in `data`. byre fails the merge unless the status is 0 and `output` has a `[GNUPG:] SIG_CREATED ` line, as git requires, and otherwise inserts the signature as a `gpgsig` header before the commit's blank line, each line after the first indented by one space, and writes the signed object with `git hash-object -t commit -w --stdin`. The merge waits up to 2 minutes 30 seconds for the reply, and a Touch ID prompt the human doesn't answer within the 2 minutes fails it. The notes commit is not signed. byre never signs with a key on the machine it runs on. When a project checkout's `commit.gpgsign` is true (`git -C <checkout> config --type=bool --get commit.gpgsign` prints it), the squash commit of a Merge or `merge_to_main` is signed on the merging user's Mac, so a key that never leaves that Mac's Secure Enclave and needs a fingerprint works. `SquashCommit` (`internal/git/git.go`) makes the unsigned commit with `git commit-tree`, reads its object with `git cat-file commit`, and hands those bytes to the signer (`internal/agents/sign.go`). The signer sends `op: sign` with the bytes in `data`, `ident` (`Name <email>` from the user's git identity) and `timeout_ms` 120000 to the user's most recently active connected client, the same order `list_clients` uses, and fails the merge when the user has none connected. The app writes the bytes to a temporary file and runs a script under `/bin/zsh -l` in the home directory that reads that Mac's own git config: `gpg.format` (default `openpgp`), its program (`gpg.openpgp.program` or `gpg.program`, default `gpg`, for `openpgp`; `gpg.x509.program`, default `gpgsm`, for `x509`; any other format fails), and `user.signingkey`, or `ident` when unset. It runs the program as git does, `<program> --status-fd=2 -bsau <key>` with the commit on stdin, with the signature written to a second temporary file, since login shell startup files can print to stdout. The reply carries the exit status, the program's stderr in `output` and the signature in `data`. byre fails the merge unless the status is 0 and `output` has a `[GNUPG:] SIG_CREATED ` line, as git requires, and otherwise inserts the signature as a `gpgsig` header before the commit's blank line, each line after the first indented by one space, and writes the signed object with `git hash-object -t commit -w --stdin`. The merge waits up to 2 minutes 30 seconds for the reply, and a Touch ID prompt the human doesn't answer within the 2 minutes fails it. The notes commit is not signed.
`byre signing <checkout>` (`cmd/byre/signing.go`) turns it on for a project, and task agents' guidance names it. It sets `commit.gpgsign` true in the checkout's git config, and false for its task worktrees, which share that config and would otherwise try to sign their end-of-turn and agents' own commits on the server, and fail. It writes `commit.gpgsign false` to `unsigned.gitconfig` in the checkout's git common directory (`git rev-parse --path-format=absolute --git-common-dir`, which resolves symlinks, as git's `gitdir:` matching does) and includes it with `includeIf.gitdir:<common dir>/worktrees/**.path`, which `git config` appends after `commit.gpgsign` so it wins. It prints both values, and fails unless the checkout's reads true. It refuses a checkout that is itself a linked worktree, since the include would turn signing off for it too. `extensions.worktreeConfig` wouldn't do it, since `git worktree add` copies the checkout's `config.worktree` into each new worktree, nor would the server's global config, which is read before the checkout's. The client Mac needs git's own signing config, the same that signs the user's local commits: `gpg.format` (`openpgp` or `x509`; `ssh` is not supported), the program when it isn't `gpg`/`gpgsm`, and `user.signingkey` when the program can't find the key from `ident`. The program must print `[GNUPG:] SIG_CREATED ` on its status fd. The signature is made for the user's git identity in byre, not the Mac's `user.email`.
No restart is needed; the next Merge or `merge_to_main` reads the checkout's config. It needs the merging user's app connected on a configured Mac, and the squash commit then carries a `gpgsig` header (`git -C <checkout> cat-file commit HEAD` shows it; the server may have no program to verify it with).
`log show --info --last 10m --predicate 'subsystem == "run.fc.byre.mac"'` on a client shows `client request <id>: <op>` lines and `byre-hands` starts and exits, which are info level and kept only in memory, so they cover the last few minutes at most; connection drops (`client connection closed`, `client connection got no pong`) are notices, which macOS keeps on disk, so `log show --start` reaches much further back for them; byre's log shows `client connected` and `client disconnected`. `log show --info --last 10m --predicate 'subsystem == "run.fc.byre.mac"'` on a client shows `client request <id>: <op>` lines and `byre-hands` starts and exits, which are info level and kept only in memory, so they cover the last few minutes at most; connection drops (`client connection closed`, `client connection got no pong`) are notices, which macOS keeps on disk, so `log show --start` reaches much further back for them; byre's log shows `client connected` and `client disconnected`.
## Changes ## Changes
+1
View File
@@ -21,6 +21,7 @@ const guidance = `You are running inside byre. The human reads your work in byre
- In that final message and in ask_human context, put long supporting detail (logs, full lists, data) in collapsed sections the human expands on demand: a line <details><summary>Title</summary>, the Markdown content, then a line </details>. - In that final message and in ask_human context, put long supporting detail (logs, full lists, data) in collapsed sections the human expands on demand: a line <details><summary>Title</summary>, the Markdown content, then a line </details>.
- Don't commit. byre commits everything in the worktree at the end of each turn, using the turn's final message. - Don't commit. byre commits everything in the worktree at the end of each turn, using the turn's final message.
- Don't push, don't switch or touch other branches, and don't run git maintenance (gc, prune, worktree prune). byre handles merging and pushing. - Don't push, don't switch or touch other branches, and don't run git maintenance (gc, prune, worktree prune). byre handles merging and pushing.
- byre signs merge commits on the merging user's Mac. To turn it on for a project, run ` + "`byre signing <checkout>`" + ` on this machine.
- TODO.md at the repo root is the project's list of work for byre. Each item is a top-level ` + "`- `" + ` line holding only a short title, which byre uses as the task's title and the item's identity, with its description in lines indented two spaces under it. ` + "`#`" + ` headings group items into sections, and a checked ` + "`- [x]`" + ` item is skipped.` - TODO.md at the repo root is the project's list of work for byre. Each item is a top-level ` + "`- `" + ` line holding only a short title, which byre uses as the task's title and the item's identity, with its description in lines indented two spaces under it. ` + "`#`" + ` headings group items into sections, and a checked ` + "`- [x]`" + ` item is skipped.`
const proseQuestionNudge = "Your final message asks the human something in prose. Ask it with byre's ask_human tool " + const proseQuestionNudge = "Your final message asks the human something in prose. Ask it with byre's ask_human tool " +