inis.run

Execution log

What inis.run records about a session's lifecycle and network activity, and how to read it back.

Every session emits a record of what actually happened — created, executed, paused, destroyed — into a hash-chained execution log. A session running under an egress allowlist also records every outbound network attempt, allowed or denied; a session on the default full-egress policy records no network events at all. This page covers the event types, the egress payload shape specifically, and how to read events back over the API.

Reading the log

GET/v1/audit/events

This lives on the account/control-plane API (https://app.inis.run), not the execution API — it's gated by your signed-in cookie session in the console, the same as /v1/keys and /v1/usage, not by an Authorization: Bearer token. Calling it with a bearer key returns 401 {"error":"not authenticated"}. The console's own Logs view calls this endpoint from the browser — that's the primary way to read it today.

Query parameters:

ParamDescription
from / toRFC3339 time range. Defaults to the last 7 days ending now.
limitPage size, default 100, max 1000.
session_idFilter to one session.
typeFilter by event type — repeatable and/or comma-separated (e.g. type=egress.denied).
qFree-text search over the event type and payload.
cursorPage token from a previous response's next_cursor, for paging further back.

Response shape:

{
  "from": "2026-07-28T19:00:00Z",
  "to": "2026-08-04T19:00:00Z",
  "next_cursor": "",
  "events": [
    {
      "timestamp": "2026-08-04T19:11:02.481Z",
      "region": "eu",
      "session_id": "9hmquuxiz7qyqdqiwfvep",
      "org_id": "org_...",
      "project_id": "proj_...",
      "event_type": "egress.denied",
      "data": {
        "dst": "93.184.215.14",
        "port": 443,
        "proto": "tcp",
        "domain": "example.com",
        "policy_default": "deny",
        "escalation_available": true
      },
      "node_id": "00000000-0000-0000-0000-000000000000",
      "seq": 4821,
      "prev_hash": "…",
      "event_hash": "…",
      "chain_verified": true
    }
  ]
}

Each event carries prev_hash/event_hash/chain_verified so you can verify the sequence hasn't been altered independently of trusting this response. The data field above is a representative shape built from the source payload definition; the values (destination, domain, node) are illustrative, not a literal captured response — reading a real one requires a browser session, not a bearer key.

Egress event types

EventWhen
egress.allowedAn outbound connection matched the session's allowlist.
egress.deniedAn outbound connection was blocked because it didn't match.
egress.policy_updatedPOST /v1/sessions/{id}/egress changed a live session's policy.

Two more names — egress.approval_requested / egress.approval_granted / egress.approval_denied — exist in the event vocabulary for a future just-in-time approval flow (a blocked attempt gets escalated to a human rather than staying blocked). They are not emitted today: nothing in the codebase produces them yet. The escalation_available field on a denial (below) is the only live signal of that direction — it tells you a control- plane path exists to widen the policy, not that the approval flow itself is built.

Payload fields (EgressData)

FieldTypeNotes
dststringDestination IP.
portintDestination port.
protostringtcp, udp, icmp, or proto-<n>.
bytesint64Observed bytes across the aggregated group (see below).
countintNumber of identical attempts folded into this one event.
domainstringHostname the attempt named — the DNS query name or the TLS SNI. Always present on a denial, and on an allow too — every egress.* event comes from a deny-mode session, because an allow-mode session produces none (see below).
policy_defaultstringThe session's egress mode (deny or allow) at the time of the attempt, so a denial carries the reason it happened.
escalation_availableboolSet on a denial when a control-plane path exists to widen the policy — groundwork for the approval flow above, not a working escalation UI yet.

Aggregation, not sampling. A guest retrying a blocked host in a loop (a resolver's own retry logic, or a script busy-looping on a denied domain) doesn't produce one audit row per attempt. Repeated denials for the same (session, domain) pair within a short window are coalesced into one event with count incremented, so a retry storm shows up as one entry with a count, not a flood.

The honest limit on count. Packet capture is itself rate-limited per session — on the order of 100 packets/second by default, applied separately to the allowed and denied paths — so a scanning guest cannot flood the capture path. Enforcement is unaffected: a packet over that rate is still allowed or dropped exactly as the policy says. But it is not captured, so under a sustained storm count is a floor, not an exact tally. At ordinary traffic rates every attempt is captured.

Why egress.* events are deny-mode-only. The capture path is part of the enforcement machinery, not a separate observer: the packet-logging rules are installed alongside the allowlist when a deny-mode policy is applied, and torn down when the policy reverts to allow. So an allow-mode session (full public egress, the default) produces no egress.* events whatsoever — not destination-less ones, not IP-only ones, none. If you need a network record for a workload, run it under an allowlist. In deny mode, domain is populated on both allowed and denied attempts, captured by the host's DNS- and TLS-SNI-aware egress filters described in Networking & egress before a packet ever reaches IP-level capture — a blocked DNS query never generates a routable packet, and an allowed spliced TLS connection never traverses the ordinary forwarding path either, so without this the deny-mode case would otherwise be invisible even though it was correctly enforced.

Payload fields (EgressPolicyUpdateData)

egress.policy_updated is a config-change record, not an observed connection — it fires once, after POST /v1/sessions/{id}/egress has actually taken effect on a live session, and carries both sides of the change so you don't have to diff it against an earlier session.created (or policy-update) event by hand.

FieldTypeNotes
previous_modestringThe mode (deny or allow) in effect immediately before this change.
previous_allow[]stringThe allowlist that went with previous_mode. Only meaningful when previous_mode is deny.
modestringThe mode now in effect.
allow[]stringThe allowlist that goes with mode. Only meaningful when mode is deny.

A request that fails to apply (the enforcer can't actually install a tightened policy) never reaches this event — the session's policy reverts and no event is emitted, so egress.policy_updated never claims a change that didn't genuinely take effect.

Why egress events aren't on the live event stream

GET /v1/events (the account-scoped SSE stream used for a live "what changed" feed) deliberately excludes egress.*: a busy deny-mode session can generate enough denied-connection attempts to flood a live per-connection stream, and that risk hasn't been closed off yet (gate behind server-side ?types= filtering once a console consumer exists). Egress events still show up in the historical export above — GET /v1/audit/events — they're just not pushed live.

Live-only transitional states

GET /v1/events also carries event types you will never see on this page's GET /v1/audit/events export or in the console's Logs view: session.creating, session.pausing, and session.resuming fire once, the moment a session enters that in-progress state — most usefully for a resume, which can take anywhere from milliseconds (a warm, same-node restore) to over a minute (restoring from a cold, cross-node archive) — so a client watching the stream can tell "another actor's transition is under way" apart from "the stream stalled," instead of seeing nothing until the matching session.created / session.paused / session.resumed event lands.

session.active_window_warning is the one member of this group that isn't a state entry: it fires a few minutes before the continuous-active-window backstop is about to force-pause (and, normally, immediately blip-resume) a session that has been live for the full window — a heads-up for an in-guest agent or process to wrap up before the blip force-closes any attached PTY, tunnel, or in-flight operation. The session itself does not change state when this fires; it stays live until the backstop's own pause a few minutes later.

These are a live-only UI hint, not part of the execution log this page otherwise documents: they are never hash-chained, never stored in the hot tier, and never replayed on an SSE reconnect. A GET on the session (or the terminal event, which is durable) is always the source of truth for where a session actually landed.

Refusal events

Two event types describe something that did not happen: the node was too full to take the work, and the disk gate refused it.

EventFires when
session.wake_refusedA paused session could not be woken — an explicit resume, an auto-resume, or a cold adopt — because its node is at or above the no-resume line, or because the account is at its concurrent live-session limit (reason: account_concurrency).
session.create_refusedA create, fork or restore was refused by the node's disk gate: the request did not fit above the node's reserve, the node is at or above the no-create line, or the node could not measure its own disk.

They exist because the party who needs to know is often not the caller. A preview URL is usually driven by an inbound webhook (a chat platform posting an update), so the application that holds the conversation never made the request and would otherwise see nothing — the room simply stops answering. Subscribe to these and you can tell the person on the other end "not right now, try again shortly" instead of going quiet. The same refusal reaches a direct caller as a 503 with code: "node_disk_pressure" and a Retry-After header.

Payload:

FieldTypeNotes
reasonstringbytes (the request itself did not fit above the node's reserve), pct (the node is at or above the ladder line), unmeasured (the node could not read its own disk, so nothing was decided on numbers), or account_concurrency (a wake refused because the account already has as many live sessions as its limit allows; the caller got a 429 with code: "account_concurrency_exceeded").
linestringno-resume for a refused wake, no-create for a refused create.
node_idstringThe node that refused. Useful to quote to support, and to tell "one node is full" from "the fleet is full".
used_pctnumberThe node's fullness at the moment of refusal. Absent on an unmeasured refusal — nothing was measured, and 0 would be a lie.
effective_avail_bytesnumberFree space the node could actually grow into. Absent on an unmeasured refusal for the same reason.
account_active, account_limitnumberThe account's live sessions on the node and its limit there. Present only on an account_concurrency refusal.
retry_after_secondsnumberThe same hint the Retry-After header carries, repeated here for a subscriber that never saw the response.
template, sizestringWhat was asked for. Present on session.create_refused, which usually has no session_id — the session was never created.

session_id is present on every session.wake_refused and absent from most session.create_refused events: there is no session to name when the create is what was refused. Two cases do carry one — a restore-in-place, which refuses against a session that already exists, and a create refused on the second, post-template-resolution disk check, by which point the session has an id and is being built.

These are debounced. A refused wake is retried aggressively by whatever is on the other end, so at most one session.wake_refused per session (and one session.create_refused per account, per node) is emitted per retry_after_seconds window. The event tells you a room is being refused now; it is not a log of every attempt. The refusals themselves are not debounced — every attempt is still refused, and still answered with the coded 503.

Bandwidth limits

Two independent caps sit alongside the allowlist and apply regardless of egress mode:

  • Per-VM bandwidth ceiling — a flat outbound rate cap per session, applied on the host with tc, default 100 Mbit/s. Configurable with INIS_EGRESS_CAP_MBIT (operator-side, not a per-request field). Sustained traffic over the cap is throttled, not blocked — a short burst above it is tolerated before shaping kicks in.
  • Per-account daily byte cap — a rolling UTC-day ceiling on total egress bytes across all of an account's sessions, default 10 GiB. Configurable with INIS_EGRESS_DAILY_CAP_GB (0 disables it). This one isn't a soft throttle: going over it suspends the account and destroys every live session on it — it's a hard stop against sustained abuse (bulk scraping, exfiltration, DDoS participation via forked sessions), not a rate limiter.

Both are fleet-operator settings today, not something you configure per session or per project.

On this page