Add pending_decisions state-hub query + monthly secrets-elevation review
New context_resolvers/state_hub.py query type: pending_decisions, a thin GET /decisions/ passthrough (topic_id/workstream_id/workplan_id/decision_type passed through, status defaults to open). Generic -- not special-cased to any one decision. New activity-definitions/monthly-secrets-elevation-review.md: fires 08:00 Berlin on the 1st of each month, sweeps open State Hub decisions under the infotech/reuse-surface topic, and opens a review task for each. First target: the temporary autoMode.allow/permissions.allow elevation added to ~/.claude/settings.json on 2026-07-07 (decision 11bf5cbf-458d-4275-a870- 77a82b4058b9, deadline 2026-07-31) for ops-warden/kubectl/OpenBao secret reads. Requested by Bernd: no existing scheduling mechanism (session-only cron, cloud routines with no local access) can durably re-check a local security posture a month out -- this closes that gap using activity-core's own durable Temporal-backed trigger instead. Verified: definition_parser.parse_file + scan_and_parse load it cleanly alongside the two existing definitions; new resolver tests pass (20/20 in that file); pending_decisions confirmed against the live local State Hub. Full suite: 241 passed, 2 pre-existing unrelated failures (confirmed via git stash -- present before this change too). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
ce03e78e26
commit
d563d516f5
3 changed files with 116 additions and 0 deletions
59
activity-definitions/monthly-secrets-elevation-review.md
Normal file
59
activity-definitions/monthly-secrets-elevation-review.md
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
---
|
||||
id: monthly-secrets-elevation-review
|
||||
name: Monthly Secrets-Elevation Review
|
||||
enabled: true
|
||||
owner: custodian-agent
|
||||
governance: custodian
|
||||
status: active
|
||||
trigger:
|
||||
type: cron
|
||||
cron_expression: "0 8 1 * *"
|
||||
timezone: Europe/Berlin
|
||||
misfire_policy: skip
|
||||
context_sources:
|
||||
- type: state-hub
|
||||
query: pending_decisions
|
||||
params:
|
||||
topic_id: f39fa2a3-c491-414c-a91b-b4c5fcc6139c
|
||||
status: open
|
||||
bind_to: context.decisions
|
||||
---
|
||||
|
||||
# Monthly Secrets-Elevation Review
|
||||
|
||||
Runs 08:00 Berlin time on the 1st of every month. Checks whether any
|
||||
temporary, broad credential/secrets-access elevations recorded as State Hub
|
||||
decisions (topic: infotech / reuse-surface) are still open past their
|
||||
review deadline, and opens a task to force a human yes/no on each one.
|
||||
|
||||
This definition exists because ad-hoc scheduling mechanisms (session-only
|
||||
cron, cloud routines with no local access) cannot reliably re-check a local
|
||||
security posture a month later — the check has to live somewhere durable.
|
||||
`pending_decisions` (added alongside this definition) is a thin, generic
|
||||
`GET /decisions/` passthrough, not special-cased to this one elevation, so
|
||||
any future time-boxed decision under this topic gets the same monthly
|
||||
check for free.
|
||||
|
||||
```rule
|
||||
id: flag-overdue-elevation-decisions
|
||||
for_each: context.decisions
|
||||
bind_as: decision
|
||||
condition: 'context.decision.status == "open"'
|
||||
action:
|
||||
task_template: 'Review time-boxed decision: {context.decision.title}'
|
||||
description: 'Deadline {context.decision.deadline}. Rationale: {context.decision.rationale} Either resolve via resolve_decision() (elevation still needed -- extend deadline) or confirm removal (elevation no longer needed -- revert the settings.json entries added for it) and resolve as done.'
|
||||
target_repo: reuse-surface
|
||||
priority: medium
|
||||
labels: ["security", "access-review", "automated"]
|
||||
```
|
||||
|
||||
`pending_decisions` returns every open decision under this topic, not just
|
||||
the secrets-elevation one -- any decision-maker who records a pending
|
||||
decision with a `deadline` under this topic gets swept into the same
|
||||
monthly nudge, which is deliberate rather than a limitation to fix later.
|
||||
|
||||
The first target is the temporary `autoMode.allow`/`permissions.allow`
|
||||
elevation added to `~/.claude/settings.json` on 2026-07-07 (State Hub
|
||||
decision `11bf5cbf-458d-4275-a870-77a82b4058b9`, deadline 2026-07-31) for
|
||||
ops-warden/kubectl/OpenBao secret reads, granted to consolidate the
|
||||
reuse-surface hub write token and audit secrets-management locations.
|
||||
|
|
@ -9,6 +9,8 @@ Supported queries:
|
|||
- next_steps: GET {STATE_HUB_URL}/state/next_steps
|
||||
- workplan_index: GET {STATE_HUB_URL}/workstreams/workplan-index
|
||||
- hub_inbox: GET {STATE_HUB_URL}/messages/?to_agent=hub&unread_only=true
|
||||
- pending_decisions: GET {STATE_HUB_URL}/decisions/?status=open (topic_id/
|
||||
workstream_id/decision_type passed through as given)
|
||||
- coding_retro: latest /progress/ item with event_type=coding_retro
|
||||
- daily_triage_digest: curated scalar JSON digest for daily WSJF triage
|
||||
- recently_on_scope_hourly: POST {STATE_HUB_URL}/recently-on-scope/hourly
|
||||
|
|
@ -111,6 +113,14 @@ class StateHubContextResolver(ContextResolver):
|
|||
"unread_only": params.get("unread_only", True),
|
||||
}
|
||||
return _fetch_json("/messages/", query_params)
|
||||
if query == "pending_decisions":
|
||||
query_params = {
|
||||
key: params[key]
|
||||
for key in ("topic_id", "workstream_id", "workplan_id", "decision_type")
|
||||
if key in params
|
||||
}
|
||||
query_params["status"] = params.get("status", "open")
|
||||
return _fetch_json("/decisions/", query_params)
|
||||
if query == "coding_retro":
|
||||
return _coding_retro(params)
|
||||
if query == "daily_triage_digest":
|
||||
|
|
|
|||
|
|
@ -77,6 +77,53 @@ def test_daily_triage_queries(monkeypatch) -> None:
|
|||
]
|
||||
|
||||
|
||||
def test_pending_decisions_query(monkeypatch) -> None:
|
||||
calls: list[dict[str, Any]] = []
|
||||
|
||||
def fake_get(url: str, **kwargs: Any) -> DummyResponse:
|
||||
calls.append({"url": url, **kwargs})
|
||||
return DummyResponse([{"id": "d1", "status": "open", "deadline": "2026-07-31T00:00:00Z"}])
|
||||
|
||||
monkeypatch.setenv("STATE_HUB_URL", "http://state-hub.test")
|
||||
monkeypatch.setattr(httpx, "get", fake_get)
|
||||
resolver = StateHubContextResolver()
|
||||
|
||||
result = resolver.resolve(
|
||||
"pending_decisions", None, {"topic_id": "topic-1", "workstream_id": "wp-1"}
|
||||
)
|
||||
|
||||
assert result == [{"id": "d1", "status": "open", "deadline": "2026-07-31T00:00:00Z"}]
|
||||
assert calls == [
|
||||
{
|
||||
"url": "http://state-hub.test/decisions/",
|
||||
"params": {"topic_id": "topic-1", "workstream_id": "wp-1", "status": "open"},
|
||||
"timeout": 10.0,
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_pending_decisions_query_defaults_status_open(monkeypatch) -> None:
|
||||
calls: list[dict[str, Any]] = []
|
||||
|
||||
def fake_get(url: str, **kwargs: Any) -> DummyResponse:
|
||||
calls.append({"url": url, **kwargs})
|
||||
return DummyResponse([])
|
||||
|
||||
monkeypatch.setenv("STATE_HUB_URL", "http://state-hub.test")
|
||||
monkeypatch.setattr(httpx, "get", fake_get)
|
||||
resolver = StateHubContextResolver()
|
||||
|
||||
resolver.resolve("pending_decisions", None, {})
|
||||
|
||||
assert calls == [
|
||||
{
|
||||
"url": "http://state-hub.test/decisions/",
|
||||
"params": {"status": "open"},
|
||||
"timeout": 10.0,
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_existing_queries_still_resolve(monkeypatch) -> None:
|
||||
calls: list[dict[str, Any]] = []
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue