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.

Informações da origem

Repositório
iii-hq/iii
Última atividade na origem
23 de setembro de 2026 às 15:45
Idioma detectado do SKILL.md
inglês
Estrelas
18.841
Forks
1.265

Opções de instalação

Por padrão, está selecionado o prompt que primeiro revisa a origem. Você pode mudar para um comando direto ou baixar uma cópia local.

Revise os arquivos de origem

Leia o SKILL.md e os arquivos complementares exibidos pelo SkillsMP antes de decidir se vai instalar.

Exibindo SKILL.md

SKILL.md
Instruções da origem · Visualização somente leitura
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.
Ver no GitHub