* feat(workspace): implement workspace model (Phase 1 of RFC 0011)
Implements workspace and membership model providing hard isolation
boundaries for multi-player OpenShell deployments.
Workspace CRUD with Kubernetes-style Terminating phase for graceful
deletion. All resources scoped by workspace via ObjectMeta. Membership
RPCs for workspace access control. Persistence migration shifts name
uniqueness to (object_type, workspace, name). Provider profiles support
platform and workspace scoping. Service routing uses workspace-prefixed
DNS labels. Inference routes renamed and workspace-scoped with
DeleteInferenceRoute RPC. Python SDK with WorkspaceClient, two-method
list pattern (workspace-scoped and for_all_workspaces), and workspace
parameter on all methods. CLI workspace flags, TUI workspace cycling.
K8s driver filters unmanaged CRs and uses delete preconditions. Podman
driver uses immutable container IDs. Label serialization fixed across
all put_if call sites.
Signed-off-by: Derek Carr <decarr@redhat.com>
* fix(cli): delegate sandbox upload command to existing upload function
The standalone `sandbox upload` command reimplemented upload logic
inline with two bugs: it used `Path::exists()` which follows symlinks
(rejecting dangling symlinks), and it ran git-aware filtering on
symlink sources. The `run::sandbox_upload()` function already handles
both cases correctly via `sandbox_upload_plan()`. Replace the inline
logic with a call to the existing function.
Signed-off-by: Derek Carr <decarr@redhat.com>
* fix(e2e): shorten sandbox names and fix test compatibility
Shorten the sandbox name in initial_sparse_policy_is_acknowledged_as_loaded
from 'e2e-2159-sparse-enrich' (22 chars) to 'e2e-sparse-enrich' (17 chars)
to comply with MAX_ROUTABLE_NAME_LEN (19 chars).
Also capture stderr in create_keep_with_args so future sandbox creation
failures include the actual CLI error instead of reporting empty output.
Signed-off-by: Derek Carr <decarr@redhat.com>
* test(workspace): add test coverage for workspace CRUD and persistence isolation
Add unit tests for workspace create happy path, get round-trip, get
not-found, get empty-name rejection, already-exists error, and
resolve_workspace not-found. Add persistence test proving cross-workspace
name uniqueness (same name in different workspaces produces separate
records). Add workspace name max-length boundary tests. Fix e2e harness
to include stderr in name-parse-failure error path. Align Python e2e
test_workspace_crud with try/finally pattern. Document provider profile
catalog workspace scoping gap in RFC 0011.
Signed-off-by: Derek Carr <decarr@redhat.com>
* fix(examples): update examples for workspace model compatibility
Shorten sandbox names in demo scripts to fit the 19-character
MAX_ROUTABLE_NAME_LEN limit: policy-demo prefix to pd-, multi-agent
notepad derives a short SANDBOX_TAG from the run ID, governance
interceptor uses gs-PID-RANDOM. Update vscode-remote-sandbox.md SSH
host aliases from openshell-{name} to openshell-{name}.{workspace}
format.
Signed-off-by: Derek Carr <decarr@redhat.com>
* feat(sdk): add workspace-scoped client and workspace CRUD
Add WorkspaceScopedClient modeled after kube::Api::namespaced — captures
workspace once and injects it into every sandbox request. Add workspace
CRUD methods (create, get, list, delete) and list_sandboxes_all_workspaces
on OpenShellClient. Extend SandboxRef with workspace field and add
WorkspaceRef type. Include mock tests for all new operations.
Signed-off-by: Derek Carr <decarr@redhat.com>
* fix(lint): resolve clippy warnings in workspace test assertions
Signed-off-by: Derek Carr <decarr@redhat.com>
* fix(docs): convert indented code blocks to fenced in RFC 0011
Signed-off-by: Derek Carr <decarr@redhat.com>
* fix(lint): resolve clippy warnings and apply cargo fmt across workspace
Auto-format with cargo fmt and fix clippy warnings exposed by the
reformat: unnecessary qualifications, map_unwrap_or, identical match
arms, unused variable prefix, dead code annotations, and let-unit-value
in e2e harness.
Signed-off-by: Derek Carr <decarr@redhat.com>
* fix(workspace): address workspace scoping issues from review
- Add workspace field to settings JSON output (CLI)
- Skip Podman containers missing workspace label instead of defaulting
to empty string, matching K8s driver behavior
- Add resource_version to list_by_scope SELECT in both SQLite and
Postgres backends, with regression test
- Gate PolicyLocalContext proposal/lookup routes on workspace readiness,
returning 503 when workspace is not yet discovered
- Block sandbox and provider creation in TUI all-workspaces mode
- Clear workspace vectors in TUI reset_sandbox_state
Signed-off-by: Derek Carr <decarr@redhat.com>
* fix(workspace): make provider profile catalog workspace-aware
Thread workspace through snapshot_catalog so the
EffectiveProviderProfileCatalog enforces workspace boundaries on both
read and write paths. UserProviderProfileSource now loads platform-scoped
profiles (workspace "") plus the target workspace's profiles, preventing
cross-workspace duplicate profile ID collisions that previously caused
global catalog failures.
Update RFC 0011 to reflect catalog scoping is implemented in Phase 1
rather than deferred to future work.
Signed-off-by: Derek Carr <decarr@redhat.com>
* fix(persistence): include workspace column in atomic policy revision INSERT
put_policy_revision_atomic omitted the workspace column from the INSERT
into the objects table in both SQLite and Postgres backends, causing
atomically-written policy revisions to lose their workspace association.
Add workspace field to AtomicPolicyRevisionWrite and thread it through
both backend INSERT statements, matching the non-atomic put_policy_revision
path which already included it.
Signed-off-by: Derek Carr <decarr@redhat.com>
* fix(proxy): skip ancestor walk when socket owner is the entrypoint
collect_ancestor_identities walked the entire process tree above the
entrypoint when the connecting process was the entrypoint itself,
SHA256-hashing every ancestor binary (IDE, shell, container runtime).
On dev machines with large binaries in the ancestor chain this exceeded
the 30-second test timeout. When start_pid == stop_pid there are no
intermediate ancestors to verify, so return an empty list immediately.
Signed-off-by: Derek Carr <decarr@redhat.com>
* fix(workspace): make provider profile catalog scope-aware
Allow the same profile ID at platform and workspace scopes by
introducing layered catalog entries where workspace profiles shadow
platform profiles. Add source and scope fields to the ProviderProfile
proto and CLI output. Migrate List/Get handlers to the catalog,
fixing divergence with runtime profile resolution.
Signed-off-by: Derek Carr <decarr@redhat.com>
* fix(e2e): align podman e2e labels with centralized driver constants
The podman driver moved its container labels to the centralized
openshell.ai/ prefix, but the e2e test harness and cleanup script
still referenced the old openshell.sandbox-* keys, causing the
local_driver_token_restart test to fail on container lookup.
Signed-off-by: Derek Carr <decarr@redhat.com>
* fix(e2e): align python profile isolation test with scope-aware catalog
Platform profiles are now visible in workspace listings as fallbacks
per the layered catalog design. Update the assertion to match.
Signed-off-by: Derek Carr <decarr@redhat.com>
* fix(workspace): honor profile_workspace in runtime profile resolution
Runtime profile lookups now consult provider.profile_workspace via
get_type_profile_for_scope. Providers created with --global-profile
(profile_workspace="") resolve to the platform profile even when a
workspace profile shadows the same ID. All 6 runtime call sites
updated; type-only call sites remain scope-agnostic.
Signed-off-by: Derek Carr <decarr@redhat.com>
---------
Signed-off-by: Derek Carr <decarr@redhat.com>
Add gateway-managed AWS STS credential refresh (provider-v2, #1576). The
gateway calls sts:AssumeRole and writes three short-lived credentials
(AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN) to the
provider record; the proxy re-signs requests with SigV4. Adds the aws and
aws-s3 provider profiles and a declarative multi-output refresh model
(additional_outputs) so one AssumeRole co-mints all three credentials.
Signed-off-by: Russell Bryant <rbryant@redhat.com>
* feat(tui): navigate panels via Up/Down arrow overflow at list boundaries
When at the bottom of a panel's item list, pressing Down/j now moves
focus to the next panel instead of being a silent no-op. Likewise,
pressing Up/k at the top moves to the previous panel with the cursor
on its last item. Empty panels are skipped and the ring wraps around.
Closes#2273
Signed-off-by: Varsha Prasad Narsing <vnarsing@nvidia.com>
Signed-off-by: Varsha Prasad Narsing <varshaprasad96@gmail.com>
* fix(tui): guard Up handlers against stale cursor in empty panels
The Down handlers already check whether the list is non-empty before
incrementing the cursor, but the Up handlers only checked cursor > 0.
When a list becomes empty after a refresh with a nonzero cursor, Up
would decrement the stale cursor instead of overflowing to the
previous panel. Add the same non-empty guard to all four Up arms.
Signed-off-by: Varsha Prasad Narsing <varshaprasad96@gmail.com>
* docs(sandboxes): add dashboard keyboard navigation to manage-sandboxes
Describe Tab/Shift+Tab panel cycling, Up/Down and j/k boundary
overflow, and middle-pane tab switching in the OpenShell Terminal
section.
Signed-off-by: Varsha Prasad Narsing <varshaprasad96@gmail.com>
---------
Signed-off-by: Varsha Prasad Narsing <vnarsing@nvidia.com>
Signed-off-by: Varsha Prasad Narsing <varshaprasad96@gmail.com>
* fix(tui): redraw after sandbox shell exits
Closes#2229
Render the restored terminal before synchronous gateway refreshes and propagate terminal lifecycle failures from both SSH handoff paths.
Signed-off-by: John T. Myers <9696606+johntmyers@users.noreply.github.com>
* fix(tui): restore input after sandbox shell
Discard stale events accumulated around the suspended TUI and let the normal periodic tick refresh state after resuming.
Signed-off-by: John T. Myers <9696606+johntmyers@users.noreply.github.com>
---------
Signed-off-by: John T. Myers <9696606+johntmyers@users.noreply.github.com>
* fix(tui): route warning logs to status bar instead of stderr
tracing::warn/info/debug calls in the TUI crate write to stderr via
the global tracing subscriber. In ratatui's alternate-screen/raw-mode,
stderr writes corrupt the terminal layout. Error-state sandboxes
amplify this as background gRPC polls fail every 2s tick.
Replace all 25 tracing calls with app.status_text assignments for
direct-access sites, and Vec<String> accumulation for the spawned
start_port_forwards task. Add ForwardWarnings event variant to
decouple forward warning delivery from the sandbox name in
CreateResult, preventing downstream gRPC lookup failures.
Closes#2120
Signed-off-by: Ian Miller <milleryan2003@gmail.com>
* feat(tui): add ForwardWarnings event variant
New event type for non-fatal port-forward warnings during sandbox
creation. Keeps warning delivery separate from the sandbox name
in CreateResult to avoid corrupting downstream gRPC lookups.
Signed-off-by: Ian Miller <milleryan2003@gmail.com>
* chore(tui): remove tracing dependency
Compile-time guard against reintroducing stderr-writing tracing
calls in the TUI crate. 14 other crates retain the dependency.
Signed-off-by: Ian Miller <milleryan2003@gmail.com>
---------
Signed-off-by: Ian Miller <milleryan2003@gmail.com>
* feat(bootstrap): add system gateway registry for installer defaults
Adds a read-only installer-seeded gateway registry that the CLI consults after per-user gateway config. The registry uses the same layout as per-user config with `active_gateway` at the root and `gateways/<name>/metadata.json` beneath it. By default the system config root is `/etc/openshell`, while `OPENSHELL_SYSTEM_GATEWAY_DIR` remains available as an override for packages that need a different location. User-managed gateways continue to shadow installer entries on name collision.
Originally-authored-by: Mark Shuttleworth <mark@ubuntu.com>
Signed-off-by: Alex Lewontin <alex.lewontin@canonical.com>
* feat(cli): show gateway config source in list and term
Expose whether a gateway registration comes from user or system config in `openshell gateway list`, the TUI gateway pane, and list JSON output. The CLI also refuses to remove system-managed registrations and the smoke tests cover the new list output.
Signed-off-by: Alex Lewontin <alex.lewontin@canonical.com>
* fix(bootstrap): preserve user shadowing on invalid metadata
Signed-off-by: Alex Lewontin <alex.lewontin@canonical.com>
* fix(cli): keep system fallback out of rollback state
* docs(gateway): describe system config fallback layout
* test(bootstrap): cover system gateway last_sandbox persistence
* test(gateway): cover system-only removal rejection
* fix(bootstrap): validate gateway names before path joins
---------
Signed-off-by: Alex Lewontin <alex.lewontin@canonical.com>
* feat(providers): support SPIFFE-backed token grants
Add provider profile token_grant metadata and expand endpoint-specific
dynamic credentials so sandbox supervisors can request SPIFFE JWT-SVIDs,
exchange them with an OAuth-style token endpoint, cache returned access
tokens, and inject bearer tokens into matching HTTP requests.
Wire Kubernetes and Helm deployments to mount the provider SPIFFE Workload
API socket into sandbox pods for token grant exchange.
Signed-off-by: Taylor Mutch <taylormutch@gmail.com>
Signed-off-by: Gordon Sim <gsim@redhat.com>
* test(examples): add SPIFFE token grant demo
Add a reusable alpha/beta demo that deploys a SPIFFE-verifying token issuer
and protected services, imports a token-grant provider profile, creates a
sandbox, and verifies endpoint-specific bearer tokens.
The script leaves Kubernetes workloads in place, deletes sandboxes through
openshell unless KEEP_SANDBOX=1, and prints protected service logs as proof
of life.
Signed-off-by: Taylor Mutch <taylormutch@gmail.com>
* fix(providers): harden SPIFFE token grants
* fix(providers): harden dynamic token grants
Signed-off-by: Taylor Mutch <taylormutch@gmail.com>
* fix(providers): harden token grant handling
---------
Signed-off-by: Taylor Mutch <taylormutch@gmail.com>
Signed-off-by: Gordon Sim <gsim@redhat.com>
* feat(telemetry): add build-time option to compile out telemetry
Gate anonymous telemetry emission behind a default-on `telemetry` Cargo
feature in openshell-core. The data model (enums, validation, emit_*/enabled*
signatures) stays always-compiled, while the endpoint, HTTP client, queue, and
emission code are feature-gated. With the feature off, enabled() returns false
and emit_* are no-ops, so dependent crates compile unchanged and no telemetry
endpoint, HTTP client, or emission code is included in the binary.
chrono and reqwest become optional dependencies of openshell-core, dropped from
its dependency graph when telemetry is disabled.
Thread the switch through the workspace: every crate depends on openshell-core
with default-features = false, and the default-on `telemetry` passthrough lives
on the binary crates that emit or collect telemetry (openshell-server,
openshell-sandbox, openshell-driver-vm). In-process drivers inherit it via
resolver v2 feature unification.
Build a telemetry-free binary with, e.g.:
cargo build --release -p openshell-server --no-default-features
The runtime OPENSHELL_TELEMETRY_ENABLED switch is unchanged for default builds.
Signed-off-by: Russell Bryant <russell.bryant@gmail.com>
* ci(telemetry): guard that telemetry can be compiled out
Add tasks/scripts/verify-telemetry-compiled-out.sh, which inspects a built
binary for telemetry markers (the telemetry endpoint host and client ID) that
exist only when emission code is compiled in. The rust:verify:telemetry-off
mise task builds the gateway with default features (positive control: markers
must be present, so the absent checks can never be silently vacuous) and with
--no-default-features (markers must be absent), and checks the
--no-default-features sandbox binary as well.
Wire the task into the Rust branch-checks job so a regression that reintroduces
telemetry code into a --no-default-features build fails CI.
Signed-off-by: Russell Bryant <russell.bryant@gmail.com>
---------
Signed-off-by: Russell Bryant <russell.bryant@gmail.com>
The openshell-tui UI module had two private draw_text_field functions
with near-identical logic in create_sandbox.rs and create_provider.rs
(differing only in cursor glyph and whether a gap row is shown), and
four confirm-dialog rendering blocks duplicated across global_settings.rs
and sandbox_settings.rs.
Extract both patterns into module-level helpers in ui/mod.rs:
- draw_text_field: accepts cursor (&str) and show_gap (bool) to cover
both callers; create_sandbox uses '█'/true, create_provider uses '_'/false.
- draw_confirm_popup: takes the already-built lines vec, a border style,
a width, and the enclosing area, and owns the Clear + Block + Paragraph
rendering that was duplicated in all four confirm functions.
Remove the now-redundant imports (Clear, Paragraph) from the settings
files and drop the local use super::centered_popup as centered_rect aliases
that are no longer needed.
Signed-off-by: Eric Curtin <eric.curtin@docker.com>
The draw_edit_overlay function in sandbox_settings and global_settings
was nearly identical — the only difference was which settings slice the
entry was resolved from (app.sandbox_settings vs app.global_settings).
Extract the common rendering logic into draw_setting_edit_overlay in
ui/mod.rs, taking key/kind/edit/area/theme as parameters. Each pane
now resolves its own entry and delegates to the single implementation.
Net: -75 lines, +61 lines across the three files.
Add PageUp/PageDown key support to the policy, logs, and draft/rules
views. All three panes now scroll by one viewport height per keypress.
Also fix scroll_policy() clamping to stop at the last viewport of
content instead of the last line, preventing a blank-screen overshoot
on G and PageDown.
Signed-off-by: Major Hayden <major@redhat.com>
* refactor(proto): move phase and current_policy_version into SandboxStatus
Move phase and current_policy_version from SandboxSpec into
SandboxStatus to correctly model mutable runtime state. Update all
callers in the gateway server, TUI, and Python SDK to read and write
these fields through SandboxStatus accessors.
Signed-off-by: Derek Carr <decarr@redhat.com>
* fix(server): preserve sandbox status on statusless driver updates
When a driver update arrives without a status payload (e.g. before
Kubernetes populates the status subresource), preserve the stored
phase, conditions, and current policy version instead of resetting
them. Adds a regression test covering the edge case.
Signed-off-by: Derek Carr <decarr@redhat.com>
---------
Signed-off-by: Derek Carr <decarr@redhat.com>
Remove ~280 lines of duplicated code across 30 files in 5 areas:
- centered_rect: consolidate 5 identical TUI layout helpers into a
single pub fn in openshell-tui/src/ui/mod.rs
- server test helpers: replace ~100 inline Store::connect() calls
with local test_store() helpers; deduplicate test_server_state()
in grpc/service.rs to use the shared test_support version
- rogue PKI: extract 20-line rogue CA+client cert generation block
(duplicated in two integration tests) into generate_rogue_pki()
in tests/common/mod.rs
- provider tests: replace 8 identical 28-line test modules with a
single macro_rules! test_discovers_env_credential! invocation
- label constants: centralize openshell.ai/ container label keys
in openshell-core::driver_utils; update Docker and Kubernetes
drivers to import from there instead of redefining them locally
Add resource_version-based optimistic concurrency control to the
persistence layer. Every write now requires an explicit WriteCondition
(MustCreate or MatchResourceVersion), enforced at compile time by
gating unconditional put/put_message behind #[cfg(test)].
- Add WriteCondition enum and put_if for conditional writes
- Add update_message_cas for atomic read-modify-write operations
- Add list_messages/list_messages_with_selector helpers that hydrate
resource_version from authoritative DB rows
- Convert all production write paths to CAS-aware methods
- Gate put/put_message behind #[cfg(test)] to prevent non-CAS writes
- Use structured PersistenceError::UniqueViolation matching instead of
string matching for duplicate detection
- Hydrate resource_version from WriteResult directly on creates,
eliminating unnecessary read-after-write round trips
Signed-off-by: Derek Carr <decarr@redhat.com>
Move EdgeAuthInterceptor from openshell-cli to openshell-core so the TUI
can use it without a circular dependency. The TUI now checks gateway
metadata for OIDC auth mode, loads the stored bearer token, and injects
it into gRPC requests via the shared interceptor.
When switching gateways, the TUI dispatches on auth_mode to build either
an mTLS or OIDC-authenticated channel. Expired tokens surface a clear
error directing users to re-authenticate with `openshell gateway login`.
The CLI and TUI build an SSH ProxyCommand string by interpolating fields
from CreateSshSessionResponse into a shell command. Three fields -
sandbox_id, token, and the assembled gateway_url (composed from
gateway_scheme, gateway_host, gateway_port, connect_path) - were not
passed through shell_escape, even though exe_command and gateway_name
were. The resulting string is handed to `ssh -o ProxyCommand=...` which
OpenSSH routes through `/bin/sh -c`, so a hostile or compromised gateway
could inject shell metacharacters and execute arbitrary commands under
the developer's workstation account.
- Add build_proxy_command in openshell-core::forward that wraps every
interpolated value in shell_escape. Use it at all four ProxyCommand
sites: openshell-cli/src/ssh.rs (ssh_session_config) and the three
TUI sites in openshell-tui/src/lib.rs (shell connect, sandbox exec,
port-forward reconciliation).
- Add validate_ssh_session_response in openshell-core::forward, called
at each site immediately after response.into_inner(). Enforces a
conservative charset per field (sandbox_id, token, gateway_host,
gateway_scheme, gateway_port, connect_path, host_key_fingerprint) so
malformed responses fail loudly at the gRPC trust boundary before
shell escaping is attempted. Belt-and-suspenders with the escape.
- Harden render_ssh_config so `gateway` and `name` are shell-escaped
even though validate_gateway_name currently gates `gateway`.
- Document the charset contract on CreateSshSessionResponse in
proto/openshell.proto: servers must uphold these rules and clients
reject responses that violate them.
- Add regression unit tests covering adversarial values in every field
and a build_proxy_command test asserting no shell metacharacter
remains outside single-quoted regions.
Tracks OS-100.
Extract shared resolve_community_image() into openshell-core so both
CLI and TUI expand bare sandbox names (e.g. "base") to full registry
references. Previously the TUI passed the bare name directly, causing
ImagePullBackOff in Kubernetes.
Closes#786
* fix(install): restrict tar extraction to expected binary member
Prevents CWE-22 path traversal by extracting only the expected APP_NAME
member instead of the full archive contents. Adds --no-same-owner and
--no-same-permissions for defense-in-depth.
OS-20
* fix(deploy): quote registry credentials in YAML heredocs
Wraps username/password values with a yaml_quote helper to prevent YAML
injection from special characters in registry credentials (CWE-94).
Applied to all three heredoc blocks that emit registries.yaml auth.
OS-23
* fix(server): redact session token in SSH tunnel rate-limit log
Logs only the last 4 characters of bearer tokens to prevent credential
exposure in log aggregation systems (CWE-532).
OS-18
* fix(server): escape gateway_display in auth connect page
Applies html_escape() to the Host/X-Forwarded-Host header value before
rendering it into the HTML template, preventing HTML injection (CWE-79).
OS-17
* fix(server): prevent XSS via code param with validation and proper JS escaping
Adds server-side validation rejecting confirmation codes that do not
match the CLI-generated format, replaces manual JS string escaping with
serde_json serialization (handling U+2028/U+2029 line terminators), and
adds a Content-Security-Policy header with nonce-based script-src.
OS-16
* fix(sandbox): add byte cap and idle timeout to streaming inference relay
Prevents resource exhaustion from upstream inference endpoints that stream
indefinitely or hold connections open. Adds a 32 MiB total body limit
and 30-second per-chunk idle timeout (CWE-400).
OS-21
* fix(policy): narrow port field from u32 to u16 to reject invalid values
Prevents meaningless port values >65535 from being accepted in policy
YAML definitions. The proto field remains uint32 (protobuf has no u16)
with validation at the conversion boundary.
OS-22
* fix(deps): migrate from archived serde_yaml to serde_yml
Replaces serde_yaml 0.9 (archived, RUSTSEC-2024-0320) with serde_yml
0.0.12, a maintained API-compatible fork. All import sites updated
across openshell-policy, openshell-sandbox, and openshell-router.
OS-19
* fix(server): re-validate sandbox-submitted security_notes and cap hit_count
The gateway now re-runs security heuristics on proposed policy chunks
instead of trusting sandbox-provided security_notes, validates host
wildcards, caps hit_count at 100, and clamps confidence to [0,1]. The
TUI approve-all path is updated to use ApproveAllDraftChunks RPC which
respects the security_notes filtering gate (CWE-284, confused deputy).
OS-15
* chore: apply cargo fmt and update Cargo.lock for serde_yml
---------
Co-authored-by: John Myers <johntmyers@users.noreply.github.com>
Closes#362
Split the CONNECT log emission in the proxy so denied connections log
immediately as CONNECT while allowed connections defer logging until
after the L7 config check. When L7 inspection follows, emit CONNECT_L7
instead of CONNECT so log consumers can distinguish standalone L4
policy decisions from tunnel lifecycle events.
Compute version strings from git describe in openshell-core's build.rs
using the guess-next-dev scheme (e.g. 0.0.4-dev.6+g2bf9969). All binary
crates and the TUI splash screen now use the shared openshell_core::VERSION
constant instead of CARGO_PKG_VERSION.
In Docker/CI builds where .git is absent, falls back to CARGO_PKG_VERSION
which is already set correctly by the sed-patch pipeline. Also adds
OPENSHELL_CARGO_VERSION support to the cluster image and fast-deploy
supervisor builds for parity with the gateway.
* feat(tui): add log copy and visual selection mode
Add clipboard support for the TUI log viewer using OSC 52 escape
sequences. Users can copy individual log lines (y), the visible
viewport (Y), or enter visual selection mode (v) to select a range
of lines with j/k and yank with y.
- Add clipboard module with OSC 52 base64 encoding
- Add format_log_line_plain() for plain-text log rendering
- Add visual selection mode with highlight styling and status bar
- Context-switch nav bar hints between normal and visual modes
- Add 7 unit tests for plain-text log formatting
Closes#274
* fix(tui): write OSC 52 to /dev/tty instead of stdout
ratatui owns stdout via the alternate screen buffer, so OSC 52
escape sequences written to stdout are swallowed by the backend.
Write directly to /dev/tty to bypass the buffer and reach the
terminal emulator.