Trust and security

See where your agents actually run.

Every Ninjafy agent sits behind eleven security boundaries, from its own container out to your AWS account. Here is each one: what is shipped today, what is in build, and what we have deliberately not finished yet.

7 of 11 boundaries are shipped today. The rest are labelled honestly, in build.

Interactive walkthrough

Explore the architecture in 3D

Open full screen

Step through every boundary around a running agent, from its container out to your AWS account. Use Next, the numbered steps or your arrow keys to move between boundaries.

Best on a bigger screen

The 3D walkthrough needs room to move. Open it full screen on your phone, or read the eleven boundaries below: they cover the same ground.

Open full screen

The architecture

Eleven boundaries, from the container out

Start at the process an agent runs in and step outward. Open any boundary for the detail, the controls behind it and the work still open.

01One agentShippedOne Claude Code process, one container

An agent is a Claude Code instance in its own container

  • One Claude Code process per agent, in a container named for that agent alone.
  • Minimal Amazon Linux 2023 with Node, Claude Code and that agent's tools.
  • Starts with every Linux capability dropped and new privileges refused.
  • CPU, memory and process ceilings: one agent cannot exhaust the machine its neighbours use.
  • The Docker socket is never mounted: an agent cannot reach the daemon supervising it.

ADR-0014 · cap-drop ALL · no-new-privileges · CPU / memory / PID ceilings

02Sandbox isolationShippedSeparate filesystems, not separate folders

One agent cannot read another

  • The boundary is the container's mount namespace, not file permissions.
  • Own working tree, temp directory, package cache and assistant state, per agent.
  • Session transcripts are masked so an agent sees only its own.
  • Closes threat T5: a compromised agent reading a neighbour's credentials or forging work into its queue.
  • Shared on purpose, and we would rather say so: the host's single assistant login, and the supervising daemon outside every container.

Mount namespace boundary · per-agent binds · closes threat T5 (cross-agent injection)

03The hostShippedNo way in, one way out

The host has no inbound ports at all

  • Host and EC2 instance are not the same thing. The host is our record of the machine; the EC2 instance is the AWS VM it runs on. Every host drawn on this page (ours, or one in your own account) runs on exactly one instance, and several agent containers share it. A self-managed host is the exception and carries no instance at all.
  • No inbound ports, no public address, nothing listening. There is no network path INTO an agent host.
  • Operator access is AWS Systems Manager only: identity-gated, audited per session. No SSH key, no bastion.
  • Each agent sits on an internal network with no route off it.
  • A per-agent squid sidecar filters egress on the CONNECT hostname against that agent's own allowlist, deny-by-default, with no TLS interception.
  • It is BUILT AND NOT YET ARMED. ENG-6579 is done and the control has never been in force on any host; arming the fleet is tracked as ENG-10740. Until then this is design, not defence.
  • Production traffic leaves from 32.236.6.24 and 54.253.255.215, which you can allowlist on your side.

hosts_ec2_consistency_check · ingress [] · SSM-only operator access · IMDSv2 · per-agent egress allowlist BUILT, NOT ARMED (ENG-6579 · arming ENG-10740)

04Key custodyIn buildSeparate from anything we connect to

Your keys are their own boundary, not part of the link

  • A boundary of its own, settled before anything is said about traffic leaving: nothing about our link to the host reaches your keys.
  • Customer-managed keys are in build and NOT shipped: the customer-CMK path parses and then throws in this build (ADR-0062).
  • Platform secrets sit in SSM Parameter Store as SecureString under our own KMS key. Every read is a CloudTrail event.
  • Integration credentials are AES-GCM enveloped in Postgres under a key that itself lives in that store. KMS protects them one step removed.
  • On the host, an agent's credentials are written to disk in the clear, protected by the container isolation on layers 01-02.
  • The transcript archive on the next boundary uses S3-managed AES256, not a key you hold. That is the gap CMEK closes.

CMEK in build · platform CMK for the secret store today · BYO-account archive stays in your bucket

05Encrypted transcriptsShippedWrite-only for the host, read-only for nobody

Every session is archived, and the host cannot read it back

  • Every session an agent runs is archived to S3 and encrypted at rest with SSE-S3 AES256 - our key today, not one you hold.
  • The host can only PUT. Its role carries s3:PutObject and AbortMultipartUpload and nothing else: no read, no list, no delete.
  • Versioned, public access blocked, and a TLS-only bucket policy.
  • Glacier at 90 days, expiry at 365, superseded versions gone at 30.
  • This is the evidence the audit index points at. The index holds identity and timing; the transcript is the thing itself.
  • The copy on the host is short-lived, so the archive is the record rather than a convenience.

session-archive.ts · SSE-S3 AES256 · PutObject-only host role · versioned · 90d Glacier · 365d expiry

06Security group · egress onlyShippedNothing dials in

The security group has no inbound rules at all

  • Zero ingress rules. Not a narrow allowlist and not a source restriction: the ingress list is empty, on both of the security groups we create.
  • Nothing initiates a connection to a host. The control plane learns state from an outbound heartbeat the host makes.
  • Operator access does not open a port either. SSM Session Manager tunnels over the agent's own outbound channel to the SSM endpoint, so there is no SSH key and no bastion.
  • Egress is deliberately wide: 0.0.0.0/0, every protocol. This control runs one way, and a diagram that implied a firewall in both directions would be selling you something.
  • Deployed into your own account the stack-created group is the same shape. Supply your own group instead and its rules are yours, which the next boundary says plainly.

host-infra/security-groups.ts ingress:[] · augmented-byo-host.yaml HostSecurityGroup · zero SecurityGroupIngress in the template

07Your AWS accountShippedYour account, your egress, your revocation

The host can run inside your own AWS account

  • You deploy a published, version-pinned CloudFormation stack into your own account.
  • It creates one role we assume, pinned twice: a tenancy-unique secret AND our exact ARN. A leaked identifier alone gets nobody in.
  • A permissions boundary denies all IAM except passing the host role, and limits changes to instances we tagged.
  • Egress leaves through a NAT gateway at your account edge, so the hop out carries an address you control.
  • Supply your own security group for the host and its rules are yours. This stack does not constrain them, and we do not claim to.
  • The archive from the previous boundary is created by this same stack, in your account, and is kept if the stack is deleted.
  • No standing credentials: sessions under an hour, every assumption in your CloudTrail, delete the role to end our access.

ADR-0028 · augmented-byo-host.yaml · sts:ExternalId + aws:PrincipalArn · 1h max session · NAT egress

08The brokerAllowlist in buildOne broker to allowlist, not one per vendor

The integrations sit behind a single broker

  • First of three destinations past the NAT hop. There are three and no others.
  • Composio fronts the popular integrations: Gmail, Drive, Calendar, HubSpot, Xero, Slack, Outlook.
  • One vendor with one address set to allowlist, rather than a different endpoint per tool.
  • Reached from 32.236.6.24 and 54.253.255.215: 25 of 25 production hosts measured on 2026-09-12.
  • Per-key pinning to those addresses is built and NOT armed: 0 of 28 keys read protected at the last audit.
  • The addresses are publishable today. The vendor-side pin is a switch nobody has thrown.

ADR-0032 (Proposed) · composio-approved-egress.json · addresses declared, per-key pin not armed

09AI GatewaysAuto-failover in buildTwo bindings, one active, a failover between them

The second destination is the model providers

  • Anthropic, Vercel and TrueFoundry, named rather than left as a hole in the egress story.
  • What leaves here is the model traffic: the prompt the agent reasons over, and the tool output it gathered.
  • Same NAT hop, same two addresses as the broker.
  • No second path: the per-agent proxy on layer 03 is an agent's only route off its network.
  • No customer integration is reached here, and nothing on this rail touches your keys.
  • A policy carries TWO bindings, primary and secondary, each with its own transport, credential and model. Promoting the secondary changes the agent's auth tuple, which respawns the session onto it. That path works today.
  • The allowlist this host computes ALREADY contains the secondary's address, deliberately - every binding is included, not only the active one, because a secondary you cannot reach fails at the moment the primary degrades. Note the allowlist is computed and not yet enforced anywhere: same state as boundary 03.
  • AUTOMATIC promotion IS NOT ARMED. The trigger flag accepts off and shadow only; armed is deliberately not an allowed value, and nothing writes an automatic failover state. Shadow evaluates every model-API error against the policy and logs the decision, acting on nothing. Today a human promotes.

ENG-9489 roles · ENG-9483 both bindings allowlisted · ENG-10156 / ADR-0074 trigger off, armed not yet a value

10Control plane & auditShippedApproval before the act, a record after it

The third destination is us, and it opens up

  • Tenant-isolated store row-level security on 248 of 249 tables. The API path reads as service role and bypasses it, so it is guarded in code and CI.
  • Control API the only part of us a host talks to, and the host dials OUT to it on a heartbeat. No inbound port exists.
  • Approval gate on the broker verbs the agent never holds the capability: the API makes the call server-side after a human taps Approve.
  • Audit index append-only by database trigger, not policy. Identity and timing for every tool invoked, addressing the archived transcript as the evidence.
  • Operator console where a person approves, denies and reads that record back.
  • On the generic approval verb the agent acts itself once recorded, and policy can auto-approve with no human. Neither is a hard block.
  • Arguments and results are never stored. A sha256 of the input is, and a scrubbed target: argv[0], a hashed path, a URL host.
  • Retained 365 days, and the archived evidence expires on the same clock. Gated on the Advanced Governance entitlement.
  • NOT in this rack, deliberately: your keys (layer 04), and your transcripts (layer 05).

Approval core · append-only tool-call index · 365-day retention · Advanced Governance tier

11Daily backup to your GitHubShips darkYour config, in a repo you own, on your own clock

The last hop leaves from us, not from a host

  • A daily sweep at 08:00 UTC commits an export bundle to a repository YOU own, through a GitHub App you installed. The destination is your git history, not ours.
  • It runs from the control plane, not from an agent. No agent holds the credential or can trigger it, and this is not on any agent's egress path.
  • On by default agent config, agent-scoped skills, org and team skills, team workflows, and a manifest of teams, members and integrations by name.
  • OFF by default, opt in per org MEMORIES and KNOWLEDGE. Memories are distilled from end-user conversation content and knowledge may carry proprietary material, so neither leaves the platform on a recurring basis unless an operator turns it on for that org.
  • Unchanged content is skipped rather than recommitted, so the history records changes and not the sweep.
  • Three consecutive PERMANENT failures - the App uninstalled, the repo gone - disable an org's backup rather than retrying a broken config forever. Rate limits and 5xx are transient and never count.
  • IT SHIPS DARK. The `github-backup` flag defaults false and the enterprise entitlement is enforced separately, so the sweep no-ops until an org is BOTH entitled and flagged on. Until then this is capability, not practice.

ENG-7646 / ENG-7667 · cron 0 8 * * ? * · BACKUP_DEFAULT_SELECTIONS: memories and knowledge off · flag defaults false

Want the full security pack?

Request our security documentation, architecture review or a walkthrough with the team.