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.

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.

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