Let a sidecar's placement narrow but never widen
sidecarPlacement took the record's placement whenever it stated one, so a record saying "any" under a no-inbound manifest placed that bot on boxes the operator meant to keep it off — the widening direction the console door already refuses by never accepting a placement field at all. A direct bucket write, or an older writer, does not go through that door. Most restrictive now wins: either voice asking for no-inbound gets it. A record may still narrow an app that states no placement of its own, which is the direction that reads as a requirement.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014WvcQVWggXkE5BAkjRDgWF
2 files changed,
+50
−4
internal/provision/sidecar_reconcile.go+17 −4internal/provision/sidecar_reconcile_test.go+33 −0
diff --git a/internal/provision/sidecar_reconcile.go b/internal/provision/sidecar_reconcile.go| index 5297cddef..d7f86c8f3 100644 |
| --- a/internal/provision/sidecar_reconcile.go |
| +++ b/internal/provision/sidecar_reconcile.go |
| @@ -75,11 +75,24 @@ func sidecarArgsJSON(m SidecarManifest) string { |
| } |
| // sidecarPlacement resolves the placement policy a record is placed under |
| -// (design §8). The record wins when it states one — it is the workload's own |
| -// requirement — and the manifest's PlacementOrDefault answers otherwise, so |
| -// an app-wide "everything I run must sit somewhere with no inbound path" |
| -// holds for records that never mention placement. |
| +// (design §8): MOST RESTRICTIVE WINS. Either voice asking for no-inbound gets |
| +// no-inbound, and only when both are silent (or both say "any") does the |
| +// workload place anywhere. |
| +// |
| +// The record can therefore still NARROW an app-wide "any" — one bot that must |
| +// sit somewhere with no inbound path, under an app with no such rule — which |
| +// is the direction that reads as a requirement. What it can no longer do is |
| +// WIDEN: a record stating "any" under a no-inbound manifest used to place that |
| +// bot on inbound boxes, turning app-wide policy into a suggestion. The console |
| +// door already refuses to write such a record (it never accepts a placement |
| +// field at all, exactly so the manifest stays authoritative); a direct bucket |
| +// write, an older writer, or anything else holding the prefix's keys does not |
| +// go through that door, and both doors hold the invariant rather than trusting |
| +// each other. |
| func sidecarPlacement(rec SidecarRecord, m SidecarManifest) string { |
| + if rec.Placement == PlacementNoInbound || m.PlacementOrDefault() == PlacementNoInbound { |
| + return PlacementNoInbound |
| + } |
| if rec.Placement != "" { |
| return rec.Placement |
| } |
diff --git a/internal/provision/sidecar_reconcile_test.go b/internal/provision/sidecar_reconcile_test.go| index cd996e5b6..81c1cf25b 100644 |
| --- a/internal/provision/sidecar_reconcile_test.go |
| +++ b/internal/provision/sidecar_reconcile_test.go |
| @@ -415,6 +415,39 @@ func TestSidecarReconcilePlacementGate(t *testing.T) { |
| hasRoute(t, f.reg, f.host("hub", "relay"), "it must land on a box that declares CARLOS_INBOUND=none") |
| } |
| +// TestSidecarReconcilePlacementIsMostRestrictive: a record may NARROW the |
| +// app's placement but never widen it. "any" on a record under a no-inbound |
| +// manifest is the widening direction — the console door refuses to write it |
| +// at all, and this pass must refuse to honour one that reached the bucket by |
| +// any other route, or app-wide policy is only a suggestion. |
| +func TestSidecarReconcilePlacementIsMostRestrictive(t *testing.T) { |
| + f := sidecarSetup(t) |
| + // App-wide: everything this app runs sits somewhere with no inbound path. |
| + f.putManifest(t, "eleven", SidecarManifest{Schema: 1, App: "eleven", |
| + Args: []string{"sidecar", "run"}, Placement: PlacementNoInbound}) |
| + f.putRecord(t, SidecarRecord{Schema: 1, Name: "bot", App: "eleven", Channel: "stable", Placement: PlacementAny}) |
| + |
| + f.reconcile(t, ReconcileConfig{}) // an ordinary box: accepts inbound work |
| + noRoute(t, f.reg, f.host("bot", "eleven"), |
| + `a record's "any" must not override the app's no-inbound placement — that is the widening direction the console door already refuses`) |
| + |
| + f.reconcile(t, ReconcileConfig{Inbound: InboundNone}) |
| + hasRoute(t, f.reg, f.host("bot", "eleven"), "it still lands where the app's own policy says it may") |
| +} |
| + |
| +// The narrowing direction is untouched: one bot that must run with no inbound |
| +// path, under an app that states nothing, is honoured. |
| +func TestSidecarReconcileRecordMayNarrowAnUnplacedApp(t *testing.T) { |
| + f := sidecarSetup(t) |
| + f.putManifest(t, "eleven", SidecarManifest{Schema: 1, App: "eleven", Args: []string{"sidecar", "run"}}) |
| + f.putRecord(t, SidecarRecord{Schema: 1, Name: "bot", App: "eleven", Channel: "stable", Placement: PlacementNoInbound}) |
| + |
| + f.reconcile(t, ReconcileConfig{}) |
| + noRoute(t, f.reg, f.host("bot", "eleven"), "a record asking for no-inbound must not land on an inbound box") |
| + f.reconcile(t, ReconcileConfig{Inbound: InboundNone}) |
| + hasRoute(t, f.reg, f.host("bot", "eleven"), "a record may narrow an app that states no placement of its own") |
| +} |
| + |
| // TestSidecarReconcilePlacementChangeEvacuatesTheRow: placement is a SCOPE |
| // axis, beside region and pool, so flipping a manifest to no-inbound takes the |
| // row off a box that accepts inbound work — the same way repointing a |