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