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.
| Level | What it may do | Who decides a change | Set where |
|---|---|---|---|
| Reads and drafts | An agent reads the datasets assigned to its version and searches knowledge. It answers with a draft. | A person, in your system or in Studio | The agent's datasets; the version pinned per environment |
| Safe | A Scan routine runs the task in a real browser and stops at every step that changes something. | A person, in Studio, per step | A Scan blueprint for a set of environments; a routine granted to an agent version as a tool |
| Autonomous | The routine runs through, every step on the record with its timing. | Nobody at run time; the admin who set the mode | The same places. Autonomous in PROD needs manage production |
/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
/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
/core/api/v1/organizations/{org_id}/scan/blueprintsSet 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_onlyset 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.
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_mismatchbefore 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.
| Boundary | What holds it |
|---|---|
| Every request | A 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 systems | A 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 assistants | An MCP key belongs to one config and one organization, and reaches only the agents in that config. |
| Agent runs | A 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 routines | A 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. |
| Secrets | Keys are written once. Reads return a mask, records show [redacted], and tests and runs use the key inside your instance. |
| Your instance | A 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.
/core/api/v1/organizations/{org_id}/runsEvery run, newest first. Filter by kind, subject_type, subject_id, status, env_id, actor_id, from, to.
NeedsRead runs
/core/api/v1/organizations/{org_id}/activityEvery change, newest first. Filter by actor_id, action, object_type, object_id, env_id, from, to.
NeedsRead activity
/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
/core/api/v1/organizations/{org_id}/activity:verifyRecompute the hash chain over from_seq to to_seq. Answers ok, checked, head_hash, and first_mismatch_seq.
NeedsRead activity
/core/api/v1/organizations/{org_id}/activity:exportExport 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.
- Changes to it.
GET …/activity?object_type=agent_config&object_id=…. For a person, filter byactor_idinstead. - Runs of it.
GET …/runs?subject_id=…, and…/runs/{run_id}/eventsfor any run you want in full. - Your copy. Export the activity range to NDJSON each night into your SIEM or object store. The lines carry the hashes.
- Prove it. Call
…/activity:verifyover the exported range and keep thehead_hash. A changed entry makes its fingerprint stop matching, along with every one after it.
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}`);
{ "ok": true, "checked": 1842, "head_hash": "a71e…", "first_mismatch_seq": null }
07Retention and recovery
- Retention settings.
GETandPUT …/settings/retentionhold 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/economicsholds the rates Scan uses to value time saved, so the numbers on…/scan/runs:statsare 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.