Skip to content

feat(observability): include the structured OCSF event in sandbox log lines served by the gateway API #4080

Description

@saichandrapandraju

User Story

As a developer building automated tooling on the OpenShell gateway API (for example, an evaluation harness that grades what an agent did inside a sandbox), I want to read each OCSF event for a sandbox as a structured OCSF record through the same API I already use for sandbox logs (GetSandboxLogs, WatchSandbox, and the SDKs' log methods), so that my tooling consumes OpenShell's audit events as data, works the same on every compute driver, and doesn't break when the human-oriented shorthand format changes.

Problem Statement

The supervisor builds a full OCSF v1.8.0 event for every network, HTTP, process, finding and config event, but the gateway API exposes only its shorthand text rendering.

When the supervisor pushes an OCSF event to the gateway, it sends format_shorthand() as the message and deliberately leaves SandboxLogLine.fields empty (log_push.rs, asserted by ocsf_events_push_shorthand_with_ocsf_level_and_no_fields). Every read path is built on that message, so all of them return the shorthand string: GetSandboxLogs, WatchSandbox, openshell logs, Rust watch_logs, and the Go SDK.

The structured record exists only in the opt-in JSONL file (ocsf_json_enabled), and that file is written inside the supervisor's own filesystem (/var/log/openshell-ocsf.*.log). No gateway API serves it.

Impact / Why This Matters

Without this, an API client that needs event details must parse the shorthand text, for example:

NET:OPEN [MED] DENIED /usr/bin/curl(0) -> audit.ext-log.com:443 [reason:transparent_tcp_policy_denied]
NET:REFUSE [MED] DENIED audit.ext-log.com [reason:policy_dns_ineligible]

Proposed Design

When a client reads sandbox logs through the gateway API, each OCSF log line can also carry the complete OCSF JSON record for that event, alongside the existing shorthand message.

  • The shorthand message, the level (OCSF) and all current behavior stay unchanged, so existing clients are unaffected.
  • The structured record is the same OCSF record the JSONL sink writes, respecting the configured ocsf_schema_version.
  • It's opt-in, to keep the default log stream small, either through the existing ocsf_json_enabled setting (per sandbox or global) or through a request option on GetSandboxLogs / WatchSandbox. Maintainers can choose which.
  • It's available on every compute driver, because it travels the existing supervisor-to-gateway log path rather than a file.
  • The SDKs and openshell logs can surface it (for example, a JSON output mode in the CLI).

This is not a request for export to external destinations (#2762). It makes the existing log API a machine-readable source that other tools can consume.

Acceptance Criteria

  • With the feature enabled, every OCSF line returned by GetSandboxLogs and WatchSandbox for a sandbox includes its complete OCSF record as JSON.
  • The record matches what the JSONL sink would write for the same event, including class_uid, action / disposition, status_detail, dst_endpoint, actor.process (with the parent process), firewall_rule and container.
  • With the feature disabled (the default), log responses are unchanged.
  • It works on the Docker, Podman, Kubernetes and VM drivers.
  • The Python, Rust and Go SDKs, and openshell logs, expose the structured record.
  • The documentation states which representation is the stable machine-readable contract.

Alternatives Considered

  1. Parse the shorthand text (what we do today). It works, but it's fragile and lossy, as described above, and every consumer re-implements the parser.
  2. Read the JSONL file from the supervisor's filesystem. It's structured, but it requires driver-specific access outside the OpenShell API, and it doesn't work on every driver (ocsf_json_enabled silently writes nothing with the MicroVM driver (supervisor cannot open /var/log) #3855, OCSF JSONL enabled and OCSF events emitted, but /var/log/openshell-ocsf.* is not created on Docker Desktop / WSL2 #3895).
  3. Flatten selected fields into SandboxLogLine.fields. This is better than text, but nested OCSF objects (actor, parent process, endpoints) don't fit a flat string map well. The full record is simpler and loses nothing.
  4. Gateway export to SIEM destinations (feat(observability): OCSF event export from gateway to external SIEM destinations #2762). Declined as out of scope. This proposal is much narrower and fits the stated direction of OpenShell producing a source of OCSF events that other tools consume.

Agent Investigation

Findings from reading v0.1.2 branch, confirmed against a live 0.1.2 gateway with the Podman driver:

  • crates/openshell-supervisor-process/src/log_push.rs (LogPushLayer::on_event, around line 57): for OCSF_TARGET events, clone_current_event() already returns the full event. The layer sends format_shorthand() with an empty fields map. to_json() (crates/openshell-ocsf/src/forma t/jsonl.rs) is available on the same value.
  • proto/openshell.proto SandboxLogLine has a fields map documented as "structured key-value fields from the tracing event", which is empty for OCSF events.
  • crates/openshell-supervisor/src/main.rs (around lines 345–360): the JSONL sink (OcsfJsonlLayer) writes to /var/log/openshell-ocsf.* in the supervisor's filesystem. In 0.1.x that's the supervisor container, not the workload, so ExecSandbox can't read it. We read it with podman cp.
  • crates/openshell-server/src/tracing_setup.rs: the gateway-side JSONL sink exists only for Windows/MXC. A code comment notes that "a cross-platform sink needs an explicit storage and configuration contract."
  • Live check (0.1.2, Podman): with ocsf_json_enabled=true on one sandbox, a blocked curl produced this JSONL record. The API returned only the corresponding shorthand line.
{"class_name":"Network Activity","activity_name":"Open","action":"Denied","disposition":"Blocked",
 "status_detail":"transparent_tcp_policy_denied",
 "dst_endpoint":{"domain":"audit.ext-log.com","ip":"198.18.0.42","port":443},
 "actor":{"process":{"name":"/usr/bin/curl","pid":0,"parent_process":{"name":"/usr/bin/bash","pid":0}}},
 "container":{"name":"eval-72e144c9d8","uid":"1d25f83b-…","image":{"name":"localhost/document-assistant-local:latest"}}}

Checklist

  • I've reviewed existing issues and the published docs
  • This is a design proposal, not a "please build this" request

Activity

  1. shalevyoni-ops commented on Oct 10, 2026

    @shalevyoni-ops

    One observation on the gateway-side file route, since this issue's "Alternatives Considered" covers the file sinks as the non-API fallback.

    On the 0.1.3 dev build, with the gateway-side sink configured, no file was produced in one run:

    • Kernel 6.8.0-142-generic, 4 vCPU, OpenShell installed directly on the host (no WSL, no nested container)
    • openshell-gateway 0.1.3-dev.136+ge1f3c82ca; the config below is accepted by this version (rejected as an unknown field on 0.1.2)
    • [openshell.gateway.ocsf_log] with path = "/var/log/openshell/gateway-ocsf.jsonl"; the directory existed before the gateway started, mode 0777
    • A sandbox created by this gateway (2026-10-09 10:26:30 UTC) emitted events — openshell logs showed HTTP:POST ×4, NET:OPEN ×3, SSH:OPEN ×3, CONFIG:PUBLISHED ×2, FINDING:CREATE and others
    • Afterwards, /var/log/openshell/ on the host contained no files

    Not established: whether a different event mix would have produced output (the docs name gateway-wide events such as TLS certificate reloads, which did not occur in this run); the cause — I have not read the source; anything about the sandbox-side file, which I did not measure correctly and am not relying on here.

    • Whether the gateway was restarted after the config was added. The
      recipe calls for it, but no file in my evidence records a gateway
      start or restart time, so I cannot assert it happened.
    • My sandbox policy also registered a gRPC middleware for the HTTP
      endpoint. I did not test without it, so I cannot say whether its
      presence matters.

    The host is destroyed, so I cannot re-run this. Posting it because if the gateway-side sink also does not produce a file in this configuration, the API route this issue proposes is the only driver-independent one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    state:triage-neededOpened without agent diagnostics and needs triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions