From d563d516f506ff519f77223d600453ef1e8e0822 Mon Sep 17 00:00:00 2001 From: tegwick Date: Tue, 7 Jul 2026 15:26:00 +0200 Subject: [PATCH] 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 --- .../monthly-secrets-elevation-review.md | 59 +++++++++++++++++++ .../context_resolvers/state_hub.py | 10 ++++ tests/test_state_hub_context_resolver.py | 47 +++++++++++++++ 3 files changed, 116 insertions(+) create mode 100644 activity-definitions/monthly-secrets-elevation-review.md diff --git a/activity-definitions/monthly-secrets-elevation-review.md b/activity-definitions/monthly-secrets-elevation-review.md new file mode 100644 index 0000000..740aaa9 --- /dev/null +++ b/activity-definitions/monthly-secrets-elevation-review.md @@ -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. diff --git a/src/activity_core/context_resolvers/state_hub.py b/src/activity_core/context_resolvers/state_hub.py index 0ebd140..f535bd5 100644 --- a/src/activity_core/context_resolvers/state_hub.py +++ b/src/activity_core/context_resolvers/state_hub.py @@ -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": diff --git a/tests/test_state_hub_context_resolver.py b/tests/test_state_hub_context_resolver.py index 39b0b6b..9458cbf 100644 --- a/tests/test_state_hub_context_resolver.py +++ b/tests/test_state_hub_context_resolver.py @@ -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]] = []