Skip to main content

Telo Evaluation Context Model (v1.0 Draft)

1. Overview​

The Telo engine relies on the Common Expression Language (CEL) to evaluate dynamic properties within declarative YAML manifests. To evaluate these expressions safely and deterministically, the kernel must provide a strictly defined Evaluation Context.

This context is conceptually divided into two distinct layers: the Module Context (initialized at boot) and the Execution Context (created per trigger/request).

2. Core Principles​

  • Immutability: The Module Context MUST be strictly read-only after the module initialization phase. The Execution Context is immutable for the duration of a single execution.
  • Namespace Isolation: Top-level keys (namespaces) are strictly defined and reserved. A user cannot arbitrarily inject new top-level keys into the context.
  • Security by Design: Sensitive data is explicitly isolated in the secrets namespace, allowing the kernel to automatically sanitize logs and error traces.
  • Performance: The kernel SHOULD NOT perform deep copies of the Module Context per execution. It should use shallow merging, references, or language-specific proxy patterns to combine the contexts efficiently.

3. The Context Layers​

3.1. Module Context​

The Module Context represents the state of the module after it has been loaded, configured, and initialized. It is shared across all incoming executions.

  • variables (Dictionary/Map): Non-sensitive configuration values provided to the module (e.g., timeouts, public URLs, environment flags).
  • secrets (Dictionary/Map): Sensitive strings or credentials (e.g., API keys, database passwords). Constraint: Any value accessed from this namespace MUST be redacted (e.g., replaced with [REDACTED]) in all standard output, logs, and error messages generated by the kernel.
  • resources (Dictionary/Map): References to instantiated resource objects in the module scope. Includes both locally-defined resources (e.g., an active database connection, an HTTP server instance) and imported modules (keyed by the user-defined alias, mapping to the exported properties of that module instance).
    • resources.<name>.status is the resource's observed state — what it learned while running, declared by its kind's status: block and readable only from fields that resolve while the application runs. status is a reserved key in every published value. See Observed state.

3.2. Execution Context (Runtime Scope)​

The Execution Context is highly ephemeral. It is constructed instantly when an event triggers the engine (e.g., an incoming HTTP request, a cron job, a message queue event) and is destroyed upon completion.

  • request (Dictionary/Map): The payload and metadata of the trigger event. The schema of this object depends on the trigger type (e.g., an HTTP trigger will populate method, path, headers, query, and body).
  • inputs (Dictionary/Map): Local arguments passed directly into a specific handler or action during the execution flow.

4. Context Examples​

Example A: Module Context (At Boot Time)​

When the Telo kernel starts and initializes a module, it builds this read-only structure in memory.

{
"variables": {
"publicApiUrl": "https://api.telo.run/v1",
"maxRetries": 3,
"environment": "production"
},
"secrets": {
"stripeApiKey": "sk_live_123abc456def...",
"databaseUrl": "postgres://user:pass@db:5432/main"
},
"resources": {
"CacheDb": {
"poolSize": 10,
"status": {
"endpoint": "redis://10.0.0.4:6379",
"pingMs": 14
}
},
"AuthModule": {
"sessionGatewayEndpoint": "http://internal-auth:8080"
}
}
}

Example B: Merged Evaluation Context (At Execution Time)​

When an event occurs (e.g., a POST request to a route), the kernel injects the Execution Context into the Module Context. This unified object is what the CEL evaluator receives to resolve expressions like !cel "secrets.stripeApiKey" or !cel "request.body.userId".

{
"variables": {
"publicApiUrl": "https://api.telo.run/v1",
"maxRetries": 3,
"environment": "production"
},
"secrets": {
"stripeApiKey": "sk_live_123abc456def...",
"databaseUrl": "postgres://user:pass@db:5432/main"
},
"resources": {
"CacheDb": {
"poolSize": 10,
"status": {
"endpoint": "redis://10.0.0.4:6379",
"pingMs": 14
}
},
"AuthModule": {
"sessionGatewayEndpoint": "http://internal-auth:8080"
}
},

"request": {
"method": "POST",
"path": "/users",
"headers": {
"content-type": "application/json",
"user-agent": "curl/7.68.0"
},
"query": {},
"body": {
"userId": "998877",
"action": "upgrade_tier"
}
},
"inputs": {
"targetResource": "billing"
}
}

5. Resource Instantiation Architecture​

5.1 Overview​

Every EvaluationContext owns its full resource lifecycle. This is achieved by injecting an InstanceFactory function into the context at construction time, rather than passing it per-call to methods like initializeResources().

5.2 InstanceFactory Injection​

The InstanceFactory type is a simple async factory function:

type InstanceFactory = (resource: ResourceManifest) => Promise<ResourceInstance | null>;

When the Kernel constructs a ModuleContext, it provides its internal _createInstance method:

// In Kernel constructor
this.moduleContextRegistry = new ModuleContextRegistry(this._createInstance.bind(this));

// In ModuleContextRegistry.declareModule()
this.store.set(moduleName, new ModuleContext({}, {}, {}, this.createInstance));

The _createInstance method handles:

  • Resolving alias-prefixed kinds (e.g., MyImport.Http.Route → Http.Route)
  • Looking up the appropriate controller
  • Validating the resource against the controller's schema
  • Creating and initializing the resource instance
  • Storing the instance in both the Kernel registry and the module context
  • Invoking post-initialization hooks (e.g., isContextProvider)

5.3 Context Hierarchy​

The context hierarchy follows a simple pattern:

EvaluationContext(context, createInstance, secretValues)
├── ModuleContext(variables, secrets, resources, createInstance)
│ ├── pendingResources: ResourceManifest[]
│ └── resourceInstances: Map<key, {resource, instance}>
│
└── ExecutionContext(moduleCtx, execProps)
├── inherits createInstance from moduleCtx
└── merges moduleCtx.context with execProps (e.g., {request, inputs})

Key properties:

  • ModuleContext is stateful and mutable — it accumulates resources during multi-pass initialization as controllers register new kinds.
  • ExecutionContext is ephemeral and read-only — it merges the module context with per-execution properties and forwards the module's createInstance factory.
  • EvaluationContext can spawn child contexts via spawnChild(), forming a lifecycle tree. All children inherit the parent's createInstance factory.

5.4 Multi-Pass Initialization​

When initializeResources() is called on any context, it walks the pending resources in creation order and initializes each one as soon as it is created, so everything a resource depends on is live before its own create() runs.

The creation order comes from the analyzer's dependency graph (prepare() → setInitOrder): every reference edge, plus the declarations a resource's kind is spelled through — the Telo.Import that registers Http comes before kind: Http.Api, the Telo.Definition that declares Page before kind: Self.Page, and a definition's extends: target before the definition. Those declaration edges only order: when one conflicts with a reference edge (an import handed an instance of a kind it exports itself), the reference wins and no cycle is reported.

Why keep multiple passes? A dependency the graph cannot see — a by-name ctx.resolveRef, a resource registered while another initializes — still defers (ERR_LOCAL_REF_PENDING / ERR_CROSS_MODULE_REF_PENDING) and is retried on the next pass, up to ten. On a manifest whose dependencies are all declared, every resource is created once and initialized once; a retry means an edge the order did not have.

5.5 Child Context Initialization​

The ResourceContext interface (implemented by ResourceContextImpl) provides methods for controllers that spawn child contexts:

/**
* Create a child EvaluationContext attached to the current module context.
* The child inherits the module's createInstance factory.
*/
spawnChildContext(): EvaluationContext {
const child = new EvaluationContext(
this.moduleContext.context,
this.moduleContext.createInstance, // ← inherited
this.moduleContext.secretValues,
);
return this.moduleContext.spawnChild(child);
}

/**
* Initialize pending resources on a child context.
* Uses the inherited createInstance factory.
*/
async initializeChildContext(ctx: EvaluationContext): Promise<void> {
await this.kernel.initializeContext(ctx); // → ctx.initializeResources()
}

5.6 Design Benefits​

  1. Eliminates ResourceInstantiator from the public API — the kernel's factory is injected once, not passed per-call.
  2. Simplifies Kernel internals — the initializationQueue and private 177-line init loop are replaced by delegating to ctx.initializeResources().
  3. Uniform resource ownership — every context type can initialize resources the same way.
  4. Testability — contexts can be constructed with mock InstanceFactory implementations for unit testing.