Skip to main content

iii-worker-manager

WebSocket listener that SDK workers connect to. Use to expose the engine to trusted (internal) and untrusted (RBAC-protected) workers, gate function and trigger registration, and audit or transform every invocation through a middleware function. Mounts channel WebSocket endpoints on every listener.

Informations de source

Dépôt
iii-hq/iii
Dernière activité de la source
23 septembre 2026 à 15:45
Langue détectée de SKILL.md
anglais
Étoiles
18 874
Forks
1 268

Options d'installation

Le prompt qui vérifie d'abord la source est sélectionné par défaut. Vous pouvez passer à une commande directe ou télécharger une copie locale.

Vérifiez les fichiers source

Lisez SKILL.md et les fichiers associés affichés par SkillsMP avant de décider de l'installer.

Affichage de SKILL.md

SKILL.md
Instructions source · Aperçu en lecture seule
name
iii-worker-manager
description
WebSocket listener that SDK workers connect to. Use to expose the engine to trusted (internal) and untrusted (RBAC-protected) workers, gate function and trigger registration, and audit or transform every invocation through a middleware function. Mounts channel WebSocket endpoints on every listener.
# iii-worker-manager The `iii-worker-manager` is a mandatory engine worker that opens WebSocket listeners. The base `engine.workers.iii-worker-manager` entry sets the main port (`49134` by default); `#instance` entries open separate listeners with their own RBAC, middleware, and registration hooks. Channels are mounted on every listener at `/ws/channels/{channel_id}`. ## When to Use - Exposing the engine to an untrusted network — RBAC is mandatory, never open the engine port itself. Use the standalone [`rbac-proxy` worker](https://workers.iii.dev/workers/rbac-proxy) (`iii trigger compose::add worker=rbac-proxy`), which applies these RBAC rules on its own port out of process. A second `#instance` listener with an `rbac` block and an `auth_function_id` is the in-engine alternative. - Restricting which functions a connected worker can invoke — combine `expose_functions` filters (operator-side) with `forbidden_functions` from `AuthResult` (per-session, hard deny). - Auditing, rate-limiting, or enriching every invocation — set `middleware_function_id` on the listener; the middleware decides whether to call the target and what to return. - Per-tenant or per-session namespace isolation — return `function_registration_prefix` from the auth function so every function/trigger this session registers is transparently prefixed without the worker code knowing. - Gating dynamic registration — wire `on_function_registration_function_id`, `on_trigger_registration_function_id`, or `on_trigger_type_registration_function_id` to validate or rewrite registrations. ## Boundaries - The infrastructure carve-out (`engine::channels::create`, `engine::workers::register`, `engine::log::*`, `engine::baggage::*`) is always allowed on RBAC listeners regardless of `expose_functions`. Adding one of those IDs to `forbidden_functions` denies it but logs a warning — workers may behave unpredictably (broken setup, lost logs, missing context). - The engine port must remain internal. Only `rbac-proxy` or an RBAC-protected `#instance` listener may face an external network. - The middleware is **not** a pre-handler — it must invoke the target function itself (typically via `iii.trigger`) and return its result. Returning early without invoking simply skips the call. - `forbidden_functions` from the auth result wins over both `allowed_functions` and `expose_functions`. There is no way for a session to override an operator's deny list. - Registration hooks return mapped fields or **throw** to deny. Omitted result fields keep their original values; returning `{}` is a no-op (allow as-is). - Triggers and functions registered through an RBAC session are scoped to that session and cleaned up automatically on disconnect. ## Configuration Two listeners — one internal, one external with in-engine RBAC (the `rbac-proxy` worker accepts the same `rbac` block through the `configuration` worker instead): ```yaml engine: workers: iii-worker-manager: port: 49134 "iii-worker-manager#rbac": host: 0.0.0.0 port: 49135 middleware_function_id: my-project::middleware rbac: auth_function_id: my-project::auth on_function_registration_function_id: my-project::on-fn-reg on_trigger_registration_function_id: my-project::on-trig-reg on_trigger_type_registration_function_id: my-project::on-trig-type-reg expose_functions: - match("api::*") - match("*::public") - metadata: public: true ``` ## Auth function Receives `AuthInput { headers, query_params, ip_address }`, returns `AuthResult`. Throw to reject the connection. ```typescript import type { AuthInput, AuthResult } from '@iii-dev/helpers/worker-connection-manager' iii.registerFunction( 'my-project::auth', async (input: AuthInput): Promise<AuthResult> => { const token = input.headers?.['authorization']?.replace(/^Bearer\s+/i, '') if (!token) throw new Error('Missing credentials') const user = await validateToken(token) return { allowed_functions: [], forbidden_functions: user.role === 'readonly' ? ['api::users::delete', 'api::users::update'] : [], allowed_trigger_types: user.role === 'admin' ? ['cron', 'webhook'] : undefined, allow_trigger_type_registration: user.role === 'admin', function_registration_prefix: `tenant-${user.tenant_id}`, context: { user_id: user.id, role: user.role, tenant_id: user.tenant_id }, } }, ) ``` `AuthResult` defaults: `allow_function_registration: true`, `allow_trigger_type_registration: false`, `allowed_trigger_types: undefined` (permissive — every type allowed). Setting `function_registration_prefix` makes the engine transparently rewrite IDs in both directions; the worker SDK never sees the prefix. ## Middleware function Runs on every invocation through this listener. Receives `MiddlewareFunctionInput { function_id, payload, action, context }`. Must call the target function itself. ```typescript import type { MiddlewareFunctionInput } from 'iii-sdk' iii.registerFunction( 'my-project::middleware', async (input: MiddlewareFunctionInput) => { console.log(`[audit] user=${input.context.user_id} → ${input.function_id}`) return iii.trigger({ function_id: input.function_id, payload: { ...input.payload, _caller_id: input.context.user_id, _caller_role: input.context.role, }, }) }, ) ``` Skipping the `iii.trigger` call short-circuits the request — useful for rate-limit rejections (return a structured error envelope instead of invoking the target). ## Registration hooks Each hook receives the registration details plus `AuthResult.context`. Return mapped fields or throw to deny. Omitted fields keep their original value. ```typescript import type { OnFunctionRegistrationInput, OnTriggerRegistrationInput, OnTriggerTypeRegistrationInput, } from '@iii-dev/helpers/worker-connection-manager' iii.registerFunction( 'my-project::on-fn-reg', async (input: OnFunctionRegistrationInput) => { if (input.function_id.startsWith('internal::')) { throw new Error('Cannot register internal functions') } return { function_id: input.function_id } }, ) iii.registerFunction( 'my-project::on-trig-reg', async (input: OnTriggerRegistrationInput) => { const role = input.context.role as string if (!input.function_id.startsWith(`${role}::`)) { throw new Error('Function ID must be prefixed with the role') } return { function_id: `${role}::${input.function_id}` } }, ) iii.registerFunction( 'my-project::on-trig-type-reg', async (input: OnTriggerTypeRegistrationInput) => { if (input.context.role !== 'admin') { throw new Error('Only admins can register trigger types') } return {} }, ) ``` A worker can register a trigger type only when `allow_trigger_type_registration: true` AND (if configured) the hook returns a result. A worker can register a trigger only when its `trigger_type` is in `allowed_trigger_types` (or the field is omitted) AND the hook returns a result. ## Function filters `expose_functions` accepts wildcard and metadata filters; multiple filters are OR'd. Metadata filters AND across keys. ```yaml expose_functions: - match("api::*") # ID glob: * matches any chars - match("*::public") # suffix match - match("api::*::read") # multi-segment match - metadata: # all keys must match public: true tier: free - metadata: name: match("*public*") # metadata values can be wildcards too ``` A function is exposed if **any** filter matches. Empty `expose_functions` = no functions exposed (other than the infrastructure carve-out). ## Channels on RBAC ports Channels are mounted at `/ws/channels/{channel_id}` on the same port the worker connected through. `engine::channels::create` is always allowed (infrastructure carve-out), so RBAC listeners can hand out channel refs even with empty `expose_functions`. SDK `createChannel()` works unmodified — channel data flows through whichever listener the worker is on, never the main engine port. Access to a channel WebSocket is independently validated by the `access_key` capability token in the `StreamChannelRef`. ## Access decision flow ```mermaid flowchart TD Start["invokeFunction"] --> Forbidden{"in forbidden_functions?"} Forbidden -- yes --> Deny["DENY"] Forbidden -- no --> Allowed{"in allowed_functions?"} Allowed -- yes --> Allow["ALLOW"] Allowed -- no --> Infra{"infrastructure ID?"} Infra -- yes --> Allow Infra -- no --> Expose{"any expose_functions filter matches?"} Expose -- yes --> Allow Expose -- no --> Deny ``` Rule of thumb: deny by default (rule 5). Operators allow via `expose_functions`; auth functions augment with `allowed_functions` and tighten with `forbidden_functions`. The infrastructure carve-out is the only path that ignores `expose_functions` entirely. ## Security notes - Always set `auth_function_id` on listeners that face untrusted networks. A listener with no `rbac` block authenticates nothing — every connection is allowed and `expose_functions` alone gates access. - Prefer narrow `expose_functions` over `match("*")`. Audit the list whenever a new namespace lands in the engine. - The middleware is the right place for request validation, rate limiting, and audit logging. Keep it idempotent — invocations may be retried. - Use `function_registration_prefix` for multi-tenant isolation rather than asking each tenant to register pre-prefixed function IDs. The prefix is invisible to the worker SDK.
Voir sur GitHub