Skip to content

Gemba Remote foundations: M1 OSS core + M2 federation + M3 hosted plane + ASDD Phase 1 - #2

Merged
MikeBengtson merged 91 commits into
mainfrom
feat/gemba-remote-foundations
May 13, 2026
Merged

MikeBengtson merged 91 commits into
mainfrom
feat/gemba-remote-foundations

Conversation

@MikeBengtson

Copy link
Copy Markdown
Collaborator

Summary

Foundational implementation of the Gemba Remote epic (gm-o9t8) and Phase 1 of ASDD (gm-v0sp). 38 commits across four milestones plus the Spec Kit interception layer.

Milestone Bead State
M1 — Single-user OSS core gm-o9t8.1 ✅
M2 — Federation and data ownership gm-o9t8.2 ✅
M3 — Hosted control plane gm-o9t8.3 ✅
ASDD Phase 1 — Spec Kit interception gm-v0sp.15–.19 ✅

Highlights

M1 — Single-user OSS core

  • gemba login / whoami + XDG credstore (0600 perms)
  • Typed Go client (oapi-codegen v2.7 over 47 endpoints)
  • Embedded Dolt sql-server supervisor (subprocess + backoff + health probe)
  • gemba bead CRUD (existing enrichment verbs nested under gemba bead enrichment)
  • gemba run / gemba agent dispatch / gemba logs -f with SSE consumer
  • gemba diff streaming git diff from server
  • gemba status mode-aware dashboard
  • gemba config show with --server / GEMBA_SERVER / config.toml / default precedence
  • web/dist/.gitkeep committed so fresh-checkout go build ./... works zero-config

M2 — Federation and data ownership

  • DoltHub remote push/pull via secrets vault
  • Server-side git canonicalization + push-back
  • Reconstruct-from-remotes capstone (git clone $GIT && dolt clone $DOLTHUB && gemba init --restore)

M3 — Hosted control plane

  • Multi-tenant orchestration plane (t-<8-chars> tenant IDs, t-XXXX:slug wsid format with bare-form backwards compat)
  • Per-workspace secrets vault (bbolt + AES-256-GCM, per-workspace DEK, KEK from GEMBA_VAULT_KEY)
  • Append-only audit log (Ed25519-signed, hash-chained, daily JSONL rotation)
  • GitHub OAuth: app config + device flow + callback + token storage (argon2id) + rotation/revocation
  • MicroVM provisioning: Firecracker supervisor (Linux) + subprocess fallback (macOS dev); volume mounts; MMDS secret injection; audit events
  • Buildroot vm-image pipeline (kernel + rootfs + gemba-vm-init); macOS-friendly make vm-image skip
  • Egress allowlist: per-workspace rules + 11-host default + glob host-pattern matcher; nftables Linux enforcement + non-Linux no-op
  • internal/dispatch Runtime strategy: native (preserved) + firecracker

ASDD Phase 1 — Spec Kit interception

  • gemba-shipped /tasks + /implement slash command templates that route through bd / gemba spec reconcile
  • PreToolUse hook rejects writes to tasks.md / todo.md (Write/Edit/NotebookEdit/Bash); preserves rejected payload via GEMBA_REJECTED_PAYLOAD for the future gm-v0sp.21 analyzer
  • Constitution key spec_strict_no_tasks_md (inherits spec_strict) + gemba constitution lint --strict reports findings
  • gemba spec preflight refuses /implement when tasks.md exists; --allow-tasks-md bypass with audit event
  • CLAUDE.md ASDD stanza in gemba init --asdd / --update-claude-md

Test plan

  • go build ./... clean
  • go test ./internal/... — all subpackages pass on macOS (firecracker via fallback; nftables tests gated on linux+root)
  • Cross-compile: GOOS=linux go build -tags linux ./internal/adapter/firecracker/...
  • OpenAPI docs/embedded byte-identical (TestOpenAPISpec_DocsCopyMatches)
  • Manual: gemba serve --embedded-dolt end-to-end against fresh tempdir
  • Manual: gemba login --github against a registered OAuth app
  • Manual: VM dispatch on a Linux + KVM host
  • Code review

Notes for reviewers

  • The branch interleaves work from this session and a parallel agent session — commit history reflects the actual implementation order rather than a clean linear narrative. Squashing on merge is fine if you prefer.
  • ASDD Phase 1.5 (gm-v0sp.21 analyzer) is filed but not implemented — it consumes the rejected-payload seam this PR ships.
  • M4 (productization — billing, SSO, console) intentionally deferred; epics open under gm-o9t8.4.

🤖 Generated with Claude Code

MikeBengtson and others added 30 commits May 12, 2026 05:48
- internal/auth/credstore: XDG-aware multi-server credential store
  with 0600 file mode and atomic temp+rename writes
- internal/cli/login.go: gemba login + whoami subcommands
- internal/server/whoami.go: GET /api/whoami behind auth middleware
- OpenAPI documents /api/whoami with WhoamiResponse + Unauthorized
- Adds golang.org/x/term as direct dep for masked TTY prompt

Subject is hardcoded "operator" until per-request subject is threaded
through the auth middleware (follow-up).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- 12 paths under /api/v1/workspaces/{wsid}/ covering spec/decision/
  constitution lint and the no-args status convenience endpoint
- X-GEMBA-Confirm declared once in components.parameters (UUID v4
  pattern) and $ref'd from every mutating endpoint
- SSE schema for /watch documented via text/event-stream + description
  enumerating bead.status, spec.drift, reconcile.complete event types
- Stub handlers (specs.go, decisions.go, labels.go, status.go) return
  501 not_implemented JSON envelope; all auth-gated; nonce-required
  on writes via requireConfirmNonce wrapper
- 524 tests pass across 5 server packages

Routes registered in router.go (the actual mounting point) rather
than bootstrap.go as the brief suggested.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- internal/adapter/dolt/supervisor: spawns dolt sql-server on a
  loopback TCP port; blocks until SELECT 1 succeeds; 5s health
  probe; exponential backoff (500ms→30s, max 5 consecutive fails)
- internal/cli/serve.go: --embedded-dolt (default true unless
  --dolt-url set) and --dolt-data-dir flags; supervisor starts
  before HTTP listener; deferred Stop runs after srv.Shutdown
  so HTTP drains against live dolt
- internal/server/readyz.go: /api/readyz returns 503 with
  checks.dolt:"down" when supervisor is not Ready; 200 with
  checks.dolt:"ok" when up
- 729 tests pass; real-dolt integration test gated on dolt-in-PATH

Went TCP (not UNIX socket) because dolt --socket support has
been historically inconsistent. Supervisor runs but bd adaptor
still resolves Dolt via --dolt-url; wiring is gm-o9t8.1.7.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resolves conflicts in docs/api/openapi.json and
internal/server/openapi/openapi.json by combining /api/whoami
from gm-o9t8.1.1.2 with the 12 /api/v1/workspaces/{wsid}/...
paths from gm-o9t8.14. Both files byte-identical post-merge
(TestOpenAPISpec_DocsCopyMatches gate satisfied).

47 total paths (34 existing + whoami + 12 govern).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- internal/client/gen: oapi-codegen v2.7.0 output covering all 47 endpoints
- internal/client/client.go: wrapper injects bearer from credstore,
  X-GEMBA-Confirm UUID v4 on POST/PATCH/PUT/DELETE, User-Agent
- internal/client/errors.go: maps Error + UnauthorizedResponse envelopes
  to sentinels (ErrUnauthorized/NotFound/Conflict/NotImplemented/
  BadRequest) and a generic *ServerError fallback
- internal/client/preprocess_spec.go: rewrites OpenAPI 3.1 type-array
  nullables to 3.0.3 nullable:true since oapi-codegen v2.x doesn't
  yet ingest 3.1; encapsulated in go generate
- gemba whoami rewired to typed client; gemba login stays ad-hoc
  (chicken-and-egg: no credential exists yet at login time)

Adds google/uuid as direct dep; oapi-codegen + runtime via go.mod.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Naming reconciliation: existing gemba bead subverbs (list, show, status,
size, extract, backfill, targets, concepts) moved under gemba bead
enrichment subgroup; top-level gemba bead now owns the CRUD verbs.

- internal/cli/bead_crud.go: create/list/show/update/close/note verbs
  via direct JSON requests through internal/client's auth + nonce
  pipeline (typed-client regeneration deferred to follow-up)
- internal/cli/bead.go: registers bead_crud verbs alongside the new
  enrichment subgroup; existing tests mechanically updated to nest
  under enrichment
- internal/client/work_items.go: hand-written wrappers (CreateWorkItem,
  ListWorkItems, GetWorkItem, PatchWorkItem, NoteWorkItem) riding the
  same RequestEditor pipeline as the generated client
- internal/server/work_items_notes.go: 501 stub for POST notes endpoint
- OpenAPI updated: POST /api/work-items (createWorkItem), PATCH
  /api/work-items/{id} (updateWorkItem), POST /api/work-items/{id}/notes
  (noteWorkItem); CreateWorkItemRequest, WorkItemPatch, NotImplemented
  components added

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds TestSchemaRoundTrip + TestSchemaRoundTrip_RealDolt covering the
M1 acceptance criterion. Three table-driven subtests verify create →
list → show preserves the CLI-visible bd-bead shape (id, title,
status, labels, priority, relationships, created_at, updated_at,
kind, state_category).

The real-dolt path is skipped with a clear pointer to the wire-up
follow-up: this worktree lacks both an in-process dolt sql-server
supervisor and CLI bead-CRUD wrappers, so the round-trip is wired
through testadaptors.FakeWorkPlane with an in-memory store that
mimics what a write-through bd/dolt adaptor would do. Reference
shape is anchored on gm-o9t8 drawn from ~/gt/gemba/.beads/issues.jsonl.
…ring

- New `gemba status` subcommand renders the Ad-hoc dashboard template
  from gm-o9t8.8 using text/tabwriter for alignment
- Bare `gemba` dispatches via root.RunE → newStatusCmd().RunE
- detectMode(workspaceRoot) is a stub returning "ad-hoc"; other three
  modes (Scaffolded/Governed/Reverse-governed) deferred to ASDD epic
  gm-v0sp (spec.md + .lockfile.json machinery not yet built)
- Data path probes /api/v1/workspaces/_/status (501 stub today) and
  falls back to /api/work-items?status={ready,in_progress,open,closed}
- Flags: --server, --json, --mode
- 7 new tests in status_test.go; all 248 CLI tests pass
MikeBengtson and others added 28 commits May 12, 2026 13:25
cmd/gemba serve now constructs a MemStore-backed tenant.Store and
seeds DefaultTenant via Migrate so GET /api/v1/tenants/t-default
returns metadata out of the box in single-user mode. Dolt-backed
SQLStore + real tenant rows land with the OAuth follow-up
(gm-o9t8.3.5).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Introduces internal/egress: Rule data model, MemStore + SQLStore
backends, hardcoded Defaults() baseline allow-list with terminal
default-deny, and a label-glob host-pattern matcher (* = one label,
** = one-or-more labels). Defaults are never persisted and never
deletable; workspace rules with higher priority shadow them via
Effective().

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds internal/adapter/firecracker with a Supervisor interface mirroring
the dolt supervisor's Start/Stop/Wait shape, factoring VMs as the unit
of lifecycle instead of a single long-lived child.

- Build-tag layout: firecracker.go (shared types) + kvm_{linux,other}.go
  + firecracker_linux.go (real firecracker-go-sdk path) + firecracker_
  fallback.go (non-Linux bash -c shim) + fallback_{unix,windows}.go for
  process-group / signal helpers.
- KVMAvailable() checks /dev/kvm device-node mode + open(O_RDONLY); off-
  Linux it's a const false.
- Linux path uses firecracker-go-sdk v1.0.0: Config{KernelImagePath,
  Drives, MachineCfg{Vcpu, Mem}}, MMDS for secret injection via
  machine.SetMetadata, tap-per-VM naming on the gemba0 bridge with dev-
  mode network skip (full bring-up belongs to gm-o9t8.3.6.*).
- Fallback spawns bash -c (default exec cat) with Spec.Secrets injected
  as env, single-shot Wait goroutine for zombie-free reaping, polite
  SIGTERM via process-group then ctx-cancel SIGKILL on timeout.
- Tests run on macOS/CI: lifecycle (Start/Stop/idempotent-Stop/Wait),
  secret env injection (subprocess self-asserts), and concurrent-VM ID
  uniqueness + independent teardown. Real-Firecracker test placeholder
  gates behind requireKVM and currently t.Skip's pending gm-o9t8.3.2.2
  kernel+rootfs artifacts.

Builds clean on macOS (fallback path) and cross-compiles with
GOOS=linux -tags linux. go test ./internal/adapter/firecracker/...
passes (4 tests).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add server-side scaffolding for GitHub OAuth:

- ServeConfig gains OAuthGitHubClientID, OAuthGitHubClientSecret,
  MultiTenantMode. Flags --oauth-github-client-id /
  --oauth-github-client-secret / --multi-tenant; env vars
  GEMBA_OAUTH_GITHUB_CLIENT_ID / GEMBA_OAUTH_GITHUB_CLIENT_SECRET /
  GEMBA_MULTI_TENANT. OAuthEnabled() derived from both being set.

- internal/auth/oauth: new package with Config / OAuth /
  CallbackPath. New() validates the config matrix (empty =
  ErrUnconfigured; partial = ErrPartialConfig). DisabledHandler
  returns 503 with stable error.code "oauth_not_configured" so
  callers distinguish "server doesn't speak OAuth" from auth errors.

- /api/v1/auth/oauth/github/callback registered on the router.
  503 when OAuth is unconfigured, otherwise serves the device-flow
  status placeholder.

- ValidateOAuthForMultiTenant runs at boot and refuses to start a
  multi-tenant rig without OAuth.

- Token-storage shape: internal/server/oauth_tokenstore.go.
  auth_tokens (tenant_id, github_id, device_label, hash_phc,
  created_at) with PRIMARY KEY (tenant_id, device_label). MemTokenStore
  for tests + single-user fallback; SQLTokenStore for the Dolt path.
  argon2id PHC strings via internal/auth.HashToken; plaintext is
  never persisted.

Tests:
- internal/auth/oauth/oauth_test.go: TestNew_ConfigMatrix,
  TestCallbackURL, TestDisabledHandler, TestFetchUserAt,
  TestFetchUserAt_Rejected.
- internal/config/bind_test.go: TestOAuthConfigMatrix.

go build ./... + go test ./internal/config/... ./internal/auth/oauth/...
pass.

Backwards compat: single-user PAT path untouched. OAuth is opt-in;
unconfigured = silent 503 on the OAuth routes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds /api/v1/workspaces/{wsid}/egress-rules (GET/POST), .../{id}
(DELETE), and .../effective (GET) to the tenant-aware route group.
POST/DELETE are nonce-gated; defaults are immutable through the
surface. Wires an optional EgressAuditor that emits the new
audit.EventEgressRuleCreate / .Delete events declared in
internal/server/audit/types.go. OpenAPI documents all four paths;
docs/api/openapi.json is regenerated from the embed source.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add the write half of the tenant admin surface: POST/PATCH/DELETE on
/api/v1/tenants plus a paginated GET listing. Mutations are nonce-gated
through the shared X-GEMBA-Confirm cache; admin gating is the v1 stub
(any valid bearer == admin) until gm-o9t8.3.5.3 lands OAuth-bound roles.

- Extend tenant.Store with Update / Delete / paginated List(opts) and
  add ErrTenantExists for create-collision distinguishability.
- Add audit.Auditor (Append + Query) with FileAuditor + MemAuditor
  implementations and tenant.create/update/delete event kinds so admin
  handlers emit a queryable audit trail.
- Router wires AttachAuditor + AttachTenantWorkspaceCount hooks; the
  delete handler refuses orphaning workspaces unless ?force=1, and
  t-default is permanently undeletable.
- OpenAPI: add four new paths, hoist tenant schema into components.
CLI side:
- internal/cli/login.go: add --github + --github-client-id flags. When
  --github is passed, route to runGitHubLogin instead of the PAT path.
  Client id resolution: --github-client-id > GEMBA_OAUTH_GITHUB_CLIENT_ID.

- internal/cli/login_github.go: three-phase implementation.
  1. requestDeviceCode: POST https://github.com/login/device/code with
     client_id + scope=repo,user:email. Print the user code + the
     verification URL (defaults to https://github.com/login/device).
  2. pollForGitHubToken: POST https://github.com/login/oauth/access_token
     every <interval> seconds. authorization_pending = keep polling;
     slow_down = bump the interval by +5s; ctx cancel = clean exit.
  3. exchangeGitHubTokenWithGemba: POST gemba's
     /api/v1/auth/oauth/github/login with the GitHub access token;
     receive the gemba bearer + tenant id; persist via credstore.

Server side:
- POST /api/v1/auth/oauth/github/login mounted outside the apiAuth
  block (the CLI hits it BEFORE it has a gemba bearer). Handler:
  - validates the GitHub token via GET https://api.github.com/user
    (test seam: SetGitHubUserFetcherForTest).
  - provisions a tenant on first login (tenant.NewID + Create); on
    subsequent logins resolves the existing row by github_id.
  - mints a 32-byte hex bearer (crypto/rand), hashes with argon2id
    via auth.HashToken, stores (tenant_id, github_id, "github-<sha>",
    hash_phc) via TokenStore.Put. Plaintext is never persisted.
  - emits best-effort auth.login audit event via AttachAuditEmit hook.

- internal/server/oauth_tokenstore.go: MemTokenStore + SQLTokenStore
  with PRIMARY KEY (tenant_id, device_label) upsert. VerifyToken is
  O(n) argon2id over the stored rows (n is small for v1).

- internal/tenant/store.go Migrate now also runs CREATE TABLE IF NOT
  EXISTS auth_tokens, so a single tenant.Migrate call provisions
  everything the multi-tenant control plane needs.

Tests:
- internal/cli/login_github_test.go: TestRunGitHubLogin_HappyPath,
  TestPollForGitHubToken_AuthorizationPending,
  TestPollForGitHubToken_SlowDown,
  TestPollForGitHubToken_Cancel,
  TestExchangeGitHubTokenWithGemba_FirstLogin,
  TestExchangeGitHubTokenWithGemba_Existing.
- internal/server/oauth_test.go: TestOAuthLogin_FirstLogin (creates
  tenant + verifies argon2id hash); TestOAuthLogin_SubsequentLogin
  (second login finds existing tenant; no duplicate rows);
  TestOAuthLogin_Disabled (503 oauth_not_configured envelope);
  TestOAuthCallback_Disabled / TestOAuthCallback_Enabled.

go test ./internal/auth/oauth/... ./internal/cli/...
./internal/server/... ./internal/tenant/... ./internal/config/...
passes (1125 tests).

Backwards compat: --github is opt-in. The single-user PAT path
(login + whoami) is unchanged; legacy callers see no behavior change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resolved router.go conflict by keeping both feature-set additions
(egress store fields + audit/tenantWorkspaceCount fields).
Resolved router.go conflicts by keeping both tenant-CRUD admin routes
(from gm-o9t8.3.1.1) and OAuth callback route (from this branch).
Fixed test call site for the extended Store.List signature.
Implements VerifyImage/LoadManifest/DefaultImagePaths and a manifest
schema describing the kernel + rootfs artifacts. Plain Go so it builds
on every host; the Linux supervisor calls VerifyImage before boot.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wire VolumeMount through Spec.validate, firecracker_linux drives, and
the non-Linux fallback (symlink-staged into per-VM tmpdir with env
hints GEMBA_VOL_<N>_PATH/_GUEST/_RO). Adds volume_test.go covering
validation rejections and subprocess readability.
Adds the build-pipeline scaffolding for the Firecracker microVM image:

- vm-image/buildroot/: Buildroot 2024.x external tree with the
  gemba_defconfig (musl busybox + git + ca-certs + virtio-fs) and a
  gemba-vm-init package that cross-compiles the Go init binary.
- vm-image/init/: standalone Go module (its own go.mod, build-tagged
  linux+init) that reads MMDS, mounts /workspace, execs the agent,
  posts a run summary, and powers off cleanly.
- vm-image/kernel/: Linux 6.1 microvm-tuned .config fragment plus a
  README pointing at upstream kernel.org.
- vm-image/scripts/build.sh: orchestrator that bakes vmlinux +
  rootfs.ext4 and writes dist/vm/manifest.json (+ optional
  ed25519 manifest.sig when GEMBA_RELEASE_KEY is set).
- Makefile: vm-image / vm-image-clean targets. On non-Linux hosts
  the target prints a friendly skip and exits 0.
- docs/server/vm-image.md: full lifecycle docs — build, ship,
  install, verify, fallback.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extend TokenStore interface and SQL schema with an opaque token id PK,
revoked_at, and last_used_at columns. Add three new handlers under
/api/v1/auth/tokens (list, revoke, rotate) plus EventAuthTokenRevoke /
EventAuthTokenRotate audit events. Rotate runs inside a SQL transaction
so revoke + insert are atomic against the (tenant, device_label) UNIQUE.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Introduces the Ruleset/Statement IR that the Firecracker supervisor
translates egress.Rule into before handing off to the platform-
specific applier. Lives in the firecracker package so both Linux
nftables emission and the non-Linux no-op share one source of truth.

Hostname resolution is one-shot at apply time via net.DefaultResolver
with a 5s timeout; wildcard patterns produce a "no eager DNS" note so
the applier can emit a log-only stub instead of blackholing traffic.

Translation matrix test exercises five rules with overlapping
priorities — accept/drop/log shapes, wildcard fallback, sanitised
table names, and stable JSON rendering.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add VaultInjector + Options.Vault on the firecracker Supervisor. Both
Linux (MMDS payload) and fallback (subprocess env) paths now pull
vault.Inject(wsid) at Start, merge with explicit Spec.Secrets
(explicit wins), and proceed. Nil vault is a normal path — only
explicit secrets are used. NewSupervisor(log) routes through the new
NewSupervisorWithOptions for backwards compat.

Note: the same Options struct also carries the LifecycleAuditor seam
used by gm-o9t8.3.2.7; the two beads share the construction surface
so wiring up the supervisor is a single call site.
- egress_linux.go installs a per-VM `gemba_<vm>` inet table with a
  forward chain hooked at priority 0 and a default-drop policy.
  Each Statement becomes one nft rule matching iifname == tap + the
  resolved daddr set; deny rules embed a log prefix
  `gemba-egress-drop:<rule-id>` for audit ingest.
- teardownEgressRules deletes the per-VM table; missing tables are a
  no-op so the cleanup path is idempotent.
- egress_other.go is the no-op stub: returns the translated Ruleset
  but emits a WARN that egress is not enforced on this platform.
- Pull in github.com/google/nftables v0.3.0 + golang.org/x/sys/unix.

Linux integration test is gated on `runtime.GOOS=="linux" && euid==0`
and skips on every developer host + CI runner we have today.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add internal/dispatch with a Runtime strategy interface, native and
firecracker implementations, and SelectRuntime helper. RuntimeNative
delegates to a NativeDispatchFunc (preserves the existing tmux
dispatch path verbatim); RuntimeFirecracker boots a per-bead microVM
via firecracker.Supervisor, mounts workspacelayout.Paths.Repo at
/workspace, and returns vm.ID as the run id. Default selection
remains native — runtime=firecracker is opt-in via constitution key
or --runtime CLI flag.
Adds EgressProvider (narrow Effective() seam) + EgressAware (a type-
assertion-only interface implemented by both backends). Callers pass
in any source that satisfies egress.Store-shaped Effective lookups.

- Linux supervisor: after machine.Start returns, fetches the
  workspace's effective rules and installs them via applyEgressRules.
  An apply failure tears down the VM (StopVMM + ctx cancel) and
  surfaces the error from Start — no VM ever runs without working
  egress controls. Stop reads VM.egressTable and best-effort deletes
  the per-VM nftables table.
- Fallback supervisor: source-compatible. Translates the plan so
  dispatch / CLI tooling can introspect it but doesn't touch the
  kernel; the WARN log in egress_other.go is the visible artifact.
- VM gains an unexported egressTable field stamped by the applier.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Declare EventVMSpawn, EventVMDestroy, EventVMEgressDenied audit
constants plus KindVMLifecycle. Add VMLifecycleAdapter that bridges
the firecracker supervisor's narrow LifecycleAuditor surface onto
audit.Auditor — keeps the supervisor free of an audit package import
cycle. Supervisor (both fallback and Linux) emits vm.spawn on Start
success with {vm_id, wsid, kernel_path, mem_mb, cpus}, and vm.destroy
on Stop with {vm_id, duration_ms, exit_status}. EventVMEgressDenied
is declared for gm-o9t8.3.6.2 to use.
…(dispatch) + 3.2.7 (audit events)

Resolved conflicts in firecracker_fallback.go / firecracker_linux.go by
keeping BOTH the EgressProvider plumbing (from gm-o9t8.3.6.2) AND the
VaultInjector + LifecycleAuditor wiring (from this bundle). audit/types.go
de-duplicates the EventVMEgressDenied constant that both branches added.
Five interlocked beads landing the ASDD interception surface:

- gm-v0sp.15: gemba spec new --kit scaffolds .claude/commands/{tasks,implement}.md
- gm-v0sp.16: PreToolUse hook handlers reject writes to tasks.md / todo.md
- gm-v0sp.17: constitution schema + parser + 'gemba constitution lint --strict'
- gm-v0sp.18: 'gemba spec preflight' refuses /implement when tasks.md exists
- gm-v0sp.19: ASDD stanza appended to CLAUDE.md via 'gemba spec init --asdd'

Analyzer (gm-v0sp.21) seam preserved: rejected payloads available on stdin
and via GEMBA_REJECTED_PAYLOAD env hand-off.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@MikeBengtson
MikeBengtson merged commit 684227c into main May 13, 2026
4 of 7 checks passed
@MikeBengtson
MikeBengtson deleted the feat/gemba-remote-foundations branch May 13, 2026 03:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant