feat(ops-mission-control): add an incident.io provider - #4951
feat(ops-mission-control): add an incident.io provider#4951stephen-meehan-lrn wants to merge 1 commit into
Conversation
78eca09 to
82a2391
Compare
Design Review (Fable 5, fork) — 🟡 CONCERNSDesign-level review of I have everything I need. The adapter is a faithful second instantiation of the provider seam — it mirrors PagerDuty's keystone-fenced identity, abstention asymmetry, and the cap+1 truncation contract, and the registry's 15s Design-Verdict: CONCERNS Sound, pattern-faithful provider addition; the two write verbs ship unexercised against the real API, and the spec's poller enumeration was not extended. Watch
Suggestions
[DESIGN-REVIEWED] f8971e9 |
GPT 5.6 Review (fork) — 🔴 changes requested (blocking)Reviewed 1 of 1 blocking finding(s) are security-class and were withheld from adjudication, so the blocking verdict stands. BLOCKING -- src/kiro_crew/apps/builtins/ops_mission_control/backend/routes.py:2142 -- Keystone write failures crash settings saves [BLOCK-MERGE] f8971e9 Adjudication (Opus 4.8) — is blocking on each finding proportionate?I have enough to rule. F1 targets the changed line at routes.py:2142 ( Fenced record for F1:
|
First Principles Review (Fable 5, fork) — 🟡 CONCERNSPremise-level review of First-Principles-Verdict: CONCERNS The new keystone write bypasses Not justified as shipped
What this change shipsIntent: let a team whose alerting lives in incident.io triage, claim and resolve those alerts from the Ops Mission Control board — an ADDITION.
Watch
Subtractions
[FIRST-PRINCIPLES-REVIEWED] f8971e9 |
UX Review (Fable 5, fork) — 🟡 CONCERNSUX-level review of UX-Verdict: CONCERNS A pattern-faithful clone of the PagerDuty provider row, but its screenshots live only on the fork — no first-time reader has seen any of it. Watch
Evidence gaps
[UX-REVIEWED] f8971e9 |
Opus 4.8 Review (fork) — ✅ no blocking findingsReviewed Review detailsI've verified the candidate against the code. The repo on disk is at the base commit, but the diff plus
The consequence is an error-response inconsistency plus a skipped failure-audit line — not data loss, corruption, privilege escalation, credential exposure, or a process crash (the write is abandoned before touching the file, failing closed). The minimal fix is in-diff. That places it as an advisory FINDING, not BLOCKING. One advisory finding: the incident.io keystone write skips the shared refusal helper every sibling write uses. FINDING — src/kiro_crew/apps/builtins/ops_mission_control/backend/routes.py:2142 — the added [OPUS-REVIEWED] f8971e9 |
82a2391 to
b92bce9
Compare
|
Both blocking findings land in
|
Head branch was pushed to by a user without write access
b92bce9 to
ba794aa
Compare
ba794aa to
26d6f55
Compare
26d6f55 to
7832b06
Compare
7832b06 to
1f44e37
Compare
Request: re-run of a failed backend shard (fork PR, no rerun permission)Could a maintainer please re-run the failed jobs on run Current head: The single failing job is Why I believe this is runner flakiness rather than a defect in the branch:
For context on why I am asking instead of pushing again: the branch has been rebased and re-pushed
Round 2's Windows shard passed on round 3 with byte-identical application code, which is what Everything else is green: 37 of 40 jobs pass, and locally the app suite is 939 passed / 44 skipped, Happy to rebase onto current |
Open PR relationship auditThis is a consolidated, point-in-time code-level audit note. It compares complete merge-base diffs and current/merged code; it does not treat a shared topic as duplication or partial coverage as completion. Relationship findings
No PR, Issue, label, branch, or review state was changed by the relationship-note portion of this audit. |
Registers `incidentio` as a public adapter implementing SignalSource, RotationSource and ActionSink over the incident.io REST API. Built on alerts rather than incidents: an alert's status is a strict firing/resolved enum, which is the same shape as a signal's own lifecycle, so absence from a poll means it cleared. A declared incident is a human artefact whose transitions run a post-incident flow, and putting one on the board would duplicate work a responder already owns. The sink offers `resolve` and `comment` only. The API has no acknowledged state and no snooze, mute or suppress call for a single alert, and advertising a verb the provider cannot perform would pass the autonomy gate and then fail at execute time, after the board had recorded the action as granted. Truncation is detected from the response cursor, not by fetching one item past the cap: 50 is the endpoint's maximum page size, so a larger request is clamped and reads back as a complete page. Alert-source filtering runs client-side because the documented filter's multi-value encoding is not specified, and a guessed encoding that matches nothing presents as a quiet estate; the truncation verdict is therefore taken from the raw response and survives filtering. The rotation identity lives on the keystone (`incidentio.user_id`), not in provider config. incident.io has no self-identity endpoint, so it cannot be derived at runtime, and provider config is agent-writable — a forged identity there would let the constrained party claim the shift and authorize a write it does not own. A source with no identity abstains rather than voting off shift, and an empty (agent-writable) schedule list with an operator identity present votes off shift rather than manufacturing an abstention.
1f44e37 to
f8971e9
Compare
|
Rebased onto main Clean rebase: no conflicts, no content changes to your commit. Gates run locally on the changed files only: Please review the rebase. A maintainer push makes the maintainer the last pusher, so under this repo's last-push rule a second approver is needed. Reply here if anything looks wrong. |
Problem / Motivation
Ops Mission Control can ingest signals from CloudWatch, Datadog, GitHub Issues and a
generic webhook, but not from incident.io. An operator whose alerting already lives in
incident.io has no way to get those alerts onto the board: they cannot be claimed,
investigated, fingerprinted against the knowledge ledger, or resolved through the app.
The on-call schedule is equally invisible, so the rotation tier has nothing to consult
for an incident.io-based team.
Why it matters
Without a provider, an incident.io shop gets no value from the board at all — the
signals it needs to triage are exactly the ones the app cannot see. Adding it also
exercises the provider seam a second time for a rotation-capable source, which is where
the authorization boundary lives.
What changed (motivation → approach → change)
Goal: make incident.io a first-class source — signals, rotation and actions —
without weakening the existing authorization model.
Approach, and why this shape:
firing/resolvedstatus, which is the same shape as a signal's own lifecycle, soabsence from a poll is meaningful. A declared incident is a human artefact with an
eight-category status whose transitions run a post-incident flow; putting one on the
board would duplicate work a responder already owns.
resolveandcommentare offered. The API has no acknowledged state andno snooze/mute/suppress for a single alert (a maintenance window is account-level
config). Advertising
ackorsilencewould pass the autonomy gate and then fail atexecute time, after the board had recorded the action as granted.
incident.io has no self-identity endpoint — an API key resolves to the key, not a
person — so the operator's own user id cannot be derived at runtime. Provider config
is agent-writable, so an identity living there could be forged to claim the shift and
authorize a write it does not own. It sits in
policy_store.OPERATOR_ONLY_KEYSasincidentio.user_id, written only by the authenticatedPUT /settings, exactly likepagerduty.user_id.ShiftStatus(on_shift=True, unknown=True)),matching the documented
unknowncontract inproviders/base.pyand PagerDuty's ownabstention branches. An empty agent-writable
schedule_idswith an operatoridentity present votes off shift instead, so a config write cannot manufacture the
abstention the tier gate treats as a non-vote.
The alerts endpoint caps a page at 50, well under the registry's 100-signal cap, so
stopping at one page made any estate over 50 firing alerts report a truncated poll on
every cycle — permanently non-authoritative, so
reconcilecould never resolve oneof this source's signals. The walk now pages until the estate is exhausted, and the
verdict is computed from whether the final slice actually discarded anything rather
than from whichever branch ended the loop. That last part is deliberate: three
separate review findings in this loop were all the same defect — a branch ending the
walk while the verdict disagreed with what the slice dropped — so deriving the two
from one fact makes "dropped an alert but called the poll complete" unrepresentable.
Also: client-side filtering of
alert_source_ids, because the endpoint documents analert_source[one_of]filter but not how to encode multiple values, and a guessedencoding that matches nothing presents as a quiet estate.
Tests
tests/test_providers_incidentio.py(15 tests):empty agent-writable
schedule_idswith an operator identity present votes off shiftrather than abstaining; a genuinely unconfigured provider abstains and fails open; the
effective schedule (
final) decides rather than the pre-override rotation rules, soa covered shift is not read as ours.
supported_actions()is exactly{resolve, comment}, andack/silenceare refused without the provider ever being called.made and carries
after) rather than reported as truncation; an estate past the capreports truncated; a terminal page that overshoots the cap still reports truncated
(the regression guard for a dropped-but-called-complete poll); an estate of exactly
the cap is not truncated; an empty page carrying a cursor is truncation, while an
empty terminal page without one stays authoritative; client-side source filtering
preserves the verdict.
provider_keywhenpresent, and the alert id is carried in labels for the action path.
Three existing tests in
tests/test_providers.pywere extended to assert the adapterregisters and reports unconfigured rather than raising.
Manual verification
Run against a real incident.io account in an isolated pod (own
KIROCREW_HOME, nocrons), not the live gateway. The provider polled 50 firing alerts and the Signals
tab showed the source
ok. That run is what surfaced the one-page truncation defectabove — 50 firing alerts is exactly one page, so the source was permanently
non-authoritative and no unit test had reason to notice.
The truncation fixes were then proven rather than assumed: reverting the production
hunk while keeping the tests makes
test_a_terminal_page_that_overshoots_the_cap_reports_truncatedfail (1 failed / 14passed), and restoring it returns 15 passed.
Local gate floor green on this commit: pytest (926 passed / 44 skipped in the app
suite), black, isort, flake8, mypy,
tsc -b, eslint, vitest, andi18n:checkat18/18 — that last one initially failed and is what forced real translations rather than
English placeholders in the non-English catalogs.
Screenshots / video
The provider card, showing the new keystone-fenced identity field, its help text, and
the roles the adapter registers:
The Signals tab with the source live against a real account:
Related Issues
no linked issue: this adds a provider that was not tracked by an existing issue.
Checklist
feat|fix|docs|refactor|perf|test|chore|ci|build|revert: ...)