spine-framework 0.4.4 → 0.5.0-rc.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +13 -3
- package/.framework/cli/commands/bootstrap.ts +392 -0
- package/.framework/cli/commands/dev.ts +91 -218
- package/.framework/cli/commands/doctor.ts +132 -26
- package/.framework/cli/commands/generate.ts +38 -298
- package/.framework/cli/commands/init.ts +20 -1
- package/.framework/cli/commands/migrate.ts +36 -30
- package/.framework/cli/commands/migrations.ts +16 -56
- package/.framework/cli/commands/upgrade.ts +35 -0
- package/.framework/cli/commands/validate-schema.ts +147 -0
- package/.framework/cli/context.ts +223 -110
- package/.framework/cli/index.ts +6 -0
- package/.framework/cli/lib/project-files.ts +241 -1
- package/.framework/functions/_shared/hash-api-key.ts +183 -79
- package/.framework/functions/_shared/logging.ts +562 -224
- package/.framework/functions/_shared/middleware.ts +47 -44
- package/.framework/functions/admin-data.ts +39 -28
- package/.framework/functions/ai-agents.ts +10 -6
- package/.framework/functions/api-keys.ts +301 -122
- package/.framework/functions/apps.ts +43 -26
- package/.framework/functions/cil-ingest.ts +48 -15
- package/.framework/functions/debug-auth.ts +14 -8
- package/.framework/functions/deliveries.ts +39 -12
- package/.framework/functions/embeddings.ts +145 -76
- package/.framework/functions/integration-outbound.ts +67 -27
- package/.framework/functions/integrations.ts +254 -127
- package/.framework/functions/logs.ts +273 -133
- package/.framework/functions/observability.ts +277 -147
- package/.framework/functions/pipeline-executions.ts +1 -1
- package/.framework/functions/pipelines.ts +14 -9
- package/.framework/functions/prompt-configs.ts +7 -4
- package/.framework/functions/roles.ts +8 -5
- package/.framework/functions/security.ts +30 -17
- package/.framework/functions/system-cron.ts +240 -127
- package/.framework/functions/tests.ts +7 -6
- package/.framework/functions/timers.ts +310 -107
- package/.framework/functions/triggers.ts +28 -13
- package/.framework/functions/types.ts +13 -10
- package/.framework/migrations/069_log_stats_window.sql +54 -0
- package/.framework/migrations/070_app_type_schema_rpcs.sql +166 -0
- package/.framework/migrations/071_timer_interval_keys.sql +96 -0
- package/.framework/migrations/072_slug_autofill.sql +77 -0
- package/.framework/migrations/073_seed_member_roles.sql +26 -0
- package/.framework/src/apps/admin/index.tsx +1141 -775
- package/.framework/src/components/ErrorBoundary.tsx +136 -0
- package/.framework/src/components/admin/AdminListPage.tsx +81 -73
- package/.framework/src/components/admin/AdminSidebar.tsx +88 -79
- package/.framework/src/components/admin/EntitySelect.tsx +181 -0
- package/.framework/src/components/admin/PageInstructions.tsx +86 -127
- package/.framework/src/components/app-shell/GenericDetailPage.tsx +37 -13
- package/.framework/src/components/app-shell/GenericListPage.tsx +28 -32
- package/.framework/src/components/layout/Sidebar.tsx +300 -177
- package/.framework/src/components/runtime/DataDetailPage.tsx +67 -35
- package/.framework/src/components/runtime/DataHeader.tsx +96 -48
- package/.framework/src/components/runtime/DataListPage.tsx +3 -26
- package/.framework/src/components/runtime/DataStats.tsx +10 -10
- package/.framework/src/components/runtime/DataTable.tsx +12 -10
- package/.framework/src/components/ui/sheet.tsx +24 -26
- package/.framework/src/components/ui/textarea.tsx +74 -48
- package/.framework/src/hooks/useEntityList.ts +12 -9
- package/.framework/src/hooks/useListSchema.ts +108 -58
- package/.framework/src/instructions/README.md +57 -0
- package/.framework/src/instructions/ai-agents.md +69 -0
- package/.framework/src/instructions/alerts.md +80 -0
- package/.framework/src/instructions/api-keys.md +87 -0
- package/.framework/src/instructions/apps.md +77 -0
- package/.framework/src/instructions/concepts/ai-and-prompts.md +150 -0
- package/.framework/src/instructions/concepts/apps-and-renderers.md +162 -0
- package/.framework/src/instructions/concepts/auth-and-api-keys.md +133 -0
- package/.framework/src/instructions/concepts/automation.md +186 -0
- package/.framework/src/instructions/concepts/concurrency-and-idempotency.md +125 -0
- package/.framework/src/instructions/concepts/core-vs-custom.md +124 -0
- package/.framework/src/instructions/concepts/design-schema.md +152 -0
- package/.framework/src/instructions/concepts/integrations-and-webhooks.md +127 -0
- package/.framework/src/instructions/concepts/observability.md +116 -0
- package/.framework/src/instructions/concepts/permissions.md +150 -0
- package/.framework/src/instructions/concepts/runtime-records.md +111 -0
- package/.framework/src/instructions/concepts/scope-and-accounts.md +112 -0
- package/.framework/src/instructions/concepts/spine-way.md +172 -0
- package/.framework/src/instructions/deliveries.md +59 -0
- package/.framework/src/instructions/guide/00-orientation.md +106 -0
- package/.framework/src/instructions/guide/01-install-and-first-run.md +253 -0
- package/.framework/src/instructions/guide/02-project-map.md +147 -0
- package/.framework/src/instructions/guide/03-domain-model.md +223 -0
- package/.framework/src/instructions/guide/04-first-app.md +227 -0
- package/.framework/src/instructions/guide/05-glossary.md +183 -0
- package/.framework/src/instructions/integrations.md +96 -0
- package/.framework/src/instructions/internal/cil-markers.md +83 -0
- package/.framework/src/instructions/invites.md +60 -0
- package/.framework/src/instructions/kb.md +80 -0
- package/.framework/src/instructions/logs.md +77 -0
- package/.framework/src/instructions/observability.md +61 -0
- package/.framework/src/instructions/operations/deployment.md +115 -0
- package/.framework/src/instructions/operations/migrations.md +125 -0
- package/.framework/src/instructions/operations/security-checklist.md +148 -0
- package/.framework/src/instructions/operations/troubleshooting.md +170 -0
- package/.framework/src/instructions/operations/upgrade.md +116 -0
- package/.framework/src/instructions/pipeline-executions.md +62 -0
- package/.framework/src/instructions/pipelines.md +78 -0
- package/.framework/src/instructions/prompts.md +95 -0
- package/.framework/src/instructions/reference/api/admin-data.md +162 -0
- package/.framework/src/instructions/reference/api/auth.md +113 -0
- package/.framework/src/instructions/reference/api/errors-and-envelopes.md +113 -0
- package/.framework/src/instructions/reference/app-manifest.md +150 -0
- package/.framework/src/instructions/reference/cli.md +124 -0
- package/.framework/src/instructions/reference/database.md +132 -0
- package/.framework/src/instructions/reference/design-schema.md +280 -0
- package/.framework/src/instructions/reference/env-vars.md +133 -0
- package/.framework/src/instructions/reference/functions/account-nodes.md +75 -0
- package/.framework/src/instructions/reference/functions/admin-data.md +195 -0
- package/.framework/src/instructions/reference/functions/admin-funnel.md +90 -0
- package/.framework/src/instructions/reference/functions/admin-install-funnel.md +107 -0
- package/.framework/src/instructions/reference/functions/admin-signals.md +102 -0
- package/.framework/src/instructions/reference/functions/ai-agents.md +148 -0
- package/.framework/src/instructions/reference/functions/ai-interactions.md +156 -0
- package/.framework/src/instructions/reference/functions/api-keys.md +130 -0
- package/.framework/src/instructions/reference/functions/apps.md +140 -0
- package/.framework/src/instructions/reference/functions/auth.md +99 -0
- package/.framework/src/instructions/reference/functions/debug-auth.md +91 -0
- package/.framework/src/instructions/reference/functions/deliveries.md +110 -0
- package/.framework/src/instructions/reference/functions/embeddings.md +166 -0
- package/.framework/src/instructions/reference/functions/eval-harness.md +149 -0
- package/.framework/src/instructions/reference/functions/index.md +103 -0
- package/.framework/src/instructions/reference/functions/integration-outbound.md +200 -0
- package/.framework/src/instructions/reference/functions/integration-routes.md +151 -0
- package/.framework/src/instructions/reference/functions/integrations.md +130 -0
- package/.framework/src/instructions/reference/functions/invites.md +182 -0
- package/.framework/src/instructions/reference/functions/item-progress.md +107 -0
- package/.framework/src/instructions/reference/functions/kb.md +166 -0
- package/.framework/src/instructions/reference/functions/logs.md +141 -0
- package/.framework/src/instructions/reference/functions/observability.md +105 -0
- package/.framework/src/instructions/reference/functions/pipeline-executions.md +132 -0
- package/.framework/src/instructions/reference/functions/pipeline-runner-background.md +121 -0
- package/.framework/src/instructions/reference/functions/pipelines.md +136 -0
- package/.framework/src/instructions/reference/functions/prompt-configs.md +121 -0
- package/.framework/src/instructions/reference/functions/roles.md +121 -0
- package/.framework/src/instructions/reference/functions/security.md +140 -0
- package/.framework/src/instructions/reference/functions/system-cron.md +136 -0
- package/.framework/src/instructions/reference/functions/system.md +106 -0
- package/.framework/src/instructions/reference/functions/tests.md +97 -0
- package/.framework/src/instructions/reference/functions/timers.md +117 -0
- package/.framework/src/instructions/reference/functions/trigger-scheduler.md +102 -0
- package/.framework/src/instructions/reference/functions/triggers.md +140 -0
- package/.framework/src/instructions/reference/functions/types.md +146 -0
- package/.framework/src/instructions/reference/seeds.md +126 -0
- package/.framework/src/instructions/reference/shared-modules.md +127 -0
- package/.framework/src/instructions/reference/spine-config.md +114 -0
- package/.framework/src/instructions/roles.md +80 -0
- package/.framework/src/instructions/runtime-detail.md +70 -0
- package/.framework/src/instructions/runtime-list.md +77 -0
- package/.framework/src/instructions/security.md +67 -0
- package/.framework/src/instructions/tasks/add-a-pipeline.md +110 -0
- package/.framework/src/instructions/tasks/add-a-role.md +96 -0
- package/.framework/src/instructions/tasks/add-a-timer.md +114 -0
- package/.framework/src/instructions/tasks/add-a-trigger.md +106 -0
- package/.framework/src/instructions/tasks/add-a-type.md +151 -0
- package/.framework/src/instructions/tasks/add-a-webhook.md +108 -0
- package/.framework/src/instructions/tasks/add-an-ai-agent.md +108 -0
- package/.framework/src/instructions/tasks/add-an-integration.md +124 -0
- package/.framework/src/instructions/tasks/build-a-custom-page.md +118 -0
- package/.framework/src/instructions/tasks/package-and-install-an-app.md +127 -0
- package/.framework/src/instructions/tasks/test-your-code.md +108 -0
- package/.framework/src/instructions/tasks/write-a-function.md +132 -0
- package/.framework/src/instructions/tasks/write-a-migration.md +110 -0
- package/.framework/src/instructions/testing.md +59 -0
- package/.framework/src/instructions/timers.md +82 -0
- package/.framework/src/instructions/triggers.md +69 -0
- package/.framework/src/instructions/types.md +93 -0
- package/.framework/src/pages/NotFoundPage.tsx +62 -31
- package/.framework/src/pages/admin/AIAgentDetailPage.tsx +117 -106
- package/.framework/src/pages/admin/AIAgentsPage.tsx +86 -48
- package/.framework/src/pages/admin/APIKeyDetailPage.tsx +153 -152
- package/.framework/src/pages/admin/APIKeysPage.tsx +91 -90
- package/.framework/src/pages/admin/AlertsConfigPage.tsx +29 -18
- package/.framework/src/pages/admin/AppDetailPage.tsx +6 -60
- package/.framework/src/pages/admin/AppsPage.tsx +104 -66
- package/.framework/src/pages/admin/DeliveriesPage.tsx +5 -5
- package/.framework/src/pages/admin/EmbeddingDetailPage.tsx +5 -23
- package/.framework/src/pages/admin/EmbeddingsPage.tsx +5 -26
- package/.framework/src/pages/admin/IntegrationDetailPage.tsx +7 -7
- package/.framework/src/pages/admin/IntegrationsPage.tsx +85 -43
- package/.framework/src/pages/admin/LogsPage.tsx +144 -76
- package/.framework/src/pages/admin/ObservabilityDashboard.tsx +223 -140
- package/.framework/src/pages/admin/PipelineDetailPage.tsx +134 -113
- package/.framework/src/pages/admin/PipelineExecutionsPage.tsx +108 -45
- package/.framework/src/pages/admin/PipelinesPage.tsx +160 -115
- package/.framework/src/pages/admin/PromptConfigDetailPage.tsx +96 -98
- package/.framework/src/pages/admin/PromptConfigsPage.tsx +101 -59
- package/.framework/src/pages/admin/RoleDetailPage.tsx +90 -79
- package/.framework/src/pages/admin/RolesPage.tsx +86 -43
- package/.framework/src/pages/admin/SecurityPage.tsx +1 -1
- package/.framework/src/pages/admin/TestingDashboard.tsx +16 -28
- package/.framework/src/pages/admin/TimerDetailPage.tsx +117 -100
- package/.framework/src/pages/admin/TimersPage.tsx +90 -62
- package/.framework/src/pages/admin/TriggerDetailPage.tsx +90 -83
- package/.framework/src/pages/admin/TriggersPage.tsx +124 -99
- package/.framework/src/pages/admin/TypeDetailPage.tsx +152 -138
- package/.framework/src/pages/admin/TypesPage.tsx +90 -48
- package/.framework/src/pages/admin/cil/CILAgentPage.tsx +17 -25
- package/.framework/src/pages/admin/cil/CILChunkDetailPage.tsx +17 -27
- package/.framework/src/pages/admin/cil/CILDashboardPage.tsx +72 -40
- package/.framework/src/pages/admin/cil/CILFileListPage.tsx +321 -129
- package/.framework/src/pages/admin/cil/CILFileSelectPage.tsx +18 -29
- package/.framework/src/pages/admin/cil/CILGraphPage.tsx +26 -38
- package/.framework/src/pages/admin/cil/CILIngestionPage.tsx +34 -46
- package/.framework/src/pages/admin/cil/CILProjectsPage.tsx +189 -103
- package/.framework/src/pages/admin/cil/CILSearchPage.tsx +246 -136
- package/.framework/src/pages/admin/cil/CILSuggestionsPage.tsx +11 -22
- package/.framework/src/pages/admin/cil/CILVersionsPage.tsx +9 -20
- package/.framework/src/pages/admin/cil/FlexiblePipelinePage.tsx +26 -38
- package/.framework/src/pages/admin/kb/KBAdminPage.tsx +7 -26
- package/.framework/templates/agent/AGENTS.md +150 -0
- package/.framework/templates/agent/skills/spine-add-ai-agent/SKILL.md +58 -0
- package/.framework/templates/agent/skills/spine-add-automation/SKILL.md +62 -0
- package/.framework/templates/agent/skills/spine-add-integration/SKILL.md +63 -0
- package/.framework/templates/agent/skills/spine-architect/SKILL.md +86 -0
- package/.framework/templates/agent/skills/spine-install-app/SKILL.md +72 -0
- package/.framework/templates/agent/skills/spine-migration/SKILL.md +75 -0
- package/.framework/templates/agent/skills/spine-new-app/SKILL.md +67 -0
- package/.framework/templates/agent/skills/spine-new-function/SKILL.md +65 -0
- package/.framework/templates/agent/skills/spine-new-type/SKILL.md +65 -0
- package/.framework/templates/agent/skills/spine-schema/SKILL.md +77 -0
- package/.framework/templates/agent/skills/spine-test/SKILL.md +67 -0
- package/.framework/templates/agent/skills/spine-troubleshoot/SKILL.md +74 -0
- package/.framework/templates/agent/skills/spine-upgrade/SKILL.md +69 -0
- package/.framework/templates/project.json +4 -1
- package/.framework/tests/unit/account-scoping-gates.test.ts +392 -0
- package/.framework/tests/unit/agent-files.test.ts +355 -0
- package/.framework/tests/unit/agent-runner.test.ts +479 -44
- package/.framework/tests/unit/api-keys-create-scope-gate.test.ts +160 -0
- package/.framework/tests/unit/api-keys-omit-secret-fields.test.ts +38 -0
- package/.framework/tests/unit/bootstrap.test.ts +242 -0
- package/.framework/tests/unit/cli-context-api-key.test.ts +114 -0
- package/.framework/tests/unit/debug-auth.test.ts +38 -0
- package/.framework/tests/unit/deliveries-account-scoping.test.ts +185 -0
- package/.framework/tests/unit/integration-outbound-credentials.test.ts +167 -0
- package/.framework/tests/unit/integrations-audit-redaction.test.ts +130 -0
- package/.framework/tests/unit/logging-app-id.test.ts +75 -0
- package/.framework/tests/unit/system-context-denial.test.ts +145 -0
- package/.framework/tests/unit/system-cron.test.ts +240 -0
- package/.framework/tests/unit/timers.test.ts +159 -0
- package/README.md +125 -166
- package/config/tsconfig.cli.json +2 -2
- package/package.json +15 -4
- package/templates/project/package-lock.json +2 -2
- package/.framework/docs/admin-routes.md +0 -76
- package/.framework/docs/api-docs-progress.md +0 -38
- package/.framework/docs/api-governance.md +0 -146
- package/.framework/docs/api-testing-results.md +0 -212
- package/.framework/docs/assembly-launch-guide.md +0 -138
- package/.framework/docs/audit-results.md +0 -590
- package/.framework/docs/db-api-inventory.md +0 -95
- package/.framework/docs/gap-remediation-backlog.md +0 -103
- package/.framework/docs/ui-adoption-verification.md +0 -111
- package/.framework/docs/ui-api-coverage.md +0 -84
- package/.framework/docs/v2-compatibility-audit.md +0 -228
- package/.framework/mcp/index.ts +0 -133
- package/.framework/mcp/load-env.ts +0 -60
- package/.framework/mcp/shared/app-manifest.ts +0 -275
- package/.framework/mcp/shared/db.ts +0 -477
- package/.framework/mcp/shared/kb.ts +0 -118
- package/.framework/mcp/tools/architect.ts +0 -431
- package/.framework/mcp/tools/cil/context.ts +0 -345
- package/.framework/mcp/tools/cil/graph.ts +0 -242
- package/.framework/mcp/tools/cil/ingest.ts +0 -1217
- package/.framework/mcp/tools/cil/pipeline-orchestrator.ts +0 -693
- package/.framework/mcp/tools/cil/pipeline.ts +0 -767
- package/.framework/mcp/tools/cil/project.ts +0 -791
- package/.framework/mcp/tools/cil/retrieval.ts +0 -336
- package/.framework/mcp/tools/cil/search-core.ts +0 -454
- package/.framework/mcp/tools/cil/search.ts +0 -97
- package/.framework/mcp/tools/cil/suggest.ts +0 -186
- package/.framework/mcp/tools/cil/validate.ts +0 -1369
- package/.framework/mcp/tools/kb/delete.ts +0 -139
- package/.framework/mcp/tools/kb/index.ts +0 -52
- package/.framework/mcp/tools/kb/ingest.ts +0 -84
- package/.framework/mcp/tools/kb/list-types.ts +0 -102
- package/.framework/mcp/tools/kb/publish.ts +0 -100
- package/.framework/mcp/tools/kb/reindex.ts +0 -86
- package/.framework/mcp/tools/kb/revoke.ts +0 -85
- package/.framework/mcp/tools/kb/search.ts +0 -101
- package/.framework/mcp/tools/kb/upsert.ts +0 -125
- package/.framework/mcp/tools/observability.ts +0 -510
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Roles
|
|
3
|
+
audience: both
|
|
4
|
+
kind: entity
|
|
5
|
+
verified_against: e13127d
|
|
6
|
+
verified_on: 2026-10-30
|
|
7
|
+
source_files:
|
|
8
|
+
- .framework/functions/roles.ts
|
|
9
|
+
- .framework/functions/_shared/permissions.ts
|
|
10
|
+
- .framework/functions/auth.ts
|
|
11
|
+
- .framework/functions/_shared/principal.ts
|
|
12
|
+
- .framework/src/pages/admin/RolesPage.tsx
|
|
13
|
+
- .framework/src/pages/admin/RoleDetailPage.tsx
|
|
14
|
+
- .framework/migrations/003_core_system_data.sql
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## What this is for
|
|
18
|
+
|
|
19
|
+
A **role** is a named principal category assigned to people (`people.role_id`). Its **slug** is what permission evaluation keys on: each type's `design_schema.record_permissions` maps role slugs to the record actions that role may perform. The role's own `permissions` column feeds `principal.permissions` for coarse gates.
|
|
20
|
+
|
|
21
|
+
## Fields and settings
|
|
22
|
+
|
|
23
|
+
- **Name** — string, required. Display label (e.g. "Support Manager"); `slug` and `name` are the API's required fields.
|
|
24
|
+
- **Slug** — string, required, unique per app. **Disabled after creation** because `record_permissions` keys and `people.role_id` references depend on it.
|
|
25
|
+
- **Description** — string, optional.
|
|
26
|
+
- **System Role** — checkbox, create-only. Reserved roles can't be deleted and are protected for core functions.
|
|
27
|
+
- **Active** — checkbox. Inactive roles can't be assigned to people.
|
|
28
|
+
- **Permissions (JSON)** — the form suggests an object mapping type slugs to action arrays (`{ "ticket": ["create","read"] }`), but the permission engine does **not** evaluate this column for record access — see Downstream effects. A **flat array** form (`["read","write","admin","system"]`) is what `auth.ts` copies into `principal.permissions`.
|
|
29
|
+
|
|
30
|
+
**Downstream effects**
|
|
31
|
+
|
|
32
|
+
- Record CRUD is governed by each type's `design_schema.record_permissions`, keyed by **role slug** — e.g. `"record_permissions": { "support-agent": ["read"] }`. Assigning a person a role takes effect immediately (the engine resolves `people.role_id → roles.slug` per request, short cache).
|
|
33
|
+
- `record_permissions` keys are matched **verbatim** against `roles.slug` — `system-admin` is a dead key that silently denies the real `system_admin` role. The `all` key is a wildcard granting every authenticated principal.
|
|
34
|
+
- `auth.ts` copies `role.permissions` into `principal.permissions` **only when it is a flat array** (`system_admin` is hardcoded to `["read","write","admin","system"]`). `principal.permissions` backs coarse checks like the invites `system` gate — not per-record access.
|
|
35
|
+
- Malformed permissions don't lock anyone out of records (the column isn't read for that) — a wrong `record_permissions` slug on the type does.
|
|
36
|
+
- Creating a role with slug `system_admin` is restricted to `system_admin` principals.
|
|
37
|
+
|
|
38
|
+
## Minimal configuration
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{
|
|
42
|
+
"name": "Support Agent",
|
|
43
|
+
"slug": "support-agent",
|
|
44
|
+
"is_active": true,
|
|
45
|
+
"permissions": { "ticket": ["create", "read", "update"] }
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Full configuration
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{
|
|
53
|
+
"name": "Support Manager",
|
|
54
|
+
"slug": "support-manager",
|
|
55
|
+
"is_active": true,
|
|
56
|
+
"is_system": false,
|
|
57
|
+
"permissions": { "ticket": ["create", "read", "update", "delete"] },
|
|
58
|
+
"metadata": { "department": "support" }
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Both examples use the object form the UI suggests — it is stored as documentation/delegated-policy data. Record access still comes from each type's `record_permissions` under the same slug.
|
|
63
|
+
|
|
64
|
+
## Example
|
|
65
|
+
|
|
66
|
+
A support app ships three roles: `support-agent`, `support-manager`, and `support-viewer`. On the `ticket` type, `record_permissions` grants `support-agent` `["create","read","update"]`, `support-manager` all four actions, and `support-viewer` `["read"]`; assignment happens on `people` records via `role_id`.
|
|
67
|
+
|
|
68
|
+
## Core files
|
|
69
|
+
|
|
70
|
+
- `.framework/functions/roles.ts` — API (list/get/create/update/delete; `system_admin` slug creation gated)
|
|
71
|
+
- `.framework/functions/_shared/permissions.ts` — `record_permissions`/`fields.*.permissions` evaluation (role-slug keyed; `all` wildcard)
|
|
72
|
+
- `.framework/functions/auth.ts` / `.framework/functions/_shared/principal.ts` — `role.permissions` → `principal.permissions` (flat array only)
|
|
73
|
+
- `.framework/src/pages/admin/RolesPage.tsx` / `RoleDetailPage.tsx` — admin UI
|
|
74
|
+
- Table: `public.roles`; assignment on `public.people.role_id`
|
|
75
|
+
|
|
76
|
+
## Agent notes
|
|
77
|
+
|
|
78
|
+
- The Permissions textarea's own placeholder shows the object form — know that it is not what gates records. The gate is `record_permissions[role_slug]` on the type.
|
|
79
|
+
- Slug is immutable after create; rename = new role + reassignment. Never introduce a hyphenated `system-admin` slug — seeded slugs use underscores.
|
|
80
|
+
- The seeded `system_admin` role must stay assigned for admin functions; `auth.ts` hardcodes its principal permissions, it is not data-driven.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Runtime Record (detail)
|
|
3
|
+
audience: both
|
|
4
|
+
kind: ui
|
|
5
|
+
verified_against: e13127d
|
|
6
|
+
verified_on: 2026-10-30
|
|
7
|
+
source_files:
|
|
8
|
+
- .framework/src/components/runtime/DataDetailPage.tsx
|
|
9
|
+
- .framework/src/hooks/useEntityRecord.ts
|
|
10
|
+
- .framework/src/components/runtime/SchemaDetailForm.tsx
|
|
11
|
+
- .framework/functions/admin-data.ts
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## What this is for
|
|
15
|
+
|
|
16
|
+
**Runtime detail** (`/spine-framework/admin/runtime/:entity/new` and `/runtime/:entity/:id`) is the generic create/view/edit form for one runtime record. It renders sections and fields from the record's stamped `design_schema.views.default_detail` — so any type with a detail view gets a working record page without a custom UI.
|
|
17
|
+
|
|
18
|
+
## Fields and settings
|
|
19
|
+
|
|
20
|
+
- **Header** — record title (`design_schema.display_field`, falling back through `full_name` → `display_name` → `name` → `title` → `slug` → `email` → "Untitled"), plus Edit / Save / Cancel / Delete buttons. `?edit=true` toggles edit mode; `new` starts in edit mode.
|
|
21
|
+
- **Sections** — `views.default_detail.sections[*]`: each section's `fields` render as form controls per `display_type` (`input`, `textarea`, `select`, `checkbox`, `date_picker`, `json`-as-textarea, ...).
|
|
22
|
+
- **Field values** — `system: true` fields read `record.<field>`; everything else reads `record.data.<field>` and is written back under `data`.
|
|
23
|
+
- **Field permissions** — `fieldPermissions` from the record response gate which inputs are editable (field-level `read`/`write` from `fields.*.permissions` over the record grant).
|
|
24
|
+
- **Delete** — confirm → `admin-data` remove → back to the entity list.
|
|
25
|
+
|
|
26
|
+
**Downstream effects**
|
|
27
|
+
|
|
28
|
+
- All writes go through `/api/admin-data` (`create`/`update`/`remove`): type stamping, `validation_schema` checks, `record_permissions`, and create/update triggers all apply — the same path custom apps must use.
|
|
29
|
+
- Creating a record stamps the type's current `design_schema` onto it — the detail page then renders from that stamped copy (`record.design_schema || record.type.design_schema`).
|
|
30
|
+
- Default `type_slug` per entity on create: `accounts→account`, `people→person`, `items→item`, `threads→thread`, `messages→message`, `links→link`, `attachments→attachment`, `watchers→watcher`, `item_progress→item_progress` — a `typeSlug` route param overrides it.
|
|
31
|
+
|
|
32
|
+
## Minimal configuration
|
|
33
|
+
|
|
34
|
+
Define `views.default_detail` on the type:
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
"default_detail": {
|
|
38
|
+
"type": "detail", "label": "Ticket",
|
|
39
|
+
"sections": [
|
|
40
|
+
{ "title": "Core", "fields": {
|
|
41
|
+
"title": { "display_type": "input" },
|
|
42
|
+
"status": { "display_type": "select" }
|
|
43
|
+
} }
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Full configuration
|
|
49
|
+
|
|
50
|
+
Split fields across multiple named sections, choose richer `display_type` controls (`textarea`, `select`, `multiselect`, `date_picker`, `checkbox`, `color_picker`, `file_upload`, `range_slider`), and set `display_field` on the schema so the header shows a meaningful title. Field-level `permissions` on `design_schema.fields` restrict which roles can write a field even when the record grant allows updates.
|
|
51
|
+
|
|
52
|
+
## Example
|
|
53
|
+
|
|
54
|
+
`/spine-framework/admin/runtime/items/<id>?edit=true` renders the ticket: a "Core" section with Title (input), Status (select), Priority (select), and a "Notes" section with a `json` field shown as a monospaced textarea. Save → `admin-data` validates against `validation_schema` and fires `item_updated` triggers.
|
|
55
|
+
|
|
56
|
+
## Core files
|
|
57
|
+
|
|
58
|
+
- `.framework/src/components/runtime/DataDetailPage.tsx` — this page
|
|
59
|
+
- `.framework/src/components/runtime/SchemaDetailForm.tsx` — renders sections/fields from the schema
|
|
60
|
+
- `.framework/src/components/runtime/DataDetailHeader.tsx` — title + action buttons
|
|
61
|
+
- `.framework/src/hooks/useEntityRecord.ts` — record fetch (`design_schema` fallback), save/delete via `admin-data`
|
|
62
|
+
- `.framework/functions/admin-data.ts` — get/create/update/remove backend
|
|
63
|
+
- `.framework/src/apps/admin/index.tsx` — `instructionKeyFor`: `runtime/:entity/...` (≥4 segments) → `runtime-detail`
|
|
64
|
+
|
|
65
|
+
## Agent notes
|
|
66
|
+
|
|
67
|
+
- Error card "Failed to load record or schema" = the record has no stamped `design_schema` or the type's `default_detail` view is missing — fix the type's schema, not this page.
|
|
68
|
+
- `system: true` fields (e.g. `title`) are top-level record columns; a field defined in `design_schema.fields` but absent from a section won't render — the form only shows what `default_detail` declares.
|
|
69
|
+
- A record stamped under an old schema renders the old view — re-save it to re-stamp the current schema.
|
|
70
|
+
- `new` with no `typeSlug` uses the default type slug map above; creating a different type's record needs the `typeSlug` route param or a different entry point.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Runtime Records (list)
|
|
3
|
+
audience: both
|
|
4
|
+
kind: ui
|
|
5
|
+
verified_against: e13127d
|
|
6
|
+
verified_on: 2026-10-30
|
|
7
|
+
source_files:
|
|
8
|
+
- .framework/src/components/runtime/DataListPage.tsx
|
|
9
|
+
- .framework/src/hooks/useListSchema.ts
|
|
10
|
+
- .framework/src/hooks/useEntityList.ts
|
|
11
|
+
- .framework/functions/admin-data.ts
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## What this is for
|
|
15
|
+
|
|
16
|
+
**Runtime list** (`/spine-framework/admin/runtime/:entity`) is the generic, schema-driven list page for any runtime entity table — `items`, `threads`, `messages`, `links`, `attachments`, `watchers`, `people`, `accounts`, `item_progress`. It renders whatever the entity's type `design_schema.views.default_list` declares, so every type gets a working admin list for free.
|
|
17
|
+
|
|
18
|
+
## Fields and settings
|
|
19
|
+
|
|
20
|
+
- **Header** — entity name, description, and a New button → `runtime/:entity/new`.
|
|
21
|
+
- **Stats cards** — driven by `views.default_list.stats` (empty when undeclared).
|
|
22
|
+
- **Filters** — driven by `views.default_list.filters`; values go into the `admin-data` list query.
|
|
23
|
+
- **Columns** — `views.default_list.fields`: label from `design_schema.fields[key].label`, sortable unless `sortable: false`, rendered per `display_type` (`text`, `badge`, `timestamp`, `currency`, ...).
|
|
24
|
+
- **Sort** — defaults to `views.default_list.default_sort` (fallback `created_at` desc); sent as `sort_field`/`sort_direction`.
|
|
25
|
+
- **Rows** — `record.data.<field>` values for non-system fields, `record.<field>` for `system: true` fields.
|
|
26
|
+
|
|
27
|
+
**How the schema is resolved** (`useListSchema`, in order)
|
|
28
|
+
|
|
29
|
+
1. Fetch one sample record (`admin-data?action=list&entity=<entity>&limit=1`) and use its **stamped** `design_schema.views.default_list`.
|
|
30
|
+
2. No stamped schema → look up `/api/types?action=list&kind=<mapped>`; used only when **exactly one** type matches the entity's kind.
|
|
31
|
+
3. Otherwise a fallback view (just `id` + `created_at` columns).
|
|
32
|
+
|
|
33
|
+
**Downstream effects**
|
|
34
|
+
|
|
35
|
+
- All data flows through `/api/admin-data` — the same permission, validation, and trigger path as any other client.
|
|
36
|
+
- Multi-type entities (e.g. `items` with `ticket`, `threshold_alert`, `kb-article`) resolve schema from the *sample record* — the columns shown are whichever type the most recent matching record carries, and `type_slug` filtering is available via the route's `typeSlug` param.
|
|
37
|
+
- A type with no `default_list` view (or a fresh table with no records and >1 candidate type) renders the fallback two-column view — fix the type's `design_schema`, not this page.
|
|
38
|
+
|
|
39
|
+
## Minimal configuration
|
|
40
|
+
|
|
41
|
+
Define `views.default_list` on the type:
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
"default_list": {
|
|
45
|
+
"type": "list", "display": "table", "label": "Tickets",
|
|
46
|
+
"fields": {
|
|
47
|
+
"title": { "sortable": true, "display_type": "text" },
|
|
48
|
+
"status": { "sortable": true, "display_type": "badge" },
|
|
49
|
+
"created_at": { "sortable": true, "display_type": "timestamp" }
|
|
50
|
+
},
|
|
51
|
+
"default_sort": { "field": "created_at", "direction": "desc" }
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Full configuration
|
|
56
|
+
|
|
57
|
+
Add `stats` and `filters` arrays to the view for stat cards and filter controls, choose `display_type` per column to control rendering, and set a different `default_sort` (e.g. `priority asc`). Valid `entity` path segments are the `admin-data` whitelist: `accounts`, `people`, `items`, `threads`, `messages`, `links`, `attachments`, `watchers`, `item_progress`, plus config entities (`types`, `link_types`, `ai_agents`, `prompt_configs`, ...).
|
|
58
|
+
|
|
59
|
+
## Example
|
|
60
|
+
|
|
61
|
+
`/spine-framework/admin/runtime/items` shows the ticket list: columns Title/Status/Priority/Created from `default_list.fields`, status rendered as a badge, sorted by `created_at` desc; the New button lands on `runtime/items/new`.
|
|
62
|
+
|
|
63
|
+
## Core files
|
|
64
|
+
|
|
65
|
+
- `.framework/src/components/runtime/DataListPage.tsx` — this page
|
|
66
|
+
- `.framework/src/components/runtime/DataTable.tsx` / `DataFilters.tsx` / `DataStats.tsx` / `DataHeader.tsx` — subcomponents
|
|
67
|
+
- `.framework/src/hooks/useListSchema.ts` — stamped-schema → type → fallback resolution
|
|
68
|
+
- `.framework/src/hooks/useEntityList.ts` — the `admin-data` list query (filters, sort)
|
|
69
|
+
- `.framework/functions/admin-data.ts` — list/get backend (`VALID_ENTITIES` whitelist)
|
|
70
|
+
- `.framework/src/apps/admin/index.tsx` — `instructionKeyFor`: `runtime/:entity` → `runtime-list`
|
|
71
|
+
|
|
72
|
+
## Agent notes
|
|
73
|
+
|
|
74
|
+
- The schema comes from a **record's stamped** `design_schema` first — old records stamped before a schema change keep old views; the list can flip shape depending on which record it samples.
|
|
75
|
+
- If the page shows only `id`/`created_at`, the type's `default_list` view is missing or ambiguous — check the type, then check that the entity has records.
|
|
76
|
+
- Non-system fields live under `record.data` — a column for a `system: true` field reads `record.<field>`; mixing these up shows blank cells.
|
|
77
|
+
- Filter/sort state is client-side over the fetched page — this is a browsing surface, not a reporting tool.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Security
|
|
3
|
+
audience: both
|
|
4
|
+
kind: entity
|
|
5
|
+
verified_against: e13127d
|
|
6
|
+
verified_on: 2026-10-30
|
|
7
|
+
source_files:
|
|
8
|
+
- .framework/src/pages/admin/SecurityPage.tsx
|
|
9
|
+
- .framework/functions/security.ts
|
|
10
|
+
- .framework/functions/_shared/security.ts
|
|
11
|
+
- .framework/migrations/049_security_blocklist.sql
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## What this is for
|
|
15
|
+
|
|
16
|
+
The **Security** page (`/spine-framework/admin/security`) is the system-admin console for abusive actors: a report of flagged users/accounts/IPs built from security signal logs, plus the platform blocklist. Its actions apply a three-layer ban — internal `security_blocklist` rows, a Supabase Auth ban, and a Netlify edge IP-deny rule.
|
|
17
|
+
|
|
18
|
+
## Fields and settings
|
|
19
|
+
|
|
20
|
+
- **Flagged actors table** — Actor (email or "(no person)"), Account, IP / Country, Signals (count + kind badges e.g. `rate_limit`, `prompt_injection`, `blocked_registration`), Last seen, Status (`active`/person status or `banned`).
|
|
21
|
+
- **Row checkboxes + Add to Blacklist** — issues one `ban_user` call per selected row and prints per-layer results (`blocklist`, `auth`, `edge`) inline.
|
|
22
|
+
- **Unban** — per-row on banned actors with a `person_id`; revokes their blocklist rows and lifts the auth ban and edge rule.
|
|
23
|
+
- **Active blocklist table** — Type (`ip`/`email`/`person`/`account`), Value, Severity, Reason, Created, with per-row Revoke (`blocklist_revoke` sets `revoked_at`).
|
|
24
|
+
- All calls go to `/api/security?action=…` (POST): `flagged_accounts`, `blocklist`, `ban_user`, `unban_user`, `blocklist_add`, `blocklist_revoke`.
|
|
25
|
+
|
|
26
|
+
**Downstream effects**
|
|
27
|
+
|
|
28
|
+
- `ban_user` accepts `person_id`, `email`, or `ip` — anonymous-signal rows (no person) still ban by account/IP. Layer 1 (internal blocklist) is authoritative; the Supabase Auth and Netlify edge layers report `applied`/`skipped`/`error` per call — the edge layer degrades gracefully when Netlify API credentials aren't configured.
|
|
29
|
+
- Blocked principals are rejected at request time by `isBlockedRequest`/`isBlockedIp` in `_shared/security.ts` (checked in middleware and at registration).
|
|
30
|
+
- Every action emits `security.*` audit events into `logs` — the same table the flagged report is aggregated from.
|
|
31
|
+
- Every action requires a `system_admin` principal.
|
|
32
|
+
|
|
33
|
+
## Minimal configuration
|
|
34
|
+
|
|
35
|
+
Nothing to configure — the report fills itself from security signals (rate-limit violations, prompt-injection detections, blocked registrations). To act: select flagged rows → Add to Blacklist.
|
|
36
|
+
|
|
37
|
+
## Full configuration
|
|
38
|
+
|
|
39
|
+
Direct API calls for finer control:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
POST /api/security?action=blocklist_add { type: "ip", value: "203.0.113.7", severity: "high", reason: "credential stuffing", expires_at: null }
|
|
43
|
+
POST /api/security?action=blocklist_revoke { id: "<row uuid>" }
|
|
44
|
+
POST /api/security?action=ban_user { email: "abuser@example.com", ip: "203.0.113.7", reason: "..." }
|
|
45
|
+
POST /api/security?action=unban_user { person_id: "<uuid>" }
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`expires_at` may be set for temporary bans; entries with `revoked_at` are hidden from the Active blocklist.
|
|
49
|
+
|
|
50
|
+
## Example
|
|
51
|
+
|
|
52
|
+
A burst of `rate_limit` signals from one IP appears in Flagged actors. Select the row, Add to Blacklist — the result line shows `banned (auth:applied edge:applied)`; subsequent requests from that IP 403 at the edge and at `isBlockedIp`. If it was a false positive, Unban restores the person and revokes the rows.
|
|
53
|
+
|
|
54
|
+
## Core files
|
|
55
|
+
|
|
56
|
+
- `.framework/src/pages/admin/SecurityPage.tsx` — this page
|
|
57
|
+
- `.framework/functions/security.ts` — flagged report, three-layer ban/unban, blocklist CRUD, Netlify edge sync
|
|
58
|
+
- `.framework/functions/_shared/security.ts` — `isBlockedRequest`/`isBlockedIp` enforcement, `emitSecurityEvent`, injection patterns
|
|
59
|
+
- `.framework/migrations/049_security_blocklist.sql` — `security_blocklist` schema
|
|
60
|
+
- Tables: `public.security_blocklist`, `public.logs` (signal source)
|
|
61
|
+
|
|
62
|
+
## Agent notes
|
|
63
|
+
|
|
64
|
+
- The Netlify edge layer needs Netlify API credentials in env — without them `edge: skipped` is normal, not a failure; the internal blocklist still enforces.
|
|
65
|
+
- Banning by `email`/`ip` does not stop a determined actor from rotating — treat the edge deny as a speed bump and watch for re-registration signals.
|
|
66
|
+
- Unban requires a `person_id` — rows banned only by IP/email must be lifted via `blocklist_revoke` on the blocklist entries.
|
|
67
|
+
- This page is marked experimental and is system-admin only; all writes are audited.
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Add a pipeline
|
|
3
|
+
audience: developers, agents
|
|
4
|
+
kind: task
|
|
5
|
+
verified_against: e13127d
|
|
6
|
+
verified_on: 2026-10-30
|
|
7
|
+
source_files:
|
|
8
|
+
- .framework/functions/pipelines.ts
|
|
9
|
+
- .framework/functions/_shared/pipeline-runner.ts
|
|
10
|
+
- .framework/migrations/053_builtin_action_seeds.sql
|
|
11
|
+
- .framework/functions/system-cron.ts
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Add a pipeline
|
|
15
|
+
|
|
16
|
+
## Goal
|
|
17
|
+
|
|
18
|
+
Create a `public.pipelines` row: an ordered `stages` array the runner
|
|
19
|
+
executes when something (a trigger, a timer, or a manual run) invokes it.
|
|
20
|
+
|
|
21
|
+
## Steps
|
|
22
|
+
|
|
23
|
+
1. **Choose the `trigger_type`.** `manual` (run by button/API), `event`
|
|
24
|
+
(fired by a trigger), `scheduled` (fired by a timer), or `webhook`. This
|
|
25
|
+
is metadata for callers — what actually runs a pipeline is whatever
|
|
26
|
+
invokes `runPipeline`, so the type should match how you plan to wire it
|
|
27
|
+
([Automation](../concepts/automation.md)).
|
|
28
|
+
|
|
29
|
+
2. **Draft the `stages` array.** Each stage needs a `stage_type` matching an
|
|
30
|
+
active slug in `public.actions` plus a `config`. Built-in stage types
|
|
31
|
+
(seeded by `.framework/migrations/053_builtin_action_seeds.sql`):
|
|
32
|
+
|
|
33
|
+
`update_item`, `create_record`, `http_request`, `send_notification`,
|
|
34
|
+
`send_email`, `run_pipeline`, `search_knowledge`, `query_items`,
|
|
35
|
+
`agent_inference`, `generate_embeddings`, `revoke_embeddings`,
|
|
36
|
+
`kb_ingest`, `kb_revoke`, `create_interaction`
|
|
37
|
+
(`.framework/functions/_shared/pipeline-runner.ts`).
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
[
|
|
41
|
+
{
|
|
42
|
+
"stage_type": "query_items",
|
|
43
|
+
"config": { "entity": "items", "type_slug": "ticket",
|
|
44
|
+
"filters": { "status": "open" } }
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"stage_type": "http_request",
|
|
48
|
+
"config": { "url": "https://example.com/hook", "method": "POST",
|
|
49
|
+
"body": { "title": "{{record.data.title}}" } }
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
- Stage config merges `action.config` (defaults on the actions row) under
|
|
55
|
+
`stage.config`, plus injected `_pipelineId` / `_executionId` /
|
|
56
|
+
`_stageIndex` / `_triggerData`
|
|
57
|
+
(`.framework/functions/_shared/pipeline-runner.ts`).
|
|
58
|
+
- `{{trigger.*}}` resolves into the trigger-data envelope; `{{record.*}}`
|
|
59
|
+
resolves into `triggerData.entity.data` — the record that fired the
|
|
60
|
+
run (`.framework/functions/_shared/pipeline-runner.ts`).
|
|
61
|
+
- `continue_on_error: true` on a stage lets the run continue after a
|
|
62
|
+
failure; otherwise a failed stage aborts the whole run
|
|
63
|
+
(`.framework/functions/_shared/pipeline-runner.ts`).
|
|
64
|
+
|
|
65
|
+
3. **Create the pipeline.** Admin UI → **Configs → Pipelines → New**
|
|
66
|
+
(`configs/pipelines/new`), or `POST /api/pipelines` with
|
|
67
|
+
`{ name, trigger_type, stages }` — all three required
|
|
68
|
+
(`.framework/functions/pipelines.ts`). Set `is_active: true`;
|
|
69
|
+
`runPipeline` refuses inactive pipelines.
|
|
70
|
+
|
|
71
|
+
4. **Wire the entry point.** A pipeline with `trigger_type: "event"` needs a
|
|
72
|
+
trigger ([Add a trigger](./add-a-trigger.md)); `scheduled` needs a timer
|
|
73
|
+
([Add a timer](./add-a-timer.md)); `manual` runs on demand. Changing
|
|
74
|
+
`trigger_type` later does not rewire existing triggers or timers — relink
|
|
75
|
+
them separately.
|
|
76
|
+
|
|
77
|
+
## Verify
|
|
78
|
+
|
|
79
|
+
- Run it directly: `npx spine-framework pipelines run <id> --data '{"x":1}'`
|
|
80
|
+
— see [CLI reference](../reference/cli.md).
|
|
81
|
+
- Check **Observability → Executions** (`observability/executions`) or
|
|
82
|
+
`pipelines executions <id>` for a `completed` row with per-stage results.
|
|
83
|
+
- A missing `stage_type` fails fast with
|
|
84
|
+
`Action not found for stage type: <type>` — the runner looks the slug up
|
|
85
|
+
in `public.actions` with `is_active = true` before executing
|
|
86
|
+
(`.framework/functions/_shared/pipeline-runner.ts`).
|
|
87
|
+
|
|
88
|
+
## Common mistakes
|
|
89
|
+
|
|
90
|
+
- **Nonexistent stage_type.** Seed validation and the runner both reject
|
|
91
|
+
unknown slugs; stick to the built-ins or seed your own `actions` row
|
|
92
|
+
(see [Package and install an app](./package-and-install-an-app.md)).
|
|
93
|
+
- **Expecting stage outputs to chain into the next stage.** Each stage gets
|
|
94
|
+
the merged `action.config` + `stage.config` + `_triggerData`; previous
|
|
95
|
+
stage outputs are collected into the execution result, not injected into
|
|
96
|
+
later configs. Templates only see `{{trigger.*}}` and `{{record.*}}`.
|
|
97
|
+
- **`scheduled` with no timer, or `event` with no trigger.** The pipeline
|
|
98
|
+
is then unreachable — `get_due_timers` only returns timers with
|
|
99
|
+
`pipeline_id` set, and triggers dispatch by `pipeline_id`.
|
|
100
|
+
- **Editing `stages` while runs are queued.** The runner reads the pipeline
|
|
101
|
+
row at execution time; a queued execution picks up whatever `stages`
|
|
102
|
+
exist when it starts, not when it was queued.
|
|
103
|
+
|
|
104
|
+
## Reference
|
|
105
|
+
|
|
106
|
+
- [pipelines entity page](../pipelines.md) ·
|
|
107
|
+
[Automation](../concepts/automation.md)
|
|
108
|
+
- [pipeline-executions reference](../reference/functions/pipeline-executions.md) ·
|
|
109
|
+
[pipeline-runner-background reference](../reference/functions/pipeline-runner-background.md)
|
|
110
|
+
- [Add a trigger](./add-a-trigger.md) · [Add a timer](./add-a-timer.md)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Add a role
|
|
3
|
+
audience: developers, agents
|
|
4
|
+
kind: task
|
|
5
|
+
verified_against: e13127d
|
|
6
|
+
verified_on: 2026-10-30
|
|
7
|
+
source_files:
|
|
8
|
+
- .framework/functions/roles.ts
|
|
9
|
+
- .framework/functions/_shared/permissions.ts
|
|
10
|
+
- .framework/migrations/001_core_schema.sql
|
|
11
|
+
- .framework/migrations/003_core_system_data.sql
|
|
12
|
+
- .framework/migrations/009_roles_slug_unique.sql
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# Add a role
|
|
16
|
+
|
|
17
|
+
## Goal
|
|
18
|
+
|
|
19
|
+
Create a `public.roles` row whose slug you can reference from
|
|
20
|
+
`record_permissions` in type design schemas, `min_role`/`required_roles` on
|
|
21
|
+
apps, and `role:<slug>` notification recipients.
|
|
22
|
+
|
|
23
|
+
## Prerequisites
|
|
24
|
+
|
|
25
|
+
- A `system_admin` login — role create/update/remove requires it
|
|
26
|
+
(`.framework/functions/roles.ts`).
|
|
27
|
+
|
|
28
|
+
## Steps
|
|
29
|
+
|
|
30
|
+
1. **Pick the slug.** Lowercase snake_case (`support_agent`). It is matched
|
|
31
|
+
verbatim as a key inside every type's `record_permissions` and is
|
|
32
|
+
effectively immutable — the admin UI disables slug editing after creation.
|
|
33
|
+
Uniqueness is enforced at both the API layer (duplicate-slug check before
|
|
34
|
+
insert) and the database (`UNIQUE(slug)`,
|
|
35
|
+
`.framework/migrations/009_roles_slug_unique.sql`).
|
|
36
|
+
|
|
37
|
+
2. **Decide the permissions payload.** `roles.permissions` is a JSON
|
|
38
|
+
document on the role row (`permissions jsonb` on the table,
|
|
39
|
+
`.framework/migrations/001_core_schema.sql`). Seeded roles carry
|
|
40
|
+
`'["*"]'` — a wildcard grant. Per-type grants are expressed on the
|
|
41
|
+
**type** side (`design_schema.record_permissions` keys are role slugs),
|
|
42
|
+
which is what `admin-data` evaluates per request — see
|
|
43
|
+
[Permission model](../concepts/permissions.md).
|
|
44
|
+
|
|
45
|
+
3. **Create the role.** Admin UI → **Configs → Roles → New**
|
|
46
|
+
(`configs/roles/new`), or `POST /api/roles` with
|
|
47
|
+
`{ slug, name, permissions, is_system }` — `slug` and `name` are required
|
|
48
|
+
(`.framework/functions/roles.ts`). The `system_admin` slug itself
|
|
49
|
+
is protected: only a system admin may create it
|
|
50
|
+
(`.framework/functions/roles.ts`).
|
|
51
|
+
|
|
52
|
+
4. **Reference the role.** Add its slug to each type's
|
|
53
|
+
`design_schema.record_permissions`, to `apps.min_role` /
|
|
54
|
+
`apps.required_roles`, and to `send_notification` stage recipients as
|
|
55
|
+
`role:<slug>` (resolved via `people.role_id → roles.slug` — see
|
|
56
|
+
[Automation](../concepts/automation.md)).
|
|
57
|
+
|
|
58
|
+
5. **Assign it to people.** A person's `role_id` joins to `roles`; the
|
|
59
|
+
permission engine resolves `people.role:role_id(slug)` and evaluates
|
|
60
|
+
`record_permissions` against that slug
|
|
61
|
+
(`.framework/functions/_shared/permissions.ts`).
|
|
62
|
+
|
|
63
|
+
## Verify
|
|
64
|
+
|
|
65
|
+
- `GET /api/roles?action=list` returns the row; `slug` is unique and active.
|
|
66
|
+
- Create or update a type whose `record_permissions` includes the new slug,
|
|
67
|
+
then perform an `admin-data` call as a person holding the role — the
|
|
68
|
+
action is permitted.
|
|
69
|
+
- `system_admin` users see all roles regardless of `is_active` filters.
|
|
70
|
+
|
|
71
|
+
## Common mistakes
|
|
72
|
+
|
|
73
|
+
- **`system-admin` vs `system_admin`.** All seeded slugs are snake_case:
|
|
74
|
+
`system_admin`, `member`, `member_admin`, `engineering`,
|
|
75
|
+
`engineering_admin`, `product`, `docs`, `qa`, `devops`, `management`,
|
|
76
|
+
`sales`, `csm`, `tam`, `support`, `support_admin`
|
|
77
|
+
(`.framework/migrations/003_core_system_data.sql`,
|
|
78
|
+
`.framework/migrations/073_seed_member_roles.sql`).
|
|
79
|
+
Using hyphens in `record_permissions` grants nothing.
|
|
80
|
+
- **Expecting `roles.permissions` to gate `admin-data`.** Record-level
|
|
81
|
+
grants live on the type's `record_permissions`; the role row's
|
|
82
|
+
`permissions` JSON is not consulted per-request for record CRUD.
|
|
83
|
+
- **Renaming a slug later.** Every type's `record_permissions` and every
|
|
84
|
+
app's `min_role`/`required_roles` reference the slug verbatim — a rename
|
|
85
|
+
orphans the grants. Create a new role and migrate people instead.
|
|
86
|
+
- **Creating `system_admin` via API as a non-admin** — explicitly blocked.
|
|
87
|
+
- **Deactivating a role with live assignees** — the slug stops resolving to
|
|
88
|
+
a grant for those people; check `people.role_id` usage first.
|
|
89
|
+
|
|
90
|
+
## Reference
|
|
91
|
+
|
|
92
|
+
- [roles entity page](../roles.md) ·
|
|
93
|
+
[roles function reference](../reference/functions/roles.md)
|
|
94
|
+
- [Permission model](../concepts/permissions.md) ·
|
|
95
|
+
[Add a type](./add-a-type.md)
|
|
96
|
+
- [Database reference](../reference/database.md) for the `roles` columns
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Add a timer
|
|
3
|
+
audience: developers, agents
|
|
4
|
+
kind: task
|
|
5
|
+
verified_against: e13127d
|
|
6
|
+
verified_on: 2026-10-30
|
|
7
|
+
source_files:
|
|
8
|
+
- .framework/functions/timers.ts
|
|
9
|
+
- .framework/functions/system-cron.ts
|
|
10
|
+
- .framework/functions/trigger-scheduler.ts
|
|
11
|
+
- .framework/migrations/020_schema_reconciliation.sql
|
|
12
|
+
- .framework/migrations/068_cron_schedule_timer_rpcs.sql
|
|
13
|
+
- .framework/src/pages/admin/TimerDetailPage.tsx
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# Add a timer
|
|
17
|
+
|
|
18
|
+
## Goal
|
|
19
|
+
|
|
20
|
+
Create a `public.timers` row that runs a pipeline every
|
|
21
|
+
`config.interval_minutes` via the system cron sweep.
|
|
22
|
+
|
|
23
|
+
## Prerequisites
|
|
24
|
+
|
|
25
|
+
- A pipeline to point at ([Add a pipeline](./add-a-pipeline.md)) —
|
|
26
|
+
`get_due_timers` ignores timers with `pipeline_id` NULL.
|
|
27
|
+
|
|
28
|
+
## Steps
|
|
29
|
+
|
|
30
|
+
1. **Create the timer.** Admin UI → **Configs → Timers → New**
|
|
31
|
+
(`configs/timers/new`), or `POST /api/timers` — `name` and `timer_type`
|
|
32
|
+
required (`.framework/functions/timers.ts`). Set
|
|
33
|
+
`pipeline_id` and `config.interval_minutes`:
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
{
|
|
37
|
+
"name": "Hourly sync",
|
|
38
|
+
"timer_type": "interval",
|
|
39
|
+
"pipeline_id": "<pipeline-uuid>",
|
|
40
|
+
"config": { "interval_minutes": 60 }
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`timer_type` is a label (`interval` | `cron` | `once` in the admin
|
|
45
|
+
detail page, `.framework/src/pages/admin/TimerDetailPage.tsx`) — the
|
|
46
|
+
only field that actually reschedules is `config.interval_minutes`
|
|
47
|
+
(see below).
|
|
48
|
+
|
|
49
|
+
2. **`next_execution` is armed for you.** On create or toggle-on, the API
|
|
50
|
+
computes `next_execution` from `config.interval_minutes` (or
|
|
51
|
+
`config.interval_seconds`, seconds ÷ 60); `once` timers arm from an
|
|
52
|
+
ISO `config.run_at`/`config.schedule`. You may also pass
|
|
53
|
+
`next_execution` explicitly. If `next_execution` ever ends up NULL on
|
|
54
|
+
an active timer it cannot fire — `get_due_timers` requires
|
|
55
|
+
`next_execution IS NOT NULL` — so check it first when a timer never
|
|
56
|
+
runs.
|
|
57
|
+
|
|
58
|
+
3. **Understand the clock.** `trigger-scheduler` is the Netlify scheduled
|
|
59
|
+
function (`* * * * *` in `netlify.toml`); each minute it invokes the
|
|
60
|
+
`system-cron` handler under the system principal, which calls
|
|
61
|
+
`get_due_timers(now())`, then `runPipeline(timer.pipeline_id, …)` with a
|
|
62
|
+
`timer:<id>` machine principal, then `update_timer_after_run`
|
|
63
|
+
(`.framework/functions/system-cron.ts`).
|
|
64
|
+
|
|
65
|
+
**`netlify dev` does not run scheduled functions.** Clockwork only exists
|
|
66
|
+
on deployed Netlify — locally, `trigger-scheduler` is loaded but never
|
|
67
|
+
invoked on schedule. To emulate a tick, invoke it manually:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
curl -X POST http://localhost:8888/.netlify/functions/trigger-scheduler \
|
|
71
|
+
-H 'x-nf-event: schedule' \
|
|
72
|
+
-d "{\"next_run\": \"$(date -u +%Y-%m-%dT%H:%M:%S.000Z)\"}"
|
|
73
|
+
# or: npx netlify functions:invoke trigger-scheduler
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
4. **Rescheduling is automatic — or never.** `update_timer_after_run`
|
|
77
|
+
stamps `last_execution`, increments `execution_count` /
|
|
78
|
+
`success_count` / `failure_count`, and sets
|
|
79
|
+
`next_execution = now() + interval_minutes` when
|
|
80
|
+
`config.interval_minutes` is a positive number — otherwise `NULL`, so
|
|
81
|
+
the timer fires once and stops
|
|
82
|
+
(`.framework/migrations/068_cron_schedule_timer_rpcs.sql`).
|
|
83
|
+
|
|
84
|
+
## Verify
|
|
85
|
+
|
|
86
|
+
- `SELECT id, next_execution, last_execution, execution_count FROM timers`
|
|
87
|
+
shows `next_execution` in the future, then advancing each tick.
|
|
88
|
+
- **Observability → Executions** shows pipeline runs with a
|
|
89
|
+
`timer:<id>` principal.
|
|
90
|
+
- `timer.execution_count` increments; `success_count` / `failure_count`
|
|
91
|
+
reflect outcomes.
|
|
92
|
+
|
|
93
|
+
## Common mistakes
|
|
94
|
+
|
|
95
|
+
- **`interval_seconds` vs `interval_minutes`.** The admin detail page seeds
|
|
96
|
+
`config` with `{ "interval_seconds": 3600 }`, but `update_timer_after_run`
|
|
97
|
+
only reads `config.interval_minutes` — a timer saved with the default
|
|
98
|
+
config fires once (if `next_execution` was set) and never reschedules.
|
|
99
|
+
- **No `next_execution`.** The API can't set it; timers created through
|
|
100
|
+
`/api/timers` or the admin UI sit dormant until the column is seeded.
|
|
101
|
+
- **`toggle` is blind.** `?action=toggle` flips `is_active` to its negation
|
|
102
|
+
with no target-state argument — read the row first.
|
|
103
|
+
- **Cron expressions do nothing.** There is no cron-expression evaluator;
|
|
104
|
+
`timer_type: "cron"` is only a label — scheduling is interval arithmetic
|
|
105
|
+
on `next_execution`.
|
|
106
|
+
- **Timer with no `pipeline_id`** — filtered out of `get_due_timers`.
|
|
107
|
+
|
|
108
|
+
## Reference
|
|
109
|
+
|
|
110
|
+
- [timers entity page](../timers.md) ·
|
|
111
|
+
[timers function reference](../reference/functions/timers.md) ·
|
|
112
|
+
[system-cron reference](../reference/functions/system-cron.md) ·
|
|
113
|
+
[trigger-scheduler reference](../reference/functions/trigger-scheduler.md)
|
|
114
|
+
- [Automation](../concepts/automation.md) — *Timers and the scheduler*
|