Handle permissions and plans
Pass narrow responder functions to a run when application code can decide permission or plan-approval asks. The raw ask remains in the event stream so a UI or audit consumer can still observe it.
Resolve ordinary permission asks
onPermissionAsk receives the ask and an AbortSignal. Return a verdict only
for decisions the application is authorized to make:
import { connect } from '@stacklok-oss/mecatl-sdk/node';
await using client = connect({
baseUrl: process.env.MECATL_URL ?? 'http://127.0.0.1:8080',
});
const session = await client.sessions.create({});
const run = await session.run('Inspect the repository without changing it', {
onPermissionAsk: (ask) => (ask.tool === 'Read' ? 'allow_once' : 'deny'),
});
console.log((await run.result()).text);
The available verdicts are allow_once, allow_always, and deny. Returning
undefined leaves the ask unresolved for another application path. Server-side
deny rules remain authoritative.
When a UI makes the decision after receiving a permission.ask event, call
run.resolveAsk(askId, verdict). The first accepted verdict wins. A duplicate
or late response throws PermissionAskAlreadyResolvedError.
When the application retained only the session, run, and ask IDs, load a fresh session handle and use prompt-free run controls:
const session = await client.sessions.get(storedSessionId);
await session.controls(storedRunId).resolveAsk(
storedAskId,
'allow_once',
{ timeoutMs: 10_000 }
);
This path resolves an ordinary root or surfaced-child permission ask on that
exact run. It can resume an ordinary ask from a persisted awaiting run after a
daemon restart. An unknown or already resolved ask returns
ask_not_pending. A plan-originated ask returns plan_resolution_required and
remains pending for the plan workflow.
The acknowledgement is one unary response. A transport failure, caller cancellation, or deadline after dispatch can reject the promise after the server accepts the verdict. Reconcile that ambiguous case from the session's durable activity before retrying.
Resolve a plan during a live run
Plan approval is separate from ordinary permission approval. Pass
onPlanApproval when the run can call PresentPlan:
import { PermissionMode } from '@stacklok-oss/mecatl-sdk/gen';
const planSession = await client.sessions.create({ mode: PermissionMode.PLAN });
const run = await planSession.run('Plan and implement the requested change', {
onPlanApproval: (_ask, signal) => (signal.aborted ? undefined : 'approve'),
});
console.log((await run.result()).stopReason);
The responder returns approve, accept_edits, iterate, or undefined.
query() requires onPlanApproval before it creates resources when the new
session uses plan mode. Set session.mode to PermissionMode.PLAN in the query
options.
session.controls(runId).resolveAsk() does not approve or deny plans. Use the
live plan responder above or session.resolvePlan() for a parked plan.
Continue a parked plan
Use session.resolvePlan() when a plan is durably parked and no local Run
handle remains:
const resolution = session.resolvePlan('approve');
const { continuation, resumed } = await resolution.result();
console.log('resumed', resumed.runId);
if (continuation !== undefined) console.log('continuation', continuation.runId);
The resumed plan run keeps its original run ID. An approved plan starts a
separate continuation run with a new ID. continuation is absent when the
resumed run does not end with plan_approved.
Like Run, a PlanResolution has one consumption mode. Iterate its merged
events or call result(), once. Use session.activity() when another observer
needs the durable timeline across both run IDs.
Next steps
- Resume durable activity to observe approved plans across both runs.
- Permissions and posture for the server-side permission model.
Related information
- TypeScript SDK core API for responder, error, and plan-resolution types.
- Permissions and guardrails for builders
- Start and resume sessions