dirigent-core 0.23.4__tar.gz → 0.24.0__tar.gz

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.
Files changed (69) hide show
  1. {dirigent_core-0.23.4 → dirigent_core-0.24.0}/PKG-INFO +4 -4
  2. {dirigent_core-0.23.4 → dirigent_core-0.24.0}/pyproject.toml +4 -4
  3. {dirigent_core-0.23.4 → dirigent_core-0.24.0}/pyproject.toml.orig +4 -4
  4. {dirigent_core-0.23.4 → dirigent_core-0.24.0}/src/dirigent_core/alembic/versions/0001_baseline_schema.py +118 -0
  5. {dirigent_core-0.23.4 → dirigent_core-0.24.0}/src/dirigent_core/blockdocs.py +43 -2
  6. {dirigent_core-0.23.4 → dirigent_core-0.24.0}/src/dirigent_core/config.py +30 -2
  7. {dirigent_core-0.23.4 → dirigent_core-0.24.0}/src/dirigent_core/documents.py +33 -2
  8. {dirigent_core-0.23.4 → dirigent_core-0.24.0}/src/dirigent_core/engine/__init__.py +2 -0
  9. {dirigent_core-0.23.4 → dirigent_core-0.24.0}/src/dirigent_core/engine/definition.py +176 -6
  10. {dirigent_core-0.23.4 → dirigent_core-0.24.0}/src/dirigent_core/engine/executor.py +14 -1
  11. {dirigent_core-0.23.4 → dirigent_core-0.24.0}/src/dirigent_core/engine/expansion.py +32 -3
  12. {dirigent_core-0.23.4 → dirigent_core-0.24.0}/src/dirigent_core/engine/references.py +12 -3
  13. dirigent_core-0.24.0/src/dirigent_core/engine/replies.py +194 -0
  14. {dirigent_core-0.23.4 → dirigent_core-0.24.0}/src/dirigent_core/engine/runs.py +117 -4
  15. {dirigent_core-0.23.4 → dirigent_core-0.24.0}/src/dirigent_core/engine/services.py +8 -0
  16. {dirigent_core-0.23.4 → dirigent_core-0.24.0}/src/dirigent_core/engine/state.py +154 -7
  17. {dirigent_core-0.23.4 → dirigent_core-0.24.0}/src/dirigent_core/examples.py +31 -0
  18. {dirigent_core-0.23.4 → dirigent_core-0.24.0}/src/dirigent_core/messages.py +215 -13
  19. {dirigent_core-0.23.4 → dirigent_core-0.24.0}/src/dirigent_core/models.py +88 -0
  20. {dirigent_core-0.23.4 → dirigent_core-0.24.0}/src/dirigent_core/pipelines.py +13 -1
  21. {dirigent_core-0.23.4 → dirigent_core-0.24.0}/src/dirigent_core/plugins.py +33 -2
  22. {dirigent_core-0.23.4 → dirigent_core-0.24.0}/src/dirigent_core/ratelimit.py +9 -4
  23. {dirigent_core-0.23.4 → dirigent_core-0.24.0}/src/dirigent_core/reporting.py +1 -1
  24. {dirigent_core-0.23.4 → dirigent_core-0.24.0}/src/dirigent_core/retention.py +15 -2
  25. dirigent_core-0.24.0/src/dirigent_core/sources/__init__.py +1 -0
  26. dirigent_core-0.24.0/src/dirigent_core/sources/endpoint.py +281 -0
  27. dirigent_core-0.24.0/src/dirigent_core/sources/entries.py +79 -0
  28. dirigent_core-0.24.0/src/dirigent_core/sources/intake.py +416 -0
  29. dirigent_core-0.24.0/src/dirigent_core/sources/kinds.py +22 -0
  30. dirigent_core-0.24.0/src/dirigent_core/sources/rows.py +407 -0
  31. dirigent_core-0.24.0/src/dirigent_core/sources/rules.py +310 -0
  32. {dirigent_core-0.23.4 → dirigent_core-0.24.0}/src/dirigent_core/telemetry.py +10 -2
  33. {dirigent_core-0.23.4 → dirigent_core-0.24.0}/src/dirigent_core/triggers/watches.py +2 -2
  34. {dirigent_core-0.23.4 → dirigent_core-0.24.0}/src/dirigent_core/wakeups.py +7 -2
  35. {dirigent_core-0.23.4 → dirigent_core-0.24.0}/LICENSE +0 -0
  36. {dirigent_core-0.23.4 → dirigent_core-0.24.0}/README.md +0 -0
  37. {dirigent_core-0.23.4 → dirigent_core-0.24.0}/src/dirigent_core/__init__.py +0 -0
  38. {dirigent_core-0.23.4 → dirigent_core-0.24.0}/src/dirigent_core/alembic/env.py +0 -0
  39. {dirigent_core-0.23.4 → dirigent_core-0.24.0}/src/dirigent_core/alembic/script.py.mako +0 -0
  40. {dirigent_core-0.23.4 → dirigent_core-0.24.0}/src/dirigent_core/alerting.py +0 -0
  41. {dirigent_core-0.23.4 → dirigent_core-0.24.0}/src/dirigent_core/artifacts.py +0 -0
  42. {dirigent_core-0.23.4 → dirigent_core-0.24.0}/src/dirigent_core/auth.py +0 -0
  43. {dirigent_core-0.23.4 → dirigent_core-0.24.0}/src/dirigent_core/configdocs.py +0 -0
  44. {dirigent_core-0.23.4 → dirigent_core-0.24.0}/src/dirigent_core/database.py +0 -0
  45. {dirigent_core-0.23.4 → dirigent_core-0.24.0}/src/dirigent_core/directory.py +0 -0
  46. {dirigent_core-0.23.4 → dirigent_core-0.24.0}/src/dirigent_core/documentschema.py +0 -0
  47. {dirigent_core-0.23.4 → dirigent_core-0.24.0}/src/dirigent_core/engine/claim.py +0 -0
  48. {dirigent_core-0.23.4 → dirigent_core-0.24.0}/src/dirigent_core/engine/context.py +0 -0
  49. {dirigent_core-0.23.4 → dirigent_core-0.24.0}/src/dirigent_core/engine/failure.py +0 -0
  50. {dirigent_core-0.23.4 → dirigent_core-0.24.0}/src/dirigent_core/engine/recovery.py +0 -0
  51. {dirigent_core-0.23.4 → dirigent_core-0.24.0}/src/dirigent_core/errors.py +0 -0
  52. {dirigent_core-0.23.4 → dirigent_core-0.24.0}/src/dirigent_core/ids.py +0 -0
  53. {dirigent_core-0.23.4 → dirigent_core-0.24.0}/src/dirigent_core/logging.py +0 -0
  54. {dirigent_core-0.23.4 → dirigent_core-0.24.0}/src/dirigent_core/migrations.py +0 -0
  55. {dirigent_core-0.23.4 → dirigent_core-0.24.0}/src/dirigent_core/protocol.py +0 -0
  56. {dirigent_core-0.23.4 → dirigent_core-0.24.0}/src/dirigent_core/py.typed +0 -0
  57. {dirigent_core-0.23.4 → dirigent_core-0.24.0}/src/dirigent_core/registry.py +0 -0
  58. {dirigent_core-0.23.4 → dirigent_core-0.24.0}/src/dirigent_core/scheduler.py +0 -0
  59. {dirigent_core-0.23.4 → dirigent_core-0.24.0}/src/dirigent_core/schemas.py +0 -0
  60. {dirigent_core-0.23.4 → dirigent_core-0.24.0}/src/dirigent_core/secrets.py +0 -0
  61. {dirigent_core-0.23.4 → dirigent_core-0.24.0}/src/dirigent_core/storage.py +0 -0
  62. {dirigent_core-0.23.4 → dirigent_core-0.24.0}/src/dirigent_core/trigger_documents.py +0 -0
  63. {dirigent_core-0.23.4 → dirigent_core-0.24.0}/src/dirigent_core/triggers/__init__.py +0 -0
  64. {dirigent_core-0.23.4 → dirigent_core-0.24.0}/src/dirigent_core/triggers/backfill.py +0 -0
  65. {dirigent_core-0.23.4 → dirigent_core-0.24.0}/src/dirigent_core/triggers/materialize.py +0 -0
  66. {dirigent_core-0.23.4 → dirigent_core-0.24.0}/src/dirigent_core/triggers/schedules.py +0 -0
  67. {dirigent_core-0.23.4 → dirigent_core-0.24.0}/src/dirigent_core/triggers/webhooks.py +0 -0
  68. {dirigent_core-0.23.4 → dirigent_core-0.24.0}/src/dirigent_core/types.py +0 -0
  69. {dirigent_core-0.23.4 → dirigent_core-0.24.0}/src/dirigent_core/worker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dirigent-core
3
- Version: 0.23.4
3
+ Version: 0.24.0
4
4
  Summary: Dirigent engine core: schema, configuration, and plugin host.
5
5
  License-Expression: LicenseRef-Proprietary
6
6
  License-File: LICENSE
@@ -12,9 +12,9 @@ Requires-Dist: argon2-cffi>=25.1.0
12
12
  Requires-Dist: asyncpg>=0.31.0
13
13
  Requires-Dist: cronsim>=2.7
14
14
  Requires-Dist: cryptography>=46.0.5
15
- Requires-Dist: dirigent-client==0.23.4
16
- Requires-Dist: dirigent-common==0.23.4
17
- Requires-Dist: dirigent-plugin==0.23.4
15
+ Requires-Dist: dirigent-client==0.24.0
16
+ Requires-Dist: dirigent-common==0.24.0
17
+ Requires-Dist: dirigent-plugin==0.24.0
18
18
  Requires-Dist: jsonschema>=4.26.0
19
19
  Requires-Dist: opentelemetry-api>=1.44.0
20
20
  Requires-Dist: pydantic-settings>=2.15.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "dirigent-core"
3
- version = "0.23.4"
3
+ version = "0.24.0"
4
4
  description = "Dirigent engine core: schema, configuration, and plugin host."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
@@ -17,9 +17,9 @@ dependencies = [
17
17
  "asyncpg>=0.31.0",
18
18
  "cronsim>=2.7",
19
19
  "cryptography>=46.0.5",
20
- "dirigent-client==0.23.4",
21
- "dirigent-common==0.23.4",
22
- "dirigent-plugin==0.23.4",
20
+ "dirigent-client==0.24.0",
21
+ "dirigent-common==0.24.0",
22
+ "dirigent-plugin==0.24.0",
23
23
  "jsonschema>=4.26.0",
24
24
  "opentelemetry-api>=1.44.0",
25
25
  "pydantic-settings>=2.15.0",
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "dirigent-core"
3
- version = "0.23.4"
3
+ version = "0.24.0"
4
4
  description = "Dirigent engine core: schema, configuration, and plugin host."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
@@ -17,9 +17,9 @@ dependencies = [
17
17
  "asyncpg>=0.31.0",
18
18
  "cronsim>=2.7",
19
19
  "cryptography>=46.0.5",
20
- "dirigent-client==0.23.4",
21
- "dirigent-common==0.23.4",
22
- "dirigent-plugin==0.23.4",
20
+ "dirigent-client==0.24.0",
21
+ "dirigent-common==0.24.0",
22
+ "dirigent-plugin==0.24.0",
23
23
  "jsonschema>=4.26.0",
24
24
  "opentelemetry-api>=1.44.0",
25
25
  "pydantic-settings>=2.15.0",
@@ -545,6 +545,71 @@ def upgrade() -> None:
545
545
  batch_op.create_index(batch_op.f("ix_watches_pipeline_id"), ["pipeline_id"], unique=False)
546
546
  batch_op.create_index(batch_op.f("ix_watches_trigger_document_id"), ["trigger_document_id"], unique=False)
547
547
 
548
+ op.create_table(
549
+ "sources",
550
+ sa.Column("id", sa.Uuid(), nullable=False),
551
+ sa.Column("pipeline_id", sa.Uuid(), nullable=False),
552
+ sa.Column("code", sa.String(length=200), nullable=False),
553
+ sa.Column("name", sa.String(length=200), nullable=True),
554
+ sa.Column("description", sa.Text(), nullable=True),
555
+ sa.Column("kind", sa.String(length=200), nullable=False),
556
+ sa.Column("config", sa.JSON().with_variant(postgresql.JSONB(astext_type=Text()), "postgresql"), nullable=False),
557
+ sa.Column("token_hash", sa.String(length=128), nullable=True),
558
+ sa.Column("token_prefix", sa.String(length=16), nullable=True),
559
+ sa.Column("paused", sa.Boolean(), server_default=sa.false(), nullable=False),
560
+ sa.Column("accepted", sa.Integer(), server_default=sa.text("0"), nullable=False),
561
+ sa.Column("refused", sa.Integer(), server_default=sa.text("0"), nullable=False),
562
+ sa.Column("last_input_at", dirigent_core.types.UtcDateTime(timezone=True), nullable=True),
563
+ sa.Column(
564
+ "created_at",
565
+ dirigent_core.types.UtcDateTime(timezone=True),
566
+ server_default=sa.func.now(),
567
+ nullable=False,
568
+ ),
569
+ sa.Column(
570
+ "updated_at",
571
+ dirigent_core.types.UtcDateTime(timezone=True),
572
+ server_default=sa.func.now(),
573
+ nullable=False,
574
+ ),
575
+ sa.ForeignKeyConstraint(
576
+ ["pipeline_id"], ["pipelines.id"], name=op.f("fk_sources_pipeline_id_pipelines"), ondelete="CASCADE"
577
+ ),
578
+ sa.PrimaryKeyConstraint("id", name=op.f("pk_sources")),
579
+ sa.UniqueConstraint("pipeline_id", "code", name=op.f("uq_sources_pipeline_id_code")),
580
+ sa.UniqueConstraint("token_hash", name=op.f("uq_sources_token_hash")),
581
+ )
582
+ with op.batch_alter_table("sources", schema=None) as batch_op:
583
+ batch_op.create_index(batch_op.f("ix_sources_pipeline_id"), ["pipeline_id"], unique=False)
584
+
585
+ op.create_table(
586
+ "source_routes",
587
+ sa.Column("id", sa.Uuid(), nullable=False),
588
+ sa.Column("source_id", sa.Uuid(), nullable=False),
589
+ sa.Column("method", sa.String(length=16), nullable=False),
590
+ sa.Column("template", sa.String(length=1000), nullable=False),
591
+ sa.Column("normalised", sa.String(length=1000), nullable=False),
592
+ sa.Column(
593
+ "created_at",
594
+ dirigent_core.types.UtcDateTime(timezone=True),
595
+ server_default=sa.func.now(),
596
+ nullable=False,
597
+ ),
598
+ sa.Column(
599
+ "updated_at",
600
+ dirigent_core.types.UtcDateTime(timezone=True),
601
+ server_default=sa.func.now(),
602
+ nullable=False,
603
+ ),
604
+ sa.ForeignKeyConstraint(
605
+ ["source_id"], ["sources.id"], name=op.f("fk_source_routes_source_id_sources"), ondelete="CASCADE"
606
+ ),
607
+ sa.PrimaryKeyConstraint("id", name=op.f("pk_source_routes")),
608
+ sa.UniqueConstraint("method", "normalised", name=op.f("uq_source_routes_method_normalised")),
609
+ )
610
+ with op.batch_alter_table("source_routes", schema=None) as batch_op:
611
+ batch_op.create_index(batch_op.f("ix_source_routes_source_id"), ["source_id"], unique=False)
612
+
548
613
  op.create_table(
549
614
  "runs",
550
615
  sa.Column("id", sa.Uuid(), nullable=False),
@@ -591,6 +656,7 @@ def upgrade() -> None:
591
656
  ),
592
657
  sa.Column("triggered_by_id", sa.Uuid(), nullable=True),
593
658
  sa.Column("triggered_by_label", sa.String(length=200), nullable=True),
659
+ sa.Column("source_step", sa.String(length=200), nullable=True),
594
660
  sa.Column("traceparent", sa.String(length=64), nullable=True),
595
661
  sa.Column("error", sa.Text(), nullable=True),
596
662
  sa.Column("error_code", sa.String(length=128), nullable=True),
@@ -639,6 +705,7 @@ def upgrade() -> None:
639
705
  "ix_runs_pipeline_id_status_created_at", ["pipeline_id", "status", "created_at"], unique=False
640
706
  )
641
707
  batch_op.create_index(batch_op.f("ix_runs_status"), ["status"], unique=False)
708
+ batch_op.create_index("ix_runs_triggered_by_id_status", ["triggered_by_id", "status"], unique=False)
642
709
 
643
710
  op.create_table(
644
711
  "notifications",
@@ -838,6 +905,43 @@ def upgrade() -> None:
838
905
  batch_op.create_index("ix_webhook_deliveries_created_at", ["created_at"], unique=False)
839
906
  batch_op.create_index("ix_webhook_deliveries_webhook_id_id", ["webhook_id", "id"], unique=False)
840
907
 
908
+ op.create_table(
909
+ "source_inputs",
910
+ sa.Column("id", sa.BigInteger().with_variant(sa.Integer(), "sqlite"), autoincrement=True, nullable=False),
911
+ sa.Column("source_id", sa.Uuid(), nullable=False),
912
+ sa.Column("run_id", sa.Uuid(), nullable=True),
913
+ sa.Column(
914
+ "outcome",
915
+ sa.Enum("accepted", "refused", name="input_outcome", native_enum=False, length=32),
916
+ nullable=False,
917
+ ),
918
+ sa.Column("status", sa.Integer(), nullable=False),
919
+ sa.Column("code", sa.String(length=128), nullable=True),
920
+ sa.Column("reason", sa.Text(), nullable=True),
921
+ sa.Column("params", sa.JSON().with_variant(postgresql.JSONB(astext_type=Text()), "postgresql"), nullable=True),
922
+ sa.Column("method", sa.String(length=16), nullable=False),
923
+ sa.Column("path", sa.String(length=2000), nullable=False),
924
+ sa.Column("body_bytes", sa.Integer(), server_default=sa.text("0"), nullable=False),
925
+ sa.Column("duration_ms", sa.Integer(), server_default=sa.text("0"), nullable=False),
926
+ sa.Column("caller", sa.String(length=64), nullable=True),
927
+ sa.Column(
928
+ "created_at",
929
+ dirigent_core.types.UtcDateTime(timezone=True),
930
+ server_default=sa.func.now(),
931
+ nullable=False,
932
+ ),
933
+ sa.ForeignKeyConstraint(
934
+ ["run_id"], ["runs.id"], name=op.f("fk_source_inputs_run_id_runs"), ondelete="SET NULL"
935
+ ),
936
+ sa.ForeignKeyConstraint(
937
+ ["source_id"], ["sources.id"], name=op.f("fk_source_inputs_source_id_sources"), ondelete="CASCADE"
938
+ ),
939
+ sa.PrimaryKeyConstraint("id", name=op.f("pk_source_inputs")),
940
+ )
941
+ with op.batch_alter_table("source_inputs", schema=None) as batch_op:
942
+ batch_op.create_index("ix_source_inputs_created_at", ["created_at"], unique=False)
943
+ batch_op.create_index("ix_source_inputs_source_id_id", ["source_id", "id"], unique=False)
944
+
841
945
  op.create_table(
842
946
  "step_attempts",
843
947
  sa.Column("id", sa.Uuid(), nullable=False),
@@ -1034,6 +1138,11 @@ def downgrade() -> None:
1034
1138
  batch_op.drop_index("ix_webhook_deliveries_created_at")
1035
1139
 
1036
1140
  op.drop_table("webhook_deliveries")
1141
+ with op.batch_alter_table("source_inputs", schema=None) as batch_op:
1142
+ batch_op.drop_index("ix_source_inputs_source_id_id")
1143
+ batch_op.drop_index("ix_source_inputs_created_at")
1144
+
1145
+ op.drop_table("source_inputs")
1037
1146
  with op.batch_alter_table("schedule_firings", schema=None) as batch_op:
1038
1147
  batch_op.drop_index("ix_schedule_firings_schedule_id_id")
1039
1148
  batch_op.drop_index("ix_schedule_firings_created_at")
@@ -1052,6 +1161,7 @@ def downgrade() -> None:
1052
1161
 
1053
1162
  op.drop_table("notifications")
1054
1163
  with op.batch_alter_table("runs", schema=None) as batch_op:
1164
+ batch_op.drop_index("ix_runs_triggered_by_id_status")
1055
1165
  batch_op.drop_index(batch_op.f("ix_runs_status"))
1056
1166
  batch_op.drop_index("ix_runs_pipeline_id_status_created_at")
1057
1167
  batch_op.drop_index("ix_runs_finished_at")
@@ -1062,6 +1172,14 @@ def downgrade() -> None:
1062
1172
  batch_op.drop_index(batch_op.f("ix_watches_pipeline_id"))
1063
1173
 
1064
1174
  op.drop_table("watches")
1175
+ with op.batch_alter_table("source_routes", schema=None) as batch_op:
1176
+ batch_op.drop_index(batch_op.f("ix_source_routes_source_id"))
1177
+
1178
+ op.drop_table("source_routes")
1179
+ with op.batch_alter_table("sources", schema=None) as batch_op:
1180
+ batch_op.drop_index(batch_op.f("ix_sources_pipeline_id"))
1181
+
1182
+ op.drop_table("sources")
1065
1183
  with op.batch_alter_table("webhook_triggers", schema=None) as batch_op:
1066
1184
  batch_op.drop_index(batch_op.f("ix_webhook_triggers_trigger_document_id"))
1067
1185
  batch_op.drop_index(batch_op.f("ix_webhook_triggers_pipeline_id"))
@@ -7,7 +7,7 @@ catalog disagree.
7
7
  import json
8
8
  from typing import Final, cast
9
9
 
10
- from dirigent_client.schemas import BlockEntry, BlockKind, Catalog, SurfaceEntry
10
+ from dirigent_client.schemas import BlockEntry, BlockKind, Catalog, SourceEntry, SurfaceEntry
11
11
  from dirigent_common import JsonMap
12
12
  from dirigent_core.configdocs import first_paragraph, one_line
13
13
 
@@ -53,7 +53,8 @@ SURFACES_PREAMBLE: Final = """## Other surfaces
53
53
  Blocks are two of the six surfaces a plugin contributes to. Three of the other four -- storage
54
54
  backends, notifiers and connection kinds -- are listed here by id, since a document references
55
55
  them by code and never by id of any other kind. The sixth, a JSON Schema format checker, is in
56
- [JSON Schema](json-schema.md).
56
+ [JSON Schema](json-schema.md). Source kinds, which a source node names as ``source.kind``, are
57
+ listed after them; see [Source and reply](concepts.md#source-and-reply).
57
58
  """
58
59
 
59
60
  NO_DESCRIPTION: Final = "--"
@@ -73,6 +74,7 @@ def render(catalog: Catalog) -> str:
73
74
  parts.append(_surfaces("Storage schemes", "scheme", catalog.storage_schemes))
74
75
  parts.append(_surfaces("Notifiers", "id", catalog.notifiers))
75
76
  parts.append(_surfaces("Connection kinds", "id", catalog.connection_kinds))
77
+ parts.append(_sources(catalog.sources))
76
78
  return "\n".join(parts).rstrip() + "\n"
77
79
 
78
80
 
@@ -210,6 +212,45 @@ def _surfaces(title: str, label: str, entries: list[SurfaceEntry]) -> str:
210
212
  )
211
213
 
212
214
 
215
+ def _sources(entries: list[SourceEntry]) -> str:
216
+ """Render the source kinds as a short table, and the input each one hands on."""
217
+ if not entries:
218
+ return "### Source kinds\n\n_None installed._\n"
219
+ rows = [
220
+ f"| `{entry.kind}` | {entry.hosting} | {_yes(entry.replies)} | {entry.summary} | `{entry.plugin}` |"
221
+ for entry in entries
222
+ ]
223
+ lines = [
224
+ "### Source kinds",
225
+ "",
226
+ "| Kind | Hosting | Replies | Summary | Contributed by |",
227
+ "| --- | --- | --- | --- | --- |",
228
+ *rows,
229
+ "",
230
+ ]
231
+ for entry in entries:
232
+ lines.extend(
233
+ [
234
+ f"#### `{entry.kind}`",
235
+ "",
236
+ "**Config**",
237
+ "",
238
+ _schema_table(entry.config_schema),
239
+ "",
240
+ "**Input it hands on**",
241
+ "",
242
+ _schema_table(entry.output_schema),
243
+ "",
244
+ ]
245
+ )
246
+ return "\n".join(lines)
247
+
248
+
249
+ def _yes(flag: bool) -> str:
250
+ """Render a flag as a table cell reads it."""
251
+ return "yes" if flag else "no"
252
+
253
+
213
254
  def _anchor(block_id: str) -> str:
214
255
  """Render the anchor mkdocs gives a heading whose text is a backticked block id."""
215
256
  return block_id.replace(".", "")
@@ -290,6 +290,9 @@ class Settings(BaseSettings):
290
290
  retention_deliveries: Duration | None = None
291
291
  """How long a webhook delivery is kept."""
292
292
 
293
+ retention_inputs: Duration | None = None
294
+ """How long the record of an input that reached a source is kept."""
295
+
293
296
  retention_firings: Duration | None = None
294
297
  """How long a schedule firing is kept."""
295
298
 
@@ -355,6 +358,31 @@ class Settings(BaseSettings):
355
358
 
356
359
  A real webhook's own ``rate_limit_per_minute`` applies on top of this."""
357
360
 
361
+ endpoint_prefix: str = "/endpoints"
362
+ """Where endpoint sources are served, outside the versioned API.
363
+
364
+ A proxy in front of an instance publishes this prefix and ``/hooks`` and keeps
365
+ ``/api/v1`` inside, so a source's route is public without the API being so."""
366
+
367
+ source_max_body: Size = Field(default=1_048_576, ge=1)
368
+ """The largest body any endpoint reads, whatever its own ``max_body`` says; reading stops one byte past it."""
369
+
370
+ source_intake_rate_per_minute: int = Field(default=600, ge=1)
371
+ """How fast calls to one method and path are taken, before any source is looked up.
372
+
373
+ A source's own ``rate`` applies on top of this."""
374
+
375
+ source_reply_max: Duration = Field(default=timedelta(seconds=60), gt=timedelta(0))
376
+ """The longest a caller is held for a reply, whatever a reply node's ``timeout`` says."""
377
+
378
+ source_max_waiting: int = Field(default=256, ge=1)
379
+ """How many callers one server process holds open for their replies at once.
380
+
381
+ A call past it is answered 503 before its run is created."""
382
+
383
+ source_reply_poll: Duration = Field(default=timedelta(milliseconds=50), gt=timedelta(0))
384
+ """How often one server process reads the replies its held callers wait for, in one query."""
385
+
358
386
  login_rate_per_minute: int = Field(default=10, ge=1)
359
387
  """How many login attempts one address, and one username, get a minute.
360
388
 
@@ -383,10 +411,10 @@ class Settings(BaseSettings):
383
411
  """Accept a comma-separated ``key=value`` string for a mapping setting."""
384
412
  return parse_string_mapping(value)
385
413
 
386
- @field_validator("api_prefix")
414
+ @field_validator("api_prefix", "endpoint_prefix")
387
415
  @classmethod
388
416
  def _normalise_prefix(cls, value: str) -> str:
389
- """Keep the API prefix rooted and free of a trailing slash."""
417
+ """Keep a prefix rooted and free of a trailing slash."""
390
418
  prefix = "/" + value.strip("/")
391
419
  return prefix.rstrip("/") or "/"
392
420
 
@@ -45,8 +45,10 @@ from dirigent_core.messages import (
45
45
  NOT_A_MAPPING,
46
46
  NOT_YAML,
47
47
  PARAMS_SCHEMA_INVALID,
48
+ REFERENCE_ERROR_NOT_UPSTREAM,
48
49
  REFERENCE_GRID_IN_CONFIG,
49
50
  REFERENCE_MALFORMED_ARTIFACTS,
51
+ REFERENCE_MALFORMED_ERROR,
50
52
  REFERENCE_MALFORMED_ITEM,
51
53
  REFERENCE_MALFORMED_RUN,
52
54
  REFERENCE_MALFORMED_STEP,
@@ -366,6 +368,7 @@ def validate_against_catalog(
366
368
  blocks: Mapping[str, Any] | None = None,
367
369
  format_checker: FormatChecker | None = None,
368
370
  target: TriggerTarget | None = None,
371
+ source_kinds: Mapping[str, Any] | None = None,
369
372
  ) -> list[ValidationIssue]:
370
373
  """Check a document against what this instance actually has, reporting everything at once.
371
374
 
@@ -379,8 +382,13 @@ def validate_against_catalog(
379
382
  against; unset, they are checked against the base formats alone, which is all a caller
380
383
  holding no host can know. ``target`` is the pipeline a triggers document names, as the
381
384
  instance holds it now; on the offline path there is none to read, and only the clocks and
382
- the codes are checked.
385
+ the codes are checked. ``source_kinds`` is the source kinds the instance knows; unset, the
386
+ built-in ones.
383
387
  """
388
+ from dirigent_core.sources.kinds import built_in_kinds
389
+ from dirigent_core.sources.rules import source_issues
390
+
391
+ kinds = source_kinds if source_kinds is not None else built_in_kinds()
384
392
  checker = format_checker if format_checker is not None else base_format_checker()
385
393
  sensors = _sensors(catalog) if check_blocks else None
386
394
  if isinstance(definition, TriggersDefinition):
@@ -406,6 +414,7 @@ def validate_against_catalog(
406
414
  issues.extend(_carried_schema_issues(definition))
407
415
  issues.extend(_reference_issues(definition))
408
416
  issues.extend(_trigger_issues(definition, checker, check_params=not malformed_params, sensors=sensors))
417
+ issues.extend(source_issues(definition, kinds))
409
418
  if check_blocks:
410
419
  issues.extend(
411
420
  _step_block_issues(
@@ -607,6 +616,10 @@ def _triggers_document_issues(
607
616
  return [ValidationIssue.of(TARGET_PIPELINE_MISSING, location="pipeline", code=repr(definition.pipeline))]
608
617
  if not target.active:
609
618
  return [ValidationIssue.of(TARGET_PIPELINE_INACTIVE, location="pipeline", code=repr(definition.pipeline))]
619
+ if target.definition is not None and target.definition.sources:
620
+ from dirigent_core.sources.rules import triggers_issues
621
+
622
+ return triggers_issues(target.code, definition.triggers)
610
623
  return _trigger_issues(definition, format_checker, against=target.definition, sensors=sensors)
611
624
 
612
625
 
@@ -672,6 +685,8 @@ def _step_block_issues(
672
685
  issues: list[ValidationIssue] = []
673
686
  for name in sorted(definition.steps):
674
687
  step = definition.steps[name]
688
+ if step.block is None:
689
+ continue
675
690
  entry = catalog.block(step.block)
676
691
  if entry is None:
677
692
  issues.append(
@@ -851,6 +866,12 @@ def _reference_issues(definition: PipelineDefinition) -> list[ValidationIssue]:
851
866
  problem = _reference_problem(reference.strip(), definition, name, upstream, declared, family)
852
867
  if problem is not None:
853
868
  issues.append(_at(f"steps.{name}.config", problem))
869
+ if step.reply is not None:
870
+ written = cast("JsonValue", step.reply.model_dump(mode="json", exclude={"timeout"}))
871
+ for reference in sorted(set(references_in(written))):
872
+ problem = _reference_problem(reference.strip(), definition, name, upstream, declared, family)
873
+ if problem is not None:
874
+ issues.append(_at(f"steps.{name}.reply", problem))
854
875
  issues.extend(_fan_out_literal_issues(name, step.for_each))
855
876
  issues.extend(_adoption_issues(definition, name))
856
877
  for reference in sorted(set(references_in(cast("JsonValue", step.for_each)))):
@@ -952,6 +973,14 @@ def _reference_problem(
952
973
  if target not in upstream:
953
974
  return Issue.of(REFERENCE_NOT_UPSTREAM, reference=reference, target=repr(target), step=repr(step))
954
975
  return None
976
+ case ["steps", target, "error", *path]:
977
+ if target not in definition.steps:
978
+ return Issue.of(REFERENCE_UNKNOWN_STEP, reference=reference, step=repr(target))
979
+ if target not in upstream:
980
+ return Issue.of(REFERENCE_ERROR_NOT_UPSTREAM, reference=reference, target=repr(target), step=repr(step))
981
+ if path[:1] not in (["code"], ["message"], ["params"]) or (path[:1] != ["params"] and len(path) > 1):
982
+ return Issue.of(REFERENCE_MALFORMED_ERROR, reference=reference)
983
+ return None
955
984
  case ["steps", target, "items"]:
956
985
  return Issue.of(REFERENCE_GRID_IN_CONFIG, reference=reference, target=repr(target), bare_target=target)
957
986
  case ["steps", target, "item", "output", *_]:
@@ -1032,7 +1061,9 @@ def summarize(definition: Document) -> DocumentSummary:
1032
1061
  pipeline = definition.pipeline if isinstance(definition, TriggersDefinition) else None
1033
1062
  steps = sorted(definition.steps) if isinstance(definition, PipelineDefinition) else []
1034
1063
  blocks = (
1035
- sorted({step.block for step in definition.steps.values()}) if isinstance(definition, PipelineDefinition) else []
1064
+ sorted({step.block for step in definition.steps.values() if step.block is not None})
1065
+ if isinstance(definition, PipelineDefinition)
1066
+ else []
1036
1067
  )
1037
1068
  return DocumentSummary(
1038
1069
  kind=definition.kind,
@@ -36,6 +36,7 @@ from dirigent_core.engine.references import ReferenceScope, UnknownReference, re
36
36
  from dirigent_core.engine.runs import (
37
37
  Attribution,
38
38
  EngineRuns,
39
+ Entry,
39
40
  FanOutError,
40
41
  Provenance,
41
42
  RunCreationError,
@@ -62,6 +63,7 @@ __all__ = [
62
63
  "KIND_PIPELINE",
63
64
  "KIND_TRIGGERS",
64
65
  "Attribution",
66
+ "Entry",
65
67
  "BufferedLogger",
66
68
  "ClaimedUnit",
67
69
  "ConcurrencyPolicy",