generated from coulomb/repo-seed
266 lines
8.8 KiB
Markdown
266 lines
8.8 KiB
Markdown
---
|
||
id: custodian-WP-0003b
|
||
type: workplan
|
||
domain: custodian
|
||
repo: activity-core
|
||
status: done
|
||
state_hub_workstream_id: b4eb45a9-69e3-4ab0-b00c-67a53c3117c5
|
||
split_from: custodian-WP-0003
|
||
split_part: 2 of 3
|
||
depends_on:
|
||
- custodian-WP-0003a # model, rules, registry must be done first
|
||
tasks:
|
||
- id: T44
|
||
title: ActivityDefinition file parser
|
||
status: done
|
||
priority: high
|
||
state_hub_task_id: a86dcffe-789a-482e-bc5f-a5ac9db9608e
|
||
- id: T45
|
||
title: ActivityDefinition sync command
|
||
status: done
|
||
priority: medium
|
||
state_hub_task_id: a714c511-9f93-4594-b745-9330bc645384
|
||
- id: T46
|
||
title: Wire rule/instruction evaluator into RunActivityWorkflow
|
||
status: done
|
||
priority: high
|
||
state_hub_task_id: 615dec10-7e63-46b3-80ad-83ff2e51d6ba
|
||
- id: T47
|
||
title: Add ScheduledTriggerConfig (one-off future datetime)
|
||
status: done
|
||
priority: medium
|
||
state_hub_task_id: 1e06cb43-d0e3-4af5-9216-0877cac2082d
|
||
- id: T48
|
||
title: Implement one-off Temporal Schedule
|
||
status: done
|
||
priority: medium
|
||
state_hub_task_id: 01b507e3-6420-422f-83ed-90c7b4d49bb7
|
||
- id: T49
|
||
title: Implement webhook receiver
|
||
status: done
|
||
priority: medium
|
||
state_hub_task_id: 673ca7b2-49cc-4314-8be2-9848f686da37
|
||
- id: T50
|
||
title: Write Gitea webhook event type definitions
|
||
status: done
|
||
priority: medium
|
||
state_hub_task_id: 7ba289e9-fef6-469a-9504-c3664227a2f7
|
||
created: "2026-05-14"
|
||
---
|
||
|
||
# activity-core WP-0003b — Parser, Workflow Wiring, Triggers & Webhooks
|
||
|
||
**Split from:** custodian-WP-0003 (part 2 of 3)
|
||
**Hub workstream:** `b4eb45a9-69e3-4ab0-b00c-67a53c3117c5`
|
||
**Depends on:** custodian-WP-0003a (model, rules, registry)
|
||
**Next part:** custodian-WP-0003c (context adapters, integration)
|
||
**Architecture:** ACT-ADR-001, ACT-ADR-002, ACT-ADR-003
|
||
|
||
## Purpose
|
||
|
||
Phases 9 and 10 of the Event Bridge implementation. With the rules module,
|
||
IssueSink, and event type registry in place (WP-0003a), this part wires
|
||
everything into the workflow: markdown-format ActivityDefinition parser,
|
||
sync command, rule/instruction pipeline in RunActivityWorkflow, one-off
|
||
scheduled trigger type, and Gitea webhook receiver.
|
||
|
||
## Prerequisites
|
||
|
||
All tasks from custodian-WP-0003a must be done:
|
||
- `src/activity_core/rules/` module with `evaluate_condition` and `execute_instruction`
|
||
- `IssueSink` ABC + `NullSink` + `IssueCoreRestSink`
|
||
- `EventEnvelope` model finalized
|
||
- `event_types` table and `sync-event-types` command working
|
||
|
||
## Build Order
|
||
|
||
```
|
||
Phase 9:
|
||
T44 (parser) → T45 (sync) → T46 (wire workflow)
|
||
T46 also needs: T36 (RuleEvaluator), T37 (InstructionExecutor), T39 (IssueSink) from 0003a
|
||
|
||
Phase 10 (parallel with phase 9):
|
||
T47 (model) → T48 (Temporal schedule)
|
||
T49, T50 (parallel — need T40, T41 from 0003a)
|
||
```
|
||
|
||
---
|
||
|
||
## Phase 9 — ActivityDefinition as Markdown
|
||
|
||
### T44: ActivityDefinition file parser
|
||
|
||
`src/activity_core/definition_parser.py`
|
||
|
||
Scans `activity-definitions/*.md` in the local repo and any registered domain
|
||
repos listed in `ACTIVITY_DEFINITION_DIRS` env var (colon-separated paths).
|
||
|
||
Parses:
|
||
- YAML frontmatter → trigger config, context_sources, governance, owner, status.
|
||
- Fenced ` ```rule ` blocks → `RuleDef` list (YAML body).
|
||
- Fenced ` ```instruction ` blocks → `InstructionDef` list (YAML body).
|
||
|
||
Returns `ActivityDefinitionDef`. Raises `ParseError(file, line, message)` on
|
||
malformed input — never silently ignores a broken definition.
|
||
|
||
---
|
||
|
||
### T45: ActivityDefinition sync command
|
||
|
||
`make sync-activity-definitions` (also in `worker.py` startup sequence, before
|
||
the Temporal run loop).
|
||
|
||
Extend `activity_definitions` table: add `rules_json` (JSONB) and
|
||
`instructions_json` (JSONB) columns. On sync: parse all definition files,
|
||
upsert rows. Definitions absent from filesystem set `status = 'inactive'`
|
||
(soft delete — Temporal Schedules for those definitions are paused, not deleted).
|
||
|
||
Alembic migration for new columns included in this task.
|
||
|
||
---
|
||
|
||
### T46: Wire rule/instruction evaluator into RunActivityWorkflow
|
||
|
||
Replace `RunActivityWorkflow`'s current unconditional task-spawning logic:
|
||
|
||
```
|
||
load_activity_definition()
|
||
→ resolve_context() [calls each context_source adapter]
|
||
→ evaluate_rules() [RuleEvaluator — all rules, all-match]
|
||
→ execute_instructions() [InstructionExecutor — all passing pre-filters]
|
||
→ emit_tasks() [IssueSink.emit() for each TaskSpec]
|
||
→ log_run() [activity_runs row + task_spawn_log rows]
|
||
```
|
||
|
||
A rule that raises `UnsafeExpression` or any other error is skipped and logged
|
||
as `rule_eval_error`; other rules in the same run still execute. The run is not
|
||
failed by a single bad rule.
|
||
|
||
---
|
||
|
||
## Phase 10 — One-off Scheduled Trigger and Webhook Receiver
|
||
|
||
### T47: Add ScheduledTriggerConfig
|
||
|
||
`src/activity_core/models.py`
|
||
|
||
```python
|
||
@dataclass
|
||
class ScheduledTriggerConfig:
|
||
at: datetime # UTC; warns if in the past
|
||
timezone: str = "UTC"
|
||
|
||
TriggerConfig = CronTriggerConfig | EventTriggerConfig | ScheduledTriggerConfig
|
||
```
|
||
|
||
Add to ActivityDefinition frontmatter schema:
|
||
```yaml
|
||
trigger:
|
||
type: scheduled
|
||
at: "2026-09-01T09:00:00Z"
|
||
timezone: "Europe/Berlin"
|
||
```
|
||
|
||
---
|
||
|
||
### T48: Implement one-off Temporal Schedule
|
||
|
||
`src/activity_core/schedule_manager.py`
|
||
|
||
```python
|
||
async def upsert_schedule(client, defn: ActivityDefinition):
|
||
if isinstance(defn.trigger_config, ScheduledTriggerConfig):
|
||
# Create Schedule with remaining_actions=1
|
||
# Schedule ID: activity-schedule-{defn.id}-once
|
||
# start_at = defn.trigger_config.at
|
||
...
|
||
```
|
||
|
||
`sync_schedules.py`: detect `ScheduledTriggerConfig`, use one-off path.
|
||
Add `cancel_scheduled(client, activity_id)` for admin cancellation of pending
|
||
one-off definitions.
|
||
|
||
---
|
||
|
||
### T49: Implement webhook receiver
|
||
|
||
`src/activity_core/webhook_receiver.py`
|
||
|
||
FastAPI `APIRouter` mounted at `/webhooks/{source}` in `api.py`.
|
||
|
||
```
|
||
POST /webhooks/gitea → validate X-Gitea-Signature (HMAC-SHA256)
|
||
POST /webhooks/github → validate X-Hub-Signature-256 (HMAC-SHA256)
|
||
```
|
||
|
||
Per-source normalisers convert raw webhook payload to `EventEnvelope`:
|
||
- Gitea `repository` event → `gitea.repo.created`
|
||
- Gitea `push` event → `gitea.push`
|
||
- Gitea `issues` event (action=closed) → `gitea.issue.closed`
|
||
|
||
After normalisation: validate type against event type registry; publish to
|
||
NATS subject `activity.events`. Discard unknown event types with HTTP 422
|
||
(not 500 — unknown type is not a server error).
|
||
|
||
Config: `WEBHOOK_SECRET_GITEA` and `WEBHOOK_SECRET_GITHUB` env vars.
|
||
|
||
---
|
||
|
||
### T50: Write Gitea webhook event type definitions
|
||
|
||
Three files under `event-types/`:
|
||
|
||
- `gitea.repo.created.md` — publisher: gitea/webhook; attrs: repo_full_name
|
||
(string), repo_slug (string, derived from repo name), owner (string),
|
||
html_url (string), created_at (datetime).
|
||
- `gitea.push.md` — publisher: gitea/webhook; attrs: repo_full_name (string),
|
||
branch (string), pusher (string), commits_count (integer), compare_url
|
||
(string), pushed_at (datetime).
|
||
- `gitea.issue.closed.md` — publisher: gitea/webhook; attrs: repo_full_name
|
||
(string), issue_number (integer), issue_title (string), closer (string),
|
||
closed_at (datetime).
|
||
|
||
Each includes a "Normaliser mapping" subsection in Consumer Notes showing the
|
||
raw Gitea webhook field → EventEnvelope attribute mapping.
|
||
|
||
---
|
||
|
||
## Completion Criteria for This Part
|
||
|
||
1. `make sync-activity-definitions` loads definitions from `activity-definitions/`
|
||
and upserts them (with `rules_json`, `instructions_json`) cleanly
|
||
2. `RunActivityWorkflow` uses the rule/instruction pipeline end-to-end (no
|
||
unconditional task spawning)
|
||
3. One-off `scheduled` trigger type creates a Temporal Schedule with
|
||
`remaining_actions=1`
|
||
4. `POST /webhooks/gitea` validates HMAC, normalises payload, publishes to NATS
|
||
5. Three Gitea event type definitions committed and loaded by `sync-event-types`
|
||
|
||
## New Files Produced
|
||
|
||
| Path | Task |
|
||
|---|---|
|
||
| `src/activity_core/definition_parser.py` | T44 |
|
||
| `src/activity_core/webhook_receiver.py` | T49 |
|
||
| `event-types/gitea.repo.created.md` | T50 |
|
||
| `event-types/gitea.push.md` | T50 |
|
||
| `event-types/gitea.issue.closed.md` | T50 |
|
||
|
||
## Modified Files
|
||
|
||
| Path | Task | Change |
|
||
|---|---|---|
|
||
| `src/activity_core/models.py` | T47 | ScheduledTriggerConfig, TriggerConfig union |
|
||
| `src/activity_core/workflows.py` | T46 | Rule/instruction pipeline replaces unconditional spawn |
|
||
| `src/activity_core/activities.py` | T46 | resolve_context calls adapter chain |
|
||
| `src/activity_core/schedule_manager.py` | T48 | One-off schedule path |
|
||
| `src/activity_core/sync_schedules.py` | T48 | Detect ScheduledTriggerConfig |
|
||
| `src/activity_core/api.py` | T49 | Mount webhook router |
|
||
| `src/activity_core/worker.py` | T45 | sync-activity-definitions at startup |
|
||
| `alembic/versions/` | T45 | Migration: rules_json, instructions_json columns |
|
||
| `Makefile` | T45 | sync-activity-definitions target |
|
||
|
||
## Change History
|
||
|
||
- v1.0 (2026-05-14): Split from custodian-WP-0003 (phases 9–10).
|