Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
d5fe1dc
docs: settle the sidecar compose filename and layout
JounQin Oct 29, 2026
e894aac
docs: use the compose filename relative to the deployment directory
JounQin Oct 29, 2026
95a6c65
docs: rely on compose's default docker-compose.yml
JounQin Oct 29, 2026
84a7ea4
docs: quote deployment paths and note the legacy compose file
JounQin Oct 29, 2026
b7fd05a
docs: render the dev compose file in the deployment directory
JounQin Oct 30, 2026
151efb9
docs: use the shared GHCR dev tag for the local host image
JounQin Oct 30, 2026
dc4adca
refactor: drop the legacy compose filename notice
JounQin Oct 30, 2026
99efea3
docs: make the development compose workflow use its deployment directory
JounQin Oct 30, 2026
0e5eb02
docs: define the deployment directory in the diagnose snippet
JounQin Oct 30, 2026
d2801ea
docs: pass the required socket directory in the dev compose example
JounQin Oct 30, 2026
518c409
docs: pass the pusher uid and gid in the dev compose example
JounQin Oct 30, 2026
0c294c4
feat: default probe and log host to github.com
JounQin Oct 30, 2026
9d2e9c4
docs: record the dev image deployment
JounQin Oct 30, 2026
0afbe0a
docs: require the sidecar to run as the pusher identity
JounQin Oct 30, 2026
eb0bf14
fix: complete the dev deployment docs and host default
JounQin Oct 30, 2026
6a9ab37
docs: use the same example pusher uid in both files
JounQin Oct 30, 2026
154aa5e
fix: reject a bare --host and document sidecar file ownership
JounQin Oct 30, 2026
20275ae
docs: include user.refresh in sidecar ownership
JounQin Oct 30, 2026
0ed8dc6
test: cover bare and empty --host refusals
JounQin Oct 30, 2026
5fa0def
test: assert probe defaults to github.com
JounQin Oct 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
```
宿主 NAS
/volume1/docker/git-cred-broker/
docker-compose.broker.yml sidecar 定义
docker-compose.yml sidecar 定义
app/ 代码(挂载之外,agent 写不到)
app.pem App 私钥 0600 ← 唯一长期凭据
broker.config.json 白名单/策略 0600
Expand Down Expand Up @@ -125,7 +125,7 @@ git-credential-broker probe --socket /home/app/.dsh/git-broker/broker.sock \
git-credential-broker diagnose --config /volume1/docker/git-cred-broker/broker.config.json

# sidecar
docker compose -f /volume1/docker/git-cred-broker/docker-compose.broker.yml ps|logs|restart|down
docker compose -f /volume1/docker/git-cred-broker/docker-compose.yml ps|logs|restart|down

# 每次签发
tail -f /volume1/docker/git-cred-broker/log/audit.jsonl
Expand Down
8 changes: 8 additions & 0 deletions .changeset/compose-layout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'gcb-client': patch
'gcb-host': patch
---

Default `gcb probe` and `gcb logs` to `github.com`, name the rendered sidecar file
`docker-compose.yml` in the deployment directory, and document the deployment layout and the
`--user` requirement.
4 changes: 0 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,6 @@ node_modules/
# GIT_BROKER_CONFIG at it, so nothing here needs a personal copy.
/broker.config.yml
/broker.config.yaml
# `compose` output, which the README tells you to redirect to this name inside the repo. It
# carries one host's deployment paths, so it is per-deployment, not source.
/docker-compose.broker.yml

# The person's refresh token, if anyone keeps one here by mistake.
user.refresh
user.token
63 changes: 45 additions & 18 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,29 +83,55 @@ npm install -g /tmp/gcb-packs/gcb-core-*.tgz /tmp/gcb-packs/gcb-host-*.tgz
gcb --version
```

As an image, build the host stage of the root Dockerfile:
### Deploying the dev images

The two dev tags are built from the current checkout and are never pushed:

```sh
docker build --target host -t gcb-host:local .
gcb compose --image gcb-host:local > docker-compose.broker.yml
docker build --network=host --target host -t code-vaults/gcb-host:dev .
docker build --network=host --target client -t code-vaults/gcb-client:dev .
```

Then keep the rendered `sh -c` wrapper (it applies `GCB_PROXY`) and change only the final
`exec npx ...` to the binary the image installed, since the default one fetches the published package:

```yaml
command:
- sh
- -c
- |
case "${GCB_PROXY-default}" in
default) ;;
"") unset http_proxy https_proxy all_proxy no_proxy HTTP_PROXY HTTPS_PROXY ALL_PROXY NO_PROXY NODE_USE_ENV_PROXY ;;
*) export http_proxy="$GCB_PROXY" https_proxy="$GCB_PROXY" no_proxy=localhost,127.0.0.1 NODE_USE_ENV_PROXY=1 ;;
esac
exec gcb daemon --config /etc/git-cred-broker/broker.config.yml
The `host` image carries `gcb` and `gh` and runs the broker. The `client` image is the
embedding source for an idsh-style base (`COPY --from=...`); it is not a second broker.

Render a deployment for the host image on the host that runs it:

```sh
DEPLOY=/path/to/deployment
SOCKET_DIR=/path/to/shared/socket
PUSHER_UID=1000
PUSHER_GID=1000
gcb compose --dir "$DEPLOY" --socket-dir "$SOCKET_DIR" \
--user "$PUSHER_UID:$PUSHER_GID" --image code-vaults/gcb-host:dev \
> "$DEPLOY/docker-compose.yml"
```

Change only the `exec npx --yes --package … gcb` prefix to `exec gcb`, keeping `daemon --config …`:

```sh
exec gcb daemon --config /etc/git-cred-broker/broker.config.yml
```

The dev image already installs the package; do **not** keep a staged `/opt/gcb` mount, or the
image's code is ignored.
Then recreate from the deployment directory, choosing the proxy at runtime:

```sh
cd "$DEPLOY"
GCB_PROXY=http://proxy.example:7890 docker compose up -d --force-recreate
docker compose logs --tail 20 gcb
```

Verify from the pushing side:

```sh
gcb probe --repo owner/repo
```

`GCB_PROXY` is read at `docker compose up` time. An unset value keeps the host proxy variables, an
empty value goes direct, and a URL overrides them; changing it needs `--force-recreate`.

## Tests

```sh
Expand Down Expand Up @@ -185,6 +211,7 @@ scripts/postbuild.mjs the shebang and exec bit on the two bins
test/unit/ policy, JWT, provider, helper, broker, config, commands, stage, daemon
test/e2e/push.test.ts a real push over authenticated smart HTTP
test/e2e/packages.test.ts npm pack and install into separate client/host prefixes
examples/ configuration and compose snippets
examples/ configuration snippets
docker-compose.example.yml tracked sidecar example
.agents/notes/ the design, its review, the verified record, environment facts
```
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
# host the broker, with gh, for running `gcb daemon`
#
# Local build:
# docker build --target client -t ghcr.io/code-vaults/gcb-client:dev .
# docker build --target host -t ghcr.io/code-vaults/gcb-host:dev .
# docker build --target client -t code-vaults/gcb-client:dev .
# docker build --target host -t code-vaults/gcb-host:dev .

FROM node:24-slim AS build
WORKDIR /src
Expand Down
57 changes: 43 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,11 @@ Keep the checkout outside every container mount.
## GitHub actions

The action path is wired through the broker; the host image must include `gh` before these commands
can run. The container never receives the token. Build the image, then pass `--image ghcr.io/code-vaults/gcb-host:dev` to
can run. The container never receives the token. Build the image, then pass `--image code-vaults/gcb-host:dev` to
the compose command in [Quick start](#quick-start-sidecar):

```sh
docker build --target host -t ghcr.io/code-vaults/gcb-host:dev .
docker build --target host -t code-vaults/gcb-host:dev .
```

The image's `CMD` starts `gcb daemon`, but `compose` still renders the registry-fetching `npx`
Expand Down Expand Up @@ -159,16 +159,42 @@ the default sidecar fetches `gcb-host` with `npx` at start, while `--code` runs
registry access at boot.

```sh
DEPLOY=/srv/git-cred-broker # broker.config.yml, app.pem and log/ live here
DEPLOY=/srv/git-cred-broker # the deployment directory
SOCKET_DIR=/srv/git-broker-socket # a host directory the pushing container already mounts
PUSHER_UID=1000 # uid of the container that will use the socket
Comment thread
i1stG marked this conversation as resolved.
Resolved
PUSHER_GID=1000 # gid of that container

gcb init --mode sidecar --dir "$DEPLOY" \
--cert ~/Downloads/app.private-key.pem \
--allow owner/repo --client-id Iv23li… --app-id 123456

gcb compose --dir "$DEPLOY" --socket-dir "$SOCKET_DIR" \
> "$DEPLOY/docker-compose.broker.yml"
docker compose -f "$DEPLOY/docker-compose.broker.yml" up -d
--user "$PUSHER_UID:$PUSHER_GID" > "$DEPLOY/docker-compose.yml"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
cd "$DEPLOY" && docker compose up -d
```

`--user` must be the uid:gid of the container that will use the socket; the socket is mode 0660 and
created by that identity. A sidecar running as a different user cannot connect. If the pusher runs
as the same user as the sidecar, the default is correct.

`init` writes the configuration and key as the invoking user, with the key mode 0600. If
`authorize` has run, it also writes `user.refresh` beside the key, mode 0600 and **read-write**:
the broker rotates that token on every exchange. The sidecar's user must read the config, key and
`user.refresh`, and write `log/` (including an existing audit file). If `PUSHER_UID` differs from
the user running `init`/`authorize`, either run both as that uid or `chown` the config, key,
`user.refresh`, the log directory and its audit file to it; re-run the `chown` after `authorize`
creates `user.refresh`. Keep the key and `user.refresh` at 0600; do not make them world-readable.

A deployment directory holds the config, key, log and compose file; `app/` is only a convention
for a staged commit, and `--code` accepts any directory outside every mount.

```text
$DEPLOY/
broker.config.yml
app.pem
log/
docker-compose.yml
app/ optional staged commit
```

Nothing above has to name the shared directories. A host process asks the container runtime — docker,
Expand All @@ -186,7 +212,8 @@ instead, run `gcb stage --to "$DEPLOY/app"`, install its dependencies in that di
root you intend to export, or pass `--repo <path>`: it defaults to the current directory and exports
that repository's `HEAD`, never the working tree. `stage` is export-only: it does not install
dependencies or build, and it never runs the commit's code. It leaves `STAGED.json` beside the code
recording the commit.
recording the commit; `--code` does not require that file, and the directory may have any name outside
the mounts.

## Quick start: host process

Expand Down Expand Up @@ -272,7 +299,8 @@ app's **private key**, so run it where the key is. On a host-process deployment
a sidecar it is inside the container:

```sh
docker compose -f docker-compose.broker.yml exec gcb \
DEPLOY=/srv/git-cred-broker
cd "$DEPLOY" && docker compose exec gcb \
npx --yes --package gcb-host gcb \
diagnose --config /etc/git-cred-broker/broker.config.yml
```
Expand Down Expand Up @@ -583,7 +611,8 @@ packages/client/src/ credential helper, setup, probe, logs, pr, gh
packages/host/src/ YAML config, providers, broker, daemon and host commands
packages/*/bin/ the two installed gcb bins (client and host)
test/ unit tests, plus an end-to-end push over authenticated smart HTTP
examples/ configuration and compose snippets
examples/ configuration snippets
docker-compose.example.yml tracked sidecar example
DEVELOPMENT.md building from source, tests, releasing
.agents/notes/ the design, its review, and the measured environment facts
```
Expand All @@ -605,21 +634,21 @@ The contract between the pushing container and the broker is deliberately small:

## Images

Two GHCR repositories mirror the two packages:
Two local dev images mirror the two packages; the release publishes the same repositories on GHCR:

| Image | Contents | Use |
|---|---|---|
| `ghcr.io/code-vaults/gcb-client:dev` | `gcb` client and Node runtime only | idsh base / `COPY --from` |
| `ghcr.io/code-vaults/gcb-host:dev` | broker, `gh`, and broker dependencies | run `gcb daemon` on the host |
| `code-vaults/gcb-client:dev` | `gcb` client and Node runtime only | idsh base / `COPY --from` |
| `code-vaults/gcb-host:dev` | broker, `gh`, and broker dependencies | run `gcb daemon` on the host |

The release publishes both repositories with `latest`, `<version>` and `sha-<sha>` tags. The
`:dev` tags are local build tags and are not published; do not pull them from GHCR.

For local development, build and reuse the `:dev` tags:

```sh
docker build --target client -t ghcr.io/code-vaults/gcb-client:dev .
docker build --target host -t ghcr.io/code-vaults/gcb-host:dev .
docker build --target client -t code-vaults/gcb-client:dev .
docker build --target host -t code-vaults/gcb-host:dev .
```

While the image exists locally, Docker uses it instead of pulling from the registry. Another machine
Expand All @@ -629,7 +658,7 @@ For idsh's dev image, name the client image as a stage and copy only the gcb pac

```dockerfile
ARG BASE=devockr/idsh:base
ARG GCB_CLIENT_IMAGE=ghcr.io/code-vaults/gcb-client:dev
ARG GCB_CLIENT_IMAGE=code-vaults/gcb-client:dev
FROM ${GCB_CLIENT_IMAGE} AS gcb-client
FROM ${BASE}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
# `gcb compose` prints the sidecar in 1b, already filled in for your paths:
#
# gcb compose --socket-dir <a host directory the DSH container mounts> \
# > docker-compose.broker.yml
# > docker-compose.yml
#
# Then, from that directory: docker compose up -d
#
# Generate it rather than retyping it. The mount targets inside the container must match the
# paths `init --mode sidecar` recorded, and both commands read them from one definition, so a
Expand Down Expand Up @@ -55,7 +57,8 @@ services:
container_name: git-cred-broker
restart: unless-stopped
# Must match the uid/gid of the container that will use the socket, or it is unusable.
user: "1026:100"
# `compose --user "<pusher uid>:<pusher gid>"` renders this from the deployment.
user: "1000:1000"
read_only: true
tmpfs:
# npx needs somewhere writable for its cache; /tmp is a tmpfs above.
Expand Down
8 changes: 5 additions & 3 deletions packages/client/src/commands/logs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const LOGS_USAGE = `Usage: gcb logs [options]
Read a workflow job log through the broker, without the credential ever reaching this side.

--socket <path> Broker socket (default: $GIT_BROKER_SOCKET or the socket file)
--host <host> Host as configured, e.g. github.com [required]
--host <host> Host as configured (default: github.com)
--repo <owner/name> Repository the job belongs to [required]
--job <id> Workflow job id, which is also the check run id [required]
--proxy <url> Use this proxy for this request
Expand All @@ -49,13 +49,15 @@ export async function runLogs(argv: readonly string[]): Promise<number> {
}

const socketPath = resolveSocketPath(args.value('socket'), process.env);
const host = args.value('host');
const hostArg = args.value('host');
if (args.has('host') && hostArg === undefined) fail('--host needs a value');
if (hostArg === '') fail('--host needs a value');
const host = hostArg ?? 'github.com';
const repo = args.value('repo');
const rawJob = args.value('job');
if (!socketPath) {
fail('no broker socket: pass --socket, set GIT_BROKER_SOCKET, or run `gcb setup`');
}
if (!host) fail('--host is required (e.g. github.com)');
if (!repo) fail('--repo is required (e.g. owner/name)');
if (!rawJob) fail('--job is required (the workflow job id, which is also the check run id)');
const jobId = Number(rawJob);
Expand Down
8 changes: 5 additions & 3 deletions packages/client/src/commands/probe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const PROBE_USAGE = `Usage: gcb probe [options]
Ask the broker for a credential without pushing. Prints a fingerprint, never the credential.

--socket <path> Broker socket (default: $GIT_BROKER_SOCKET or the socket file)
--host <host> Host as git reports it, e.g. github.com [required]
--host <host> Host as git reports it (default: github.com)
--repo <owner/name> Repository path [required]
--protocol <p> https (default) or http for a host that opted in
--proxy <url> Use this proxy for this request
Expand All @@ -40,12 +40,14 @@ export async function runProbe(argv: readonly string[]): Promise<number> {
}

const socketPath = resolveSocketPath(args.value('socket'), process.env);
const host = args.value('host');
const hostArg = args.value('host');
if (args.has('host') && hostArg === undefined) fail('--host needs a value');
if (hostArg === '') fail('--host needs a value');
const host = hostArg ?? 'github.com';
const repo = args.value('repo');
if (!socketPath) {
fail('no broker socket: pass --socket, set GIT_BROKER_SOCKET, or run `gcb setup`');
}
if (!host) fail('--host is required (e.g. github.com)');
if (!repo) fail('--repo is required (e.g. owner/name)');

const protocol = args.value('protocol') ?? 'https';
Expand Down
13 changes: 10 additions & 3 deletions packages/host/src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
type DeploymentMode,
} from '../config.ts';
import { parseAllowEntry } from '../policy.ts';

import type { BrokerConfig, GithubAppHostConfig } from 'gcb-core';
import {
brokerPaths,
Expand All @@ -39,6 +40,11 @@ import {
import type { BrokerPaths } from './deployment.ts';
import { fail, insideMountedPath, isInsideContainer, parseArgs, say, writeSecretFile } from 'gcb-core';

/** Quote one value as a single shell word, including paths with spaces. */
function shellQuote(value: string): string {
return `'${value.replaceAll("'", "'\\''")}'`;
}

/** The environment `init` reasons about, injectable so the guard rails can be tested. */
export interface InitEnvironment {
/** Whether this process is inside the container. */
Expand Down Expand Up @@ -604,7 +610,7 @@ export async function runInit(argv: readonly string[]): Promise<number> {
if (result.previous) {
say('Restart the broker to pick this up:');
if (result.mode === 'sidecar') {
say(' docker compose -f docker-compose.broker.yml up -d # sidecar');
say(` cd ${shellQuote(path.dirname(result.keyPath))} && docker compose up -d # sidecar`);
} else {
say(` gcb daemon --config ${result.configPath} # host process`);
}
Expand All @@ -626,9 +632,10 @@ export async function runInit(argv: readonly string[]): Promise<number> {
if (result.mode === 'sidecar') {
say('Then start the sidecar, which mounts those paths exactly as recorded:');
say(
` gcb compose --dir ${path.dirname(result.keyPath)} --config ${result.configPath} --socket-dir <a directory the pusher mounts> \\`,
` gcb compose --dir ${shellQuote(path.dirname(result.keyPath))} --config ${shellQuote(result.configPath)} --socket-dir <a directory the pusher mounts> --user <pusher uid:gid> \\`,
);
say(' > docker-compose.broker.yml && docker compose -f docker-compose.broker.yml up -d');
say(` > ${shellQuote(path.join(path.dirname(result.keyPath), 'docker-compose.yml'))}`);
say(` cd ${shellQuote(path.dirname(result.keyPath))} && docker compose up -d`);
} else {
say('Then start the broker as a host process:');
say(` gcb daemon --config ${result.configPath} --check # validate, bind nothing`);
Expand Down
Loading
Loading