/** * Template validation and instantiation for orchestration. * * @module @nhtio/adk/batteries/orchestration/templates * * @remarks * A template is a consumer-defined plan *shape* — written in TypeScript, registered with * `createOrchestration` at construction, and therefore versioned with the consuming application. * It needs no store seeding and can be **validated once at boot** rather than once per * instantiation, so a misconfigured deployment fails at startup with a named issue rather than at * the first use months later. * * Why templates exist at all is the small-model story: a small model working on a forty-node plan * does far better filling in five declared parameters than authoring forty nodes. The template is * the static part of that bargain — the consumer writes the shape, the model fills the holes. * * A template is **not** a plan. It has no lifecycle, no digest, no run, and cannot be approved. * Only its instantiations are plans, which is what keeps "one plan id, at most one run" intact * when the same template is instantiated fifty times: each instantiation is an independent plan * with its own id and digest. * * The two exported functions are the whole surface: * * - {@link validateTemplate} — runs at **construction**. Every issue it returns is decidable and * total *because* a registered template is immutable: the graph cannot change after the check, * so the answer cannot go stale. The most important check is the laundering rule (below). * - {@link instantiateTemplate} — validates the arguments a model offers against the declared * `params`, mints a fresh plan, substitutes every hole, and appends the graph as authored ops. * * ## The laundering rule * * A `ParamRef` reaching a `call` node's `args` is refused unless a node on **every route** to that * call declares the corresponding field in `declassifies`. This is the taint story made static: * a substituted parameter value is like entry input — untrusted — and it may reach a `reason` * prompt but not a `call` node's args unless a node on every path to the call has declassified it. * * The check lives over the **template, not over an instantiation**, and that is the whole reason * it can be total: it runs at construction, once, over a graph that is immutable from that moment. * * **The narrower invariant, stated honestly:** *a template cannot launder its own parameters.* It * does **not** claim that a substituted value's template origin is tracked through arbitrary later * edits — nothing in a freely-mutable graph can track that. Once instantiated, the result is an * ordinary `editable` plan and a substituted value is an ordinary literal; a later `set_node_field` * routing that literal into a `call` arg is exactly as visible as in any hand-authored plan, which * is to say: it is in the operator's rendered prose, and the operator approves it. That is the * honest boundary, and it is the same one every hand-authored plan already has. */ import type { PlanStore } from "./store"; import type { EncodableValue, InstantiateResult, InvocableTools, PlanIssue, PlanTemplate } from "./types"; /** * Validate a template against the invocable allowlist, once, at construction. * * @remarks * Every issue returned here is a blocking refusal: a deployment whose template fails this check * should fail to boot, not fail at the first instantiation months later. This is the whole point * of validating over the immutable template rather than over each (mutable) instantiation. * * The checks: * * 1. **Undeclared holes.** A `ParamRef` whose `path` does not name a declared `params` entry is * refused — a template cannot substitute a parameter it never declared. * 2. **Unknown tools.** A `call` node naming a tool absent from `invocable.has(tool)` is refused, * and the message names what *is* available so the author can fix it. * 3. **The laundering check.** A `ParamRef` reaching a `call` node's `args` is refused unless a * node on every route to that call declares the corresponding field in `declassifies`. See the * module TSDoc for the honest, narrower invariant this enforces — *a template cannot launder * its own parameters* — and why nothing more is claimed. * * The route enumeration is capped at {@link MAX_ROUTES} paths. A template with more distinct * simple paths than that cannot prove that *every* route declassifies, and is conservatively * refused rather than trusted on a partial count. * * @param tpl - The template to validate. * @param invocable - The tier-C allowlist a staged `call` may invoke. * @returns Every blocking issue the template raises; an empty array means it is safe to register. */ export declare function validateTemplate(tpl: PlanTemplate, invocable: InvocableTools): PlanIssue[]; /** * Instantiate a template into a fresh, ordinary `editable` plan. * * @remarks * A template holds **op inputs without identity**: a `PlanOp` requires `opId`/`actorId`/`lamport`/ * `at`, and no static literal can carry those — the same reason bounds are a fold seed rather than * an implied op. So instantiation **mints** that identity here, under the passed `actorId`, with a * monotonic lamport. * * The steps, in order: * * 1. **Validate `args` against `params`** — types (plus `maxBytes` on strings) and enum * membership. On failure it returns `{ok: false, reason: 'invalid_args', detail}` naming the * offending param and what was expected, rather than minting a broken plan. * 2. **`store.createPlan(planId, {provenance: {kind: 'template', template: tpl.id, args}})`** — the * provenance is persisted by the store and returned by `readProvenance`, for the renderer and * for audit. It is **not** a taint mechanism (see the module TSDoc). * 3. **Substitute every `ParamRef`** with the corresponding argument value, then append * `add_node` / `add_edge` / `set_bounds` ops. * * The result is an **ordinary `editable` plan**: no inherited approval, no special state, nothing * downstream needs to know it came from a template. Two instantiations of one template yield * independent plans with different ids and digests. * * `planId` is minted (not a parameter), because a fresh plan needs a fresh id — a caller does not * pre-choose one. If the mint races a duplicate, an error is thrown rather than returning a broken * or mislabelled result. * * @param store - The plan store to write into. * @param tpl - The registered template to materialise. * @param args - The concrete values, keyed by declared param `path`, to substitute for holes. * @param actorId - The identity under which the minted ops are authored. * @returns The instantiation result. */ export declare function instantiateTemplate(store: PlanStore, tpl: PlanTemplate, args: Record, actorId: string): Promise;