Contexts & Authorization
AGBoss strictly separates domain state (DTOs carried in the Path Scope and persisted in checkpoints) from runtime identity and metadata (injected via three hierarchical context classes). Exactly three context tokens exist — ApplicationContext, TenantContext, and ExecutionContext — forming a parent-linked chain inspired by Spring’s context hierarchy.
How the three context levels are structured
Section titled “How the three context levels are structured”Each level links upward to its parent, so a component injecting ExecutionContext can reach executionContext.tenant and executionContext.tenant.application, and ApplicationContext.environment provides typed access to the application’s environment contract:
| Context Class | Lifecycle Scope | Key Properties | Typical Consumers |
|---|---|---|---|
ApplicationContext |
Singleton per booted App instance |
name, version, instanceId, startedAt, environmentName, environment, host (hostname, pid, platform, runtimeVersion) |
Startup @Injectable services, @Budget (Application scope), observers |
TenantContext |
Per organization / tenant | tenantId ("default" when omitted), weight (positive integer for fair @ModelProvider queuing), application, plus custom subclass fields |
@Budget (Tenant scope), tenant-aware services |
ExecutionContext |
Per workflow execution pass | executionId, threadId, ownerId, tenant, signal (AbortSignal), workflow, step (nodeId, visitNumber, idempotencyKey, callId, itemIndex, copyNumber) |
@Tool, @Action, @Router, @Arbiter, @Judge, @InputGuardrail, @OutputGuardrail, @Channel |
Compiler diagnostic
[context.prompt-unused]: BecauseTenantContextandExecutionContextfields are supplied by your server caller (app.execute(Start, input, { tenant, execution })) and never generated by an LLM, their@Text(),@Integer(), or@OneOf()decorators must omitprompt([context.prompt-unused]).
import { ExecutionContext, TenantContext, Tool, type ToolHandler } from "@agboss/core";import { Integer, Text } from "@agboss/core/dto";
export class SupportTenantContext extends TenantContext { @Text() planTier!: string;
@Integer() monthlyQuotaCents!: number;}
export class SupportExecutionContext extends ExecutionContext { @Text() requestTraceId!: string;}
@Tool({ name: "issue_credit", description: "Issues a tenant-scoped credit using ExecutionContext idempotencyKey", input: CreditRequestDto, output: CreditReceiptDto, deps: [SupportExecutionContext, SupportTenantContext],})export class IssueCreditTool implements ToolHandler<CreditRequestDto, CreditReceiptDto> { constructor( private readonly ctx: SupportExecutionContext, private readonly tenant: SupportTenantContext, ) {}
async run(input: CreditRequestDto): Promise<CreditReceiptDto> { this.ctx.signal.throwIfAborted(); return { receiptId: `cr_${this.ctx.step.idempotencyKey}`, tenantId: this.tenant.tenantId, planTier: this.tenant.planTier, traceId: this.ctx.requestTraceId, amountCents: input.amountCents, }; }}import { Application, EmptyEnvironment } from "@agboss/core";import { SupportTenantContext, SupportExecutionContext } from "./config/support-contexts.js";
@Application({ name: "support-application", version: "1.0.0", environment: EmptyEnvironment, tenantContext: SupportTenantContext, executionContext: SupportExecutionContext, workflows: [SupportWorkflow], budgets: [CtxAppBudget, CtxTenantBudget, CtxExecBudget],})export class SupportApplication {}import { expect, it } from "vitest";import { testTool } from "@agboss/core/testing";import { IssueCreditTool } from "../src/config/support-contexts.js";
it("injects custom TenantContext and ExecutionContext fields into tools", async () => { const runCredit = testTool(IssueCreditTool, { tenant: { tenantId: "acme", weight: 4 }, execution: { requestTraceId: "req_9" }, }); const res = await runCredit({ amountCents: 25 }); expect(res.output.tenantId).toBe("acme");});How tenantId and ownerId authorization protects threads and pauses
Section titled “How tenantId and ownerId authorization protects threads and pauses”When an execution starts with tenant: { tenantId: "acme" } and ownerId: "user-42", AGBoss records ownership on the thread and every pause record:
- Cross-tenant isolation: Calling
app.execute,app.resume, orapp.cancelwith a mismatchedtenantIdthrowsUnauthorizedExecutionAccessErrorand firesOnExecutionRejectedbefore loading any checkpoint state. - Owner vs. channel responder: A user-owned thread (
ownerId) can only be continued or cancelled by thatownerId, while out-of-process@Channelapprovals recordansweredByin the audit trail.