feat(operator): containment set + dual-writer deny rule (OPS-1214) - #136
Merged
Conversation
The rebuild gate for the in-cluster operator (ecosystem plan 2026-08-16, operator-ratified): - HOLD switch: LINEARCTL_HOLD=1 or a hold file (default /etc/linearctl/hold/HOLD, ConfigMap-mounted) pauses role runs, queue polling (messages stay queued, no pull-then-ack), and /delegate (503). Mechanical handle for the estate hold-batch-ops SOP — no redeploy needed. - Mutation budget: per-run cap on issue writes (LINEARCTL_MUTATION_BUDGET, default 10). Truncation is narrated with exact dropped counts, never silent. Dry-run previews the same capped slice an apply would take. - Rate-limit preflight: the operator's role scheduler probes Linear quota before each run (fetchRateLimit now takes a full Authorization value so OAuth actors can probe); exhausted quota skips the tick, probe errors fail open. Bare scheduleRole keeps a sync HOLD-only gate (no network). - Dual-writer deny rule (plan section 2b2, soma split): issues labeled soma-ingest (extendable via LINEARCTL_DENY_LABELS) are read-only to this operator entirely. Enforced at the single guardrail checkpoint via ProposedAction.targetLabels + partitionDeniedTargets for batches; the stale sweep now carries label names (labels selected in the flat issues query) and grooming partitions before labeling; intake-triage checks its comment target's live labels before posting. - Chart 0.3.0: optional <release>-hold ConfigMap mounted read-only at /etc/linearctl/hold.
…ied findings)
- Hold ConfigMap is no longer chart-managed: a chart-rendered `data: {}`
is reverted by any sync/self-heal, silently RELEASING an engaged hold
mid-incident. The mount stays optional; the ConfigMap is operator-
created (untracked by GitOps, survives sync). Engage/release documented
in the deployment template.
- Deny partition moved INTO applyStaleLabel (the write layer): the
operator role, `linearctl stale --apply`, and future callers exclude
deny-labeled issues by construction; denied identifiers are returned
and surfaced by every caller. The watch path's state move
(moveToStartedIfDelegated) now label-checks too — a deny-labeled
delegated issue gets session activities only, no issue mutation.
- LINEARCTL_DENY_LABELS now EXTENDS the built-in set instead of
replacing it — an empty or typo'd deploy value can no longer silently
drop the partition. Label comparison trims + lowercases.
- LINEARCTL_HOLD_FILE="" falls back to the default path (was: silently
disabled the file hold — existsSync("") is always false).
- intake-triage's comment path degrades gracefully: budget-zero, denied
target, or fetch failure skips ONLY the comment and keeps the
read-only summary (was: three throw sites made every cadence fail
forever under the documented write-freeze).
- /readyz stays green while held (heartbeat maintained; `held: true`
surfaced) — a hold reads as held, not as an outage, and rollouts
during a hold can pass readiness.
- Scheduler: bare scheduleRole is now explicitly UNGATED (pass-through
default, honest JSDoc — the daemon wires the full preflight); `stopped`
re-checked after the async preflight so SIGTERM during the quota probe
cannot launch a fresh mutating run.
- Comments reworded to public-repo-appropriate terms: internal plan
sections and program narrative removed; the partition rationale now
cites docs/funnel-contract.md (already public) only.
todie
added a commit
that referenced
this pull request
Aug 16, 2026
CF API tokens for Queues may scope account-wide (per-queue resource
scoping unverified — runbook step 3 carries the caveat), so a producer
token could forge AgentSessionEvents into linear-agent-events. Defense:
the webhook receiver signs every body at enqueue; the operator verifies
before processing, so queue-level write access alone cannot reach the
event loop.
- src/core/hmac-envelope.ts: v1 envelope {v, alg, ts, sig, body}; sig =
HMAC-SHA256(key, "v1\n"+audience+"\n"+ts+"\n"+body). Audience (queue
name) binding kills cross-queue replay; the MAC-covered timestamp
bounds same-queue replay to a 10-min window. Fixed-set failure reasons
(no attacker-controlled interpolation), 64-hex sig format check before
any hashing, constant-time digest compare. Replay WITHIN the window by
a queue-read-capable principal remains possible — documented v1 limit.
- Poller: key/prev-key/mode resolved ONCE at startup (injectable via
OperatorOptions.hmac); SET-but-empty key refuses startup; posture
logged at startup and surfaced in /readyz (hmac: off|warn|enforce);
"warn" migration mode processes non-verifying messages with an
UNVERIFIED log line (closes the rollout-window reject+ack data-loss
trap); LINEARCTL_QUEUE_HMAC_KEY_PREV accepted during rotation.
- Control socket hardened to 0600 (dir 0700) — the /delegate trust
boundary is filesystem perms, not umask luck.
- Chart: queueHmacKey/queueHmacKeyPrev/queueHmacMode env (guarded);
values.yaml documents the 4-step rollout order + rotation. Runbook:
step-3 scoping caveat, step-4 env + envelope format, troubleshooting
rows for every rejection reason.
Rebuilt as one commit on post-#136 main (supersedes 85867b2+a4959db;
review findings from the 8-angle pass are incorporated).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The rebuild gate for the in-cluster operator, per the operator-ratified ecosystem plan (2026-08-16) and the OPS-1214 pause+rebuild ruling.
What
LINEARCTL_HOLD=1or hold-file existence (default/etc/linearctl/hold/HOLD, mounted from an optional<release>-holdConfigMap) pauses role runs, queue polling (messages stay queued — no pull-then-ack), and/delegate(503). The mechanical handle for the estate hold-batch-ops SOP; flips without a redeploy.LINEARCTL_MUTATION_BUDGET, default 10); truncation narrated with exact dropped counts. Dry-run previews the same capped slice an apply would take, so the log tells the truth about apply behavior.scheduleRolekeeps a sync HOLD-only gate — no network in tests.soma-ingest-labeled issues are read-only to this operator entirely (soma keys idempotency onupdatedAt; even additive comments perturb it). Enforced at the single guardrail checkpoint (targetLabels+partitionDeniedTargets); the stale sweep now carries label names; grooming partitions before labeling and logs the denied slice; intake-triage checks its comment target's live labels.Verification
bun run typecheckclean;bun test500 pass / 0 fail (new: containment, scheduler-preflight suites).helm lintclean.Write-enablement gating note: this PR merging is one of the three redeploy conditions (containment merged + deny rule + writer-inventory fold into the split doc). Redeploy itself remains operator-gated.