The Workglow entitlement system provides a declarative, capability-based security model for task pipelines. Every task declares the permissions it requires -- network access, filesystem operations, code execution, credential usage, AI model inference, and more -- and an entitlement enforcer decides at runtime whether those permissions are granted. This design enables safe execution of untrusted or user-constructed pipelines: a browser environment can deny filesystem access, a sandboxed server can restrict network calls to specific domains, and a desktop application can grant broad permissions.
The system is built on four principles:
- Hierarchical identifiers. Entitlement IDs use colon-separated namespacing
(
"network","network:http","network:websocket"). Granting a parent implicitly covers all children. - Resource scoping. Grants can be narrowed to specific resources using glob
patterns (e.g.,
"/tmp/*"for filesystem reads,"claude-*"for AI models). - Static and dynamic declaration. Tasks declare entitlements both as static class methods (for pre-execution analysis) and as instance methods (for runtime-dependent permissions).
- Graph-level aggregation. A
TaskGraphorWorkflowcan compute the union of all entitlements required by its tasks, enabling upfront approval before execution begins.
The entitlement types are defined in @workglow/task-graph in the
TaskEntitlements.ts module, with enforcement logic in EntitlementEnforcer.ts
and pre-built profiles in EntitlementProfiles.ts.
Entitlement identifiers are plain strings that use colons as namespace separators. The hierarchy is implicit in the string structure:
network
network:http
network:websocket
network:private
Granting "network" implicitly covers "network:http", "network:websocket",
and "network:private". This is implemented by the entitlementCovers()
function:
function entitlementCovers(granted: EntitlementId, required: EntitlementId): boolean {
return required === granted || required.startsWith(granted + ":");
}This means a grant of "network" matches a requirement of "network:http"
because "network:http".startsWith("network:") is true. But a grant of
"network:http" does not cover a requirement of "network" -- children
cannot satisfy parent requirements.
The Entitlements object defines the standard entitlement constants. Tasks may
also use custom IDs beyond these.
| Constant | ID | Description |
|---|---|---|
NETWORK |
"network" |
All network access |
NETWORK_HTTP |
"network:http" |
HTTP/HTTPS requests |
NETWORK_WEBSOCKET |
"network:websocket" |
WebSocket connections |
NETWORK_PRIVATE |
"network:private" |
Access to private/internal network addresses |
| Constant | ID | Description |
|---|---|---|
FILESYSTEM |
"filesystem" |
All filesystem access |
FILESYSTEM_READ |
"filesystem:read" |
Read-only access |
FILESYSTEM_WRITE |
"filesystem:write" |
Write access |
| Constant | ID | Description |
|---|---|---|
CODE_EXECUTION |
"code-execution" |
All code execution |
CODE_EXECUTION_JS |
"code-execution:javascript" |
JavaScript code execution |
| Constant | ID | Description |
|---|---|---|
CREDENTIAL |
"credential" |
Access to the credential store |
| Constant | ID | Description |
|---|---|---|
AI_MODEL |
"ai:model" |
Use of specific AI models |
AI_INFERENCE |
"ai:inference" |
Running AI model inference |
| Constant | ID | Description |
|---|---|---|
MCP |
"mcp" |
All MCP operations |
MCP_TOOL_CALL |
"mcp:tool-call" |
Calling MCP tools |
MCP_RESOURCE_READ |
"mcp:resource-read" |
Reading MCP resources |
MCP_PROMPT_GET |
"mcp:prompt-get" |
Getting MCP prompts |
MCP_STDIO |
"mcp:stdio" |
MCP via stdio transport |
| Constant | ID | Description |
|---|---|---|
STORAGE |
"storage" |
All storage operations |
STORAGE_READ |
"storage:read" |
Reading from storage |
STORAGE_WRITE |
"storage:write" |
Writing to storage |
A single entitlement declaration is represented by the TaskEntitlement
interface:
interface TaskEntitlement {
readonly id: EntitlementId;
readonly reason?: string;
readonly optional?: boolean;
readonly resources?: readonly string[];
}| Field | Type | Description |
|---|---|---|
id |
string |
Hierarchical identifier (e.g., "network:http") |
reason |
string | undefined |
Human-readable explanation of why the entitlement is needed |
optional |
boolean | undefined |
If true, the task can degrade gracefully without this permission |
resources |
string[] | undefined |
Specific resources this entitlement applies to (URL patterns, model IDs, server names). When undefined, the entitlement applies broadly. |
Multiple entitlements are grouped in the TaskEntitlements container:
interface TaskEntitlements {
readonly entitlements: readonly TaskEntitlement[];
}A shared EMPTY_ENTITLEMENTS singleton (frozen object with an empty array) is
used to avoid allocations for tasks that require no entitlements.
For graph-level analysis, TrackedTaskEntitlement extends TaskEntitlement with
origin tracking:
interface TrackedTaskEntitlement extends TaskEntitlement {
readonly sourceTaskIds: readonly unknown[];
}This allows UIs and policy engines to show which tasks in a graph require each entitlement, enabling targeted approval or task removal.
Entitlement grants support resource-level scoping using glob patterns with any
number of * wildcards. Each * matches zero or more characters of any kind,
including path separators like /. The resourcePatternMatches() function
implements pattern matching:
function resourcePatternMatches(grantPattern: string, requiredResource: string): boolean;Matching rules:
- Without
*: exact string match only. "prefix*"matches anything starting with"prefix"."*.example.com"matches anything ending with".example.com"."pre*suf"matches strings with the given prefix and suffix, with any content in between."a*b*c"matches strings containing"a", then"b", then"c"in order."https://localhost:*/*"matches any URL on localhost with a path segment.
Examples:
| Grant Pattern | Required Resource | Match? |
|---|---|---|
"/tmp/*" |
"/tmp/data.json" |
Yes |
"/tmp/*" |
"/tmp/sub/file.txt" |
Yes |
"claude-*" |
"claude-3-opus" |
Yes |
"claude-*" |
"gpt-4o" |
No |
"*.example.com" |
"api.example.com" |
Yes |
"gpt-4o" |
"gpt-4o" |
Yes |
"gpt-4o" |
"gpt-4o-mini" |
No |
"https://localhost:*/*" |
"https://localhost:3000/foo" |
Yes |
"https://localhost:*/*" |
"https://localhost:3000" |
No |
"a*b*c" |
"aXXbYYc" |
Yes |
The grantCoversResources() function checks whether a grant satisfies the
resource requirements of an entitlement:
function grantCoversResources(grant: EntitlementGrant, required: TaskEntitlement): boolean;The matching rules are:
- Broad grant (no
resourceson the grant): covers any requirement. - Broad requirement (no
resourceson the entitlement): only a broad grant covers it. A scoped grant cannot satisfy a broad need. - Both have resources: every required resource must match at least one grant pattern.
Tasks declare their base entitlements by overriding the static entitlements()
method on their class:
class FetchUrlTask extends Task<FetchInput, FetchOutput> {
static readonly type = "FetchUrlTask";
public static override entitlements(): TaskEntitlements {
return {
entitlements: [
{ id: Entitlements.NETWORK_HTTP, reason: "Fetches data from URLs via HTTP/HTTPS" },
{
id: Entitlements.CREDENTIAL,
reason: "May use Bearer token authentication",
optional: true,
},
],
};
}
}Static entitlements are available without instantiating the task. They are used for pre-execution analysis, UI display, and graph-level policy checks.
When a task's required permissions depend on its runtime configuration, it
overrides the instance entitlements() method and sets the static
hasDynamicEntitlements flag:
class AiTask extends Task<AiInput, AiOutput> {
public static override hasDynamicEntitlements: boolean = true;
public static override entitlements(): TaskEntitlements {
return {
entitlements: [{ id: Entitlements.AI_INFERENCE, reason: "Runs AI model inference" }],
};
}
public override entitlements(): TaskEntitlements {
const base: TaskEntitlement[] = [
{ id: Entitlements.AI_INFERENCE, reason: "Runs AI model inference" },
];
const modelId = typeof this.defaults.model === "string" ? this.defaults.model : undefined;
if (modelId) {
base.push({
id: Entitlements.AI_MODEL,
reason: `Uses model ${modelId}`,
resources: [modelId],
});
}
return { entitlements: base };
}
}The hasDynamicEntitlements flag signals to the framework that static analysis
alone is insufficient and instance-level entitlements should be checked.
Tasks can notify listeners of entitlement changes by calling
emitEntitlementChange():
protected emitEntitlementChange(entitlements?: TaskEntitlements): void {
const final = entitlements ?? this.entitlements();
this.emit("entitlementChange", final);
}JavaScriptTask -- requires code execution:
public static override entitlements(): TaskEntitlements {
return {
entitlements: [
{
id: Entitlements.CODE_EXECUTION_JS,
reason: "Executes user-provided JavaScript code in a sandboxed interpreter",
},
],
};
}McpToolCallTask -- static entitlements plus dynamic server scoping:
// Static: base MCP permissions
public static override entitlements(): TaskEntitlements {
return {
entitlements: [
{ id: Entitlements.MCP_TOOL_CALL, reason: "Calls MCP tools" },
],
};
}
// Instance: adds the specific server name as a resource
public override entitlements(): TaskEntitlements {
const base = McpToolCallTask.entitlements();
if (this.defaults.serverName) {
return {
entitlements: [
...base.entitlements,
{ id: Entitlements.MCP, resources: [this.defaults.serverName] },
],
};
}
return base;
}The IEntitlementEnforcer interface defines the contract for checking whether
required entitlements are granted:
interface IEntitlementEnforcer {
check(required: TaskEntitlements): readonly TaskEntitlement[];
}The check() method returns an array of denied (non-optional) entitlements.
An empty array means all entitlements are granted.
Permissive enforcer -- grants everything, suitable for trusted environments:
const PERMISSIVE_ENFORCER: IEntitlementEnforcer = { check: () => [] };Grant-list enforcer -- checks against a list of entitlement ID strings (broad grants only):
const enforcer = createGrantListEnforcer(["network", "ai", "storage"]);Scoped enforcer -- supports resource-level matching with glob patterns:
const enforcer = createScopedEnforcer([
{ id: "network:http" },
{ id: "filesystem:read", resources: ["/tmp/*"] },
{ id: "ai:model", resources: ["claude-*", "gpt-4o"] },
{ id: "code-execution" },
]);The scoped enforcer iterates each required entitlement, finds grants whose IDs
cover it (using entitlementCovers() for hierarchy), and then verifies resource
coverage (using grantCoversResources()). Optional entitlements are never
denied.
Pre-built profiles provide grant sets for common runtime environments:
type EntitlementProfile = "browser" | "desktop" | "server";Browser profile -- no filesystem, no code execution, no stdio MCP:
const BROWSER_GRANTS: readonly EntitlementGrant[] = [
{ id: "network" },
{ id: "ai" },
{ id: "mcp:tool-call" },
{ id: "mcp:resource-read" },
{ id: "mcp:prompt-get" },
{ id: "storage" },
{ id: "credential" },
];Desktop profile -- adds filesystem, code execution, and stdio MCP:
const DESKTOP_GRANTS: readonly EntitlementGrant[] = [
...BROWSER_GRANTS,
{ id: "filesystem" },
{ id: "code-execution" },
{ id: "mcp:stdio" },
];Server profile -- same as desktop (can be further scoped):
const SERVER_GRANTS: readonly EntitlementGrant[] = [...DESKTOP_GRANTS];Create an enforcer for a profile with:
const enforcer = createProfileEnforcer("browser");The enforcer is registered in the ServiceRegistry under the
ENTITLEMENT_ENFORCER service token:
import { globalServiceRegistry } from "@workglow/util";
import { ENTITLEMENT_ENFORCER, createProfileEnforcer } from "@workglow/task-graph";
globalServiceRegistry.registerInstance(ENTITLEMENT_ENFORCER, createProfileEnforcer("browser"));The computeGraphEntitlements() function aggregates entitlements across all
tasks in a TaskGraph:
function computeGraphEntitlements(
graph: TaskGraph,
options?: GraphEntitlementOptions
): TaskEntitlements;Options:
| Option | Type | Default | Description |
|---|---|---|---|
trackOrigins |
boolean |
false |
Annotate each entitlement with source task IDs |
conditionalBranches |
"all" | "active" |
"all" |
Which conditional branches to include |
When conditionalBranches is "all" (the default), entitlements from every
branch of a ConditionalTask are included -- this is conservative and suitable
for pre-execution approval. When set to "active", only entitlements from
non-disabled branches are included, which is useful for runtime checks after
conditions have been evaluated.
// Pre-execution: analyze all possible entitlements
const allEntitlements = computeGraphEntitlements(graph, { trackOrigins: true });
for (const e of allEntitlements.entitlements) {
console.log(`${e.id} required by tasks: ${e.sourceTaskIds.join(", ")}`);
}
// Check against enforcer
const denied = enforcer.check(allEntitlements);
if (denied.length > 0) {
throw new Error(`Denied entitlements: ${denied.map((e) => e.id).join(", ")}`);
}The mergeEntitlements() function combines two TaskEntitlements objects into
their union:
function mergeEntitlements(a: TaskEntitlements, b: TaskEntitlements): TaskEntitlements;Merge semantics for entitlements with the same ID:
optional:falsewins (most restrictive). If either side says the entitlement is mandatory, the merged result is mandatory.reason: first non-empty reason wins.resources: union of all resource arrays.
When a task is serialized via toJSON(), its entitlements are included in the
output if non-empty:
{
"id": "task-1",
"type": "FetchUrlTask",
"defaults": { "url": "https://example.com" },
"entitlements": {
"entitlements": [
{ "id": "network:http", "reason": "Fetches data from URLs via HTTP/HTTPS" },
{ "id": "credential", "reason": "May use Bearer token authentication", "optional": true }
]
}
}This enables offline policy analysis and UI display of required permissions without needing to instantiate task classes.
| Type | Description |
|---|---|
EntitlementId |
string -- hierarchical entitlement identifier |
TaskEntitlement |
Single entitlement declaration with id, reason, optional, resources |
TaskEntitlements |
Container with entitlements: readonly TaskEntitlement[] |
TrackedTaskEntitlement |
TaskEntitlement plus sourceTaskIds for origin tracking |
TrackedTaskEntitlements |
Container with entitlements: readonly TrackedTaskEntitlement[] |
EntitlementGrant |
Grant declaration with id and optional resources (glob patterns) |
EntitlementProfile |
"browser" | "desktop" | "server" |
IEntitlementEnforcer |
Interface with check(required): readonly TaskEntitlement[] |
| Function | Signature | Description |
|---|---|---|
entitlementCovers |
(granted: string, required: string) => boolean |
Check if a granted ID covers a required ID in the hierarchy |
resourcePatternMatches |
(grantPattern: string, requiredResource: string) => boolean |
Check if a glob pattern matches a resource string |
grantCoversResources |
(grant: EntitlementGrant, required: TaskEntitlement) => boolean |
Check if a grant covers the resource requirements of an entitlement |
mergeEntitlements |
(a: TaskEntitlements, b: TaskEntitlements) => TaskEntitlements |
Merge two entitlement sets into their union |
createGrantListEnforcer |
(grants: readonly string[]) => IEntitlementEnforcer |
Create an enforcer from a list of broad grant IDs |
createScopedEnforcer |
(grants: readonly EntitlementGrant[]) => IEntitlementEnforcer |
Create an enforcer with resource-level scoping |
createProfileEnforcer |
(profile: EntitlementProfile) => IEntitlementEnforcer |
Create an enforcer for a standard runtime profile |
getProfileGrants |
(profile: EntitlementProfile) => readonly EntitlementGrant[] |
Get the grant list for a profile |
computeGraphEntitlements |
(graph: TaskGraph, options?) => TaskEntitlements |
Aggregate entitlements across all tasks in a graph |
| Constant | Description |
|---|---|
Entitlements |
Object containing all well-known entitlement ID constants |
EMPTY_ENTITLEMENTS |
Frozen singleton with an empty entitlements array |
PERMISSIVE_ENFORCER |
Enforcer that grants everything |
ENTITLEMENT_ENFORCER |
Service token for registering a custom enforcer |
BROWSER_GRANTS |
Grant array for browser environments |
DESKTOP_GRANTS |
Grant array for desktop environments |
SERVER_GRANTS |
Grant array for server environments |