mirror of
https://github.com/harivansh-afk/deskctl.git
synced 2026-04-15 05:02:08 +00:00
runtime contract enforcement (#6)
This commit is contained in:
parent
61f4738311
commit
543d41c3a2
7 changed files with 958 additions and 157 deletions
|
|
@ -21,6 +21,7 @@ pnpm --dir site install
|
||||||
- `src/` holds production code and unit tests
|
- `src/` holds production code and unit tests
|
||||||
- `tests/` holds integration tests
|
- `tests/` holds integration tests
|
||||||
- `tests/support/` holds shared X11 and daemon helpers for integration coverage
|
- `tests/support/` holds shared X11 and daemon helpers for integration coverage
|
||||||
|
- `docs/runtime-output.md` is the stable-vs-best-effort runtime output contract for agent-facing CLI work
|
||||||
|
|
||||||
Keep integration-only helpers out of `src/`.
|
Keep integration-only helpers out of `src/`.
|
||||||
|
|
||||||
|
|
|
||||||
11
README.md
11
README.md
|
|
@ -100,6 +100,7 @@ deskctl doctor
|
||||||
- `@wN` refs are short-lived handles assigned by `snapshot` and `list-windows`
|
- `@wN` refs are short-lived handles assigned by `snapshot` and `list-windows`
|
||||||
- `--json` output includes a stable `window_id` for programmatic targeting within the current daemon session
|
- `--json` output includes a stable `window_id` for programmatic targeting within the current daemon session
|
||||||
- `list-windows` is a cheap read-only operation and does not capture or write a screenshot
|
- `list-windows` is a cheap read-only operation and does not capture or write a screenshot
|
||||||
|
- the stable runtime JSON/error contract is documented in [docs/runtime-output.md](docs/runtime-output.md)
|
||||||
|
|
||||||
## Read and Wait Surface
|
## Read and Wait Surface
|
||||||
|
|
||||||
|
|
@ -147,6 +148,16 @@ Successful `get active-window`, `wait window`, and `wait focus` responses return
|
||||||
|
|
||||||
Wait timeout and selector failures are structured in `--json` mode so agents can recover without string parsing.
|
Wait timeout and selector failures are structured in `--json` mode so agents can recover without string parsing.
|
||||||
|
|
||||||
|
## Output Policy
|
||||||
|
|
||||||
|
Text mode is compact and follow-up-oriented, but JSON is the parsing contract.
|
||||||
|
|
||||||
|
- use `--json` when an agent needs strict parsing
|
||||||
|
- rely on `window_id`, selector-related fields, grouped read payloads, and structured error `kind` values for stable automation
|
||||||
|
- treat monitor naming, incidental whitespace, and default screenshot file names as best-effort
|
||||||
|
|
||||||
|
See [docs/runtime-output.md](docs/runtime-output.md) for the exact stable-vs-best-effort breakdown.
|
||||||
|
|
||||||
## Selector Contract
|
## Selector Contract
|
||||||
|
|
||||||
Explicit selector modes:
|
Explicit selector modes:
|
||||||
|
|
|
||||||
178
docs/runtime-output.md
Normal file
178
docs/runtime-output.md
Normal file
|
|
@ -0,0 +1,178 @@
|
||||||
|
# Runtime Output Contract
|
||||||
|
|
||||||
|
This document defines the current output contract for `deskctl`.
|
||||||
|
|
||||||
|
It is intentionally scoped to the current Linux X11 runtime surface.
|
||||||
|
It does not promise stability for future Wayland or window-manager-specific features.
|
||||||
|
|
||||||
|
## Goals
|
||||||
|
|
||||||
|
- Keep `deskctl` fully non-interactive
|
||||||
|
- Make text output actionable for quick terminal and agent loops
|
||||||
|
- Make `--json` safe for agent consumption without depending on incidental formatting
|
||||||
|
|
||||||
|
## JSON Envelope
|
||||||
|
|
||||||
|
Every runtime command uses the same top-level JSON envelope:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {},
|
||||||
|
"error": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Stable top-level fields:
|
||||||
|
|
||||||
|
- `success`
|
||||||
|
- `data`
|
||||||
|
- `error`
|
||||||
|
|
||||||
|
`success` is always the authoritative success/failure bit.
|
||||||
|
When `success` is `false`, the CLI exits non-zero in both text mode and `--json` mode.
|
||||||
|
|
||||||
|
## Stable Fields
|
||||||
|
|
||||||
|
These fields are stable for agent consumption in the current Phase 1 runtime contract.
|
||||||
|
|
||||||
|
### Window Identity
|
||||||
|
|
||||||
|
Whenever a runtime response includes a window payload, these fields are stable:
|
||||||
|
|
||||||
|
- `ref_id`
|
||||||
|
- `window_id`
|
||||||
|
- `title`
|
||||||
|
- `app_name`
|
||||||
|
- `x`
|
||||||
|
- `y`
|
||||||
|
- `width`
|
||||||
|
- `height`
|
||||||
|
- `focused`
|
||||||
|
- `minimized`
|
||||||
|
|
||||||
|
`window_id` is the stable public identifier for a live daemon session.
|
||||||
|
`ref_id` is a short-lived convenience handle for the current window snapshot/ref map.
|
||||||
|
|
||||||
|
### Grouped Reads
|
||||||
|
|
||||||
|
`deskctl get active-window`
|
||||||
|
|
||||||
|
- stable: `data.window`
|
||||||
|
|
||||||
|
`deskctl get monitors`
|
||||||
|
|
||||||
|
- stable: `data.count`
|
||||||
|
- stable: `data.monitors`
|
||||||
|
- stable per monitor:
|
||||||
|
- `name`
|
||||||
|
- `x`
|
||||||
|
- `y`
|
||||||
|
- `width`
|
||||||
|
- `height`
|
||||||
|
- `width_mm`
|
||||||
|
- `height_mm`
|
||||||
|
- `primary`
|
||||||
|
- `automatic`
|
||||||
|
|
||||||
|
`deskctl get version`
|
||||||
|
|
||||||
|
- stable: `data.version`
|
||||||
|
- stable: `data.backend`
|
||||||
|
|
||||||
|
`deskctl get systeminfo`
|
||||||
|
|
||||||
|
- stable: `data.backend`
|
||||||
|
- stable: `data.display`
|
||||||
|
- stable: `data.session_type`
|
||||||
|
- stable: `data.session`
|
||||||
|
- stable: `data.socket_path`
|
||||||
|
- stable: `data.screen`
|
||||||
|
- stable: `data.monitor_count`
|
||||||
|
- stable: `data.monitors`
|
||||||
|
|
||||||
|
### Waits
|
||||||
|
|
||||||
|
`deskctl wait window`
|
||||||
|
`deskctl wait focus`
|
||||||
|
|
||||||
|
- stable: `data.wait`
|
||||||
|
- stable: `data.selector`
|
||||||
|
- stable: `data.elapsed_ms`
|
||||||
|
- stable: `data.window`
|
||||||
|
|
||||||
|
### Selector-Driven Action Success
|
||||||
|
|
||||||
|
For selector-driven action commands that resolve a window target, these identifiers are stable when present:
|
||||||
|
|
||||||
|
- `data.ref_id`
|
||||||
|
- `data.window_id`
|
||||||
|
- `data.title`
|
||||||
|
- `data.selector`
|
||||||
|
|
||||||
|
This applies to:
|
||||||
|
|
||||||
|
- `click`
|
||||||
|
- `dblclick`
|
||||||
|
- `focus`
|
||||||
|
- `close`
|
||||||
|
- `move-window`
|
||||||
|
- `resize-window`
|
||||||
|
|
||||||
|
The exact human-readable text rendering of those commands is not part of the JSON contract.
|
||||||
|
|
||||||
|
### Artifact-Producing Commands
|
||||||
|
|
||||||
|
`snapshot`
|
||||||
|
`screenshot`
|
||||||
|
|
||||||
|
- stable: `data.screenshot`
|
||||||
|
|
||||||
|
When the command also returns windows, `data.windows` uses the stable window payload documented above.
|
||||||
|
|
||||||
|
## Stable Structured Error Kinds
|
||||||
|
|
||||||
|
When a runtime command returns structured JSON failure data, these error kinds are stable:
|
||||||
|
|
||||||
|
- `selector_not_found`
|
||||||
|
- `selector_ambiguous`
|
||||||
|
- `selector_invalid`
|
||||||
|
- `timeout`
|
||||||
|
- `not_found`
|
||||||
|
- `window_not_focused` as `data.last_observation.kind` or equivalent observation payload
|
||||||
|
|
||||||
|
Stable structured failure fields include:
|
||||||
|
|
||||||
|
- `data.kind`
|
||||||
|
- `data.selector` when selector-related
|
||||||
|
- `data.mode` when selector-related
|
||||||
|
- `data.candidates` for ambiguous selector failures
|
||||||
|
- `data.message` for invalid selector failures
|
||||||
|
- `data.wait`
|
||||||
|
- `data.timeout_ms`
|
||||||
|
- `data.poll_ms`
|
||||||
|
- `data.last_observation`
|
||||||
|
|
||||||
|
## Best-Effort Fields
|
||||||
|
|
||||||
|
These values are useful but environment-dependent and should be treated as best-effort:
|
||||||
|
|
||||||
|
- exact monitor naming conventions
|
||||||
|
- EWMH/window-manager-dependent window ordering details
|
||||||
|
- cosmetic text formatting in non-JSON mode
|
||||||
|
- screenshot file names when the caller did not provide an explicit path
|
||||||
|
- command stderr wording outside the structured `kind` classifications above
|
||||||
|
|
||||||
|
## Text Mode Expectations
|
||||||
|
|
||||||
|
Text mode is intended to stay compact and follow-up-useful.
|
||||||
|
|
||||||
|
The exact whitespace/alignment of text output is not stable.
|
||||||
|
The following expectations are stable at the behavioral level:
|
||||||
|
|
||||||
|
- important runtime reads print actionable identifiers or geometry
|
||||||
|
- selector failures print enough detail to recover without `--json`
|
||||||
|
- artifact-producing commands print the artifact path
|
||||||
|
- window listings print both `@wN` refs and `window_id` values
|
||||||
|
|
||||||
|
If an agent needs strict parsing, it should use `--json`.
|
||||||
|
|
@ -90,6 +90,14 @@ deskctl daemon status # Check daemon status
|
||||||
- `--session NAME` : Session name for multiple daemon instances (default: "default")
|
- `--session NAME` : Session name for multiple daemon instances (default: "default")
|
||||||
- `--socket PATH` : Custom Unix socket path
|
- `--socket PATH` : Custom Unix socket path
|
||||||
|
|
||||||
|
## Output Contract
|
||||||
|
|
||||||
|
- Prefer `--json` when an agent needs strict parsing.
|
||||||
|
- Use `window_id` for stable targeting inside a live daemon session.
|
||||||
|
- Use `ref_id` / `@wN` for quick short-lived follow-up actions after `snapshot` or `list-windows`.
|
||||||
|
- Structured JSON failures expose machine-usable `kind` values for selector and wait failures.
|
||||||
|
- The exact text formatting is intentionally compact but not the parsing contract. See `docs/runtime-output.md` for the stable field policy.
|
||||||
|
|
||||||
## Window Refs
|
## Window Refs
|
||||||
|
|
||||||
After `snapshot` or `list-windows`, windows are assigned short refs:
|
After `snapshot` or `list-windows`, windows are assigned short refs:
|
||||||
|
|
|
||||||
872
src/cli/mod.rs
872
src/cli/mod.rs
File diff suppressed because it is too large
Load diff
|
|
@ -88,9 +88,21 @@ impl std::fmt::Display for WindowInfo {
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
fn truncate(s: &str, max: usize) -> String {
|
fn truncate(s: &str, max: usize) -> String {
|
||||||
if s.len() <= max {
|
if s.chars().count() <= max {
|
||||||
s.to_string()
|
s.to_string()
|
||||||
} else {
|
} else {
|
||||||
format!("{}...", &s[..max - 3])
|
let truncated: String = s.chars().take(max.saturating_sub(3)).collect();
|
||||||
|
format!("{truncated}...")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::truncate;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn truncate_is_char_safe() {
|
||||||
|
let input = format!("fire{}fox", '\u{00E9}');
|
||||||
|
assert_eq!(truncate(&input, 7), "fire...");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -81,9 +81,13 @@ async fn handle_click(request: &Request, state: &Arc<Mutex<DaemonState>>) -> Res
|
||||||
ResolveResult::Match(entry) => {
|
ResolveResult::Match(entry) => {
|
||||||
let (x, y) = entry.center();
|
let (x, y) = entry.center();
|
||||||
match state.backend.click(x, y) {
|
match state.backend.click(x, y) {
|
||||||
Ok(()) => Response::ok(
|
Ok(()) => Response::ok(serde_json::json!({
|
||||||
serde_json::json!({"clicked": {"x": x, "y": y, "selector": selector, "window_id": entry.window_id}}),
|
"clicked": {"x": x, "y": y},
|
||||||
),
|
"selector": selector,
|
||||||
|
"ref_id": entry.ref_id,
|
||||||
|
"window_id": entry.window_id,
|
||||||
|
"title": entry.title,
|
||||||
|
})),
|
||||||
Err(error) => Response::err(format!("Click failed: {error}")),
|
Err(error) => Response::err(format!("Click failed: {error}")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -117,9 +121,13 @@ async fn handle_dblclick(request: &Request, state: &Arc<Mutex<DaemonState>>) ->
|
||||||
ResolveResult::Match(entry) => {
|
ResolveResult::Match(entry) => {
|
||||||
let (x, y) = entry.center();
|
let (x, y) = entry.center();
|
||||||
match state.backend.dblclick(x, y) {
|
match state.backend.dblclick(x, y) {
|
||||||
Ok(()) => Response::ok(
|
Ok(()) => Response::ok(serde_json::json!({
|
||||||
serde_json::json!({"double_clicked": {"x": x, "y": y, "selector": selector, "window_id": entry.window_id}}),
|
"double_clicked": {"x": x, "y": y},
|
||||||
),
|
"selector": selector,
|
||||||
|
"ref_id": entry.ref_id,
|
||||||
|
"window_id": entry.window_id,
|
||||||
|
"title": entry.title,
|
||||||
|
})),
|
||||||
Err(error) => Response::err(format!("Double-click failed: {error}")),
|
Err(error) => Response::err(format!("Double-click failed: {error}")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -267,7 +275,10 @@ async fn handle_window_action(
|
||||||
Ok(()) => Response::ok(serde_json::json!({
|
Ok(()) => Response::ok(serde_json::json!({
|
||||||
"action": action,
|
"action": action,
|
||||||
"window": entry.title,
|
"window": entry.title,
|
||||||
|
"title": entry.title,
|
||||||
|
"ref_id": entry.ref_id,
|
||||||
"window_id": entry.window_id,
|
"window_id": entry.window_id,
|
||||||
|
"selector": selector,
|
||||||
})),
|
})),
|
||||||
Err(error) => Response::err(format!("{action} failed: {error}")),
|
Err(error) => Response::err(format!("{action} failed: {error}")),
|
||||||
}
|
}
|
||||||
|
|
@ -296,7 +307,10 @@ async fn handle_move_window(request: &Request, state: &Arc<Mutex<DaemonState>>)
|
||||||
match state.backend.move_window(entry.backend_window_id, x, y) {
|
match state.backend.move_window(entry.backend_window_id, x, y) {
|
||||||
Ok(()) => Response::ok(serde_json::json!({
|
Ok(()) => Response::ok(serde_json::json!({
|
||||||
"moved": entry.title,
|
"moved": entry.title,
|
||||||
|
"title": entry.title,
|
||||||
|
"ref_id": entry.ref_id,
|
||||||
"window_id": entry.window_id,
|
"window_id": entry.window_id,
|
||||||
|
"selector": selector,
|
||||||
"x": x,
|
"x": x,
|
||||||
"y": y,
|
"y": y,
|
||||||
})),
|
})),
|
||||||
|
|
@ -338,7 +352,10 @@ async fn handle_resize_window(request: &Request, state: &Arc<Mutex<DaemonState>>
|
||||||
{
|
{
|
||||||
Ok(()) => Response::ok(serde_json::json!({
|
Ok(()) => Response::ok(serde_json::json!({
|
||||||
"resized": entry.title,
|
"resized": entry.title,
|
||||||
|
"title": entry.title,
|
||||||
|
"ref_id": entry.ref_id,
|
||||||
"window_id": entry.window_id,
|
"window_id": entry.window_id,
|
||||||
|
"selector": selector,
|
||||||
"width": width,
|
"width": width,
|
||||||
"height": height,
|
"height": height,
|
||||||
})),
|
})),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue