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.

Quellinformationen

Repository
iii-hq/iii
Letzte Quellaktivität
23. September 2026 um 15:45
Erkannte Sprache von SKILL.md
Englisch
Sterne
18.841
Forks
1.265

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
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.
Auf GitHub ansehen