Skip to main content

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