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,150 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "AI agents, prompt configs, and embeddings"
|
|
3
|
+
audience: developers, agents
|
|
4
|
+
kind: concept
|
|
5
|
+
verified_against: f69c37d
|
|
6
|
+
verified_on: 2026-10-29
|
|
7
|
+
source_files:
|
|
8
|
+
- .framework/functions/ai-agents.ts
|
|
9
|
+
- .framework/functions/prompt-configs.ts
|
|
10
|
+
- .framework/functions/embeddings.ts
|
|
11
|
+
- .framework/functions/ai-interactions.ts
|
|
12
|
+
- .framework/functions/_shared/agent-runner.ts
|
|
13
|
+
- .framework/functions/_shared/llm.ts
|
|
14
|
+
- .framework/functions/_shared/pipeline-runner.ts
|
|
15
|
+
- .framework/migrations/034_match_embeddings_knn.sql
|
|
16
|
+
- .framework/migrations/044_match_embeddings_filtered_knn.sql
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# AI agents, prompt configs, and embeddings
|
|
20
|
+
|
|
21
|
+
Three config entities shape Spine's AI behavior:
|
|
22
|
+
|
|
23
|
+
- An **AI agent** (`public.ai_agents`) is the *who*: a named model + system
|
|
24
|
+
prompt + tool set + guardrails.
|
|
25
|
+
- A **prompt config** (`public.prompt_configs`) is the *how*: the prompt
|
|
26
|
+
template, model parameters, retrieval sources, and escalation policy applied
|
|
27
|
+
to a conversation.
|
|
28
|
+
- An **embedding** (`public.embeddings`) is the *memory*: per-document vector
|
|
29
|
+
rows that semantic retrieval searches over.
|
|
30
|
+
|
|
31
|
+
All three are ordinary admin-data-managed rows: `ai-agents` and
|
|
32
|
+
`prompt-configs` expose CRUD via their own functions, and the conversational
|
|
33
|
+
runtime lives in `.framework/functions/_shared/agent-runner.ts`.
|
|
34
|
+
|
|
35
|
+
## Agent configuration
|
|
36
|
+
|
|
37
|
+
`ai-agents` (`.framework/functions/ai-agents.ts`) stores, per row:
|
|
38
|
+
|
|
39
|
+
- `name` + `agent_type` — required (`chat`, `analysis`, `automation`,
|
|
40
|
+
`custom` in the admin UI).
|
|
41
|
+
- `model_config` — `{ model, temperature, max_tokens, tools }`. At inference
|
|
42
|
+
time the model resolves as `model_config.model || LLM_DEFAULT_MODEL ||
|
|
43
|
+
'gpt-4o'`; `model_config.tools` is passed through as OpenAI tool definitions
|
|
44
|
+
with `tool_choice: 'auto'`.
|
|
45
|
+
- `system_prompt` — concatenated with the prompt config's `system_prompt`
|
|
46
|
+
(agent first) to form the system message.
|
|
47
|
+
- `tools`, `capabilities`, `constraints`, `metadata` — `metadata
|
|
48
|
+
.default_prompt_config_id` is the fallback used when the thread doesn't
|
|
49
|
+
stamp its own `prompt_config_id`.
|
|
50
|
+
- `is_active` — inactive agents fail `resolveAgentConfig` lookup.
|
|
51
|
+
|
|
52
|
+
## Prompt configs and templating
|
|
53
|
+
|
|
54
|
+
`prompt-configs` (`.framework/functions/prompt-configs.ts`) carries the fields
|
|
55
|
+
the runner consumes per turn: `system_prompt`, `context_template`, `model`
|
|
56
|
+
(default `gpt-4o`), `temperature` (0.7), `max_tokens` (4000),
|
|
57
|
+
`is_multi_turn` (true), `max_history_messages` (20), `confidence_threshold`,
|
|
58
|
+
`escalation_action` / `escalation_target`, `output_mode` / `output_field`,
|
|
59
|
+
`requires_review`, `knowledge_sources`, `available_tools`, and
|
|
60
|
+
`tool_constraints`.
|
|
61
|
+
|
|
62
|
+
`context_template` supports exactly two placeholders, substituted in
|
|
63
|
+
`buildContext`: `{{user_message}}` (the inbound text, truncated to 4000 chars)
|
|
64
|
+
and `{{conversation_history}}` (prior messages rendered as
|
|
65
|
+
`Customer:`/`Agent:` lines, truncated to 4000 chars).
|
|
66
|
+
|
|
67
|
+
## How a thread resolves its agent and prompt
|
|
68
|
+
|
|
69
|
+
`resolveAgentConfig` in `.framework/functions/_shared/agent-runner.ts`:
|
|
70
|
+
|
|
71
|
+
1. Reads the thread (`threads`, joined with its `types` row).
|
|
72
|
+
2. Agent id comes from `thread.data.agent_id`, falling back to the thread
|
|
73
|
+
type's `design_schema.default_agent_id`. No agent → the run aborts.
|
|
74
|
+
3. Prompt config id comes from `thread.data.prompt_config_id`, falling back to
|
|
75
|
+
`agent.metadata.default_prompt_config_id`.
|
|
76
|
+
4. Both rows must be `is_active`.
|
|
77
|
+
|
|
78
|
+
`buildContext` then assembles the model input: combined system prompt, the
|
|
79
|
+
rendered `context_template`, retrieved knowledge excerpts (numbered `[n]` and
|
|
80
|
+
explicitly marked *untrusted* — the prompt instructs the model to treat them as
|
|
81
|
+
evidence, never instructions), `## Conversation History`, `## Tool Results`,
|
|
82
|
+
the filtered `available_tools` list, and finally `## Current Message` with a
|
|
83
|
+
prompt-injection warning.
|
|
84
|
+
|
|
85
|
+
## Invoking an agent
|
|
86
|
+
|
|
87
|
+
The interactive path is `POST /api/ai-agents?action=run` with `{ thread_id,
|
|
88
|
+
user_message, mode }` — `mode` is `step`, `complete`, or `episode` (default
|
|
89
|
+
`complete`). Before any tokens are spent the handler runs a prompt-injection
|
|
90
|
+
preflight (`detectInjection`); the app's `security.prompt_injection` setting
|
|
91
|
+
chooses `off` | `flag` (default) | `block`. Each claimed run debits the
|
|
92
|
+
`agent_step` rate limit.
|
|
93
|
+
|
|
94
|
+
Inside a run, `runAgent`/`runAgentStep` build context, call inference, dispatch
|
|
95
|
+
any `tool_calls` the model returns through `executeToolAction`, persist the
|
|
96
|
+
outbound `messages` row stamped with `agent_id` and `prompt_config_id`, and
|
|
97
|
+
manage the episode lease (see [concurrency](./concurrency-and-idempotency.md)).
|
|
98
|
+
|
|
99
|
+
Pipelines reach AI through stage types, not the `run` action:
|
|
100
|
+
|
|
101
|
+
- `agent_inference` — POSTs `{ context, model, temperature, max_tokens }` to
|
|
102
|
+
`config.webhook_url` via `integration-outbound` (`generic-http`); without a
|
|
103
|
+
`webhook_url` it returns a mock response.
|
|
104
|
+
- `search_knowledge` — retrieval over knowledge sources for the stage config's
|
|
105
|
+
`query`.
|
|
106
|
+
- `generate_embeddings` / `revoke_embeddings` / `kb_ingest` / `kb_revoke` /
|
|
107
|
+
`create_interaction` — delegate to the `embeddings`, `kb`, and
|
|
108
|
+
`ai-interactions` functions described below.
|
|
109
|
+
|
|
110
|
+
## The LLM call itself
|
|
111
|
+
|
|
112
|
+
`_shared/llm.ts` does not hold API keys. `callOpenAI` routes every request
|
|
113
|
+
through `integration-outbound` with `integration_slug: 'openai'` and
|
|
114
|
+
`credential_field: 'api_token'` — credentials come from the `integrations`
|
|
115
|
+
table like any other outbound call. `callOpenAIChat` defaults to
|
|
116
|
+
`gpt-4o-mini`/0.7/4000 tokens, overridable per call and via
|
|
117
|
+
`integration.config.models.chat` / `.embedding`. When no OpenAI integration or
|
|
118
|
+
credential exists, inference returns a clearly-marked mock response rather than
|
|
119
|
+
failing.
|
|
120
|
+
|
|
121
|
+
## Embeddings and retrieval
|
|
122
|
+
|
|
123
|
+
`embeddings` (`.framework/functions/embeddings.ts`) manages the
|
|
124
|
+
`public.embeddings` vector store:
|
|
125
|
+
|
|
126
|
+
- `action=generate` builds embeddable text per **vector type**. The type's
|
|
127
|
+
`design_schema.vector_types` decides which builders run — default
|
|
128
|
+
`['semantic', 'structure']`; registered builders also cover `intent`,
|
|
129
|
+
`cause`, `procedure`, `outcome`, `context`, and `error_signal`. Each vector
|
|
130
|
+
type occupies a fixed `chunk_index` slot so concurrent types never collide
|
|
131
|
+
on the unique constraint, and unchanged content is skipped by hash.
|
|
132
|
+
- `action=revoke` sets `revoked_at` on a document's vectors; `action=approve`
|
|
133
|
+
sets `approved_at` and clears revocation.
|
|
134
|
+
- `action=search-similar` calls the `match_embeddings` RPC (pgvector KNN,
|
|
135
|
+
defined in migrations `034`/`044`) with `query_embedding`, `match_count`,
|
|
136
|
+
`similarity_threshold`, and optional account/vector-type/item-type/document
|
|
137
|
+
filters.
|
|
138
|
+
- `action=search-semantic` is a Postgres `websearch` full-text query over the
|
|
139
|
+
`content` column — the lexical complement to vector search.
|
|
140
|
+
|
|
141
|
+
## Interaction records
|
|
142
|
+
|
|
143
|
+
`ai-interactions` (`.framework/functions/ai-interactions.ts`) writes the
|
|
144
|
+
feedback/observability side of the loop as `items` rows (default type
|
|
145
|
+
`ai_feedback_record`, overridable via `type_slug`; the call returns
|
|
146
|
+
`{ skipped: true }` when the type isn't installed). Actions:
|
|
147
|
+
`submit_feedback`, `create_interaction` (what the pipeline stage calls — it
|
|
148
|
+
stamps `thread_id`, `ai_message_id`, `retrieved_sources`, `ai_confidence`,
|
|
149
|
+
`kb_candidate`, and review fields), `update_interaction`,
|
|
150
|
+
`create_cil_proposal`, and `list`.
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Apps and Renderers
|
|
3
|
+
audience: developers, agents
|
|
4
|
+
kind: concept
|
|
5
|
+
verified_against: f69c37d
|
|
6
|
+
verified_on: 2026-10-29
|
|
7
|
+
source_files:
|
|
8
|
+
- .framework/migrations/001_core_schema.sql
|
|
9
|
+
- .framework/migrations/013_get_account_apps_role_filter.sql
|
|
10
|
+
- .framework/migrations/014_apps_required_roles.sql
|
|
11
|
+
- .framework/src/App.tsx
|
|
12
|
+
- .framework/src/components/AppWrapper.tsx
|
|
13
|
+
- .framework/src/components/CustomAppLoader.tsx
|
|
14
|
+
- .framework/src/components/app-shell/GenericAppShell.tsx
|
|
15
|
+
- .framework/src/components/app-shell/GenericListPage.tsx
|
|
16
|
+
- .framework/src/components/app-shell/GenericDetailPage.tsx
|
|
17
|
+
- .framework/functions/_shared/app-manifest.ts
|
|
18
|
+
- .framework/functions/apps.ts
|
|
19
|
+
- .framework/cli/commands/create-app.ts
|
|
20
|
+
- .framework/cli/commands/install-app.ts
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
# Apps and Renderers
|
|
24
|
+
|
|
25
|
+
An app in Spine is a **row in `public.apps`** plus — for apps that render their
|
|
26
|
+
own UI — a directory of code. The registry row decides where the app mounts,
|
|
27
|
+
who can open it, and *how* it renders. The `renderer` column is the fork in
|
|
28
|
+
the road.
|
|
29
|
+
|
|
30
|
+
## The registry row
|
|
31
|
+
|
|
32
|
+
`.framework/migrations/001_core_schema.sql` defines `public.apps`: unique
|
|
33
|
+
`slug`, `name`, `route_prefix`, `renderer` (CHECK constraint:
|
|
34
|
+
`'generic' | 'custom' | 'none'`), `min_role`, `required_roles` (added in
|
|
35
|
+
migration 014), `nav_items`, `config`, `integration_deps`, `metadata`,
|
|
36
|
+
`is_active`, `is_system`, `owner_account_id`, domains.
|
|
37
|
+
|
|
38
|
+
- `route_prefix` (defaults to `/<slug>`) is where the SPA mounts the app —
|
|
39
|
+
`.framework/src/App.tsx` builds a `<Route>` per active app.
|
|
40
|
+
- `is_active: false` removes the app from navigation and routing.
|
|
41
|
+
- `is_system: true` protects the row from tenant-level deletion.
|
|
42
|
+
|
|
43
|
+
## Role gating
|
|
44
|
+
|
|
45
|
+
Two layers decide who sees an app:
|
|
46
|
+
|
|
47
|
+
- Server side, `get_account_apps` filters the app list by the caller's role
|
|
48
|
+
slugs (`.framework/functions/apps.ts`;
|
|
49
|
+
`.framework/migrations/013_get_account_apps_role_filter.sql`).
|
|
50
|
+
- Client side, `AppWrapper` computes `required_roles` (falling back to
|
|
51
|
+
`[min_role]`) and renders an access-denied screen unless the user holds
|
|
52
|
+
`system_admin` or one of those roles (`.framework/src/components/AppWrapper.tsx`).
|
|
53
|
+
|
|
54
|
+
This gates *opening the app*; it does not replace record-level
|
|
55
|
+
`record_permissions` on the data underneath.
|
|
56
|
+
|
|
57
|
+
## `renderer = 'generic'`
|
|
58
|
+
|
|
59
|
+
The platform renders the app's list and detail pages for you, straight from
|
|
60
|
+
each type's `design_schema.views`:
|
|
61
|
+
|
|
62
|
+
- `App.tsx` mounts `GenericAppShell` for `renderer === 'generic'` apps.
|
|
63
|
+
- `GenericAppShell` builds the sidebar from the app's `nav_items` and, for
|
|
64
|
+
every nav item carrying `type_slug` (and optional `view`), generates
|
|
65
|
+
list/detail/new routes backed by `GenericListPage` and
|
|
66
|
+
`GenericDetailPage`.
|
|
67
|
+
- `GenericListPage` turns `views.default_list` (or the nav item's named view)
|
|
68
|
+
into sortable columns; `GenericDetailPage` renders `views.default_detail`
|
|
69
|
+
sections with per-field read/write awareness.
|
|
70
|
+
|
|
71
|
+
**What you get for free:** a working list page, detail page, create/edit form,
|
|
72
|
+
sorting, filters, field-type rendering, and permission-aware editing — with
|
|
73
|
+
zero app code. **What it costs:** every type the app serves must have complete
|
|
74
|
+
`fields`, `views.default_list`, and `views.default_detail`, or the pages fail
|
|
75
|
+
with "No schema available" / schema-load errors. See
|
|
76
|
+
[Design Schema](design-schema.md).
|
|
77
|
+
|
|
78
|
+
## `renderer = 'custom'`
|
|
79
|
+
|
|
80
|
+
The app ships its own React code under `custom/apps/<slug>/` (a
|
|
81
|
+
project-owned directory, not part of the framework package). `App.tsx` mounts
|
|
82
|
+
`CustomAppLoader`, which resolves the app through an
|
|
83
|
+
`import.meta.glob('custom/apps/*/index.tsx')` — the app's `index.tsx` is its
|
|
84
|
+
entry point and default-exports the routed component (the `create-app`
|
|
85
|
+
scaffold generates exactly this shape).
|
|
86
|
+
|
|
87
|
+
A custom app renders its own pages but still uses the platform for data:
|
|
88
|
+
`/api/admin-data` for runtime CRUD, `/api/types` for schemas. Custom apps
|
|
89
|
+
must never write their own CRUD wrappers — that bypasses permissions,
|
|
90
|
+
validation, audit, and triggers (see [Runtime Records](runtime-records.md)).
|
|
91
|
+
|
|
92
|
+
`renderer = 'none'` registers the app (data, types, integrations) with no UI
|
|
93
|
+
surface.
|
|
94
|
+
|
|
95
|
+
## The manifest
|
|
96
|
+
|
|
97
|
+
`custom/apps/<slug>/manifest.json` is the app's declarative half, validated by
|
|
98
|
+
`loadManifest` in `.framework/functions/_shared/app-manifest.ts` (the
|
|
99
|
+
`AppManifest` interface is the contract):
|
|
100
|
+
|
|
101
|
+
```json
|
|
102
|
+
{
|
|
103
|
+
"name": "My App",
|
|
104
|
+
"slug": "my-app",
|
|
105
|
+
"description": "…",
|
|
106
|
+
"version": "1.0.0",
|
|
107
|
+
"required_roles": ["member"],
|
|
108
|
+
"route_prefix": "/my-app",
|
|
109
|
+
"renderer": "custom",
|
|
110
|
+
"routes": ["/my-app", "/my-app/dashboard"],
|
|
111
|
+
"nav_items": [{ "title": "Dashboard", "path": "/my-app/dashboard", "icon": "LayoutDashboard", "order": 1 }],
|
|
112
|
+
"features": [],
|
|
113
|
+
"dependencies": [],
|
|
114
|
+
"entry_point": "./index.tsx",
|
|
115
|
+
"is_public": false,
|
|
116
|
+
"auth_required": true
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Validated requirements: non-empty `name`, `slug` matching
|
|
121
|
+
`^[a-z][a-z0-9-]*$` (and equal to the directory name for discovery), and
|
|
122
|
+
`required_roles` as an array of strings. Optional keys the install flow also
|
|
123
|
+
honors: `config` (merged onto the apps row), `seed` (declared seed entries),
|
|
124
|
+
`owned_types`, `registration`, `metadata`, `min_role`,
|
|
125
|
+
`preview_domain`/`prod_domain`.
|
|
126
|
+
|
|
127
|
+
## Install and seed flow
|
|
128
|
+
|
|
129
|
+
Two paths put an app on disk and in the registry:
|
|
130
|
+
|
|
131
|
+
- **`npx spine-framework create-app <slug>`** — scaffolds
|
|
132
|
+
`custom/apps/<slug>/` (`manifest.json` + `index.tsx` + `components/`) and
|
|
133
|
+
inserts a `public.apps` row with `renderer: 'custom'` and
|
|
134
|
+
`route_prefix: /<slug>` (`.framework/cli/commands/create-app.ts`).
|
|
135
|
+
- **`npx spine-framework install-app <package>`** — copies a packaged app into
|
|
136
|
+
`custom/apps/<slug>/`, runs migrations, then `seedApp`:
|
|
137
|
+
- upserts the `apps` row from the manifest (`appRowPayload`: name,
|
|
138
|
+
description, version, nav_items, route_prefix, renderer, required_roles,
|
|
139
|
+
min_role, config merge, metadata);
|
|
140
|
+
- applies seed files in dependency order — conventional
|
|
141
|
+
`seed/roles.json` → `roles` (conflict `slug`), `seed/types.json` →
|
|
142
|
+
`types` (conflict `kind,slug`, **no** `app_id` injection,
|
|
143
|
+
`ignoreDuplicates`) — plus any `manifest.seed` entries;
|
|
144
|
+
- refuses to overwrite a `types` row whose `design_schema` differs from the
|
|
145
|
+
seed unless the slug is declared in `manifest.owned_types` and
|
|
146
|
+
`--reconcile-owned-types` was passed (types are global — see
|
|
147
|
+
[Scope and Accounts](scope-and-accounts.md));
|
|
148
|
+
- syncs the `registration` block into `spine.config.json` (the project-owned
|
|
149
|
+
config file at the project root).
|
|
150
|
+
- **`update-db-app <slug>`** re-syncs a local `manifest.json` to the DB without
|
|
151
|
+
touching files; **`seed-app <slug>`** re-runs seeds;
|
|
152
|
+
**`uninstall-app`** removes the app (`--keep-data` preserves seeded rows).
|
|
153
|
+
All in `.framework/cli/commands/install-app.ts`.
|
|
154
|
+
|
|
155
|
+
## The practical split
|
|
156
|
+
|
|
157
|
+
Use `generic` when the app is "records in, records out" — lists, detail forms,
|
|
158
|
+
admin-style editing — and keep views accurate. Use `custom` when the UX is a
|
|
159
|
+
product surface (dashboards, wizards, bespoke layouts) and treat the manifest
|
|
160
|
+
as your app's contract: registration, roles, seeds, nav. Either way, types and
|
|
161
|
+
`design_schema` do the data modeling — the renderer only decides who draws the
|
|
162
|
+
pixels.
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Auth: sessions, API keys, and invites"
|
|
3
|
+
audience: developers, agents
|
|
4
|
+
kind: concept
|
|
5
|
+
verified_against: f69c37d
|
|
6
|
+
verified_on: 2026-10-29
|
|
7
|
+
source_files:
|
|
8
|
+
- .framework/functions/_shared/principal.ts
|
|
9
|
+
- .framework/functions/_shared/middleware.ts
|
|
10
|
+
- .framework/functions/_shared/hash-api-key.ts
|
|
11
|
+
- .framework/functions/_shared/rate-limit.ts
|
|
12
|
+
- .framework/functions/api-keys.ts
|
|
13
|
+
- .framework/functions/invites.ts
|
|
14
|
+
- .framework/functions/integration-routes.ts
|
|
15
|
+
- .framework/migrations/035_rate_limits.sql
|
|
16
|
+
- .framework/migrations/052_registration_security.sql
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# Auth: sessions, API keys, and invites
|
|
20
|
+
|
|
21
|
+
Every request to a Spine function resolves to one **Principal**
|
|
22
|
+
(`_shared/principal.ts`): either `type: 'human'` (a `people` row + roles) or
|
|
23
|
+
`type: 'machine'` (an `api_keys` row + scopes), plus `ANONYMOUS_PRINCIPAL` and
|
|
24
|
+
`SYSTEM_PRINCIPAL` sentinels. `resolvePrincipal` is the single dispatch point,
|
|
25
|
+
run by `createHandler` / `createPublicHandler` in `_shared/middleware.ts`:
|
|
26
|
+
|
|
27
|
+
1. `x-api-key` header → **machine** principal.
|
|
28
|
+
2. A valid `INTERNAL_INVOKE_SECRET` (function-to-function) → `x-cron-id` /
|
|
29
|
+
`x-trigger-id` impersonation, else `SYSTEM_PRINCIPAL`. These headers are
|
|
30
|
+
honored only alongside the secret — a bare id is ignored.
|
|
31
|
+
3. `Authorization: Bearer <jwt>` → **human** principal via Supabase auth.
|
|
32
|
+
4. Nothing → `ANONYMOUS_PRINCIPAL` (machine, no scopes, no account).
|
|
33
|
+
|
|
34
|
+
The principal also selects the database client (`getPrincipalDb`): humans get a
|
|
35
|
+
JWT-scoped client (row-level security applies); machines and anonymous get the
|
|
36
|
+
service-role client, so endpoint code is responsible for checking machine
|
|
37
|
+
scopes — `machineHasScope` honors `resource:action`, `resource:*`, and `*:*`,
|
|
38
|
+
and `admin-data` treats a machine with `*:*` as authorized (with role
|
|
39
|
+
`system_admin`) while lesser keys still pass through record permissions.
|
|
40
|
+
|
|
41
|
+
## Browser sessions (humans)
|
|
42
|
+
|
|
43
|
+
`Bearer <supabase-jwt>` → `supabase.auth.getUser` → `resolveInternalPersonId`
|
|
44
|
+
binds the auth user to a `people` row by `auth_uid` (email fallback never
|
|
45
|
+
binds unclaimed invite stubs), then loads the person with its role join. The
|
|
46
|
+
principal carries `roles: [role.slug]`, `accountId`, and the raw JWT in
|
|
47
|
+
`authContext` (never serialized — `formatPrincipalForAudit` omits it). Results
|
|
48
|
+
cache for 60 seconds per warm instance, so role/account changes take up to a
|
|
49
|
+
minute to propagate; a person with neither `account_id` nor `role_id`
|
|
50
|
+
(registration not yet placed) is never cached.
|
|
51
|
+
|
|
52
|
+
A human's effective rights come from `roles` → `isSystemAdmin` /
|
|
53
|
+
`humanHasRole` → the `design_schema.record_permissions` on each type. An
|
|
54
|
+
invalid or expired JWT silently becomes anonymous — endpoints then fail with
|
|
55
|
+
"Authentication required" / "Account context required" rather than 401s.
|
|
56
|
+
|
|
57
|
+
## API keys (machines)
|
|
58
|
+
|
|
59
|
+
Mint a key with `POST /api/api-keys` (`.framework/functions/api-keys.ts`):
|
|
60
|
+
`{ name, key_type?, scopes?, rate_limit?, expires_at?, integration_id?,
|
|
61
|
+
app_id?, machine_type?, is_internal? }`. The response returns `raw_key` —
|
|
62
|
+
`sk_<32 hex>` — **once**, with a "store this now" warning. Only the first 8
|
|
63
|
+
characters (`key_prefix`, the lookup index) and a PBKDF2-SHA256 hash
|
|
64
|
+
(100k iterations, `pbkdf2$iterations$salt$hash`) are stored; `omitSecretFields`
|
|
65
|
+
strips secret material from every read response.
|
|
66
|
+
|
|
67
|
+
Use it as `x-api-key: sk_...` on any function endpoint. Resolution:
|
|
68
|
+
|
|
69
|
+
- Prefix-matched candidate keys must be `is_active` and unexpired, then the
|
|
70
|
+
hash is verified with a timing-safe compare.
|
|
71
|
+
- Per-key rate limit: `rate_limit` (default 100) requests per
|
|
72
|
+
`config.rate_limit_window_seconds` (default 60), counted in
|
|
73
|
+
`api_key_usage_logs`.
|
|
74
|
+
- Per-IP ceiling: `rate_limit_hit` RPC on `ip:<ip>:<path>`, default 300/minute
|
|
75
|
+
(`SPINE_IP_RATE_LIMIT`), fail-open.
|
|
76
|
+
- Every request writes an `api_key_usage_logs` row (method, path, IP,
|
|
77
|
+
user-agent) — `action=usage-logs` on the same endpoint reads them back.
|
|
78
|
+
|
|
79
|
+
The resulting machine principal inherits `key.scopes`, `key.machine_type`
|
|
80
|
+
(default `'integration'`), `key.is_internal`, and `key.account_id`. A `*:*`
|
|
81
|
+
scope acts as `system_admin`. `integration-routes` (inbound webhooks)
|
|
82
|
+
*requires* a machine principal — a JWT cannot deliver webhooks — and keys
|
|
83
|
+
pinned to `integration_id`/`app_id` are restricted to those integrations.
|
|
84
|
+
Related actions: `action=validate` (verify a key + check permissions),
|
|
85
|
+
`action=revoke` (sets `is_active=false`), `action=usage-logs`.
|
|
86
|
+
|
|
87
|
+
Shared throttling also applies: `checkRateLimit(ctx, action)`
|
|
88
|
+
(`_shared/rate-limit.ts`) resolves named policies from
|
|
89
|
+
`accounts.data.rate_limits[action]` (account override) or the calling app's
|
|
90
|
+
`config.rate_limits` (with `rate_limit_profile` indirection), and enforces
|
|
91
|
+
them through the atomic `rate_limit_hit` RPC on `public.rate_limits`
|
|
92
|
+
(migration `035`). Over-limit → 429 (`RateLimitError`); a `fail_closed`
|
|
93
|
+
policy → 503 when the counter RPC is unavailable.
|
|
94
|
+
|
|
95
|
+
## First admin and invites
|
|
96
|
+
|
|
97
|
+
Bootstrap is a Postgres trigger, not an endpoint: on the first `auth.users`
|
|
98
|
+
signup, `handle_new_auth_user` (migrations `010`, hardened in `052`) takes an
|
|
99
|
+
advisory lock, creates the `people` row, and — only when **no** `people` rows
|
|
100
|
+
exist — grants the `system_admin` role under the `spine-system` account.
|
|
101
|
+
Serialization means two concurrent first signups cannot both become
|
|
102
|
+
system-admin; `system_admin` can never be assigned through registration.
|
|
103
|
+
|
|
104
|
+
Everyone after that arrives by invite (`.framework/functions/invites.ts`):
|
|
105
|
+
|
|
106
|
+
- `action=create-invite` — requires `system_admin` or an `*_admin` role on the
|
|
107
|
+
target account; `role_slug: 'system_admin'` is refused. Creates a stub
|
|
108
|
+
`people` row holding `data.invite_token` (16-char) and
|
|
109
|
+
`invite_expires_at` (7 days), returns
|
|
110
|
+
`{APP_BASE_URL}/register?t=<token>`, and can send the email through a
|
|
111
|
+
configured email integration.
|
|
112
|
+
- `action=validate-token` (public) — checks the token exists, is unblocked,
|
|
113
|
+
and unexpired.
|
|
114
|
+
- `action=complete-registration` — after Supabase signup, calls the
|
|
115
|
+
service-role-only `complete_registration_placement` RPC, which atomically
|
|
116
|
+
stamps `account_id` + `role_id` from the app's server-side registration
|
|
117
|
+
config (`new_account`, `existing_account`, `join_request` paths). Client
|
|
118
|
+
claims about role or target are never trusted — the RPC is the only
|
|
119
|
+
placement writer.
|
|
120
|
+
- Admin management: `list-invites`, `revoke-invite`, `reinvite`,
|
|
121
|
+
`block-invite`, `approve`, `deny`, `list-members`, `update-member`,
|
|
122
|
+
`deactivate-member`. `check-first-user` and `check-domain` are public
|
|
123
|
+
read-only probes used by the registration UI.
|
|
124
|
+
|
|
125
|
+
## What each surface can reach
|
|
126
|
+
|
|
127
|
+
| | Browser JWT | API key | Internal secret |
|
|
128
|
+
|---|---|---|---|
|
|
129
|
+
| Principal | human (roles) | machine (scopes) | system / cron / trigger |
|
|
130
|
+
| DB client | JWT-scoped (RLS) | service role | service role |
|
|
131
|
+
| `admin-data`, entity APIs | role + record permissions | scopes; `*:*` = system admin | full |
|
|
132
|
+
| `integration-routes` (inbound) | no | required | — |
|
|
133
|
+
| `system-cron` / `trigger-scheduler` | no | `SCHEDULER_API_KEY` or internal machine | yes |
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Automation: pipelines, triggers, and timers"
|
|
3
|
+
audience: developers, agents
|
|
4
|
+
kind: concept
|
|
5
|
+
verified_against: f69c37d
|
|
6
|
+
verified_on: 2026-10-29
|
|
7
|
+
source_files:
|
|
8
|
+
- .framework/functions/pipelines.ts
|
|
9
|
+
- .framework/functions/pipeline-executions.ts
|
|
10
|
+
- .framework/functions/pipeline-runner-background.ts
|
|
11
|
+
- .framework/functions/_shared/pipeline-runner.ts
|
|
12
|
+
- .framework/functions/_shared/trigger-engine.ts
|
|
13
|
+
- .framework/functions/timers.ts
|
|
14
|
+
- .framework/functions/triggers.ts
|
|
15
|
+
- .framework/functions/trigger-scheduler.ts
|
|
16
|
+
- .framework/functions/system-cron.ts
|
|
17
|
+
- .framework/functions/admin-data.ts
|
|
18
|
+
- .framework/migrations/001_core_schema.sql
|
|
19
|
+
- .framework/migrations/018_runtime_tables.sql
|
|
20
|
+
- .framework/migrations/020_schema_reconciliation.sql
|
|
21
|
+
- .framework/migrations/068_cron_schedule_timer_rpcs.sql
|
|
22
|
+
- netlify.toml
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
# Automation: pipelines, triggers, and timers
|
|
26
|
+
|
|
27
|
+
Spine automation is three primitives plus a ledger:
|
|
28
|
+
|
|
29
|
+
- A **pipeline** (`public.pipelines`) is an ordered JSON `stages` array — the
|
|
30
|
+
definition of *what runs*.
|
|
31
|
+
- A **trigger** (`public.triggers`) maps a record event
|
|
32
|
+
(`<kind>_created` / `<kind>_updated` / `<kind>_deleted`) to a pipeline — *when
|
|
33
|
+
data changes*.
|
|
34
|
+
- A **timer** (`public.timers`) points at a pipeline and carries
|
|
35
|
+
`next_execution` — *when the clock says so*.
|
|
36
|
+
- A **pipeline execution** (`public.pipeline_executions`) is the durable record
|
|
37
|
+
of one run — *what actually happened*.
|
|
38
|
+
|
|
39
|
+
There is no separate "workflow engine" service. Everything runs inside the
|
|
40
|
+
Netlify functions, coordinated through Postgres rows.
|
|
41
|
+
|
|
42
|
+
## Pipelines and the runner
|
|
43
|
+
|
|
44
|
+
`runPipeline` in `.framework/functions/_shared/pipeline-runner.ts` is the single
|
|
45
|
+
executor. It loads the pipeline row (inactive or missing pipelines throw),
|
|
46
|
+
creates or claims a `pipeline_executions` row, then walks `pipeline.stages` in
|
|
47
|
+
array order.
|
|
48
|
+
|
|
49
|
+
Each stage names a `stage_type`. That value is **not** a function name — it is a
|
|
50
|
+
slug looked up in `public.actions` (`eq('slug', stage.stage_type)`,
|
|
51
|
+
`is_active = true`). The action row supplies `handler_module` and `handler`;
|
|
52
|
+
the stage's `config` is merged over `action.config` along with internal keys
|
|
53
|
+
`_pipelineId`, `_executionId`, `_stageIndex`, and `_triggerData`. The runner
|
|
54
|
+
then routes on `handler_module`:
|
|
55
|
+
|
|
56
|
+
- `functions` — dispatch to a named handler in the runner's `stageHandlers`
|
|
57
|
+
map. Built-ins include `update_item`, `create_record`, `http_request`,
|
|
58
|
+
`send_notification`, `send_email`, `run_pipeline`, `query_items`,
|
|
59
|
+
`search_knowledge`, `agent_inference`, `generate_embeddings`,
|
|
60
|
+
`revoke_embeddings`, `kb_ingest`, `kb_revoke`, and `create_interaction`.
|
|
61
|
+
- `integrations` — dispatch through `integration-outbound` (the same path
|
|
62
|
+
outbound webhooks use).
|
|
63
|
+
- `webhook` — POST/GET to `action.config.url` via `integration-outbound`.
|
|
64
|
+
|
|
65
|
+
Stage config strings support `{{trigger.*}}` and `{{record.*}}` placeholders,
|
|
66
|
+
resolved recursively against the trigger envelope and the firing record's data
|
|
67
|
+
(`record` is `triggerData.entity.data`). Timer- and manual-fired runs have **no
|
|
68
|
+
record context** — `{{record.*}}` placeholders pass through unresolved and
|
|
69
|
+
appear literally in stage output (e.g. notification text). Author timer-driven
|
|
70
|
+
stages against `{{trigger.*}}` or fixed text.
|
|
71
|
+
|
|
72
|
+
A stage that throws is recorded as `failed` in the stage results; the run then
|
|
73
|
+
aborts **unless** the stage sets `continue_on_error: true`. Completed and
|
|
74
|
+
failed runs both write terminal status, stage results, and duration back to the
|
|
75
|
+
execution row — `pipeline-executions` is therefore the ground truth for "did it
|
|
76
|
+
work", and `status: 'cancelled'` is available for operator-initiated stops.
|
|
77
|
+
|
|
78
|
+
## Triggers: record events
|
|
79
|
+
|
|
80
|
+
Writes that go through `admin-data` call into
|
|
81
|
+
`.framework/functions/_shared/trigger-engine.ts`: creates fire
|
|
82
|
+
`fireCreateTriggers`, updates fire `fireUpdateTriggers` with the previous row
|
|
83
|
+
snapshot. (The delete path currently does not invoke `fireDeleteTriggers` —
|
|
84
|
+
`*_deleted` event types exist in the union, but nothing emits them from
|
|
85
|
+
`admin-data`.) Trigger fires are caught-and-logged, so a failing trigger never
|
|
86
|
+
rolls back the write that caused it.
|
|
87
|
+
|
|
88
|
+
`checkAndFireTriggers` loads active triggers for the event type and evaluates
|
|
89
|
+
each against `config`:
|
|
90
|
+
|
|
91
|
+
- `config.entity_type` must match the runtime table's kind (`items` → `item`,
|
|
92
|
+
`people` → `person`, and so on).
|
|
93
|
+
- `config.type_slug` restricts to one `types.slug`; the engine resolves the
|
|
94
|
+
slug from the record's `type_id` when needed.
|
|
95
|
+
- `config.filters` is a map of path → expected value. Paths support dot
|
|
96
|
+
notation and Postgres-style `data->>'field'` accessors; expected values may
|
|
97
|
+
be literals, arrays (membership), or operator objects using `$eq`, `$ne`,
|
|
98
|
+
`$gt`, `$gte`, `$lt`, `$lte`, `$in`, `$nin`, `$exists`.
|
|
99
|
+
- `config.fire_on_transition` makes an update trigger fire only when the record
|
|
100
|
+
*moves into* the matching state — the previous snapshot must not already
|
|
101
|
+
match. This is what prevents "every save to a published record re-fires the
|
|
102
|
+
pipeline".
|
|
103
|
+
|
|
104
|
+
A matching trigger does **not** run the pipeline inline. It:
|
|
105
|
+
|
|
106
|
+
1. Inserts a `pipeline_executions` row with `status: 'queued'` (a stable
|
|
107
|
+
execution id exists before any work starts).
|
|
108
|
+
2. POSTs to `/.netlify/functions/pipeline-runner-background` with an
|
|
109
|
+
`X-Internal-Secret` header, awaiting only the dispatch ack (bounded by a 5s
|
|
110
|
+
timeout) — the caller's request never waits for the pipeline to finish.
|
|
111
|
+
3. Records `record_trigger_fire` bookkeeping and emits a `trigger.fired` audit
|
|
112
|
+
event (or `trigger.failed` on error).
|
|
113
|
+
|
|
114
|
+
The background function verifies `INTERNAL_INVOKE_SECRET`, reconstructs a
|
|
115
|
+
machine context, and calls `runPipeline` with the pre-created `execution_id`,
|
|
116
|
+
which claims the row `queued → running` instead of inserting a duplicate.
|
|
117
|
+
|
|
118
|
+
## Timers and the scheduler
|
|
119
|
+
|
|
120
|
+
A timer row carries `timer_type`, `pipeline_id`, `config`, and
|
|
121
|
+
`next_execution`. The `get_due_timers` RPC returns active timers whose
|
|
122
|
+
`pipeline_id` is set and `next_execution <= now()`; `update_timer_after_run`
|
|
123
|
+
stamps `last_execution`, increments `execution_count`/`success_count`/
|
|
124
|
+
`failure_count`, and reschedules `next_execution` from
|
|
125
|
+
`config.interval_minutes` — a timer with no positive `interval_minutes` gets
|
|
126
|
+
`next_execution = NULL` and never runs again.
|
|
127
|
+
|
|
128
|
+
The clock is `trigger-scheduler`, a Netlify scheduled function configured for
|
|
129
|
+
`* * * * *` (every minute) in `netlify.toml`. It accepts genuine Netlify
|
|
130
|
+
Clockwork invocations (schedule header or `Netlify Clockwork` user-agent *plus*
|
|
131
|
+
a `{"next_run": ...}` body) and fabricates a system-principal context;
|
|
132
|
+
non-Clockwork calls fall through to the cron handler's own auth, which accepts
|
|
133
|
+
either `?api_key=$SCHEDULER_API_KEY` or an internal machine principal — anything
|
|
134
|
+
else gets 403 and an audit event.
|
|
135
|
+
|
|
136
|
+
Each tick, `system-cron` does all of the following:
|
|
137
|
+
|
|
138
|
+
- **Sweeps stale executions** — `sweepStaleExecutions` marks
|
|
139
|
+
`running`/`pending`/`queued` rows untouched for over 10 minutes as `failed`.
|
|
140
|
+
This is the safety net for a runner that died mid-flight or a dispatch that
|
|
141
|
+
never arrived.
|
|
142
|
+
- **Runs due schedules** — `get_due_schedules` → validate the creator → load
|
|
143
|
+
the `public.actions` row → execute as the schedule's delegated machine
|
|
144
|
+
principal (`schedules`/`actions`/`schedule_executions` are the runtime
|
|
145
|
+
scheduling tables).
|
|
146
|
+
- **Runs due timers** — each due timer runs its pipeline as machine principal
|
|
147
|
+
`timer:<timer_id>` with scope `pipelines:execute`, then
|
|
148
|
+
`update_timer_after_run` reschedules it.
|
|
149
|
+
- **Evaluates threshold alerts** — `threshold_alerts` rows compare
|
|
150
|
+
`error_rate`, `latency_p95`, or `pipeline_failure_rate` metrics against a
|
|
151
|
+
value and fire the alert's `pipeline_id` on breach.
|
|
152
|
+
- **Retries webhook deliveries** — `retryPendingDeliveries` runs every tick.
|
|
153
|
+
- **Cleans logs** — `cleanup_old_logs` runs on the 00:00 UTC tick.
|
|
154
|
+
|
|
155
|
+
## Observing executions
|
|
156
|
+
|
|
157
|
+
- `pipeline-executions` (`.framework/functions/pipeline-executions.ts`):
|
|
158
|
+
`GET` list/get, `action=running`, `action=stats`, `action=cleanup`
|
|
159
|
+
(`days_to_keep` defaults to 30), plus `start`/`complete`/`cancel`.
|
|
160
|
+
- `pipelines` (`.framework/functions/pipelines.ts`): CRUD plus
|
|
161
|
+
`action=by-trigger`, `action=executions`, `action=toggle`.
|
|
162
|
+
- `triggers` and `timers` (`.framework/functions/triggers.ts`,
|
|
163
|
+
`.framework/functions/timers.ts`): CRUD plus `toggle`; triggers also expose
|
|
164
|
+
`listByEvent` and `getExecutions`.
|
|
165
|
+
- CLI: `spine-framework pipelines run <id>` executes a pipeline;
|
|
166
|
+
`spine-framework pipelines executions <id>` lists recent runs
|
|
167
|
+
(`.framework/cli/commands/pipelines.ts`).
|
|
168
|
+
|
|
169
|
+
An execution stuck in `queued` means dispatch failed before the background
|
|
170
|
+
function claimed it — the sweeper fails it after 10 minutes. An execution stuck
|
|
171
|
+
in `running` means the runner died — same sweeper, same outcome.
|
|
172
|
+
|
|
173
|
+
## How the pieces connect
|
|
174
|
+
|
|
175
|
+
1. A client writes a record through `admin-data`; `fireCreateTriggers` /
|
|
176
|
+
`fireUpdateTriggers` run in-process after the write.
|
|
177
|
+
2. The trigger engine matches the event to active `triggers` rows, inserts a
|
|
178
|
+
`queued` execution, and dispatches to `pipeline-runner-background`.
|
|
179
|
+
3. Independently, Netlify Clockwork invokes `trigger-scheduler` every minute;
|
|
180
|
+
`system-cron` runs due timers and schedules through the same `runPipeline`,
|
|
181
|
+
sweeps dead executions, retries deliveries, and evaluates alert thresholds.
|
|
182
|
+
4. `runPipeline` resolves each `stage_type` through `public.actions` and runs
|
|
183
|
+
the stage handlers — which is also where outbound integrations, AI agent
|
|
184
|
+
calls, and embedding jobs enter the system.
|
|
185
|
+
5. Every step lands in `pipeline_executions`, `logs`, and the audit trail,
|
|
186
|
+
which is what the admin UI and `pipeline-executions` API read.
|