Input: Design documents from dev/specs/infp-504-artifact-composition/
Prerequisites: plan.md, spec.md, research.md, data-model.md, contracts/
Jira Epic: IFC-2275
Tests: Included with each implementation task (per project convention).
Organization: Tasks grouped by user story. US4 (security gate) is foundational and combined with US1 as both are P1.
- [P]: Can run in parallel (different files, no dependencies)
- [Story]: Which user story this task belongs to (e.g., US1, US2, US3, US4)
- Include exact file paths in descriptions
Purpose: Exception class and trust model that ALL user stories depend on
CRITICAL: No user story work can begin until this phase is complete
- T001 [P] Create
JinjaFilterErrorexception class ininfrahub_sdk/template/exceptions.py— subclass ofJinjaTemplateErrorwithfilter_name,message, and optionalhintattributes. Include unit tests for instantiation, inheritance chain, and message formatting. (IFC-2367) - T002 [P] Implement
ExecutionContextflag enum and migrateFilterDefinitionininfrahub_sdk/template/filters.py— addExecutionContext(Flag)withCORE,WORKER,LOCAL,ALLvalues. ReplaceFilterDefinition.trusted: boolwithallowed_contexts: ExecutionContext. Add backward-compattrustedproperty. Migrate all 138 existing filter entries (trusted=True→ALL,trusted=False→LOCAL). Updatevalidate()ininfrahub_sdk/template/__init__.pyto accept optionalcontext: ExecutionContextparameter (takes precedence overrestricted;restricted=True→CORE,restricted=False→LOCAL). Include unit tests for all 3 contexts with existing filters, backward compat path, and no regressions. (IFC-2368)
Checkpoint: Foundation ready — JinjaFilterError and ExecutionContext available for all stories
Goal: A Jinja2 template can use storage_id | artifact_content to inline rendered sub-artifact content. Validation blocks this filter in CORE context but allows it in WORKER context.
Independent Test: Render a template calling artifact_content with a mocked InfrahubClient and verify output matches expected content. Validate the same template in CORE context raises JinjaTemplateOperationViolationError, and in WORKER context passes.
- T003 [US1] Create
InfrahubFiltersclass ininfrahub_sdk/template/infrahub_filters.py— new file. Class holdsInfrahubClientreference, exposes async filter methods. Methods areasync def(Jinja2auto_awaithandles them in async render mode per R-001). RaisesJinjaFilterErrorwhen called without a client. Include unit tests for instantiation with/without client. (IFC-2371) - T004 [US1] Implement
artifact_contentasync method onInfrahubFiltersininfrahub_sdk/template/infrahub_filters.py— usesself.client.object_store.get(identifier=storage_id). RaisesJinjaFilterErroron: null/empty storage_id, retrieval failure, permission denied (catchAuthenticationErrorper R-006). Artifacts are always text (no binary check needed per R-003). Include unit tests: happy path (mocked ObjectStore), null, empty, not-found, network error, permission denied, no-client error with descriptive message. (IFC-2372) - T005 [US1] [US4] Add
clientparameter toJinja2Template.__init__and wire up filter registration ininfrahub_sdk/template/__init__.py— addclient: InfrahubClient | None = Noneparam. When client provided: instantiateInfrahubFilters, registerartifact_contentinto Jinja2 env filter map. Addenable_async=Trueto_get_file_based_environment()(per R-001 caveat). Registerartifact_contentinFilterDefinitionregistry withallowed_contexts=ExecutionContext.WORKER. Include unit tests: render with client (mocked), render without client (error), validation in CORE (blocked), WORKER (allowed), LOCAL (allowed). Verify existing untrusted filters likesaferemain blocked in WORKER context (US4 AC3). Also addedset_client()setter for deferred client injection per PR #885 feedback. (IFC-2375 partial + IFC-2376 partial)
Checkpoint: US1 + US4 fully functional. artifact_content renders in WORKER context, blocked in CORE. MVP complete.
Goal: A Jinja2 template can use storage_id | file_object_content to inline file object content with binary rejection.
Independent Test: Render a template calling file_object_content with a mocked client. Verify text content returned, binary content rejected. Validation blocks in CORE, allows in WORKER.
- T006 [P] [US2] Add
get_file_by_storage_id()method toObjectStoreininfrahub_sdk/object_store.py— async method using endpointGET /api/files/by-storage-id/{storage_id}. Checkcontent-typeresponse header: allowtext/*,application/json,application/yaml,application/x-yaml; reject all others with descriptive error. Handle 401/403 asAuthenticationError. Include unit tests: text response, binary rejection, 404, auth failure, network error. (IFC-2373) - T007 [US2] Implement
file_object_contentasync method onInfrahubFiltersininfrahub_sdk/template/infrahub_filters.py— uses newself.client.object_store.get_file_by_storage_id(storage_id). Same error handling asartifact_contentplus binary content error (delegated to ObjectStore). Include unit tests: happy path, all error conditions, binary content rejection. (IFC-2374) - T008 [US2] Register
file_object_contentfilter inJinja2TemplateandFilterDefinitionininfrahub_sdk/template/__init__.pyandinfrahub_sdk/template/filters.py— register when client provided.allowed_contexts=ExecutionContext.WORKER. Include unit tests: render with client, validation in CORE (blocked), WORKER (allowed). (IFC-2375 partial + IFC-2376 partial)
Checkpoint: US2 complete. file_object_content works alongside artifact_content.
Goal: Templates can chain artifact_content | from_json or artifact_content | from_yaml to access structured data.
Independent Test: Render a template chaining artifact_content | from_json and verify parsed fields accessible. from_json("") and from_yaml("") return {}.
- T009 [P] [US3] Implement
from_jsonfilter function ininfrahub_sdk/template/infrahub_filters.py— pure sync function (no client needed). Empty string →{}(explicit special-case sincejson.loads("")raises). Malformed JSON →JinjaFilterError. Register inFilterDefinitionwithallowed_contexts=ExecutionContext.ALL. Register unconditionally inJinja2Template._set_filters(). Include unit tests: valid JSON dict, valid JSON list, empty string →{}, malformed → error, render through template. (IFC-2369) - T010 [P] [US3] Implement
from_yamlfilter function ininfrahub_sdk/template/infrahub_filters.py— pure sync function. Empty string →{}(explicit special-case sinceyaml.safe_load("")returnsNone). Malformed YAML →JinjaFilterError. Register inFilterDefinitionwithallowed_contexts=ExecutionContext.ALL. Register unconditionally inJinja2Template._set_filters(). Include unit tests: valid YAML mapping, valid YAML list, empty string →{}, malformed → error, render through template. (IFC-2370) - T011 [US3] Integration test for filter chaining in
tests/unit/template/test_infrahub_filters.py— testartifact_content | from_jsonandartifact_content | from_yamlend-to-end with mocked ObjectStore returning JSON/YAML content. Verify template can access parsed fields. (IFC-2376 partial, SC-006)
Checkpoint: US3 complete. All 4 filters work, chain correctly, and are validated per context.
Purpose: Documentation, full regression, and server-side tasks
- T012 Run full unit test suite (
uv run pytest tests/unit/) and verify zero regressions (SC-005) - T013 Run
uv run invoke format lint-codeand fix any issues - T014 Documentation: artifact composition usage guide — create or update docs with Jinja2 filter examples, Python transform example using
client.object_store.get(identifier=storage_id), GraphQL query patterns, known limitations (no ordering guarantee). Runuv run invoke docs-generateanduv run invoke docs-validate. (IFC-2377) - T015 [Infrahub server] Thread SDK client into
Jinja2Templateinintegrator.py— passself.sdkfromrender_jinja2_templateasJinja2Template(client=...)on Prefect workers. Integration test with composite template. (IFC-2378) - T016 [Infrahub server] Schema validation: block new filters in computed attributes — validate with
context=ExecutionContext.COREat schema load time. Templates usingartifact_content/file_object_contentmust be rejected. (IFC-2379)
- Foundational (Phase 1): No dependencies — start immediately
- US1 + US4 (Phase 2): Depends on Phase 1 completion — BLOCKS remaining stories
- US2 (Phase 3): Depends on Phase 2 (uses InfrahubFilters + Jinja2Template wiring)
- US3 (Phase 4): Depends on Phase 1 only (from_json/from_yaml need JinjaFilterError). Can start in parallel with Phase 2 if desired.
- Polish (Phase 5): Depends on all user stories being complete
- US4 + US1 (P1): Can start after Phase 1 — No dependencies on other stories. This is the MVP.
- US2 (P2): Depends on US1 (reuses InfrahubFilters and Jinja2Template wiring from Phase 2)
- US3 (P3): Depends on Phase 1 only for
from_json/from_yaml. Chaining test (T011) depends on US1.
- Tasks marked [P] can run in parallel (different files)
- Tests are included within each implementation task
- Core implementation before wiring/registration
- T001 and T002 can run in parallel (different files: exceptions.py vs filters.py)
- T006 can run in parallel with T003/T004 (ObjectStore vs InfrahubFilters)
- T009 and T010 can run in parallel (from_json and from_yaml are independent)
- US3 Phase 4 (T009, T010) can start in parallel with Phase 2 after Phase 1 completes
# Launch both foundational tasks together (different files):
Task T001: "JinjaFilterError in infrahub_sdk/template/exceptions.py"
Task T002: "ExecutionContext + FilterDefinition in infrahub_sdk/template/filters.py"
# Launch both parsing filters together (same file but independent functions):
Task T009: "from_json filter in infrahub_sdk/template/infrahub_filters.py"
Task T010: "from_yaml filter in infrahub_sdk/template/infrahub_filters.py"
- Complete Phase 1: Foundational (T001, T002)
- Complete Phase 2: US1 + US4 (T003, T004, T005)
- STOP and VALIDATE: artifact_content renders, validation blocks in CORE, allows in WORKER
- This alone delivers the primary value proposition
- Phase 1 → Foundation ready
- Phase 2 → artifact_content + security gate → MVP deployed
- Phase 3 → file_object_content extends to file objects
- Phase 4 → from_json/from_yaml enable structured composition
- Phase 5 → Documentation + server integration
- Each phase adds value without breaking previous phases
| Task | Jira | Phase |
|---|---|---|
| T001 | IFC-2367 | 1 |
| T002 | IFC-2368 | 1 |
| T003 | IFC-2371 | 2 |
| T004 | IFC-2372 | 2 |
| T005 | IFC-2375 + IFC-2376 (partial) | 2 |
| T006 | IFC-2373 | 3 |
| T007 | IFC-2374 | 3 |
| T008 | IFC-2375 + IFC-2376 (partial) | 3 |
| T009 | IFC-2369 | 4 |
| T010 | IFC-2370 | 4 |
| T011 | IFC-2376 (partial) | 4 |
| T012-T013 | — | 5 |
| T014 | IFC-2377 | 5 |
| T015 | IFC-2378 | 5 |
| T016 | IFC-2379 | 5 |
- [P] tasks = different files, no dependencies
- [Story] label maps task to specific user story for traceability
- Tests are included in each implementation task (not separate)
- All error paths must produce actionable messages with filter name, cause, and remediation hint
- Commit after each task or logical group
- Stop at any checkpoint to validate story independently