Live package

@stacksona/sdk

Gate one exact action without rebuilding the request, review, revision, proof, and execution loop yourself.

Fast path

  1. 1
    Install.

    npm install @stacksona/sdk

  2. 2
    Create a client.

    Use your Gate URL and sg_ agent API key.

  3. 3
    Wrap the real side effect.

    runGatedAction calls it only after Gate allows or approves the exact request.

Gate an action

ts
import { StacksonaGateClient } from '@stacksona/sdk';

const gate = new StacksonaGateClient({
  baseUrl: process.env.STACKSONA_GATE_URL!,
  apiKey: process.env.STACKSONA_API_KEY!,
});

const { decision, executed, result } = await gate.runGatedAction(
  'refund-order-8821',
  {
    tool_name: 'issue_refund',
    subject: 'Refund order #8821',
    risk_level: 'high',
    payload: { order_id: '8821', amount: 500, currency: 'usd' },
  },
  async (_decision, request) => issueRefund(request.payload),
);

if (!executed) console.log(`Action stopped: ${decision.status}`);
What the SDK handles

It sends the Gate request, follows the exact review thread, returns requested changes as actionable state, fails closed on unexpected states, and only invokes your callback after allow or approved. The request payload is snapshotted so the callback receives the JSON Gate evaluated.

Reviewer requests changes

Add reviseAction when your runtime can revise a proposal. The revision is submitted on the same thread_id and the SDK continues following that review.

ts
await gate.runGatedAction(taskId, request, executeTool, {
  reviseAction: async ({ decision, request }) => ({
    tool_name: request.tool_name,
    subject: request.subject ?? 'Revised action',
    request_payload: await reviseWithAgent(
      request.payload,
      decision.modification?.requested_changes,
    ),
  }),
});

Signed approval proof

ts
await gate.runGatedAction(
  taskId,
  { tool_name: 'deploy_production', payload: deployArgs },
  async (_decision, request) => deploy(request.payload),
  { validateSignedApprovalToken: true },
);

An approved review without required proof, or token validation with valid: false, fails closed.

Action-bound V2 approval

The updated @stacksona/sdk accepts actionHash on decision requests, serializes it as action_hash, and returns Gate's hash on polling. The trusted runtime must compute SHA-256 over its complete immutable action envelope, not just the tool arguments.

ts
const payload = { order_id: '8821', amount: 250 };
const actionHash = runtime.computeActionHash(immutableAction); // Runtime-owned
const initial = await gate.requestDecision({
  taskId: 'refund-8821',
  workflowName: 'RefundOrder',
  taskLabel: 'Refund',
  toolName: 'issue_refund',
  subject: 'Issue refund',
  payload,
  actionHash,
});
if (initial.status === 'pending_review') {
  const approved = await gate.pollDecision({ thread_id: initial.thread_id! });
  if (approved.status === 'approved') {
    if (!approved.approval_token || approved.action_hash !== actionHash) {
      throw new Error('Missing or mismatched action-bound proof');
    }
    const check = await gate.validateApprovalToken({
      taskId: approved.task_id,
      threadId: approved.thread_id!,
      signature: approved.approval_token,
      requestPayload: payload,
      actionHash,
    });
    if (!check.valid || check.action_hash !== actionHash) throw new Error('Denied');
    await runtime.executeImmutableAction(immutableAction);
  }
}

The initial Gate allow, reject, or pending_review response echoes the supplied V2 action_hash. The SDK checks the echoed hash and task ID before returning the response; missing or mismatched identity fails closed.

validateApprovalToken accepts camelCase and snake_case variants of threadId, requestPayload, and actionHash. For V2, all three are required in addition to task ID and signature. Both V1 and V2 require the exact original requestPayload, even {}. The SDK validates Gate's returned V2 task, thread, and hash identity; V2 cannot silently downgrade to V1.

When revising a V2 review with logRevision, supply the fresh actionHash. The SDK authenticates Gate's commitment, but your trusted runtime must ensure the actual handler executes the committed action. Read the Agent API reference for wire schemas.

Methods

MethodPurpose
resolveTools(tools)Resolve registered names, descriptions, and input schemas.
logEvent(taskId, event)Append timeline evidence.
requestDecision(taskId, input)Evaluate one exact proposed tool call.
getDecision(query)Read a review by thread or task.
listDecisions(query)List recent pending or completed reviews.
pollDecision(query, options)Wait for approved, rejected, or changes requested.
requestDecisionAndPoll(taskId, input, options)Request and automatically wait on human review.
submitRevision(taskId, threadId, revision)Submit requested changes on the same review thread.
validateApprovalToken(input)Validate and consume one-time approval proof.
runGatedAction(taskId, input, action, options)Own the normal Gate lifecycle for one action.

Dynamic tools

ts
const { tools, unregistered } = await gate.resolveTools([
  'issue_refund',
  'send_customer_email',
]);

Use the returned exact tool contracts when the runtime builds its model tool list dynamically.

Manual durable flow

When your app already has a queue or durable resume mechanism, use requestDecision and persist the returned thread_id, then resume with getDecision or pollDecision.

Package version

@stacksona/sdk 0.5.0 · Node.js 18+