Skip to content

feat(orthrus): write-mode support + muzzle allowlist parity (#1160, #1161)#1166

Open
Wikid82 wants to merge 22 commits into
developmentfrom
feature/orthrus
Open

feat(orthrus): write-mode support + muzzle allowlist parity (#1160, #1161)#1166
Wikid82 wants to merge 22 commits into
developmentfrom
feature/orthrus

Conversation

@Wikid82

@Wikid82 Wikid82 commented Jul 20, 2026

Copy link
Copy Markdown
Owner

Summary

  • Adds opt-in Orthrus write-mode: allowlisted write endpoints, request-body validation, rate limiting, audit logging, PATCH/proxy-status API extensions, and the corresponding UI (write-mode toggle, audit-log deep-linking, i18n).
  • Fixes GH orthrus: path-normalization order differs between backend and agent muzzle filters #1160: the backend and remote-agent Docker API muzzle filters normalized request paths in a different order, allowing three independent accept/reject divergences (including on a write endpoint) that could theoretically bypass the read-only guarantee if the agent's filter were ever reached without the backend filter running first.
  • Fixes GH orthrus: unify the two independently hand-maintained muzzle allowlists (and give agent/ CI coverage) #1161: unifies enforcement of the two independently hand-maintained muzzle allowlists via a new stdlib-only structural parity checker (scripts/ci/check_muzzle_allowlist_parity.go), and brings the agent/ Go module up to the same CI bar as backend/ (lint, staticcheck, vet, coverage gate).

Root cause (#1160)

Backend (backend/internal/orthrus/muzzle.go) stripped the /vX.Y Docker API version prefix before path.Clean; the agent (agent/muzzle/muzzle.go) did path.Clean first and used a looser path.Match("v*", ...) wildcard instead of a numeric-anchored regex. Three divergences were found and closed, all agent-more-permissive-than-backend:

  1. /foo/../v1.44/images/x/json (the original issue example)
  2. /vFOO/containers/json (non-numeric version segment)
  3. POST /vFOO/containers/abc/start — same class of bug, but on a write endpoint, once write-mode is enabled

Design decision (#1161)

Evaluated a shared importable Go package and codegen from a single source-of-truth file; rejected both — agent/ is intentionally a minimal, dependency-free FROM scratch binary, and codegen was disproportionate to how rarely this data changes. Instead: a go/ast-based structural drift checker that diffs all 8 paired allowlist declarations (including the normalization regex itself) between the two files, wired into CI, layered on top of a shared behavioral test corpus (backend/internal/orthrus/testdata/muzzle_corpus.json) consumed by both modules' test suites.

Test plan

  • Backend + agent unit tests green, including new corpus rows that previously exposed the divergence (now pass on both sides)
  • check_muzzle_allowlist_parity.go passes; independently verified its negative-path detection (both a data-mismatch and a regex-mismatch injection were correctly caught)
  • Staticcheck: 0 issues, both modules
  • Coverage: backend 89.0% (gate 87%), agent 65.3% (gate 65%)
  • CodeQL (Go/JS), Semgrep, govulncheck, Trivy: 0 findings
  • Full QA/security audit: docs/reports/qa_report.md — READY, zero CRITICAL/HIGH findings
  • Playwright E2E write-mode specs enabled

Wikid82 added 16 commits July 20, 2026 05:21
…view rounds

Records the fully revised, Supervisor-approved spec for opt-in per-agent
Docker write access through Orthrus, including the closed
VolumeOptions.DriverConfig bind-mount bypass and the agent-side
body-inspection mechanism. Status line documents the direct user
sign-off gate and the earlier corrected self-certification attempt.
Playwright specs for the write-mode toggle UX per the approved spec's
Phase 1: default-off, typed-name confirmation gating enable (not
disable), reconnect notice, WRITE badge, and the audit-log deep link.
All test.fixme until the corresponding UI lands.
Foundation commit for opt-in per-agent Docker write access (spec at
docs/plans/current_spec.md):

- models.OrthrusAgent gains WriteEnabled bool (default false).
- AgentSession/NewAgentSession, ExternalProxyStatus, and Muzzle/NewMuzzle
  gain write-mode-related fields and parameters (writeEnabled,
  writeLimiter, auditLogger, agentUUID), but no write-endpoint allowlist
  branch exists yet — every call site passes false/nil, so behavior is
  byte-for-byte unchanged.
- agent/muzzle.Filter becomes connection-scoped instead of
  process-scoped (New(writeEnabled bool)); agent/leash.Leash constructs
  a fresh Filter per successful dial instead of once per process,
  closed over by that connection's streams, so a mid-connection
  write-mode toggle can't retroactively affect an already-negotiated
  session.

Full backend and agent test suites pass unmodified — this is pure
plumbing, not yet reachable behavior.
…tion, rate limiting, and audit logging

Core write-path implementation for the Orthrus write-mode feature
(docs/plans/current_spec.md), in both independently-enforcing muzzle
filters:

- backend/internal/orthrus/muzzle.go and agent/muzzle/muzzle.go each
  gain a fixed write-endpoint allowlist (image pull, container
  start/stop/restart/create/remove), consulted only when a session
  negotiated write mode. Every other endpoint (exec, volume/network
  mutation, image delete, build, prune, auth, commit, Swarm/service)
  stays permanently blocked regardless of the flag.
- POST /containers/create gets hybrid allowlist/value-level body
  validation: an exhaustive HostConfig key allowlist (fail-closed
  against unknown future Docker API fields), a value check on
  NetworkMode (rejects "host" and "container:*"), and a Mounts check
  that rejects bind-type mounts AND any Mounts entry carrying
  VolumeOptions.DriverConfig at all — closing the documented bypass
  where Docker's "local" volume driver can make a Type:"volume" mount
  behave as an arbitrary host bind mount. Identical logic in both
  files, duplicated by design (agent/ is a separate minimal-binary
  module), verified identical via a new shared JSON test corpus
  (backend/internal/orthrus/testdata/muzzle_corpus.json) loaded by
  both packages' test suites — the concrete first implementation of
  the anti-drift mitigation GH #1161 already recommended.
- Write traffic gets its own session-scoped rate limiter
  (0.5 req/s, burst 5 — one full pull/stop/remove/create/start cycle),
  since the External Proxy's raw http.Server has never been covered by
  Charon's existing Gin-based rate limiting.
- Every write attempt (allowed, blocked, or rate-limited) produces one
  SecurityAudit entry via a new narrow AuditLogger interface (avoids
  an orthrus->services import cycle).
- X-Orthrus-Write-Enabled handshake header negotiates write mode
  atomically at WebSocket upgrade time; the agent's Filter becomes
  connection-scoped so a DB toggle only takes effect on next reconnect.
- CI: orthrus-build.yml and nightly-build.yml now run the agent
  module's own test suite (previously zero test execution in CI for
  agent/, confirmed via direct workflow grep) before the Docker build
  step, gating both the PR-facing and the nightly force-push-based
  publish paths.

Full backend and agent test suites green, including the dangerous-body
corpus cases (Privileged, CapAdd, Binds, NetworkMode:host,
bind-type Mounts, the VolumeOptions.DriverConfig bypass, Devices,
Sysctls, unrecognized future fields) — all independently rejected by
both filters.
…it logging

Section 3.2 of docs/plans/current_spec.md:

- OrthrusService.Patch gains a writeEnabled *bool parameter, following
  the existing "*T present = intend to change" pattern used by every
  other field on this endpoint.
- OrthrusHandler.Patch emits a SecurityAudit entry
  (orthrus_write_enabled/orthrus_write_disabled) whenever an operator
  toggles write_enabled, using this codebase's existing
  actorFromContext(c) convention for admin-initiated audit events
  (matches security_handler.go/crowdsec_handler.go) rather than
  growing OrthrusService's dependencies with a *gin.Context-derived
  actor — this is a handler-layer concern, separate from the
  per-write-request audit entries Muzzle already emits directly.
- GetProxyStatus response gains configured_write_enabled (DB value)
  and active_write_enabled (live session value, from the extended
  ExternalProxyStatus), so the frontend can detect and surface the
  same "configured differs from active, reconnect needed" situation
  already modeled for external_proxy_port.
- routes.go wires orthrusServer.SetAuditLogger(securityService) so
  Muzzle's per-write-request audit entries (added in the previous
  commit) actually reach the database in the running app, not just in
  tests.

New test coverage: write_enabled toggling on/off at the service layer,
audit-entry emission (and non-emission when write_enabled isn't part
of the request) at the handler layer via SecurityService.Flush() +
ListAuditLogs, and the two new proxy-status response fields. Full
backend suite green; GORM security scan clean (0 critical/high).
Section 3.5 of docs/plans/current_spec.md:

- OrthrusAgent/PatchAgentRequest/ExternalProxyStatus types gain
  write_enabled / configured_write_enabled / active_write_enabled,
  matching the backend response shapes from the previous two commits.
- New AgentWriteModeDialog, deliberately separate from
  AgentExternalProxyDialog rather than a section bolted onto it (see
  in-file rationale: transport-layer vs. permissions-escalation
  concerns, each with independent "configured vs. active" state).
  Toggling on requires typing the agent's exact current name before
  Save enables; toggling off requires no confirmation. Shows the fixed,
  non-editable list of permitted operations, a security warning
  distinct from the external-proxy dialog's, a reconnect notice
  mirroring the existing configuredDiffersFromActive pattern, and a
  link into a pre-filtered audit-log view.
- OrthrusAgentManager gains a write-mode action button (ShieldCheck
  icon) and a WRITE badge, alongside the existing PROXY badge.
- AuditLogs.tsx now initializes its filter state from the URL query
  string on mount (event_category, resource_uuid, actor, action,
  start_date, end_date) via react-router-dom's useSearchParams,
  auto-expanding the filter panel when any are present — closes a
  real, previously undocumented deep-linking gap found during this
  feature's research: a link into a filtered audit view landed
  unfiltered. Generic fix, not Orthrus-specific.
- EventCategory/AuditAction TS unions extended with orthrus_write and
  its five action values.
- i18n: new hecate.writeMode.* keys and agentManager.writeMode across
  all five locales (en fully localized; fr/de/es/zh carry English
  placeholder text for the new keys, matching this repo's existing
  handling of other not-yet-translated strings like configureProxy).

Full frontend suite green: 2988 tests passed, 0 failures. Type-check
and production build both clean.
…y claims

Final commit for the Orthrus write-mode feature (docs/plans/current_spec.md):

- tests/orthrus-write-mode.spec.ts: un-skip all Phase 1 specs now that
  the UI exists. Fixed two issues found only by running against the
  real app (not visible from code review): the Switch component's
  underlying <input role="switch"> is sr-only with near-zero hit area,
  so clicking it directly (even with force:true) can land on a
  clipped coordinate — clicking the wrapping <label> instead is both
  more robust and closer to real user interaction. Also split the
  "enable succeeds" test in two: a real save always closes the dialog
  (matching AgentExternalProxyDialog's existing pattern), so the
  reconnect notice can only be observed on a later re-open, not
  within the same session — the original single test's premise didn't
  match the dialog's actual (correct) behavior.
- docs/features/orthrus.md: rewrites the three absolute "cannot be
  changed" / "no way to turn it off" claims (Section 2.8 of the spec)
  to accurately describe the read-only default (still unconditional,
  can't be loosened by accident) alongside the new opt-in, per-agent,
  audited write capability — including the six-operation fixed list,
  the typed-confirmation requirement, and the multi-network
  limitation from Section 4.2.

Validation: full backend/agent test suites, GORM scan, and frontend
suite already green from prior commits. This commit's own testable
surface — tests/orthrus-write-mode.spec.ts — run 3x consecutively
against a freshly rebuilt E2E container (9/9 passing each time, no
flakiness). Full `npx playwright test --project=firefox` run: 848
passed, 37 skipped, 14 failed — all 14 failures verified via
`git diff 0d81fc9..HEAD` to be pre-existing and untouched by any
commit in this branch (zero diff on every failing test's file).
…zation divergence (#1160)

Add 7 shared corpus entries (backend/internal/orthrus/testdata/muzzle_corpus.json)
covering the version-prefix normalization order divergence between
backend/internal/orthrus/muzzle.go and agent/muzzle/muzzle.go: a
traversal-disguised version prefix, a non-numeric fake version prefix on a
read path/HEAD path/write path, plus three already-agreed traversal/double-
slash regression cases migrated from agent-only tests.

No production code changes. TestMuzzle_SharedCorpus (backend) passes
unchanged. TestFilter_SharedCorpus (agent) fails on exactly the 4 divergence
rows -- this is the intentional red state proving the bug before the fix
lands in a following commit.
…le (#1160)

Extract ServeHTTP's existing strip-then-clean sequence into a named
normalizeDockerPath(rawPath string) string helper, purely for naming
symmetry with the forthcoming agent-side helper (agent/muzzle/muzzle.go)
and to give the upcoming structural parity checker (#1161) a stable,
identically-named anchor in both files.

No behavior change -- full pre-existing test suite passes unmodified.
Adds direct unit tests for normalizeDockerPath covering the two GH #1160
divergence rows (traversal-disguised version prefix, non-numeric fake
version prefix) plus existing traversal/double-slash regression cases.
)

Introduce normalizeDockerPath in agent/muzzle/muzzle.go, applying
versionPrefixRe strip-then-path.Clean in the same order as
backend/internal/orthrus/muzzle.go's helper of the same name, and route
every allowlist check (read, HEAD /_ping, and write) through it.

Collapses the previously-duplicated allowedPatterns list into
allowedDockerPaths (exact) + allowedDockerPatterns (dynamic), renames
imageDistributionPatterns to allowedDockerPrefixSuffixPatterns, and drops
all now-redundant "/v*/..." pattern entries -- version handling is unified
through normalizeDockerPath instead of a loose path.Match "v*" wildcard,
which required no digits and was strictly more permissive than backend's
numeric-anchored ^/v\d+\.\d+ regex.

allowWrite's signature changes to accept a pre-normalized path computed
once by its caller (Allow), instead of separately re-deriving an
"unversioned" value for its exact-path branch only while its pattern branch
matched the raw path directly -- closing a second, independent instance of
the same divergence class reaching a write endpoint.

Turns green the 4 corpus cases added red in the prior commit. Full agent
and backend/internal/orthrus test suites pass.
Add scripts/ci/check_muzzle_allowlist_parity.go, a stdlib-only (go/ast,
go/parser, go/constant) drift guard that structurally compares all eight
paired declarations between backend/internal/orthrus/muzzle.go and
agent/muzzle/muzzle.go: the seven allowlist/body-size-constant data
declarations plus versionPrefixRe's regex source string (extracted from its
regexp.MustCompile(...) call expression). Fails loudly, not silently, on
any declaration it cannot find or extract, and reports every mismatch with
the specific missing/extra entries.

The existing shared behavioral corpus proves outcome parity for the inputs
it contains but cannot catch "a new pattern was added to one file's
declaration and nobody added a corresponding corpus case" -- the exact
mechanism behind the production incident that motivated this plan. This is
a second, independent guard layered on top of the corpus.

Wired into lefthook.yml's pre-commit stage (glob-scoped to the two muzzle
files) and a new always-run quality-checks.yml job (not gated on agent/**
paths, so a lefthook bypass via --no-verify is still caught in CI).

Manually verified (throwaway edit + revert, not committed) that the checker
independently catches both AST-extraction shapes: an added
allowedWritePatterns entry, and a loosened versionPrefixRe regex.
…for agent module (#1161)

Extends agent/'s CI parity with backend/ beyond "tests run" (already landed
in the write-mode commits) to "tests run, lint enforced, coverage gated":

- Move backend/.golangci-fast.yml to repo-root .golangci-fast.yml (single
  shared source of truth); golangci-lint-fast.sh/-full.sh now loop over
  both backend and agent.
- Split lefthook.yml's go-vet into module-scoped go-vet (backend/**/*.go)
  and go-vet-agent (agent/**/*.go), and add agent-test-coverage to the
  manual `testing` pipeline.
- Add scripts/agent-test-coverage.sh: same line-coverage-from-coverprofile
  gate arithmetic as scripts/go-test-coverage.sh, minus encryption-key
  bootstrapping (agent has none). Threshold calibrated to the module's
  actual post-this-PR aggregate (65%) per R8 -- agent/leash's pre-existing
  44% coverage is explicitly out of scope to raise here.
- Add agent-quality job to quality-checks.yml: go vet, golangci-lint (fast,
  root-shared config), coverage gate -- runs unconditionally on every PR,
  not gated on agent/** paths. Deliberately omits backend-quality's
  encryption-key resolution, GORM scan, and Perf Asserts steps (agent has
  none of those concerns).
- Implement the previously-dangling scripts/check-module-coverage.sh
  (Makefile referenced it but it didn't exist on disk -- a pre-existing,
  unrelated bug found during this PR's research and fixed alongside the new
  agent script); Makefile's check-module-coverage echo text corrected from
  the already-inaccurate "backend + frontend" to "backend + agent".
- Add agent/cert/cert_test.go (LoadOrGenerate had 0% coverage despite being
  genuine ECDSA/x509 logic) and agent/protocol/message_test.go (wire-format
  constant-value regression guard).
- Extend scripts/local-patch-report.sh, backend/cmd/localpatchreport, and
  backend/internal/patchreport to compute and report agent/ patch coverage:
  patchreport.ParseUnifiedDiffChangedLines gains a third (agent) return
  value; ParseGoCoverageProfile and normalizeGoCoveragePath gain a
  moduleDir parameter (was hardcoded to backend's prefix-stripping rules,
  which would have silently produced 0% patch coverage for every agent file
  -- the same class of masked-by-a-different-mechanism bug the Root Cause
  Analysis Protocol warns about). Verified end-to-end: a local run against
  a diff touching agent/muzzle/muzzle.go produces a populated, non-zero
  "Agent" row in local-patch-report.md.
- Add agent-codecov job to codecov-upload.yml (uploads agent/coverage.txt
  under a distinct `agent` flag) and exclude agent/main.go from that flag's
  Codecov accounting in codecov.yml, mirroring backend/cmd/api/**'s
  existing entrypoint-exclusion treatment.

Also fixes 10 pre-existing errcheck/shadow findings in agent/leash and
agent/muzzle (unchecked Close() return values, one shadowed err) --
necessary for the new agent-quality lint gate to pass cleanly rather than
inheriting a backlog of findings never checked before this commit. No
behavior change: every fix is an explicit `_ = x.Close()` or a renamed
local variable.

Also fixes make lint-staticcheck-only, broken pre-existing (golangci-lint
v2 doesn't support the v1 --disable-all flag) -- switched to --enable-only,
found while wiring up the same target for agent.
CHANGELOG.md: add Security entry for the GH #1160 muzzle normalization
order fix, and a CI/CD entry summarizing the GH #1161 agent CI parity work
(lint/vet/coverage gates, structural allowlist parity checker, agent patch
coverage, agent-codecov upload).

ARCHITECTURE.md: update the Pre-Commit Checks and Continuous Integration
sections to describe agent/'s new lint/vet/coverage parity with backend/,
the muzzle allowlist parity checker, and the agent Codecov flag. Also
corrects a stale reference to ".pre-commit-config.yaml" (the repo migrated
to lefthook.yml; the file no longer exists).
The GH #1160/#1161 implementation spec was iterated on and approved
but never committed to feature/orthrus, leaving a dangling reference
in check_muzzle_allowlist_parity.go's doc comment (which points readers
to this file's "Design decision" section) unresolved at HEAD.
@codecov

codecov Bot commented Jul 20, 2026

Copy link
Copy Markdown

@github-advanced-security

Copy link
Copy Markdown
Contributor

You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool.

What Enabling Code Scanning Means:

  • The 'Security' tab will display more code scanning analysis results (e.g., for the default branch).
  • Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results.
  • You will be able to see the analysis results for the pull request's branch on this overview once the scans have completed and the checks have passed.

For more information about GitHub Code Scanning, check out the documentation.

@github-actions

github-actions Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

✅ Supply Chain Verification Results

PASSED

📦 SBOM Summary

  • Components: 1502

🔍 Vulnerability Scan

Severity Count
🔴 Critical 0
🟠 High 0
🟡 Medium 4
🟢 Low 2
Total 10

📎 Artifacts

  • SBOM (CycloneDX JSON) and Grype results available in workflow artifacts

Generated by Supply Chain Verification workflow • View Details

Wikid82 added 6 commits July 20, 2026 21:15
…d enforce threshold locally

scripts/local-patch-report.sh always resolved its diff baseline to
origin/main first, never matching PR #1166's actual GitHub base
(development), so the local tool's patch-coverage numbers silently
diverged from what Codecov reports on the PR. Separately, the tool
computed WARN-level coverage scopes but never failed the process on
them, so CLAUDE.md's Definition of Done Step 2 ("local patch coverage
preflight is MANDATORY") was unenforceable in practice.

- scripts/local-patch-report.sh: three-tier baseline resolution -
  explicit $CHARON_PATCH_BASELINE override, then `gh pr view`'s real
  baseRefName, then a development-preferring static heuristic (main
  is only ever the nightly-promotion target, not the default
  integration branch). Restructures the Go-tool invocation so a
  strict-mode non-zero exit still lets the script's own artifact
  checks run before propagating the real exit code.
- backend/cmd/localpatchreport/main.go: new `-advisory` flag (default
  strict - non-zero exit when any coverage scope is below threshold,
  checked only after both report artifacts are written and the WARN
  diagnostic lines are printed, so a failing gate still leaves a full
  report to read); report.Mode now truthfully reflects "strict" or
  "advisory" instead of the previous hardcoded "warn" literal.
- backend/internal/patchreport: new HasWarnStatus helper, unit-tested
  independently of os.Exit/CLI parsing.
- New scripts/tests/local-patch-report_baseline.bats covering all four
  baseline-resolution branches (gh success, gh absent, gh erroring,
  explicit override winning).
- .gitignore: cover the backend/localpatchreport compiled binary
  (previously untracked but unignored stray build artifact).

No behavior change to agent/muzzle or agent/leash in this commit -
this only fixes the measurement/gating tool itself, so its own
validation gate can still correctly report warn/exit 1 against the
genuinely-uncovered agent code, proving the fix works before the
follow-up commit closes that coverage gap for a real reason.
…flagged by Codecov on PR #1166

Codecov's real patch-coverage comment on PR #1166 flagged agent/muzzle/muzzle.go
(81.72%, 9 missing/8 partial) and agent/leash/leash.go (36.36%, 7 missing) -
existing, already-shipped fail-closed validation branches and the
connection-scoped write-mode dispatch wiring that had never been exercised
by any test. No production code changes; test-only.

agent/muzzle/muzzle_test.go adds:
- 4 new malformed-body cases isolating still-untested fail-closed unmarshal
  branches in validateContainerCreateBody (NetworkMode, Mounts, Mounts
  VolumeOptions, HostConfig itself).
- TestFilter_Allow_ContainersCreate_EmptyBodyAllowed, locking in the one
  deliberately permissive branch (empty body allowed through).
- TestFilter_ServeProxy_BodyReadError_ReturnsError and
  _BodyTooLarge_Returns403AndError for ServeProxy's body-handling branches.
- TestFilter_ServeProxy_DockerWriteError_ReturnsError for the
  req.Write(conn) forwarding-failure branch. Forcing specifically the write
  (not the dial, not the response read) to fail via a real kernel socket is
  inherently racy with no synchronization hook available from outside
  ServeProxy; an accept-then-SO_LINGER=0-close approach was tried first per
  Supervisor review and found flaky (write often succeeds locally before
  the close propagates). Uses a bounded-retry technique instead (closing
  the listener before ever accepting, retried on a fresh socket up to 200
  times, accepting only the specific error this branch produces) -
  verified flake-free across 100+ in-process and 40+ freshly-exec'd process
  runs under -race.

agent/leash/leash_test.go adds two integration-style tests proving the
write-mode connection-scoped filter dispatch wiring actually executes for a
real accepted yamux stream, not just in isolation:
- TestLeash_Connect_DockerStreamDispatchesThroughFilter: connect()'s
  AcceptStream loop -> handleStream's streamTypeDocker dispatch ->
  handleDockerStream -> filter.ServeProxy -> net.Dial(dockerSock).
- TestLeash_Connect_PortForwardStreamDialsTarget: handleStream's
  streamTypePortForward dispatch -> handlePortForward's successful-dial
  path and its deferred conn.Close().

Both drive the real exported Leash/Config/Run surface via a test WebSocket
server running a yamux server session, mirroring TestLeash_Reconnect's
existing harness (NewWSNetConn) rather than adding white-box test surface.
Each new test was verified to fail for the intended reason by temporarily
breaking its target code path and confirming the failure, then reverting.
Dockhand's standard image-update flow renames the old container out of
the way (e.g. to <name>_old) before starting the new one under the
original name. POST /containers/*/rename was missing from both
dual-maintained write-mode allowlists, so a live update pulled the new
image successfully but then 403'd on the rename step.

Add the pattern to allowedWritePatterns in both
backend/internal/orthrus/muzzle.go and agent/muzzle/muzzle.go,
following the existing no-body/query-param handling already used for
/start, /stop, and /restart (Docker's rename endpoint takes the new
name via ?name=, not a request body). Covered by new corpus and unit
test cases in both modules; scripts/ci/check_muzzle_allowlist_parity.go
confirms the two allowlists stay byte-identical.
…nt restart

Turning write mode on for an Orthrus agent only takes effect after the
agent's next reconnect, but the dialog gave no indication of that at
save time -- an operator could enable write access, believe it was
live immediately, and be confused when writes still failed until a
manual restart.

AgentWriteModeDialog now checks the PATCH response's fresh agent
status: if write mode was just turned on (off->on transition) and the
agent is currently online, fire a toast (via the existing
react-hot-toast mechanism, no new dependency) naming the agent and
instructing the operator to restart it on its remote machine. No toast
fires when turning write mode off, on a no-op save, or for an agent
that isn't currently connected, since a not-yet-connected agent will
simply pick up the new value on its first connection.

New i18n key hecate.writeMode.restartRequiredToast added to all 5
locale files, matching this block's existing precedent of leaving some
strings English-only in non-en locales.
… design

Append an addendum to the PR #1166 spec documenting both fixes: the
dual-allowlist rename gap (root cause, fix, and validation) and the
write-mode restart toast's design research (toast mechanism precedent,
connection-status source, exact file/function targets, and the Vitest
test matrix), for traceability alongside the already-shipped
GH #1160/#1161 muzzle-parity work documented earlier in the same file.
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.

2 participants