Skip to content

Split 7 of 7: command line tools, episode export and the example app - #1798

Draft
martien-wdy wants to merge 12 commits into
split/6-uploadfrom
split/7-tools-and-example
Draft

Split 7 of 7: command line tools, episode export and the example app#1798
martien-wdy wants to merge 12 commits into
split/6-uploadfrom
split/7-tools-and-example

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 7 of 7, the last in the stack.

Cause

After chunk 6 a device captures episodes and uploads them, but there is no way for a person to list, download or inspect what it recorded, and no worked example of an app that uses the sensor-read entitlement.

Solution

Adds the read side and the illustrative material:

  • wendy data command line interface commands for listing sources and downloading episodes.
  • go/internal/episodeexport, which converts a stored episode into a playable form, and the episode-playable command that drives it. This package imports nothing else internal.
  • Examples/WendyDataModelApp, a reference app declaring the sensor-read and episode-write entitlements, with its Python client helpers and its tests.
  • Examples/WendyDataCampaign, a worked campaign.
  • The remaining documentation, including the wendy data command reference and the data platform demonstration walkthrough.
  • Two end to end test assertions in swift/WendyE2ETests updated for the new command surface.

Deliberately left to later chunks

Nothing. With this chunk merged the stack is byte for byte equal to feature/wendy-data-platform, verified with an empty git diff feature/wendy-data-platform..split/7-tools-and-example.

Depends on

Chunk 6 (split/6-upload), which this pull request is based on.

Verification

  • CC=/usr/bin/clang go build ./... succeeds.
  • CC=/usr/bin/clang go test ./go/internal/cli/commands/... ./go/internal/episodeexport/... ./go/cmd/episode-playable/... ./go/internal/shared/appconfig/... ./go/internal/agent/data/... passes.
  • gofmt -l go/ is empty and go vet is clean for the touched packages.
  • Diff size: 5209 lines.

Adds the read-side of the data platform: the wendy data commands, the
episode export package and its playable converter, the reference model
and campaign examples, and the remaining documentation.

Part 7 of 7 in the split of the Wendy Data Platform change. With this
chunk the stack is equivalent to feature/wendy-data-platform.
@github-actions

Copy link
Copy Markdown
Contributor

Swift E2E Review

⚠️ local-macos host setup blocked on sudo password prompt

The local-macos target failed before any Swift tests ran: the host-setup step tried sudo without a controlling terminal or askpass helper and aborted with sudo: a password is required. No tests executed on macOS (tests=0, observations=0). This is not caused by the diff on this branch — the diff only touches Go services, protos, docs, examples, and two Swift E2E specs (WendyBuildTests.swift, WendyDiscoverTests.swift), all of which passed on local-ubuntu-24.

Recommended next action: fix the macOS self-hosted runner so the Swift E2E host-setup step can obtain sudo non-interactively (passwordless sudoers entry for the runner user, SUDO_ASKPASS helper, or pre-install the required dependencies so no sudo is needed), and re-run the macOS attempt to confirm coverage returns.

Details

Observed vs. expected

  • Expected: local-macos runs the full Swift E2E suite and reports per-test outcomes, matching the 420 tests that ran on local-ubuntu-24.

  • Observed: the runner exited with status 1 after five log lines during host setup. Full attempts/local-macos/0001/attempt.log:

    ==> Running Swift E2E host setup
    ==> Setting up Swift E2E dependencies for macOS
    ==> Requesting sudo access
    sudo: a terminal is required to read the password; either use the -S option to read from standard input or configure an askpass helper
    sudo: a password is required
    

overview.json records local-macos as outcome=FAILED, attempts=1, tests=0, and the attempt marker is diagnosis-needed. Because no observations were produced, none of the changed behavior on this branch was exercised on macOS.

Likely root cause

The macOS Swift E2E host-setup script invokes sudo (to install/refresh dependencies) inside a non-interactive GitHub Actions runner session where there is no TTY and no SUDO_ASKPASS helper. sudo therefore has no way to prompt for a password and immediately fails. This is a runner/infrastructure problem, not a product regression: nothing in origin/main...HEAD modifies the workflow, the macOS setup path, or anything else that would change how sudo is invoked (see git-diff-name-only.txt — only Go, proto, docs, examples, and two Swift specs are touched; no .github/, no CI scripts, no macOS setup script).

Confidence

High that the failure is infrastructure (message is explicit) and that it is unrelated to this diff (no touched files could plausibly change host-setup behavior).

Concrete next steps

  • On the macOS self-hosted runner: grant the runner user passwordless sudo for the specific commands the setup script needs (/etc/sudoers.d/wendy-e2e), or export SUDO_ASKPASS pointing at a scripted helper before running setup.
  • Alternatively, pre-provision the dependencies during runner image build so the setup step no longer needs sudo at test time.
  • Once the runner is unblocked, re-run this workflow and confirm local-macos produces per-test observations comparable to local-ubuntu-24 (tests=420).
  • Consider making the runner-setup script fail fast with a clear message ("no TTY and no SUDO_ASKPASS; refusing to hang") instead of relying on sudo's default error, so future infra regressions are easier to spot.

Inspected artifacts

  • attempts/local-macos/0001/attempt.log

  • attempts/local-macos/0001/attempt.json

  • overview.json (summary + targets)

  • git-diff-name-only.txt, git-diff-stat.txt (to rule out diff-caused setup changes)

  • Scope: run

  • Reviewer: claude-opus-4-7

  • Confidence: high

  • Full review: review.claude-opus-4-7/local-macos-host-setup-blocked-on-sudo-password-prompt.md


Report artifact: swift-e2e-tests.gh32842026833.run.0001

slot["discarded"] += 1
slot["frame"] = frame
state.notify()
except BaseException as exc: # noqa: BLE001 - re-raised on the consumer thread
# Reached only once the stream stayed gone for the whole reconnect
# budget; frames_with_reconnect has already said why.
log.info("no more frames from %s; shutting down", source_id)
except KeyboardInterrupt:
Comment thread go/internal/cli/commands/data.go Fixed
@Joannis
Joannis marked this pull request as draft August 27, 2026 18:57
Moves the reference app's vendored proto copy to
proto/wendy/agent/apps/v1/ alongside the canonical move, and updates
the Dockerfile codegen path, the Python stub imports, the namespace
packages the unit test stubs out, the README, and the path constants in
TestExampleSensorProtoMatchesCanonical.
…ce app

Follows the entitlement rename in the shared app configuration. The reference
application is what an author copies, so it must model the rule rather than the
exception: its wendy.json now declares the sensor-read entitlement with an
allowlist naming the source it actually subscribes to, v4l2:/dev/video0, and
episode-write for its prediction and event records. The README says that the
allowlist has to match what `wendy data sources` prints on the device, because a
source outside it is neither listed nor subscribable, and records that only
camera sources are subscribable in this release.

The example's build-time copy of sensor_service.proto is refreshed from the
canonical Proto/ file, which the parity test requires.
…ty binding

An app that wants to use ordinary tooling (ffmpeg, GStreamer) cannot consume
the harness today: the sensors entitlement delivers frames only over gRPC as
SensorService.Subscribe, so any such tool needs a bespoke integration. The
camera entitlement hands over a raw device node instead, which makes the app an
independent second reader, so the frame a model scores is not provably the frame
the episode recorded.

This adds one producer with two planes. The data plane is a v4l2loopback node
fed from the SAME producer hub that episode capture and the gRPC subscribers
already consume, read with any standard tool. The control plane is a new
SensorService.SubscribeFrameIdentity stream carrying identity only, never
pixels.

The binding between them is not what the obvious design would suggest. Writing
sample_id into v4l2_buffer.sequence is impossible: v4l2loopback's QBUF handler
overwrites the sequence of any queued output buffer with its own write_position
counter, discarding whatever the writer supplied, and no option disables it. The
same handler does copy the buffer back out to userspace after stamping it and
before advancing the counter, so the writer learns the sequence the kernel
assigned. The binding is therefore inverted: the pump observes the kernel's
number and publishes the mapping, which needs no assumption about arrival order.

Scope is limited to what is sound. Only whole H.264 access units from the native
V4L2 producer are written frame-for-frame; unaligned byte-stream producers are
refused, and no decoder is inserted, because decoder reordering would break the
one-frame-in-one-frame-out correspondence the binding depends on.

The V4L2 OUTPUT writer is UNVERIFIED ON HARDWARE.
A ROS 2 graph has only ever been addressable as a whole DDS domain, so
there was no identifier for a single topic and no parser for one.

Add the grammar in the data package, which owns episode sources and is
the one package both the agent adapter that mints these identifiers and
the campaign resolver that matches against them can import.

The per-topic form is the domain form with ":" and the topic name
appended, "ros2:<rmw>:domain-<n>:<topic>", rather than a shorter shape
such as "ros2:<domain>:<topic>". The domain identifier is then literally
the prefix of every topic identifier on that domain, so one parser reads
both spellings and backwards compatibility is a property of the scheme
rather than a branch in the resolver. Keeping the Remote Middleware
Interface (RMW) name also keeps the identifier collision free: two RMW
implementations can be live on the same DDS domain number and they do
not interoperate, so "/chatter" on each is two different topics.

The topic is always the last field. ROS 2 graph names admit only
letters, digits, underscores and slashes, so a ":" cannot occur inside
one and everything after the third ":" is the topic however many slashes
it contains.
`wendy data sources` showed one opaque entry per Data Distribution
Service (DDS) domain, and a campaign naming it recorded the entire
domain because the adapter always ran `ros2 bag record -a`. On a real
robot that is every camera topic and every high-rate inertial
measurement unit topic whether they were wanted or not, and there was no
way to see what the graph even offered without leaving Wendy for ROS 2
tooling.

Discover now enumerates the graph and emits one source per topic, with
the message type as the Detail so the DETAIL column says what the topic
carries. The enumeration reuses the existing `ros2 topic list -t` path,
which returns names and types in a single exec, so there is no second
enumerator. The domain-level source stays listed as the "record
everything on this graph" handle.

Start groups the selected sources by domain and runs one recorder per
domain: `-a` when the domain-level identifier was selected, otherwise
`ros2 bag record <topic>...` for exactly the topics named. One
invocation rather than one per topic, because each rosbag2 process pays
its own DDS discovery and produces its own bag and its own clock
mapping, and splitting a domain across several would leave messages that
share one timeline with several independent mappings. Every selected
source still gets its own CaptureResult, so the manifest accounts for
what the campaign named.

Healthy is now derived rather than hardcoded true. A domain whose topic
listing fails enumerates as unhealthy with the failure in its Detail,
instead of advertising a dead graph as recordable.

Discovery is cached with a five second time to live, keyed by sidecar
identity and droppable outright through invalidateDiscovery. `ros2 topic
list -t` costs about 0.6 seconds beyond the command-line baseline on
hardware and does not grow with topic count, and Discover runs once for
`wendy data sources` plus twice more for a single campaign trigger.
Publisher and subscriber counts are deliberately not fetched: that path
is a per-topic `ros2 topic info` exec measured at roughly 0.8 seconds
per bounded round, so it would cost ten seconds or more on a
hundred-topic robot and answers no question involved in choosing what to
record.
Campaign YAML has documented `ros2: <topic>` since the campaign format
existed, but ResolveCampaignSources ignored the value and selected every
healthy ROS 2 source, which was the whole DDS domain. A plan asking for
the lidar and the joint states recorded the entire graph.

A topic selector now resolves to that topic's own source on every
healthy graph publishing it, so the episode records that topic and
nothing else. A full per-topic identifier resolves to that one source. A
domain-level identifier, with or without the "ros2:" prefix, resolves to
the whole domain and still records with `-a`, as does any unrecognized
value, which is exactly what every selector did before. No deployed plan
changes meaning except the topic selectors, which now mean what they
say.

A topic selector naming a topic no healthy graph publishes is an error
rather than a fallback to the whole domain. Falling back would restore
the behaviour this change exists to remove, and the alternative failure
is the one this package already refuses for audio selectors: a sealed,
checksummed, uploaded episode holding none of the data the plan asked
for.
Two-plane camera access: loopback data plane fed from the producer hub,
frame identity over the sensor socket
Per-topic ROS 2 sources: topic-addressable capture, derived health,
cached discovery
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.

1 participant