feat: passthrough dialect (cli-guard) → ward ssh + tailscale ssh #158

Closed
opened 2026-06-19 07:11:13 +00:00 by coilyco-ops · 4 comments
Member

feat: passthrough dialect (cli-guard) → ward ssh + tailscale ssh

Guard ssh (and tailscale ssh) as a default-allow passthrough so agents on workstations get fast, gated ssh egress. Two repos; cli-guard is the prerequisite.

Part 1 — cli-guard engine (DONE, ready to merge)

Implemented and pushed on branch feat/passthrough-ssh-dialect (1 commit off main, all hooks/tests/lint/godoc green):
https://forgejo.coilysiren.me/coilyco-flight-deck/cli-guard/compare/main...feat/passthrough-ssh-dialect

  • passthrough <bin> [prefix...] — sugar for exec <bin> + an implicit can run "*" funnel (first arg = binary, rest = fixed leading argv). Mutually exclusive with exec/can run.
  • never pass <token...> / never pass when <sel> is <glob...> / only pass when <sel> is <glob...> — wrap-level guards enforced on every leaf.
  • shell <cmd> selector source — trimmed stdout of a command exec'd directly (no shell interpretation) once at invocation; resolver error fails closed. The host gate.
  • New docs/passthrough.md; docs/execverb.md + docs/FEATURES.md updated; tests in cli/execverb/execverb_test.go.

Task: open a PR from that branch to main, confirm CI green, merge (auto minor release, e.g. v0.45.0).

Part 2 — ward rollout (release-gated; do after Part 1 releases)

In coilyco-flight-deck/ward, once the new cli-guard tag exists:

  1. Bump Makefile REF from v0.44.0 to the new tag.
  2. Add cmd/ward-kdl/ward-kdl.ssh.guardfile.kdl:
wrap ward-kdl ssh {
    passthrough ssh
    never pass rm {
        describe "rm over ssh is blocked at the funnel; use the local shell"
    }
    only pass when shell hostname is "*macbook*" "*-laptop" "*windows*" "*tower*" {
        describe "only workstations may originate ssh; servers fail closed"
    }
}
  1. Add cmd/ward-kdl/ward-kdl.tailscale-ssh.guardfile.kdl (same body, binary tailscale with ssh prefix):
wrap ward-kdl tailscale ssh {
    passthrough tailscale ssh
    never pass rm {
        describe "rm over ssh is blocked at the funnel; use the local shell"
    }
    only pass when shell hostname is "*macbook*" "*-laptop" "*windows*" "*tower*" {
        describe "only workstations may originate ssh; servers fail closed"
    }
}
  1. Rename the existing tailscale HTTP surface: in cmd/ward-kdl/ward-kdl.tailscale.guardfile.kdl, change the wrap path wrap ward-kdl ops tailscale {wrap ward-kdl tailscale api { (so tailscale groups under its own name: api = guarded HTTP, ssh = passthrough). No external callers of ops tailscale exist (only the guardfile + its generated doc).
  2. make build-ward-kdl (driver discovers the new guardfiles + regenerates the .guardfile.md docs under docs/; relocate per the Makefile). Update the tailscale .guardfile.md heading from ops tailscale to tailscale api.
  3. pre-commit run --all-files, land.

Host-gate notes (confirmed)

  • Globs match local hostname (note kais-macbook-pro-2.local carries .local, so *macbook* not mac*). Admits mac/laptop/windows/tower; denies kai-server, ser8, and k8s/service nodes (fail closed).
  • never pass rm is a speed-bump, not a boundary (ssh runs opaque remote strings). The host gate is the real boundary. Full rationale in docs/passthrough.md.
  • Re-introduces a guarded agent ssh egress surface (coily previously removed its ssh passthrough); the host gate is load-bearing.
## feat: passthrough dialect (cli-guard) → ward ssh + tailscale ssh Guard `ssh` (and `tailscale ssh`) as a default-allow passthrough so agents on workstations get fast, gated ssh egress. Two repos; cli-guard is the prerequisite. ### Part 1 — cli-guard engine (DONE, ready to merge) Implemented and pushed on branch **`feat/passthrough-ssh-dialect`** (1 commit off main, all hooks/tests/lint/godoc green): https://forgejo.coilysiren.me/coilyco-flight-deck/cli-guard/compare/main...feat/passthrough-ssh-dialect - `passthrough <bin> [prefix...]` — sugar for `exec <bin>` + an implicit `can run "*"` funnel (first arg = binary, rest = fixed leading argv). Mutually exclusive with `exec`/`can run`. - `never pass <token...>` / `never pass when <sel> is <glob...>` / `only pass when <sel> is <glob...>` — wrap-level guards enforced on every leaf. - `shell <cmd>` selector source — trimmed stdout of a command exec'd directly (no shell interpretation) once at invocation; resolver error fails closed. The host gate. - New `docs/passthrough.md`; `docs/execverb.md` + `docs/FEATURES.md` updated; tests in `cli/execverb/execverb_test.go`. **Task:** open a PR from that branch to `main`, confirm CI green, merge (auto minor release, e.g. v0.45.0). ### Part 2 — ward rollout (release-gated; do after Part 1 releases) In `coilyco-flight-deck/ward`, once the new cli-guard tag exists: 1. Bump `Makefile` `REF` from `v0.44.0` to the new tag. 2. Add `cmd/ward-kdl/ward-kdl.ssh.guardfile.kdl`: ```kdl wrap ward-kdl ssh { passthrough ssh never pass rm { describe "rm over ssh is blocked at the funnel; use the local shell" } only pass when shell hostname is "*macbook*" "*-laptop" "*windows*" "*tower*" { describe "only workstations may originate ssh; servers fail closed" } } ``` 3. Add `cmd/ward-kdl/ward-kdl.tailscale-ssh.guardfile.kdl` (same body, binary `tailscale` with `ssh` prefix): ```kdl wrap ward-kdl tailscale ssh { passthrough tailscale ssh never pass rm { describe "rm over ssh is blocked at the funnel; use the local shell" } only pass when shell hostname is "*macbook*" "*-laptop" "*windows*" "*tower*" { describe "only workstations may originate ssh; servers fail closed" } } ``` 4. Rename the existing tailscale HTTP surface: in `cmd/ward-kdl/ward-kdl.tailscale.guardfile.kdl`, change the wrap path `wrap ward-kdl ops tailscale {` → `wrap ward-kdl tailscale api {` (so tailscale groups under its own name: `api` = guarded HTTP, `ssh` = passthrough). No external callers of `ops tailscale` exist (only the guardfile + its generated doc). 5. `make build-ward-kdl` (driver discovers the new guardfiles + regenerates the `.guardfile.md` docs under `docs/`; relocate per the Makefile). Update the tailscale `.guardfile.md` heading from `ops tailscale` to `tailscale api`. 6. `pre-commit run --all-files`, land. ### Host-gate notes (confirmed) - Globs match local `hostname` (note `kais-macbook-pro-2.local` carries `.local`, so `*macbook*` not `mac*`). Admits mac/laptop/windows/tower; denies `kai-server`, `ser8`, and k8s/service nodes (fail closed). - `never pass rm` is a speed-bump, not a boundary (ssh runs opaque remote strings). The host gate is the real boundary. Full rationale in `docs/passthrough.md`. - Re-introduces a guarded agent ssh egress surface (coily previously removed its ssh passthrough); the host gate is load-bearing.
Author
Member

🔒 Reserved by ward agent claude — container ward-cli-guard-issue-158-claude-2f02881b on host kais-macbook-pro-2.local is carrying this issue (reserved 2026-06-19T07:11:39Z). Concurrent ward agent runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); --force overrides.

— Claude (she/her), via ward agent

<!-- ward-agent-reservation --> 🔒 Reserved by `ward agent claude` — container `ward-cli-guard-issue-158-claude-2f02881b` on host `kais-macbook-pro-2.local` is carrying this issue (reserved 2026-06-19T07:11:39Z). Concurrent `ward agent` runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); `--force` overrides. <!-- ward-agent-signature --> — Claude (she/her), via `ward agent`
Author
Member

🔒 Reserved by ward agent --driver claude — container ward-cli-guard-issue-158-claude-48be8e0c on host kais-macbook-pro-2.local is carrying this issue (reserved 2026-06-24T06:43:59Z). Concurrent ward agent runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); --force overrides.

— Claude (she/her), via ward agent

<!-- ward-agent-reservation --> 🔒 Reserved by `ward agent --driver claude` — container `ward-cli-guard-issue-158-claude-48be8e0c` on host `kais-macbook-pro-2.local` is carrying this issue (reserved 2026-06-24T06:43:59Z). Concurrent `ward agent` runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); `--force` overrides. <!-- ward-agent-signature --> — Claude (she/her), via `ward agent`
Author
Member

🔒 Reserved by ward agent --driver claude — container ward-cli-guard-issue-158-claude-8b900595 on host kais-macbook-pro-2.local is carrying this issue (reserved 2026-06-24T08:36:14Z). Concurrent ward agent runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); --force overrides.

— Claude (she/her), via ward agent

<!-- ward-agent-reservation --> 🔒 Reserved by `ward agent --driver claude` — container `ward-cli-guard-issue-158-claude-8b900595` on host `kais-macbook-pro-2.local` is carrying this issue (reserved 2026-06-24T08:36:14Z). Concurrent `ward agent` runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); `--force` overrides. <!-- ward-agent-signature --> — Claude (she/her), via `ward agent`
Owner

Part 1 (cli-guard engine) landed on main as merge 778fbd1 — the passthrough <bin> dialect plus the wrap-level host-gate guards (never pass / only pass / shell <cmd> selector). The push auto-cuts the minor release (the new tag ward bumps to).

This merge reconciled the branch against the allow inspect-list sugar that landed on main in parallel (4b09142): the merged Guardfile carries both wrap-level guard sets, wildcardLeaf threads the HostResolver through the shared funnel path, and Parse/applyNode were factored into helpers to hold gocyclo under the repo's complexity-12 ceiling. Verified green: build, vet, golangci-lint (cli/execverb: 0 issues), godoc pinned, full unit suite (the cli/sandbox seccomp/ptrace tests need kernel caps absent in the build container and fail identically on clean main).

Part 2 (ward rollout) is not in this change — it lives in coilyco-flight-deck/ward, is release-gated on the tag this push produces, and is tracked by the Part 2 checklist in the issue body. Closing #158 on the Part 1 deliverable per the task; the ward-side rollout proceeds as its own work once the tag is published.

Part 1 (cli-guard engine) landed on `main` as merge 778fbd1 — the `passthrough <bin>` dialect plus the wrap-level host-gate guards (`never pass` / `only pass` / `shell <cmd>` selector). The push auto-cuts the minor release (the new tag ward bumps to). This merge reconciled the branch against the `allow` inspect-list sugar that landed on main in parallel (4b09142): the merged Guardfile carries both wrap-level guard sets, `wildcardLeaf` threads the `HostResolver` through the shared funnel path, and `Parse`/`applyNode` were factored into helpers to hold gocyclo under the repo's complexity-12 ceiling. Verified green: build, vet, golangci-lint (`cli/execverb`: 0 issues), godoc pinned, full unit suite (the `cli/sandbox` seccomp/ptrace tests need kernel caps absent in the build container and fail identically on clean main). **Part 2 (ward rollout) is not in this change** — it lives in `coilyco-flight-deck/ward`, is release-gated on the tag this push produces, and is tracked by the Part 2 checklist in the issue body. Closing #158 on the Part 1 deliverable per the task; the ward-side rollout proceeds as its own work once the tag is published.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
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
coilyco-flight-deck/cli-guard#158
No description provided.