Autonomy, permissions, and audit

Decide how much an agent may do, who may change what, and in which environment. Then read the record of all of it. Every call is checked for identity, organization, permission, and environment. Nothing is trusted for where it came from.

For
Architects and security
Modes
Reads and drafts · Safe · Autonomous
Roles
Admin · Builder · Member
Audit
Hash-chained, exportable

01Assign autonomy

Autonomy is set per agent version and per environment, in three levels that ship today.

LevelWhat it may doWho decides a changeSet where
Reads and draftsAn agent reads the datasets assigned to its version and searches knowledge. It answers with a draft.A person, in your system or in StudioThe agent's datasets; the version pinned per environment
SafeA Scan routine runs the task in a real browser and stops at every step that changes something.A person, in Studio, per stepA Scan blueprint for a set of environments; a routine granted to an agent version as a tool
AutonomousThe routine runs through, every step on the record with its timing.Nobody at run time; the admin who set the modeThe same places. Autonomous in PROD needs manage production
PUT/agents/api/v1/organizations/{org_id}/agents-config/{agent_config_id}/pins/{env_id}

Pin a version to an environment. The run in that environment uses the pinned version.

NeedsUpdate agents, plus manage production for a PROD environment

PUT/agents/api/v1/organizations/{org_id}/agents-config/{agent_config_id}/tools/{tool_ref}

Grant a Scan routine to the agent as a tool, Safe or Autonomous. Recorded; runs call it once the Scan tool runtime ships.

NeedsUpdate agents

POST/core/api/v1/organizations/{org_id}/scan/blueprints

Set a routine up to run, Safe or Autonomous, for a set of environments.

NeedsPublish Scan routines, plus manage production for Autonomous in PROD

  • Versions, not edits. Publishing creates a numbered version with a changelog; a pin moves an environment from one version to the next, and the move is an activity entry.
  • Lock what must not change. An agent with read_only set answers 403 to everyone but a platform administrator.
  • Start Safe. Run a routine Safe in UAT until its record shows it checked out, then set Autonomous where the risk allows.

02Permissions: who may do what

Three roles, one permission per route. Admin holds every permission. Builder creates, edits, and runs everyday objects outside PROD. Member reads everything except secrets, machine access, members, and billing. Every route on the API reference names the permission it needs.

GET/core/api/v1/organizations/{org_id}/me/permissions

curl -s "$NSTACK_BASE/core/api/v1/organizations/$NSTACK_ORG/me/permissions" -H "$H"
curl -s "$NSTACK_BASE/core/api/v1/organizations/$NSTACK_ORG/permissions" -H "$H"   # the catalog

const me = await get(`${core}/me`);
const can = (perm: string) => me.permissions.includes(perm);   // strings as the catalog at …/permissions names them

if (can(PERM.readRuns)) showRunsPanel();
if (!can(PERM.manageProduction)) hideProdActions();

Check permissions before you offer an action, and expect the service to check again. A refused call answers 403 forbidden with the permission it wanted.

03Organization, environment, production

  • Organization on every call. The path names it, the credential must belong to it, and a mismatch answers 403 org_mismatch before anything is read. Platform admins get no exception.
  • Environment on everything that touches data or runs. Connection settings, knowledge indexes, pins, schedules, and machine tokens are all per environment, so DEV never reaches PROD credentials.
  • Production is a second gate. A change to a PROD environment needs the permission to manage production on top of the route's own. Studio asks the member to type the environment's alias; your code should confirm too.
  • Fail closed. When a service cannot confirm whether an environment is production, it refuses the change with 503 production_check_unavailable.

04Zero trust, as shipped

No call is trusted for the network it came from, and no run is trusted for the agent that started it. These are the checks in the services today.

ChecksWhat every request passes before it changes anything, drawn on a sample call.

BoundaryWhat holds it
Every requestA credential each service verifies itself: a session from your sign-in application, a machine token, or an MCP key. Then organization, permission, and environment, in that order.
Your systemsA machine token belongs to one environment and to the webhooks an admin attaches it to. It starts those automations there and nothing else. Revoke it and it stops working at once.
Outside assistantsAn MCP key belongs to one config and one organization, and reaches only the agents in that config.
Agent runsA run executes in a sandbox with the tools its version was given: a query per assigned dataset and a knowledge search, both scoped to the run's environment. Nothing else is callable from a run today.
Scan routinesA routine runs in a real browser, one step at a time, with every step on the record. In Safe mode each step that writes waits for a person.
SecretsKeys are written once. Reads return a mask, records show [redacted], and tests and runs use the key inside your instance.
Your instanceA dedicated instance runs its own databases, event stream, storage, sign-in, and domain. Firms on the shared NSTACK Cloud get the same per-request checks.

05Observability trails, per permission

Two records cover the platform. The run record holds everything that ran: steps, events, actor, duration, and cost. The activity feed holds every configuration change: who changed what, from which value to which, with secrets redacted. Each has its own read permission, so a team can hold one without the other.

GET/core/api/v1/organizations/{org_id}/runs

Every run, newest first. Filter by kind, subject_type, subject_id, status, env_id, actor_id, from, to.

NeedsRead runs

GET/core/api/v1/organizations/{org_id}/activity

Every change, newest first. Filter by actor_id, action, object_type, object_id, env_id, from, to.

NeedsRead activity

GET/core/api/v1/organizations/{org_id}/activity/{event_id}

One entry with its changes as a JSON Patch, its references, and its hashes.

NeedsRead activity

GET/core/api/v1/organizations/{org_id}/activity:verify

Recompute the hash chain over from_seq to to_seq. Answers ok, checked, head_hash, and first_mismatch_seq.

NeedsRead activity

POST/core/api/v1/organizations/{org_id}/activity:export

Export a time range as NDJSON. Each line holds the chain link and the CloudEvent it hashes.

NeedsRead activity

06Set up a trail on anything

Pick the object, filter both records by it, and keep your own copy. The same recipe works for an agent, a connection, an automation, an environment, or a person.

  1. Changes to it. GET …/activity?object_type=agent_config&object_id=…. For a person, filter by actor_id instead.
  2. Runs of it. GET …/runs?subject_id=…, and …/runs/{run_id}/events for any run you want in full.
  3. Your copy. Export the activity range to NDJSON each night into your SIEM or object store. The lines carry the hashes.
  4. Prove it. Call …/activity:verify over the exported range and keep the head_hash. A changed entry makes its fingerprint stop matching, along with every one after it.
Nightly export and check

FROM=$(date -u -v-1d +%Y-%m-%dT00:00:00Z); TO=$(date -u +%Y-%m-%dT00:00:00Z)

curl -s -X POST "$NSTACK_BASE/core/api/v1/organizations/$NSTACK_ORG/activity:export" -H "$H" \
  -H "Content-Type: application/json" -d "{\"from\":\"$FROM\",\"to\":\"$TO\"}" > "activity-$FROM.ndjson"

FIRST=$(head -1 "activity-$FROM.ndjson" | python3 -c "import sys,json; print(json.load(sys.stdin)['seq'])")
LAST=$(tail -1 "activity-$FROM.ndjson" | python3 -c "import sys,json; print(json.load(sys.stdin)['seq'])")
curl -s "$NSTACK_BASE/core/api/v1/organizations/$NSTACK_ORG/activity:verify?from_seq=$FIRST&to_seq=$LAST" -H "$H"

const lines = await (await fetch(`${core}/activity:export`, {
  method: "POST", headers: { ...auth, "Content-Type": "application/json" },
  body: JSON.stringify({ from, to }),
})).text();
await siem.put(`nstack/activity/${from}.ndjson`, lines);

const rows = lines.trim().split("\n").map((l) => JSON.parse(l));
const check = await get(`${core}/activity:verify?from_seq=${rows[0].seq}&to_seq=${rows.at(-1).seq}`);
if (!check.ok) page(ops.security, `activity chain mismatch at seq ${check.first_mismatch_seq}`);
Verify200 · sample values

{ "ok": true, "checked": 1842, "head_hash": "a71e…", "first_mismatch_seq": null }

07Retention and recovery

  • Retention settings. GET and PUT …/settings/retention hold the organization's retention settings; updating them needs the permission to update the organization.
  • Deletes are soft. A connection, dataset, ontology template, or agent comes back within 30 days with POST …:restore.
  • Cost settings. …/settings/economics holds the rates Scan uses to value time saved, so the numbers on …/scan/runs:stats are yours.

08In design

  • In design Scopes per MCP key and OAuth 2.1 for assistants that support it.
  • In design Machine access to REST, scoped by permission and environment.
  • In design Approvals through the API, with decision routes and SLA timers.

09Next