Skip to content

Split 4 of 7: read-only sensors entitlement and per-app sockets - #1795

Draft
martien-wdy wants to merge 5 commits into
split/3-protosfrom
split/4-sensors-entitlement
Draft

Split 4 of 7: read-only sensors entitlement and per-app sockets#1795
martien-wdy wants to merge 5 commits into
split/3-protosfrom
split/4-sensors-entitlement

Conversation

@martien-wdy

@martien-wdy martien-wdy commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Problem

See chunk 1 for why pull request #1751 is being split. This is chunk 4 of 7, and it is the security sensitive one: it is the reason the whole split exists. It adds a new entitlement and two new Unix domain sockets, and in the unsplit pull request neither could have been reviewed by the Claude Security Review job at all.

Cause

A model running in an app container needs sensor input. Today the only way to get it is the camera entitlement, which hands the container a raw /dev/videoN device node with the device cgroup rules that go with it, and which is single holder: an app that takes the camera takes it away from episode capture. That is far more authority than "read frames" requires, and it puts the app and the agent in conflict over the device.

Solution

Adds a narrow, read-only sensor-read entitlement and the two per-app sockets that carry the data platform's grants.

The trust boundary

  • Identity comes from the mount, never from the request. Each app gets its own host directory containing exactly one socket, bind mounted read-only with nosuid and noexec into the container. The SensorService and the data socket handler bind one instance to one app identity, taken from the socket the request arrived on. Nothing in a request body can name a different app. This is the same construction the existing System API socket uses, and the directory rather than the socket inode is mounted so the agent can recreate the socket under a running container.
  • The sensor grant adds no device nodes. applySensorSocket adds a bind mount, a supplementary group and one environment variable. It adds no device nodes and no device cgroup rules. An app that genuinely wants the raw device still asks for camera and still gets the single holder semantics that come with it.
  • No new device contention. A sensors subscriber joins the same multiplexing producer episode capture consumes, so the app and the agent never fight over a device.
  • The allowlist is enforced live, at the service. The entitlement takes an optional allowlist of source identifiers, mirroring what the camera entitlement's allowlist does for device nodes. SensorService.permits is consulted on every enumeration and every subscribe, not cached at construction, because a multi service app's owner set changes while the socket keeps serving. An app that declares an allowlist can neither see nor subscribe to any other source on the device. An entitlement with no allowlist is unrestricted, which is the documented meaning.
  • Peer credentials are read at accept time. The data socket reads SO_PEERCRED when it accepts. It fails open only in the cases where the connection structurally carries no peer identity to read, which are a non Unix socket or a platform with no SO_PEERCRED, and those cases are named explicitly rather than being a silent fallthrough.
  • Bounded by construction. Records are size capped, the per-app record rate is capped across all of that app's connections combined, subscriptions per app are capped, and receive and send message sizes are set deliberately rather than inheriting the generous gRPC defaults.

Deliberately left to later chunks

Nothing wires these sockets into the agent here. main.go wiring, the camera producer that actually satisfies the sensor provider interface, and the episode capture path all arrive in chunk 5. This chunk is deliberately kept to the grant and its enforcement so the security review has a small surface to look at.

Seam note

sensor_service.go and sensor_service_test.go were moved back from chunk 5 into this chunk. app_sensor_socket.go cannot compile without them, and on reflection they belong here anyway: SensorService is where the entitlement allowlist is enforced, so the grant and its enforcement point are now reviewed together.

Examples/WendyDataModelApp/wendy.json and go/internal/shared/appconfig/wendy_data_model_app_test.go were left in chunk 7. That test only guards the shipped example against schema drift and needs three example files as fixtures; the entitlement's own unit coverage is appconfig_sensors_test.go, which is in this chunk.

Depends on

Chunk 3 (split/3-protos), which this pull request is based on.

Verification

  • CC=/usr/bin/clang go build ./... succeeds.
  • CC=/usr/bin/clang go test ./go/internal/agent/services/... ./go/internal/shared/appconfig/... ./go/internal/agent/oci/... ./go/internal/agent/containerd/... ./go/internal/cli/... passes. The command line interface packages are included because this chunk changes embedded documentation assets.
  • gofmt -l go/ is empty and go vet is clean for the touched packages.
  • Diff size: 2978 lines, comfortably inside the 20000 line ceiling, so the Claude Security Review job can run.

Adds the narrow, read-only "sensors" entitlement and the two per-app
Unix domain sockets that carry it: a data socket for an app writing into
its own episodes, and a sensor socket for an app subscribing to sensor
sources.

App identity comes from the private bind mount the socket lives on, never
from the request body, matching the existing System API socket. The
sensors entitlement carries an optional allowlist that the SensorService
consults live, so an app that declares one can neither enumerate nor
subscribe to any other source on the device.

Part 4 of 7 in the split of the Wendy Data Platform change.
Comment on lines +52 to +62
// EntitlementSensors grants read-only subscription to agent-hosted sensor
// streams over an app-private socket that serves nothing else. It is the
// first-class model-input path: the app becomes one more subscriber of the
// same producer episode capture consumes, so the two never fight over a
// device, and every sample the app receives is recorded into the active
// episode under the identifier the app was given. It grants no device
// nodes; raw device access remains the separate "camera" entitlement. An
// optional allowlist restricts it to named source ids, mirroring what the
// camera entitlement's allowlist does for device nodes; without one the app
// may subscribe to any sensor source the device offers.
EntitlementSensors = "sensors"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of sending sensor data, it might be more effective and efficient for the campaign (the agent) to fetch or poll the latest state by calling or tailing a file descriptor created by the app.

Comment on lines +50 to +62
// EntitlementData grants only the app-private episode event socket.
EntitlementData = "data"
// EntitlementSensors grants read-only subscription to agent-hosted sensor
// streams over an app-private socket that serves nothing else. It is the
// first-class model-input path: the app becomes one more subscriber of the
// same producer episode capture consumes, so the two never fight over a
// device, and every sample the app receives is recorded into the active
// episode under the identifier the app was given. It grants no device
// nodes; raw device access remains the separate "camera" entitlement. An
// optional allowlist restricts it to named source ids, mirroring what the
// camera entitlement's allowlist does for device nodes; without one the app
// may subscribe to any sensor source the device offers.
EntitlementSensors = "sensors"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should merge these 2 entitlements

@Joannis Joannis left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See split 3/7 for the remainder of review

@Joannis
Joannis marked this pull request as draft August 27, 2026 18:57
…ps.v1

Follows the proto package move in chunk 3. The per-app socket now
registers appspbv1.SensorService, and the defence-in-depth method
allowlist on that socket matches the new fully qualified name
/wendy.agent.apps.v1.SensorService/ rather than the old control-plane
name, which would otherwise have rejected every call.
@martien-wdy

Copy link
Copy Markdown
Contributor Author

On the file descriptor idea; a unix socket already is one, so the difference is really who initiates and whether we keep typing and backpressure. I'd rather keep the app pushing over gRPC, because the connection opening and closing is exactly where we learn the publisher went away, and a tailed file gives us no liveness signal at all.

…ire an allowlist

Problem: the two entitlements the Wendy Data Platform work added, "sensors" and
"data", were named for their subject rather than their direction, and a reviewer
read them as duplicates and proposed merging them. They are not duplicates. They
run in opposite directions and carry very different risk: "sensors" is a
read-only subscription to agent-hosted sensor streams (in practice, permission
to see the cameras and microphones), while "data" writes the app's own event and
prediction records into the device's episode recorder (in practice, permission
to write into the recorded dataset, and stronger than it looks, because campaign
triggers match on application event names and prediction attributes, so an app
holding it can start recordings). Merging them would give anything that wants to
log an event the camera, and anything that wants to read a sensor the training
corpus.

Two further holes sat behind the names. The sensor allowlist was optional, and
omitting it granted every subscribable source, so an app declaring the bare
entitlement got the cameras today and would silently gain microphones and the
Robot Operating System 2 (ROS 2) graph the day those sources became
subscribable. And subscribing to a source whose kind has no producer hub failed
with "not available to model subscribers", which reads like the source does not
exist.

Solution:

- "sensors" becomes "sensor-read" and "data" becomes "episode-write", so the
  direction and the object are both visible in the name. The Go constants are
  EntitlementSensorRead and EntitlementEpisodeWrite. Neither name has shipped,
  so there is deliberately NO alias for the old spellings: an alias would leave
  both spellings in the world and defeat the rename. A test pins that the old
  spellings are rejected.
- The sensor-read allowlist is now required and must name at least one source.
  The JSON schema marks it required with minItems 1, appconfig validation
  rejects a missing, empty, or blank-entry allowlist with a message naming the
  fix, and AppSensorSocketManager.Ensure refuses an owner without one so the
  agent does not depend on the command line interface having validated first.
  appSensorSocket.permits now fails closed on an owner with no allowlist rather
  than treating it as a blanket grant.
- Subscribing to a source kind that has no producer hub now returns Unimplemented
  naming the kind and saying it is not subscribable in this release, and a source
  the device does not have at all returns NotFound saying so. Only camera sources
  have a producer hub today.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants