@lumifai/harness
v0.2.1
Published
Lumif's thin harness factory around Mastra's `AgentController` runtime. The controller is shared; each conversation is owned by a Mastra `Session`. Server integrations add scoped identity, durable lifecycle, reconnect, and transport behavior.
Downloads
248
Readme
@lumifai/harness
Lumif's thin harness factory around Mastra's AgentController runtime. The controller is shared;
each conversation is owned by a Mastra Session. Server integrations add scoped identity,
durable lifecycle, reconnect, and transport behavior.
Layered install
- Core —
@lumifai/harness,@lumifai/harness-protocol - Presets —
@lumifai/harness-presets - Tool packs — individual
@lumifai/harness-tool-pack-*packages - Server —
@lumifai/harness-server,@lumifai/harness-server-fastify,@lumifai/harness-server-nest - Client / React —
@lumifai/harness-client,@lumifai/harness-react,@lumifai/harness-react-mantine
Usage
import { LibSQLStore } from '@mastra/libsql';
import { createLumifHarness } from '@lumifai/harness';
import { createPresetHarnessConfig, createPresetWorkspace } from '@lumifai/harness-presets';
const presets = createPresetHarnessConfig({
includeRequestAccess: true,
});
const controller = createLumifHarness({
id: 'my-service-agent',
storage: new LibSQLStore({ url: 'file:./data.db' }),
workspace: createPresetWorkspace({
workspaceMode: 'local',
sandboxEnabled: true,
}),
modes: presets.modes,
subagents: presets.subagents,
tools: presets.tools,
});
await controller.init();
const session = await controller.createSession({
id: 'session-1',
ownerId: 'user-1',
resourceId: 'resource-1',
});
await session.sendMessage({ content: 'Hello!' });workspace is required. Everything else is forwarded to Mastra's harness and can be overridden as needed.
Production defaults
The server wrapper in @lumifai/harness-server makes persistence and tenancy explicit:
storagedefaults to a local LibSQL file store under the workspace pathmemorydefaults tonew Memory({ storage }), but you can pass any Mastra memory or a factorythreadLockcan be supplied for multi-pod / multi-process coordinationcontextResolverlets adapters injecttenantId,userId,resourceId,sessionId,threadId, and arbitrary request contextauthorizelets adapters enforce route-level access control before state changes or readsallowClientResourceIdisfalseby default so resource scope comes from server context, not the browser/healthzand/readyzare exposed by the Fastify server
Workspace modes
createPresetWorkspace() and createHarnessServerWorkspace() make the workspace shape explicit:
workspaceMode: 'local'-LocalFilesystemplus optionalLocalSandboxworkspaceMode: 'filesystem'- a single filesystem provider, no sandboxworkspaceMode: 'mounts'- mount-backed workspace for sandbox-visible cloud storage
filesystem and mounts are mutually exclusive. Use mounts when the sandbox must see cloud storage paths directly.
Sandbox is off by default. In local mode, set sandboxEnabled: true to attach a LocalSandbox whose workingDirectory matches the LocalFilesystem basePath. You can also pass a custom sandbox or tune local sandbox settings via localSandboxOptions.
Mount-backed example for cloud storage visible inside the sandbox:
import { LocalSandbox } from '@mastra/core/workspace';
createPresetWorkspace({
workspaceMode: 'mounts',
mounts: { '/data': myCloudFilesystem },
sandbox: new LocalSandbox({ workingDirectory: './workspace' }),
});Persistence and resume
The server integration uses two Mastra storage layers:
- Mastra's
harnessdomain stores one scoped session record containing tenant/owner/resource identity, the active thread, mode/model, configuration, lifecycle status, and all pending native suspensions. - Mastra memory/thread storage remains the source of truth for messages, threads, and AgentController state. The session record is a registry and lifecycle index, not a second message store.
The external sessionId is a client handle. The durable record ID is a versioned SHA-256 scope key
over tenant, owner, resource, and external session ID, so identical browser IDs cannot collide
across users or tenants. Explicit destroy deletes owned threads and soft-deletes the record with
deletedAt; storage retention/pruning handles physical cleanup.
Tool approvals are intentionally not persisted: the approval gate is an in-memory parked promise that does not survive a restart, so a pending approval is resolvable only within the live process. Interactive tool suspensions (ask_user, submit_plan, request_access) are the durable, resumable primitive.
Resume flow:
- A request comes in with
sessionId HarnessSessionManagerderives the trusted scope and looks up the harness record directly- A fresh
HarnessSessionis created on the current pod - The harness is reattached to the persisted
threadIdandresourceId - Every persisted native suspension is re-registered from the record
- SSE/event subscriptions attach to the new in-memory session instance
This is what makes the harness resumable across pods. Pod-local memory is not the source of truth.
Display state and SSE events include currentRunId plus an array of pending suspensions. Each item
has its own toolCallId and optional runId, so parallel human-input prompts can be rendered and
resumed independently.
Mastra approvals are deliberately process-local. If a process dies while an approval is parked,
the next runtime reports the session as interrupted; an old approval response cannot be replayed.
Native tool suspensions are the durable human-input primitive.
While a session is running, sendMessage starts a normal run, followUp queues input behind the
current run, and steer aborts the current run and redirects it. A session waiting on native input
does not keep a model process alive: it can be evicted from the in-memory LRU and reconstructed
when the user returns minutes or days later.
Human-in-the-loop tools
Lumif presets replace Mastra's built-in ask_user and submit_plan with host-owned versions
(same tool ids) and disable those builtins via disableBuiltinTools:
ask_user— free-text or choice questions, includingselectionMode. Coerces free-form mistakes (selectionModewithout options) into a real free-text suspend instead of Mastra's soft failuresubmit_plan— plan review and approval in plan moderequest_access— sandbox directory access prompts (only whenincludeRequestAccess: trueand the workspace exposes aLocalFilesystem)
All interactive flows suspend via Mastra's tool suspension API instead of ad-hoc harness events.
Presets keep autoResumeSuspendedTools off so the host (Studio / API
respondToToolSuspension) owns resumes — Mastra's auto-resume path otherwise coaches the model
to stuff resumeData into tool-call arguments. Lumif keeps the public
{ answer: string | string[] } response shape for ask_user and converts it to Mastra's native
resume value at the server boundary.
The server disables Mastra builtins for each same-id preset tool that is registered. Turn a tool
off entirely with toolToggles.ask_user = false (also disables the Mastra builtin).
Studio treats empty options and single “Use free text” proxy choices as a textarea so free-form questions still render correctly when the model mis-shapes the payload.
Server integration
Use @lumifai/harness-server with Fastify or NestJS adapters. Boot config owns workspace shape, tool packs, and permission rules:
import { createHarnessSessionManager } from '@lumifai/harness-server';
// Fastify: harnessFastifyPlugin from '@lumifai/harness-server-fastify'
// NestJS: HarnessModule from '@lumifai/harness-server-nest'
createHarnessSessionManager({
bootConfig: {
workspaceBasePath: './workspace',
workspaceMode: 'local',
sandboxEnabled: true,
storage: new LibSQLStore({ url: 'file:/absolute/path/to/harness.db' }),
threadLock: {
acquire: async ({ threadId }) => {
/* distributed lock */
},
release: async ({ threadId }) => {
/* distributed unlock */
},
},
toolPacks: [
/* ... */
],
permissionRules: { categories: { execute: 'ask' } },
},
});Built-in workspace boot options (ignored when workspace is provided):
workspaceMode—'local' | 'filesystem' | 'mounts'workspaceFilesystem— custom filesystem providerworkspaceMounts— mount map formountsmodesandboxEnabled,sandbox,localSandboxOptionsonMount— hook before mounting cloud filesystems into a sandbox
Adapter hooks available on the HTTP layer:
contextResolver(request)— derive tenancy and ownership from auth/session headersauthorize({ action, request, context })— gate create/read/write/resume operationsallowClientResourceId— only enable for local development or trusted callers
NestJS standalone server:
import { createStandaloneHarnessNestServer } from '@lumifai/harness-server-nest';
await createStandaloneHarnessNestServer({
port: 4310,
bootConfig: { workspaceMode: 'local', sandboxEnabled: true },
});Tool permissions
The server applies preset permission defaults automatically via buildHarnessOptions():
- Read tools (workspace reads, schema inspection) → auto-approved
- Edit / execute / other categories → prompt for approval (
ask) - Tool pack defaults → per-tool overrides from each pack's
toolConfig request_accessis only enabled when the workspace exposes a local filesystem, because that path is persisted via local filesystem state.
Override from server boot (recommended):
import { createHarnessSessionManager } from '@lumifai/harness-server';
import { createPostgresToolPack } from '@lumifai/harness-tool-pack-postgres';
createHarnessSessionManager({
bootConfig: {
toolPacks: [
createPostgresToolPack({
connectionString: process.env.DATABASE_URL!,
allowedTables: ['public.campaign_briefs'],
allowedColumns: {
'public.campaign_briefs': ['id', 'name', 'metadata', 'tenant_id'],
},
jsonColumns: { 'public.campaign_briefs': ['metadata'] },
}),
],
permissionRules: {
tools: { my_custom_tool: 'ask' },
categories: { execute: 'deny' },
},
},
});Direct harness construction:
import { createPresetPermissionConfig } from '@lumifai/harness-presets';
createLumifHarness({
...createPresetPermissionConfig({
packs: [
/* tool packs */
],
overrides: { tools: { my_custom_tool: 'ask' } },
}),
// workspace, modes, tools, ...
});Custom tools:
- Add via
bootConfig.extraTools(Fastify/Nest) orcreatePresetTools({ extra: { my_tool: createTool({...}) } }) - Set deployment policy in
bootConfig.permissionRules.tools.my_tool - Use
requireApprovaloncreateToolonly when approval is intrinsic to the tool design - Optional: wrap reusable custom tools in a local
ToolPackwithtoolConfig/permissionPolicies - For durable HITL, give the tool
suspendSchema/resumeSchemaand callsuspend(); pair with a clientdefineSuspensionRenderer(see Browser UI)
Permissions are server-owned and are not exposed on the session HarnessConfigPatch API.
Browser UI
@lumifai/harness-client— framework-agnostic browser client and shared reducer/state helpers@lumifai/harness-react— headless provider, hooks, browser client, session store, suspension renderer registry@lumifai/harness-react-mantine— MantineHarnessStudiocomponents
The browser client refreshes its authoritative session snapshot after an SSE failure,
and the serialized display state retains the latest run error as lastError. The
Mantine studio renders concurrent pending suspensions and supports free-text,
single-select, and multi-select ask_user responses.
Custom suspension UIs: pass suspensionRenderers to HarnessProvider and/or HarnessStudio.
Studio merges builtins (ask_user, submit_plan, request_access) then provider then studio
props (later wins by toolName). Headless apps can call useResolvedSuspensionRenderer(suspension)
and resumeToolSuspension(data, toolCallId).
Presets
Import configurable presets from @lumifai/harness-presets:
createPresetModes()—planandbuildmodescreatePresetCustomModes()— returns the built-in modes plus your custom list when you need the final array yourselfcreatePresetSubagents()—exploresubagent; opt-inwebsearchsubagent viaincludeWebSearch: truecreatePresetTools()— Lumif baseline withask_userandsubmit_plan(displaces Mastra builtins). Passpacks,toolPackToggles,toolToggles,includeRequestAccess, and/orextracreatePresetSkills()— default.agents/skillsresolver pathscreatePresetHarnessConfig()— combines the above and merges enabled pack skill pathscreatePresetPermissionConfig()— default category resolver and mergedpermissionRulesfor harness init
Tool packs
Import opt-in capability bundles from @lumifai/harness-tool-pack-* packages. Shared types and merge helpers live in @lumifai/harness-tool-packs:
ToolPack— named bundle of related tools (and optional skills) that flattens intotoolsat harness initcreatePostgresToolPack()— PostgreSQL schema inspection, query, and optional allowlisted write tools (@lumifai/harness-tool-pack-postgres)createWorkspaceDataToolPack()— workspace JSON, CSV, and jq query tools (@lumifai/harness-tool-pack-workspace-data)createServerlessWorkflowToolPack()— serverless runtime tool pack (@lumifai/harness-tool-pack-serverless-workflow)createWebSearchToolPack()— provider-native web search with structured output (@lumifai/harness-tool-pack-web-search)createFileProcessingToolPack()— PDF text extraction to/processed/(@lumifai/harness-tool-pack-file-processing)createOfficeEditingToolPack()— headless revision-checked DOCX, XLSX, PPTX, and PDF editing (@lumifai/harness-tool-pack-office-editing)
Override any preset or pack via the options objects on each factory.
Example:
import { createPresetHarnessConfig } from '@lumifai/harness-presets';
import { createPostgresToolPack } from '@lumifai/harness-tool-pack-postgres';
import { createServerlessWorkflowToolPack } from '@lumifai/harness-tool-pack-serverless-workflow';
import { createWebSearchToolPack } from '@lumifai/harness-tool-pack-web-search';
import { createFileProcessingToolPack } from '@lumifai/harness-tool-pack-file-processing';
import { createOfficeEditingToolPack } from '@lumifai/harness-tool-pack-office-editing';
import { createWorkspaceDataToolPack } from '@lumifai/harness-tool-pack-workspace-data';
import { Agent } from '@mastra/core/agent';
const presets = createPresetHarnessConfig({
subagents: {
includeWebSearch: true,
webSearchProvider: 'openai',
},
customModes: [
{
id: 'review',
name: 'Review',
defaultModelId: 'openai/gpt-5.4-mini',
color: '#059669',
agent: new Agent({
id: 'lumif-review-agent',
name: 'Review Agent',
instructions: 'Review the user changes and report issues.',
model: 'openai/gpt-5.4-mini',
}),
},
],
skills: {
paths: ['.agents/skills'],
},
tools: {
packs: [
createWorkspaceDataToolPack(),
createPostgresToolPack({
connectionString: process.env.DATABASE_URL,
allowAllRoleVisible: true,
// or resolveAccessPolicy / resolveConnectionString for production tenants
}),
createServerlessWorkflowToolPack({
runtimeHost: process.env.RUNTIME_HOST!,
workflowNamespace: 'lumif',
resolveAuthContext: (ctx) => ({
accessToken: ctx.requestContext?.auth?.accessToken ?? '',
}),
}),
createWebSearchToolPack({
defaultProvider: 'openai',
}),
createFileProcessingToolPack(),
createOfficeEditingToolPack(),
],
toolPackToggles: {
postgres: true,
},
toolToggles: {
postgres_query: true,
},
},
});Tool pack toggles
toolPackToggles— enable or disable an entire pack (tools and bundled skills)toolToggles— enable or disable individual tools by id within enabled packstoolConfig.enabledByDefaulton a pack — default per-tool state beforetoolTogglesoverrides
Tool packs are published as separate @lumifai/harness-tool-pack-* packages. Their bundled skills are merged into workspace.skills automatically when the pack is enabled.
