Say at startup when this checkout is behind its remote #237

Merged
branderson merged 1 commits from update-check into development 2026-08-26 17:54:39 -07:00
Collaborator

Summary generated by LLM agent

dejima has no release channel - it updates by git pull - so "a newer version is available" means the branch this checkout already tracks has commits this checkout does not. bin/dejima-update-check asks the remote for that branch's tip and says so, once, before the image build rather than after it, since an operator who wants the newer dejima would rather know before waiting on a build of the one they have.

It is advisory. A check that cannot answer does not stop a launch, and [core] update_check = off stops it making the request at all.

The part that needed care

Reaching the remote by name would have been the obvious implementation and is the wrong one. git ls-remote origin reads the checkout's own config for how to get there, and core.sshCommand, credential.helper and url.<base>.insteadOf each name a command git then runs on the host. That is measured, not assumed - by remote name a repo-local core.sshCommand executes, and by URL from outside the repository it does not.

That matters here specifically because dejima mounts its own checkout writable when that checkout is the workspace, which is how this repository gets worked on. A session that wrote .git/config there would otherwise get host execution out of the next launch. It is the same class as the core.hooksPath route docs/security.md already describes and accepts, with one difference worth the extra work: hooksPath waits for the operator to run a git command, and a check that runs at every startup waits for nothing.

So the URL is read from the config, checked, and handed to a git started outside the repository. protocol.allow holds the transports to ones that cannot name a command, and a URL that could be read as an option, or that names a remote helper, is refused rather than passed on. What still crosses is the URL itself, which the same write can repoint - that buys one outbound request and no execution, and the audit reports the config change it took.

Since dejima's argument is about what leaves and through which control, a request dejima makes on its own account is disclosed under its own heading in docs/security.md rather than left to be discovered.

Behaviour

The remote is asked at most once an hour. The answer it gave is compared against HEAD on every launch, so the notice appears each time until you pull and then stops immediately rather than an hour later - throttling the notice instead of the network call would mean being told once and then not again. A failed lookup is recorded as well, or a checkout with no route to its remote pays the timeout on every single launch.

Silent when current or ahead, when HEAD is detached, when the branch has no counterpart on the remote, and whenever the remote cannot be reached.

The commit count is given only when the commits are actually present locally. When the remote's tip has never been fetched - the common case - counting what it can reach would report the commits it already has rather than the ones it is missing, so it says there are commits without inventing a number.

Tests

Ten in config.bats, against a real checkout and a real remote, neither of them this repository. No git push anywhere in the fixture: dejima's own forge guard refuses a push to a path, so a fixture built that way passes on a host and fails inside a sandbox.

Four are mutation-checked against a deliberately broken implementation. Two notes on getting those right rather than merely green:

The security test first asserted on output, and the mutation that reaches the remote by name passed it - the tool sends git's stderr to /dev/null, so the assertion held whether the command ran or not. It now observes a filesystem side effect. It also asserts the control, so that if a future git stops honouring a repository's core.sshCommand the test says the threat went away rather than reading as a mitigation working.

A separate test covers a branch whose name differs from its upstream. That was a real bug in the first draft - it looked up branch.<name>.remote and then asked for refs/heads/<local branch name>, which on a git worktree add -b x origin/development checkout asks for a branch that does not exist. An empty answer is indistinguishable from being up to date, so the check went silent on exactly the branches someone developing dejima stands on. The fixture would not have caught it, since both names were the same there.

Testing

All seven confined suites pass: config 172, static 388, audit 164, providers 94, project 141, remote 109, proxy 32. shellcheck clean. bin/dejima and dejima-config-lib.sh both changed, which is why the whole set ran.

test/runtime.bats needs Docker and was not run locally. Nothing here touches the container, the mounts or the firewall - the tool runs entirely on the host, before the container exists.

No new CI destination: the fixture's remote is a local path, so nothing added here reaches the network from a runner.

**Summary generated by LLM agent** dejima has no release channel - it updates by `git pull` - so "a newer version is available" means the branch this checkout already tracks has commits this checkout does not. `bin/dejima-update-check` asks the remote for that branch's tip and says so, once, before the image build rather than after it, since an operator who wants the newer dejima would rather know before waiting on a build of the one they have. It is advisory. A check that cannot answer does not stop a launch, and `[core] update_check = off` stops it making the request at all. ## The part that needed care Reaching the remote by name would have been the obvious implementation and is the wrong one. `git ls-remote origin` reads the checkout's own config for how to get there, and `core.sshCommand`, `credential.helper` and `url.<base>.insteadOf` each name a command git then runs on the host. That is measured, not assumed - by remote name a repo-local `core.sshCommand` executes, and by URL from outside the repository it does not. That matters here specifically because dejima mounts its own checkout writable when that checkout is the workspace, which is how this repository gets worked on. A session that wrote `.git/config` there would otherwise get host execution out of the *next* launch. It is the same class as the `core.hooksPath` route `docs/security.md` already describes and accepts, with one difference worth the extra work: `hooksPath` waits for the operator to run a git command, and a check that runs at every startup waits for nothing. So the URL is read from the config, checked, and handed to a git started outside the repository. `protocol.allow` holds the transports to ones that cannot name a command, and a URL that could be read as an option, or that names a remote helper, is refused rather than passed on. What still crosses is the URL itself, which the same write can repoint - that buys one outbound request and no execution, and the audit reports the config change it took. Since dejima's argument is about what leaves and through which control, a request dejima makes on its own account is disclosed under its own heading in `docs/security.md` rather than left to be discovered. ## Behaviour The remote is asked at most once an hour. The answer it gave is compared against HEAD on *every* launch, so the notice appears each time until you pull and then stops immediately rather than an hour later - throttling the notice instead of the network call would mean being told once and then not again. A failed lookup is recorded as well, or a checkout with no route to its remote pays the timeout on every single launch. Silent when current or ahead, when HEAD is detached, when the branch has no counterpart on the remote, and whenever the remote cannot be reached. The commit count is given only when the commits are actually present locally. When the remote's tip has never been fetched - the common case - counting what it can reach would report the commits it already has rather than the ones it is missing, so it says there are commits without inventing a number. ## Tests Ten in `config.bats`, against a real checkout and a real remote, neither of them this repository. No `git push` anywhere in the fixture: dejima's own forge guard refuses a push to a path, so a fixture built that way passes on a host and fails inside a sandbox. Four are mutation-checked against a deliberately broken implementation. Two notes on getting those right rather than merely green: The security test first asserted on output, and the mutation that reaches the remote by name *passed* it - the tool sends git's stderr to `/dev/null`, so the assertion held whether the command ran or not. It now observes a filesystem side effect. It also asserts the control, so that if a future git stops honouring a repository's `core.sshCommand` the test says the threat went away rather than reading as a mitigation working. A separate test covers a branch whose name differs from its upstream. That was a real bug in the first draft - it looked up `branch.<name>.remote` and then asked for `refs/heads/<local branch name>`, which on a `git worktree add -b x origin/development` checkout asks for a branch that does not exist. An empty answer is indistinguishable from being up to date, so the check went silent on exactly the branches someone developing dejima stands on. The fixture would not have caught it, since both names were the same there. ## Testing All seven confined suites pass: config 172, static 388, audit 164, providers 94, project 141, remote 109, proxy 32. `shellcheck` clean. `bin/dejima` and `dejima-config-lib.sh` both changed, which is why the whole set ran. `test/runtime.bats` needs Docker and was not run locally. Nothing here touches the container, the mounts or the firewall - the tool runs entirely on the host, before the container exists. No new CI destination: the fixture's remote is a local path, so nothing added here reaches the network from a runner.
dejima-agent added 1 commit 2026-08-26 14:56:32 -07:00
Say at startup when this checkout is behind its remote
tests / confined (proxy) (pull_request_target) Successful in 1m14s
tests / confined (conversation) (pull_request_target) Successful in 1m26s
tests / privileged (pull_request_target) Successful in 3m55s
tests / confined (static) (pull_request_target) Successful in 5m31s
tests / confined (config) (pull_request_target) Successful in 9m37s
tests / runner-env (confined) (pull_request_target) Successful in 37s
tests / confined (shellcheck) (pull_request_target) Successful in 59s
tests / confined (providers) (pull_request_target) Successful in 2m49s
tests / confined (remote) (pull_request_target) Successful in 4m9s
tests / runner-env (privileged) (pull_request_target) Successful in 39s
tests / confined (project) (pull_request_target) Successful in 5m54s
tests / confined (audit) (pull_request_target) Successful in 9m49s
831221d930
dejima updates by `git pull`, so a launch can tell you there is a newer one
by asking the remote your checkout already tracks for the tip of the branch
it already follows. One line, before the image build rather than after it,
and advisory: a check that cannot answer does not stop a launch.

The remote is reached by URL, from outside the repository, and never by
remote name. `git ls-remote origin` reads the checkout's own config for how
to get there, and core.sshCommand, credential.helper and url.<base>.insteadOf
each name a command git then runs on the host - measured, not assumed. dejima
mounts its own checkout writable when that checkout is the workspace, so a
session writing .git/config there would otherwise get host execution out of
the next launch. Same class as the core.hooksPath route docs/security.md
already describes, with one difference: hooksPath waits for the operator to
run a git command, and a startup check waits for nothing. protocol.allow
holds the transports to ones that cannot name a command, and a URL that
could be read as an option is refused rather than passed on.

What still crosses is the URL, which the same write can repoint. That buys
one outbound request and no execution, and the audit reports the config
change it took. Stated in docs/security.md under its own heading, because a
request dejima makes on its own account is the one an operator is least
expecting.

The remote is asked at most hourly; the answer it gave is compared against
HEAD on every launch, so the notice appears each time until you pull and then
stops immediately rather than an hour later. A failed lookup is recorded too,
or a checkout with no route to its remote pays the timeout every launch.

[core] update_check = off stops it entirely, and then dejima contacts nothing
of its own accord.

Tests: ten in config.bats against a real checkout and a real remote, neither
of them this repository and no push anywhere - dejima's own forge guard
refuses a push to a path, so a fixture built that way would pass on a host
and fail inside a sandbox.

Four are mutation-checked. The security one observes a filesystem side effect
rather than a message, because the tool sends git's stderr to /dev/null and an
output assertion held whether the command ran or not; it also asserts the
control, so it cannot pass by the threat quietly going away.
branderson approved these changes 2026-08-26 17:52:07 -07:00
branderson merged commit be1d4521ec into development 2026-08-26 17:54:39 -07:00
Sign in to join this conversation.