wai: one operation catalogue (noun.verb) for every door, wai ops, the agent rules on every command, and the wai-* service binaries #506
104 changed files with 7736 additions and 1016 deletions
5
.github/workflows/joule-moq-room.yml
vendored
5
.github/workflows/joule-moq-room.yml
vendored
|
|
@ -28,12 +28,13 @@ name: joule-moq-room room service
|
|||
# past its removal, every record verified offline; and a call (tests/call_room.rs) rung, accepted
|
||||
# and carrying audio and video through the relay, and another missed. tests/openapi_contract.rs
|
||||
# holds openapi.json to the service: every operationId is the operation's name in the toolchain's
|
||||
# catalogue (noun.verb, by wai::ops's rule), the routes the routers match are exactly its
|
||||
# catalogue (wai/wai-rs/src/catalogue.rs, at the route it gives) and every operation the
|
||||
# catalogue routes to the room service is described, the routes the routers match are exactly its
|
||||
# operations, each status it lists is one its handlers return and each status they return is
|
||||
# listed, and the service, run in each of its modes, answers every operation with a status it
|
||||
# lists. tests/mcp_dev_room.rs builds `wai` with the feature `mcp` (a target directory of its own)
|
||||
# and drives `wai mcp --toolset core,dev` with an MCP client: the tool room_grant and POST /grant
|
||||
# named alike (room.grant), a development room (`joule-moq-room dev`) started on loopback, grants
|
||||
# named alike (room.grant), a development room (`wai-room dev`) started on loopback, grants
|
||||
# for two test principals, each joining and publishing, the session verified as live-rooms §9
|
||||
# verifies a room, a changed record refused, and a grant from a service off the machine refused.
|
||||
#
|
||||
|
|
|
|||
136
.github/workflows/wai-catalogue.yml
vendored
Normal file
136
.github/workflows/wai-catalogue.yml
vendored
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
name: WAI operation catalogue
|
||||
|
||||
# WAI's toolchain has one catalogue of operations, `noun.verb` (wai/wai-rs/src/catalogue.rs),
|
||||
# and every door names them by it. This workflow fails on drift:
|
||||
#
|
||||
# - the export, wai/catalogue/ops.json, is the table (`wai ops export --check`);
|
||||
# - the table keeps its naming rules (the library's catalogue tests);
|
||||
# - the command line reaches operations through the catalogue alone, and every operation
|
||||
# keeps the agent rules: its help, the exit codes, the JSON envelope, --dry-run on every
|
||||
# operation that is not read-only, --yes (tests/cli_ops.rs, and the binary's own tests);
|
||||
# - the other doors name operations as the catalogue does: MCP tool names, the services'
|
||||
# OpenAPI operationIds, the C header, and the Swift and Kotlin sinks
|
||||
# (scripts/check-wai-ops-doors.py, with its selftest).
|
||||
#
|
||||
# Push filters match the push's delta, so every path a door or a test reads is listed.
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'wai/wai-rs/**'
|
||||
- 'wai/catalogue/**'
|
||||
- 'wai/examples/**'
|
||||
- 'wai/http-conformance/**'
|
||||
- 'wai/score-conformance/**'
|
||||
- 'wai/live-conformance/fleet/**'
|
||||
- 'wai/REGISTRY.md'
|
||||
- 'wai/LICENSE'
|
||||
- 'wai/wai-quantum/**'
|
||||
- 'wai/wai-ops/**'
|
||||
- 'wai/wai-app/**'
|
||||
- 'wai/wai-sink-swift/**'
|
||||
- 'wai/wai-sink-android/**'
|
||||
- 'joule-code/joule-code-rs/crates/*/openapi.json'
|
||||
- 'scripts/check-wai-ops-doors.py'
|
||||
- 'scripts/check-wai-ops-doors-selftest.sh'
|
||||
- 'rust-toolchain.toml'
|
||||
- '.github/workflows/wai-catalogue.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'wai/wai-rs/**'
|
||||
- 'wai/catalogue/**'
|
||||
- 'wai/examples/**'
|
||||
- 'wai/http-conformance/**'
|
||||
- 'wai/score-conformance/**'
|
||||
- 'wai/live-conformance/fleet/**'
|
||||
- 'wai/REGISTRY.md'
|
||||
- 'wai/LICENSE'
|
||||
- 'wai/wai-quantum/**'
|
||||
- 'wai/wai-ops/**'
|
||||
- 'wai/wai-app/**'
|
||||
- 'wai/wai-sink-swift/**'
|
||||
- 'wai/wai-sink-android/**'
|
||||
- 'joule-code/joule-code-rs/crates/*/openapi.json'
|
||||
- 'scripts/check-wai-ops-doors.py'
|
||||
- 'scripts/check-wai-ops-doors-selftest.sh'
|
||||
- 'rust-toolchain.toml'
|
||||
- '.github/workflows/wai-catalogue.yml'
|
||||
|
||||
jobs:
|
||||
# The path filters gate the trigger; this job gates the work from the pull request's own
|
||||
# diff (scripts/pr-touches-paths.py, reading the list above), as wai-pack.yml explains.
|
||||
relevant:
|
||||
name: does this change touch it
|
||||
runs-on: linux-arm64
|
||||
outputs:
|
||||
run: ${{ steps.gate.outputs.run }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- id: gate
|
||||
run: |
|
||||
set -euo pipefail
|
||||
r=$(python3 scripts/pr-touches-paths.py .github/workflows/wai-catalogue.yml \
|
||||
"${{ github.event.pull_request.base.sha }}" \
|
||||
"${{ github.event.pull_request.head.sha }}")
|
||||
echo "this change touches the operation catalogue: $r"
|
||||
echo "run=$r" >> "$GITHUB_OUTPUT"
|
||||
|
||||
catalogue:
|
||||
name: the catalogue and every door · linux
|
||||
needs: relevant
|
||||
if: needs.relevant.outputs.run == 'true'
|
||||
runs-on: linux
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: Install Rust 1.98.0
|
||||
shell: bash
|
||||
run: |
|
||||
export PATH="$HOME/.cargo/bin:$PATH"
|
||||
command -v rustup >/dev/null 2>&1 || \
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain 1.98.0
|
||||
rustup toolchain install 1.98.0 --profile minimal
|
||||
rustup default 1.98.0
|
||||
SUDO=""; [ "$(id -u)" -ne 0 ] && SUDO="sudo"
|
||||
command -v python3 >/dev/null || { $SUDO apt-get update -qq; $SUDO apt-get install -y -qq python3; }
|
||||
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||
# The export other tools and this workflow's door check read is the table, byte for
|
||||
# byte; the message says how to regenerate it.
|
||||
- name: The catalogue's export is up to date
|
||||
working-directory: wai/wai-rs
|
||||
run: cargo run -q --locked --no-default-features --bin wai -- ops export --check ../catalogue/ops.json
|
||||
- name: Every door names its operations as the catalogue does
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
bash scripts/check-wai-ops-doors-selftest.sh
|
||||
python3 -I scripts/check-wai-ops-doors.py
|
||||
# The library's catalogue tests: the naming rules, each broken in a copy of the table
|
||||
# and refused. The binary's own tests: every implementation names an operation of the
|
||||
# catalogue and every operation with a command line has one; help is the catalogue's.
|
||||
# tests/cli_ops.rs: wai ops, help, the exit codes, the envelope, --dry-run and --yes (and
|
||||
# --confirm, its deprecated spelling),
|
||||
# first in the archive's build (quickstart), then with every command-line feature that
|
||||
# needs no system library, so --dry-run is held on generate, score and stage too, and with
|
||||
# Create's (app), so it is held on wai app export.
|
||||
- name: The catalogue's rules, and the command line through it
|
||||
shell: bash
|
||||
working-directory: wai/wai-rs
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ran() { grep -qE '^test result: ok\. [1-9][0-9]* passed; 0 failed' "$1" || { echo "::error::no test ran in $1"; exit 1; }; }
|
||||
cargo test --locked --no-default-features --lib catalogue 2>&1 | tee "$RUNNER_TEMP/lib.log"
|
||||
ran "$RUNNER_TEMP/lib.log"
|
||||
cargo test --locked --no-default-features --features quickstart --bin wai --test cli_ops 2>&1 | tee "$RUNNER_TEMP/cli.log"
|
||||
ran "$RUNNER_TEMP/cli.log"
|
||||
cargo test --locked --no-default-features --features quickstart,generate,score,staged,video_int,audio_int,splat_int --test cli_ops 2>&1 | tee "$RUNNER_TEMP/cli-full.log"
|
||||
ran "$RUNNER_TEMP/cli-full.log"
|
||||
cargo test --locked --no-default-features --features quickstart,app --test cli_ops 2>&1 | tee "$RUNNER_TEMP/cli-app.log"
|
||||
ran "$RUNNER_TEMP/cli-app.log"
|
||||
17
.github/workflows/wai-conformance.yml
vendored
17
.github/workflows/wai-conformance.yml
vendored
|
|
@ -636,16 +636,19 @@ jobs:
|
|||
# and every tool. receipt_verify agrees vector by vector with jwp-conformance (group and
|
||||
# object receipts), claim-conformance (each claim with the context the tool takes),
|
||||
# live-conformance/rooms and live-conformance/audit, and a receipt changed by one character
|
||||
# is refused; package_build writes only inside the project directory and never through a
|
||||
# is refused; envelope_wrap writes only inside the project directory and never through a
|
||||
# link; conformance_run runs live_rooms_verify.py; --read-only offers no tool that writes,
|
||||
# serves or asks for a grant; the dev tools refuse any service off this machine before
|
||||
# connecting, and any target but local (the room itself, joined, published to and verified
|
||||
# through the tools, is joule-moq-room's tests/mcp_dev_room.rs). `--bin wai` runs the
|
||||
# binary's unit tests: every tool's operation named by the rule, the loopback guard and
|
||||
# setup-agent's merges and diff; `--lib ops::` the rule itself. tests/cli_setup_agent.rs
|
||||
# runs `wai setup-agent` in scratch projects as an agent does (no terminal, the JSON
|
||||
# envelope out): a fresh project, a second run that changes nothing, a CLAUDE.md made only
|
||||
# where one is, a file that exists changed only with --confirm (exit 4 and the proposed
|
||||
# binary's unit tests: the tool table held to the operation catalogue both ways (every tool
|
||||
# an operation the catalogue says wai mcp serves, in its toolset, every such operation a
|
||||
# tool, a tool of MCP alone taking the catalogue's input), the loopback guard and
|
||||
# setup-agent's merges and diff; `--lib catalogue::` the catalogue's rules (and
|
||||
# wai-catalogue.yml the rest). tests/cli_setup_agent.rs runs `wai setup-agent` in scratch
|
||||
# projects as an agent does (no terminal, --json, the JSON envelope out): a fresh project, a
|
||||
# second run that changes nothing, a CLAUDE.md made only where one is, a file that exists
|
||||
# changed only with --yes (exit 4 and the proposed
|
||||
# changes otherwise), a dry run, --toolset registered, a JSON file with comments left
|
||||
# alone, nothing written through a link out of the project, and the table's exit statuses.
|
||||
# Cargo.toml declares cli_mcp with required-features = ["mcp"].
|
||||
|
|
@ -653,7 +656,7 @@ jobs:
|
|||
working-directory: wai/wai-rs
|
||||
run: |
|
||||
cargo test --no-default-features --features mcp --bin wai --test cli_mcp --test cli_setup_agent
|
||||
cargo test --no-default-features --lib ops::
|
||||
cargo test --no-default-features --lib catalogue::
|
||||
# Apps made with Create, through `wai` built with the feature `app` (wai/wai-app linked
|
||||
# in). tests/cli_app.rs: `wai create --from app.oui` writes an ordinary project, its
|
||||
# wai.json laid out as a template's and naming the program and the room `wai dev` runs,
|
||||
|
|
|
|||
6
.github/workflows/wai-examples.yml
vendored
6
.github/workflows/wai-examples.yml
vendored
|
|
@ -93,8 +93,8 @@ jobs:
|
|||
b="$RUNNER_TEMP/bin"; mkdir -p "$b"
|
||||
out() { (cd "$1" && cargo metadata --format-version 1 --no-deps --locked | python3 -c 'import json,sys; print(json.load(sys.stdin)["target_directory"])'); }
|
||||
(cd wai/wai-rs && cargo build --locked --release --no-default-features --features quickstart --bin wai) && cp "$(out wai/wai-rs)/release/wai" "$b/"
|
||||
(cd joule-code/joule-code-rs && cargo build --locked --release -p joule-moq-relay --bin joule-moq-relay) && cp "$(out joule-code/joule-code-rs)/release/joule-moq-relay" "$b/"
|
||||
(cd joule-code/joule-code-rs/crates/joule-moq-room && cargo build --locked --release --bin joule-moq-room) && cp "$(out joule-code/joule-code-rs/crates/joule-moq-room)/release/joule-moq-room" "$b/"
|
||||
(cd joule-code/joule-code-rs && cargo build --locked --release -p joule-moq-relay --bin wai-relay) && cp "$(out joule-code/joule-code-rs)/release/wai-relay" "$b/"
|
||||
(cd joule-code/joule-code-rs/crates/joule-moq-room && cargo build --locked --release --bin wai-room) && cp "$(out joule-code/joule-code-rs/crates/joule-moq-room)/release/wai-room" "$b/"
|
||||
(cd joule-code/joule-code-rs/crates/joule-moq-remote && cargo build --locked --release --bin wai-remote-host) && cp "$(out joule-code/joule-code-rs/crates/joule-moq-remote)/release/wai-remote-host" "$b/"
|
||||
(cd wai/wai-pack && cargo build --locked --release --bin wai-pack) && cp "$(out wai/wai-pack)/release/wai-pack" "$b/"
|
||||
echo "$b" >> "$GITHUB_PATH"
|
||||
|
|
@ -142,7 +142,7 @@ jobs:
|
|||
- name: The examples in the browser
|
||||
shell: bash
|
||||
env:
|
||||
BROWSER_BIN: /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
|
||||
BROWSER_BIN: /Applications/Google Chrome.app/Contents/MacOS/Google Chrome # allow-name: the macOS runner's browser executable, a file path
|
||||
run: |
|
||||
set -euo pipefail
|
||||
test -x "$BROWSER_BIN" || { echo "::error::this runner has no browser at $BROWSER_BIN"; exit 1; }
|
||||
|
|
|
|||
6
.github/workflows/wai-release.yml
vendored
6
.github/workflows/wai-release.yml
vendored
|
|
@ -3,7 +3,7 @@ name: wai command line release
|
|||
# On a tag wai-v<version> (the version wai/wai-rs/Cargo.toml states), builds the quickstart
|
||||
# archive (wai/quickstart/build.sh) on each runner and attaches it to the forge release of that
|
||||
# tag, each archive with its SHA-256. An archive holds bin/ (wai with the feature quickstart,
|
||||
# joule-moq-relay, joule-moq-room, wai-remote-host, wai-pack) and share/wai/web/ (the room
|
||||
# wai-relay, wai-room, wai-remote-host, wai-pack) and share/wai/web/ (the room
|
||||
# client, built once, on the macOS host, which has the WebAssembly toolchain):
|
||||
# macOS aarch64-apple-darwin and x86_64-apple-darwin;
|
||||
# Linux x86_64- and aarch64-unknown-linux-musl, static, so any distribution runs it;
|
||||
|
|
@ -189,8 +189,8 @@ jobs:
|
|||
New-Item -ItemType Directory -Force -Path (Join-Path $stage 'bin'), (Join-Path $stage 'share\wai'), $dist | Out-Null
|
||||
$builds = @(
|
||||
@{ dir = 'wai\wai-rs'; bin = 'wai'; args = @('--no-default-features', '--features', 'quickstart', '--bin', 'wai') },
|
||||
@{ dir = 'joule-code\joule-code-rs'; bin = 'joule-moq-relay'; args = @('-p', 'joule-moq-relay', '--bin', 'joule-moq-relay') },
|
||||
@{ dir = 'joule-code\joule-code-rs\crates\joule-moq-room'; bin = 'joule-moq-room'; args = @('--bin', 'joule-moq-room') },
|
||||
@{ dir = 'joule-code\joule-code-rs'; bin = 'wai-relay'; args = @('-p', 'joule-moq-relay', '--bin', 'wai-relay') },
|
||||
@{ dir = 'joule-code\joule-code-rs\crates\joule-moq-room'; bin = 'wai-room'; args = @('--bin', 'wai-room') },
|
||||
@{ dir = 'joule-code\joule-code-rs\crates\joule-moq-remote'; bin = 'wai-remote-host'; args = @('--bin', 'wai-remote-host') },
|
||||
@{ dir = 'wai\wai-pack'; bin = 'wai-pack'; args = @('--bin', 'wai-pack') }
|
||||
)
|
||||
|
|
|
|||
|
|
@ -10,8 +10,9 @@ repository = "https://github.com/transaction-science/open-standards"
|
|||
[lib]
|
||||
path = "src/lib.rs"
|
||||
|
||||
# The binary is `wai-ingest`; the crate keeps its name (WAI toolchain decision 11).
|
||||
[[bin]]
|
||||
name = "joule-moq-ingest"
|
||||
name = "wai-ingest"
|
||||
path = "src/main.rs"
|
||||
|
||||
# The link records' conformance corpus, wai/live-conformance/links/.
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ cues. It also takes contribution over **SRT** and **RIST** into the same tracks
|
|||
recovery and latency. The mappings it implements are WAI's `extensions/live-ingest.md`,
|
||||
`extensions/cue-injection.md` and `extensions/live-contribution.md`.
|
||||
|
||||
It is a library (`GatewayServer`, `Gateway`) and a binary, `joule-moq-ingest`. The relay
|
||||
is `joule-moq-relay`; the MOQT codec is `joule-moq`'s, `draft-ietf-moq-transport-22`.
|
||||
It is a library (`GatewayServer`, `Gateway`) and a binary, `wai-ingest` (the crate keeps its
|
||||
name). The relay is `joule-moq-relay` (binary `wai-relay`); the MOQT codec is `joule-moq`'s, `draft-ietf-moq-transport-22`.
|
||||
|
||||
## Resources
|
||||
|
||||
|
|
@ -187,7 +187,7 @@ Each is a field of `[limits]`, `[groups]`, `[timeline]` or `[whep]` in the confi
|
|||
signing key are files, and a room is its public, signed descriptor.
|
||||
|
||||
```text
|
||||
joule-moq-ingest --config gateway.toml # JOULE_MOQ_INGEST_LOG sets the log level
|
||||
wai-ingest --config gateway.toml # JOULE_MOQ_INGEST_LOG sets the log level
|
||||
```
|
||||
|
||||
Every listener is loopback by default: put a TLS front before the HTTP resources (RFC 9725
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
//! `joule-moq-ingest`: run the gateway.
|
||||
//! `wai-ingest` (crate `joule-moq-ingest`): run the gateway.
|
||||
//!
|
||||
//! ```text
|
||||
//! joule-moq-ingest --config gateway.toml
|
||||
//! wai-ingest --config gateway.toml
|
||||
//! ```
|
||||
//!
|
||||
//! It logs where it listens and the key it signs receipts and session records with. On
|
||||
|
|
@ -19,19 +19,19 @@ async fn main() -> ExitCode {
|
|||
["--config", path] => match std::fs::read_to_string(path).map_err(|e| e.to_string()).and_then(|s| GatewayConfig::from_toml(&s)) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
eprintln!("joule-moq-ingest: {path}: {e}");
|
||||
eprintln!("wai-ingest: {path}: {e}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
eprintln!("usage: joule-moq-ingest --config <file.toml>");
|
||||
eprintln!("usage: wai-ingest --config <file.toml>");
|
||||
return ExitCode::from(2);
|
||||
}
|
||||
};
|
||||
let server = match GatewayServer::start(cfg).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
eprintln!("joule-moq-ingest: {e}");
|
||||
eprintln!("wai-ingest: {e}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ T=$1
|
|||
OUT=$2
|
||||
shift 2
|
||||
mkdir -p "$OUT"
|
||||
JOULE_MOQ_RELAY_LOG=debug "$T/debug/joule-moq-relay" --dev > "$OUT/relay.log" 2>&1 &
|
||||
JOULE_MOQ_RELAY_LOG=debug "$T/debug/wai-relay" --dev > "$OUT/relay.log" 2>&1 &
|
||||
R=$!
|
||||
# The relay is gone before this script is: a next run binds the same port.
|
||||
trap 'kill $R 2>/dev/null; wait $R 2>/dev/null' EXIT
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
# scripts/local.sh <target-dir>
|
||||
set -u
|
||||
T=${1:-target}
|
||||
"$T/debug/joule-moq-relay" --dev > /tmp/joule-moq-relay-local.log 2>&1 &
|
||||
"$T/debug/wai-relay" --dev > /tmp/wai-relay-local.log 2>&1 &
|
||||
R=$!
|
||||
# The relay is gone before this script is: a next run binds the same port.
|
||||
trap 'kill $R 2>/dev/null; wait $R 2>/dev/null' EXIT
|
||||
|
|
|
|||
|
|
@ -10,8 +10,9 @@ repository = "https://github.com/transaction-science/open-standards"
|
|||
[lib]
|
||||
path = "src/lib.rs"
|
||||
|
||||
# The binary is `wai-relay`; the crate keeps its name (WAI toolchain decision 11).
|
||||
[[bin]]
|
||||
name = "joule-moq-relay"
|
||||
name = "wai-relay"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ draft's priorities give, dropping what passes a subscriber's delivery timeout at
|
|||
Whatever it drops, the object's receipts still reach the subscriber, on the track's
|
||||
receipts track.
|
||||
|
||||
It is a library (`Relay`, `RelayServer`, `client::Client`) and a binary,
|
||||
`joule-moq-relay`. The wire codec is `joule-moq`'s.
|
||||
It is a library (`Relay`, `RelayServer`, `client::Client`) and a binary, `wai-relay` (the
|
||||
crate keeps its name; the binary is named for WAI). The wire codec is `joule-moq`'s.
|
||||
|
||||
## The drafts it speaks
|
||||
|
||||
|
|
@ -740,7 +740,7 @@ The store must be kept where the room's region pin allows (live-rooms §3.7).
|
|||
|
||||
## Configuration
|
||||
|
||||
`joule-moq-relay --config relay.toml`, or `joule-moq-relay --dev` for loopback with a
|
||||
`wai-relay --config relay.toml`, or `wai-relay --dev` for loopback with a
|
||||
development certificate. `relay.example.toml` shows every field: the listen addresses and
|
||||
path, the certificate files (or the names for a development certificate), the cache's byte
|
||||
budget, the limits (sessions, requests and streams in flight, MAX_REQUEST_UPDATES, the token
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# joule-moq-relay: an example configuration. Every field is optional; these are the
|
||||
# wai-relay (crate joule-moq-relay): an example configuration. Every field is optional; these are the
|
||||
# defaults unless a comment says otherwise. Nothing secret belongs here: keys are files
|
||||
# the operator provides, and tokens are named by their SHA-256.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
//! `joule-moq-relay`: run the relay.
|
||||
//! `wai-relay` (crate `joule-moq-relay`): run the relay.
|
||||
//!
|
||||
//! ```text
|
||||
//! joule-moq-relay --config relay.toml
|
||||
//! joule-moq-relay --dev # loopback, a development certificate
|
||||
//! wai-relay --config relay.toml
|
||||
//! wai-relay --dev # loopback, a development certificate
|
||||
//! ```
|
||||
//!
|
||||
//! It logs where it listens, the certificate's SHA-256 (for a page's
|
||||
|
|
@ -15,7 +15,7 @@ use joule_moq_relay::{RelayConfig, RelayServer};
|
|||
use std::process::ExitCode;
|
||||
|
||||
fn usage() -> ExitCode {
|
||||
eprintln!("usage: joule-moq-relay (--config <file.toml> | --dev)");
|
||||
eprintln!("usage: wai-relay (--config <file.toml> | --dev)");
|
||||
ExitCode::from(2)
|
||||
}
|
||||
|
||||
|
|
@ -29,7 +29,7 @@ async fn main() -> ExitCode {
|
|||
["--config", path] => match std::fs::read_to_string(path).map_err(|e| e.to_string()).and_then(|s| RelayConfig::from_toml(&s)) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
eprintln!("joule-moq-relay: {path}: {e}");
|
||||
eprintln!("wai-relay: {path}: {e}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
},
|
||||
|
|
@ -38,7 +38,7 @@ async fn main() -> ExitCode {
|
|||
let server = match RelayServer::start(cfg).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
eprintln!("joule-moq-relay: {e}");
|
||||
eprintln!("wai-relay: {e}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ takes every principal's records in order. The host makes a key once, and runs in
|
|||
```
|
||||
wai-remote-host key ./host.key # mode 0600; prints the public key
|
||||
wai-remote-host run --room https://room.example/room-api --key ./host.key \
|
||||
[--invitation <hex from joule-moq-room invite>] --relay wss://relay.example/moq \
|
||||
[--invitation <hex from wai-room invite>] --relay wss://relay.example/moq \
|
||||
[--ca ./private-ca.pem] --floor floor --give-on-request
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -20,8 +20,9 @@ wtransport = { path = "../../vendor/wtransport" }
|
|||
[lib]
|
||||
path = "src/lib.rs"
|
||||
|
||||
# The binary is `wai-room`; the crate keeps its name (WAI toolchain decision 11).
|
||||
[[bin]]
|
||||
name = "joule-moq-room"
|
||||
name = "wai-room"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
# joule-moq-room
|
||||
|
||||
A room service for WAI live rooms (`wai/extensions/live-rooms.md`) over
|
||||
`joule-moq-relay`. It holds one room's authority and clock keys and:
|
||||
`joule-moq-relay`. Its binary is `wai-room` (the crate keeps its name). It holds one room's
|
||||
authority and clock keys and:
|
||||
|
||||
- **issues grants.** A visitor's page makes an Ed25519 key and asks for a grant for it
|
||||
(`POST /grant`). The service signs a short-lived `WRG1` grant: a publisher's (its own
|
||||
|
|
@ -27,7 +28,7 @@ A room service for WAI live rooms (`wai/extensions/live-rooms.md`) over
|
|||
permits `meta`, so that each principal sends the extension's statements. The host's grant
|
||||
carries `remote_host`; a controller's the control power, `remote_control` = `control`; a
|
||||
viewer's neither. The host is the key `host` names or, when it names none, the first key that
|
||||
presents an invitation the room's authority signed (`joule-moq-room invite`), to which the
|
||||
presents an invitation the room's authority signed (`wai-room invite`), to which the
|
||||
invitation is then bound. Who may ask for a controller's grant is `allow`: `all`, `listed`
|
||||
(the keys `controllers` lists) or `none`. The host hands control over by a grant exchange
|
||||
(`POST /remote/control`, its grant and a proof over the service's challenge), which takes
|
||||
|
|
@ -177,14 +178,14 @@ each room's grants and chain hold one stretch of company.
|
|||
## Running it
|
||||
|
||||
```text
|
||||
joule-moq-room keygen <keys.toml> new keys (mode 0600, never over a file); prints the
|
||||
relay configuration lines that trust them
|
||||
joule-moq-room relay-lines <keys.toml> those lines again
|
||||
joule-moq-room invite <keys.toml> [<s>] an invitation to host a remote desktop room, signed by
|
||||
the room's authority, good for <s> seconds (3600)
|
||||
joule-moq-room --config <room.toml> serve (room.example.toml shows every field)
|
||||
joule-moq-room dev [--publishers <n>] a development room: the relay and the service in one
|
||||
process on loopback, driven by JSON control lines
|
||||
wai-room keygen <keys.toml> new keys (mode 0600, never over a file); prints the
|
||||
relay configuration lines that trust them
|
||||
wai-room relay-lines <keys.toml> those lines again
|
||||
wai-room invite <keys.toml> [<s>] an invitation to host a remote desktop room, signed by
|
||||
the room's authority, good for <s> seconds (3600)
|
||||
wai-room --config <room.toml> serve (room.example.toml shows every field)
|
||||
wai-room dev [--publishers <n>] a development room: the relay and the service in one
|
||||
process on loopback, driven by JSON control lines
|
||||
```
|
||||
|
||||
The keys file must be readable by its owner alone; the service refuses it otherwise.
|
||||
|
|
@ -218,7 +219,7 @@ trust_field = 1
|
|||
|
||||
### A development room
|
||||
|
||||
`joule-moq-room dev` (`src/dev.rs`) runs a relay and the service together in one process, for
|
||||
`wai-room dev` (`src/dev.rs`) runs a relay and the service together in one process, for
|
||||
a developer's machine and for tests, and never for anyone else. It binds 127.0.0.1 alone, on
|
||||
ports the system picks; its keys are made at start and kept only in the process; its relay
|
||||
holds media tracks to no receipts, so a test principal publishes plain objects; and its rate
|
||||
|
|
|
|||
|
|
@ -1378,7 +1378,7 @@
|
|||
},
|
||||
"/watch/challenge": {
|
||||
"get": {
|
||||
"operationId": "watchchallenge.create",
|
||||
"operationId": "watch.setup",
|
||||
"tags": [
|
||||
"watch"
|
||||
],
|
||||
|
|
@ -2151,7 +2151,7 @@
|
|||
},
|
||||
"/msg/challenge": {
|
||||
"get": {
|
||||
"operationId": "conversationchallenge.create",
|
||||
"operationId": "conversation.setup",
|
||||
"tags": [
|
||||
"messaging"
|
||||
],
|
||||
|
|
@ -3180,7 +3180,7 @@
|
|||
},
|
||||
"/call/challenge": {
|
||||
"get": {
|
||||
"operationId": "callchallenge.create",
|
||||
"operationId": "call.setup",
|
||||
"tags": [
|
||||
"calls"
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# joule-moq-room: an example configuration. Nothing secret belongs here: the keys are a
|
||||
# file of their own (`joule-moq-room keygen <path>` makes it, mode 0600), outside every
|
||||
# wai-room (crate joule-moq-room): an example configuration. Nothing secret belongs here: the
|
||||
# keys are a file of their own (`wai-room keygen <path>` makes it, mode 0600), outside every
|
||||
# repository, and the relay knows the clock's token by its SHA-256 alone.
|
||||
|
||||
# The HTTP service, on loopback behind the front that serves the room's page.
|
||||
|
|
@ -128,7 +128,7 @@ global_every_s = 10
|
|||
|
||||
# Every room is a remote desktop room (WAI remote-desktop): one host's screen shared into it. The
|
||||
# host is the key `host` names, or, empty, the first key that presents an invitation the room's
|
||||
# authority signed (`joule-moq-room invite <keys.toml>`). Not also e2ee, a watch room, a room with
|
||||
# authority signed (`wai-room invite <keys.toml>`). Not also e2ee, a watch room, a room with
|
||||
# co-performance, one admitted by age or a world room. The relay holds the host's screen to its key
|
||||
# with a door:
|
||||
# [[receipts.door]] prefix = ["*", "*", "screen"], trust_field = 1.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//! A **development room**: the relay and the room service in one process, on loopback, for a
|
||||
//! developer's machine and for tests. `joule-moq-room dev` runs one and takes its control
|
||||
//! developer's machine and for tests. `wai-room dev` runs one and takes its control
|
||||
//! lines on standard input; `wai mcp` drives it that way.
|
||||
//!
|
||||
//! It is not a deployment, and differs from one on purpose:
|
||||
|
|
@ -56,7 +56,7 @@ const STEP: Duration = Duration::from_secs(20);
|
|||
const MAX_OBJECTS: u64 = 64;
|
||||
|
||||
/// What `stderr` says first: the room is for development only.
|
||||
pub const BANNER: &str = "joule-moq-room dev: a DEVELOPMENT room on loopback. Its keys exist only in this process, its relay \
|
||||
pub const BANNER: &str = "wai-room dev: a DEVELOPMENT room on loopback. Its keys exist only in this process, its relay \
|
||||
holds media tracks to no receipts, and its rate limits are open. Never serve it to anyone.";
|
||||
|
||||
/// A test principal that has entered: its session and the tracks it holds open.
|
||||
|
|
@ -369,7 +369,7 @@ impl DevRoom {
|
|||
}
|
||||
}
|
||||
|
||||
/// `joule-moq-room dev [--publishers <n>]`: run a development room and answer control lines
|
||||
/// `wai-room dev [--publishers <n>]`: run a development room and answer control lines
|
||||
/// until `stop` or until standard input closes.
|
||||
pub async fn run(args: &[String]) -> std::process::ExitCode {
|
||||
let mut publishers: u16 = 4;
|
||||
|
|
@ -380,14 +380,14 @@ pub async fn run(args: &[String]) -> std::process::ExitCode {
|
|||
publishers = match n.parse() {
|
||||
Ok(n) if (1..=64).contains(&n) => n,
|
||||
_ => {
|
||||
eprintln!("joule-moq-room dev: --publishers takes 1 to 64");
|
||||
eprintln!("wai-room dev: --publishers takes 1 to 64");
|
||||
return std::process::ExitCode::from(2);
|
||||
}
|
||||
};
|
||||
it.next();
|
||||
}
|
||||
_ => {
|
||||
eprintln!("usage: joule-moq-room dev [--publishers <n>]");
|
||||
eprintln!("usage: wai-room dev [--publishers <n>]");
|
||||
return std::process::ExitCode::from(2);
|
||||
}
|
||||
}
|
||||
|
|
@ -396,7 +396,7 @@ pub async fn run(args: &[String]) -> std::process::ExitCode {
|
|||
let room = match DevRoom::start(publishers, "").await {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
eprintln!("joule-moq-room dev: {e}");
|
||||
eprintln!("wai-room dev: {e}");
|
||||
println!("{}", json!({"ready": false, "error": e}));
|
||||
return std::process::ExitCode::FAILURE;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ impl Keys {
|
|||
|
||||
pub fn to_toml(&self) -> String {
|
||||
format!(
|
||||
"# joule-moq-room's keys. Secret: mode 0600, outside every repository.\nauthority_seed = \"{}\"\nclock_seed = \"{}\"\nclock_token = \"{}\"\n",
|
||||
"# wai-room's keys. Secret: mode 0600, outside every repository.\nauthority_seed = \"{}\"\nclock_seed = \"{}\"\nclock_token = \"{}\"\n",
|
||||
hex::encode(self.authority.to_bytes()),
|
||||
hex::encode(self.clock.to_bytes()),
|
||||
hex::encode(self.clock_token)
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
//! `joule-moq-room`: run the room service.
|
||||
//! `wai-room` (crate `joule-moq-room`): run the room service.
|
||||
//!
|
||||
//! ```text
|
||||
//! joule-moq-room keygen <keys.toml> new keys (mode 0600, never over a file), and the
|
||||
//! wai-room keygen <keys.toml> new keys (mode 0600, never over a file), and the
|
||||
//! relay configuration lines that trust them
|
||||
//! joule-moq-room relay-lines <keys.toml> those lines again, for keys that exist
|
||||
//! joule-moq-room invite <keys.toml> [<s>] an invitation to host a remote desktop room
|
||||
//! wai-room relay-lines <keys.toml> those lines again, for keys that exist
|
||||
//! wai-room invite <keys.toml> [<s>] an invitation to host a remote desktop room
|
||||
//! (`[remote]` with no `host`), signed by the
|
||||
//! room's authority, good for <s> seconds (3600)
|
||||
//! joule-moq-room --config <room.toml> serve
|
||||
//! joule-moq-room dev [--publishers <n>] a development room on loopback: the relay and the
|
||||
//! service in one process, fresh keys held in memory,
|
||||
//! driven by JSON control lines on standard input
|
||||
//! (`joule_moq_room::dev`); never for deployment
|
||||
//! wai-room --config <room.toml> serve
|
||||
//! wai-room dev [--publishers <n>] a development room on loopback: the relay and the
|
||||
//! service in one process, fresh keys held in memory,
|
||||
//! driven by JSON control lines on standard input
|
||||
//! (`joule_moq_room::dev`); never for deployment
|
||||
//! ```
|
||||
//!
|
||||
//! It stops on Ctrl-C or SIGTERM. With `max_resident_mb` set it exits with status 70 once
|
||||
|
|
@ -23,7 +23,7 @@ use std::path::Path;
|
|||
use std::process::ExitCode;
|
||||
|
||||
fn usage() -> ExitCode {
|
||||
eprintln!("usage: joule-moq-room (keygen <keys.toml> | relay-lines <keys.toml> | invite <keys.toml> [<seconds>] | --config <room.toml> | dev [--publishers <n>])");
|
||||
eprintln!("usage: wai-room (keygen <keys.toml> | relay-lines <keys.toml> | invite <keys.toml> [<seconds>] | --config <room.toml> | dev [--publishers <n>])");
|
||||
ExitCode::from(2)
|
||||
}
|
||||
|
||||
|
|
@ -43,11 +43,11 @@ async fn main() -> ExitCode {
|
|||
["keygen", path] => match Keys::create(Path::new(path)) {
|
||||
Ok(k) => {
|
||||
println!("{}", k.relay_lines());
|
||||
eprintln!("joule-moq-room: keys written to {path} (mode 0600)");
|
||||
eprintln!("wai-room: keys written to {path} (mode 0600)");
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("joule-moq-room: {e}");
|
||||
eprintln!("wai-room: {e}");
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
},
|
||||
|
|
@ -57,7 +57,7 @@ async fn main() -> ExitCode {
|
|||
ExitCode::SUCCESS
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("joule-moq-room: {e}");
|
||||
eprintln!("wai-room: {e}");
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
},
|
||||
|
|
@ -70,11 +70,11 @@ async fn main() -> ExitCode {
|
|||
match Keys::load(Path::new(path)) {
|
||||
Ok(k) => {
|
||||
println!("{}", joule_moq_room::remote::new_invitation(&k, valid_s));
|
||||
eprintln!("joule-moq-room: an invitation to host, good for {valid_s} s, bound to the first key that presents it");
|
||||
eprintln!("wai-room: an invitation to host, good for {valid_s} s, bound to the first key that presents it");
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("joule-moq-room: {e}");
|
||||
eprintln!("wai-room: {e}");
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
|
|
@ -83,14 +83,14 @@ async fn main() -> ExitCode {
|
|||
let cfg = match std::fs::read_to_string(path).map_err(|e| e.to_string()).and_then(|s| Config::from_toml(&s)) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
eprintln!("joule-moq-room: {path}: {e}");
|
||||
eprintln!("wai-room: {path}: {e}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
let keys = match Keys::load(&cfg.keys) {
|
||||
Ok(k) => k,
|
||||
Err(e) => {
|
||||
eprintln!("joule-moq-room: {e}");
|
||||
eprintln!("wai-room: {e}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
|
|
@ -104,14 +104,14 @@ async fn serve(cfg: Config, keys: Keys) -> ExitCode {
|
|||
let listener = match tokio::net::TcpListener::bind(cfg.listen).await {
|
||||
Ok(l) => l,
|
||||
Err(e) => {
|
||||
eprintln!("joule-moq-room: binding {}: {e}", cfg.listen);
|
||||
eprintln!("wai-room: binding {}: {e}", cfg.listen);
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
let svc = match Service::try_new(keys, &cfg) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
eprintln!("joule-moq-room: {e}");
|
||||
eprintln!("wai-room: {e}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
//! `screen` kind; a controller's carries the control power, `remote_control` with the classes
|
||||
//! the configuration gives (`control`); a viewer's carries neither. The host is the key the
|
||||
//! configuration names (`host`), or, when it names none, the first visitor who presents an
|
||||
//! invitation the room's authority signed (`joule-moq-room invite`); an invitation is bound to
|
||||
//! invitation the room's authority signed (`wai-room invite`); an invitation is bound to
|
||||
//! the key that presented it first. Who may ask for a controller's grant is the configuration's
|
||||
//! allow rule (`allow`: everyone, the keys listed, or no one); the host hands control over by a
|
||||
//! grant exchange (live-rooms §4.6, §7.1 here), which it asks for with its grant and a proof of
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//! End to end, as a coding agent works: an MCP client drives `wai mcp --toolset core,dev`, which
|
||||
//! runs this crate's development room (`joule-moq-room dev`) as a child, and through its tools
|
||||
//! runs this crate's development room (`wai-room dev`) as a child, and through its tools
|
||||
//!
|
||||
//! - finds the one operation the two doors share named alike: the tool `room_grant` is the
|
||||
//! operation `room.grant`, which `openapi.json` describes as the service's `POST /grant`;
|
||||
|
|
@ -48,7 +48,7 @@ impl Agent {
|
|||
.args(["mcp", "--toolset", "core,dev", "--project"])
|
||||
.arg(project)
|
||||
.arg("--room-bin")
|
||||
.arg(env!("CARGO_BIN_EXE_joule-moq-room"))
|
||||
.arg(env!("CARGO_BIN_EXE_wai-room"))
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::inherit())
|
||||
|
|
@ -143,7 +143,7 @@ fn an_agent_starts_a_room_joins_publishes_and_verifies_the_session() {
|
|||
assert_eq!(again["key"].as_str(), Some(keys[0].as_str()));
|
||||
|
||||
// The session, verified.
|
||||
let s = a.ok("room_session", json!({}));
|
||||
let s = a.ok("session_show", json!({}));
|
||||
assert_eq!(s["room"].as_str(), Some(id.as_str()));
|
||||
let args = json!({"descriptor": s["descriptor"], "grants": s["grants"], "key_log_head": s["key_log_head"], "chains": s["chains"]});
|
||||
let v = a.ok("receipt_verify", args.clone());
|
||||
|
|
@ -168,13 +168,13 @@ fn an_agent_starts_a_room_joins_publishes_and_verifies_the_session() {
|
|||
// Alice leaves; the session verifies with Bob alone present.
|
||||
let l = a.ok("room_leave", json!({"principal": "alice"}));
|
||||
assert!(l["events"].as_array().unwrap().iter().any(|e| e == "leave"), "{l}");
|
||||
let s = a.ok("room_session", json!({}));
|
||||
let s = a.ok("session_show", json!({}));
|
||||
let v = a.ok("receipt_verify", json!({"kind": "room_session", "descriptor": s["descriptor"], "grants": s["grants"], "key_log_head": s["key_log_head"], "chains": s["chains"]}));
|
||||
assert_eq!(v["outcome"], "accept", "{v}");
|
||||
assert_eq!(v["present"], json!([keys[1]]), "{v}");
|
||||
assert_eq!(v["records_in_order"], 7);
|
||||
|
||||
assert_eq!(a.ok("dev_stop", json!({}))["stopped"], true);
|
||||
assert!(a.call("room_session", json!({})).is_err(), "no room once stopped");
|
||||
assert!(a.call("session_show", json!({})).is_err(), "no room once stopped");
|
||||
let _ = std::fs::remove_dir_all(&project);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
//! The service's OpenAPI description (`openapi.json`) held to the code, in both directions:
|
||||
//!
|
||||
//! - **Names.** Every operation's `operationId` is its name in the toolchain's catalogue, the
|
||||
//! one `HANDLERS` gives it: `noun.verb` by the rule every door keeps (`wai::ops::check_id`),
|
||||
//! each name once.
|
||||
//! one `HANDLERS` gives it: an operation of `wai::catalogue` that runs on this service, at the
|
||||
//! route the catalogue gives it, each name once; and every operation the catalogue routes to
|
||||
//! this service is described here.
|
||||
//! - **Routes.** The (method, path) pairs the routers match (`src/http.rs`,
|
||||
//! `src/messaging.rs`, `src/call.rs`) are exactly the description's operations.
|
||||
//! - **Statuses, from the source.** Each operation names the functions that answer it below
|
||||
|
|
@ -139,7 +140,7 @@ const HANDLERS: &[(&str, &str, &str, Handlers)] = &[
|
|||
("POST", "/co/fidelity", "fidelity.create", &[("http.rs", "co_request"), ("http.rs", "unauthorised"), ("http.rs", "signed_reply")]),
|
||||
("GET", "/watch/time", "time.show", &[("http.rs", "handle")]),
|
||||
("GET", "/watch/source", "source.fetch", &[("http.rs", "watch_source")]),
|
||||
("GET", "/watch/challenge", "watchchallenge.create", &[("http.rs", "handle")]),
|
||||
("GET", "/watch/challenge", "watch.setup", &[("http.rs", "handle")]),
|
||||
("GET", "/watch", "watch.tail", &[("http.rs", "watch_log")]),
|
||||
("GET", "/watch/room", "watch.show", &[("http.rs", "handle")]),
|
||||
("POST", "/watch/join", "watch.join", &[("http.rs", "handle"), ("http.rs", "watch_join")]),
|
||||
|
|
@ -148,7 +149,7 @@ const HANDLERS: &[(&str, &str, &str, Handlers)] = &[
|
|||
("GET", "/remote/room", "remote.show", &[("remote.rs", "session"), ("remote.rs", "admit_reader"), ("http.rs", "unauthorised")]),
|
||||
("GET", "/remote/state", "remote.verify", &[("remote.rs", "state"), ("remote.rs", "admit_reader"), ("http.rs", "unauthorised")]),
|
||||
("GET", "/msg", "conversation.show", &[("messaging.rs", "handle")]),
|
||||
("GET", "/msg/challenge", "conversationchallenge.create", &[("messaging.rs", "handle")]),
|
||||
("GET", "/msg/challenge", "conversation.setup", &[("messaging.rs", "handle")]),
|
||||
("POST", "/msg/join", "conversation.join", &[("messaging.rs", "handle"), ("messaging.rs", "join"), ("messaging.rs", "unauthorised")]),
|
||||
("GET", "/msg/log", "conversation.tail", &[("messaging.rs", "handle"), ("messaging.rs", "log"), ("messaging.rs", "unauthorised")]),
|
||||
("POST", "/msg/key-package", "conversation.import", &[("messaging.rs", "handle"), ("messaging.rs", "unauthorised"), ("messaging.rs", "body"), ("messaging.rs", "act")]),
|
||||
|
|
@ -157,7 +158,7 @@ const HANDLERS: &[(&str, &str, &str, Handlers)] = &[
|
|||
("POST", "/msg/ack", "acknowledgement.create", &[("messaging.rs", "handle"), ("messaging.rs", "unauthorised"), ("messaging.rs", "body"), ("messaging.rs", "act")]),
|
||||
("POST", "/msg/leave", "conversation.leave", &[("messaging.rs", "handle"), ("messaging.rs", "unauthorised"), ("messaging.rs", "body"), ("messaging.rs", "act")]),
|
||||
("POST", "/msg/remove", "member.delete", &[("messaging.rs", "handle"), ("messaging.rs", "unauthorised"), ("messaging.rs", "body"), ("messaging.rs", "act")]),
|
||||
("GET", "/call/challenge", "callchallenge.create", &[("http.rs", "handle"), ("call.rs", "handle")]),
|
||||
("GET", "/call/challenge", "call.setup", &[("http.rs", "handle"), ("call.rs", "handle")]),
|
||||
("POST", "/call/place", "call.create", &[("http.rs", "handle"), ("call.rs", "handle"), ("call.rs", "unauthorised"), ("call.rs", "body"), ("call.rs", "place")]),
|
||||
("POST", "/call/ring", "call.invite", &[("http.rs", "handle"), ("call.rs", "handle"), ("http.rs", "unauthorised"), ("call.rs", "body"), ("call.rs", "ring")]),
|
||||
("GET", "/inbox", "inbox.show", &[("http.rs", "handle"), ("call.rs", "handle"), ("call.rs", "unauthorised"), ("call.rs", "inbox")]),
|
||||
|
|
@ -182,14 +183,17 @@ fn every_operation_is_named_as_the_catalogue_names_it() {
|
|||
let doc = description();
|
||||
let mut seen = BTreeSet::new();
|
||||
for (m, p, op, _) in HANDLERS {
|
||||
assert_eq!(wai::ops::check_id(op), Ok(()), "{m} {p}: {op}");
|
||||
let o = wai::catalogue::op(op).unwrap_or_else(|| panic!("{m} {p}: {op} is not an operation of the catalogue (wai/wai-rs/src/catalogue.rs)"));
|
||||
let r = o.route.unwrap_or_else(|| panic!("{op}: the catalogue gives it no route"));
|
||||
assert_eq!((r.service, r.method, r.path), ("wai-room", *m, *p), "{op}: the catalogue's route");
|
||||
assert!(seen.insert(*op), "{op} names two operations");
|
||||
let id = doc["paths"][*p][m.to_ascii_lowercase()]["operationId"].as_str();
|
||||
assert_eq!(id, Some(*op), "{m} {p}: its operationId is its name in the catalogue");
|
||||
}
|
||||
// The operations the catalogue names with their routes.
|
||||
for (m, p, op) in [("GET", "/room", "room.show"), ("POST", "/grant", "room.grant")] {
|
||||
assert!(HANDLERS.iter().any(|h| (h.0, h.1, h.2) == (m, p, op)), "{op} is {m} {p}");
|
||||
// Every operation the catalogue routes to this service is one of these.
|
||||
for o in wai::catalogue::OPS.iter().filter(|o| o.route.is_some_and(|r| r.service == "wai-room")) {
|
||||
let r = o.route.expect("routed");
|
||||
assert!(HANDLERS.iter().any(|h| (h.0, h.1, h.2) == (r.method, r.path, o.id)), "the catalogue routes {} to {} {}, which this service does not describe", o.id, r.method, r.path);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ function relayToml(c) {
|
|||
// A DVR window of every track (WAI live-recording §8).
|
||||
if (c.dvr) t += `[dvr]\npath = ${q(c.dvr.path)}\nroom_keys = ${arr(c.dvr.room_keys || [])}\n\n[[dvr.window]]\nprefix = ["*"]\nmax_age_s = ${c.dvr.max_age_s}\nmax_bytes = ${c.dvr.max_bytes}\n\n`;
|
||||
t += `[summary]\nout_path = ${q(c.summary_out)}\n\n[auth]\nrequired = ${c.auth ? "true" : "false"}\n\n`;
|
||||
// Lines a room service prints for the relay that trusts it (`joule-moq-room relay-lines`).
|
||||
// Lines a room service prints for the relay that trusts it (`wai-room relay-lines`).
|
||||
if (c.extra) t += c.extra + "\n";
|
||||
// A grant that may subscribe may also seek back below the live edge (live-rooms §10.6),
|
||||
// as a late joiner does, unless the test says otherwise.
|
||||
|
|
@ -181,7 +181,7 @@ export function tmpdir(label) {
|
|||
return fs.mkdtempSync(path.join(process.env.TMPDIR || os.tmpdir(), `moq-web-${label}-`));
|
||||
}
|
||||
|
||||
// A room service (joule-moq-room) for a relay: its keys made in `dir` (mode 0600), the
|
||||
// A room service (wai-room, crate joule-moq-room) for a relay: its keys made in `dir` (mode 0600), the
|
||||
// relay's lines that trust them, then, once the relay listens, the service itself.
|
||||
export function roomKeys(bin, dir) {
|
||||
const keys = path.join(dir, "room-keys.toml");
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
// End-to-end tests of the messaging view (web/room/messages.html) and the call flow
|
||||
// (web/room/call.html) against a room service (joule-moq-room) on loopback, in the host's
|
||||
// (web/room/call.html) against a room service (wai-room) on loopback, in the host's
|
||||
// installed browser driven by the automation package of e2e/package.json, with a fake camera
|
||||
// and microphone (WAI live-messaging).
|
||||
//
|
||||
// node e2e/messaging.e2e.mjs --room <joule-moq-room> --relay <joule-moq-relay> --web <web dir>
|
||||
// node e2e/messaging.e2e.mjs --room <wai-room> --relay <wai-relay> --web <web dir>
|
||||
// [--browser <executable>] [--only messaging,call] [--out <dir>] [--runs N]
|
||||
//
|
||||
// Scenarios:
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@
|
|||
// host's installed browser driven by the automation package of e2e/package.json, with a
|
||||
// fake camera and microphone.
|
||||
//
|
||||
// node e2e/room.e2e.mjs --relay <joule-moq-relay> --wai <wai_live_web> --web <web dir>
|
||||
// node e2e/room.e2e.mjs --relay <wai-relay> --wai <wai_live_web> --web <web dir>
|
||||
// --corpus <live-conformance/compose/vectors.json> [--browser <executable>]
|
||||
// [--only compose,webtransport,websocket,goaway,dvr,service,e2ee,age,co,ageco,binding,cowatch,
|
||||
// origin,termination,remote,world,privateworld]
|
||||
// [--runs N]
|
||||
// [--between <s>] [--out <dir>] [--room <joule-moq-room>] [--base <origin>] [--seek on|off]
|
||||
// [--between <s>] [--out <dir>] [--room <wai-room>] [--base <origin>] [--seek on|off]
|
||||
// [--wai-dir <wai>]
|
||||
//
|
||||
// Scenarios:
|
||||
|
|
@ -564,7 +564,7 @@ async function scenarioService(run) {
|
|||
if (BASE) {
|
||||
base = `${BASE}/room/`;
|
||||
} else {
|
||||
if (!ROOM) throw new Error("--room <joule-moq-room> is needed for the service scenario");
|
||||
if (!ROOM) throw new Error("--room <wai-room> is needed for the service scenario");
|
||||
const k = roomKeys(ROOM, dir);
|
||||
const door = ["audio", "video"].map((m) => ({ prefix: ["*", "*", m], trust_field: 1, hold_ms: 2000 }));
|
||||
// A DVR window over every track and a cache far smaller than the seconds sought back, as
|
||||
|
|
@ -872,7 +872,7 @@ async function provenGet(url, grantHex, seedHex) {
|
|||
// remote_desktop_verify.py, from the extension's text alone.
|
||||
async function scenarioRemote(run) {
|
||||
const label = "remote";
|
||||
if (!REMOTE || !ROOM) throw new Error("--remote <wai-remote-host> and --room <joule-moq-room> are needed for the remote scenario");
|
||||
if (!REMOTE || !ROOM) throw new Error("--remote <wai-remote-host> and --room <wai-room> are needed for the remote scenario");
|
||||
const dir = path.join(OUT, `run${run}-remote`);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
const k = roomKeys(ROOM, dir);
|
||||
|
|
@ -1051,7 +1051,7 @@ async function scenarioE2ee(run) {
|
|||
const label = "e2ee";
|
||||
const dir = path.join(OUT, `run${run}-e2ee`);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
if (!ROOM) throw new Error("--room <joule-moq-room> is needed for the e2ee scenario");
|
||||
if (!ROOM) throw new Error("--room <wai-room> is needed for the e2ee scenario");
|
||||
const k = roomKeys(ROOM, dir);
|
||||
const door = ["audio", "video"].map((m) => ({ prefix: ["*", "*", m], trust_field: 1, hold_ms: 2000 }));
|
||||
// A DVR window over every track: the relay keeps nothing of a room whose objects are sealed.
|
||||
|
|
@ -1251,7 +1251,7 @@ async function scenarioAge(run) {
|
|||
const label = "age";
|
||||
const dir = path.join(OUT, `run${run}-age`);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
if (!ROOM) throw new Error("--room <joule-moq-room> is needed for the age scenario");
|
||||
if (!ROOM) throw new Error("--room <wai-room> is needed for the age scenario");
|
||||
const keys = testKeys(path.join(path.dirname(CORPUS), "..", "age", "vectors.json"));
|
||||
const issuer = await startIssuer(keys);
|
||||
const k = roomKeys(ROOM, dir);
|
||||
|
|
@ -1351,7 +1351,7 @@ async function scenarioOrigin(run) {
|
|||
const label = "origin";
|
||||
const dir = path.join(OUT, `run${run}-origin`);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
if (!ROOM) throw new Error("--room <joule-moq-room> is needed for the origin scenario");
|
||||
if (!ROOM) throw new Error("--room <wai-room> is needed for the origin scenario");
|
||||
const k = roomKeys(ROOM, dir);
|
||||
const door = ["audio", "video"].map((m) => ({ prefix: ["*", "*", m], trust_field: 1, hold_ms: 2000 }));
|
||||
const relay = await startRelay(RELAY, { no_quic: true, auth: true, door, extra: k.lines, summary_out: path.join(dir, "summaries.jsonl"), limits: { max_session_ms: 600000 } }, dir, "relay");
|
||||
|
|
@ -1450,7 +1450,7 @@ async function scenarioAgeCo(run) {
|
|||
const label = "ageco";
|
||||
const dir = path.join(OUT, `run${run}-ageco`);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
if (!ROOM) throw new Error("--room <joule-moq-room> is needed for the ageco scenario");
|
||||
if (!ROOM) throw new Error("--room <wai-room> is needed for the ageco scenario");
|
||||
const keys = testKeys(path.join(path.dirname(CORPUS), "..", "age", "vectors.json"));
|
||||
const issuer = await startIssuer(keys);
|
||||
const k = roomKeys(ROOM, dir);
|
||||
|
|
@ -1555,7 +1555,7 @@ async function scenarioCo(run) {
|
|||
const label = "co";
|
||||
const dir = path.join(OUT, `run${run}-co`);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
if (!ROOM) throw new Error("--room <joule-moq-room> is needed for the co scenario");
|
||||
if (!ROOM) throw new Error("--room <wai-room> is needed for the co scenario");
|
||||
const k = roomKeys(ROOM, dir);
|
||||
const door = ["audio", "video"].map((m) => ({ prefix: ["*", "*", m], trust_field: 1, hold_ms: 2000 }));
|
||||
// The relay holds the floor and the audience's inputs, honouring the clock's statements.
|
||||
|
|
@ -2022,7 +2022,7 @@ async function scenarioCowatch(run) {
|
|||
const label = "cowatch";
|
||||
const dir = path.join(OUT, `run${run}-cowatch`);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
if (!ROOM) throw new Error("--room <joule-moq-room> is needed for the cowatch scenario");
|
||||
if (!ROOM) throw new Error("--room <wai-room> is needed for the cowatch scenario");
|
||||
const k = roomKeys(ROOM, dir);
|
||||
const source = path.join(dir, "programme.wav");
|
||||
const wav = writeWav(source, 120);
|
||||
|
|
@ -2303,7 +2303,7 @@ const WORLD_TOML = `[world]\nmodel_id = "${"07".repeat(32)}"\nwidth = 64\nheight
|
|||
async function worldRoom(run, label, sealed) {
|
||||
const dir = path.join(OUT, `run${run}-${label}`);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
if (!ROOM) throw new Error(`--room <joule-moq-room> is needed for the ${label} scenario`);
|
||||
if (!ROOM) throw new Error(`--room <wai-room> is needed for the ${label} scenario`);
|
||||
const k = roomKeys(ROOM, dir);
|
||||
// The relay-lines' world door (§4.3), and a DVR window that keeps the room's world tracks for
|
||||
// a page that enters late (§7); it keeps nothing of a private world.
|
||||
|
|
|
|||
|
|
@ -43,9 +43,9 @@ mkdir -p "$PKG"
|
|||
|
||||
step() { echo "== $*"; }
|
||||
|
||||
step "native: joule-moq-relay, joule-moq-room, wai_live_web"
|
||||
(cd "$JC" && cargo build $LOCKED --release -p joule-moq-relay --bin joule-moq-relay)
|
||||
(cd "$CRATE/../joule-moq-room" && cargo build $LOCKED --release --bin joule-moq-room)
|
||||
step "native: wai-relay, wai-room, wai_live_web"
|
||||
(cd "$JC" && cargo build $LOCKED --release -p joule-moq-relay --bin wai-relay)
|
||||
(cd "$CRATE/../joule-moq-room" && cargo build $LOCKED --release --bin wai-room)
|
||||
(cd "$CRATE/../joule-moq-remote" && cargo build $LOCKED --release --bin wai-remote-host)
|
||||
(cd "$WLW" && cargo build $LOCKED --release --bin wai_live_web)
|
||||
|
||||
|
|
@ -93,8 +93,8 @@ while [ $# -gt 0 ]; do
|
|||
done
|
||||
if [ "$only" = 0 ] || [ -n "$room_only" ]; then
|
||||
node "$HERE/room.e2e.mjs" \
|
||||
--relay "$TARGET/release/joule-moq-relay" \
|
||||
--room "$TARGET/release/joule-moq-room" \
|
||||
--relay "$TARGET/release/wai-relay" \
|
||||
--room "$TARGET/release/wai-room" \
|
||||
--wai "$TARGET/release/wai_live_web" \
|
||||
--remote "$TARGET/release/wai-remote-host" \
|
||||
--web "$CRATE/web" \
|
||||
|
|
@ -108,8 +108,8 @@ fi
|
|||
if [ -n "$msg_only" ]; then
|
||||
step "end-to-end: conversations and calls"
|
||||
node "$HERE/messaging.e2e.mjs" \
|
||||
--relay "$TARGET/release/joule-moq-relay" \
|
||||
--room "$TARGET/release/joule-moq-room" \
|
||||
--relay "$TARGET/release/wai-relay" \
|
||||
--room "$TARGET/release/wai-room" \
|
||||
--web "$CRATE/web" \
|
||||
--out "$OUT/results/messaging" \
|
||||
--only "$msg_only" \
|
||||
|
|
|
|||
160
scripts/check-wai-ops-doors-selftest.sh
Executable file
160
scripts/check-wai-ops-doors-selftest.sh
Executable file
|
|
@ -0,0 +1,160 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# check-wai-ops-doors-selftest.sh — check-wai-ops-doors.py can fail.
|
||||
#
|
||||
# A check that cannot fail is decoration. This builds a scratch tree shaped like the
|
||||
# repository (a catalogue export, an MCP tool table, an OpenAPI document, the C header and a
|
||||
# Swift and a Kotlin source) whose every door names its operations as the catalogue does, and
|
||||
# asserts that the check passes it and fails each way a door can misname an operation,
|
||||
# saying which.
|
||||
set -uo pipefail
|
||||
|
||||
here=$(cd "$(dirname "$0")" && pwd)
|
||||
check="$here/check-wai-ops-doors.py"
|
||||
tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
|
||||
mk() {
|
||||
rm -rf "$tmp/wai" "$tmp/joule-code"
|
||||
mkdir -p "$tmp/wai/catalogue" "$tmp/wai/wai-rs/src/bin/wai_cli/mcp" "$tmp/wai/wai-rs/include" \
|
||||
"$tmp/joule-code/joule-code-rs/crates/svc" "$tmp/wai/wai-sink-swift/Sources/WaiSink" \
|
||||
"$tmp/wai/wai-sink-android/core/src/main/kotlin/wai/sink"
|
||||
cat > "$tmp/wai/catalogue/ops.json" <<'J'
|
||||
{"format": "wai.ops/1", "operations": [
|
||||
{"id": "envelope.inspect", "placement": "local", "route": null},
|
||||
{"id": "receipt.verify", "placement": "local", "route": null},
|
||||
{"id": "room.grant", "placement": "service", "route": {"method": "POST", "path": "/v1/rooms/{room}/grants"}}
|
||||
]}
|
||||
J
|
||||
cat > "$tmp/wai/wai-rs/src/bin/wai_cli/mcp/tools.rs" <<'R'
|
||||
pub(crate) const TOOLS: &[Tool] = &[
|
||||
Tool { name: "envelope_inspect", title: "Inspect", run: inspect },
|
||||
Tool {
|
||||
name: "room_grant",
|
||||
title: "Grant",
|
||||
run: grant,
|
||||
},
|
||||
];
|
||||
R
|
||||
cat > "$tmp/joule-code/joule-code-rs/crates/svc/openapi.json" <<'J'
|
||||
{"openapi": "3.1.0", "paths": {"/v1/rooms/{room}/grants": {"post": {"operationId": "room.grant"}, "parameters": []}}}
|
||||
J
|
||||
cat > "$tmp/wai/wai-rs/include/wai.h" <<'H'
|
||||
int32_t wai_envelope_inspect(const uint8_t *p, size_t n);
|
||||
int32_t wai_envelope_pack(const uint8_t *p, size_t n);
|
||||
void wai_buffer_free(struct WaiBuffer b);
|
||||
int32_t wai_image_png_decode(const uint8_t *p);
|
||||
H
|
||||
cat > "$tmp/wai/wai-sink-swift/Sources/WaiSink/Envelope.swift" <<'S'
|
||||
public final class WaiEnvelope: @unchecked Sendable {
|
||||
public init() {}
|
||||
public func inspect(_ path: String) -> String { "{" }
|
||||
public func pack() {}
|
||||
private func verifyQuietly() {}
|
||||
}
|
||||
public final class WaiSink {
|
||||
public func info() -> String { "" }
|
||||
}
|
||||
S
|
||||
cat > "$tmp/wai/wai-sink-android/core/src/main/kotlin/wai/sink/WaiReceipt.kt" <<'K'
|
||||
package wai.sink
|
||||
|
||||
class WaiReceipt(
|
||||
val owner: String,
|
||||
private val pool: Runnable = Runnable { },
|
||||
) {
|
||||
fun verify(bytes: ByteArray): Boolean = bytes.isNotEmpty()
|
||||
private fun checkQuietly() {}
|
||||
companion object {
|
||||
fun verifyAll(list: List<ByteArray>) = list.all { it.isNotEmpty() }
|
||||
}
|
||||
}
|
||||
K
|
||||
}
|
||||
|
||||
fails=0
|
||||
expect() { # expect <pass|fail> <what> [text the output must hold]
|
||||
local want=$1 what=$2 needle=${3:-} out rc
|
||||
out=$(python3 -I "$check" --root "$tmp" 2>&1); rc=$?
|
||||
if [ "$want" = pass ] && [ $rc -ne 0 ]; then echo "FAIL $what: the check failed"; printf '%s\n' "$out" | sed 's/^/ /'; fails=$((fails+1)); return; fi
|
||||
if [ "$want" = fail ] && [ $rc -eq 0 ]; then echo "FAIL $what: the check passed"; printf '%s\n' "$out" | sed 's/^/ /'; fails=$((fails+1)); return; fi
|
||||
if [ -n "$needle" ] && ! grep -qF -- "$needle" <<<"$out"; then echo "FAIL $what: the output does not say '$needle'"; printf '%s\n' "$out" | sed 's/^/ /'; fails=$((fails+1)); return; fi
|
||||
echo "ok $what"
|
||||
}
|
||||
sub() { # sub <file> <from> <to>: one replacement, which must apply
|
||||
python3 - "$1" "$2" "$3" <<'P' || { echo "FAIL the fixture edit did not apply: $2"; fails=$((fails+1)); }
|
||||
import sys
|
||||
p, a, b = sys.argv[1:]
|
||||
s = open(p).read()
|
||||
if s.count(a) != 1:
|
||||
sys.exit(1)
|
||||
open(p, "w").write(s.replace(a, b))
|
||||
P
|
||||
}
|
||||
|
||||
mk; expect pass "a tree whose every door names its operations as the catalogue does" "MCP: 2 tools, each an operation"
|
||||
mk; expect pass "the C door's held names and its library API" "C: 1 functions name operations of the catalogue"
|
||||
mk; expect pass "a Swift and a Kotlin method that name operations" "Kotlin: 2 methods name operations"
|
||||
|
||||
mk; rm "$tmp/wai/wai-rs/src/bin/wai_cli/mcp/tools.rs"
|
||||
expect pass "a door with no operation yet says so" "no MCP door yet"
|
||||
|
||||
mk; sub "$tmp/wai/wai-rs/src/bin/wai_cli/mcp/tools.rs" '"envelope_inspect"' '"inspect_envelope"'
|
||||
expect fail "an MCP tool misnamed" "tool 'inspect_envelope' names inspect.envelope, which the catalogue does not hold"
|
||||
|
||||
mk; sub "$tmp/wai/wai-rs/src/bin/wai_cli/mcp/tools.rs" '"room_grant"' '"dev_room_grant"'
|
||||
expect fail "an MCP tool that is not noun_verb" "tool 'dev_room_grant' is not noun_verb"
|
||||
|
||||
mk; cat > "$tmp/wai/wai-rs/src/bin/wai_cli/mcp/tools.rs" <<'R'
|
||||
pub(crate) const TOOLS: &[Tool] = &[
|
||||
Tool { op: "envelope.inspect", toolset: Toolset::Core, effect: Effect::Reads, run: inspect,
|
||||
description: "What an envelope holds." },
|
||||
Tool { op: "room.grant", toolset: Toolset::Dev, effect: Effect::DevRoom, run: grant,
|
||||
description: "A grant." },
|
||||
];
|
||||
R
|
||||
expect pass "a tool table that names each tool by its operation" "MCP: 2 tools, each an operation"
|
||||
|
||||
mk; cat > "$tmp/wai/wai-rs/src/bin/wai_cli/mcp/tools.rs" <<'R'
|
||||
pub(crate) const TOOLS: &[Tool] = &[
|
||||
Tool { op: "envelope.inspect", toolset: Toolset::Core, run: inspect, description: "x" },
|
||||
Tool { op: "receipt.check", toolset: Toolset::Core, run: check, description: "y" },
|
||||
];
|
||||
R
|
||||
expect fail "a tool named by an operation the catalogue does not hold" "tool 'receipt_check' names receipt.check"
|
||||
|
||||
mk; printf 'pub(crate) const TOOLS: &[Tool] = &[];\n' > "$tmp/wai/wai-rs/src/bin/wai_cli/mcp/tools.rs"
|
||||
expect fail "an MCP tool table the check cannot read" "no tool name could be read"
|
||||
|
||||
mk; sub "$tmp/joule-code/joule-code-rs/crates/svc/openapi.json" '"room.grant"' '"grant"'
|
||||
expect fail "an OpenAPI operationId misnamed" "operationId 'grant' is not an operation of the catalogue"
|
||||
|
||||
mk; sub "$tmp/joule-code/joule-code-rs/crates/svc/openapi.json" '/v1/rooms/{room}/grants' '/grant'
|
||||
expect fail "an OpenAPI route that is not the catalogue's" "the catalogue routes room.grant to POST /v1/rooms/{room}/grants"
|
||||
|
||||
mk; sub "$tmp/joule-code/joule-code-rs/crates/svc/openapi.json" '"room.grant"' '"envelope.inspect"'
|
||||
expect fail "an OpenAPI operation the catalogue places on this machine" "envelope.inspect is placed local, not on a service"
|
||||
|
||||
mk; printf 'int32_t wai_envelope_decode(const uint8_t *p);\n' >> "$tmp/wai/wai-rs/include/wai.h"
|
||||
expect fail "a C function misnamed" "wai_envelope_decode names envelope.decode, which the catalogue does not hold"
|
||||
|
||||
mk; printf 'int32_t wai_receipt_get(const uint8_t *p);\n' >> "$tmp/wai/wai-rs/include/wai.h"
|
||||
expect fail "a C function with a near-synonym verb" "wai_receipt_get: the verb get is a near-synonym; the operation verb is show"
|
||||
|
||||
mk; sub "$tmp/wai/wai-sink-swift/Sources/WaiSink/Envelope.swift" 'public func pack() {}' 'public func verify() {}'
|
||||
expect fail "a Swift method misnamed" "WaiEnvelope.verify names envelope.verify, which the catalogue does not hold"
|
||||
|
||||
mk; sub "$tmp/wai/wai-sink-android/core/src/main/kotlin/wai/sink/WaiReceipt.kt" 'fun verify(bytes' 'fun check(bytes'
|
||||
expect fail "a Kotlin method misnamed" "WaiReceipt.check names receipt.check, which the catalogue does not hold"
|
||||
|
||||
mk; sub "$tmp/wai/wai-sink-android/core/src/main/kotlin/wai/sink/WaiReceipt.kt" 'fun verifyAll(' 'fun describeAll('
|
||||
expect fail "a Kotlin companion method with a near-synonym verb" "WaiReceipt.describeAll: the verb describe is a near-synonym"
|
||||
|
||||
mk; sub "$tmp/wai/wai-sink-android/core/src/main/kotlin/wai/sink/WaiReceipt.kt" 'private fun checkQuietly' 'fun checkQuietly'
|
||||
expect fail "a private Kotlin method made public under an operation verb" "WaiReceipt.checkQuietly names receipt.check"
|
||||
|
||||
if [ "$fails" -ne 0 ]; then
|
||||
echo "check-wai-ops-doors-selftest: $fails case(s) failed"
|
||||
exit 1
|
||||
fi
|
||||
echo "check-wai-ops-doors-selftest: every case held"
|
||||
292
scripts/check-wai-ops-doors.py
Executable file
292
scripts/check-wai-ops-doors.py
Executable file
|
|
@ -0,0 +1,292 @@
|
|||
#!/usr/bin/env python3
|
||||
"""check-wai-ops-doors.py: every door of WAI names its operations as the catalogue does.
|
||||
|
||||
WAI's operations are one catalogue, `noun.verb` (wai/wai-rs/src/catalogue.rs), exported as
|
||||
wai/catalogue/ops.json by `wai ops export` (CI holds the export to the table with
|
||||
`wai ops export --check`). Each door names an operation by a mechanical transform of its id:
|
||||
|
||||
MCP tool `noun_verb` (or its `op`) wai/wai-rs/src/bin/wai_cli/mcp/tools.rs
|
||||
HTTP API operationId `noun.verb` every openapi.json of the services
|
||||
C function `wai_noun_verb` wai/wai-rs/include/wai.h
|
||||
Swift type `Wai<Noun>`, method `<verb>…` wai/wai-sink-swift/Sources
|
||||
Kotlin class `Wai<Noun>`, fun `<verb>…` wai/wai-sink-android/*/src/main
|
||||
|
||||
The command line is held by the `wai` binary's own tests (wai_cli/door.rs).
|
||||
|
||||
What each door is held to:
|
||||
|
||||
- MCP: every tool's name is `noun_verb` of an operation in the catalogue.
|
||||
- HTTP API: every operationId is an operation of the catalogue that runs on a service
|
||||
(placement `service` or `both`), at the route the catalogue gives it.
|
||||
- C, Swift, Kotlin: a name in a catalogue noun's namespace whose verb is an operation verb
|
||||
(the closed list below) names an operation, and that operation must be in the catalogue;
|
||||
a near-synonym verb (`get`, `describe`, `info`, `keygen`, `init`, `new`) in a catalogue
|
||||
noun's namespace is refused. Any other name is library API the catalogue does not list
|
||||
(the C ABI's codec and sink functions, the native sinks' classes), and is reported as such.
|
||||
|
||||
A door that has no operation of the catalogue yet is reported as such; that is not a
|
||||
failure. A wrong name is.
|
||||
|
||||
check-wai-ops-doors.py [--root DIR] [--ops FILE]
|
||||
|
||||
Exit 0 when every door holds, 1 with one line a finding otherwise. Standard library only.
|
||||
"""
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
# The operation verbs: the closed list of WAI toolchain v1 (naming rule N3).
|
||||
VERBS = {
|
||||
"create", "list", "show", "update", "delete", "check", "verify", "run", "build", "export",
|
||||
"import", "start", "stop", "join", "leave", "grant", "invite", "revoke", "rotate", "tail",
|
||||
"search", "test", "load", "encode", "decode", "inspect", "wrap", "unwrap", "fetch",
|
||||
"generate", "promote", "rollback", "lock", "upgrade", "setup", "guide", "stream",
|
||||
}
|
||||
# Near-synonyms of an operation verb, and the verb to use.
|
||||
NEAR = {"get": "show", "describe": "show", "info": "show", "keygen": "create", "init": "create", "new": "create"}
|
||||
|
||||
MCP_TABLE = "wai/wai-rs/src/bin/wai_cli/mcp/tools.rs"
|
||||
OPENAPI_GLOBS = ["joule-code/joule-code-rs/crates/*/openapi.json", "wai/*/openapi.json", "wai/wai-rs/openapi.json"]
|
||||
C_HEADER = "wai/wai-rs/include/wai.h"
|
||||
SWIFT_GLOB = "wai/wai-sink-swift/Sources/**/*.swift"
|
||||
KOTLIN_GLOB = "wai/wai-sink-android/*/src/main/**/*.kt"
|
||||
|
||||
|
||||
def load_ops(path):
|
||||
with open(path, encoding="utf-8") as f:
|
||||
doc = json.load(f)
|
||||
if doc.get("format") != "wai.ops/1":
|
||||
raise SystemExit(f"{path}: not a catalogue export (format {doc.get('format')!r})")
|
||||
return {o["id"]: o for o in doc["operations"]}
|
||||
|
||||
|
||||
def split_id(i):
|
||||
n, _, v = i.partition(".")
|
||||
return n, v
|
||||
|
||||
|
||||
def check_mcp(root, ops, findings, notes):
|
||||
path = os.path.join(root, MCP_TABLE)
|
||||
if not os.path.exists(path):
|
||||
notes.append("MCP: no MCP door yet (no tool table); nothing to hold")
|
||||
return
|
||||
with open(path, encoding="utf-8") as f:
|
||||
src = f.read()
|
||||
# A tool is named outright (`name: "noun_verb"`) or by its operation (`op: "noun.verb"`),
|
||||
# its name then the operation's with `_` for the dot.
|
||||
names = []
|
||||
for body in re.findall(r"\bTool\s*\{(.*?)\}\s*,", src, re.S):
|
||||
named = re.search(r'\bname:\s*"([^"]+)"', body)
|
||||
by_op = re.search(r'\bop:\s*"([^"]+)"', body)
|
||||
if named:
|
||||
names.append(named.group(1))
|
||||
elif by_op:
|
||||
names.append(by_op.group(1).replace(".", "_", 1))
|
||||
if not names:
|
||||
findings.append(f"MCP: {MCP_TABLE}: no tool name could be read (`Tool {{ name: \"…\" }}` or `Tool {{ op: \"…\" }}`)")
|
||||
return
|
||||
bad = 0
|
||||
for t in names:
|
||||
if t.count("_") != 1:
|
||||
findings.append(f"MCP: tool {t!r} is not noun_verb of an operation")
|
||||
bad += 1
|
||||
continue
|
||||
i = t.replace("_", ".")
|
||||
if i not in ops:
|
||||
findings.append(f"MCP: tool {t!r} names {i}, which the catalogue does not hold")
|
||||
bad += 1
|
||||
if not bad:
|
||||
notes.append(f"MCP: {len(names)} tools, each an operation of the catalogue")
|
||||
|
||||
|
||||
def openapi_operations(doc):
|
||||
for path, item in (doc.get("paths") or {}).items():
|
||||
for method, op in item.items():
|
||||
if method.lower() in ("get", "put", "post", "delete", "patch", "head", "options", "trace") and isinstance(op, dict):
|
||||
yield method.upper(), path, op.get("operationId")
|
||||
|
||||
|
||||
def check_openapi(root, ops, findings, notes):
|
||||
files = sorted({f for g in OPENAPI_GLOBS for f in glob.glob(os.path.join(root, g))})
|
||||
if not files:
|
||||
notes.append("HTTP API: no OpenAPI document yet; nothing to hold")
|
||||
return
|
||||
for f in files:
|
||||
rel = os.path.relpath(f, root)
|
||||
with open(f, encoding="utf-8") as fh:
|
||||
doc = json.load(fh)
|
||||
n = bad = 0
|
||||
for method, path, oid in openapi_operations(doc):
|
||||
n += 1
|
||||
where = f"HTTP API: {rel}: {method} {path}"
|
||||
if not oid:
|
||||
findings.append(f"{where}: no operationId")
|
||||
bad += 1
|
||||
elif oid not in ops:
|
||||
findings.append(f"{where}: operationId {oid!r} is not an operation of the catalogue")
|
||||
bad += 1
|
||||
else:
|
||||
o = ops[oid]
|
||||
if o["placement"] not in ("service", "both"):
|
||||
findings.append(f"{where}: {oid} is placed {o['placement']}, not on a service")
|
||||
bad += 1
|
||||
r = o.get("route")
|
||||
if r and (r["method"].upper(), r["path"]) != (method, path):
|
||||
findings.append(f"{where}: the catalogue routes {oid} to {r['method']} {r['path']}")
|
||||
bad += 1
|
||||
if not bad:
|
||||
notes.append(f"HTTP API: {rel}: {n} operations, each an operation of the catalogue at its route")
|
||||
|
||||
|
||||
def held_name(door, where, noun, verb, nouns, ops, findings):
|
||||
"""A name in `noun`'s namespace with `verb`: a finding when it names an operation the
|
||||
catalogue does not hold, or uses a near-synonym. True when it names an operation."""
|
||||
if noun not in nouns:
|
||||
return False
|
||||
if verb in NEAR:
|
||||
findings.append(f"{door}: {where}: the verb {verb} is a near-synonym; the operation verb is {NEAR[verb]}")
|
||||
return True
|
||||
if verb not in VERBS:
|
||||
return False
|
||||
i = f"{noun}.{verb}"
|
||||
if i not in ops:
|
||||
near = sorted(o for o in ops if split_id(o)[1] == verb)
|
||||
hint = f" (operations with that verb: {', '.join(near)})" if near else ""
|
||||
findings.append(f"{door}: {where} names {i}, which the catalogue does not hold{hint}")
|
||||
return True
|
||||
|
||||
|
||||
def check_c(root, ops, nouns, findings, notes):
|
||||
path = os.path.join(root, C_HEADER)
|
||||
if not os.path.exists(path):
|
||||
notes.append("C: no header; nothing to hold")
|
||||
return
|
||||
with open(path, encoding="utf-8") as f:
|
||||
src = f.read()
|
||||
fns = sorted(set(re.findall(r"\b(wai_[a-z0-9_]+)\s*\(", src)))
|
||||
mapped = other = 0
|
||||
before = len(findings)
|
||||
for fn in fns:
|
||||
parts = fn.split("_")[1:]
|
||||
if len(parts) >= 2 and held_name("C", fn, parts[0], parts[1], nouns, ops, findings):
|
||||
mapped += 1
|
||||
else:
|
||||
other += 1
|
||||
if len(findings) == before:
|
||||
if mapped:
|
||||
notes.append(f"C: {mapped} functions name operations of the catalogue, each held; {other} are library API")
|
||||
else:
|
||||
notes.append(f"C: no operation of the catalogue in this door yet; its {other} functions are library API (codecs, envelopes, the sink)")
|
||||
|
||||
|
||||
def camel_verb(name):
|
||||
m = re.match(r"[a-z]+", name)
|
||||
return m.group(0) if m else ""
|
||||
|
||||
|
||||
TYPE_RE = {
|
||||
"Swift": re.compile(r"^\s*(?:@\w+\s+)*(public|open)\s+(?:final\s+)?(?:class|struct|enum|actor)\s+(\w+)|^\s*(?:public\s+)?extension\s+(\w+)"),
|
||||
"Kotlin": re.compile(r"^\s*(?!private|internal)(?:public\s+)?(?:open\s+|abstract\s+|sealed\s+|data\s+|enum\s+|final\s+)*(?:class|object|interface)\s+(\w+)"),
|
||||
}
|
||||
FUNC_RE = {
|
||||
"Swift": re.compile(r"^\s*(?:@\w+\s+)*(?:public|open)\s+(?:static\s+|class\s+|final\s+|override\s+)*func\s+(\w+)"),
|
||||
"Kotlin": re.compile(r"^\s*(?!.*\b(?:private|internal|protected)\b)(?:public\s+|override\s+|suspend\s+|inline\s+|operator\s+)*fun\s+(?:<[^>]*>\s*)?(\w+)"),
|
||||
}
|
||||
|
||||
|
||||
def sdk_names(lang, src):
|
||||
"""Every (type, method) the source declares public: a method under the type whose body
|
||||
holds it, a companion object's under its class. A type's body opens at the first line
|
||||
after its declaration that ends with `{` (a Kotlin primary constructor can take several
|
||||
lines, and a default argument can hold a lambda's braces)."""
|
||||
out = []
|
||||
stack = [] # (type name, brace depth inside its body)
|
||||
depth = 0
|
||||
pending = None
|
||||
for line in src.splitlines():
|
||||
code = re.sub(r'"(?:\\.|[^"\\])*"', '""', line.split("//")[0])
|
||||
name = None
|
||||
t = TYPE_RE[lang].match(code)
|
||||
if t:
|
||||
name = next(g for g in t.groups() if g and g not in ("public", "open"))
|
||||
elif lang == "Kotlin" and re.match(r"^\s*companion\s+object\b", code) and stack:
|
||||
name = stack[-1][0]
|
||||
f = FUNC_RE[lang].match(code)
|
||||
if f and stack:
|
||||
out.append((stack[-1][0], f.group(1)))
|
||||
if name:
|
||||
pending = name
|
||||
if pending and code.rstrip().endswith("{"):
|
||||
stack.append((pending, depth + 1))
|
||||
pending = None
|
||||
for ch in code:
|
||||
if ch == "{":
|
||||
depth += 1
|
||||
elif ch == "}":
|
||||
depth -= 1
|
||||
while stack and depth < stack[-1][1]:
|
||||
stack.pop()
|
||||
return out
|
||||
|
||||
|
||||
def check_sdk(lang, root, pattern, ops, nouns, findings, notes):
|
||||
files = sorted(glob.glob(os.path.join(root, pattern), recursive=True))
|
||||
if not files:
|
||||
notes.append(f"{lang}: no sources; nothing to hold")
|
||||
return
|
||||
mapped = other = 0
|
||||
before = len(findings)
|
||||
for f in files:
|
||||
with open(f, encoding="utf-8") as fh:
|
||||
src = fh.read()
|
||||
for typ, meth in sdk_names(lang, src):
|
||||
if not typ.startswith("Wai") or len(typ) <= 3:
|
||||
other += 1
|
||||
continue
|
||||
noun = typ[3:].lower()
|
||||
where = f"{os.path.relpath(f, root)}: {typ}.{meth}"
|
||||
if held_name(lang, where, noun, camel_verb(meth), nouns, ops, findings):
|
||||
mapped += 1
|
||||
else:
|
||||
other += 1
|
||||
if len(findings) == before:
|
||||
if mapped:
|
||||
notes.append(f"{lang}: {mapped} methods name operations of the catalogue, each held; {other} are library API")
|
||||
else:
|
||||
notes.append(f"{lang}: no operation of the catalogue in this door yet; its {other} public names are the native sink's API, whose nouns the catalogue does not hold")
|
||||
|
||||
|
||||
def main(argv):
|
||||
root, ops_path = ".", None
|
||||
i = 0
|
||||
while i < len(argv):
|
||||
if argv[i] == "--root" and i + 1 < len(argv):
|
||||
root = argv[i + 1]
|
||||
i += 2
|
||||
elif argv[i] == "--ops" and i + 1 < len(argv):
|
||||
ops_path = argv[i + 1]
|
||||
i += 2
|
||||
else:
|
||||
print(__doc__.strip().splitlines()[0], file=sys.stderr)
|
||||
print("usage: check-wai-ops-doors.py [--root DIR] [--ops FILE]", file=sys.stderr)
|
||||
return 2
|
||||
ops = load_ops(ops_path or os.path.join(root, "wai/catalogue/ops.json"))
|
||||
nouns = {split_id(i)[0] for i in ops}
|
||||
findings, notes = [], []
|
||||
check_mcp(root, ops, findings, notes)
|
||||
check_openapi(root, ops, findings, notes)
|
||||
check_c(root, ops, nouns, findings, notes)
|
||||
check_sdk("Swift", root, SWIFT_GLOB, ops, nouns, findings, notes)
|
||||
check_sdk("Kotlin", root, KOTLIN_GLOB, ops, nouns, findings, notes)
|
||||
for n in notes:
|
||||
print(f"ok {n}")
|
||||
for f in findings:
|
||||
print(f"FAIL {f}")
|
||||
print(f"check-wai-ops-doors: {len(ops)} operations; {len(findings)} finding(s)")
|
||||
return 1 if findings else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv[1:]))
|
||||
|
|
@ -6,6 +6,76 @@ wire format may change and such changes are recorded here.
|
|||
|
||||
## v0.4.0 — current
|
||||
|
||||
### One catalogue of operations, the agent rules on every command, and the service binaries named `wai-*`
|
||||
|
||||
- **The operation catalogue** (`wai::catalogue`): every operation the toolchain offers, once,
|
||||
named `noun.verb` (`envelope.inspect`, `project.create`, `receipt.verify`), the verb from
|
||||
naming rule N3's closed list, with its summary, command line, placement, input and output
|
||||
JSON Schemas, effect, toolset, stability and the build features it needs. It lists the
|
||||
operations that work today, `wai breaking` (`compat.check`), `wai conform`
|
||||
(`conformance.run`, `conformance.list`) and the fleet's `wai enrolment create|list` and
|
||||
`wai report create|export` (`enrolment.create`, `enrolment.list`, `report.create`,
|
||||
`report.export`, each placed on the client, against a fleet service) among them. Each entry
|
||||
says when it waits for confirmation (`confirm`). A command a person already types keeps
|
||||
its spelling (`wai hello` is `toolchain.test`, `wai stage refine` is `stage.encode`,
|
||||
`wai http seal-playlist` is `playlist.lock`). **`wai ops list`**, **`wai ops search
|
||||
"<task>"`** (offline, up to five ranked matches), **`wai ops show <id>`** and **`wai ops
|
||||
export`**, whose output is [`catalogue/ops.json`](catalogue/ops.json).
|
||||
- **The command line through it.** A command resolves through the catalogue alone, by the
|
||||
spelling it has today (`wai inspect`) or by its id's words (`wai envelope inspect`); help and
|
||||
usage come from the catalogue's entries.
|
||||
- **The agent rules on every command.** Exit codes 0 to 7 (0 done, 1 a check failed, 2 usage,
|
||||
3 permission, 4 confirmation required, 5 not found, 6 target unavailable, 7 internal);
|
||||
`--json` prints one envelope, `{"wai": "envelope/1", "op", "ok", "data" | "error", "exit"}`,
|
||||
from one module that `wai breaking` and `wai conform` share; a command's own JSON (`wai create
|
||||
--list`, `wai generate`, `wai score`, `wai stage apply`, `wai dev --check`, `wai breaking`,
|
||||
`wai conform`) is now that envelope's `data`. Without `--json` the output is text, on a
|
||||
terminal or not. `--dry-run` on every operation that is not
|
||||
read-only writes nothing and says what it would write. An operation that would replace a
|
||||
file the command line did not name (`out.wai` when `-o` is not given, an earlier
|
||||
`wai hello`'s files) asks at a terminal and otherwise exits 4 until `--yes` is given. An
|
||||
unknown command exits 5, not 2.
|
||||
- **One confirmation flag, `--yes`.** `--confirm`, which `wai conform`, `wai setup-agent` and
|
||||
the fleet's operations took, is its deprecated spelling: it still works and does the same,
|
||||
and `wai --help`, each operation's help and the export (`agent_flags`) say it is deprecated.
|
||||
Every confirming command a run prints names `--yes`. The fleet's operations, `wai conform`
|
||||
and `wai setup-agent` wait as their entries say: a fleet service changed, or a file that
|
||||
exists replaced.
|
||||
- **The fleet's operations on the same rules**: their JSON is the envelope with `--json` (it was
|
||||
also printed whenever standard output was not a terminal), a dry run's `plan` names each file
|
||||
and each request to the fleet service, none of them sent, and a confirmation is exit 4 with
|
||||
`proposed` and the confirming command. `wai conform` and `wai breaking` print text unless
|
||||
`--json` is given, as every command does.
|
||||
- **The doors held to it** (`.github/workflows/wai-catalogue.yml`): the export is the table;
|
||||
`scripts/check-wai-ops-doors.py` holds MCP tool names, the services' OpenAPI operationIds,
|
||||
the C header and the Swift and Kotlin sinks' public names to the catalogue. `wai mcp`'s tool
|
||||
table is held to it both ways (every tool an operation the catalogue says it serves, in that
|
||||
toolset, and every such operation a tool), and so is the room service's OpenAPI document
|
||||
(every `operationId` at the catalogue's route, every routed operation described).
|
||||
- **Renamed to the verb rule (N3)**: the MCP tool `package_build`, which carries a file in an
|
||||
envelope as `wai wrap` does, is `envelope_wrap`; `room_session` is `session_show`; the room
|
||||
service's challenges are `watch.setup`, `conversation.setup` and `call.setup` (they were
|
||||
`watchchallenge.create`, `conversationchallenge.create` and `callchallenge.create`);
|
||||
`wai mcp` is the operation `mcp.start`. Create's export is also `wai app export`.
|
||||
- **Breaking**, intended: the module that held the naming rule is replaced by the catalogue,
|
||||
which holds the rule and every name. `api:rust:wai::ops`, `api:rust:wai::ops::VERBS`,
|
||||
`api:rust:wai::ops::NAMED_AS_IS`, `api:rust:wai::ops::check_id` and
|
||||
`api:rust:wai::ops::mcp_tool_name` are gone; use `wai::catalogue`, its `VERBS`, its rule
|
||||
(`findings_of`) and each operation's name in every door (`Op::names`). The names the rule
|
||||
once let stand are held to it now, so `NAMED_AS_IS` has no replacement.
|
||||
- **Breaking**, intended: the room service's three challenges keep their routes and take the
|
||||
names the rule gives them, `watch.setup`, `conversation.setup` and `call.setup`, as their
|
||||
`operationId`s: `api:openapi:joule-moq-room:GET /watch/challenge`,
|
||||
`api:openapi:joule-moq-room:GET /msg/challenge` and
|
||||
`api:openapi:joule-moq-room:GET /call/challenge`. A client generated from the document calls
|
||||
them by those names; the requests and answers are unchanged.
|
||||
- **The service binaries are `wai-relay`, `wai-room` and `wai-ingest`**; their crates keep
|
||||
their names (`joule-moq-relay`, `joule-moq-room`, `joule-moq-ingest`). `wai dev` and the
|
||||
quickstart archive use the new names.
|
||||
- **Every example is Apache-2.0** and carries `LICENSE`, which `wai create` writes into the
|
||||
project.
|
||||
- `wai` sends nothing off your machine unless you name a remote service.
|
||||
|
||||
### WAI Studio on the web: one front end, its core in WebAssembly, served by `wai dev` at `/_wai/`
|
||||
|
||||
- **[`wai-studio`](wai-studio/)**: one front end with two hosts, chosen when the page starts.
|
||||
|
|
@ -71,10 +141,10 @@ wire format may change and such changes are recorded here.
|
|||
- **Measurements are not compared**: `wai breaking` leaves out the corpus files it lists as
|
||||
records of a measured run (the external decodes of the format probes, the relay's recorded
|
||||
metering run), each with its reason; the vectors built from them are still compared.
|
||||
- Both follow the toolchain's agent rules: the JSON envelope `envelope/1` with `--json` or off a
|
||||
terminal, one exit status for each outcome (0 held, 1 a check failed, 2 usage, 4 a record
|
||||
would be replaced without `--confirm`, 5 not found, 6 the implementation does not answer, 7
|
||||
internal), and `wai conform --dry-run`.
|
||||
- Both follow the toolchain's agent rules: the JSON envelope `envelope/1` with `--json`, one
|
||||
exit status for each outcome (0 held, 1 a check failed, 2 usage, 4 a record would be replaced
|
||||
without `--yes`, 5 not found, 6 the implementation does not answer, 7 internal), and
|
||||
`wai conform --dry-run`.
|
||||
|
||||
### Fleet: an organisation's nodes enrolled by their keys, their records verified, and a report it signs for a period
|
||||
|
||||
|
|
@ -143,39 +213,40 @@ wire format may change and such changes are recorded here.
|
|||
|
||||
### Coding agents: `wai mcp`, `wai setup-agent`, and the room service's API described
|
||||
|
||||
- **`wai mcp`** (`agent.mcp`; the feature `mcp`, which the quickstart's `wai` holds): WAI's
|
||||
- **`wai mcp`** (`mcp.start`; the feature `mcp`, which the quickstart's `wai` holds): WAI's
|
||||
operations as tools for a coding assistant, a Model Context Protocol server (revision
|
||||
2025-06-18) on standard input and output. Each tool is an operation of the toolchain's
|
||||
catalogue, named by it with `_` for the dot, in toolsets. `core`, the default:
|
||||
`envelope_inspect` (an envelope or a package), `registry_list` and `registry_show` (the
|
||||
registered capabilities), `receipt_verify` (a delivery receipt, a claim with the context the
|
||||
tool takes, a live-room session or a stream audit bundle, by `kind` or read from the
|
||||
arguments; each answer the verifier's verdict and reason code), `package_build` (a media file
|
||||
arguments; each answer the verifier's verdict and reason code), `envelope_wrap` (a media file
|
||||
under `wai wrap`'s rules, inside the project directory only), `conformance_list` and
|
||||
`conformance_run` (a corpus's independent verifier), and `docs_search` (the specification's
|
||||
text). `dev`, marked development only, on the target `local` alone: `dev_start` and `dev_stop`
|
||||
(a relay and a room service on loopback), `room_grant` (grants for test principals, refusing
|
||||
any service whose address is not a loopback address, before connecting), `room_join`,
|
||||
`room_leave` and `room_session`. `--toolset` chooses; `--read-only` offers only the tools that
|
||||
`room_leave` and `session_show`. `--toolset` chooses; `--read-only` offers only the tools that
|
||||
read or run a verifier. The verdicts agree vector by vector with jwp-conformance,
|
||||
claim-conformance, live-conformance/rooms and live-conformance/audit (`tests/cli_mcp.rs`).
|
||||
- **`wai setup-agent`** (`agent.setup`): writes `AGENTS.md` in a project, adds an import of it to
|
||||
a `CLAUDE.md` the project already has, and registers `wai mcp` (with `--toolset`, those
|
||||
toolsets) in its MCP configuration files, by their documented formats; it changes a file that
|
||||
exists only after its diff is shown and confirmed, has a dry run, and writes nothing outside
|
||||
the project directory. Off a terminal it answers in the toolchain's JSON envelope
|
||||
the project directory. With `--json` it answers in the toolchain's JSON envelope
|
||||
(`envelope/1`), with the table's exit statuses: 2 usage, 3 permission, 4 confirmation
|
||||
required (each proposed change and the confirming command given), 5 not found
|
||||
(`tests/cli_setup_agent.rs`).
|
||||
- **`wai mcp` and `wai setup-agent` are verbs of the one table** (`dx::VERBS`) that holds `hello`,
|
||||
`create`, `dev` and `verify`. `wai::ops` holds the rule operation names keep: `noun.verb`,
|
||||
lowercase, the verb from a closed list.
|
||||
`create`, `dev` and `verify`, and operations of the catalogue (`wai::catalogue`): `mcp.start`,
|
||||
`agent.setup`, every tool's operation, and the room service's operations at their routes,
|
||||
held to it both ways by the tool table's tests and the room service's contract test.
|
||||
- **The room service's API**: `joule-moq-room/openapi.json` (OpenAPI 3.1) describes every route,
|
||||
request and status of the room service in each of its modes, each operation's `operationId`
|
||||
its name in the catalogue (`room.show`, `room.grant`, `watch.join`, `message.create`, ...),
|
||||
held to the code by a contract test that matches the names, the routers' routes, each
|
||||
operation's statuses against its handlers', and the running service's answers in every mode.
|
||||
`joule-moq-room dev` runs a development room on loopback for `wai mcp` and for tests.
|
||||
`wai-room dev` runs a development room on loopback for `wai mcp` and for tests.
|
||||
|
||||
### The developer's path: `wai hello`, `create`, `dev` and `verify`; the floor with no system library; the examples
|
||||
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ cargo install --locked --path wai-rs --no-default-features --features quickstart
|
|||
| [`wai-rs/`](wai-rs/) | Rust reference implementation (lib + cdylib + staticlib + C FFI) |
|
||||
| [`examples/`](examples/README.md) | Small runnable projects, one per template of `wai create`: a video room, watching together, messaging and a call, an encoder into a room, packaging, receipts in Rust and Python, native sinks on Apple and Android platforms, remote desktop |
|
||||
| [`quickstart/`](quickstart/README.md) | The prebuilt command line's archive: what it holds, how it is built and released, and its installer |
|
||||
| [`catalogue/`](catalogue/README.md) | The operation catalogue as JSON (`wai ops export`): every operation of the toolchain, `noun.verb`, and the name each door gives it |
|
||||
| [`REGISTRY.md`](REGISTRY.md), [`registry/capabilities.json`](registry/capabilities.json) | The registry: every registered string with its kind, status, classes and edition, generated from the reference implementation (`wai registry export`), with its JSON Schema and the policy for registering a string |
|
||||
| [`vocab/`](vocab/) | RDF vocabulary: v2 generated from the registry, v1 frozen |
|
||||
| [`extensions/`](extensions/) | Sub-specifications (confidentiality, receipts, worlds, film, quantum operations, integer operators, vector graphics, …) |
|
||||
|
|
@ -76,8 +77,17 @@ cd my-room && wai dev # the relay, the room service and yo
|
|||
wai verify ../wai-hello # the receipts hello wrote, checked offline
|
||||
```
|
||||
|
||||
[`examples/`](examples/README.md) holds each template, with a test that runs it. The library's
|
||||
own checks, from a clone:
|
||||
[`examples/`](examples/README.md) holds each template, with a test that runs it.
|
||||
|
||||
Every command is an operation of one catalogue, named `noun.verb` (`envelope.inspect`,
|
||||
`project.create`): `wai ops list` lists them, `wai ops search "<task>"` finds one offline, and
|
||||
`wai ops export` writes the catalogue as JSON ([`catalogue/ops.json`](catalogue/README.md)).
|
||||
Every command takes `--json` (one JSON envelope on stdout), `--dry-run` where it would write,
|
||||
and `--yes` where a change waits for confirmation (`--confirm`, deprecated, does the same), and
|
||||
exits with one of eight codes (`wai --help` lists them). `wai` sends nothing off your machine
|
||||
unless you name a remote service.
|
||||
|
||||
The library's own checks, from a clone:
|
||||
|
||||
```bash
|
||||
cd wai-rs
|
||||
|
|
@ -113,9 +123,9 @@ project already has, and registers `wai mcp` in the project's MCP configuration:
|
|||
and the project-level MCP file of each agent whose directory the project already holds (`--all`
|
||||
writes every one it knows, `--dry-run` lists them, `--toolset core,dev` registers the server
|
||||
with those toolsets). A file that does not exist is created; one that exists is changed only
|
||||
after its diff is shown and the change confirmed (asked on a terminal, or `--confirm`); without
|
||||
after its diff is shown and the change confirmed (asked on a terminal, or `--yes`); without
|
||||
either, nothing that exists changes and the command exits 4 with each proposed change and the
|
||||
command that confirms. It writes nothing outside the project directory. Off a terminal its
|
||||
command that confirms. It writes nothing outside the project directory. With `--json` its
|
||||
output is the toolchain's JSON envelope.
|
||||
|
||||
`wai mcp` is a Model Context Protocol server on standard input and output. Each tool is an
|
||||
|
|
@ -123,14 +133,14 @@ operation of the toolchain, named by it with `_` for the dot, and the tools come
|
|||
|
||||
| Toolset | Tools |
|
||||
|---|---|
|
||||
| `core` (the default) | `envelope_inspect`, `registry_list`, `registry_show`, `receipt_verify`, `package_build`, `conformance_list`, `conformance_run`, `docs_search` |
|
||||
| `dev` | `dev_start`, `dev_stop`, `room_grant`, `room_join`, `room_leave`, `room_session` |
|
||||
| `core` (the default) | `envelope_inspect`, `registry_list`, `registry_show`, `receipt_verify`, `envelope_wrap`, `conformance_list`, `conformance_run`, `docs_search` |
|
||||
| `dev` | `dev_start`, `dev_stop`, `room_grant`, `room_join`, `room_leave`, `session_show` |
|
||||
| `app` (a build with the feature `app`) | `app_guide`, `app_check`, `app_stream`, `app_export`: Create's, below |
|
||||
|
||||
`receipt_verify` verifies a delivery receipt, a claim, a live-room session or a stream audit
|
||||
bundle, and answers with the verifier's verdict and reason code. The `dev` tools, marked
|
||||
development only, act on the target `local` alone: they start a relay and a room service on
|
||||
loopback (`joule-moq-room dev`), mint grants for test principals, join and publish as them, and
|
||||
loopback (`wai-room dev`), mint grants for test principals, join and publish as them, and
|
||||
read the session for verification; the grant tool refuses any service that is not on a loopback
|
||||
address. `--read-only` offers only the tools that read or run a verifier; the packaging tool
|
||||
writes only inside the project directory. No tool sends anything off the machine.
|
||||
|
|
|
|||
92
wai/catalogue/README.md
Normal file
92
wai/catalogue/README.md
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
# The operation catalogue
|
||||
|
||||
Every operation of the WAI toolchain, once. [`ops.json`](ops.json) is written by
|
||||
`wai ops export` from the table in `wai-rs/src/catalogue.rs`, and CI fails when the two differ
|
||||
(`.github/workflows/wai-catalogue.yml`). Do not edit it by hand: change the table, then
|
||||
|
||||
```bash
|
||||
cd wai/wai-rs && cargo run -q --no-default-features --bin wai -- ops export --out ../catalogue/ops.json
|
||||
```
|
||||
|
||||
## An operation
|
||||
|
||||
An operation is named `noun.verb`: lowercase ASCII, a singular noun, and a verb from naming
|
||||
rule N3's closed list (create, list, show, update, delete, check, verify, run, build, export,
|
||||
import, start, stop, join, leave, grant, invite, revoke, rotate, tail, search, test, load, encode,
|
||||
decode, inspect, wrap, unwrap, fetch, generate, promote, rollback, lock, upgrade, setup, guide,
|
||||
stream), never a near-synonym (`get`, `describe` and `info` are `show`; `keygen`, `init` and
|
||||
`new` are `create`). A command a person already types keeps its spelling as the operation's
|
||||
command line: `wai hello` is `toolchain.test`, `wai stage refine` is `stage.encode`. Its entry
|
||||
(format `wai.ops/1`) carries:
|
||||
|
||||
| Member | Meaning |
|
||||
|---|---|
|
||||
| `id` | `noun.verb` |
|
||||
| `summary` | One line, for help, docs and the search |
|
||||
| `cli`, `aliases` | The command line today, and other spellings; every operation also answers to `wai <noun> <verb>` |
|
||||
| `usage` | The synopsis |
|
||||
| `placement` | `local`, `client` (on this machine, against a service it names), `service` or `both` |
|
||||
| `effect` | `reads`, `runs`, `writes-project`, `writes-target` or `destructive` |
|
||||
| `dry_run` | Whether it takes `--dry-run`: every operation whose effect is not `reads` |
|
||||
| `confirm` | When it waits for `--yes`: `never`, `when-replacing-an-unnamed-file`, `when-replacing-a-file`, `when-changing-the-target-or-replacing-a-file` or `always` |
|
||||
| `route` | Its HTTP method and path, for an operation that runs on a service |
|
||||
| `toolset` | `core`, `dev`, `app`, `quantum` or `maintainer` |
|
||||
| `stability` | `stable`, `preview` or `internal` |
|
||||
| `input`, `output`, `stream` | JSON Schemas: its arguments (each with the flag or position that gives it), the `data` of its `--json` envelope, and each line of a streaming operation |
|
||||
| `since`, `deprecated` | The toolchain version that first carries it; its replacement, once renamed |
|
||||
| `receipt` | Whether it writes a signed receipt or record that a `verify` operation checks |
|
||||
| `needs` | The build features it needs (`all` or `any` of them) |
|
||||
| `names` | Its name in each door |
|
||||
|
||||
## One name in every door
|
||||
|
||||
| Door | `envelope.inspect` | Held by |
|
||||
|---|---|---|
|
||||
| CLI | `wai inspect`, `wai envelope inspect` | the `wai` binary's tests (`tests/cli_ops.rs`) |
|
||||
| MCP | `envelope_inspect` | `scripts/check-wai-ops-doors.py` |
|
||||
| HTTP API | `operationId: envelope.inspect` | `scripts/check-wai-ops-doors.py` |
|
||||
| C | `wai_envelope_inspect` | `scripts/check-wai-ops-doors.py` |
|
||||
| Rust | `wai::envelope::inspect` | |
|
||||
| JavaScript, Swift, Kotlin | `wai.envelope.inspect` (Swift and Kotlin: `WaiEnvelope.inspect`) | `scripts/check-wai-ops-doors.py` (Swift, Kotlin) |
|
||||
|
||||
A door that does not carry an operation yet is reported as such by the check; a door that
|
||||
names one differently fails it. In the C header and the native sinks, a name is an operation's
|
||||
when its noun is a catalogue noun and its verb an operation verb; the codec, envelope and sink
|
||||
functions that are not operations are library API.
|
||||
|
||||
## Agent rules
|
||||
|
||||
Every command of the catalogue keeps them.
|
||||
|
||||
| Exit | Meaning |
|
||||
|---|---|
|
||||
| 0 | Done; every check held |
|
||||
| 1 | Ran, and a check failed |
|
||||
| 2 | Usage error or invalid input |
|
||||
| 3 | Authentication or permission |
|
||||
| 4 | Confirmation required; nothing changed |
|
||||
| 5 | Not found (file, target, room, operation, or a build feature) |
|
||||
| 6 | Target unavailable; safe to retry |
|
||||
| 7 | Internal error |
|
||||
|
||||
With `--json`, stdout holds one envelope and logs stay on stderr; without it the output is text,
|
||||
on a terminal or not. One module holds these rules for every command
|
||||
(`wai-rs/src/bin/wai_cli/dx/envelope.rs`).
|
||||
|
||||
```json
|
||||
{ "wai": "envelope/1", "op": "envelope.wrap", "ok": false, "exit": 4,
|
||||
"error": { "code": "confirmation-required", "message": "out.wai exists, and the command line did not name it as the output (-o); nothing was changed",
|
||||
"hint": "read the proposed changes, then run the confirming command", "retryable": false },
|
||||
"proposed": [ { "replace": "out.wai" } ],
|
||||
"confirm": "wai wrap wai.text.zstd p.bin --yes" }
|
||||
```
|
||||
|
||||
A success carries `data`: the command's own JSON where it has one, `{"text": ...}` otherwise.
|
||||
A failure carries its `exit`; a check that failed (exit 1) carries both `error` and `data`. A dry run adds `"dry_run": true`
|
||||
and the `plan` of what it would write, and of each request it would send to a service it names
|
||||
(a fleet service), none of them sent. `--yes` is never implied; off a terminal, or with
|
||||
`--no-input`, a change that waits for it exits 4. `--confirm` is a deprecated spelling of `--yes`
|
||||
that does the same; the export lists the agent flags (`agent_flags`) with it as a deprecated
|
||||
alias.
|
||||
|
||||
`wai` sends nothing off your machine unless you name a remote service.
|
||||
|
|
@ -36,6 +36,36 @@
|
|||
"meaning": "internal error"
|
||||
}
|
||||
],
|
||||
"agent_flags": [
|
||||
{
|
||||
"flag": "--json",
|
||||
"summary": "one JSON envelope on stdout: {\"wai\": \"envelope/1\", \"op\", \"ok\", \"data\" | \"error\"}",
|
||||
"aliases": []
|
||||
},
|
||||
{
|
||||
"flag": "--dry-run",
|
||||
"summary": "write nothing and send nothing; say what would be done (an operation that only reads ignores it)",
|
||||
"aliases": []
|
||||
},
|
||||
{
|
||||
"flag": "--yes",
|
||||
"summary": "confirm a change that waits for confirmation; off a terminal, without it, exit 4",
|
||||
"aliases": [
|
||||
{
|
||||
"flag": "--confirm",
|
||||
"deprecated": {
|
||||
"since": "0.3.0",
|
||||
"use_instead": "--yes"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"flag": "--no-input",
|
||||
"summary": "never ask: a change that waits for confirmation exits 4 unless --yes is given",
|
||||
"aliases": []
|
||||
}
|
||||
],
|
||||
"operations": [
|
||||
{
|
||||
"id": "toolchain.test",
|
||||
|
|
@ -180,7 +210,8 @@
|
|||
"aliases": [],
|
||||
"usage": [
|
||||
"wai dev [--port P] [--dir D] [--site DIR] [--watch FILE [--watch-ms MS]] [--call] [--remote]",
|
||||
" [--messaging] [--e2ee] [--publishers N] [--check] [--for S] [--web DIR] [--relay BIN] [--room BIN]",
|
||||
" [--show [--audience N]] [--messaging] [--e2ee] [--publishers N] [--check] [--for S] [--web DIR]",
|
||||
" [--relay BIN] [--room BIN]",
|
||||
" --port P the origin's port on 127.0.0.1 (8080; 0 picks a free one)",
|
||||
" --dir D keys, configuration and logs (.wai/dev)",
|
||||
" --site DIR your page: DIR is served at /, the room client beside it at /room/",
|
||||
|
|
@ -189,6 +220,9 @@
|
|||
" .ogg, .mp3); --watch-ms MS its duration, read from a WAV file's header",
|
||||
" --call the room is a call: /room/call.html places one, rings, answers",
|
||||
" --remote the room is one host's shared screen; prints the host's invitation",
|
||||
" --show the room is a show: hosts on camera, an audience that votes in polls",
|
||||
" the room's clock opens and signs (co-performance); --audience N each",
|
||||
" viewer's inputs to a poll (1)",
|
||||
" --messaging a conversation beside the room, at /msg-api/: /room/messages.html",
|
||||
" --e2ee every room end-to-end encrypted",
|
||||
" --check start, check every piece through the origin, report, stop",
|
||||
|
|
@ -196,6 +230,8 @@
|
|||
" --web DIR the room client's files (beside this binary: ../share/wai/web; $WAI_WEB)",
|
||||
" --relay BIN wai-relay (beside this binary, then $PATH; $WAI_RELAY_BIN)",
|
||||
" --room BIN wai-room (beside this binary, then $PATH; $WAI_ROOM_BIN)",
|
||||
" WAI Studio is served at /_wai/ when its files are found ($WAI_STUDIO, or share/wai/studio",
|
||||
" beside this binary).",
|
||||
" A wai.json in the current directory (`wai create` writes one) gives the defaults; a flag",
|
||||
" wins over it. Ctrl-C stops everything. With --json and without --check, the first line",
|
||||
" is {\"ready\": true, ...}."
|
||||
|
|
@ -267,6 +303,20 @@
|
|||
"flag": "--remote"
|
||||
}
|
||||
},
|
||||
"show": {
|
||||
"type": "boolean",
|
||||
"description": "The room is a show: hosts on camera, an audience that votes in polls.",
|
||||
"x-cli": {
|
||||
"flag": "--show"
|
||||
}
|
||||
},
|
||||
"audience": {
|
||||
"type": "integer",
|
||||
"description": "With --show, each viewer's inputs to a poll (1).",
|
||||
"x-cli": {
|
||||
"flag": "--audience"
|
||||
}
|
||||
},
|
||||
"messaging": {
|
||||
"type": "boolean",
|
||||
"description": "A conversation beside the room.",
|
||||
|
|
@ -368,11 +418,12 @@
|
|||
},
|
||||
{
|
||||
"id": "receipt.verify",
|
||||
"summary": "Every receipt index (*.wai.receipts) under each path, its group receipts and claims checked offline",
|
||||
"summary": "Every receipt index (*.wai.receipts) under each path, its group receipts and claims checked offline, and every conformance result and fleet report bundle there",
|
||||
"cli": "wai verify",
|
||||
"aliases": [],
|
||||
"usage": [
|
||||
"wai verify <path>... the receipts under each path (receipt indexes *.wai.receipts), checked offline"
|
||||
"wai verify <path>... the receipts (receipt indexes *.wai.receipts), conformance results (*.wai-conform.json)",
|
||||
" and fleet report bundles (*.wai-fleet.json) under each path, checked offline"
|
||||
],
|
||||
"placement": "local",
|
||||
"effect": "reads",
|
||||
|
|
@ -394,7 +445,7 @@
|
|||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Files or directories holding receipt indexes.",
|
||||
"description": "Files or directories holding receipt indexes, conformance results or fleet report bundles.",
|
||||
"x-cli": {
|
||||
"positional": true
|
||||
}
|
||||
|
|
@ -2739,13 +2790,13 @@
|
|||
"aliases": [],
|
||||
"usage": [
|
||||
"wai conform <kind> --suite <suite> --impl <command or URL> [--key <file>] [--out <file>] [--root <wai-dir>]",
|
||||
" [--timeout <seconds>] [--name <text>] [--version <text>] [--all] `wai conformance list` names the kinds",
|
||||
" and suites; a record that exists is replaced only with --yes"
|
||||
" [--timeout <seconds>] [--name <text>] [--version <text>] [--all] [--yes] `wai conformance list` names",
|
||||
" the kinds and suites; a record that exists is replaced only with --yes (--confirm, deprecated, is the same)"
|
||||
],
|
||||
"placement": "local",
|
||||
"effect": "writes-project",
|
||||
"dry_run": true,
|
||||
"confirm": "when-replacing-an-unnamed-file",
|
||||
"confirm": "when-replacing-a-file",
|
||||
"route": null,
|
||||
"doors": {
|
||||
"cli": true,
|
||||
|
|
@ -2921,6 +2972,335 @@
|
|||
"sdk": "wai.conformance.list"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "enrolment.create",
|
||||
"summary": "A node's enrolment signed with the organisation key, written to a file and, with --target, given to a fleet service",
|
||||
"cli": "wai enrolment create",
|
||||
"aliases": [],
|
||||
"usage": [
|
||||
"wai enrolment create <node-key> --role <role> --key <file> [--label L] [--from µs] [--until µs] [--out F]",
|
||||
" [--target URL] [--yes] a node's enrolment (WFE1), signed with the organisation key, written to F",
|
||||
" (<node>.wfe without --target) and, with --target, given to the fleet service there; changing the service",
|
||||
" or replacing a file waits for --yes (--confirm, deprecated, is the same)"
|
||||
],
|
||||
"placement": "client",
|
||||
"effect": "writes-target",
|
||||
"dry_run": true,
|
||||
"confirm": "when-changing-the-target-or-replacing-a-file",
|
||||
"route": null,
|
||||
"doors": {
|
||||
"cli": true,
|
||||
"mcp": false,
|
||||
"http": false
|
||||
},
|
||||
"toolset": "core",
|
||||
"stability": "preview",
|
||||
"input": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"node": {
|
||||
"type": "string",
|
||||
"description": "The node's Ed25519 public key, 64 hexadecimal digits.",
|
||||
"x-cli": {
|
||||
"positional": true
|
||||
}
|
||||
},
|
||||
"role": {
|
||||
"type": "string",
|
||||
"description": "relay, compositor, ingest, room, ledger or audit.",
|
||||
"x-cli": {
|
||||
"flag": "--role"
|
||||
}
|
||||
},
|
||||
"key": {
|
||||
"type": "string",
|
||||
"format": "path",
|
||||
"description": "The organisation key's file: 32 bytes, or 64 hexadecimal digits.",
|
||||
"x-cli": {
|
||||
"flag": "--key"
|
||||
}
|
||||
},
|
||||
"label": {
|
||||
"type": "string",
|
||||
"description": "The node's label, at most 64 bytes.",
|
||||
"x-cli": {
|
||||
"flag": "--label"
|
||||
}
|
||||
},
|
||||
"from": {
|
||||
"type": "integer",
|
||||
"description": "The start of the span it holds, microseconds since 1970 (0).",
|
||||
"x-cli": {
|
||||
"flag": "--from"
|
||||
}
|
||||
},
|
||||
"until": {
|
||||
"type": "integer",
|
||||
"description": "The end of that span, microseconds since 1970 (none).",
|
||||
"x-cli": {
|
||||
"flag": "--until"
|
||||
}
|
||||
},
|
||||
"out": {
|
||||
"type": "string",
|
||||
"format": "path",
|
||||
"description": "The enrolment's file (<node>.wfe when no --target is given).",
|
||||
"x-cli": {
|
||||
"flag": "--out"
|
||||
}
|
||||
},
|
||||
"target": {
|
||||
"type": "string",
|
||||
"description": "A fleet service's http:// address, given the enrolment.",
|
||||
"x-cli": {
|
||||
"flag": "--target"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"node",
|
||||
"role",
|
||||
"key"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"output": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"node",
|
||||
"role",
|
||||
"organisation",
|
||||
"digest",
|
||||
"enrolment"
|
||||
],
|
||||
"description": "The enrolment (WFE1, hexadecimal), its digest, and the file and service it went to"
|
||||
},
|
||||
"stream": null,
|
||||
"since": "0.3.0",
|
||||
"deprecated": null,
|
||||
"receipt": true,
|
||||
"needs": {
|
||||
"all": [
|
||||
"fleet"
|
||||
]
|
||||
},
|
||||
"names": {
|
||||
"cli": "wai enrolment create",
|
||||
"mcp": null,
|
||||
"c": "wai_enrolment_create",
|
||||
"rust": "wai::enrolment::create",
|
||||
"sdk": "wai.enrolment.create"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "enrolment.list",
|
||||
"summary": "The nodes a fleet service holds enrolled, each with its role, label and how many records it gave",
|
||||
"cli": "wai enrolment list",
|
||||
"aliases": [],
|
||||
"usage": [
|
||||
"wai enrolment list --target URL the nodes the fleet service at URL holds enrolled"
|
||||
],
|
||||
"placement": "client",
|
||||
"effect": "reads",
|
||||
"dry_run": false,
|
||||
"confirm": "never",
|
||||
"route": null,
|
||||
"doors": {
|
||||
"cli": true,
|
||||
"mcp": false,
|
||||
"http": false
|
||||
},
|
||||
"toolset": "core",
|
||||
"stability": "preview",
|
||||
"input": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"target": {
|
||||
"type": "string",
|
||||
"description": "The fleet service's http:// address.",
|
||||
"x-cli": {
|
||||
"flag": "--target"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"target"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"output": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"enrolments"
|
||||
],
|
||||
"description": "Each node the fleet service holds enrolled: its key, role, label and records"
|
||||
},
|
||||
"stream": null,
|
||||
"since": "0.3.0",
|
||||
"deprecated": null,
|
||||
"receipt": false,
|
||||
"needs": null,
|
||||
"names": {
|
||||
"cli": "wai enrolment list",
|
||||
"mcp": null,
|
||||
"c": "wai_enrolment_list",
|
||||
"rust": "wai::enrolment::list",
|
||||
"sdk": "wai.enrolment.list"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "report.create",
|
||||
"summary": "The fleet report of a period, signed by a fleet service over the enrolments and records it holds",
|
||||
"cli": "wai report create",
|
||||
"aliases": [],
|
||||
"usage": [
|
||||
"wai report create --target URL --start µs --end µs [--yes] the fleet report of [start, end), signed by the",
|
||||
" fleet service at URL; it changes the service, so it waits for --yes (--confirm, deprecated, is the same)"
|
||||
],
|
||||
"placement": "client",
|
||||
"effect": "writes-target",
|
||||
"dry_run": true,
|
||||
"confirm": "always",
|
||||
"route": null,
|
||||
"doors": {
|
||||
"cli": true,
|
||||
"mcp": false,
|
||||
"http": false
|
||||
},
|
||||
"toolset": "core",
|
||||
"stability": "preview",
|
||||
"input": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"target": {
|
||||
"type": "string",
|
||||
"description": "The fleet service's http:// address.",
|
||||
"x-cli": {
|
||||
"flag": "--target"
|
||||
}
|
||||
},
|
||||
"start": {
|
||||
"type": "integer",
|
||||
"description": "The period's start, microseconds since 1970.",
|
||||
"x-cli": {
|
||||
"flag": "--start"
|
||||
}
|
||||
},
|
||||
"end": {
|
||||
"type": "integer",
|
||||
"description": "The period's end, after its start.",
|
||||
"x-cli": {
|
||||
"flag": "--end"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"target",
|
||||
"start",
|
||||
"end"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"output": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"report",
|
||||
"outcome"
|
||||
],
|
||||
"description": "The report's digest and its outcome as extensions/fleet.md §7.3 states it"
|
||||
},
|
||||
"stream": null,
|
||||
"since": "0.3.0",
|
||||
"deprecated": null,
|
||||
"receipt": true,
|
||||
"needs": null,
|
||||
"names": {
|
||||
"cli": "wai report create",
|
||||
"mcp": null,
|
||||
"c": "wai_report_create",
|
||||
"rust": "wai::report::create",
|
||||
"sdk": "wai.report.create"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "report.export",
|
||||
"summary": "A fleet report's bundle fetched from a fleet service, its report recomputed here, then written to a file",
|
||||
"cli": "wai report export",
|
||||
"aliases": [],
|
||||
"usage": [
|
||||
"wai report export <digest> --target URL [--out F] [--yes] the report's bundle from the fleet service at URL,",
|
||||
" recomputed by this build and written to F (<digest>.wai-fleet.json) for `wai verify`; replacing a file",
|
||||
" waits for --yes (--confirm, deprecated, is the same)"
|
||||
],
|
||||
"placement": "client",
|
||||
"effect": "writes-project",
|
||||
"dry_run": true,
|
||||
"confirm": "when-replacing-a-file",
|
||||
"route": null,
|
||||
"doors": {
|
||||
"cli": true,
|
||||
"mcp": false,
|
||||
"http": false
|
||||
},
|
||||
"toolset": "core",
|
||||
"stability": "preview",
|
||||
"input": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"digest": {
|
||||
"type": "string",
|
||||
"description": "The report's digest, 64 hexadecimal digits.",
|
||||
"x-cli": {
|
||||
"positional": true
|
||||
}
|
||||
},
|
||||
"target": {
|
||||
"type": "string",
|
||||
"description": "The fleet service's http:// address.",
|
||||
"x-cli": {
|
||||
"flag": "--target"
|
||||
}
|
||||
},
|
||||
"out": {
|
||||
"type": "string",
|
||||
"format": "path",
|
||||
"description": "The bundle's file (<digest>.wai-fleet.json).",
|
||||
"x-cli": {
|
||||
"flag": "--out"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"digest",
|
||||
"target"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"output": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"file",
|
||||
"outcome"
|
||||
],
|
||||
"description": "The bundle's file and its outcome, recomputed here"
|
||||
},
|
||||
"stream": null,
|
||||
"since": "0.3.0",
|
||||
"deprecated": null,
|
||||
"receipt": true,
|
||||
"needs": {
|
||||
"all": [
|
||||
"fleet"
|
||||
]
|
||||
},
|
||||
"names": {
|
||||
"cli": "wai report export",
|
||||
"mcp": null,
|
||||
"c": "wai_report_export",
|
||||
"rust": "wai::report::export",
|
||||
"sdk": "wai.report.export"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "mcp.start",
|
||||
"summary": "WAI's operations as tools for a coding agent: a Model Context Protocol server on standard input and output, in toolsets",
|
||||
|
|
@ -3029,7 +3409,7 @@
|
|||
"placement": "local",
|
||||
"effect": "writes-project",
|
||||
"dry_run": true,
|
||||
"confirm": "when-replacing-an-unnamed-file",
|
||||
"confirm": "when-replacing-a-file",
|
||||
"route": null,
|
||||
"doors": {
|
||||
"cli": true,
|
||||
|
|
@ -21,6 +21,9 @@ wai dev
|
|||
| [`android-app`](android-app/README.md) | The native sink's Kotlin API, the one an Android app's player session calls | a JDK 17, the `wai-sink-android` project |
|
||||
| [`remote-desktop`](remote-desktop/README.md) | One machine's screen shared into a room, with control handed over and every input signed | `wai dev`, `wai-remote-host`, a browser |
|
||||
|
||||
Every example is Apache-2.0, and carries the license's text as `LICENSE`, which a project
|
||||
`wai create` makes from it receives with the rest.
|
||||
|
||||
Each example's `test/` directory holds the check that it works; it is not part of the project
|
||||
`wai create` writes. `bash ../quickstart/test/run-examples.sh portable|browser|apple|android`
|
||||
runs them, as `.github/workflows/wai-examples.yml` does.
|
||||
|
|
|
|||
201
wai/examples/android-app/LICENSE
Normal file
201
wai/examples/android-app/LICENSE
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for describing the origin of the Work and
|
||||
reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Support. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or support.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2026 EBUNDLE contributors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied. See the License for the specific language governing permissions
|
||||
and limitations under the License.
|
||||
|
|
@ -28,4 +28,5 @@ README covers it.
|
|||
**With a coding assistant.** Run `wai setup-agent` in this directory: it writes the
|
||||
instructions an assistant reads and connects it to WAI's tools.
|
||||
|
||||
**Licence.** Apache-2.0: `wai create` writes the licence beside the project, as `LICENSE`.
|
||||
**License.** Apache-2.0: the text is in `LICENSE`, and a project `wai create` makes from this
|
||||
example carries it.
|
||||
|
|
|
|||
201
wai/examples/apple-app/LICENSE
Normal file
201
wai/examples/apple-app/LICENSE
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for describing the origin of the Work and
|
||||
reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Support. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or support.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2026 EBUNDLE contributors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied. See the License for the specific language governing permissions
|
||||
and limitations under the License.
|
||||
|
|
@ -30,4 +30,5 @@ that the bundle verifies. The player opens a window: give it an HLS address (the
|
|||
**With a coding assistant.** Run `wai setup-agent` in this directory: it writes the
|
||||
instructions an assistant reads and connects it to WAI's tools.
|
||||
|
||||
**Licence.** Apache-2.0: `wai create` writes the licence beside the project, as `LICENSE`.
|
||||
**License.** Apache-2.0: the text is in `LICENSE`, and a project `wai create` makes from this
|
||||
example carries it.
|
||||
|
|
|
|||
201
wai/examples/encoder-ingest/LICENSE
Normal file
201
wai/examples/encoder-ingest/LICENSE
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for describing the origin of the Work and
|
||||
reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Support. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or support.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2026 EBUNDLE contributors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied. See the License for the specific language governing permissions
|
||||
and limitations under the License.
|
||||
|
|
@ -29,4 +29,5 @@ first build fetches the repository and compiles the relay and the gateway.
|
|||
**With a coding assistant.** Run `wai setup-agent` in this directory: it writes the
|
||||
instructions an assistant reads and connects it to WAI's tools.
|
||||
|
||||
**Licence.** Apache-2.0: `wai create` writes the licence beside the project, as `LICENSE`.
|
||||
**License.** Apache-2.0: the text is in `LICENSE`, and a project `wai create` makes from this
|
||||
example carries it.
|
||||
|
|
|
|||
201
wai/examples/messaging/LICENSE
Normal file
201
wai/examples/messaging/LICENSE
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for describing the origin of the Work and
|
||||
reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Support. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or support.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2026 EBUNDLE contributors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied. See the License for the specific language governing permissions
|
||||
and limitations under the License.
|
||||
|
|
@ -30,4 +30,5 @@ first tab's inbox rings, and answering opens the call, camera and microphone on
|
|||
**With a coding assistant.** Run `wai setup-agent` in this directory: it writes the
|
||||
instructions an assistant reads and connects it to WAI's tools.
|
||||
|
||||
**Licence.** Apache-2.0: `wai create` writes the licence beside the project, as `LICENSE`.
|
||||
**License.** Apache-2.0: the text is in `LICENSE`, and a project `wai create` makes from this
|
||||
example carries it.
|
||||
|
|
|
|||
201
wai/examples/package-and-deliver/LICENSE
Normal file
201
wai/examples/package-and-deliver/LICENSE
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for describing the origin of the Work and
|
||||
reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Support. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or support.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2026 EBUNDLE contributors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied. See the License for the specific language governing permissions
|
||||
and limitations under the License.
|
||||
|
|
@ -28,4 +28,5 @@ prints its commands, live packaging among them.
|
|||
**With a coding assistant.** Run `wai setup-agent` in this directory: it writes the
|
||||
instructions an assistant reads and connects it to WAI's tools.
|
||||
|
||||
**Licence.** Apache-2.0: `wai create` writes the licence beside the project, as `LICENSE`.
|
||||
**License.** Apache-2.0: the text is in `LICENSE`, and a project `wai create` makes from this
|
||||
example carries it.
|
||||
|
|
|
|||
201
wai/examples/remote-desktop/LICENSE
Normal file
201
wai/examples/remote-desktop/LICENSE
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for describing the origin of the Work and
|
||||
reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Support. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or support.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2026 EBUNDLE contributors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied. See the License for the specific language governing permissions
|
||||
and limitations under the License.
|
||||
|
|
@ -30,4 +30,5 @@ session's records; the independent verifier in the WAI repository checks them:
|
|||
**With a coding assistant.** Run `wai setup-agent` in this directory: it writes the
|
||||
instructions an assistant reads and connects it to WAI's tools.
|
||||
|
||||
**Licence.** Apache-2.0: `wai create` writes the licence beside the project, as `LICENSE`.
|
||||
**License.** Apache-2.0: the text is in `LICENSE`, and a project `wai create` makes from this
|
||||
example carries it.
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
set -eu
|
||||
ORIGIN=${WAI_DEV_ORIGIN:-http://127.0.0.1:8080}
|
||||
[ -f host.key ] || wai-remote-host key host.key > /dev/null
|
||||
INVITATION=$(joule-moq-room invite .wai/dev/room-keys.toml 3600)
|
||||
INVITATION=$(wai-room invite .wai/dev/room-keys.toml 3600)
|
||||
SOURCE=display
|
||||
if [ "${1:-}" = "--pattern" ]; then
|
||||
SOURCE="pattern --encoder ipcm"
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
# repository's independent verifier must accept the recorded session.
|
||||
#
|
||||
# bash wai/examples/remote-desktop/test/remote-desktop.test.sh
|
||||
# Needs wai, joule-moq-relay, joule-moq-room and wai-remote-host on PATH (or WAI_RELAY_BIN,
|
||||
# Needs wai, wai-relay, wai-room and wai-remote-host on PATH (or WAI_RELAY_BIN,
|
||||
# WAI_ROOM_BIN, WAI_WEB as `wai dev` takes them), and python3.
|
||||
# No -e: each check records its own result, and the run goes on to the next.
|
||||
set -uo pipefail
|
||||
|
|
|
|||
201
wai/examples/verify-receipts/LICENSE
Normal file
201
wai/examples/verify-receipts/LICENSE
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for describing the origin of the Work and
|
||||
reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Support. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or support.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2026 EBUNDLE contributors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied. See the License for the specific language governing permissions
|
||||
and limitations under the License.
|
||||
|
|
@ -28,4 +28,5 @@ command line.
|
|||
**With a coding assistant.** Run `wai setup-agent` in this directory: it writes the
|
||||
instructions an assistant reads and connects it to WAI's tools.
|
||||
|
||||
**Licence.** Apache-2.0: `wai create` writes the licence beside the project, as `LICENSE`.
|
||||
**License.** Apache-2.0: the text is in `LICENSE`, and a project `wai create` makes from this
|
||||
example carries it.
|
||||
|
|
|
|||
201
wai/examples/video-room/LICENSE
Normal file
201
wai/examples/video-room/LICENSE
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for describing the origin of the Work and
|
||||
reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Support. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or support.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2026 EBUNDLE contributors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied. See the License for the specific language governing permissions
|
||||
and limitations under the License.
|
||||
|
|
@ -24,4 +24,5 @@ origin; the API is described at https://wai.transaction.science/docs/api/.
|
|||
**With a coding assistant.** Run `wai setup-agent` in this directory: it writes the
|
||||
instructions an assistant reads and connects it to WAI's tools.
|
||||
|
||||
**Licence.** Apache-2.0: `wai create` writes the licence beside the project, as `LICENSE`.
|
||||
**License.** Apache-2.0: the text is in `LICENSE`, and a project `wai create` makes from this
|
||||
example carries it.
|
||||
|
|
|
|||
201
wai/examples/watch-together/LICENSE
Normal file
201
wai/examples/watch-together/LICENSE
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for describing the origin of the Work and
|
||||
reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Support. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or support.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2026 EBUNDLE contributors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied. See the License for the specific language governing permissions
|
||||
and limitations under the License.
|
||||
|
|
@ -27,4 +27,5 @@ but a WAV file. Or run `wai dev --watch film.webm --watch-ms 90000`.
|
|||
**With a coding assistant.** Run `wai setup-agent` in this directory: it writes the
|
||||
instructions an assistant reads and connects it to WAI's tools.
|
||||
|
||||
**Licence.** Apache-2.0: `wai create` writes the licence beside the project, as `LICENSE`.
|
||||
**License.** Apache-2.0: the text is in `LICENSE`, and a project `wai create` makes from this
|
||||
example carries it.
|
||||
|
|
|
|||
|
|
@ -494,9 +494,10 @@ in order):
|
|||
| `report.export` | `wai report export <digest> --target URL [--out F]` | `GET /v1/fleet/reports/{digest}` | writes a file |
|
||||
| `receipt.verify` | `wai verify <bundle>` | `POST /v1/fleet/verify` | reads |
|
||||
|
||||
Each takes `--json`, and writes its JSON envelope when standard output is not a terminal; each that
|
||||
writes takes `--dry-run`, which writes nothing and says what it would write; one that changes the
|
||||
service waits for `--confirm`, and without it exits with the command that confirms.
|
||||
Each keeps the toolchain's agent rules: `--json` writes its JSON envelope; each that writes takes
|
||||
`--dry-run`, which writes nothing, sends nothing and says what it would; one that changes the
|
||||
service, or would replace a file, waits for `--yes` (`--confirm`, deprecated, is the same), and
|
||||
without it exits 4 with the proposed change and the command that confirms it.
|
||||
|
||||
## 10. Reasons
|
||||
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ of it is open source under Apache-2.0, built from the open-standards repository
|
|||
| File | What it is |
|
||||
|---|---|
|
||||
| `bin/wai` | The command line: `hello`, `create`, `dev`, `verify`, and the envelope tools (`inspect`, `wrap`, `unwrap`, `encode`, `decode` for the mandatory floor, `capabilities`) |
|
||||
| `bin/joule-moq-relay` | The relay a room's media goes through: MOQT over QUIC, WebTransport and WebSocket |
|
||||
| `bin/joule-moq-room` | The room service: a room's keys, its grants, its clock |
|
||||
| `bin/wai-relay` | The relay a room's media goes through: MOQT over QUIC, WebTransport and WebSocket |
|
||||
| `bin/wai-room` | The room service: a room's keys, its grants, its clock |
|
||||
| `bin/wai-remote-host` | A remote desktop room's host agent |
|
||||
| `bin/wai-pack` | The packager: a title as WAI objects, HTTP playlists and a MoQ catalog |
|
||||
| `share/wai/web/` | The room client a browser loads: its pages and their WebAssembly |
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@
|
|||
# Builds, each release and --locked, each from its own workspace:
|
||||
# wai wai/wai-rs, --no-default-features --features quickstart: the mandatory
|
||||
# floor, hello, create, dev and verify; no system codec library
|
||||
# joule-moq-relay the relay (joule-code/joule-code-rs)
|
||||
# joule-moq-room the room service
|
||||
# wai-relay the relay (crate joule-moq-relay, joule-code/joule-code-rs)
|
||||
# wai-room the room service (crate joule-moq-room)
|
||||
# wai-remote-host a remote desktop room's host agent
|
||||
# wai-pack the packager
|
||||
# and writes <out-dir>/wai-<version>-<triple>.tar.gz holding bin/ (those five), share/wai/web/
|
||||
|
|
@ -61,8 +61,8 @@ STAGE=$(mktemp -d)
|
|||
trap 'rm -rf "$STAGE"' EXIT
|
||||
mkdir -p "$STAGE/$NAME/bin" "$STAGE/$NAME/share/wai"
|
||||
need "$ROOT/wai/wai-rs" wai --no-default-features --features quickstart --bin wai
|
||||
need "$ROOT/joule-code/joule-code-rs" joule-moq-relay -p joule-moq-relay --bin joule-moq-relay
|
||||
need "$ROOT/joule-code/joule-code-rs/crates/joule-moq-room" joule-moq-room --bin joule-moq-room
|
||||
need "$ROOT/joule-code/joule-code-rs" wai-relay -p joule-moq-relay --bin wai-relay
|
||||
need "$ROOT/joule-code/joule-code-rs/crates/joule-moq-room" wai-room --bin wai-room
|
||||
need "$ROOT/joule-code/joule-code-rs/crates/joule-moq-remote" wai-remote-host --bin wai-remote-host
|
||||
need "$ROOT/wai/wai-pack" wai-pack --bin wai-pack
|
||||
cp -R "$WEBDIR" "$STAGE/$NAME/share/wai/web"
|
||||
|
|
@ -78,7 +78,7 @@ if [ "$TRIPLE" = "$HOST" ]; then
|
|||
import json, os, subprocess, sys, tempfile
|
||||
wai, ex = sys.argv[1], sys.argv[2]
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
for t in json.loads(subprocess.check_output([wai, "create", "--list", "--json"])):
|
||||
for t in json.loads(subprocess.check_output([wai, "create", "--list", "--json"]))["data"]:
|
||||
out = os.path.join(d, t["template"])
|
||||
subprocess.check_output([wai, "create", out, "--template", t["template"]])
|
||||
for f in t["files"]:
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ if win_sha:
|
|||
"homepage": HOME,
|
||||
"license": "Apache-2.0",
|
||||
"architecture": {"64bit": {"url": url(win + ".zip"), "hash": win_sha, "extract_dir": win}},
|
||||
"bin": ["bin\\wai.exe", "bin\\joule-moq-relay.exe", "bin\\joule-moq-room.exe", "bin\\wai-remote-host.exe", "bin\\wai-pack.exe"],
|
||||
"bin": ["bin\\wai.exe", "bin\\wai-relay.exe", "bin\\wai-room.exe", "bin\\wai-remote-host.exe", "bin\\wai-pack.exe"],
|
||||
}
|
||||
pathlib.Path(out, "wai.json").write_text(json.dumps(manifest, indent=2) + "\n")
|
||||
print(f"wrote the formula{' and the manifest' if win_sha else ''} for wai {version}")
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ BASE="$FORGE_API/repos/$FORGE_REPO/releases"
|
|||
release_id() { curl -fsS -H "$AUTH" "$BASE/tags/$TAG" 2>/dev/null | python3 -c 'import json,sys; print(json.load(sys.stdin).get("id", ""))' 2>/dev/null || true; }
|
||||
ID="$(release_id)"
|
||||
if [ -z "$ID" ]; then
|
||||
BODY="$(python3 -c 'import json,sys; print(json.dumps({"tag_name": sys.argv[1], "name": sys.argv[2], "body": "The WAI command line and what `wai dev` runs, for macOS, Linux and Windows: each archive holds bin/ (wai, joule-moq-relay, joule-moq-room, wai-remote-host, wai-pack), share/wai/web/ (the room client), a README and the licence; each .sha256 file checks its archive (`shasum -a 256 -c`)."}))' "$TAG" "$TITLE")"
|
||||
BODY="$(python3 -c 'import json,sys; print(json.dumps({"tag_name": sys.argv[1], "name": sys.argv[2], "body": "The WAI command line and what `wai dev` runs, for macOS, Linux and Windows: each archive holds bin/ (wai, wai-relay, wai-room, wai-remote-host, wai-pack), share/wai/web/ (the room client), a README and the licence; each .sha256 file checks its archive (`shasum -a 256 -c`)."}))' "$TAG" "$TITLE")"
|
||||
curl -fsS -H "$AUTH" -H 'content-type: application/json' -X POST "$BASE" -d "$BODY" > /dev/null 2>&1 || true
|
||||
# Builds run side by side; one of them created it if this did not.
|
||||
for _ in 1 2 3 4 5 6; do ID="$(release_id)"; [ -n "$ID" ] && break; sleep 3; done
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
// poll opened and answered, the app taken away as app.oui and made a project by
|
||||
// `wai create --from app.oui`, and that project checked and run with `wai dev`.
|
||||
//
|
||||
// node create.e2e.mjs --rt <runtime dir> --relay <joule-moq-relay> --room <joule-moq-room>
|
||||
// node create.e2e.mjs --rt <runtime dir> --relay <wai-relay> --room <wai-room>
|
||||
// --wai <wai, built with app> --web <wai dev's room client> --out <dir>
|
||||
// [--browser <executable>] [--site <dist>]
|
||||
//
|
||||
|
|
@ -232,7 +232,7 @@ async function scenarioBuilt() {
|
|||
const { project, dir } = shared;
|
||||
const ok = wai(["app", "check", "--json"], project);
|
||||
const okv = envelope(ok.out);
|
||||
check("built: `wai app check` passes on the exported project", ok.code === 0 && okv.ok === true, ((okv.result && okv.result.checked) || []).join("; "));
|
||||
check("built: `wai app check` passes on the exported project", ok.code === 0 && okv.ok === true, ((okv.data && okv.data.checked) || []).join("; "));
|
||||
// Negative controls: the check refuses a project that is not whole.
|
||||
const broken = path.join(dir, "exported-broken");
|
||||
const edit = (f, from, to) => { const t = fs.readFileSync(f, "utf8"); if (!from.test(t)) throw new Error(`${f}: ${from}`); fs.writeFileSync(f, t.replace(from, to)); };
|
||||
|
|
@ -248,7 +248,7 @@ async function scenarioBuilt() {
|
|||
mutate(broken);
|
||||
const r = wai(["app", "check", "--json"], broken);
|
||||
const v = envelope(r.out);
|
||||
const failures = ((v.result && v.result.failures) || []).join(" | ");
|
||||
const failures = ((v.data && v.data.failures) || []).join(" | ");
|
||||
check(`built: \`wai app check\` refuses a project ${what}`, r.code === 1 && expect.test(failures), failures.slice(0, 200));
|
||||
}
|
||||
// The project's own development setup: `wai dev` in it, from its wai.json.
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
#
|
||||
# wai/wai-app/e2e/run.sh [--only create,built,refused,own,agent]
|
||||
#
|
||||
# 1. joule-moq-relay and joule-moq-room (native, release), and `wai` with its feature `app`
|
||||
# 1. wai-relay and wai-room (crates joule-moq-relay and joule-moq-room; native, release), and `wai` with its feature `app`
|
||||
# (wai/wai-rs, in a target directory of its own: two workspaces build wai-rs differently);
|
||||
# 2. the WebAssembly packages, through wasm-bindgen's web target, each with its own --out-name,
|
||||
# into the runtime directory: joule-moq-web (room/moq/pkg/wai_room_moq), wai-live-web
|
||||
|
|
@ -35,9 +35,9 @@ mkdir -p "$OUT/results" "$RT"
|
|||
|
||||
step() { echo "== $*"; }
|
||||
|
||||
step "native: joule-moq-relay, joule-moq-room, wai (feature app)"
|
||||
(cd "$JC" && cargo build $LOCKED --release -p joule-moq-relay --bin joule-moq-relay)
|
||||
(cd "$JC/crates/joule-moq-room" && cargo build $LOCKED --release --bin joule-moq-room)
|
||||
step "native: wai-relay, wai-room, wai (feature app)"
|
||||
(cd "$JC" && cargo build $LOCKED --release -p joule-moq-relay --bin wai-relay)
|
||||
(cd "$JC/crates/joule-moq-room" && cargo build $LOCKED --release --bin wai-room)
|
||||
WAI_TARGET="$TARGET/wai-app-cli"
|
||||
(cd "$ROOT/wai/wai-rs" && CARGO_TARGET_DIR="$WAI_TARGET" cargo build $LOCKED --release --no-default-features --features app --bin wai)
|
||||
|
||||
|
|
@ -75,8 +75,8 @@ browser=()
|
|||
step "end-to-end"
|
||||
node "$HERE/create.e2e.mjs" \
|
||||
--rt "$RT" \
|
||||
--relay "$TARGET/release/joule-moq-relay" \
|
||||
--room "$TARGET/release/joule-moq-room" \
|
||||
--relay "$TARGET/release/wai-relay" \
|
||||
--room "$TARGET/release/wai-room" \
|
||||
--wai "$WAI_TARGET/release/wai" \
|
||||
--web "$OUT/web" \
|
||||
--out "$OUT/results" \
|
||||
|
|
|
|||
|
|
@ -1507,6 +1507,13 @@ name = "cli_dx"
|
|||
path = "tests/cli_dx.rs"
|
||||
required-features = ["quickstart"]
|
||||
|
||||
# The operation catalogue through the binary: `wai ops`, help, the exit codes, the JSON
|
||||
# envelope, --dry-run on every operation that is not read-only, and --yes.
|
||||
[[test]]
|
||||
name = "cli_ops"
|
||||
path = "tests/cli_ops.rs"
|
||||
required-features = ["quickstart"]
|
||||
|
||||
# `wai breaking` needs no feature; `wai conform` runs the reference verifiers, the score
|
||||
# renderer and the origin of this crate as the implementations under test.
|
||||
[[test]]
|
||||
|
|
|
|||
|
|
@ -5,17 +5,23 @@
|
|||
//! (AVIF/JPEG-XL/PNG/Opus/FLAC/AV1/zstd/XZ). Designed for scripting and quick
|
||||
//! verification; production paths go through the Rust API or the C FFI.
|
||||
//!
|
||||
//! Every command is an operation of the catalogue (`wai::catalogue`), named `noun.verb`,
|
||||
//! and the command line reaches it only through the catalogue (`wai_cli/door.rs`): its
|
||||
//! words, its usage and its help come from there, and so do the agent rules every command
|
||||
//! keeps (`wai_cli/agent.rs`: the exit codes, `--json`, `--dry-run`, `--yes`). `wai ops`
|
||||
//! lists, exports and searches the catalogue.
|
||||
//!
|
||||
//! Every command except `encode` and `decode` builds without the `codecs`
|
||||
//! feature (no system codec libraries), which is how CI runs it. With the `floor`
|
||||
//! feature alone, `encode` writes PNG and zstd and `decode` opens the mandatory floor
|
||||
//! (PNG, FLAC, zstd), still with no system library. `up` (a local relay, room service
|
||||
//! (PNG, FLAC, zstd), still with no system library. `dev` (a local relay, room service
|
||||
//! and room page on one origin) and `create` (a new project from an example) need no
|
||||
//! feature. `generate` (make a
|
||||
//! `wai.generate.spectral` picture natively and print its SHA-256) needs the feature
|
||||
//! `generate`, and `score build|render` (write a `WSCR` from integer note fields,
|
||||
//! render one and print its digests) the feature `score`; neither needs a codec library.
|
||||
//! `wai` sends nothing off the machine unless a command names a remote service.
|
||||
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::process::ExitCode;
|
||||
|
||||
|
|
@ -54,11 +60,19 @@ mod score;
|
|||
#[path = "wai_cli/encode.rs"]
|
||||
mod platform_encode_cli;
|
||||
|
||||
// Every verb kept in a module of its own, named in one table (dx::VERBS): the developer's
|
||||
// path (`hello`, `create`, `dev`, `verify`) and the agent door (`mcp`, `setup-agent`).
|
||||
// The developer's path (`hello`, `create`, `dev`, `verify`) and the agent door (`mcp`,
|
||||
// `setup-agent`), one module an operation, their implementations by catalogue id in dx::VERBS.
|
||||
#[path = "wai_cli/dx/mod.rs"]
|
||||
mod dx;
|
||||
|
||||
// The command line's door to the catalogue: dispatch, help and `wai ops`.
|
||||
#[path = "wai_cli/door.rs"]
|
||||
mod door;
|
||||
|
||||
// Every file a command writes, through one place, so `--dry-run` writes none.
|
||||
#[path = "wai_cli/out.rs"]
|
||||
mod out;
|
||||
|
||||
#[cfg(feature = "http")]
|
||||
#[path = "wai_cli/http.rs"]
|
||||
mod http;
|
||||
|
|
@ -69,51 +83,6 @@ mod http_vectors;
|
|||
#[path = "wai_cli/http_playlists.rs"]
|
||||
mod http_playlists;
|
||||
|
||||
fn die(msg: impl std::fmt::Display) -> ExitCode {
|
||||
eprintln!("error: {msg}");
|
||||
ExitCode::from(2)
|
||||
}
|
||||
|
||||
fn usage() {
|
||||
eprintln!("wai encode <codec> <input> [-o out.wai] [-q quality]");
|
||||
eprintln!("wai decode <in.wai> -o <output>");
|
||||
eprintln!("wai inspect <in.wai>");
|
||||
eprintln!("wai wrap <capability> <payload> [-o out.wai] [--fallback <cap>] [--intent <intent>]");
|
||||
eprintln!(" [--companion <cap> <payload>]... WAI2: companions bound to the payload");
|
||||
eprintln!("wai unwrap <in.wai> [-o payload] [--component <i>] carry/extract ANY registered codec's bytes");
|
||||
eprintln!("wai capabilities list registered capabilities + tier + license + replicate class + role");
|
||||
eprintln!("wai registry export [--root <wai-dir>] [--check]");
|
||||
eprintln!(" write (or, with --check, compare) registry/capabilities.json, its schema,");
|
||||
eprintln!(" vocab/v2.* and the generated table of REGISTRY.md");
|
||||
eprintln!("wai prior-set <root> print the listing of a parameter set (wai.prior.set) and its pin");
|
||||
eprintln!("wai stage refine|apply|inspect|pack|explode … staged-delivery layers (a build with `staged`,");
|
||||
eprintln!(" `video_int`, `audio_int` and `splat_int`); `wai stage` alone prints its usage");
|
||||
eprintln!("wai http publish|codecs|origin|vectors|bench … HTTP delivery (a build with `http`);");
|
||||
eprintln!(" `wai http` alone prints its usage");
|
||||
eprintln!("wai generate --model <json> (--prompt <s> --seed <u32> --steps <n> | --instr <object>)");
|
||||
eprintln!(" --size WxH -o <out.rgb> [--json] make a wai.generate.spectral picture (RGB8) and");
|
||||
eprintln!(" print its SHA-256; --sizes WxH,... --write-instr <object> writes the instruction");
|
||||
eprintln!(" object of form v0 (SPEC \u{a7}5). A build with `generate`");
|
||||
eprintln!("wai score build <notes.json> -o <out.wscr> [--json] a WSCR from integer note fields");
|
||||
eprintln!("wai score render <in.wscr> [--pcm <out.s16>] [--json] the reference mixdown and its digests");
|
||||
eprintln!(" (pcm_sha256, mixdown_blake3, score_hash). A build with `score`");
|
||||
#[cfg(feature = "platform_encode")]
|
||||
platform_encode_cli::usage();
|
||||
#[cfg(not(feature = "platform_encode"))]
|
||||
eprintln!("wai encode avc|hevc …, wai encoders, wai encode-record verify … a build with `platform_encode`");
|
||||
eprintln!();
|
||||
dx::usage();
|
||||
eprintln!();
|
||||
eprintln!("encode and decode need a build with the `codecs` feature; a build with `floor` alone");
|
||||
eprintln!("encodes png and zstd and decodes png, flac and zstd (the mandatory floor, no system library).");
|
||||
eprintln!("codecs: png jpeg avif jxl jxl-lossless opus flac av1 av1-lossless zstd xz");
|
||||
eprintln!("inputs:");
|
||||
eprintln!(" image codecs: .rgb (with .meta containing 'H W' ASCII) or PNG/JPEG file");
|
||||
eprintln!(" audio codecs: .f32 (raw mono float32) with .meta containing sample-rate");
|
||||
eprintln!(" video codecs: .rgb-seq directory of frame files + .meta with 'N H W fps_num fps_den'");
|
||||
eprintln!(" text codecs: any file");
|
||||
}
|
||||
|
||||
/// The capabilities this binary decodes natively: the crate's sink set
|
||||
/// (`wai::sink_capabilities`) in a build with the `codecs` feature, the mandatory
|
||||
/// floor (`wai::floor_capabilities`: PNG, FLAC, zstd) in a build with `floor` alone,
|
||||
|
|
@ -293,8 +262,9 @@ fn encode(codec: &str, input: &str, out: &str, q: u8) -> Result<(), String> {
|
|||
};
|
||||
|
||||
let manifest = Manifest::new(media_for(cap), "replicate", ModelRequirement::new(cap), Conditioning::new(kind));
|
||||
let n = Wai::new(manifest, payload).write(out).map_err(|e| e.to_string())?;
|
||||
println!("encoded → {out} ({} bytes, capability {})", n, cap);
|
||||
let bytes = Wai::new(manifest, payload).to_bytes().map_err(|e| e.to_string())?;
|
||||
out::write(out, &bytes).map_err(|e| format!("{out}: {e}"))?;
|
||||
println!("encoded → {out} ({} bytes, capability {})", bytes.len(), cap);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -325,48 +295,48 @@ fn decode(input: &str, out: &str) -> Result<(), String> {
|
|||
match cap {
|
||||
c if c == CAP_IMAGE_PNG => {
|
||||
let (rgb, h, w) = image::png_decode(&wai.payload).map_err(|e| format!("{:?}", e))?;
|
||||
fs::write(out, &rgb).map_err(|e| e.to_string())?;
|
||||
fs::write(format!("{out}.meta"), format!("{h} {w}\n")).map_err(|e| e.to_string())?;
|
||||
out::write(out, &rgb).map_err(|e| e.to_string())?;
|
||||
out::write(format!("{out}.meta"), format!("{h} {w}\n")).map_err(|e| e.to_string())?;
|
||||
}
|
||||
#[cfg(feature = "codecs")]
|
||||
c if c == CAP_IMAGE_JPEG => {
|
||||
let (rgb, h, w) = image::jpeg_decode(&wai.payload).map_err(|e| format!("{:?}", e))?;
|
||||
fs::write(out, &rgb).map_err(|e| e.to_string())?;
|
||||
fs::write(format!("{out}.meta"), format!("{h} {w}\n")).map_err(|e| e.to_string())?;
|
||||
out::write(out, &rgb).map_err(|e| e.to_string())?;
|
||||
out::write(format!("{out}.meta"), format!("{h} {w}\n")).map_err(|e| e.to_string())?;
|
||||
}
|
||||
#[cfg(feature = "codecs")]
|
||||
c if c == CAP_IMAGE_AVIF => {
|
||||
let (rgb, h, w) = image::avif_decode(&wai.payload).map_err(|e| format!("{:?}", e))?;
|
||||
fs::write(out, &rgb).map_err(|e| e.to_string())?;
|
||||
fs::write(format!("{out}.meta"), format!("{h} {w}\n")).map_err(|e| e.to_string())?;
|
||||
out::write(out, &rgb).map_err(|e| e.to_string())?;
|
||||
out::write(format!("{out}.meta"), format!("{h} {w}\n")).map_err(|e| e.to_string())?;
|
||||
}
|
||||
#[cfg(feature = "codecs")]
|
||||
c if c == CAP_IMAGE_JXL => {
|
||||
let (rgb, h, w) = image::jxl_decode(&wai.payload).map_err(|e| format!("{:?}", e))?;
|
||||
fs::write(out, &rgb).map_err(|e| e.to_string())?;
|
||||
fs::write(format!("{out}.meta"), format!("{h} {w}\n")).map_err(|e| e.to_string())?;
|
||||
out::write(out, &rgb).map_err(|e| e.to_string())?;
|
||||
out::write(format!("{out}.meta"), format!("{h} {w}\n")).map_err(|e| e.to_string())?;
|
||||
}
|
||||
#[cfg(feature = "codecs")]
|
||||
c if c == CAP_AUDIO_OPUS => {
|
||||
let (samples, sr) = audio::opus_decode(&wai.payload).map_err(|e| format!("{:?}", e))?;
|
||||
let bytes: Vec<u8> = samples.iter().flat_map(|f| f.to_le_bytes()).collect();
|
||||
fs::write(out, &bytes).map_err(|e| e.to_string())?;
|
||||
fs::write(format!("{out}.meta"), format!("{sr}\n")).map_err(|e| e.to_string())?;
|
||||
out::write(out, &bytes).map_err(|e| e.to_string())?;
|
||||
out::write(format!("{out}.meta"), format!("{sr}\n")).map_err(|e| e.to_string())?;
|
||||
}
|
||||
c if c == CAP_AUDIO_FLAC => {
|
||||
let (samples, sr) = audio::flac_decode(&wai.payload).map_err(|e| format!("{:?}", e))?;
|
||||
let bytes: Vec<u8> = samples.iter().flat_map(|f| f.to_le_bytes()).collect();
|
||||
fs::write(out, &bytes).map_err(|e| e.to_string())?;
|
||||
fs::write(format!("{out}.meta"), format!("{sr}\n")).map_err(|e| e.to_string())?;
|
||||
out::write(out, &bytes).map_err(|e| e.to_string())?;
|
||||
out::write(format!("{out}.meta"), format!("{sr}\n")).map_err(|e| e.to_string())?;
|
||||
}
|
||||
c if c == CAP_TEXT_ZSTD => {
|
||||
let d = text::zstd_decode(&wai.payload).map_err(|e| format!("{:?}", e))?;
|
||||
fs::write(out, &d).map_err(|e| e.to_string())?;
|
||||
out::write(out, &d).map_err(|e| e.to_string())?;
|
||||
}
|
||||
#[cfg(feature = "codecs")]
|
||||
c if c == CAP_TEXT_XZ => {
|
||||
let d = text::xz_decode(&wai.payload).map_err(|e| format!("{:?}", e))?;
|
||||
fs::write(out, &d).map_err(|e| e.to_string())?;
|
||||
out::write(out, &d).map_err(|e| e.to_string())?;
|
||||
}
|
||||
#[cfg(feature = "codecs")]
|
||||
c if c == CAP_VIDEO_AV1 || c == CAP_VIDEO_AV1_LOSSLESS => {
|
||||
|
|
@ -374,7 +344,7 @@ fn decode(input: &str, out: &str) -> Result<(), String> {
|
|||
// decode to far more than fits in memory at once.
|
||||
use std::io::Write as _;
|
||||
let hd = video::av1_header(&wai.payload).map_err(|e| format!("{:?}", e))?;
|
||||
let mut file = std::io::BufWriter::new(fs::File::create(out).map_err(|e| e.to_string())?);
|
||||
let mut file = std::io::BufWriter::new(out::create(out).map_err(|e| e.to_string())?);
|
||||
let mut n = 0u32;
|
||||
video::av1_decode_each(&wai.payload, |rgb, w, h| {
|
||||
if (w, h) != (hd.w, hd.h) {
|
||||
|
|
@ -385,7 +355,7 @@ fn decode(input: &str, out: &str) -> Result<(), String> {
|
|||
file.write_all(rgb).map_err(|e| video::VideoError(e.to_string()))
|
||||
}).map_err(|e| format!("{:?}", e))?;
|
||||
file.flush().map_err(|e| e.to_string())?;
|
||||
fs::write(format!("{out}.meta"),
|
||||
out::write(format!("{out}.meta"),
|
||||
format!("{n} {} {} {} {}\n", hd.h, hd.w, hd.fps_num, hd.fps_den))
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
|
@ -692,7 +662,9 @@ fn wrap(cap: &str, input: &str, out: &str, fallback: Option<&str>,
|
|||
"{e}: a layer names what it refines; `wai stage refine` writes one (extensions/staged-delivery.md)"),
|
||||
_ => format!("{e} (SPEC \u{a7}5)"),
|
||||
})?;
|
||||
let n = envelope.write(out).map_err(|e| e.to_string())?;
|
||||
let bytes = envelope.to_bytes().map_err(|e| e.to_string())?;
|
||||
out::write(out, &bytes).map_err(|e| format!("{out}: {e}"))?;
|
||||
let n = bytes.len();
|
||||
let tier = wai::codecs::capability_tier(cap).map(|t| format!("{t:?}")).unwrap_or_else(|| "?".into());
|
||||
Ok(Wrapped {
|
||||
lines: vec![
|
||||
|
|
@ -804,7 +776,9 @@ fn wrap_with_companions(cap: &str, payload: Vec<u8>, out: &str, fallback: Option
|
|||
envelope.check_int_payload_framing().map_err(|(i, e)| format!("not writing {out}: component {i}: {e}"))?;
|
||||
envelope.check_component_roles()
|
||||
.map_err(|e| format!("{e} (SPEC \u{a7}5, \u{a7}7.1)"))?;
|
||||
let n = envelope.write(out).map_err(|e| e.to_string())?;
|
||||
let bytes = envelope.to_bytes().map_err(|e| e.to_string())?;
|
||||
out::write(out, &bytes).map_err(|e| format!("{out}: {e}"))?;
|
||||
let n = bytes.len();
|
||||
let mut lines = vec![format!("wrapped → {out} ({n} B WAI2 envelope, {} components, intent {intent})",
|
||||
envelope.renditions.len())];
|
||||
lines.extend(component_lines(&envelope));
|
||||
|
|
@ -882,7 +856,7 @@ fn unwrap(input: &str, out: Option<&str>, component: Option<&str>) -> Result<(),
|
|||
}
|
||||
println!(" payload {} bytes", w.payload.len());
|
||||
if let Some(out) = out {
|
||||
std::fs::write(out, &w.payload).map_err(|e| format!("write {out}: {e}"))?;
|
||||
out::write(out, &w.payload).map_err(|e| format!("write {out}: {e}"))?;
|
||||
println!(" → wrote payload to {out} (byte-identical to the wrapped input)");
|
||||
}
|
||||
Ok(())
|
||||
|
|
@ -917,7 +891,7 @@ fn unwrap_component(input: &str, bytes: &[u8], out: Option<&str>,
|
|||
}
|
||||
println!(" component [{idx}] {} · {} bytes", meta.capability, payload.len());
|
||||
if let Some(out) = out {
|
||||
std::fs::write(out, payload).map_err(|e| format!("write {out}: {e}"))?;
|
||||
out::write(out, payload).map_err(|e| format!("write {out}: {e}"))?;
|
||||
println!(" → wrote component {idx} to {out} (byte-identical to the wrapped input)");
|
||||
}
|
||||
Ok(())
|
||||
|
|
@ -983,9 +957,9 @@ fn registry(args: &[String]) -> Result<bool, String> {
|
|||
stale.push(rel.clone());
|
||||
} else {
|
||||
if let Some(dir) = path.parent() {
|
||||
fs::create_dir_all(dir).map_err(|e| format!("create {}: {e}", dir.display()))?;
|
||||
out::create_dir_all(dir).map_err(|e| format!("create {}: {e}", dir.display()))?;
|
||||
}
|
||||
fs::write(&path, want).map_err(|e| format!("write {}: {e}", path.display()))?;
|
||||
out::write(&path, want).map_err(|e| format!("write {}: {e}", path.display()))?;
|
||||
println!("wrote {}", path.display());
|
||||
}
|
||||
}
|
||||
|
|
@ -1001,84 +975,7 @@ fn registry(args: &[String]) -> Result<bool, String> {
|
|||
Ok(true)
|
||||
}
|
||||
|
||||
/// Every command line goes through the catalogue's door.
|
||||
fn main() -> ExitCode {
|
||||
let args: Vec<String> = env::args().skip(1).collect();
|
||||
if args.is_empty() {
|
||||
usage();
|
||||
return ExitCode::from(2);
|
||||
}
|
||||
if let Some(r) = dx::dispatch(&args) {
|
||||
return match r {
|
||||
Ok(code) => ExitCode::from(code),
|
||||
Err(e) => die(e),
|
||||
};
|
||||
}
|
||||
let r = match args[0].as_str() {
|
||||
#[cfg(feature = "platform_encode")]
|
||||
"encode" if args.len() >= 3 && matches!(args[1].as_str(), "avc" | "hevc") => platform_encode_cli::encode(&args[1..]),
|
||||
#[cfg(not(feature = "platform_encode"))]
|
||||
"encode" if args.len() >= 3 && matches!(args[1].as_str(), "avc" | "hevc") => {
|
||||
Err("built without platform_encode: `wai encode avc|hevc` needs a build with the feature `platform_encode`".into())
|
||||
}
|
||||
#[cfg(feature = "platform_encode")]
|
||||
"encoders" => platform_encode_cli::encoders(&args[1..]),
|
||||
#[cfg(feature = "platform_encode")]
|
||||
"encode-record" => match platform_encode_cli::record_cmd(&args[1..]) {
|
||||
Ok(true) => Ok(()),
|
||||
Ok(false) => return ExitCode::from(1),
|
||||
Err(e) => Err(e),
|
||||
},
|
||||
#[cfg(not(feature = "platform_encode"))]
|
||||
"encoders" | "encode-record" => Err("built without platform_encode: this command needs a build with the feature `platform_encode`".into()),
|
||||
"encode" if args.len() >= 3 => {
|
||||
let out = opt(&args[3..], "-o").map(String::from).unwrap_or("out.wai".into());
|
||||
let q: u8 = opt(&args[3..], "-q").and_then(|s| s.parse().ok()).unwrap_or(75);
|
||||
encode(&args[1], &args[2], &out, q)
|
||||
}
|
||||
"decode" if args.len() >= 4 => decode(&args[1], &args[3]),
|
||||
"wrap" if args.len() >= 3 => {
|
||||
let out = opt(&args[3..], "-o").map(String::from).unwrap_or("out.wai".into());
|
||||
match companion_args(&args[3..]) {
|
||||
Ok(comps) => wrap(&args[1], &args[2], &out, opt(&args[3..], "--fallback"),
|
||||
opt(&args[3..], "--intent"), &comps)
|
||||
.map(|w| w.lines.iter().for_each(|l| println!("{l}"))),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
"unwrap" if args.len() >= 2 => unwrap(&args[1], opt(&args[2..], "-o"),
|
||||
opt(&args[2..], "--component")),
|
||||
"inspect" if args.len() >= 2 => inspect(&args[1]),
|
||||
"capabilities" => capabilities(),
|
||||
"registry" => match registry(&args[1..]) {
|
||||
Ok(true) => Ok(()),
|
||||
Ok(false) => return ExitCode::from(1),
|
||||
Err(e) => Err(e),
|
||||
},
|
||||
"prior-set" if args.len() >= 2 => prior_set(&args[1]),
|
||||
#[cfg(all(feature = "staged", feature = "video_int", feature = "audio_int", feature = "splat_int"))]
|
||||
"stage" => stage::main(&args[1..]),
|
||||
#[cfg(not(all(feature = "staged", feature = "video_int", feature = "audio_int", feature = "splat_int")))]
|
||||
"stage" => Err("`wai stage` needs a build with the features staged, video_int, audio_int and splat_int".into()),
|
||||
#[cfg(feature = "http")]
|
||||
"http" => match http::main(&args[1..]) {
|
||||
Ok(true) => Ok(()),
|
||||
Ok(false) => return ExitCode::from(1),
|
||||
Err(e) => Err(e),
|
||||
},
|
||||
#[cfg(not(feature = "http"))]
|
||||
"http" => Err("built without http: `wai http` needs a build with the feature `http`".into()),
|
||||
#[cfg(feature = "generate")]
|
||||
"generate" => generate::main(&args[1..]),
|
||||
#[cfg(not(feature = "generate"))]
|
||||
"generate" => Err("built without generate: `wai generate` needs a build with the feature `generate`".into()),
|
||||
#[cfg(feature = "score")]
|
||||
"score" => score::main(&args[1..]),
|
||||
#[cfg(not(feature = "score"))]
|
||||
"score" => Err("built without score: `wai score` needs a build with the feature `score`".into()),
|
||||
_ => { usage(); return ExitCode::from(2); }
|
||||
};
|
||||
match r {
|
||||
Ok(()) => ExitCode::SUCCESS,
|
||||
Err(e) => die(e),
|
||||
}
|
||||
door::main()
|
||||
}
|
||||
|
|
|
|||
672
wai/wai-rs/src/bin/wai_cli/door.rs
Normal file
672
wai/wai-rs/src/bin/wai_cli/door.rs
Normal file
|
|
@ -0,0 +1,672 @@
|
|||
//! The command line's door to the operation catalogue (`wai::catalogue`).
|
||||
//!
|
||||
//! A command line is resolved through the catalogue alone: its words name an operation (its
|
||||
//! CLI spelling, an alias, or its mechanical form `wai <noun> <verb>`), and the operation's
|
||||
//! implementation is found by its id in [`impls`]. A command the catalogue does not hold
|
||||
//! cannot run, and help is written from the catalogue's entries. This module's tests hold
|
||||
//! the two tables to each other: every implementation names an operation of the catalogue,
|
||||
//! and every operation with a command line has exactly one implementation.
|
||||
//!
|
||||
//! The agent rules (`agent`) apply to every operation here: the exit codes, `--json`,
|
||||
//! `--dry-run` and `--yes`. An operation that prints text runs again as a child for
|
||||
//! `--json`, its output carried in the envelope's `data`; one with a JSON form of its own
|
||||
//! carries that.
|
||||
|
||||
use crate::dx::envelope::{self as agent, Ctx, OpError};
|
||||
use crate::out;
|
||||
use serde_json::{Value, json};
|
||||
use std::process::{Command, ExitCode, Stdio};
|
||||
use wai::catalogue::{self, Effect, Op, OPS};
|
||||
|
||||
/// An operation's implementation: its arguments after the command's words, `Ok(true)` when
|
||||
/// everything it checked held, `Ok(false)` when it ran and a check did not (exit 1).
|
||||
pub type Run = fn(&[String]) -> Result<bool, OpError>;
|
||||
|
||||
/// What an operation prints for `--json`.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Json {
|
||||
/// Text: carried as `{"text": ...}`.
|
||||
Text,
|
||||
/// A JSON form of its own, asked for with `--json`.
|
||||
Own,
|
||||
/// Its own JSON form when this flag is among its arguments; text otherwise.
|
||||
OwnWith(&'static str),
|
||||
/// It prints the envelope itself.
|
||||
Native,
|
||||
/// It streams JSON Lines itself, unless this flag is among its arguments, when it has a
|
||||
/// JSON form of its own.
|
||||
StreamUnless(&'static str),
|
||||
}
|
||||
|
||||
/// One implementation, by its operation's id.
|
||||
pub struct Impl {
|
||||
pub id: &'static str,
|
||||
pub run: Run,
|
||||
pub json: Json,
|
||||
}
|
||||
|
||||
/// Every implementation: the developer's path (`dx::VERBS`), then the rest.
|
||||
pub fn impls() -> impl Iterator<Item = &'static Impl> {
|
||||
crate::dx::VERBS.iter().chain(IMPLS.iter())
|
||||
}
|
||||
|
||||
fn impl_of(id: &str) -> Option<&'static Impl> {
|
||||
impls().find(|i| i.id == id)
|
||||
}
|
||||
|
||||
/// Whether `a` is a flag every operation takes (`catalogue::AGENT_FLAGS`), read wherever it is
|
||||
/// on the command line.
|
||||
fn global(a: &str) -> bool {
|
||||
catalogue::is_agent_flag(a)
|
||||
}
|
||||
|
||||
pub fn main() -> ExitCode {
|
||||
let raw: Vec<String> = std::env::args().skip(1).collect();
|
||||
let child = std::env::var_os(agent::CHILD_ENV).is_some();
|
||||
let has = |f: &str| raw.iter().any(|a| a == f);
|
||||
let c = Ctx {
|
||||
// A child's `--json` is its operation's own flag, given by its parent.
|
||||
json: !child && has("--json"),
|
||||
dry_run: has("--dry-run"),
|
||||
// `--confirm` is the deprecated spelling of `--yes`, and does the same.
|
||||
yes: has("--yes") || has("--confirm"),
|
||||
no_input: has("--no-input"),
|
||||
child,
|
||||
argv: raw.iter().filter(|a| *a != "--json").cloned().collect(),
|
||||
};
|
||||
let json = c.json;
|
||||
out::set_dry_run(c.dry_run);
|
||||
agent::set(c);
|
||||
let args: Vec<String> = raw.iter().filter(|a| !global(a) || (child && *a == "--json")).cloned().collect();
|
||||
|
||||
match args.first().map(String::as_str) {
|
||||
None => {
|
||||
eprint!("{}", usage_text());
|
||||
return fail(None, &OpError::usage("name an operation: `wai ops list` lists them, `wai --help` describes them", ""), json);
|
||||
}
|
||||
Some("--help" | "-h") => {
|
||||
print!("{}", usage_text());
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
Some("help") => {
|
||||
match catalogue::resolve(&args[1..]) {
|
||||
Some((op, _)) => print!("{}", op_help(op)),
|
||||
None => print!("{}", usage_text()),
|
||||
}
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
let Some((op, n)) = catalogue::resolve(&args) else {
|
||||
return fail(None, &unknown(&args), json);
|
||||
};
|
||||
let rest = &args[n..];
|
||||
if rest.iter().any(|a| a == "--help" || a == "-h") {
|
||||
print!("{}", op_help(op));
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
if !op.available() {
|
||||
return fail(Some(op.id), ¬_built(op), json);
|
||||
}
|
||||
let Some(imp) = impl_of(op.id) else {
|
||||
return fail(Some(op.id), &OpError::internal(format!("{} has no implementation in this binary", op.id)), json);
|
||||
};
|
||||
if json {
|
||||
match imp.json {
|
||||
Json::Native => {}
|
||||
Json::StreamUnless(f) if !rest.iter().any(|a| a == f) => {
|
||||
let mut with = rest.to_vec();
|
||||
with.push("--json".into());
|
||||
return finish(op, imp, run(imp, &with), json);
|
||||
}
|
||||
Json::Text | Json::Own | Json::OwnWith(_) | Json::StreamUnless(_) => return enveloped(op, imp, rest),
|
||||
}
|
||||
}
|
||||
finish(op, imp, run(imp, rest), json)
|
||||
}
|
||||
|
||||
/// Run an implementation, a panic in it an internal error.
|
||||
fn run(imp: &Impl, args: &[String]) -> Result<bool, OpError> {
|
||||
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| (imp.run)(args)))
|
||||
.unwrap_or_else(|_| Err(OpError::internal(format!("{} stopped on an internal error (above)", imp.id))))
|
||||
}
|
||||
|
||||
/// The exit of an operation run here, with what a dry run would have done (an operation that
|
||||
/// prints its own envelope says that itself).
|
||||
fn finish(op: &Op, imp: &Impl, r: Result<bool, OpError>, json: bool) -> ExitCode {
|
||||
match r {
|
||||
Ok(ok) => {
|
||||
if out::dry_run() && op.effect != Effect::Reads && imp.json != Json::Native {
|
||||
if agent::ctx().child {
|
||||
eprintln!("{}{}", agent::TRAILER_PLAN, Value::Array(out::plan()));
|
||||
} else if !json {
|
||||
let lines = out::plan_lines();
|
||||
if lines.is_empty() {
|
||||
println!("dry run: nothing would be written");
|
||||
} else {
|
||||
println!("dry run: nothing was written. Without --dry-run, wai would:");
|
||||
for l in lines {
|
||||
println!(" {l}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ok { ExitCode::SUCCESS } else { ExitCode::from(1) }
|
||||
}
|
||||
Err(f) => fail(Some(op.id), &f, json),
|
||||
}
|
||||
}
|
||||
|
||||
/// Report a failure (as an envelope with `--json`) and exit with its code; one the command
|
||||
/// reported itself only exits.
|
||||
fn fail(op: Option<&str>, f: &OpError, json: bool) -> ExitCode {
|
||||
if f.reported {
|
||||
return ExitCode::from(f.exit);
|
||||
}
|
||||
agent::report(op, f);
|
||||
if json {
|
||||
agent::print(&agent::error_envelope_with(op, f, None));
|
||||
}
|
||||
ExitCode::from(f.exit)
|
||||
}
|
||||
|
||||
/// `--json` for an operation that prints text, or JSON of its own: it runs again as a child
|
||||
/// of this process, and its output and exit become the envelope.
|
||||
fn enveloped(op: &Op, imp: &Impl, rest: &[String]) -> ExitCode {
|
||||
let own = match imp.json {
|
||||
Json::Own => true,
|
||||
Json::OwnWith(f) | Json::StreamUnless(f) => rest.iter().any(|a| a == f),
|
||||
Json::Text | Json::Native => false,
|
||||
};
|
||||
let mut a = agent::ctx().argv.clone();
|
||||
if own {
|
||||
a.push("--json".into());
|
||||
}
|
||||
let exe = match std::env::current_exe() {
|
||||
Ok(e) => e,
|
||||
Err(e) => return fail(Some(op.id), &OpError::internal(format!("this binary's path: {e}")), true),
|
||||
};
|
||||
let o = match Command::new(exe).args(&a).env(agent::CHILD_ENV, "1").stdin(Stdio::inherit()).output() {
|
||||
Ok(o) => o,
|
||||
Err(e) => return fail(Some(op.id), &OpError::internal(format!("running {}: {e}", op.id)), true),
|
||||
};
|
||||
let code = o.status.code().and_then(|c| u8::try_from(c).ok()).unwrap_or(7);
|
||||
let (mut trailer, mut plan, mut logs) = (None::<Value>, None::<Value>, Vec::new());
|
||||
for line in String::from_utf8_lossy(&o.stderr).lines() {
|
||||
if let Some(j) = line.strip_prefix(agent::TRAILER_ERROR) {
|
||||
trailer = serde_json::from_str(j).ok();
|
||||
} else if let Some(j) = line.strip_prefix(agent::TRAILER_PLAN) {
|
||||
plan = serde_json::from_str(j).ok();
|
||||
} else {
|
||||
logs.push(line.to_string());
|
||||
}
|
||||
}
|
||||
for l in &logs {
|
||||
eprintln!("{l}");
|
||||
}
|
||||
let stdout = String::from_utf8_lossy(&o.stdout).into_owned();
|
||||
let data = || -> Value {
|
||||
if own && let Ok(v) = serde_json::from_str::<Value>(stdout.trim()) {
|
||||
return v;
|
||||
}
|
||||
json!({"text": stdout})
|
||||
};
|
||||
let v = match code {
|
||||
0 => agent::envelope_with(op.id, true, data(), plan.and_then(|p| p.as_array().cloned())),
|
||||
1 => {
|
||||
let msg = logs.iter().rev().find(|l| !l.trim().is_empty()).cloned().unwrap_or_else(|| format!("{}: a check did not hold", op.id));
|
||||
agent::error_envelope_with(Some(op.id), &OpError::of(1, "check-failed", msg), Some(data()))
|
||||
}
|
||||
_ => match trailer {
|
||||
Some(t) => {
|
||||
let mut v = json!({"wai": catalogue::ENVELOPE, "op": op.id, "ok": false, "error": t["error"], "exit": code});
|
||||
if code == 4 {
|
||||
v["proposed"] = t["proposed"].clone();
|
||||
v["confirm"] = t["confirm"].clone();
|
||||
}
|
||||
v
|
||||
}
|
||||
None => {
|
||||
let msg = logs.iter().rev().find(|l| !l.trim().is_empty()).cloned().unwrap_or_else(|| format!("{} exited {code}", op.id));
|
||||
let f = if code == 7 || o.status.code().is_none() { OpError::internal(msg) } else { OpError::from(msg) };
|
||||
agent::error_envelope_with(Some(op.id), &f, None)
|
||||
}
|
||||
},
|
||||
};
|
||||
agent::print(&v);
|
||||
ExitCode::from(code)
|
||||
}
|
||||
|
||||
/// A command line that names no operation: exit 5, with the closest operations.
|
||||
fn unknown(args: &[String]) -> OpError {
|
||||
let words: Vec<&str> = args.iter().take_while(|a| !a.starts_with('-')).map(String::as_str).collect();
|
||||
let near: Vec<String> = catalogue::search(&words.join(" "), 5).iter().filter(|m| m.op.cli.is_some()).take(3).map(|m| format!("{} ({})", m.op.command(), m.op.id)).collect();
|
||||
let f = OpError::of(5, "operation-not-found", format!("`wai {}` names no operation", words.join(" ")));
|
||||
if near.is_empty() {
|
||||
f.with_hint("`wai ops list` lists every operation; `wai ops search \"<task>\"` finds one")
|
||||
} else {
|
||||
f.with_hint(format!("the closest: {}; `wai ops search \"<task>\"` finds others", near.join(", ")))
|
||||
}
|
||||
}
|
||||
|
||||
/// An operation this build does not hold: exit 5, naming the build that does.
|
||||
pub fn not_built(op: &Op) -> OpError {
|
||||
let need = match op.needs {
|
||||
catalogue::Needs::Nothing => String::new(),
|
||||
catalogue::Needs::All(f) => format!("the feature{} {}", if f.len() > 1 { "s" } else { "" }, f.join(", ")),
|
||||
catalogue::Needs::Any(f) => format!("one of the features {}", f.join(", ")),
|
||||
};
|
||||
OpError::of(5, "operation-not-in-build", format!("`{}` ({}) needs a build with {need}", op.command(), op.id))
|
||||
.with_hint("the release archive's `wai` holds the developer's path; a source build takes --features")
|
||||
}
|
||||
|
||||
// ─────────────────────────────── help, from the catalogue ───────────────────────────────
|
||||
|
||||
/// The help `wai --help` prints: every operation, the developer's path first.
|
||||
pub fn usage_text() -> String {
|
||||
let mut s = String::new();
|
||||
s += "wai — the WAI toolchain. Every operation has an id, noun.verb, and `wai <noun> <verb>` runs it\n";
|
||||
s += "as well as the command shown.\n\nStart here:\n";
|
||||
let line = |o: &Op| -> String {
|
||||
let cmd = o.command();
|
||||
let note = if o.available() { String::new() } else { " (not in this build)".into() };
|
||||
format!(" {cmd:<26} {:<20} {}{note}\n", o.id, o.summary)
|
||||
};
|
||||
let start = ["toolchain.test", "project.create", "dev.start", "receipt.verify"];
|
||||
for id in start {
|
||||
if let Some(o) = catalogue::op(id) {
|
||||
s += &line(o);
|
||||
}
|
||||
}
|
||||
s += "\nEvery other operation:\n";
|
||||
for o in OPS.iter().filter(|o| o.cli.is_some() && !start.contains(&o.id)) {
|
||||
s += &line(o);
|
||||
}
|
||||
s += "\nFor every operation:\n";
|
||||
for f in catalogue::AGENT_FLAGS {
|
||||
s += &format!(" {:<11} {}\n", f.flag, f.summary);
|
||||
if let Some((alias, _)) = f.deprecated_alias {
|
||||
s += &format!(" {alias:<11} deprecated: the same as {}, which replaces it\n", f.flag);
|
||||
}
|
||||
}
|
||||
s += " --help the operation's usage, effect and id\n";
|
||||
s += "exit codes: ";
|
||||
s += &catalogue::EXIT_CODES.iter().map(|(c, m)| format!("{c} {m}")).collect::<Vec<_>>().join("; ");
|
||||
s += "\n\n`wai ops search \"<task>\"` finds an operation; `wai ops schema <id>` describes one as JSON.\n";
|
||||
s += "`wai` sends nothing off your machine unless you name a remote service.\n";
|
||||
s
|
||||
}
|
||||
|
||||
/// One operation's help.
|
||||
pub fn op_help(o: &Op) -> String {
|
||||
let mut s = format!("{} — {}\n{}\n\nusage:\n", o.command(), o.id, o.summary);
|
||||
for l in o.usage.lines() {
|
||||
if l.starts_with(' ') {
|
||||
s += &format!(" {l}\n");
|
||||
} else {
|
||||
s += &format!(" wai {l}\n");
|
||||
}
|
||||
}
|
||||
let forms: Vec<String> = o.cli_forms().iter().map(|f| format!("wai {}", f.join(" "))).filter(|f| *f != o.command()).collect();
|
||||
if !forms.is_empty() {
|
||||
s += &format!("also: {}\n", forms.join(", "));
|
||||
}
|
||||
s += &format!("\neffect {} · runs {} · toolset {} · {}\n", o.effect.label(), o.placement.label(), o.toolset.label(), o.stability.label());
|
||||
if o.dry_run() {
|
||||
s += "--dry-run writes nothing and says what would be written.\n";
|
||||
}
|
||||
if let Some(when) = o.confirms().sentence() {
|
||||
s += &format!("{when}: --yes, or yes at a terminal (--confirm, deprecated, is the same as --yes).\n");
|
||||
}
|
||||
if !o.available() {
|
||||
s += &format!("{}\n", not_built(o).message);
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
// ─────────────────────────────── wai ops ───────────────────────────────
|
||||
|
||||
fn ops_list(a: &[String]) -> Result<bool, OpError> {
|
||||
if let Some(x) = a.first() {
|
||||
return Err(OpError::usage(format!("wai ops list: {x}: takes no argument"), ""));
|
||||
}
|
||||
let rows: Vec<Value> = OPS
|
||||
.iter()
|
||||
.map(|o| json!({"id": o.id, "summary": o.summary, "cli": o.cli.map(|_| o.command()), "effect": o.effect.label(), "placement": o.placement.label(), "toolset": o.toolset.label(), "stability": o.stability.label(), "available": o.available()}))
|
||||
.collect();
|
||||
if agent::ctx().json {
|
||||
agent::print(&agent::envelope_with("ops.list", true, Value::Array(rows), None));
|
||||
} else {
|
||||
for o in OPS {
|
||||
let cmd = if o.cli.is_some() { o.command() } else { format!("({} only)", o.route.map_or("MCP", |_| "HTTP")) };
|
||||
println!("{:<22} {:<30} {:<15}{}", o.id, cmd, o.effect.label(), if o.available() { "" } else { " (not in this build)" });
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn ops_search(a: &[String]) -> Result<bool, OpError> {
|
||||
if a.is_empty() || a.iter().any(|x| x.starts_with("--")) {
|
||||
return Err(OpError::usage("usage: wai ops search \"<task>\"", ""));
|
||||
}
|
||||
let found = catalogue::search(&a.join(" "), 5);
|
||||
if agent::ctx().json {
|
||||
let rows: Vec<Value> = found.iter().map(|m| json!({"id": m.op.id, "summary": m.op.summary, "cli": m.op.command(), "score": m.score})).collect();
|
||||
agent::print(&agent::envelope_with("ops.search", true, Value::Array(rows), None));
|
||||
} else if found.is_empty() {
|
||||
println!("no operation matches; `wai ops list` lists them all");
|
||||
} else {
|
||||
for m in &found {
|
||||
println!("{:<22} {:<30} {}", m.op.id, m.op.command(), m.op.summary);
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn ops_schema(a: &[String]) -> Result<bool, OpError> {
|
||||
let [id] = a else {
|
||||
return Err(OpError::usage("usage: wai ops schema <id>", ""));
|
||||
};
|
||||
let o = catalogue::op(id).ok_or_else(|| {
|
||||
let near: Vec<&str> = catalogue::search(&id.replace(['.', '_'], " "), 3).iter().map(|m| m.op.id).collect();
|
||||
OpError::of(5, "operation-not-found", format!("no operation {id}")).with_hint(format!("the closest: {}", near.join(", ")))
|
||||
})?;
|
||||
if agent::ctx().json {
|
||||
agent::print(&agent::envelope_with("ops.show", true, o.to_json(), None));
|
||||
} else {
|
||||
println!("{}", serde_json::to_string_pretty(&o.to_json()).expect("json"));
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn ops_export(a: &[String]) -> Result<bool, OpError> {
|
||||
let (mut out_path, mut check) = (None, None);
|
||||
let mut i = 0;
|
||||
while i < a.len() {
|
||||
match (a[i].as_str(), a.get(i + 1)) {
|
||||
("--out", Some(v)) => out_path = Some(v.clone()),
|
||||
("--check", Some(v)) => check = Some(v.clone()),
|
||||
(x, _) => return Err(OpError::usage(format!("wai ops export: {x}: not an option (--out FILE | --check FILE)"), "")),
|
||||
}
|
||||
i += 2;
|
||||
}
|
||||
let text = catalogue::export_text();
|
||||
let json = agent::ctx().json;
|
||||
match (out_path, check) {
|
||||
(Some(_), Some(_)) => Err(OpError::usage("wai ops export: --out or --check, not both", "")),
|
||||
(None, Some(f)) => {
|
||||
let have = std::fs::read_to_string(&f).map_err(|e| OpError::from(format!("{f}: {e}")))?;
|
||||
let same = have == text;
|
||||
if json {
|
||||
let d = json!({"file": f, "up_to_date": same});
|
||||
if same {
|
||||
agent::print(&agent::envelope_with("ops.export", true, d, None));
|
||||
} else {
|
||||
agent::print(&agent::error_envelope_with(Some("ops.export"), &OpError::of(1, "check-failed", format!("{f} differs from the catalogue")), Some(d)));
|
||||
}
|
||||
} else if same {
|
||||
println!("ops export: {f} is up to date");
|
||||
} else {
|
||||
eprintln!("{f} differs from the catalogue in wai-rs/src/catalogue.rs");
|
||||
eprintln!("regenerate: cd wai/wai-rs && cargo run -q --no-default-features --bin wai -- ops export --out {f}");
|
||||
}
|
||||
Ok(same)
|
||||
}
|
||||
(Some(f), None) => {
|
||||
out::write(&f, &text).map_err(|e| OpError::from(format!("{f}: {e}")))?;
|
||||
if json {
|
||||
agent::print(&agent::envelope_with("ops.export", true, json!({"file": f, "bytes": text.len()}), out::dry_run().then(out::plan)));
|
||||
} else if out::dry_run() {
|
||||
println!("dry run: nothing was written; without --dry-run, wai would write {f} ({} bytes)", text.len());
|
||||
} else {
|
||||
println!("wrote {f}");
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
(None, None) => {
|
||||
if json {
|
||||
agent::print(&agent::envelope_with("ops.export", true, catalogue::export(), None));
|
||||
} else {
|
||||
print!("{text}");
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────── the other implementations ───────────────────────────────
|
||||
|
||||
type R = Result<bool, OpError>;
|
||||
|
||||
/// The usage failure of operation `id`.
|
||||
fn usage_of(id: &str) -> OpError {
|
||||
let o = catalogue::op(id).expect("an operation of the catalogue");
|
||||
OpError::usage(format!("usage: {}", op_help(o).split("usage:\n").nth(1).unwrap_or("").split("\n\n").next().unwrap_or("").trim()), "")
|
||||
}
|
||||
|
||||
/// The output path a writing command was given with `-o`, or its default; a default that
|
||||
/// would replace a file waits for confirmation.
|
||||
fn named_or_default<'a>(rest: &'a [String], default: &'a str) -> Result<&'a str, OpError> {
|
||||
match crate::opt(rest, "-o") {
|
||||
Some(o) => Ok(o),
|
||||
None => {
|
||||
if std::path::Path::new(default).exists() {
|
||||
agent::confirm(&format!("{default} exists, and the command line did not name it as the output (-o)"), vec![json!({"replace": default})])?;
|
||||
}
|
||||
Ok(default)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn envelope_inspect(a: &[String]) -> R {
|
||||
let [p, ..] = a else { return Err(usage_of("envelope.inspect")) };
|
||||
crate::inspect(p)?;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn envelope_wrap(a: &[String]) -> R {
|
||||
let [cap, payload, rest @ ..] = a else { return Err(usage_of("envelope.wrap")) };
|
||||
let comps = crate::companion_args(rest)?;
|
||||
let out = named_or_default(rest, "out.wai")?;
|
||||
let w = crate::wrap(cap, payload, out, crate::opt(rest, "--fallback"), crate::opt(rest, "--intent"), &comps)?;
|
||||
for l in &w.lines {
|
||||
println!("{l}");
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn envelope_unwrap(a: &[String]) -> R {
|
||||
let [p, rest @ ..] = a else { return Err(usage_of("envelope.unwrap")) };
|
||||
crate::unwrap(p, crate::opt(rest, "-o"), crate::opt(rest, "--component"))?;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn media_encode(a: &[String]) -> R {
|
||||
if matches!(a.first().map(String::as_str), Some("avc" | "hevc")) {
|
||||
return platform_encode(a);
|
||||
}
|
||||
// In a build without `floor` or `codecs`, `crate::encode` says so.
|
||||
let [codec, input, rest @ ..] = a else { return Err(usage_of("media.encode")) };
|
||||
let out = named_or_default(rest, "out.wai")?;
|
||||
let q: u8 = crate::opt(rest, "-q").and_then(|s| s.parse().ok()).unwrap_or(75);
|
||||
crate::encode(codec, input, out, q)?;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn media_decode(a: &[String]) -> R {
|
||||
let [p, rest @ ..] = a else { return Err(usage_of("media.decode")) };
|
||||
// The output was the third word (`wai decode in.wai -o out`), whatever the second.
|
||||
let out = crate::opt(rest, "-o").or_else(|| rest.get(1).map(String::as_str)).ok_or_else(|| usage_of("media.decode"))?;
|
||||
crate::decode(p, out)?;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn registry_list(a: &[String]) -> R {
|
||||
if let Some(x) = a.first() {
|
||||
return Err(OpError::usage(format!("wai capabilities: {x}: takes no argument"), ""));
|
||||
}
|
||||
crate::capabilities()?;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn registry_export(a: &[String]) -> R {
|
||||
let mut v = vec!["export".to_string()];
|
||||
v.extend_from_slice(a);
|
||||
Ok(crate::registry(&v)?)
|
||||
}
|
||||
|
||||
fn prior_list(a: &[String]) -> R {
|
||||
let [root, ..] = a else { return Err(usage_of("prior.list")) };
|
||||
crate::prior_set(root)?;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// `words` then `a`.
|
||||
#[allow(dead_code)]
|
||||
fn with(words: &[&str], a: &[String]) -> Vec<String> {
|
||||
words.iter().map(|w| w.to_string()).chain(a.iter().cloned()).collect()
|
||||
}
|
||||
|
||||
/// An implementation that needs build features: compiled with them, and without them a
|
||||
/// function that says which (the door checks the catalogue's `needs` before either runs).
|
||||
macro_rules! gated {
|
||||
($name:ident, $id:literal, cfg($($cfg:tt)*), |$a:ident| $body:expr) => {
|
||||
#[cfg($($cfg)*)]
|
||||
fn $name($a: &[String]) -> R {
|
||||
$body
|
||||
}
|
||||
#[cfg(not($($cfg)*))]
|
||||
fn $name(_: &[String]) -> R {
|
||||
Err(not_built(catalogue::op($id).expect("listed")))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
gated!(platform_encode, "media.encode", cfg(feature = "platform_encode"), |a| {
|
||||
crate::platform_encode_cli::encode(a)?;
|
||||
Ok(true)
|
||||
});
|
||||
gated!(media_generate, "media.generate", cfg(feature = "generate"), |a| {
|
||||
crate::generate::main(a)?;
|
||||
Ok(true)
|
||||
});
|
||||
gated!(score_build, "score.build", cfg(feature = "score"), |a| {
|
||||
crate::score::main(&with(&["build"], a))?;
|
||||
Ok(true)
|
||||
});
|
||||
gated!(score_render, "score.decode", cfg(feature = "score"), |a| {
|
||||
crate::score::main(&with(&["render"], a))?;
|
||||
Ok(true)
|
||||
});
|
||||
gated!(encoder_list, "encoder.list", cfg(feature = "platform_encode"), |a| {
|
||||
crate::platform_encode_cli::encoders(a)?;
|
||||
Ok(true)
|
||||
});
|
||||
gated!(encoderecord_verify, "encoder.verify", cfg(feature = "platform_encode"), |a| Ok(crate::platform_encode_cli::record_cmd(&with(&["verify"], a))?));
|
||||
|
||||
macro_rules! stage_op {
|
||||
($name:ident, $sub:literal, $id:literal) => {
|
||||
gated!($name, $id, cfg(all(feature = "staged", feature = "video_int", feature = "audio_int", feature = "splat_int")), |a| {
|
||||
crate::stage::main(&with(&[$sub], a))?;
|
||||
Ok(true)
|
||||
});
|
||||
};
|
||||
}
|
||||
stage_op!(stage_refine, "refine", "stage.encode");
|
||||
stage_op!(stage_apply, "apply", "stage.decode");
|
||||
stage_op!(stage_base, "base", "stage.build");
|
||||
stage_op!(stage_inspect, "inspect", "stage.inspect");
|
||||
stage_op!(stage_pack, "pack", "stage.wrap");
|
||||
stage_op!(stage_explode, "explode", "stage.unwrap");
|
||||
|
||||
macro_rules! http_op {
|
||||
($name:ident, $sub:literal, $id:literal) => {
|
||||
gated!($name, $id, cfg(feature = "http"), |a| Ok(crate::http::main(&with(&[$sub], a))?));
|
||||
};
|
||||
}
|
||||
http_op!(http_publish, "publish", "http.export");
|
||||
http_op!(http_codecs, "codecs", "http.inspect");
|
||||
http_op!(http_origin, "origin", "http.start");
|
||||
http_op!(http_hls, "hls", "hls.build");
|
||||
http_op!(http_mpd, "mpd", "dash.build");
|
||||
http_op!(http_sealplaylist, "seal-playlist", "playlist.lock");
|
||||
http_op!(http_checkplaylist, "check-playlist", "playlist.verify");
|
||||
http_op!(http_vectors, "vectors", "corpus.generate");
|
||||
http_op!(http_bench, "bench", "http.load");
|
||||
http_op!(http_receipts, "receipts", "receipt.fetch");
|
||||
|
||||
/// Every implementation but the developer's path (`dx::VERBS`), by its operation's id.
|
||||
pub const IMPLS: &[Impl] = &[
|
||||
Impl { id: "envelope.inspect", run: envelope_inspect, json: Json::Text },
|
||||
Impl { id: "envelope.wrap", run: envelope_wrap, json: Json::Text },
|
||||
Impl { id: "envelope.unwrap", run: envelope_unwrap, json: Json::Text },
|
||||
Impl { id: "media.encode", run: media_encode, json: Json::Text },
|
||||
Impl { id: "media.decode", run: media_decode, json: Json::Text },
|
||||
Impl { id: "media.generate", run: media_generate, json: Json::Own },
|
||||
Impl { id: "score.build", run: score_build, json: Json::Own },
|
||||
Impl { id: "score.decode", run: score_render, json: Json::Own },
|
||||
Impl { id: "encoder.list", run: encoder_list, json: Json::Text },
|
||||
Impl { id: "encoder.verify", run: encoderecord_verify, json: Json::Text },
|
||||
Impl { id: "registry.list", run: registry_list, json: Json::Text },
|
||||
Impl { id: "registry.export", run: registry_export, json: Json::Text },
|
||||
Impl { id: "prior.list", run: prior_list, json: Json::Text },
|
||||
Impl { id: "stage.encode", run: stage_refine, json: Json::Text },
|
||||
Impl { id: "stage.decode", run: stage_apply, json: Json::Own },
|
||||
Impl { id: "stage.build", run: stage_base, json: Json::Text },
|
||||
Impl { id: "stage.inspect", run: stage_inspect, json: Json::Text },
|
||||
Impl { id: "stage.wrap", run: stage_pack, json: Json::Text },
|
||||
Impl { id: "stage.unwrap", run: stage_explode, json: Json::Text },
|
||||
Impl { id: "http.export", run: http_publish, json: Json::Text },
|
||||
Impl { id: "http.inspect", run: http_codecs, json: Json::Text },
|
||||
Impl { id: "http.start", run: http_origin, json: Json::Text },
|
||||
Impl { id: "hls.build", run: http_hls, json: Json::Text },
|
||||
Impl { id: "dash.build", run: http_mpd, json: Json::Text },
|
||||
Impl { id: "playlist.lock", run: http_sealplaylist, json: Json::Text },
|
||||
Impl { id: "playlist.verify", run: http_checkplaylist, json: Json::Text },
|
||||
Impl { id: "corpus.generate", run: http_vectors, json: Json::Text },
|
||||
Impl { id: "http.load", run: http_bench, json: Json::Text },
|
||||
Impl { id: "receipt.fetch", run: http_receipts, json: Json::Text },
|
||||
Impl { id: "ops.list", run: ops_list, json: Json::Native },
|
||||
Impl { id: "ops.search", run: ops_search, json: Json::Native },
|
||||
Impl { id: "ops.show", run: ops_schema, json: Json::Native },
|
||||
Impl { id: "ops.export", run: ops_export, json: Json::Native },
|
||||
];
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn every_implementation_names_an_operation_of_the_catalogue() {
|
||||
for i in impls() {
|
||||
assert!(catalogue::op(i.id).is_some(), "the CLI runs {}, which the catalogue does not hold: add it to wai-rs/src/catalogue.rs", i.id);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_operation_with_a_command_line_has_exactly_one_implementation() {
|
||||
for o in OPS.iter().filter(|o| o.cli.is_some()) {
|
||||
let n = impls().filter(|i| i.id == o.id).count();
|
||||
assert_eq!(n, 1, "{} ({}) has {n} implementations in the CLI", o.id, o.command());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn help_is_written_from_the_catalogue_and_names_every_operation() {
|
||||
let u = usage_text();
|
||||
for o in OPS.iter().filter(|o| o.cli.is_some()) {
|
||||
assert!(u.contains(o.id) && u.contains(&o.command()), "{} is missing from the help", o.id);
|
||||
let h = op_help(o);
|
||||
assert!(h.contains(o.id) && h.contains(o.summary), "{}'s own help", o.id);
|
||||
assert_eq!(h.contains("--dry-run"), o.effect != Effect::Reads, "{}: the help says --dry-run exactly when it applies", o.id);
|
||||
}
|
||||
assert!(u.contains("sends nothing off your machine unless you name a remote service"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_usage_failure_quotes_the_operations_synopsis() {
|
||||
let f = usage_of("envelope.wrap");
|
||||
assert_eq!(f.exit, 2);
|
||||
assert!(f.message.contains("wai wrap <capability> <payload>"), "{}", f.message);
|
||||
}
|
||||
}
|
||||
|
|
@ -18,7 +18,7 @@ pub fn main(_: &[String]) -> Result<bool, String> {
|
|||
|
||||
#[cfg(feature = "app")]
|
||||
pub fn main(args: &[String]) -> Result<bool, String> {
|
||||
use super::{flag, positional};
|
||||
use super::positional;
|
||||
use std::path::Path;
|
||||
match args.first().map(String::as_str) {
|
||||
Some("guide") => {
|
||||
|
|
@ -36,14 +36,10 @@ pub fn main(args: &[String]) -> Result<bool, String> {
|
|||
let at = Path::new(pos.first().copied().unwrap_or("."));
|
||||
let report = check(at)?;
|
||||
let ok = report.failures.is_empty();
|
||||
if flag(args, "--json") || !std::io::IsTerminal::is_terminal(&std::io::stdout()) {
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::json!({
|
||||
"wai": "envelope/1", "op": "app.check", "ok": ok,
|
||||
"result": { "program": report.program, "project": report.project, "room": report.room, "errors": report.errors, "failures": report.failures, "checked": report.checked },
|
||||
})
|
||||
);
|
||||
if super::envelope::json_wanted(args) {
|
||||
let body = serde_json::json!({ "program": report.program, "project": report.project, "room": report.room, "errors": report.errors, "failures": report.failures, "checked": report.checked });
|
||||
let serde_json::Value::Object(body) = body else { unreachable!("an object") };
|
||||
println!("{}", super::envelope::envelope("app.check", ok, body));
|
||||
} else {
|
||||
println!("wai app check: {}", report.program);
|
||||
for c in &report.checked {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
//! each case's outcome, and the standard's version.
|
||||
//!
|
||||
//! Exit status: 0 when every case passed, 1 when one did not, 2 for a usage error, 4 when
|
||||
//! the record would replace a file and `--confirm` was not given, 5 when the suite, corpus
|
||||
//! the record would replace a file and `--yes` was not given, 5 when the suite, corpus
|
||||
//! or program is not found, 6 when the implementation's URL does not answer, 7 otherwise.
|
||||
|
||||
mod adapter;
|
||||
|
|
@ -18,7 +18,7 @@ mod http;
|
|||
pub mod record;
|
||||
pub mod suites;
|
||||
|
||||
use super::envelope::{self, EXIT_CONFIRM, EXIT_FAILED, EXIT_INTERNAL, EXIT_NOT_FOUND, EXIT_UNAVAILABLE, OpError};
|
||||
use super::envelope::{self, EXIT_FAILED, EXIT_INTERNAL, EXIT_NOT_FOUND, EXIT_UNAVAILABLE, OpError};
|
||||
use serde_json::{Map, Value, json};
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
|
@ -27,7 +27,7 @@ use suites::{Case, Expect, Input, SUITES, Suite};
|
|||
|
||||
pub const OP: &str = "conformance.run";
|
||||
|
||||
const USAGE: &str = "wai conform <kind> --suite <suite> --impl <command or URL> [--key <file>] [--out <file>] [--confirm]\n\
|
||||
const USAGE: &str = "wai conform <kind> --suite <suite> --impl <command or URL> [--key <file>] [--out <file>] [--yes]\n\
|
||||
[--root <wai-dir>] [--timeout <seconds>] [--name <text>] [--version <text>] [--dry-run] [--all] [--json|--text]\n\
|
||||
wai conform --list";
|
||||
|
||||
|
|
@ -403,7 +403,7 @@ pub fn main(args: &[String]) -> Result<bool, String> {
|
|||
}
|
||||
|
||||
fn run(args: &[String], json_mode: bool) -> Result<bool, OpError> {
|
||||
let flags = ["--suite", "--impl", "--key", "--out", "--confirm", "--root", "--timeout", "--name", "--version", "--dry-run", "--all", "--json", "--text", "--list"];
|
||||
let flags = ["--suite", "--impl", "--key", "--out", "--root", "--timeout", "--name", "--version", "--dry-run", "--all", "--json", "--text", "--list"];
|
||||
let with_value = ["--suite", "--impl", "--key", "--out", "--root", "--timeout", "--name", "--version"];
|
||||
let pos = envelope::check_args(args, &flags, &with_value, 1, USAGE)?;
|
||||
let v = |f: &str| envelope::value(args, f, USAGE);
|
||||
|
|
@ -451,6 +451,8 @@ fn run(args: &[String], json_mode: bool) -> Result<bool, OpError> {
|
|||
}
|
||||
let out = PathBuf::from(v("--out")?.map(String::from).unwrap_or_else(|| format!("{}{}", suite.name, record::SUFFIX)));
|
||||
if args.iter().any(|a| a == "--dry-run") {
|
||||
// The record it would write, in the envelope's plan.
|
||||
crate::out::would_write(&out);
|
||||
let first = &loaded.cases[0];
|
||||
let plan = match &imp {
|
||||
Impl::Command(c) if whole => adapter::fill(c, &[("corpus", loaded.corpus_file.as_deref().unwrap_or(Path::new("")))]),
|
||||
|
|
@ -471,12 +473,14 @@ fn run(args: &[String], json_mode: bool) -> Result<bool, OpError> {
|
|||
println!("{}", envelope::envelope(OP, true, m));
|
||||
} else {
|
||||
println!("wai conform {kind} {}: {} cases from {}; would run: {plan}", suite.name, loaded.cases.len(), suite.dir);
|
||||
println!("the record would be written to {}", out.display());
|
||||
println!("dry run: nothing was run and nothing written; the record would be written to {}", out.display());
|
||||
}
|
||||
return Ok(true);
|
||||
}
|
||||
if out.exists() && !args.iter().any(|a| a == "--confirm") {
|
||||
return Err(OpError::new(EXIT_CONFIRM, "confirmation-required", format!("{} exists; the record would replace it", out.display()), format!("re-run with --confirm, or --out <another file>: wai conform {} --confirm", args.join(" "))));
|
||||
if out.exists() {
|
||||
// `--yes` (or its deprecated spelling `--confirm`), or yes at a terminal; otherwise exit 4
|
||||
// with the proposal and the confirming command.
|
||||
envelope::confirm(&format!("{} exists, and the record would replace it", out.display()), vec![json!({"write": out.display().to_string(), "replaces": true})])?;
|
||||
}
|
||||
let key = match v("--key")? {
|
||||
Some(p) => (record::key_from_file(Path::new(p)).map_err(|e| OpError::new(EXIT_NOT_FOUND, "key-not-read", e, "--key names a file holding a 32-byte Ed25519 seed in hexadecimal"))?, "supplied"),
|
||||
|
|
@ -527,7 +531,7 @@ fn run(args: &[String], json_mode: bool) -> Result<bool, OpError> {
|
|||
rec.insert("key".into(), json!(key.1));
|
||||
let rec = record::sign(rec, &key.0).map_err(|e| OpError::new(EXIT_INTERNAL, "internal", e, ""))?;
|
||||
let text = serde_json::to_string_pretty(&Value::Object(rec.clone())).map_err(|e| OpError::new(EXIT_INTERNAL, "internal", e.to_string(), ""))?;
|
||||
std::fs::write(&out, format!("{text}\n")).map_err(|e| OpError::new(EXIT_INTERNAL, "internal", format!("{}: {e}", out.display()), ""))?;
|
||||
crate::out::write(&out, format!("{text}\n")).map_err(|e| OpError::new(EXIT_INTERNAL, "internal", format!("{}: {e}", out.display()), ""))?;
|
||||
let pubkey = rec.get("signer_pubkey").and_then(Value::as_str).unwrap_or("").to_string();
|
||||
if json_mode {
|
||||
let mut m = Map::new();
|
||||
|
|
|
|||
|
|
@ -5,25 +5,27 @@
|
|||
//! Each example's `test/` directory is the repository's check that the example works, and is
|
||||
//! not part of the project. Where a template names a generated file (a watch room's sample
|
||||
//! sound), it is made here.
|
||||
//! - `wai create <dir> --from app.oui` (catalogue `app.export`; a build with `app`): from a
|
||||
//! program made with Create. The app's files are `wai_app::export`'s, its runtime is copied
|
||||
//! in from a Create runtime (`--runtime DIR`, `$WAI_APP_RUNTIME`, or `share/wai/app` beside
|
||||
//! this binary) and checked file by file, and its project file names the program and the
|
||||
//! room `wai dev` runs for it.
|
||||
//! - `wai create <dir> --from app.oui` (also `wai app export`, catalogue `app.export`; a build
|
||||
//! with `app`): from a program made with Create. The app's files are `wai_app::export`'s, its
|
||||
//! runtime is copied in from a Create runtime (`--runtime DIR`, `$WAI_APP_RUNTIME`, or
|
||||
//! `share/wai/app` beside this binary) and checked file by file, and its project file names
|
||||
//! the program and the room `wai dev` runs for it.
|
||||
//!
|
||||
//! Either way the project file is written by [`project::render`], and every project carries
|
||||
//! the licence its files are under, Apache-2.0, as `LICENSE`. It writes into a directory that
|
||||
//! does not exist or is empty, and never over a file.
|
||||
//! Either way the project file is written by [`project::render`] and read before anything is
|
||||
//! written, and every project carries the licence its files are under, Apache-2.0, as
|
||||
//! `LICENSE` (each example holds it too). It writes into a directory that does not exist or is
|
||||
//! empty, and never over a file; every write goes through `out`, so `--dry-run` writes none.
|
||||
|
||||
#[path = "templates.rs"]
|
||||
mod templates;
|
||||
|
||||
use super::{flag, opt, positional, project};
|
||||
use super::envelope::OpError;
|
||||
use crate::out;
|
||||
use std::path::Path;
|
||||
|
||||
pub const USAGE: &str = "wai create <dir> --template <name> | wai create <dir> --from app.oui [--runtime DIR] | wai create --list [--json]\n a new project from one of the examples, or from an app made with Create";
|
||||
|
||||
/// The licence every project `wai create` writes is under, and the file that says so.
|
||||
/// The licence every project `wai create` writes is under, and the file that says so: one copy
|
||||
/// in this binary, which each example's `LICENSE` equals byte for byte.
|
||||
const LICENSE: (&str, &[u8]) = ("LICENSE", include_bytes!("../../../../../LICENSE"));
|
||||
|
||||
/// A file of a template: its path in the project, and its bytes.
|
||||
|
|
@ -39,12 +41,12 @@ pub struct Template {
|
|||
pub generated: &'static [Generated],
|
||||
}
|
||||
|
||||
pub fn main(args: &[String]) -> Result<bool, String> {
|
||||
pub fn main(args: &[String]) -> Result<bool, OpError> {
|
||||
if flag(args, "--list") {
|
||||
if flag(args, "--json") {
|
||||
let v: Vec<serde_json::Value> = templates::TEMPLATES
|
||||
.iter()
|
||||
.map(|t| serde_json::json!({ "template": t.name, "title": t.title, "files": t.files.iter().map(|f| f.0).chain(t.generated.iter().map(|g| g.0)).chain([LICENSE.0]).collect::<Vec<_>>() }))
|
||||
.map(|t| serde_json::json!({ "template": t.name, "title": t.title, "files": t.files.iter().map(|f| f.0).chain(t.generated.iter().map(|g| g.0)).collect::<Vec<_>>() }))
|
||||
.collect();
|
||||
println!("{}", serde_json::to_string_pretty(&v).expect("json"));
|
||||
} else {
|
||||
|
|
@ -54,15 +56,16 @@ pub fn main(args: &[String]) -> Result<bool, String> {
|
|||
}
|
||||
return Ok(true);
|
||||
}
|
||||
let usage = super::usage("project.create");
|
||||
if let Some(a) = args.iter().find(|a| a.starts_with("--") && !["--template", "--from", "--runtime"].contains(&a.as_str())) {
|
||||
return Err(format!("wai create: {a}: not an option ({USAGE})"));
|
||||
return Err(OpError::usage(format!("wai create: {a}: not an option ({usage})"), ""));
|
||||
}
|
||||
if let Some(program) = opt(args, "--from") {
|
||||
let pos = positional(args, &["--from", "--runtime"]);
|
||||
let (Some(dir), 1, None) = (pos.first(), pos.len(), opt(args, "--template")) else {
|
||||
return Err(format!("wai create <dir> --from app.oui [--runtime DIR]\n{USAGE}"));
|
||||
return Err(OpError::usage(format!("wai create <dir> --from app.oui [--runtime DIR]\n{usage}"), ""));
|
||||
};
|
||||
return from_app(Path::new(program), Path::new(dir), opt(args, "--runtime").map(Path::new)).map(|written| {
|
||||
return from_app(Path::new(program), Path::new(dir), opt(args, "--runtime").map(Path::new)).map_err(OpError::from).map(|written| {
|
||||
println!("wai create: an app made with Create, in {dir}");
|
||||
for f in written.iter().filter(|f| !f.starts_with("vendor/")) {
|
||||
println!(" {f}");
|
||||
|
|
@ -74,7 +77,7 @@ pub fn main(args: &[String]) -> Result<bool, String> {
|
|||
}
|
||||
let pos = positional(args, &["--template"]);
|
||||
let (Some(dir), Some(name), 1) = (pos.first(), opt(args, "--template"), pos.len()) else {
|
||||
return Err(format!("{USAGE}\ntemplates: {}", names()));
|
||||
return Err(OpError::usage(format!("{usage}\ntemplates: {}", names()), ""));
|
||||
};
|
||||
let t = templates::TEMPLATES.iter().find(|t| t.name == name).ok_or_else(|| format!("no template {name}; the templates: {}", names()))?;
|
||||
let dir = Path::new(dir);
|
||||
|
|
@ -114,19 +117,20 @@ fn write_project(dir: &Path, mut files: Vec<(String, Vec<u8>)>) -> Result<(), St
|
|||
if !files.iter().any(|(p, _)| p == LICENSE.0) {
|
||||
files.push((LICENSE.0.into(), LICENSE.1.to_vec()));
|
||||
}
|
||||
// The project file must read before anything is written: a template is checked as it is
|
||||
// made.
|
||||
let pf = files.iter().find(|(p, _)| *p == project::FILE).map(|(_, b)| String::from_utf8_lossy(b).into_owned()).unwrap_or_default();
|
||||
project::parse(dir, &pf).map_err(|e| format!("{}: {e}", dir.join(project::FILE).display()))?;
|
||||
for (rel, bytes) in &files {
|
||||
if rel.split('/').any(|s| s.is_empty() || s == "." || s == "..") || rel.starts_with('/') {
|
||||
return Err(format!("{rel}: not a path a template may write"));
|
||||
}
|
||||
let p = dir.join(rel);
|
||||
if let Some(parent) = p.parent() {
|
||||
std::fs::create_dir_all(parent).map_err(|e| format!("{}: {e}", parent.display()))?;
|
||||
out::create_dir_all(parent).map_err(|e| format!("{}: {e}", parent.display()))?;
|
||||
}
|
||||
let mut f = std::fs::OpenOptions::new().write(true).create_new(true).open(&p).map_err(|e| format!("{}: {e}", p.display()))?;
|
||||
std::io::Write::write_all(&mut f, bytes).map_err(|e| format!("{}: {e}", p.display()))?;
|
||||
out::write_new(&p, bytes).map_err(|e| format!("{}: {e}", p.display()))?;
|
||||
}
|
||||
// The project file must read: a project is checked as it is made.
|
||||
project::load(dir)?.ok_or("the project file is missing")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//! `wai dev`: a relay (`joule-moq-relay`), a room service (`joule-moq-room`) and the room
|
||||
//! page on one origin, on this machine, for development.
|
||||
//! `wai dev`: a relay (`wai-relay`), a room service (`wai-room`) and the room page on one
|
||||
//! origin, on this machine, for development.
|
||||
//!
|
||||
//! It makes the room's keys in its directory (`.wai/dev/` by default) once, writes the
|
||||
//! relay's and the service's configuration there, starts both on loopback ports of their
|
||||
|
|
@ -12,12 +12,16 @@
|
|||
//! WebAssembly, the service and its clock's session on the relay, a viewer's grant, the
|
||||
//! relay's WebSocket upgrade), prints how long it took, and stops: exit 0 when every check
|
||||
//! held. Without it, `wai dev` runs until Ctrl-C (which stops the relay and the service
|
||||
//! with it) or `--for` seconds.
|
||||
//! with it) or `--for` seconds; with `--json` its first line on stdout is
|
||||
//! `{"ready": true, ...}`. `--dry-run` says what it would start and writes nothing. Its
|
||||
//! usage and help are the catalogue's (`dev.start`).
|
||||
//!
|
||||
//! The same origin serves WAI Studio at `/_wai/` when its files are found ([`studio`]): the
|
||||
//! page, and the dev stack's own routes for it under `/_wai/api/`. It opens no listener.
|
||||
|
||||
use super::{flag, opt, project};
|
||||
use super::envelope::OpError;
|
||||
use crate::out;
|
||||
use serde_json::{Value, json};
|
||||
use std::io::{BufRead, BufReader, Read, Write};
|
||||
use std::net::{Shutdown, SocketAddr, TcpListener, TcpStream};
|
||||
|
|
@ -29,34 +33,6 @@ use std::time::{Duration, Instant};
|
|||
#[path = "studio.rs"]
|
||||
mod studio;
|
||||
|
||||
pub const USAGE: &str = "wai dev [--port P] [--dir D] [--site DIR] [--watch FILE [--watch-ms MS]] [--call] [--remote]\n [--show [--audience N]] [--messaging] [--e2ee] [--publishers N] [--check [--json]] [--for S]\n a local relay, room service and room page on one origin (`wai dev --help`)";
|
||||
|
||||
const HELP: &str = "wai dev — a relay, a room service and the room page on one origin, on this machine
|
||||
|
||||
--port P the origin's port on 127.0.0.1 (8080; 0 picks a free one)
|
||||
--dir D keys, configuration and logs (.wai/dev)
|
||||
--site DIR your page: DIR is served at /, the room client beside it at /room/
|
||||
--publishers N places in the room (4)
|
||||
--watch FILE a watch room: everyone in it watches FILE together (.wav, .webm, .mp4,
|
||||
.ogg, .mp3); --watch-ms MS its duration, read from a WAV file's header
|
||||
--call the room is a call: /room/call.html places one, rings, answers
|
||||
--remote the room is one host's shared screen; prints the host's invitation
|
||||
--show the room is a show: hosts on camera, an audience that votes in polls
|
||||
the room's clock opens and signs (co-performance); --audience N each
|
||||
viewer's inputs to a poll (1)
|
||||
--messaging a conversation beside the room, at /msg-api/: /room/messages.html
|
||||
--e2ee every room end-to-end encrypted
|
||||
--check start, check every piece through the origin, report, stop
|
||||
--json with --check, the report as JSON
|
||||
--for S stop after S seconds
|
||||
--web DIR the room client's files (beside this binary: ../share/wai/web; $WAI_WEB)
|
||||
--relay BIN joule-moq-relay (beside this binary, then $PATH; $WAI_RELAY_BIN)
|
||||
--room BIN joule-moq-room (beside this binary, then $PATH; $WAI_ROOM_BIN)
|
||||
|
||||
WAI Studio is served at /_wai/ when its files are found ($WAI_STUDIO, or share/wai/studio
|
||||
beside this binary). A wai.json in the current directory (`wai create` writes one) gives
|
||||
the defaults; a flag wins over it. Ctrl-C stops everything.";
|
||||
|
||||
/// The relay's WebSocket path on the origin.
|
||||
const RELAY_PATH: &str = "/moq";
|
||||
/// The paths the services are served under.
|
||||
|
|
@ -97,18 +73,56 @@ struct Opts {
|
|||
for_s: Option<u64>,
|
||||
}
|
||||
|
||||
pub fn main(args: &[String]) -> Result<bool, String> {
|
||||
if flag(args, "--help") || flag(args, "-h") {
|
||||
println!("{HELP}");
|
||||
return Ok(true);
|
||||
}
|
||||
pub fn main(args: &[String]) -> Result<bool, OpError> {
|
||||
let started = Instant::now();
|
||||
let o = options(args)?;
|
||||
run(o, started)
|
||||
if out::dry_run() {
|
||||
plan(&o);
|
||||
return Ok(true);
|
||||
}
|
||||
// Past its options, what stops `wai dev` is the stack on this machine not coming up (a
|
||||
// port held, a service that exits or does not answer): starting it again may succeed.
|
||||
run(o, started).map_err(OpError::unavailable)
|
||||
}
|
||||
|
||||
/// What `wai dev` would start, for `--dry-run`: it starts nothing and writes nothing.
|
||||
fn plan(o: &Opts) {
|
||||
let mode = match &o.mode {
|
||||
Mode::Room => "room".to_string(),
|
||||
Mode::Watch { file, ms } => format!("watch {} ({ms} ms)", file.display()),
|
||||
Mode::Call => "call".into(),
|
||||
Mode::Remote => "remote".into(),
|
||||
Mode::Show { audience } => format!("show ({audience} input(s) a viewer)"),
|
||||
};
|
||||
let v = json!({
|
||||
"dry_run": true,
|
||||
"origin": format!("http://127.0.0.1:{}", o.port),
|
||||
"relay": o.relay.display().to_string(),
|
||||
"room": o.room.display().to_string(),
|
||||
"web": o.web.display().to_string(),
|
||||
"site": o.site.as_ref().map(|s| s.display().to_string()),
|
||||
"dir": o.dir.display().to_string(),
|
||||
"mode": mode,
|
||||
"publishers": o.publishers,
|
||||
"messaging": o.messaging,
|
||||
"e2ee": o.e2ee,
|
||||
"check": o.check,
|
||||
});
|
||||
if o.json {
|
||||
println!("{v}");
|
||||
return;
|
||||
}
|
||||
println!("wai dev --dry-run: nothing started, nothing written. It would:");
|
||||
println!(" start {} (the relay) and {} (the room service){}", o.relay.display(), o.room.display(), if o.messaging { ", and a conversation" } else { "" });
|
||||
println!(" serve http://127.0.0.1:{}: {}the room client from {}", o.port, o.site.as_ref().map(|s| format!("your page from {}, ", s.display())).unwrap_or_default(), o.web.display());
|
||||
println!(" keep keys, configuration and logs in {}", o.dir.display());
|
||||
println!(" run a {mode} room, {} places{}{}", o.publishers, if o.e2ee { ", end-to-end encrypted" } else { "" }, if o.check { "; check every piece, then stop" } else { "" });
|
||||
}
|
||||
|
||||
fn options(args: &[String]) -> Result<Opts, String> {
|
||||
let known_values = ["--port", "--dir", "--site", "--publishers", "--watch", "--watch-ms", "--for", "--web", "--relay", "--room", "--audience"];
|
||||
// `--json` reaches here as the operation's own flag: with --check, the report as JSON;
|
||||
// without, the ready line.
|
||||
let known_flags = ["--call", "--remote", "--show", "--messaging", "--e2ee", "--check", "--json"];
|
||||
let mut i = 0;
|
||||
while i < args.len() {
|
||||
|
|
@ -186,11 +200,12 @@ fn options(args: &[String]) -> Result<Opts, String> {
|
|||
let exe_dir = std::env::current_exe().ok().and_then(|e| std::fs::canonicalize(e).ok()).and_then(|e| e.parent().map(Path::to_path_buf));
|
||||
let web = find_web(opt(args, "--web"), exe_dir.as_deref())?;
|
||||
let studio = studio::find(exe_dir.as_deref());
|
||||
let relay = find_bin("joule-moq-relay", opt(args, "--relay"), "WAI_RELAY_BIN", exe_dir.as_deref())?;
|
||||
let room = find_bin("joule-moq-room", opt(args, "--room"), "WAI_ROOM_BIN", exe_dir.as_deref())?;
|
||||
let relay = find_bin("wai-relay", opt(args, "--relay"), "WAI_RELAY_BIN", exe_dir.as_deref())?;
|
||||
let room = find_bin("wai-room", opt(args, "--room"), "WAI_ROOM_BIN", exe_dir.as_deref())?;
|
||||
let dir = opt(args, "--dir").map(PathBuf::from).unwrap_or_else(|| cwd.join(".wai").join("dev"));
|
||||
std::fs::create_dir_all(&dir).map_err(|e| format!("{}: {e}", dir.display()))?;
|
||||
let dir = std::fs::canonicalize(&dir).map_err(|e| e.to_string())?;
|
||||
out::create_dir_all(&dir).map_err(|e| format!("{}: {e}", dir.display()))?;
|
||||
// A dry run makes no directory, so one that is not there yet is named as it would be.
|
||||
let dir = if out::dry_run() && !dir.exists() { cwd.join(&dir) } else { std::fs::canonicalize(&dir).map_err(|e| e.to_string())? };
|
||||
let for_s = num("--for")?;
|
||||
Ok(Opts { port, dir, site, page: d.page.clone(), web, studio, relay, room, publishers, mode, messaging, e2ee, check: flag(args, "--check"), json: flag(args, "--json"), for_s })
|
||||
}
|
||||
|
|
@ -320,9 +335,9 @@ fn free_port() -> Result<u16, String> {
|
|||
/// The relay lines of a room service's keys, made once in `keys`.
|
||||
fn keys(room: &Path, keys: &Path) -> Result<String, String> {
|
||||
let verb = if keys.exists() { "relay-lines" } else { "keygen" };
|
||||
let out = Command::new(room).args([verb, &keys.to_string_lossy()]).output().map_err(|e| format!("joule-moq-room {verb}: {e}"))?;
|
||||
let out = Command::new(room).args([verb, &keys.to_string_lossy()]).output().map_err(|e| format!("wai-room {verb}: {e}"))?;
|
||||
if !out.status.success() {
|
||||
return Err(format!("joule-moq-room {verb}: {}", String::from_utf8_lossy(&out.stderr).trim()));
|
||||
return Err(format!("wai-room {verb}: {}", String::from_utf8_lossy(&out.stderr).trim()));
|
||||
}
|
||||
Ok(String::from_utf8_lossy(&out.stdout).into_owned())
|
||||
}
|
||||
|
|
@ -424,11 +439,11 @@ fn run(o: Opts, started: Instant) -> Result<bool, String> {
|
|||
// The loopback address each process listens on, in the order they start.
|
||||
let mut listens: Vec<SocketAddr> = Vec::new();
|
||||
let result = (|| -> Result<Front, String> {
|
||||
procs.push(Proc::start("joule-moq-relay", &o.relay, &["--config", &relay_cfg.to_string_lossy()], o.dir.join("relay.log"), &[])?);
|
||||
procs.push(Proc::start("wai-relay", &o.relay, &["--config", &relay_cfg.to_string_lossy()], o.dir.join("relay.log"), &[])?);
|
||||
listens.push(([127, 0, 0, 1], ws).into());
|
||||
wait_for("the relay's WebSocket binding", 30_000, &mut procs, || TcpStream::connect(("127.0.0.1", ws)).is_ok())?;
|
||||
times.push(("relay", started.elapsed().as_secs_f64()));
|
||||
procs.push(Proc::start("joule-moq-room", &o.room, &["--config", &room_cfg.to_string_lossy()], o.dir.join("room.log"), &[])?);
|
||||
procs.push(Proc::start("wai-room", &o.room, &["--config", &room_cfg.to_string_lossy()], o.dir.join("room.log"), &[])?);
|
||||
let svc_addr: SocketAddr = ([127, 0, 0, 1], svc).into();
|
||||
listens.push(svc_addr);
|
||||
wait_for("the room service's clock on the relay", 30_000, &mut procs, || {
|
||||
|
|
@ -441,7 +456,7 @@ fn run(o: Opts, started: Instant) -> Result<bool, String> {
|
|||
let mport = free_port()?;
|
||||
let mcfg = o.dir.join("conversation.toml");
|
||||
std::fs::write(&mcfg, conversation_toml(&o, mport)).map_err(|e| e.to_string())?;
|
||||
procs.push(Proc::start("joule-moq-room (conversation)", &o.room, &["--config", &mcfg.to_string_lossy()], o.dir.join("conversation.log"), &[])?);
|
||||
procs.push(Proc::start("wai-room (conversation)", &o.room, &["--config", &mcfg.to_string_lossy()], o.dir.join("conversation.log"), &[])?);
|
||||
let maddr: SocketAddr = ([127, 0, 0, 1], mport).into();
|
||||
listens.push(maddr);
|
||||
wait_for("the conversation's service", 30_000, &mut procs, || http(maddr, "GET", "/msg", &[], b"").is_ok_and(|(s, _)| s == 200))?;
|
||||
|
|
@ -486,7 +501,12 @@ fn run(o: Opts, started: Instant) -> Result<bool, String> {
|
|||
}
|
||||
passed
|
||||
} else {
|
||||
announce(&o, &base);
|
||||
if o.json {
|
||||
// A streaming operation's first line (JSON Lines): the stack is up.
|
||||
println!("{}", json!({"ready": true, "origin": base, "room_api": format!("{base}{ROOM_API}"), "relay": format!("ws://{origin}{RELAY_PATH}"), "dir": o.dir.display().to_string()}));
|
||||
} else {
|
||||
announce(&o, &base);
|
||||
}
|
||||
let end = o.for_s.map(|s| Instant::now() + Duration::from_secs(s));
|
||||
loop {
|
||||
if let Some(e) = procs.iter_mut().find_map(Proc::exited) {
|
||||
|
|
@ -571,7 +591,7 @@ fn announce(o: &Opts, base: &str) {
|
|||
println!(" the host wai-remote-host run --room {base}{ROOM_API} --key host.key --invitation {inv} --relay ws://{}{RELAY_PATH}", base.trim_start_matches("http://"));
|
||||
println!(" (`wai-remote-host key host.key` makes the key; the invitation holds for an hour)");
|
||||
}
|
||||
_ => println!(" the host `joule-moq-room invite {}` makes the host's invitation", o.dir.join("room-keys.toml").display()),
|
||||
_ => println!(" the host `wai-room invite {}` makes the host's invitation", o.dir.join("room-keys.toml").display()),
|
||||
}
|
||||
}
|
||||
Mode::Watch { file, ms } => println!(" watch room {base}/room/?service={ROOM_API} {} ({} s), in sync for everyone in it", file.display(), ms / 1000),
|
||||
|
|
|
|||
|
|
@ -1,81 +1,278 @@
|
|||
//! The agent rules `wai breaking` and `wai conform` follow (WAI toolchain v1, rules A1–A3 and
|
||||
//! A6): text on a terminal, the JSON envelope `envelope/1` when standard output is not a
|
||||
//! terminal or with `--json` (`--text` keeps text), data on standard output and progress on
|
||||
//! standard error, and one exit status for each outcome:
|
||||
//! The agent rules every command of the catalogue keeps (WAI toolchain v1, rules A1 to A6),
|
||||
//! for a person and for an agent alike, in this one place:
|
||||
//!
|
||||
//! | exit | meaning |
|
||||
//! |---|---|
|
||||
//! | 0 | done; every check held |
|
||||
//! | 1 | ran, and a check failed |
|
||||
//! | 2 | usage error or invalid input |
|
||||
//! | 4 | confirmation required; nothing changed |
|
||||
//! | 5 | not found (file, ref, suite, program) |
|
||||
//! | 6 | target unavailable; safe to retry |
|
||||
//! | 7 | internal error |
|
||||
//! - **exit codes**, one meaning each (`wai::catalogue::EXIT_CODES`):
|
||||
//!
|
||||
//! The operation catalogue will hold these rules for every command; until it does, this is
|
||||
//! the one place these two operations take them from.
|
||||
//! | exit | meaning |
|
||||
//! |---|---|
|
||||
//! | 0 | done; every check held |
|
||||
//! | 1 | ran, and a check failed |
|
||||
//! | 2 | usage error or invalid input |
|
||||
//! | 3 | authentication or permission |
|
||||
//! | 4 | confirmation required; nothing changed |
|
||||
//! | 5 | not found (file, ref, suite, program, operation, or a build feature) |
|
||||
//! | 6 | target unavailable; safe to retry |
|
||||
//! | 7 | internal error |
|
||||
//!
|
||||
//! - **`--json`**: stdout carries one JSON envelope, `{"wai": "envelope/1", "op", "ok", "data"
|
||||
//! | "error": {"code", "message", "hint", "retryable"}, ...}`, and progress and logs stay on
|
||||
//! stderr. A check that failed (exit 1) carries `error` and the report in `data`; a failure
|
||||
//! carries its `exit`; a confirmation adds `proposed` and `confirm`; a dry run adds
|
||||
//! `"dry_run": true` and the `plan` of what it would write. Text otherwise (`--text` asks for
|
||||
//! it too). Off a terminal without `--json` the output is still text: every command's piped
|
||||
//! output stays what it was.
|
||||
//! - **`--dry-run`** on every operation whose effect is not `reads`: nothing is written, and
|
||||
//! the command says what it would write (`out.rs`).
|
||||
//! - **`--yes`**: an operation that would replace a file the command line did not name, or
|
||||
//! change a target (as its catalogue entry's `confirm` says), asks on a terminal, and without
|
||||
//! one (or with `--no-input`) exits 4 with the proposed change and the command that confirms
|
||||
//! it, which names `--yes`. `--yes` is never implied. `--confirm` is its deprecated spelling
|
||||
//! and does the same (`wai::catalogue::AGENT_FLAGS`).
|
||||
|
||||
use serde_json::{Map, Value, json};
|
||||
use std::io::{IsTerminal, Write};
|
||||
use std::io::{BufRead, IsTerminal, Write};
|
||||
use std::sync::OnceLock;
|
||||
|
||||
pub const EXIT_FAILED: u8 = 1;
|
||||
pub const EXIT_USAGE: u8 = 2;
|
||||
#[cfg_attr(not(feature = "quickstart"), allow(dead_code))]
|
||||
pub const EXIT_PERMISSION: u8 = 3;
|
||||
pub const EXIT_CONFIRM: u8 = 4;
|
||||
pub const EXIT_NOT_FOUND: u8 = 5;
|
||||
pub const EXIT_UNAVAILABLE: u8 = 6;
|
||||
pub const EXIT_INTERNAL: u8 = 7;
|
||||
|
||||
/// An operation that did not run to a verdict: its exit status, a stable code, what
|
||||
/// happened, what to do about it, and whether running it again may help.
|
||||
#[derive(Debug)]
|
||||
/// The environment variable a parent `wai --json` sets on the `wai` it runs for an envelope.
|
||||
pub const CHILD_ENV: &str = "WAI_ENVELOPE_CHILD";
|
||||
/// The prefix of the line a child writes last on stderr: its failure, as JSON.
|
||||
pub const TRAILER_ERROR: &str = "wai-envelope-error: ";
|
||||
/// The prefix of the line a child writes on stderr after a dry run: its plan, as JSON.
|
||||
pub const TRAILER_PLAN: &str = "wai-envelope-plan: ";
|
||||
|
||||
/// This run's agent flags, read wherever they are on the command line.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Ctx {
|
||||
/// `--json`: one envelope on stdout.
|
||||
pub json: bool,
|
||||
/// `--dry-run`.
|
||||
pub dry_run: bool,
|
||||
/// `--yes`, or `--confirm`, its deprecated spelling.
|
||||
pub yes: bool,
|
||||
/// `--no-input`: never ask.
|
||||
pub no_input: bool,
|
||||
/// Run by a parent `wai --json` for its envelope.
|
||||
pub child: bool,
|
||||
/// The command line after `wai`, without `--json`, for the command that confirms.
|
||||
pub argv: Vec<String>,
|
||||
}
|
||||
|
||||
static CTX: OnceLock<Ctx> = OnceLock::new();
|
||||
|
||||
/// Set this run's flags, once.
|
||||
pub fn set(c: Ctx) {
|
||||
let _ = CTX.set(c);
|
||||
}
|
||||
|
||||
/// This run's flags.
|
||||
pub fn ctx() -> &'static Ctx {
|
||||
CTX.get_or_init(Ctx::default)
|
||||
}
|
||||
|
||||
/// An operation that did not run to a verdict: its exit status, a stable code, what happened,
|
||||
/// what to do about it, whether running it again may help, and, waiting for confirmation, the
|
||||
/// proposed changes and the command that confirms them.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct OpError {
|
||||
pub exit: u8,
|
||||
/// A stable string: `usage`, `invalid-input`, `check-failed`, `permission-denied`,
|
||||
/// `confirmation-required`, `not-found`, `ref-not-found`, `operation-not-found`,
|
||||
/// `operation-not-in-build`, `target-unavailable`, `internal`, ...
|
||||
pub code: &'static str,
|
||||
pub message: String,
|
||||
pub hint: String,
|
||||
pub retryable: bool,
|
||||
/// With exit 4: the changes that wait for confirmation.
|
||||
pub proposed: Vec<Value>,
|
||||
/// With exit 4: the command that confirms them.
|
||||
pub confirm: Option<String>,
|
||||
/// The command reported it itself, in text or in its envelope: only its exit is left.
|
||||
pub reported: bool,
|
||||
}
|
||||
|
||||
impl OpError {
|
||||
pub fn new(exit: u8, code: &'static str, message: impl Into<String>, hint: impl Into<String>) -> OpError {
|
||||
OpError { exit, code, message: message.into(), hint: hint.into(), retryable: exit == EXIT_UNAVAILABLE }
|
||||
OpError { exit, code, message: message.into(), hint: hint.into(), retryable: exit == EXIT_UNAVAILABLE, proposed: Vec::new(), confirm: None, reported: false }
|
||||
}
|
||||
|
||||
/// A failure with no hint yet.
|
||||
pub fn of(exit: u8, code: &'static str, message: impl Into<String>) -> OpError {
|
||||
OpError::new(exit, code, message, "")
|
||||
}
|
||||
|
||||
pub fn with_hint(mut self, h: impl Into<String>) -> OpError {
|
||||
self.hint = h.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn usage(message: impl Into<String>, usage: &str) -> OpError {
|
||||
OpError::new(EXIT_USAGE, "usage", message, usage)
|
||||
}
|
||||
|
||||
pub fn unavailable(message: impl Into<String>) -> OpError {
|
||||
OpError::new(EXIT_UNAVAILABLE, "target-unavailable", message, "start it again; nothing was changed")
|
||||
}
|
||||
|
||||
pub fn internal(message: impl Into<String>) -> OpError {
|
||||
OpError::of(EXIT_INTERNAL, "internal", message)
|
||||
}
|
||||
|
||||
/// A failure the command has reported itself; it ends with `exit`.
|
||||
#[allow(dead_code)]
|
||||
pub fn reported(exit: u8) -> OpError {
|
||||
OpError { reported: true, ..OpError::of(exit, "reported", "") }
|
||||
}
|
||||
|
||||
/// The envelope's `error` member.
|
||||
pub fn to_json(&self) -> Value {
|
||||
json!({"code": self.code, "message": self.message, "hint": (!self.hint.is_empty()).then_some(&self.hint), "retryable": self.retryable})
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the output is the JSON envelope: `--json` asks for it, `--text` refuses it, and
|
||||
/// otherwise it is used when standard output is not a terminal.
|
||||
pub fn json_wanted(args: &[String]) -> bool {
|
||||
if args.iter().any(|a| a == "--json") {
|
||||
return true;
|
||||
/// An error a command reports as text, read for its exit code: a refused permission (3), a
|
||||
/// file or a binary that is not there, or an operation this build does not hold (5), a usage
|
||||
/// line (2), and any other refusal of the input (2).
|
||||
impl From<String> for OpError {
|
||||
fn from(message: String) -> OpError {
|
||||
let m = message.to_ascii_lowercase();
|
||||
let has = |s: &[&str]| s.iter().any(|x| m.contains(x));
|
||||
if has(&["(os error 13)", "permission denied", "access is denied"]) {
|
||||
OpError::of(EXIT_PERMISSION, "permission-denied", message)
|
||||
} else if has(&["(os error 2)", "(os error 3)", "no such file or directory", "cannot find the file", "cannot find the path", "nor on path", "is not a file", "are not found"]) {
|
||||
OpError::of(EXIT_NOT_FOUND, "not-found", message)
|
||||
} else if has(&["needs a build with", "built without"]) {
|
||||
OpError::of(EXIT_NOT_FOUND, "operation-not-in-build", message)
|
||||
} else if m.starts_with("usage") || m.starts_with("wai ") || has(&["not an option", "takes a value", "is required"]) {
|
||||
OpError::of(EXIT_USAGE, "usage", message)
|
||||
} else {
|
||||
OpError::of(EXIT_USAGE, "invalid-input", message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for OpError {
|
||||
fn from(m: &str) -> OpError {
|
||||
OpError::from(m.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the output is the JSON envelope: `--json` asks for it (the agent flag `wai` read,
|
||||
/// or the operation's own when a parent `wai --json` runs it), `--text` refuses it.
|
||||
pub fn json_wanted(args: &[String]) -> bool {
|
||||
if args.iter().any(|a| a == "--text") {
|
||||
return false;
|
||||
}
|
||||
!std::io::stdout().is_terminal()
|
||||
ctx().json || args.iter().any(|a| a == "--json")
|
||||
}
|
||||
|
||||
/// The envelope of an operation that reached a verdict: `ok` and the body's members.
|
||||
/// A word as a POSIX shell reads it back.
|
||||
fn shell_word(w: &str) -> String {
|
||||
if !w.is_empty() && w.bytes().all(|c| c.is_ascii_alphanumeric() || b"-_./=:,@%+".contains(&c)) {
|
||||
w.to_string()
|
||||
} else {
|
||||
format!("'{}'", w.replace('\'', "'\\''"))
|
||||
}
|
||||
}
|
||||
|
||||
/// The command that runs this one again, confirmed.
|
||||
pub fn confirm_command() -> String {
|
||||
let mut words = vec!["wai".to_string()];
|
||||
words.extend(ctx().argv.iter().filter(|a| *a != "--yes" && *a != "--confirm").map(|a| shell_word(a)));
|
||||
words.push("--yes".into());
|
||||
words.join(" ")
|
||||
}
|
||||
|
||||
/// Ask before `proposed` changes are made. Go ahead with `--yes`, in a dry run (which makes
|
||||
/// none), or when a person at a terminal answers yes; otherwise nothing changes and the
|
||||
/// command exits 4 with the proposal and the command that confirms it.
|
||||
pub fn confirm(what: &str, proposed: Vec<Value>) -> Result<(), OpError> {
|
||||
let c = ctx();
|
||||
if c.yes || c.dry_run {
|
||||
return Ok(());
|
||||
}
|
||||
let interactive = !c.json && !c.child && !c.no_input && std::io::stdin().is_terminal() && std::io::stdout().is_terminal();
|
||||
if interactive {
|
||||
eprint!("wai: {what}. Go ahead? [y/N] ");
|
||||
let _ = std::io::stderr().flush();
|
||||
let mut line = String::new();
|
||||
if std::io::stdin().lock().read_line(&mut line).is_ok() && matches!(line.trim(), "y" | "Y" | "yes") {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
Err(OpError {
|
||||
proposed,
|
||||
confirm: Some(confirm_command()),
|
||||
..OpError::new(EXIT_CONFIRM, "confirmation-required", format!("{what}; nothing was changed"), "read the proposed changes, then run the confirming command")
|
||||
})
|
||||
}
|
||||
|
||||
/// The envelope of an operation that reached a verdict, its body as `data`; in a dry run,
|
||||
/// with the plan of what it would have written.
|
||||
pub fn envelope(op: &str, ok: bool, body: Map<String, Value>) -> Value {
|
||||
let mut m = Map::new();
|
||||
m.insert("wai".into(), json!("envelope/1"));
|
||||
m.insert("op".into(), json!(op));
|
||||
m.insert("ok".into(), json!(ok));
|
||||
m.extend(body);
|
||||
Value::Object(m)
|
||||
envelope_with(op, ok, Value::Object(body), crate::out::dry_run().then(crate::out::plan))
|
||||
}
|
||||
|
||||
/// The envelope of an operation that reached a verdict: `data`, and a dry run's plan.
|
||||
pub fn envelope_with(op: &str, ok: bool, data: Value, plan: Option<Vec<Value>>) -> Value {
|
||||
let mut v = json!({"wai": wai::catalogue::ENVELOPE, "op": op, "ok": ok, "data": data});
|
||||
if let Some(p) = plan {
|
||||
v["dry_run"] = Value::Bool(true);
|
||||
v["plan"] = Value::Array(p);
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
/// The envelope of an operation that did not.
|
||||
pub fn error_envelope(op: &str, e: &OpError) -> Value {
|
||||
json!({
|
||||
"wai": "envelope/1", "op": op, "ok": false,
|
||||
"error": {"code": e.code, "message": e.message, "hint": e.hint, "retryable": e.retryable},
|
||||
"exit": e.exit,
|
||||
})
|
||||
error_envelope_with(Some(op), e, None)
|
||||
}
|
||||
|
||||
/// The envelope of a failure, for an operation or for a command line that named none; with
|
||||
/// exit 1 (a check failed) it carries the command's `data` as well, the report of what did not
|
||||
/// hold.
|
||||
pub fn error_envelope_with(op: Option<&str>, e: &OpError, data: Option<Value>) -> Value {
|
||||
let mut v = json!({"wai": wai::catalogue::ENVELOPE, "op": op, "ok": false, "error": e.to_json(), "exit": e.exit});
|
||||
if let Some(d) = data {
|
||||
v["data"] = d;
|
||||
}
|
||||
if e.exit == EXIT_CONFIRM {
|
||||
v["proposed"] = Value::Array(e.proposed.clone());
|
||||
v["confirm"] = json!(e.confirm);
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
/// Print an envelope on stdout, one JSON document.
|
||||
pub fn print(v: &Value) {
|
||||
println!("{}", serde_json::to_string_pretty(v).expect("an envelope is JSON"));
|
||||
}
|
||||
|
||||
/// Report a failure: as text on stderr, with what waits for confirmation; in a child, also as
|
||||
/// the trailer line its parent reads.
|
||||
pub fn report(op: Option<&str>, e: &OpError) {
|
||||
eprintln!("error: {}", e.message);
|
||||
if e.exit == EXIT_CONFIRM {
|
||||
for p in &e.proposed {
|
||||
eprintln!(" would {}", p);
|
||||
}
|
||||
if let Some(c) = &e.confirm {
|
||||
eprintln!("to go ahead: {c}");
|
||||
}
|
||||
} else if !e.hint.is_empty() {
|
||||
eprintln!("hint: {}", e.hint);
|
||||
}
|
||||
if ctx().child {
|
||||
let t = json!({"error": e.to_json(), "exit": e.exit, "proposed": e.proposed, "confirm": e.confirm, "op": op});
|
||||
eprintln!("{TRAILER_ERROR}{t}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Print an error the way the output mode asks, then exit with its status.
|
||||
|
|
@ -83,10 +280,7 @@ pub fn fail(op: &str, json_mode: bool, e: &OpError) -> ! {
|
|||
if json_mode {
|
||||
println!("{}", error_envelope(op, e));
|
||||
} else {
|
||||
eprintln!("error: {}", e.message);
|
||||
if !e.hint.is_empty() {
|
||||
eprintln!("hint: {}", e.hint);
|
||||
}
|
||||
report(Some(op), e);
|
||||
}
|
||||
exit(e.exit)
|
||||
}
|
||||
|
|
@ -133,3 +327,47 @@ pub fn check_args<'a>(args: &'a [String], known: &[&str], with_value: &[&str], m
|
|||
}
|
||||
Ok(pos)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn a_text_error_is_read_for_its_exit_code() {
|
||||
let e = |m: &str| OpError::from(m.to_string()).exit;
|
||||
assert_eq!(e("x.wai: No such file or directory (os error 2)"), EXIT_NOT_FOUND);
|
||||
assert_eq!(e("read x: Permission denied (os error 13)"), EXIT_PERMISSION);
|
||||
assert_eq!(e("`wai hello` needs a build with the feature `quickstart`"), EXIT_NOT_FOUND);
|
||||
assert_eq!(e("built without generate: `wai generate` needs ..."), EXIT_NOT_FOUND);
|
||||
assert_eq!(e("wai create: --x: not an option"), EXIT_USAGE);
|
||||
assert_eq!(e("'wai.x' is not a registered capability"), EXIT_USAGE);
|
||||
assert_eq!(e("wai-relay is not beside `wai` nor on PATH"), EXIT_NOT_FOUND);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_codes_are_the_catalogues_table() {
|
||||
let named = [(EXIT_FAILED, 1), (EXIT_USAGE, 2), (EXIT_PERMISSION, 3), (EXIT_CONFIRM, 4), (EXIT_NOT_FOUND, 5), (EXIT_UNAVAILABLE, 6), (EXIT_INTERNAL, 7)];
|
||||
for (c, n) in named {
|
||||
assert_eq!(c, n);
|
||||
assert!(wai::catalogue::EXIT_CODES.iter().any(|(x, _)| *x == c), "{c} is in the catalogue's table");
|
||||
}
|
||||
assert_eq!(wai::catalogue::EXIT_CODES.len(), 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_confirming_command_quotes_what_a_shell_would_split() {
|
||||
assert_eq!(shell_word("out.wai"), "out.wai");
|
||||
assert_eq!(shell_word("a b"), "'a b'");
|
||||
assert_eq!(shell_word("it's"), "'it'\\''s'");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_failure_envelope_names_its_code_and_a_confirmation_its_command() {
|
||||
let e = OpError { proposed: vec![json!({"replace": "out.wai"})], confirm: Some("wai wrap x y --yes".into()), ..OpError::of(EXIT_CONFIRM, "confirmation-required", "m") };
|
||||
let v = error_envelope("envelope.wrap", &e);
|
||||
assert_eq!((v["ok"].as_bool(), v["error"]["code"].as_str(), v["confirm"].as_str(), v["exit"].as_u64()), (Some(false), Some("confirmation-required"), Some("wai wrap x y --yes"), Some(4)));
|
||||
assert_eq!(v["wai"], wai::catalogue::ENVELOPE);
|
||||
let ok = envelope_with("envelope.inspect", true, json!({"text": "t"}), None);
|
||||
assert!(ok["ok"] == true && ok.get("error").is_none() && ok.get("dry_run").is_none() && ok["data"]["text"] == "t");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,133 +1,51 @@
|
|||
//! The fleet's operations (extensions/fleet.md §9), on the toolchain's rules: `wai enrolment
|
||||
//! create` (`enrolment.create`), `wai enrolment list` (`enrolment.list`), `wai report create`
|
||||
//! (`report.create`) and `wai report export` (`report.export`). A bundle is checked by `wai verify`.
|
||||
//! The fleet's operations (extensions/fleet.md §9): `wai enrolment create` (`enrolment.create`),
|
||||
//! `wai enrolment list` (`enrolment.list`), `wai report create` (`report.create`) and `wai report
|
||||
//! export` (`report.export`), against a fleet service. A bundle is checked by `wai verify`.
|
||||
//!
|
||||
//! - Output: text on a terminal; the JSON envelope (`{"wai": "envelope/1", "op", "ok", ...}`) with
|
||||
//! `--json` or when standard output is not a terminal. Data to standard output.
|
||||
//! - Errors: `{code, message, hint, retryable}`; a refusal by the fleet's text has that text's
|
||||
//! reason as its code (`enrolment_duplicate`, `signer_not_enrolled`, ...).
|
||||
//! - Exit statuses: 0 done, 1 refused, 2 usage, 3 permission, 4 confirmation required, 5 not
|
||||
//! found, 6 the service unavailable (retry), 7 internal.
|
||||
//! - Every operation that writes takes `--dry-run`, which writes and sends nothing and says what
|
||||
//! it would. One that changes a service (`--target`), or replaces a file, waits for
|
||||
//! `--confirm` (asked for on a terminal); without it, it exits 4 with the confirming command.
|
||||
//! They keep the agent rules of [`super::envelope`], as every operation of the catalogue does:
|
||||
//! the exit codes, the JSON envelope with `--json` (data to standard output, logs to standard
|
||||
//! error), `--dry-run` (every file through `out.rs`, every request the service would be sent named
|
||||
//! in the plan, nothing written and nothing sent), and `--yes` before a fleet service is changed
|
||||
//! or a file replaced (asked at a terminal; without one, exit 4 with the proposed changes and the
|
||||
//! confirming command). A refusal by the fleet's text has that text's reason as its code
|
||||
//! (`enrolment_duplicate`, `signer_not_enrolled`, ...); the service unavailable is exit 6.
|
||||
//!
|
||||
//! A target is a fleet service's `http://` address (`wai-fleet serve`, which listens on a
|
||||
//! loopback address unless told otherwise).
|
||||
|
||||
use serde_json::{json, Value};
|
||||
use std::io::{IsTerminal, Read, Write};
|
||||
use super::envelope::{self as agent, EXIT_FAILED, EXIT_INTERNAL, EXIT_NOT_FOUND, EXIT_PERMISSION, EXIT_UNAVAILABLE, OpError};
|
||||
use crate::out;
|
||||
use serde_json::{Value, json};
|
||||
use std::io::{Read, Write};
|
||||
|
||||
pub const ENROLMENT_USAGE: &str = "wai enrolment create <node-key> --role <role> --key <file> [--label L] [--from µs] [--until µs] [--out F] [--target URL] [--dry-run] [--confirm] [--json]\n a node's enrolment, signed with the organisation key; with --target, given to a fleet service\n (enrolment.create). wai enrolment list --target URL [--json]: the fleet's nodes (enrolment.list)";
|
||||
pub const REPORT_USAGE: &str = "wai report create --target URL --start µs --end µs [--dry-run] [--confirm] [--json]\n the fleet report of a period, signed by a fleet service (report.create).\n wai report export <digest> --target URL [--out F] [--dry-run] [--confirm] [--json]: its bundle,\n verified here and written to a file (report.export)";
|
||||
type R = Result<bool, OpError>;
|
||||
|
||||
const DONE: u8 = 0;
|
||||
const REFUSED: u8 = 1;
|
||||
const USAGE: u8 = 2;
|
||||
/// Print what an operation did: its envelope with `--json`, its text otherwise; in a dry run of
|
||||
/// an operation that changes something, with the plan of what it would have done.
|
||||
fn done(op: &str, writes: bool, data: Value, text: &str) -> R {
|
||||
let plan = (writes && out::dry_run()).then(out::plan);
|
||||
if agent::ctx().json {
|
||||
agent::print(&agent::envelope_with(op, true, data, plan));
|
||||
} else if plan.is_some() {
|
||||
println!("{text}");
|
||||
println!("dry run: nothing was written or sent. Without --dry-run, wai would:");
|
||||
for l in out::plan_lines() {
|
||||
println!(" {l}");
|
||||
}
|
||||
} else {
|
||||
println!("{text}");
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// A file that could not be read or written.
|
||||
#[cfg_attr(not(feature = "fleet"), allow(dead_code))]
|
||||
const PERMISSION: u8 = 3;
|
||||
const CONFIRM: u8 = 4;
|
||||
const NOT_FOUND: u8 = 5;
|
||||
const UNAVAILABLE: u8 = 6;
|
||||
const INTERNAL: u8 = 7;
|
||||
|
||||
/// How one operation reports.
|
||||
struct Out {
|
||||
op: &'static str,
|
||||
json: bool,
|
||||
}
|
||||
|
||||
/// Why an operation stops.
|
||||
struct Fail {
|
||||
exit: u8,
|
||||
code: String,
|
||||
message: String,
|
||||
hint: String,
|
||||
retryable: bool,
|
||||
}
|
||||
|
||||
fn fail(exit: u8, code: &str, message: impl Into<String>, hint: impl Into<String>) -> Fail {
|
||||
Fail { exit, code: code.into(), message: message.into(), hint: hint.into(), retryable: exit == UNAVAILABLE }
|
||||
}
|
||||
|
||||
fn usage(message: impl Into<String>, hint: &str) -> Fail {
|
||||
fail(USAGE, "usage", message, hint)
|
||||
}
|
||||
|
||||
impl Out {
|
||||
fn new(op: &'static str, args: &[String]) -> Out {
|
||||
Out { op, json: super::flag(args, "--json") || !std::io::stdout().is_terminal() }
|
||||
fn io_fail(path: &str, e: &std::io::Error, hint: &str) -> OpError {
|
||||
match e.kind() {
|
||||
std::io::ErrorKind::NotFound => OpError::new(EXIT_NOT_FOUND, "not-found", format!("{path}: {e}"), hint),
|
||||
std::io::ErrorKind::PermissionDenied => OpError::new(EXIT_PERMISSION, "permission-denied", format!("{path}: {e}"), "the file's owner runs this"),
|
||||
_ => OpError::new(EXIT_INTERNAL, "io-error", format!("{path}: {e}"), hint),
|
||||
}
|
||||
|
||||
fn envelope(&self, ok: bool) -> serde_json::Map<String, Value> {
|
||||
let mut m = serde_json::Map::new();
|
||||
m.insert("wai".into(), json!("envelope/1"));
|
||||
m.insert("op".into(), json!(self.op));
|
||||
m.insert("ok".into(), json!(ok));
|
||||
m
|
||||
}
|
||||
|
||||
fn done(&self, data: Value, text: &str) -> u8 {
|
||||
if self.json {
|
||||
let mut m = self.envelope(true);
|
||||
m.insert("data".into(), data);
|
||||
println!("{}", Value::Object(m));
|
||||
} else {
|
||||
println!("{text}");
|
||||
}
|
||||
DONE
|
||||
}
|
||||
|
||||
fn fail(&self, f: Fail) -> u8 {
|
||||
if self.json {
|
||||
let mut m = self.envelope(false);
|
||||
m.insert("error".into(), json!({"code": f.code, "message": f.message, "hint": f.hint, "retryable": f.retryable}));
|
||||
println!("{}", Value::Object(m));
|
||||
} else {
|
||||
eprintln!("wai: {} ({})\n {}", f.message, f.code, f.hint);
|
||||
}
|
||||
f.exit
|
||||
}
|
||||
|
||||
/// The proposed changes, and the command that confirms them: status 4, nothing changed.
|
||||
fn confirmation(&self, proposed: Value, confirm: String) -> u8 {
|
||||
if self.json {
|
||||
let mut m = self.envelope(false);
|
||||
m.insert("error".into(), json!({"code": "confirmation-required", "message": "a change waits for confirmation", "hint": "re-run with --confirm", "retryable": false}));
|
||||
m.insert("proposed".into(), proposed);
|
||||
m.insert("confirm".into(), json!(confirm));
|
||||
println!("{}", Value::Object(m));
|
||||
} else {
|
||||
eprintln!("wai: a change waits for confirmation:\n {proposed}\n to make it: {confirm}");
|
||||
}
|
||||
CONFIRM
|
||||
}
|
||||
|
||||
/// A dry run: what would be done, and nothing done.
|
||||
fn dry_run(&self, proposed: Value) -> u8 {
|
||||
let text = format!("dry run, nothing written or sent: {proposed}");
|
||||
self.done(json!({"dry_run": true, "proposed": proposed}), &text)
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a change is confirmed: by `--confirm`, or by a person answering `y` on a terminal.
|
||||
fn confirmed(args: &[String], out: &Out, what: &str) -> bool {
|
||||
if super::flag(args, "--confirm") {
|
||||
return true;
|
||||
}
|
||||
if out.json || !std::io::stdin().is_terminal() || !std::io::stdout().is_terminal() {
|
||||
return false;
|
||||
}
|
||||
eprint!("{what} [y/N] ");
|
||||
let _ = std::io::stderr().flush();
|
||||
let mut line = String::new();
|
||||
std::io::stdin().read_line(&mut line).is_ok() && line.trim().eq_ignore_ascii_case("y")
|
||||
}
|
||||
|
||||
/// The command line again, with `--confirm`.
|
||||
fn confirm_command(words: &str, args: &[String]) -> String {
|
||||
let rest: Vec<String> = args.iter().filter(|a| *a != "--json").map(|a| if a.chars().all(|c| c.is_ascii_alphanumeric() || "-_./:".contains(c)) { a.clone() } else { format!("'{}'", a.replace('\'', "'\\''")) }).collect();
|
||||
format!("wai {words} {} --confirm", rest.join(" "))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------
|
||||
|
|
@ -136,14 +54,14 @@ fn confirm_command(words: &str, args: &[String]) -> String {
|
|||
|
||||
/// One request to a fleet service at `target` (`http://host:port`), and its answer: the
|
||||
/// status and the body read as JSON.
|
||||
fn call(target: &str, method: &str, path: &str, body: Option<&Value>) -> Result<(u16, Value), Fail> {
|
||||
fn call(target: &str, method: &str, path: &str, body: Option<&Value>) -> Result<(u16, Value), OpError> {
|
||||
let Some(rest) = target.strip_prefix("http://") else {
|
||||
return Err(usage(format!("{target}: a fleet service's http:// address"), "give --target http://HOST:PORT, the address wai-fleet serve listens on"));
|
||||
return Err(OpError::usage(format!("{target}: a fleet service's http:// address"), "give --target http://HOST:PORT, the address wai-fleet serve listens on"));
|
||||
};
|
||||
let authority = rest.split('/').next().unwrap_or("");
|
||||
let host = authority.rsplit_once(':').map_or(authority, |(h, _)| h);
|
||||
let addr = if authority.contains(':') { authority.to_string() } else { format!("{authority}:80") };
|
||||
let unavailable = |e: String| fail(UNAVAILABLE, "target-unavailable", format!("{target}: {e}"), "start the fleet service, or check its address");
|
||||
let unavailable = |e: String| OpError::new(EXIT_UNAVAILABLE, "target-unavailable", format!("{target}: {e}"), "start the fleet service, or check its address");
|
||||
use std::net::ToSocketAddrs;
|
||||
let sock = addr.to_socket_addrs().map_err(|e| unavailable(e.to_string()))?.next().ok_or_else(|| unavailable("no address".into()))?;
|
||||
let mut s = std::net::TcpStream::connect_timeout(&sock, std::time::Duration::from_secs(10)).map_err(|e| unavailable(e.to_string()))?;
|
||||
|
|
@ -165,16 +83,30 @@ fn call(target: &str, method: &str, path: &str, body: Option<&Value>) -> Result<
|
|||
Ok((status, v))
|
||||
}
|
||||
|
||||
/// A reason of extensions/fleet.md §10 as the stable code it is; `refused` for a reason this
|
||||
/// build does not hold (the message carries the service's own).
|
||||
fn reason_code(code: &str) -> &'static str {
|
||||
#[cfg(feature = "fleet")]
|
||||
{
|
||||
use wai::fleet::Reason;
|
||||
if let Some(r) = Reason::ALL.iter().chain(Reason::REUSED.iter()).find(|r| r.code() == code) {
|
||||
return r.code();
|
||||
}
|
||||
}
|
||||
let _ = code;
|
||||
"refused"
|
||||
}
|
||||
|
||||
/// An answer that is not a success, as the toolchain's failure.
|
||||
fn answer_fail(status: u16, v: &Value) -> Fail {
|
||||
fn answer_fail(status: u16, v: &Value) -> OpError {
|
||||
match status {
|
||||
422 => {
|
||||
let r = v["reason"].as_str().unwrap_or("refused");
|
||||
fail(REFUSED, r, format!("the fleet service refused it: {r}"), "see extensions/fleet.md §10 for the reason")
|
||||
OpError::new(EXIT_FAILED, reason_code(r), format!("the fleet service refused it: {r}"), "see extensions/fleet.md §10 for the reason")
|
||||
}
|
||||
404 => fail(NOT_FOUND, "not-found", v["error"].as_str().unwrap_or("not found").to_string(), "list what the service holds"),
|
||||
400 => usage(v["error"].as_str().unwrap_or("a bad request").to_string(), "see the operation's usage"),
|
||||
_ => fail(INTERNAL, "service-error", format!("HTTP {status}: {v}"), "see the fleet service's log"),
|
||||
404 => OpError::new(EXIT_NOT_FOUND, "not-found", v["error"].as_str().unwrap_or("not found").to_string(), "list what the service holds"),
|
||||
400 => OpError::usage(v["error"].as_str().unwrap_or("a bad request").to_string(), "see the operation's usage"),
|
||||
_ => OpError::new(EXIT_INTERNAL, "service-error", format!("HTTP {status}: {v}"), "see the fleet service's log"),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -191,87 +123,90 @@ fn unhex(s: &str) -> Option<Vec<u8>> {
|
|||
(0..s.len()).step_by(2).map(|i| u8::from_str_radix(s.get(i..i + 2)?, 16).ok()).collect()
|
||||
}
|
||||
|
||||
fn number(args: &[String], flag: &str, default: u64) -> Result<u64, Fail> {
|
||||
fn number(args: &[String], flag: &str, default: u64) -> Result<u64, OpError> {
|
||||
match super::opt(args, flag) {
|
||||
None => Ok(default),
|
||||
Some(v) => v.parse().map_err(|_| usage(format!("{flag} {v}: microseconds since 1970, an integer"), "give a number of microseconds")),
|
||||
Some(v) => v.parse().map_err(|_| OpError::usage(format!("{flag} {v}: microseconds since 1970, an integer"), "give a number of microseconds")),
|
||||
}
|
||||
}
|
||||
|
||||
/// The value of `flag`, which the operation requires.
|
||||
fn required<'a>(args: &'a [String], flag: &str, what: &str, usage: &str) -> Result<&'a str, OpError> {
|
||||
super::opt(args, flag).ok_or_else(|| OpError::usage(format!("{flag}: {what}"), usage))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------
|
||||
// enrolment.create, enrolment.list
|
||||
// ---------------------------------------------------------------------------------------
|
||||
|
||||
/// `wai enrolment create|list`.
|
||||
pub fn enrolment(args: &[String]) -> u8 {
|
||||
match args.first().map(String::as_str) {
|
||||
Some("create") => {
|
||||
let out = Out::new("enrolment.create", args);
|
||||
match enrolment_create(&args[1..], &out) {
|
||||
Ok(c) => c,
|
||||
Err(f) => out.fail(f),
|
||||
}
|
||||
}
|
||||
Some("list") => {
|
||||
let out = Out::new("enrolment.list", args);
|
||||
match enrolment_list(&args[1..], &out) {
|
||||
Ok(c) => c,
|
||||
Err(f) => out.fail(f),
|
||||
}
|
||||
}
|
||||
_ => Out::new("enrolment.create", args).fail(usage(ENROLMENT_USAGE, "wai enrolment create … or wai enrolment list …")),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "fleet"))]
|
||||
fn enrolment_create(_: &[String], _: &Out) -> Result<u8, Fail> {
|
||||
Err(fail(USAGE, "feature-missing", "`wai enrolment create` needs a build with the feature `fleet`", "cargo install --path wai/wai-rs --no-default-features --features quickstart"))
|
||||
pub fn enrolment_create(_: &[String]) -> R {
|
||||
Err(crate::door::not_built(wai::catalogue::op("enrolment.create").expect("listed")))
|
||||
}
|
||||
|
||||
/// `wai enrolment create <node-key> --role <role> --key <file> ...`: the node's enrolment, signed
|
||||
/// with the organisation key, written to a file and, with `--target`, given to a fleet service.
|
||||
#[cfg(feature = "fleet")]
|
||||
fn enrolment_create(args: &[String], out: &Out) -> Result<u8, Fail> {
|
||||
use wai::fleet::{role, Enrolment, NO_END};
|
||||
pub fn enrolment_create(args: &[String]) -> R {
|
||||
use wai::fleet::{Enrolment, NO_END, role};
|
||||
let usage = super::usage("enrolment.create");
|
||||
let with_value = ["--role", "--key", "--label", "--from", "--until", "--out", "--target"];
|
||||
let pos = super::positional(args, &with_value);
|
||||
let node: [u8; 32] = pos.first().and_then(|s| unhex(s)).and_then(|b| b.try_into().ok()).ok_or_else(|| usage("the node's key: 64 hexadecimal digits", ENROLMENT_USAGE))?;
|
||||
let role_arg = super::opt(args, "--role").ok_or_else(|| usage("--role: relay, compositor, ingest, room, ledger or audit", ENROLMENT_USAGE))?;
|
||||
let r = role::code(role_arg).or_else(|| role_arg.parse::<u8>().ok().filter(|r| role::name(*r).is_some())).ok_or_else(|| usage(format!("--role {role_arg}: relay, compositor, ingest, room, ledger or audit"), ENROLMENT_USAGE))?;
|
||||
let key_path = super::opt(args, "--key").ok_or_else(|| usage("--key: the organisation key's file (32 bytes, or 64 hexadecimal digits)", ENROLMENT_USAGE))?;
|
||||
let raw = std::fs::read(key_path).map_err(|e| match e.kind() {
|
||||
std::io::ErrorKind::NotFound => fail(NOT_FOUND, "not-found", format!("{key_path}: {e}"), "give the organisation key's file"),
|
||||
std::io::ErrorKind::PermissionDenied => fail(PERMISSION, "permission-denied", format!("{key_path}: {e}"), "the key's owner runs this"),
|
||||
_ => fail(INTERNAL, "io-error", format!("{key_path}: {e}"), "check the file"),
|
||||
})?;
|
||||
let seed: [u8; 32] = if raw.len() == 32 { raw.try_into().expect("32") } else { std::str::from_utf8(&raw).ok().and_then(|s| unhex(s.trim())).and_then(|b| b.try_into().ok()).ok_or_else(|| usage(format!("{key_path}: 32 bytes, or 64 hexadecimal digits"), "the organisation key's seed"))? };
|
||||
let pos = agent::check_args(args, &with_value, &with_value, 1, &usage)?;
|
||||
let node: [u8; 32] = pos.first().and_then(|s| unhex(s)).and_then(|b| b.try_into().ok()).ok_or_else(|| OpError::usage("the node's key: 64 hexadecimal digits", &usage))?;
|
||||
let role_arg = required(args, "--role", "relay, compositor, ingest, room, ledger or audit", &usage)?;
|
||||
let r = role::code(role_arg)
|
||||
.or_else(|| role_arg.parse::<u8>().ok().filter(|r| role::name(*r).is_some()))
|
||||
.ok_or_else(|| OpError::usage(format!("--role {role_arg}: relay, compositor, ingest, room, ledger or audit"), &usage))?;
|
||||
let key_path = required(args, "--key", "the organisation key's file (32 bytes, or 64 hexadecimal digits)", &usage)?;
|
||||
let raw = std::fs::read(key_path).map_err(|e| io_fail(key_path, &e, "give the organisation key's file"))?;
|
||||
let seed: [u8; 32] = if raw.len() == 32 {
|
||||
raw.try_into().expect("32")
|
||||
} else {
|
||||
std::str::from_utf8(&raw).ok().and_then(|s| unhex(s.trim())).and_then(|b| b.try_into().ok()).ok_or_else(|| OpError::usage(format!("{key_path}: 32 bytes, or 64 hexadecimal digits"), "the organisation key's seed"))?
|
||||
};
|
||||
let key = ed25519_dalek::SigningKey::from_bytes(&seed);
|
||||
let label = super::opt(args, "--label").unwrap_or("");
|
||||
let (from, until) = (number(args, "--from", 0)?, number(args, "--until", NO_END)?);
|
||||
let e = Enrolment::new(key.verifying_key().to_bytes(), node, r, from, until, label).sign(&key);
|
||||
let bytes = e.to_bytes();
|
||||
// Its own rules, before anything is written or sent.
|
||||
Enrolment::parse(&bytes).map_err(|x| fail(USAGE, x.code(), format!("the enrolment is refused: {x}"), "extensions/fleet.md §3.3: from below until, a label of at most 64 bytes, a node that is not the organisation"))?;
|
||||
Enrolment::parse(&bytes).map_err(|x| {
|
||||
OpError::new(agent::EXIT_USAGE, x.code(), format!("the enrolment is refused: {x}"), "extensions/fleet.md §3.3: from below until, a label of at most 64 bytes, a node that is not the organisation")
|
||||
})?;
|
||||
let target = super::opt(args, "--target");
|
||||
let file = super::opt(args, "--out").map(str::to_string).or_else(|| target.is_none().then(|| format!("{}.wfe", &hex(&node)[..16])));
|
||||
let data = json!({
|
||||
"node": hex(&node), "role": role::name(r), "label": label, "from": from, "until": until,
|
||||
"organisation": hex(&key.verifying_key().to_bytes()), "digest": hex(&e.digest()), "enrolment": hex(&bytes), "file": file, "target": target,
|
||||
});
|
||||
let mut proposed = Vec::new();
|
||||
if let Some(f) = &file {
|
||||
proposed.push(json!({"action": "write", "file": f, "bytes": bytes.len(), "replaces": std::path::Path::new(f).exists()}));
|
||||
let who = format!("{} as {} ({label:?}), digest {}", &hex(&node)[..16], role::name(r).unwrap_or("?"), &hex(&e.digest())[..16]);
|
||||
if out::dry_run() {
|
||||
if let Some(f) = &file {
|
||||
out::write(f, &bytes).map_err(|e| io_fail(f, &e, "check the directory"))?;
|
||||
}
|
||||
if let Some(t) = target {
|
||||
out::would_send(t, "POST", "/v1/fleet/enrolments");
|
||||
}
|
||||
return done("enrolment.create", true, data, &format!("would enrol {who}"));
|
||||
}
|
||||
if let Some(t) = target {
|
||||
proposed.push(json!({"action": "enrol", "target": t, "node": hex(&node), "role": role::name(r)}));
|
||||
}
|
||||
if super::flag(args, "--dry-run") {
|
||||
return Ok(out.dry_run(Value::Array(proposed)));
|
||||
}
|
||||
let replaces = file.as_ref().is_some_and(|f| std::path::Path::new(f).exists());
|
||||
if (target.is_some() || replaces) && !confirmed(args, out, &format!("{}?", proposed.iter().map(Value::to_string).collect::<Vec<_>>().join("; "))) {
|
||||
return Ok(out.confirmation(Value::Array(proposed), confirm_command("enrolment create", args)));
|
||||
let replaces = file.as_ref().filter(|f| std::path::Path::new(f).exists());
|
||||
if target.is_some() || replaces.is_some() {
|
||||
let mut what = Vec::new();
|
||||
let mut proposed = Vec::new();
|
||||
if let Some(f) = replaces {
|
||||
what.push(format!("{f} exists, and the enrolment would replace it"));
|
||||
proposed.push(json!({"write": f, "bytes": bytes.len(), "replaces": true}));
|
||||
} else if let Some(f) = &file {
|
||||
proposed.push(json!({"write": f, "bytes": bytes.len(), "replaces": false}));
|
||||
}
|
||||
if let Some(t) = target {
|
||||
what.push(format!("the fleet service at {t} would hold the enrolment of {}…", &hex(&node)[..16]));
|
||||
proposed.push(json!({"request": "POST /v1/fleet/enrolments", "target": t, "node": hex(&node), "role": role::name(r)}));
|
||||
}
|
||||
agent::confirm(&what.join("; "), proposed)?;
|
||||
}
|
||||
if let Some(f) = &file {
|
||||
std::fs::write(f, &bytes).map_err(|e| fail(if e.kind() == std::io::ErrorKind::PermissionDenied { PERMISSION } else { INTERNAL }, "io-error", format!("{f}: {e}"), "check the directory"))?;
|
||||
out::write(f, &bytes).map_err(|e| io_fail(f, &e, "check the directory"))?;
|
||||
}
|
||||
if let Some(t) = target {
|
||||
let (status, v) = call(t, "POST", "/v1/fleet/enrolments", Some(&json!({"enrolment": hex(&bytes)})))?;
|
||||
|
|
@ -280,19 +215,18 @@ fn enrolment_create(args: &[String], out: &Out) -> Result<u8, Fail> {
|
|||
}
|
||||
}
|
||||
let text = format!(
|
||||
"enrolled {} as {} ({:?}), digest {}{}{}",
|
||||
&hex(&node)[..16],
|
||||
role::name(r).unwrap_or("?"),
|
||||
label,
|
||||
&hex(&e.digest())[..16],
|
||||
"enrolled {who}{}{}",
|
||||
file.as_ref().map(|f| format!("; written to {f}")).unwrap_or_default(),
|
||||
target.map(|t| format!("; held by {t}")).unwrap_or_default()
|
||||
);
|
||||
Ok(out.done(data, &text))
|
||||
done("enrolment.create", true, data, &text)
|
||||
}
|
||||
|
||||
fn enrolment_list(args: &[String], out: &Out) -> Result<u8, Fail> {
|
||||
let target = super::opt(args, "--target").ok_or_else(|| usage("--target: the fleet service's http:// address", ENROLMENT_USAGE))?;
|
||||
/// `wai enrolment list --target URL`: the nodes a fleet service holds.
|
||||
pub fn enrolment_list(args: &[String]) -> R {
|
||||
let usage = super::usage("enrolment.list");
|
||||
agent::check_args(args, &["--target"], &["--target"], 0, &usage)?;
|
||||
let target = required(args, "--target", "the fleet service's http:// address", &usage)?;
|
||||
let (status, v) = call(target, "GET", "/v1/fleet/enrolments", None)?;
|
||||
if status != 200 {
|
||||
return Err(answer_fail(status, &v));
|
||||
|
|
@ -300,46 +234,29 @@ fn enrolment_list(args: &[String], out: &Out) -> Result<u8, Fail> {
|
|||
let list = v["enrolments"].as_array().cloned().unwrap_or_default();
|
||||
let mut text = format!("{} node(s) enrolled at {target}", list.len());
|
||||
for e in &list {
|
||||
text.push_str(&format!(
|
||||
"\n {}… {:<10} {:<16} records {}",
|
||||
&e["node"].as_str().unwrap_or("")[..16.min(e["node"].as_str().unwrap_or("").len())],
|
||||
e["role"].as_str().unwrap_or("?"),
|
||||
e["label"].as_str().unwrap_or(""),
|
||||
e["records"]
|
||||
));
|
||||
let node = e["node"].as_str().unwrap_or("");
|
||||
text.push_str(&format!("\n {}… {:<10} {:<16} records {}", &node[..16.min(node.len())], e["role"].as_str().unwrap_or("?"), e["label"].as_str().unwrap_or(""), e["records"]));
|
||||
}
|
||||
Ok(out.done(json!({"enrolments": list}), &text))
|
||||
done("enrolment.list", false, json!({"enrolments": list}), &text)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------
|
||||
// report.create, report.export
|
||||
// ---------------------------------------------------------------------------------------
|
||||
|
||||
/// `wai report create|export`.
|
||||
pub fn report(args: &[String]) -> u8 {
|
||||
match args.first().map(String::as_str) {
|
||||
Some("create") => {
|
||||
let out = Out::new("report.create", args);
|
||||
match report_create(&args[1..], &out) {
|
||||
Ok(c) => c,
|
||||
Err(f) => out.fail(f),
|
||||
}
|
||||
}
|
||||
Some("export") => {
|
||||
let out = Out::new("report.export", args);
|
||||
match report_export(&args[1..], &out) {
|
||||
Ok(c) => c,
|
||||
Err(f) => out.fail(f),
|
||||
}
|
||||
}
|
||||
_ => Out::new("report.create", args).fail(usage(REPORT_USAGE, "wai report create … or wai report export …")),
|
||||
}
|
||||
}
|
||||
|
||||
/// A report's outcome, for a person: its period, its lines and each scope's coverage.
|
||||
fn summarise(o: &Value) -> String {
|
||||
let cov = |e: &Value| format!("sessions {}, hops {}", e["sessions"]["coverage"].as_str().unwrap_or("?"), e["hops"]["coverage"].as_str().unwrap_or("?"));
|
||||
let mut t = format!("report {} of [{}, {}): {} node line(s), {} room line(s), {} record(s)", &o["digest"].as_str().unwrap_or("")[..16.min(o["digest"].as_str().unwrap_or("").len())], o["start"], o["end"], o["nodes"].as_array().map_or(0, Vec::len), o["rooms"].as_array().map_or(0, Vec::len), o["records"]);
|
||||
let digest = o["digest"].as_str().unwrap_or("");
|
||||
let mut t = format!(
|
||||
"report {} of [{}, {}): {} node line(s), {} room line(s), {} record(s)",
|
||||
&digest[..16.min(digest.len())],
|
||||
o["start"],
|
||||
o["end"],
|
||||
o["nodes"].as_array().map_or(0, Vec::len),
|
||||
o["rooms"].as_array().map_or(0, Vec::len),
|
||||
o["records"]
|
||||
);
|
||||
for n in o["nodes"].as_array().into_iter().flatten() {
|
||||
t.push_str(&format!("\n node {:<16} {:<10} sessions {} links {} bytes in {} out {}; energy: {}", n["label"].as_str().unwrap_or(""), n["role"].as_str().unwrap_or(""), n["sessions"], n["links"], n["bytes_in"], n["bytes_out"], cov(&n["energy"])));
|
||||
}
|
||||
|
|
@ -350,44 +267,54 @@ fn summarise(o: &Value) -> String {
|
|||
t
|
||||
}
|
||||
|
||||
fn report_create(args: &[String], out: &Out) -> Result<u8, Fail> {
|
||||
let target = super::opt(args, "--target").ok_or_else(|| usage("--target: the fleet service's http:// address", REPORT_USAGE))?;
|
||||
let start = number(args, "--start", 0)?;
|
||||
let end = number(args, "--end", 0)?;
|
||||
/// `wai report create --target URL --start µs --end µs`: the fleet report of a period, which the
|
||||
/// service signs. It changes the service, so it waits for `--yes`.
|
||||
pub fn report_create(args: &[String]) -> R {
|
||||
let usage = super::usage("report.create");
|
||||
let with_value = ["--target", "--start", "--end"];
|
||||
agent::check_args(args, &with_value, &with_value, 0, &usage)?;
|
||||
let target = required(args, "--target", "the fleet service's http:// address", &usage)?;
|
||||
let (start, end) = (number(args, "--start", 0)?, number(args, "--end", 0)?);
|
||||
if super::opt(args, "--start").is_none() || super::opt(args, "--end").is_none() || start >= end {
|
||||
return Err(usage("--start and --end: the period, in microseconds since 1970, start below end", REPORT_USAGE));
|
||||
return Err(OpError::usage("--start and --end: the period, in microseconds since 1970, start below end", &usage));
|
||||
}
|
||||
let proposed = json!([{"action": "sign a report", "target": target, "start": start, "end": end}]);
|
||||
if super::flag(args, "--dry-run") {
|
||||
return Ok(out.dry_run(proposed));
|
||||
}
|
||||
if !confirmed(args, out, &format!("have {target} sign the report of [{start}, {end})?")) {
|
||||
return Ok(out.confirmation(proposed, confirm_command("report create", args)));
|
||||
let period = json!({"target": target, "start": start, "end": end});
|
||||
if out::dry_run() {
|
||||
out::would_send(target, "POST", "/v1/fleet/reports");
|
||||
return done("report.create", true, period, &format!("would have {target} sign the fleet report of [{start}, {end})"));
|
||||
}
|
||||
agent::confirm(
|
||||
&format!("the fleet service at {target} would sign the fleet report of [{start}, {end})"),
|
||||
vec![json!({"request": "POST /v1/fleet/reports", "target": target, "start": start, "end": end})],
|
||||
)?;
|
||||
let (status, v) = call(target, "POST", "/v1/fleet/reports", Some(&json!({"start": start, "end": end})))?;
|
||||
if status != 201 {
|
||||
return Err(answer_fail(status, &v));
|
||||
}
|
||||
let o = &v["outcome"];
|
||||
let text = summarise(o);
|
||||
Ok(out.done(json!({"report": v["report"], "outcome": o}), &text))
|
||||
done("report.create", true, json!({"report": v["report"], "outcome": o}), &summarise(o))
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "fleet"))]
|
||||
fn report_export(_: &[String], _: &Out) -> Result<u8, Fail> {
|
||||
Err(fail(USAGE, "feature-missing", "`wai report export` needs a build with the feature `fleet`", "cargo install --path wai/wai-rs --no-default-features --features quickstart"))
|
||||
pub fn report_export(_: &[String]) -> R {
|
||||
Err(crate::door::not_built(wai::catalogue::op("report.export").expect("listed")))
|
||||
}
|
||||
|
||||
/// `wai report export <digest> --target URL [--out F]`: a report's bundle, fetched, verified here
|
||||
/// (the service is not trusted) and written to a file.
|
||||
#[cfg(feature = "fleet")]
|
||||
fn report_export(args: &[String], out: &Out) -> Result<u8, Fail> {
|
||||
let target = super::opt(args, "--target").ok_or_else(|| usage("--target: the fleet service's http:// address", REPORT_USAGE))?;
|
||||
let pos = super::positional(args, &["--target", "--out"]);
|
||||
let digest = pos.first().filter(|d| d.len() == 64 && unhex(d).is_some()).ok_or_else(|| usage("the report's digest: 64 hexadecimal digits", REPORT_USAGE))?;
|
||||
pub fn report_export(args: &[String]) -> R {
|
||||
let usage = super::usage("report.export");
|
||||
let with_value = ["--target", "--out"];
|
||||
let pos = agent::check_args(args, &with_value, &with_value, 1, &usage)?;
|
||||
let target = required(args, "--target", "the fleet service's http:// address", &usage)?;
|
||||
let digest = *pos.first().filter(|d| d.len() == 64 && unhex(d).is_some()).ok_or_else(|| OpError::usage("the report's digest: 64 hexadecimal digits", &usage))?;
|
||||
let file = super::opt(args, "--out").map(str::to_string).unwrap_or_else(|| format!("{}{}", &digest[..16], wai::fleet::BUNDLE_SUFFIX));
|
||||
let replaces = std::path::Path::new(&file).exists();
|
||||
let proposed = json!([{"action": "write", "file": file, "replaces": replaces}]);
|
||||
if super::flag(args, "--dry-run") {
|
||||
return Ok(out.dry_run(proposed));
|
||||
if out::dry_run() {
|
||||
out::would_send(target, "GET", &format!("/v1/fleet/reports/{digest}"));
|
||||
out::would_write(&file);
|
||||
return done("report.export", true, json!({"digest": digest, "target": target, "file": file}), &format!("would export the bundle of report {}… from {target} to {file}", &digest[..16]));
|
||||
}
|
||||
let (status, v) = call(target, "GET", &format!("/v1/fleet/reports/{digest}"), None)?;
|
||||
if status != 200 {
|
||||
|
|
@ -396,13 +323,14 @@ fn report_export(args: &[String], out: &Out) -> Result<u8, Fail> {
|
|||
// The bundle verified here, by this build, before it is written: the service is not trusted.
|
||||
let o = wai::fleet::outcome_json(&wai::fleet::verify(&v["bundle"]));
|
||||
if o["outcome"] != "accept" || o["digest"] != json!(digest) {
|
||||
return Err(fail(REFUSED, o["reason"].as_str().unwrap_or("report_mismatch"), format!("the service's bundle does not verify: {o}"), "report it to the fleet's operator"));
|
||||
let r = o["reason"].as_str().unwrap_or("report_mismatch");
|
||||
return Err(OpError::new(EXIT_FAILED, reason_code(r), format!("the service's bundle does not verify: {o}"), "report it to the fleet's operator"));
|
||||
}
|
||||
if replaces && !confirmed(args, out, &format!("replace {file}?")) {
|
||||
return Ok(out.confirmation(proposed, confirm_command("report export", args)));
|
||||
if replaces {
|
||||
agent::confirm(&format!("{file} exists, and the bundle would replace it"), vec![json!({"write": file, "replaces": true})])?;
|
||||
}
|
||||
let text_out = serde_json::to_string_pretty(&v["bundle"]).expect("JSON") + "\n";
|
||||
std::fs::write(&file, text_out).map_err(|e| fail(if e.kind() == std::io::ErrorKind::PermissionDenied { PERMISSION } else { INTERNAL }, "io-error", format!("{file}: {e}"), "check the directory"))?;
|
||||
out::write(&file, text_out).map_err(|e| io_fail(&file, &e, "check the directory"))?;
|
||||
let text = format!("{}\nwritten to {file}; check it anywhere with `wai verify {file}`", summarise(&o));
|
||||
Ok(out.done(json!({"file": file, "outcome": o}), &text))
|
||||
done("report.export", true, json!({"file": file, "outcome": o}), &text)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,38 +15,42 @@
|
|||
//! The key's secret is never written: the receipt proves the bytes were signed by the key
|
||||
//! it names, and a later `wai verify` checks that again, offline.
|
||||
|
||||
pub const USAGE: &str = "wai hello [<dir>] WAI's four steps (name, dispatch, rebuild, receipt) on a sample, written to <dir> (wai-hello)";
|
||||
use super::envelope::OpError;
|
||||
|
||||
#[cfg(not(feature = "quickstart"))]
|
||||
pub fn main(_: &[String]) -> Result<bool, String> {
|
||||
Err("`wai hello` needs a build with the feature `quickstart`".into())
|
||||
pub fn main(_: &[String]) -> Result<bool, OpError> {
|
||||
Err(OpError::from("`wai hello` needs a build with the feature `quickstart`"))
|
||||
}
|
||||
|
||||
#[cfg(feature = "quickstart")]
|
||||
pub fn main(args: &[String]) -> Result<bool, String> {
|
||||
pub fn main(args: &[String]) -> Result<bool, OpError> {
|
||||
use super::hex;
|
||||
use crate::out;
|
||||
use std::path::PathBuf;
|
||||
use wai::codecs::image;
|
||||
use wai::http_binding::receipts::{IndexItem, RECEIPT_DIR, group_file, index_file};
|
||||
use wai::jwp_delivery::{GroupBuilder, ObjectLabel, WaiReceiptProperty};
|
||||
use wai::{CAP_IMAGE_PNG, Conditioning, Manifest, ModelRequirement, Wai};
|
||||
|
||||
let usage = super::usage("toolchain.test");
|
||||
if let Some(a) = args.iter().find(|a| a.starts_with("--")) {
|
||||
return Err(format!("wai hello: {a}: not an option ({USAGE})"));
|
||||
return Err(OpError::usage(format!("wai hello: {a}: not an option ({usage})"), ""));
|
||||
}
|
||||
let pos = super::positional(args, &[]);
|
||||
if pos.len() > 1 {
|
||||
return Err(USAGE.into());
|
||||
return Err(OpError::usage(usage, ""));
|
||||
}
|
||||
let dir = PathBuf::from(pos.first().copied().unwrap_or("wai-hello"));
|
||||
// Only a directory that is empty, or one an earlier `wai hello` wrote, is written to.
|
||||
// Only a directory that is empty, or one an earlier `wai hello` wrote, is written to; an
|
||||
// earlier run's files are replaced only once that is confirmed.
|
||||
let mut earlier = Vec::new();
|
||||
if dir.exists() {
|
||||
let theirs = std::fs::read_dir(&dir).map_err(|e| format!("{}: {e}", dir.display()))?.flatten().any(|e| {
|
||||
let n = e.file_name().to_string_lossy().into_owned();
|
||||
!(n == "rcpt" || n.ends_with(".wai") || n.ends_with(".wai.receipts"))
|
||||
});
|
||||
if theirs {
|
||||
return Err(format!("{}: holds files `wai hello` did not write; name another directory", dir.display()));
|
||||
return Err(OpError::from(format!("{}: holds files `wai hello` did not write; name another directory", dir.display())));
|
||||
}
|
||||
// An earlier run's object, index and receipt files, and nothing else.
|
||||
for e in std::fs::read_dir(&dir).map_err(|e| e.to_string())?.flatten() {
|
||||
|
|
@ -54,15 +58,24 @@ pub fn main(args: &[String]) -> Result<bool, String> {
|
|||
if e.file_name() == "rcpt" {
|
||||
for r in std::fs::read_dir(&p).map_err(|e| e.to_string())?.flatten() {
|
||||
if r.file_name().to_string_lossy().ends_with(".json") {
|
||||
let _ = std::fs::remove_file(r.path());
|
||||
earlier.push(r.path());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let _ = std::fs::remove_file(&p);
|
||||
earlier.push(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
std::fs::create_dir_all(dir.join(RECEIPT_DIR)).map_err(|e| format!("{}: {e}", dir.display()))?;
|
||||
if !earlier.is_empty() {
|
||||
super::envelope::confirm(
|
||||
&format!("{} holds an earlier `wai hello`'s {} files, which this run replaces", dir.display(), earlier.len()),
|
||||
earlier.iter().map(|p| serde_json::json!({"remove": p.display().to_string()})).collect(),
|
||||
)?;
|
||||
for p in &earlier {
|
||||
let _ = out::remove_file(p);
|
||||
}
|
||||
}
|
||||
out::create_dir_all(dir.join(RECEIPT_DIR)).map_err(|e| format!("{}: {e}", dir.display()))?;
|
||||
println!("wai hello: WAI's four steps, on this machine\n");
|
||||
|
||||
// 1. Name.
|
||||
|
|
@ -79,7 +92,7 @@ pub fn main(args: &[String]) -> Result<bool, String> {
|
|||
let envelope = Wai::new(manifest, png).to_bytes().map_err(|e| e.to_string())?;
|
||||
let cid = wai::jwp_hash::content_hash(&envelope);
|
||||
let object = dir.join(format!("{}.wai", hex(&cid)));
|
||||
std::fs::write(&object, &envelope).map_err(|e| e.to_string())?;
|
||||
out::write(&object, &envelope).map_err(|e| e.to_string())?;
|
||||
println!("1 Name a {w}×{h} picture, packed in an envelope that names the capability it needs:");
|
||||
println!(" {CAP_IMAGE_PNG} (PNG, one of the mandatory floor every sink implements)");
|
||||
println!(" {} ({} bytes)\n", object.display(), envelope.len());
|
||||
|
|
@ -111,16 +124,21 @@ pub fn main(args: &[String]) -> Result<bool, String> {
|
|||
b.add(cid, 0, "wai hello", Some(ObjectLabel::Unmetered));
|
||||
let (group, objects) = b.finish(&key, &wai::signer_id::did_key(&pk), 0, 0, &start, &end, None, true).ok_or("the group would not seal")?;
|
||||
let link = group.link_hash();
|
||||
std::fs::write(dir.join(format!("{RECEIPT_DIR}{}.json", hex(&link))), group_file(&group)?).map_err(|e| e.to_string())?;
|
||||
out::write(dir.join(format!("{RECEIPT_DIR}{}.json", hex(&link))), group_file(&group)?).map_err(|e| e.to_string())?;
|
||||
let item = IndexItem {
|
||||
property: WaiReceiptProperty { content_hash: cid, joules_micro: 0, acquisition: String::new(), acquisition_evidence: Vec::new(), group_receipt: link },
|
||||
object_receipt: objects[0].clone(),
|
||||
};
|
||||
std::fs::write(dir.join(format!("{}.wai.receipts", hex(&cid))), index_file(&cid, &[item], &[])?).map_err(|e| e.to_string())?;
|
||||
let index = dir.join(format!("{}.wai.receipts", hex(&cid)));
|
||||
out::write(&index, index_file(&cid, &[item], &[])?).map_err(|e| e.to_string())?;
|
||||
println!("4 Receipt the object's BLAKE3 content hash, in a group receipt signed with a key made for");
|
||||
println!(" this run (Ed25519 {}…; its secret is not kept). Energy is stated as not", &hex(&pk)[..16]);
|
||||
println!(" measured: this run reads no meter");
|
||||
let (ok, lines) = super::verify::check_index_file(&dir.join(format!("{}.wai.receipts", hex(&cid))))?;
|
||||
if out::dry_run() {
|
||||
println!(" (a dry run: the receipt is not written, so it is not checked here)");
|
||||
return Ok(true);
|
||||
}
|
||||
let (ok, lines) = super::verify::check_index_file(&index)?;
|
||||
for l in lines {
|
||||
println!(" {l}");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,25 +1,34 @@
|
|||
//! Every verb kept in a module of its own, one verb a module, every verb in [`VERBS`]:
|
||||
//! The developer's path through WAI and the agent door, one operation a module:
|
||||
//!
|
||||
//! - `hello`: the four steps of WAI (name, dispatch, rebuild, receipt) on a bundled sample;
|
||||
//! - `create`: a new project from one of the examples (`wai/examples/`), or from an app made
|
||||
//! with Create (`--from app.oui`, a build with `app`);
|
||||
//! - `dev`: a relay, a room service and the room page on one origin, on this machine;
|
||||
//! - `verify`: the receipts under a directory, conformance results and fleet report bundles,
|
||||
//! checked offline;
|
||||
//! - `app` (`app.guide`, `app.check`; a build with `app`): an app made with Create, its guide
|
||||
//! and its check;
|
||||
//! - `mcp` (`agent.mcp`): WAI's operations as tools for a coding agent, a Model Context
|
||||
//! - `toolchain.test` (`wai hello`): the four steps of WAI (name, dispatch, rebuild,
|
||||
//! receipt) on a bundled sample;
|
||||
//! - `project.create` (`wai create`): a new project from one of the examples
|
||||
//! (`wai/examples/`), or from an app made with Create (`--from app.oui`, `app.export`);
|
||||
//! - `dev.start` (`wai dev`): a relay, a room service and the room page on one origin, on this
|
||||
//! machine;
|
||||
//! - `receipt.verify` (`wai verify`): the receipts under a directory, conformance results and
|
||||
//! fleet report bundles, checked offline;
|
||||
//! - `compat.check` (`wai breaking`): what the working tree breaks against a git ref;
|
||||
//! - `conformance.run` (`wai conform`): WAI's conformance corpus run against another
|
||||
//! implementation, its result a signed record; `conformance.list` (`wai conformance list`,
|
||||
//! also `wai conform --list`) the kinds and suites;
|
||||
//! - `app.guide`, `app.check`, `app.export` (`wai app guide|check|export`; a build with
|
||||
//! `app`): an app made with Create;
|
||||
//! - `mcp.start` (`wai mcp`): WAI's operations as tools for a coding agent, a Model Context
|
||||
//! Protocol server on standard input and output, in toolsets (`../mcp/`);
|
||||
//! - `setup-agent` (`agent.setup`): a project made ready for coding agents (`../setup_agent.rs`);
|
||||
//! - `breaking`: what the working tree breaks against a git ref (operation `compat.check`);
|
||||
//! - `conform`: WAI's conformance corpus run against another implementation (operation
|
||||
//! `conformance.run`), its result a signed record;
|
||||
//! - `enrolment` (`enrolment.create`, `enrolment.list`) and `report` (`report.create`,
|
||||
//! `report.export`): a fleet's nodes enrolled and its reports, against a fleet service
|
||||
//! (extensions/fleet.md §9; `fleet.rs`).
|
||||
//! - `agent.setup` (`wai setup-agent`): a project made ready for coding agents
|
||||
//! (`../setup_agent.rs`);
|
||||
//! - `enrolment.create`, `enrolment.list` (`wai enrolment create|list`) and `report.create`,
|
||||
//! `report.export` (`wai report create|export`): a fleet's nodes enrolled and its reports,
|
||||
//! against a fleet service (extensions/fleet.md §9; `fleet.rs`).
|
||||
//!
|
||||
//! A verb is named in this table and nowhere else: renaming or regrouping one is a line
|
||||
//! here. The project file a template writes (`wai.json`) is read by [`project`] alone.
|
||||
//! [`envelope`] holds the agent rules every command keeps: the exit codes, the JSON envelope,
|
||||
//! `--dry-run`, `--yes`.
|
||||
//!
|
||||
//! [`VERBS`] is this module's view of the operation catalogue (`wai::catalogue`): each
|
||||
//! entry names its operation by id, and the name a person types, its usage and its help come
|
||||
//! from the catalogue's entry. The project file a template writes (`wai.json`) is read by
|
||||
//! [`project`] alone.
|
||||
|
||||
pub mod project;
|
||||
|
||||
|
|
@ -29,7 +38,7 @@ mod breaking;
|
|||
pub(crate) mod conform;
|
||||
mod create;
|
||||
mod dev;
|
||||
mod envelope;
|
||||
pub(crate) mod envelope;
|
||||
mod fleet;
|
||||
mod hello;
|
||||
mod verify;
|
||||
|
|
@ -40,85 +49,115 @@ mod mcp;
|
|||
#[path = "../setup_agent.rs"]
|
||||
mod setup_agent;
|
||||
|
||||
use crate::door::{Impl, Json};
|
||||
use envelope::OpError;
|
||||
|
||||
/// The verb `wai mcp` is called by; `setup-agent` registers the server under it.
|
||||
pub const MCP: &str = "mcp";
|
||||
|
||||
/// How a verb ends.
|
||||
pub enum Run {
|
||||
/// `Ok(true)` when everything it checked held (exit 0), `Ok(false)` when it ran and
|
||||
/// something did not (exit 1); an error is a usage error (exit 2).
|
||||
Checks(fn(&[String]) -> Result<bool, String>),
|
||||
/// An exit status of the toolchain's table (0 done, 1 a check failed, 2 usage, 3
|
||||
/// permission, 4 confirmation required, 5 not found, 6 unavailable, 7 internal), the
|
||||
/// verb's output already written.
|
||||
Status(fn(&[String]) -> u8),
|
||||
}
|
||||
|
||||
/// One verb: its name, its usage, and what runs it.
|
||||
pub struct Verb {
|
||||
pub name: &'static str,
|
||||
pub usage: &'static str,
|
||||
pub run: Run,
|
||||
}
|
||||
|
||||
pub const VERBS: &[Verb] = &[
|
||||
Verb { name: "hello", usage: hello::USAGE, run: Run::Checks(hello::main) },
|
||||
Verb { name: "create", usage: create::USAGE, run: Run::Checks(create::main) },
|
||||
Verb { name: "dev", usage: dev::USAGE, run: Run::Checks(dev::main) },
|
||||
Verb { name: "verify", usage: verify::USAGE, run: Run::Checks(verify::main) },
|
||||
Verb { name: "app", usage: app::USAGE, run: Run::Checks(app::main) },
|
||||
Verb { name: MCP, usage: MCP_USAGE, run: Run::Checks(mcp_main) },
|
||||
Verb { name: "setup-agent", usage: setup_agent::USAGE, run: Run::Status(setup_agent::main) },
|
||||
// Operations of the toolchain's catalogue, kept here until it lands: `compat.check` and
|
||||
// `conformance.run`.
|
||||
Verb { name: "breaking", usage: breaking::USAGE, run: Run::Checks(breaking::main) },
|
||||
Verb { name: "conform", usage: CONFORM_USAGE, run: Run::Checks(conform_main) },
|
||||
Verb { name: "enrolment", usage: fleet::ENROLMENT_USAGE, run: Run::Status(fleet::enrolment) },
|
||||
Verb { name: "report", usage: fleet::REPORT_USAGE, run: Run::Status(fleet::report) },
|
||||
/// The developer's path, by operation id.
|
||||
pub const VERBS: &[Impl] = &[
|
||||
Impl { id: "toolchain.test", run: hello::main, json: Json::Text },
|
||||
Impl { id: "project.create", run: create::main, json: Json::OwnWith("--list") },
|
||||
Impl { id: "dev.start", run: dev::main, json: Json::StreamUnless("--check") },
|
||||
Impl { id: "receipt.verify", run: verify::main, json: Json::Text },
|
||||
Impl { id: "compat.check", run: compat_check, json: Json::Native },
|
||||
Impl { id: "conformance.run", run: conformance_run, json: Json::Native },
|
||||
Impl { id: "conformance.list", run: conformance_list, json: Json::Native },
|
||||
Impl { id: "app.guide", run: app_guide, json: Json::Text },
|
||||
Impl { id: "app.check", run: app_check, json: Json::Native },
|
||||
Impl { id: "app.export", run: app_export, json: Json::Text },
|
||||
Impl { id: "mcp.start", run: mcp_main, json: Json::Native },
|
||||
Impl { id: "agent.setup", run: setup_agent::main, json: Json::Native },
|
||||
Impl { id: "enrolment.create", run: fleet::enrolment_create, json: Json::Native },
|
||||
Impl { id: "enrolment.list", run: fleet::enrolment_list, json: Json::Native },
|
||||
Impl { id: "report.create", run: fleet::report_create, json: Json::Native },
|
||||
Impl { id: "report.export", run: fleet::report_export, json: Json::Native },
|
||||
];
|
||||
|
||||
const MCP_USAGE: &str = "wai mcp [--toolset core,dev,app] [--read-only] [--project DIR] [--spec-root DIR] [--room-bin BIN]\n WAI's operations as tools for a coding agent: a Model Context Protocol server on standard\n input and output (a build with `mcp`)";
|
||||
|
||||
#[cfg(feature = "mcp")]
|
||||
fn mcp_main(args: &[String]) -> Result<bool, String> {
|
||||
mcp::main(args)
|
||||
/// An operation's synopsis, from the catalogue, for a usage error.
|
||||
pub(crate) fn usage(id: &str) -> String {
|
||||
let o = wai::catalogue::op(id).expect("an operation of the catalogue");
|
||||
o.usage.lines().next().map(|l| format!("wai {l}")).unwrap_or_default()
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "mcp"))]
|
||||
fn mcp_main(_: &[String]) -> Result<bool, String> {
|
||||
Err(format!("`wai {MCP}` needs a build with the feature `mcp` (cargo install --path wai/wai-rs --no-default-features --features mcp)"))
|
||||
/// `args` with the agent flags `wai` read off the command line handed back, those of `takes`
|
||||
/// that the operation reads itself (`--json`, `--dry-run`). `--yes` is read where a change
|
||||
/// waits for it, from [`envelope::ctx`] ([`envelope::confirm`]).
|
||||
fn with_agent_flags(args: &[String], takes: &[&str]) -> Vec<String> {
|
||||
let c = envelope::ctx();
|
||||
let mut a = args.to_vec();
|
||||
for (on, flag) in [(c.json, "--json"), (c.dry_run, "--dry-run")] {
|
||||
if on && takes.contains(&flag) && !a.iter().any(|x| x == flag) {
|
||||
a.push(flag.into());
|
||||
}
|
||||
}
|
||||
a
|
||||
}
|
||||
|
||||
const CONFORM_USAGE: &str = "wai conform <kind> --suite <suite> --impl <command or URL> [--key <file>] [--out <file>] [--confirm]\n\
|
||||
[--root <wai-dir>] [--dry-run] [--json|--text] WAI's conformance corpus run against another\n\
|
||||
implementation, the result a signed record `wai verify` checks; `wai conform --list` names the\n\
|
||||
kinds and suites (operation conformance.run)";
|
||||
fn compat_check(args: &[String]) -> Result<bool, OpError> {
|
||||
Ok(breaking::main(&with_agent_flags(args, &["--json"]))?)
|
||||
}
|
||||
|
||||
#[cfg(feature = "quickstart")]
|
||||
fn conform_main(args: &[String]) -> Result<bool, String> {
|
||||
conform::main(args)
|
||||
fn conformance_run(args: &[String]) -> Result<bool, OpError> {
|
||||
Ok(conform::main(&with_agent_flags(args, &["--json", "--dry-run"]))?)
|
||||
}
|
||||
|
||||
#[cfg(feature = "quickstart")]
|
||||
fn conformance_list(args: &[String]) -> Result<bool, OpError> {
|
||||
let mut a = vec!["--list".to_string()];
|
||||
a.extend(with_agent_flags(args, &["--json"]));
|
||||
Ok(conform::main(&a)?)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "quickstart"))]
|
||||
fn conform_main(_: &[String]) -> Result<bool, String> {
|
||||
Err("`wai conform` needs a build with the feature `quickstart`".into())
|
||||
fn conformance_run(_: &[String]) -> Result<bool, OpError> {
|
||||
Err(crate::door::not_built(wai::catalogue::op("conformance.run").expect("listed")))
|
||||
}
|
||||
|
||||
/// The verb `args[0]` names, run on the rest, and its exit status; `None` when it names none
|
||||
/// of these.
|
||||
pub fn dispatch(args: &[String]) -> Option<Result<u8, String>> {
|
||||
let v = VERBS.iter().find(|v| Some(v.name) == args.first().map(String::as_str))?;
|
||||
Some(match v.run {
|
||||
Run::Checks(run) => run(&args[1..]).map(|held| if held { 0 } else { 1 }),
|
||||
Run::Status(run) => Ok(run(&args[1..])),
|
||||
})
|
||||
#[cfg(not(feature = "quickstart"))]
|
||||
fn conformance_list(_: &[String]) -> Result<bool, OpError> {
|
||||
Err(crate::door::not_built(wai::catalogue::op("conformance.list").expect("listed")))
|
||||
}
|
||||
|
||||
/// Each verb's usage line, for `wai`'s own usage.
|
||||
pub fn usage() {
|
||||
for v in VERBS {
|
||||
eprintln!("{}", v.usage);
|
||||
/// `words` then `args`.
|
||||
fn with(words: &[&str], args: &[String]) -> Vec<String> {
|
||||
words.iter().map(|w| w.to_string()).chain(args.iter().cloned()).collect()
|
||||
}
|
||||
|
||||
fn app_guide(args: &[String]) -> Result<bool, OpError> {
|
||||
Ok(app::main(&with(&["guide"], args))?)
|
||||
}
|
||||
|
||||
fn app_check(args: &[String]) -> Result<bool, OpError> {
|
||||
Ok(app::main(&with(&["check"], &with_agent_flags(args, &["--json"])))?)
|
||||
}
|
||||
|
||||
/// `wai app export <app.oui> <dir> [--runtime DIR]`: `wai create <dir> --from <app.oui>`.
|
||||
fn app_export(args: &[String]) -> Result<bool, OpError> {
|
||||
let pos = positional(args, &["--runtime"]);
|
||||
let [program, dir] = pos.as_slice() else {
|
||||
return Err(OpError::usage(usage("app.export"), ""));
|
||||
};
|
||||
let mut a = vec![dir.to_string(), "--from".into(), program.to_string()];
|
||||
if let Some(r) = opt(args, "--runtime") {
|
||||
a.extend(["--runtime".into(), r.to_string()]);
|
||||
}
|
||||
create::main(&a)
|
||||
}
|
||||
|
||||
#[cfg(feature = "mcp")]
|
||||
fn mcp_main(args: &[String]) -> Result<bool, OpError> {
|
||||
if crate::out::dry_run() {
|
||||
return mcp::plan(args).map_err(OpError::from);
|
||||
}
|
||||
mcp::main(args).map_err(OpError::from)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "mcp"))]
|
||||
fn mcp_main(_: &[String]) -> Result<bool, OpError> {
|
||||
Err(crate::door::not_built(wai::catalogue::op("mcp.start").expect("listed")))
|
||||
}
|
||||
|
||||
/// The value after `flag` among `args`.
|
||||
|
|
|
|||
|
|
@ -1,8 +1,13 @@
|
|||
//! The templates `wai create` writes: each example of `wai/examples/` but its `test/`
|
||||
//! directory, its files held in this binary. `tests/cli_dx.rs` holds this table to the
|
||||
//! examples on disk, file for file and byte for byte, so the two cannot drift.
|
||||
//! examples on disk, file for file and byte for byte, so the two cannot drift. Every
|
||||
//! template is Apache-2.0 and carries the license's text as `LICENSE`: one copy in this
|
||||
//! binary, `wai/LICENSE`, which each example's `LICENSE` must equal byte for byte.
|
||||
|
||||
use super::Template;
|
||||
use super::{File, Template};
|
||||
|
||||
/// The Apache-2.0 text every template carries: the one copy `wai create` holds.
|
||||
const LICENSE: File = super::LICENSE;
|
||||
|
||||
macro_rules! ex {
|
||||
($t:literal, $f:literal) => {
|
||||
|
|
@ -15,6 +20,7 @@ pub const TEMPLATES: &[Template] = &[
|
|||
name: "video-room",
|
||||
title: "A video room in one page",
|
||||
files: &[
|
||||
LICENSE,
|
||||
ex!("video-room", ".gitignore"),
|
||||
ex!("video-room", "README.md"),
|
||||
ex!("video-room", "index.html"),
|
||||
|
|
@ -26,6 +32,7 @@ pub const TEMPLATES: &[Template] = &[
|
|||
name: "watch-together",
|
||||
title: "Watch together",
|
||||
files: &[
|
||||
LICENSE,
|
||||
ex!("watch-together", ".gitignore"),
|
||||
ex!("watch-together", "README.md"),
|
||||
ex!("watch-together", "index.html"),
|
||||
|
|
@ -37,6 +44,7 @@ pub const TEMPLATES: &[Template] = &[
|
|||
name: "messaging",
|
||||
title: "Messaging and a call",
|
||||
files: &[
|
||||
LICENSE,
|
||||
ex!("messaging", ".gitignore"),
|
||||
ex!("messaging", "README.md"),
|
||||
ex!("messaging", "index.html"),
|
||||
|
|
@ -48,6 +56,7 @@ pub const TEMPLATES: &[Template] = &[
|
|||
name: "encoder-ingest",
|
||||
title: "Stream from an encoder into a room",
|
||||
files: &[
|
||||
LICENSE,
|
||||
ex!("encoder-ingest", ".gitignore"),
|
||||
ex!("encoder-ingest", "Cargo.toml"),
|
||||
ex!("encoder-ingest", "README.md"),
|
||||
|
|
@ -60,6 +69,7 @@ pub const TEMPLATES: &[Template] = &[
|
|||
name: "package-and-deliver",
|
||||
title: "Package a title and play it",
|
||||
files: &[
|
||||
LICENSE,
|
||||
ex!("package-and-deliver", ".gitignore"),
|
||||
ex!("package-and-deliver", "README.md"),
|
||||
ex!("package-and-deliver", "floor/v.obu"),
|
||||
|
|
@ -76,6 +86,7 @@ pub const TEMPLATES: &[Template] = &[
|
|||
name: "verify-receipts",
|
||||
title: "Verify receipts in Rust and in Python",
|
||||
files: &[
|
||||
LICENSE,
|
||||
ex!("verify-receipts", ".gitignore"),
|
||||
ex!("verify-receipts", "README.md"),
|
||||
ex!("verify-receipts", "python/_blake3.py"),
|
||||
|
|
@ -91,6 +102,7 @@ pub const TEMPLATES: &[Template] = &[
|
|||
name: "apple-app",
|
||||
title: "A native app on Apple platforms",
|
||||
files: &[
|
||||
LICENSE,
|
||||
ex!("apple-app", ".gitignore"),
|
||||
ex!("apple-app", "Package.swift"),
|
||||
ex!("apple-app", "README.md"),
|
||||
|
|
@ -106,6 +118,7 @@ pub const TEMPLATES: &[Template] = &[
|
|||
name: "android-app",
|
||||
title: "The native sink in Kotlin, for Android",
|
||||
files: &[
|
||||
LICENSE,
|
||||
ex!("android-app", ".gitignore"),
|
||||
ex!("android-app", "README.md"),
|
||||
ex!("android-app", "replay/build.gradle.kts"),
|
||||
|
|
@ -120,6 +133,7 @@ pub const TEMPLATES: &[Template] = &[
|
|||
name: "remote-desktop",
|
||||
title: "Remote desktop in a room",
|
||||
files: &[
|
||||
LICENSE,
|
||||
ex!("remote-desktop", ".gitignore"),
|
||||
ex!("remote-desktop", "README.md"),
|
||||
ex!("remote-desktop", "host.sh"),
|
||||
|
|
|
|||
|
|
@ -15,23 +15,23 @@
|
|||
//! A fleet report's bundle (`*.wai-fleet.json`, extensions/fleet.md §7) under a path is verified
|
||||
//! too, in a build with `fleet`: the report recomputed from its enrolments and records.
|
||||
|
||||
pub const USAGE: &str = "wai verify <path>... the receipts (receipt indexes *.wai.receipts), conformance results\n\
|
||||
(*.wai-conform.json) and fleet report bundles (*.wai-fleet.json) under each path, checked offline";
|
||||
use super::envelope::OpError;
|
||||
|
||||
#[cfg(not(feature = "quickstart"))]
|
||||
pub fn main(_: &[String]) -> Result<bool, String> {
|
||||
Err("`wai verify` needs a build with the feature `quickstart`".into())
|
||||
pub fn main(_: &[String]) -> Result<bool, OpError> {
|
||||
Err(OpError::from("`wai verify` needs a build with the feature `quickstart`"))
|
||||
}
|
||||
|
||||
#[cfg(feature = "quickstart")]
|
||||
pub fn main(args: &[String]) -> Result<bool, String> {
|
||||
pub fn main(args: &[String]) -> Result<bool, OpError> {
|
||||
use std::path::{Path, PathBuf};
|
||||
let usage = super::usage("receipt.verify");
|
||||
if let Some(a) = args.iter().find(|a| a.starts_with("--")) {
|
||||
return Err(format!("wai verify: {a}: not an option ({USAGE})"));
|
||||
return Err(OpError::usage(format!("wai verify: {a}: not an option ({usage})"), ""));
|
||||
}
|
||||
let paths = super::positional(args, &[]);
|
||||
if paths.is_empty() {
|
||||
return Err(USAGE.into());
|
||||
return Err(OpError::usage(usage, ""));
|
||||
}
|
||||
fn walk(p: &Path, out: &mut Vec<PathBuf>, bundles: &mut Vec<PathBuf>, depth: usize) -> Result<(), String> {
|
||||
if p.is_file() {
|
||||
|
|
@ -64,10 +64,10 @@ pub fn main(args: &[String]) -> Result<bool, String> {
|
|||
walk(Path::new(p), &mut indexes, &mut bundles, 0)?;
|
||||
}
|
||||
if indexes.is_empty() && bundles.is_empty() {
|
||||
return Err(format!(
|
||||
return Err(OpError::from(format!(
|
||||
"no receipt index (*.wai.receipts), conformance result (*.wai-conform.json) or fleet bundle (*.wai-fleet.json) under {}",
|
||||
paths.join(", ")
|
||||
));
|
||||
)));
|
||||
}
|
||||
let mut all = true;
|
||||
let (mut n_ix, mut n_rec) = (0, 0);
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ fn read(p: &Path) -> Result<Vec<u8>, String> {
|
|||
}
|
||||
|
||||
fn write(p: &str, b: &[u8]) -> Result<(), String> {
|
||||
std::fs::write(p, b).map_err(|e| format!("{p}: {e}"))
|
||||
crate::out::write(p, b).map_err(|e| format!("{p}: {e}"))
|
||||
}
|
||||
|
||||
fn seed(s: &str) -> Result<[u8; 32], String> {
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ fn flags(args: &[String]) -> Result<(String, u32, u32), String> {
|
|||
}
|
||||
|
||||
fn write_and_report(rgb: &[u8], w: u32, h: u32, out: &str, json: bool) -> Result<(), String> {
|
||||
std::fs::write(out, rgb).map_err(|e| format!("write {out}: {e}"))?;
|
||||
crate::out::write(out, rgb).map_err(|e| format!("write {out}: {e}"))?;
|
||||
let sha = wai::pin::sha256_hex(rgb);
|
||||
if json {
|
||||
println!("{}", serde_json::json!({ "w": w, "h": h, "bytes": rgb.len(), "sha256": sha }));
|
||||
|
|
@ -113,7 +113,7 @@ pub fn main(args: &[String]) -> Result<(), String> {
|
|||
let object = i.canonical();
|
||||
// What is written is what a sink reads.
|
||||
debug_assert_eq!(Instruction::parse(&object).as_ref(), Ok(&i));
|
||||
std::fs::write(dest, &object).map_err(|e| format!("write {dest}: {e}"))?;
|
||||
crate::out::write(dest, &object).map_err(|e| format!("write {dest}: {e}"))?;
|
||||
println!("instruction object → {dest} ({} B)", object.len());
|
||||
return Ok(());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ fn read(p: &str) -> Result<Vec<u8>, String> {
|
|||
|
||||
fn publish(args: &[String]) -> Result<(), String> {
|
||||
let out = PathBuf::from(opt(args, "--out").ok_or("publish needs --out <dir>")?);
|
||||
std::fs::create_dir_all(&out).map_err(|e| e.to_string())?;
|
||||
crate::out::create_dir_all(&out).map_err(|e| e.to_string())?;
|
||||
let inputs = positional(args, &["--out"]);
|
||||
if inputs.is_empty() {
|
||||
return Err(usage());
|
||||
|
|
@ -147,9 +147,9 @@ fn publish(args: &[String]) -> Result<(), String> {
|
|||
let cid = content_hash(&env);
|
||||
let name = object_name(&cid);
|
||||
let proof = HeadProof::build(&env).map_err(|e| format!("{p}: {e}"))?;
|
||||
std::fs::write(out.join(&name), &env).map_err(|e| e.to_string())?;
|
||||
std::fs::write(out.join(format!("{name}.hp")), proof.to_bytes()).map_err(|e| e.to_string())?;
|
||||
std::fs::write(out.join(format!("{name}.etag")), format!("\"{}\"\n", hex(&cid))).map_err(|e| e.to_string())?;
|
||||
crate::out::write(out.join(&name), &env).map_err(|e| e.to_string())?;
|
||||
crate::out::write(out.join(format!("{name}.hp")), proof.to_bytes()).map_err(|e| e.to_string())?;
|
||||
crate::out::write(out.join(format!("{name}.etag")), format!("\"{}\"\n", hex(&cid))).map_err(|e| e.to_string())?;
|
||||
let element = codecs::element_of(&head).map(|e| codecs::render(&e)).unwrap_or_else(|e| format!("({e})"));
|
||||
println!("{}\tn={}\th={}\t{element}\t{p}", hex(&cid), env.len(), head.covered_len());
|
||||
}
|
||||
|
|
@ -625,6 +625,14 @@ fn origin_cmd(args: &[String]) -> Result<(), String> {
|
|||
None => None,
|
||||
};
|
||||
let cfg = OriginConfig { root: root.clone(), delay, live, hits, max_connections, max_per_peer, read_timeout, lifetime, active, peers: Default::default(), live_edge };
|
||||
// `--dry-run`: say what would be served, and serve nothing.
|
||||
if crate::out::dry_run() {
|
||||
if !root.is_dir() {
|
||||
return Err(format!("--root {}: no such directory", root.display()));
|
||||
}
|
||||
println!("wai http origin --dry-run: nothing served. It would serve {} on http://127.0.0.1:{port}/", root.display());
|
||||
return Ok(());
|
||||
}
|
||||
let listener = TcpListener::bind(("127.0.0.1", port)).map_err(|e| format!("bind 127.0.0.1:{port}: {e}"))?;
|
||||
println!("serving {} on http://127.0.0.1:{}/", root.display(), listener.local_addr().map_err(|e| e.to_string())?.port());
|
||||
for stream in listener.incoming().flatten() {
|
||||
|
|
@ -861,7 +869,7 @@ fn bench(args: &[String]) -> Result<(), String> {
|
|||
"refinement_deadline_ms": deadline,
|
||||
"energy": "omitted: not measured",
|
||||
});
|
||||
std::fs::write(out, serde_json::to_string_pretty(&rec).expect("json") + "\n").map_err(|e| e.to_string())?;
|
||||
crate::out::write(out, serde_json::to_string_pretty(&rec).expect("json") + "\n").map_err(|e| e.to_string())?;
|
||||
println!("wrote {out}");
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,9 +62,9 @@ fn read(p: &Path) -> Result<Vec<u8>, String> {
|
|||
|
||||
fn write(p: &Path, b: &[u8]) -> Result<(), String> {
|
||||
if let Some(d) = p.parent() {
|
||||
std::fs::create_dir_all(d).map_err(|e| format!("{}: {e}", d.display()))?;
|
||||
crate::out::create_dir_all(d).map_err(|e| format!("{}: {e}", d.display()))?;
|
||||
}
|
||||
std::fs::write(p, b).map_err(|e| format!("{}: {e}", p.display()))
|
||||
crate::out::write(p, b).map_err(|e| format!("{}: {e}", p.display()))
|
||||
}
|
||||
|
||||
/// One variant of an objects list, read: its classical playlist and the objects that
|
||||
|
|
|
|||
|
|
@ -67,8 +67,8 @@ pub fn main(args: &[String]) -> Result<bool, String> {
|
|||
let files = generate(&dir)?;
|
||||
for (rel, bytes) in &files {
|
||||
let p = dir.join(rel);
|
||||
std::fs::create_dir_all(p.parent().expect("a parent")).map_err(|e| e.to_string())?;
|
||||
std::fs::write(&p, bytes).map_err(|e| format!("{}: {e}", p.display()))?;
|
||||
crate::out::create_dir_all(p.parent().expect("a parent")).map_err(|e| e.to_string())?;
|
||||
crate::out::write(&p, bytes).map_err(|e| format!("{}: {e}", p.display()))?;
|
||||
}
|
||||
println!("wrote {} files under {}", files.len(), dir.display());
|
||||
Ok(true)
|
||||
|
|
@ -79,7 +79,7 @@ pub fn main(args: &[String]) -> Result<bool, String> {
|
|||
let count: usize = opt(args, "--count").and_then(|s| s.parse().ok()).ok_or("fuzz needs --count")?;
|
||||
let out = opt(args, "--out").ok_or("fuzz needs --out")?;
|
||||
let (bytes, n, stats) = fuzz(Path::new(dir), seed, count)?;
|
||||
std::fs::write(out, bytes).map_err(|e| e.to_string())?;
|
||||
crate::out::write(out, bytes).map_err(|e| e.to_string())?;
|
||||
println!("wrote {n} cases to {out}");
|
||||
for (family, (generated, refused)) in &stats {
|
||||
println!(" {family}: {generated} generated, {refused} refused");
|
||||
|
|
@ -91,7 +91,7 @@ pub fn main(args: &[String]) -> Result<bool, String> {
|
|||
let count: usize = opt(args, "--count").and_then(|s| s.parse().ok()).ok_or("diff needs --count")?;
|
||||
let out = opt(args, "--out").ok_or("diff needs --out")?;
|
||||
let _ = dir;
|
||||
std::fs::write(out, differential(seed, count)?).map_err(|e| e.to_string())?;
|
||||
crate::out::write(out, differential(seed, count)?).map_err(|e| e.to_string())?;
|
||||
println!("wrote {count} envelopes' prefixes and proofs to {out}");
|
||||
Ok(true)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
//! The `dev` toolset: a relay and a room service on loopback, run as the room service's `dev`
|
||||
//! mode (`joule-moq-room dev`, a child process driven by JSON control lines) by `dev_start` and
|
||||
//! mode (`wai-room dev`, a child process driven by JSON control lines) by `dev_start` and
|
||||
//! `dev_stop`; and on that room, the target `local`, grants minted for test principals
|
||||
//! (`room_grant`), those principals joining, publishing and leaving (`room_join`, `room_leave`),
|
||||
//! and the room's session as a verifier is given it (`room_session`).
|
||||
//! and the room's session as a verifier is given it (`session_show`).
|
||||
//!
|
||||
//! **Development only.** The room binds 127.0.0.1 alone and its keys live only in the child
|
||||
//! process. A grant is asked for over HTTP from a service whose address is a loopback address,
|
||||
|
|
@ -137,8 +137,8 @@ fn dev_mut(server: &mut Server) -> Result<&mut Dev, String> {
|
|||
fn room_bin(server: &Server, args: &Value) -> Result<PathBuf, String> {
|
||||
let given = opt_str(args, "room_bin").map(String::from).or_else(|| server.room_bin.as_ref().map(|p| p.display().to_string()));
|
||||
let exe_dir = std::env::current_exe().ok().and_then(|e| std::fs::canonicalize(e).ok()).and_then(|e| e.parent().map(Path::to_path_buf));
|
||||
super::super::dev::find_bin("joule-moq-room", given.as_deref(), "WAI_ROOM_BIN", exe_dir.as_deref()).map_err(|e| {
|
||||
format!("{e}. The development room runs the room service joule-moq-room (built from joule-code/joule-code-rs/crates/joule-moq-room): give room_bin, --room-bin or WAI_ROOM_BIN")
|
||||
super::super::dev::find_bin("wai-room", given.as_deref(), "WAI_ROOM_BIN", exe_dir.as_deref()).map_err(|e| {
|
||||
format!("{e}. The development room runs the room service wai-room (built from joule-code/joule-code-rs/crates/joule-moq-room): give room_bin, --room-bin or WAI_ROOM_BIN")
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
//! `wai mcp` (catalogue `agent.mcp`): WAI's operations as tools for a coding assistant, served
|
||||
//! `wai mcp` (catalogue `mcp.start`): WAI's operations as tools for a coding assistant, served
|
||||
//! over the Model Context Protocol (revision 2025-06-18) on standard input and output, one
|
||||
//! JSON-RPC message per line. Standard output carries the protocol alone; anything else goes to
|
||||
//! standard error.
|
||||
|
|
@ -28,7 +28,7 @@
|
|||
//! comma-separated). `--spec-root` (or `WAI_SPEC_ROOT`) is the `wai/` directory whose
|
||||
//! specification text and conformance corpora the tools read; by default the one this binary
|
||||
//! was built from, when it is still there. `--room-bin` (or `WAI_ROOM_BIN`) is the room service
|
||||
//! program the development room runs (`joule-moq-room`); by default the one beside this binary,
|
||||
//! program the development room runs (`wai-room`); by default the one beside this binary,
|
||||
//! then on `PATH`. `--project` is the directory the server may write in (the packaging tool's
|
||||
//! envelopes), by default the one it starts in; it writes nowhere else. `--read-only` offers only
|
||||
//! the tools that read or run a verifier: nothing that writes a file, starts a process that
|
||||
|
|
@ -210,8 +210,16 @@ fn usage() -> String {
|
|||
)
|
||||
}
|
||||
|
||||
/// `wai mcp`: serve until standard input closes.
|
||||
pub fn main(args: &[String]) -> Result<bool, String> {
|
||||
/// What `wai mcp` is started with.
|
||||
struct Options {
|
||||
toolsets: Vec<tools::Toolset>,
|
||||
spec_root: Option<PathBuf>,
|
||||
room_bin: Option<PathBuf>,
|
||||
project: PathBuf,
|
||||
read_only: bool,
|
||||
}
|
||||
|
||||
fn options(args: &[String]) -> Result<Options, String> {
|
||||
let mut toolsets: Vec<tools::Toolset> = Vec::new();
|
||||
let mut spec_root = None;
|
||||
let mut room_bin = None;
|
||||
|
|
@ -244,8 +252,32 @@ pub fn main(args: &[String]) -> Result<bool, String> {
|
|||
toolsets.push(tools::Toolset::Core);
|
||||
}
|
||||
let project = project.map_or_else(std::env::current_dir, Ok).and_then(|p| p.canonicalize()).map_err(|e| format!("the project directory: {e}"))?;
|
||||
Ok(Options { toolsets, spec_root, room_bin, project, read_only })
|
||||
}
|
||||
|
||||
/// `wai mcp --dry-run`: the tools it would serve, and nothing served (no bridge started).
|
||||
pub fn plan(args: &[String]) -> Result<bool, String> {
|
||||
let o = options(args)?;
|
||||
let sets: Vec<&str> = o.toolsets.iter().map(|t| t.name()).collect();
|
||||
let server = Server::new(o.toolsets.clone(), o.spec_root, o.room_bin, o.project.clone(), o.read_only);
|
||||
let names: Vec<String> = tools::offered(&server).map(|t| t.name()).collect();
|
||||
if crate::dx::envelope::ctx().json {
|
||||
let data = json!({"toolsets": sets, "read_only": o.read_only, "project": o.project.display().to_string(), "tools": names});
|
||||
crate::dx::envelope::print(&crate::dx::envelope::envelope_with("mcp.start", true, data, Some(Vec::new())));
|
||||
} else {
|
||||
println!("wai mcp --dry-run: nothing served. On standard input and output it would serve the toolsets {}{}, {} tools:",
|
||||
sets.join(", "), if o.read_only { " read-only" } else { "" }, names.len());
|
||||
println!(" {}", names.join(", "));
|
||||
println!(" writing only inside {}", o.project.display());
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// `wai mcp`: serve until standard input closes.
|
||||
pub fn main(args: &[String]) -> Result<bool, String> {
|
||||
let o = options(args)?;
|
||||
#[cfg_attr(not(feature = "app"), allow(unused_mut))]
|
||||
let mut server = Server::new(toolsets, spec_root, room_bin, project, read_only);
|
||||
let mut server = Server::new(o.toolsets, o.spec_root, o.room_bin, o.project, o.read_only);
|
||||
#[cfg(feature = "app")]
|
||||
if server.toolsets.contains(&tools::Toolset::App) && !server.read_only {
|
||||
let b = wai_app::bridge::Bridge::start(0).map_err(|e| format!("the bridge for Create pages: {e}"))?;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
//! The tools `wai mcp` serves, in one table ([`TOOLS`]): each tool's operation (its name in the
|
||||
//! toolchain's catalogue), its toolset, title, one-line description, input schema, its safety
|
||||
//! hints and the function that runs it. A tool's name is its operation's, with `_` for the dot
|
||||
//! (`wai::ops::mcp_tool_name`); nothing else names it.
|
||||
//! (`wai::catalogue::Op::names`); nothing else names it. Its tests hold this table to the
|
||||
//! catalogue both ways: every tool is an operation the catalogue says `wai mcp` serves, in the
|
||||
//! catalogue's toolset, and every such operation is a tool here.
|
||||
|
||||
#[cfg(feature = "app")]
|
||||
use super::app;
|
||||
|
|
@ -76,7 +78,7 @@ pub(crate) struct Tool {
|
|||
impl Tool {
|
||||
/// The tool's name: its operation's, with `_` for the dot.
|
||||
pub(crate) fn name(&self) -> String {
|
||||
wai::ops::mcp_tool_name(self.op)
|
||||
wai::catalogue::op(self.op).map_or_else(|| self.op.replace('.', "_"), |o| o.names().mcp)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -90,7 +92,7 @@ pub(crate) const TOOLS: &[Tool] = &[
|
|||
description: "One capability's registry entry, whether this build decodes it natively, whether it is dispatched standalone, companion-only or a candidate, and the specification lines that name it." },
|
||||
Tool { op: "receipt.verify", toolset: Toolset::Core, title: "Verify a receipt, a claim, a room session or an audit bundle", effect: Effect::Reads, schema: s_verify, run: receipt_verify,
|
||||
description: "Verify, by kind (given, or read from the arguments): a JWP delivery receipt (jwp-receipts §2.2 to §3, a group receipt or an object receipt against its group), a claim receipt (§2.4, §3.1, with the context given), a live-room session (live-rooms §9) or a stream audit bundle (stream-audit §9, §10). The verifier's verdict, and the step and reason code it refuses at." },
|
||||
Tool { op: "package.build", toolset: Toolset::Core, title: "Package a media file", effect: Effect::Writes, schema: s_package, run: package_build,
|
||||
Tool { op: "envelope.wrap", toolset: Toolset::Core, title: "Package a media file", effect: Effect::Writes, schema: s_package, run: package_build,
|
||||
description: "Carry a media file's bytes in a WAI envelope under a registered capability, with an optional fallback or companions, as `wai wrap` does and under the same rules; writes the envelope inside the project directory and returns what it holds." },
|
||||
Tool { op: "conformance.list", toolset: Toolset::Core, title: "List conformance corpora", effect: Effect::Reads, schema: s_none, run: conformance_list,
|
||||
description: "The conformance corpora in the specification tree and the independent verifier that checks each, as CI runs them." },
|
||||
|
|
@ -108,7 +110,7 @@ pub(crate) const TOOLS: &[Tool] = &[
|
|||
description: "DEVELOPMENT ONLY. On the target local: enter the development room as a test principal with its grant and a proof of its key, publish its audio and video tracks with a few objects, and wait until the room's clock has signed its join and publishes." },
|
||||
Tool { op: "room.leave", toolset: Toolset::Dev, title: "Leave as a test principal", effect: Effect::DevRoom, schema: s_room_principal, run: dev_room::leave,
|
||||
description: "DEVELOPMENT ONLY. On the target local: close a test principal's session in the development room and wait for the clock's leave." },
|
||||
Tool { op: "room.session", toolset: Toolset::Dev, title: "Read the development room's session", effect: Effect::DevRoom, schema: s_target, run: dev_room::session,
|
||||
Tool { op: "session.show", toolset: Toolset::Dev, title: "Read the development room's session", effect: Effect::DevRoom, schema: s_target, run: dev_room::session,
|
||||
description: "DEVELOPMENT ONLY. On the target local: the development room as a verifier is given it (descriptor, grants, key-log head and the clock's event chain), ready for receipt_verify." },
|
||||
#[cfg(feature = "app")]
|
||||
Tool { op: "app.guide", toolset: Toolset::App, title: "The guide to writing a WAI app", effect: Effect::Reads, schema: s_none, run: app::guide,
|
||||
|
|
@ -264,7 +266,7 @@ fn s_target() -> Value {
|
|||
fn s_dev_start() -> Value {
|
||||
obj(
|
||||
json!({
|
||||
"room_bin": {"type": "string", "description": "The room service program (joule-moq-room); default --room-bin, WAI_ROOM_BIN, beside wai, then PATH."},
|
||||
"room_bin": {"type": "string", "description": "The room service program (wai-room); default --room-bin, WAI_ROOM_BIN, beside wai, then PATH."},
|
||||
"publishers": {"type": "integer", "minimum": 1, "maximum": 64, "description": "Publishers at once (default 4)."}
|
||||
}),
|
||||
&[],
|
||||
|
|
@ -964,7 +966,9 @@ mod tests {
|
|||
fn every_tool_is_an_operation_named_by_the_rule_once() {
|
||||
let mut seen = std::collections::BTreeSet::new();
|
||||
for t in TOOLS {
|
||||
assert_eq!(wai::ops::check_id(t.op), Ok(()), "{}", t.op);
|
||||
let op = wai::catalogue::op(t.op).unwrap_or_else(|| panic!("the tool {} serves {}, which the operation catalogue does not hold", t.name(), t.op));
|
||||
assert!(op.mcp, "{}: the catalogue does not say wai mcp serves it", t.op);
|
||||
assert_eq!(op.toolset.label(), t.toolset.name(), "{}: the catalogue's toolset", t.op);
|
||||
assert!(seen.insert(t.op), "{} twice", t.op);
|
||||
assert_eq!(t.name(), t.op.replace('.', "_"));
|
||||
assert!(!t.description.contains('\n'), "{}: one line", t.op);
|
||||
|
|
@ -975,4 +979,36 @@ mod tests {
|
|||
assert!(TOOLS.iter().any(|x| x.toolset == *t), "{}: a toolset with no tool", t.name());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_operation_the_catalogue_says_wai_mcp_serves_is_a_tool() {
|
||||
// An operation of a toolset this build does not hold (`app` without the feature) is not
|
||||
// asked for here.
|
||||
for o in wai::catalogue::OPS.iter().filter(|o| o.mcp && o.available()) {
|
||||
assert!(TOOLS.iter().any(|t| t.op == o.id), "the catalogue says wai mcp serves {}, and no tool does", o.id);
|
||||
}
|
||||
}
|
||||
|
||||
/// An operation wai mcp alone carries takes the catalogue's input: the tool's arguments
|
||||
/// are the catalogue's, the required ones too.
|
||||
#[test]
|
||||
fn a_tool_of_mcp_alone_takes_the_catalogues_input() {
|
||||
for t in TOOLS {
|
||||
let o = wai::catalogue::op(t.op).expect("a catalogue operation");
|
||||
if o.cli.is_some() || o.route.is_some() {
|
||||
continue;
|
||||
}
|
||||
let schema = (t.schema)();
|
||||
let mut have: Vec<&str> = schema["properties"].as_object().unwrap().keys().map(String::as_str).collect();
|
||||
let mut want: Vec<&str> = o.input.iter().map(|a| a.name).collect();
|
||||
have.sort();
|
||||
want.sort();
|
||||
assert_eq!(have, want, "{}: the tool's arguments are the catalogue's input", t.op);
|
||||
let mut req: Vec<&str> = schema["required"].as_array().unwrap().iter().map(|r| r.as_str().unwrap()).collect();
|
||||
let mut want_req: Vec<&str> = o.input.iter().filter(|a| a.required).map(|a| a.name).collect();
|
||||
req.sort();
|
||||
want_req.sort();
|
||||
assert_eq!(req, want_req, "{}: the required arguments", t.op);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
135
wai/wai-rs/src/bin/wai_cli/out.rs
Normal file
135
wai/wai-rs/src/bin/wai_cli/out.rs
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
//! Every file the `wai` command line writes, makes or removes goes through here, so that
|
||||
//! `--dry-run` writes nothing: each call is recorded in the plan instead, and the plan is
|
||||
//! what the command reports it would have done. A request that would change a service the
|
||||
//! command line names (a fleet service) is named in the plan the same way, and not sent.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use std::io::{self, Write};
|
||||
use std::path::Path;
|
||||
use std::sync::Mutex;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
static DRY_RUN: AtomicBool = AtomicBool::new(false);
|
||||
static PLAN: Mutex<Vec<Value>> = Mutex::new(Vec::new());
|
||||
|
||||
/// Turn dry running on or off for the rest of the process.
|
||||
pub fn set_dry_run(on: bool) {
|
||||
DRY_RUN.store(on, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
/// Whether this run writes nothing.
|
||||
pub fn dry_run() -> bool {
|
||||
DRY_RUN.load(Ordering::SeqCst)
|
||||
}
|
||||
|
||||
fn note(v: Value) {
|
||||
PLAN.lock().unwrap_or_else(|e| e.into_inner()).push(v);
|
||||
}
|
||||
|
||||
/// What a dry run would have done, in order: `{"write": path, "bytes": n}`,
|
||||
/// `{"create-dir": path}`, `{"remove": path}`, `{"request": "METHOD /path", "target": url}`.
|
||||
pub fn plan() -> Vec<Value> {
|
||||
PLAN.lock().unwrap_or_else(|e| e.into_inner()).clone()
|
||||
}
|
||||
|
||||
fn shown(p: &Path) -> String {
|
||||
p.display().to_string()
|
||||
}
|
||||
|
||||
/// A file a dry run would write whose bytes it does not make (a record signed only after a
|
||||
/// run): named in the plan, in a dry run alone.
|
||||
#[cfg_attr(not(feature = "quickstart"), allow(dead_code))]
|
||||
pub fn would_write(path: impl AsRef<Path>) {
|
||||
let p = path.as_ref();
|
||||
if dry_run() {
|
||||
note(json!({"write": shown(p), "replaces": p.exists()}));
|
||||
}
|
||||
}
|
||||
|
||||
/// A request a dry run would send to the service at `target` (and does not): named in the
|
||||
/// plan, in a dry run alone.
|
||||
pub fn would_send(target: &str, method: &str, path: &str) {
|
||||
if dry_run() {
|
||||
note(json!({"request": format!("{method} {path}"), "target": target}));
|
||||
}
|
||||
}
|
||||
|
||||
/// `std::fs::write`.
|
||||
pub fn write(path: impl AsRef<Path>, bytes: impl AsRef<[u8]>) -> io::Result<()> {
|
||||
let (p, b) = (path.as_ref(), bytes.as_ref());
|
||||
if dry_run() {
|
||||
note(json!({"write": shown(p), "bytes": b.len(), "replaces": p.exists()}));
|
||||
return Ok(());
|
||||
}
|
||||
std::fs::write(p, b)
|
||||
}
|
||||
|
||||
/// A new file that must not exist yet (`create_new`), with its bytes.
|
||||
pub fn write_new(path: impl AsRef<Path>, bytes: impl AsRef<[u8]>) -> io::Result<()> {
|
||||
let (p, b) = (path.as_ref(), bytes.as_ref());
|
||||
if dry_run() {
|
||||
if p.exists() {
|
||||
return Err(io::Error::new(io::ErrorKind::AlreadyExists, "exists"));
|
||||
}
|
||||
note(json!({"write": shown(p), "bytes": b.len(), "replaces": false}));
|
||||
return Ok(());
|
||||
}
|
||||
let mut f = std::fs::OpenOptions::new().write(true).create_new(true).open(p)?;
|
||||
f.write_all(b)
|
||||
}
|
||||
|
||||
/// `std::fs::create_dir_all`.
|
||||
pub fn create_dir_all(path: impl AsRef<Path>) -> io::Result<()> {
|
||||
let p = path.as_ref();
|
||||
if dry_run() {
|
||||
if !p.is_dir() {
|
||||
note(json!({"create-dir": shown(p)}));
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
std::fs::create_dir_all(p)
|
||||
}
|
||||
|
||||
/// `std::fs::remove_file`.
|
||||
#[cfg_attr(not(feature = "quickstart"), allow(dead_code))]
|
||||
pub fn remove_file(path: impl AsRef<Path>) -> io::Result<()> {
|
||||
let p = path.as_ref();
|
||||
if dry_run() {
|
||||
note(json!({"remove": shown(p)}));
|
||||
return Ok(());
|
||||
}
|
||||
std::fs::remove_file(p)
|
||||
}
|
||||
|
||||
/// `std::fs::File::create`, as a writer: one that discards in a dry run.
|
||||
#[cfg_attr(not(feature = "codecs"), allow(dead_code))]
|
||||
pub fn create(path: impl AsRef<Path>) -> io::Result<Box<dyn Write>> {
|
||||
let p = path.as_ref();
|
||||
if dry_run() {
|
||||
note(json!({"write": shown(p), "replaces": p.exists()}));
|
||||
return Ok(Box::new(io::sink()));
|
||||
}
|
||||
Ok(Box::new(std::fs::File::create(p)?))
|
||||
}
|
||||
|
||||
/// The plan as lines, for a person.
|
||||
pub fn plan_lines() -> Vec<String> {
|
||||
plan()
|
||||
.iter()
|
||||
.map(|v| {
|
||||
if let Some(p) = v["write"].as_str() {
|
||||
let n = v["bytes"].as_u64().map(|n| format!(" ({n} bytes)")).unwrap_or_default();
|
||||
let r = if v["replaces"] == true { ", replacing the file there" } else { "" };
|
||||
format!("write {p}{n}{r}")
|
||||
} else if let Some(p) = v["create-dir"].as_str() {
|
||||
format!("make the directory {p}")
|
||||
} else if let Some(p) = v["remove"].as_str() {
|
||||
format!("remove {p}")
|
||||
} else if let (Some(r), Some(t)) = (v["request"].as_str(), v["target"].as_str()) {
|
||||
format!("send {r} to {t}")
|
||||
} else {
|
||||
v.to_string()
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
|
@ -204,7 +204,7 @@ fn build(args: &[String]) -> Result<(), String> {
|
|||
let bytes = std::fs::read(input).map_err(|e| format!("read {input}: {e}"))?;
|
||||
let s = from_notes_json(&bytes).map_err(|e| format!("{input}: refused: {e}"))?;
|
||||
let wscr = s.to_bytes();
|
||||
std::fs::write(out, &wscr).map_err(|e| format!("write {out}: {e}"))?;
|
||||
crate::out::write(out, &wscr).map_err(|e| format!("write {out}: {e}"))?;
|
||||
let score_hash = hex(&s.score_hash());
|
||||
if args.iter().any(|a| a == "--json") {
|
||||
println!("{}", serde_json::json!({
|
||||
|
|
@ -224,7 +224,7 @@ fn render(args: &[String]) -> Result<(), String> {
|
|||
let pcm_sha256 = wai::pin::sha256_hex(&pcm);
|
||||
let (mixdown, score_hash) = (hex(&s.mixdown_hash()), hex(&s.score_hash()));
|
||||
if let Some(p) = opt(args, "--pcm") {
|
||||
std::fs::write(p, &pcm).map_err(|e| format!("write {p}: {e}"))?;
|
||||
crate::out::write(p, &pcm).map_err(|e| format!("write {p}: {e}"))?;
|
||||
}
|
||||
if args.iter().any(|a| a == "--json") {
|
||||
println!("{}", serde_json::json!({
|
||||
|
|
|
|||
|
|
@ -5,16 +5,16 @@
|
|||
//! documented (`TARGETS`).
|
||||
//!
|
||||
//! ```text
|
||||
//! wai setup-agent [--dir <project>] [--dry-run] [--confirm] [--json] [--all] [--only <file>[,<file>...]]
|
||||
//! wai setup-agent [--dir <project>] [--dry-run] [--yes] [--json] [--all] [--only <file>[,<file>...]]
|
||||
//! [--toolset <set>[,<set>...]] [--command <program>]
|
||||
//! ```
|
||||
//!
|
||||
//! - It writes nothing outside the project directory: every path is checked against the
|
||||
//! directory's canonical path, and a link among the files it would write is refused.
|
||||
//! - A file that does not exist is created. A file that exists is changed only after its diff is
|
||||
//! shown and the change is accepted: asked for on a terminal, or given with `--confirm`.
|
||||
//! Without a terminal and without `--confirm`, nothing that exists is changed and the command
|
||||
//! exits 4, with every proposed change and the command that confirms them.
|
||||
//! shown and the change is accepted: asked for on a terminal, or given with `--yes` (also
|
||||
//! `--confirm`). Without a terminal and without `--yes`, nothing that exists is changed and the
|
||||
//! command exits 4, with every proposed change and the command that confirms them.
|
||||
//! - `--dry-run` shows what would change and writes nothing.
|
||||
//! - In Markdown it owns one block between two markers, which it adds or replaces; the rest of
|
||||
//! the file is kept. In JSON it sets one member, `wai`, under the file's server list; the
|
||||
|
|
@ -22,20 +22,20 @@
|
|||
//! left alone, and the entry to add by hand is printed.
|
||||
//! - `--toolset` names the toolsets the registered server offers (`wai mcp --toolset ...`);
|
||||
//! without it the server offers its default, `core`.
|
||||
//! - Output is text on a terminal, and otherwise (or with `--json`) the toolchain's JSON
|
||||
//! envelope (`{"wai": "envelope/1", "op": "agent.setup", "ok", ...}`).
|
||||
//! - With `--json`, the toolchain's JSON envelope (`{"wai": "envelope/1", "op":
|
||||
//! "agent.setup", "ok", "data" | "error", ...}`); text otherwise.
|
||||
//! - `wai` reads `--dry-run`, `--yes` and `--json` wherever they are on the command line (the
|
||||
//! agent rules every command keeps, `agent.rs`), and hands them on here.
|
||||
//!
|
||||
//! Exit status, the toolchain's table: 0 done (or a dry run); 2 a usage error; 3 a file it may
|
||||
//! not read or write; 4 changes to existing files wait for confirmation; 5 the project
|
||||
//! directory is not there; 7 a file could not be read or written otherwise.
|
||||
|
||||
use crate::dx::envelope::OpError;
|
||||
use serde_json::{json, Map, Value};
|
||||
use std::io::{BufRead, IsTerminal, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// The command's usage, for `wai`'s own.
|
||||
pub const USAGE: &str = "wai setup-agent [--dir D] [--dry-run] [--confirm] [--json] [--all] [--only FILE,...] [--toolset core,dev,app]\n write AGENTS.md (and an import of it in a CLAUDE.md already there) and register `wai mcp`\n in the project's agent configuration";
|
||||
|
||||
/// The operation, as the catalogue names it.
|
||||
const OP: &str = "agent.setup";
|
||||
|
||||
|
|
@ -104,7 +104,7 @@ struct Plan {
|
|||
}
|
||||
|
||||
fn usage() -> &'static str {
|
||||
"usage: wai setup-agent [--dir <project>] [--dry-run] [--confirm] [--json] [--all] [--only <file>[,<file>...]] [--toolset <set>[,<set>...]] [--command <program>]"
|
||||
"usage: wai setup-agent [--dir <project>] [--dry-run] [--yes] [--json] [--all] [--only <file>[,<file>...]] [--toolset <set>[,<set>...]] [--command <program>]"
|
||||
}
|
||||
|
||||
/// The toolsets this build's `wai mcp` serves, for `--toolset`.
|
||||
|
|
@ -403,18 +403,35 @@ fn write(project: &Path, path: &str, text: &str) -> Result<(), Fail> {
|
|||
/// The toolchain's JSON envelope for this command (`envelope/1`).
|
||||
fn envelope(ok: bool, fail: Option<&Fail>) -> Map<String, Value> {
|
||||
let mut v = Map::new();
|
||||
v.insert("wai".into(), json!("envelope/1"));
|
||||
v.insert("wai".into(), json!(wai::catalogue::ENVELOPE));
|
||||
v.insert("op".into(), json!(OP));
|
||||
v.insert("ok".into(), json!(ok));
|
||||
if let Some(f) = fail {
|
||||
v.insert("error".into(), json!({"code": f.code, "message": f.message, "hint": f.hint, "retryable": false}));
|
||||
v.insert("exit".into(), json!(f.exit));
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
/// `wai setup-agent` (`agent.setup`), through the catalogue's door: the agent flags `wai` read
|
||||
/// are handed on, and the command reports its own outcome.
|
||||
pub fn main(args: &[String]) -> Result<bool, OpError> {
|
||||
let c = crate::dx::envelope::ctx();
|
||||
let mut a = args.to_vec();
|
||||
for (on, flag) in [(c.dry_run, "--dry-run"), (c.yes, "--confirm"), (c.json, "--json")] {
|
||||
if on {
|
||||
a.push(flag.into());
|
||||
}
|
||||
}
|
||||
match run(&a) {
|
||||
0 => Ok(true),
|
||||
code => Err(OpError::reported(code)),
|
||||
}
|
||||
}
|
||||
|
||||
/// `wai setup-agent`: its exit status, its output written.
|
||||
pub fn main(args: &[String]) -> u8 {
|
||||
let json_out = args.iter().any(|a| a == "--json") || !std::io::stdout().is_terminal();
|
||||
fn run(args: &[String]) -> u8 {
|
||||
let json_out = args.iter().any(|a| a == "--json");
|
||||
let fail = |f: Fail| {
|
||||
if json_out {
|
||||
println!("{}", Value::Object(envelope(false, Some(&f))));
|
||||
|
|
@ -486,7 +503,7 @@ pub fn main(args: &[String]) -> u8 {
|
|||
f.insert("action".into(), json!(action));
|
||||
files.push(Value::Object(f));
|
||||
}
|
||||
let confirm = format!("wai setup-agent --dir {} --confirm{}{}{}", shell_word(&project.display().to_string()),
|
||||
let confirm = format!("wai setup-agent --dir {} --yes{}{}{}", shell_word(&project.display().to_string()),
|
||||
o.only.as_ref().map(|x| format!(" --only {}", x.join(","))).unwrap_or_default(),
|
||||
o.toolsets.as_ref().map(|x| format!(" --toolset {}", x.join(","))).unwrap_or_default(),
|
||||
if o.all { " --all" } else { "" });
|
||||
|
|
@ -495,13 +512,20 @@ pub fn main(args: &[String]) -> u8 {
|
|||
exit: CONFIRMATION_REQUIRED,
|
||||
code: "confirmation-required",
|
||||
message: format!("{} change{} to existing files", proposed.len(), if proposed.len() == 1 { "" } else { "s" }),
|
||||
hint: "read each diff, then re-run with --confirm",
|
||||
hint: "read each diff, then re-run with --yes",
|
||||
};
|
||||
let stop = failed.as_ref().or(if waiting { Some(&pending) } else { None });
|
||||
let code = stop.map_or(0, |f| f.exit);
|
||||
if json_out {
|
||||
let mut v = envelope(code == 0, stop);
|
||||
v.insert("result".into(), json!({"project": project.display().to_string(), "dry_run": o.dry_run, "files": files}));
|
||||
v.insert("data".into(), json!({"project": project.display().to_string(), "dry_run": o.dry_run, "files": files}));
|
||||
if o.dry_run {
|
||||
// The envelope's dry-run members, as every command gives them.
|
||||
let plan: Vec<Value> = files.iter().filter(|f| f["action"] == "would_create" || f["action"] == "would_update")
|
||||
.map(|f| json!({"write": f["path"], "replaces": f["action"] == "would_update"})).collect();
|
||||
v.insert("dry_run".into(), json!(true));
|
||||
v.insert("plan".into(), json!(plan));
|
||||
}
|
||||
if waiting {
|
||||
v.insert("proposed".into(), json!(proposed));
|
||||
v.insert("confirm".into(), json!(confirm));
|
||||
|
|
@ -521,6 +545,9 @@ pub fn main(args: &[String]) -> u8 {
|
|||
if waiting {
|
||||
println!("existing files would change and nothing confirmed it; to apply: {confirm}");
|
||||
}
|
||||
if o.dry_run {
|
||||
println!("dry run: nothing was written");
|
||||
}
|
||||
if let Some(f) = &failed {
|
||||
eprintln!("error: {}\nhint: {}", f.message, f.hint);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -464,7 +464,7 @@ fn base_cmd(args: &[String]) -> Result<(), String> {
|
|||
if units.len() != recon.len() || units.iter().zip(&recon).any(|(u, r)| u.samples != Samples::U8(r.clone())) {
|
||||
return Err("the base does not decode to the encoder's reconstruction".into());
|
||||
}
|
||||
std::fs::write(out, &bytes).map_err(|e| format!("write {out}: {e}"))?;
|
||||
crate::out::write(out, &bytes).map_err(|e| format!("write {out}: {e}"))?;
|
||||
let err = frames.iter().flatten().zip(recon.iter().flatten()).map(|(a, b)| a.abs_diff(*b)).max().unwrap_or(0);
|
||||
println!("base → {out} ({} B) {cap} {n} frames of {h}x{w} q {q} tier {} largest error {err}", bytes.len(), tier::token(info.tier));
|
||||
println!(" content hash {}", hex(&info.content_hash));
|
||||
|
|
@ -572,7 +572,7 @@ fn refine(args: &[String]) -> Result<(), String> {
|
|||
let env = Wai::new(m, payload);
|
||||
let bytes = env.to_emit_bytes().map_err(|e| e.to_string())?;
|
||||
env.check_component_roles().map_err(|e| e.to_string())?;
|
||||
std::fs::write(out, &bytes).map_err(|e| format!("write {out}: {e}"))?;
|
||||
crate::out::write(out, &bytes).map_err(|e| format!("write {out}: {e}"))?;
|
||||
let err: u64 = recon.iter().zip(&targets).map(|(r, t)| match (r, t) {
|
||||
(Next::U8(a), Samples::U8(b)) => a.iter().zip(b).map(|(x, y)| (*x as i64 - *y as i64).unsigned_abs()).max().unwrap_or(0),
|
||||
(Next::I16(a), Samples::I16(b)) => a.iter().zip(b).map(|(x, y)| (*x as i64 - *y as i64).unsigned_abs()).max().unwrap_or(0),
|
||||
|
|
@ -637,7 +637,7 @@ fn pack(args: &[String]) -> Result<(), String> {
|
|||
let bytes = WaiMulti::pack(&refs).map_err(|e| e.to_string())?;
|
||||
let w = WaiMulti::from_bytes(&bytes).map_err(|e| e.to_string())?;
|
||||
w.check_component_roles().map_err(|e| e.to_string())?;
|
||||
std::fs::write(out, &bytes).map_err(|e| format!("write {out}: {e}"))?;
|
||||
crate::out::write(out, &bytes).map_err(|e| format!("write {out}: {e}"))?;
|
||||
println!("packed {} envelopes → {out} ({} B)", files.len(), bytes.len());
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -659,7 +659,7 @@ fn explode(args: &[String]) -> Result<(), String> {
|
|||
let bytes = WaiMulti::explode(&envelope, i).map_err(|e| e.to_string())?;
|
||||
println!("component {i}: {} B content hash {}", bytes.len(), hex(&wai::jwp_hash::content_hash(&bytes)));
|
||||
if let Some(out) = opt(args, "-o") {
|
||||
std::fs::write(out, &bytes).map_err(|e| format!("write {out}: {e}"))?;
|
||||
crate::out::write(out, &bytes).map_err(|e| format!("write {out}: {e}"))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,12 @@ use std::process::ExitCode;
|
|||
#[path = "wai_cli/http_vectors.rs"]
|
||||
mod http_vectors;
|
||||
|
||||
// The writes `http_vectors` makes go through `out`, as they do in `wai http vectors`, which
|
||||
// takes `--dry-run`; this tool never sets it.
|
||||
#[allow(dead_code)]
|
||||
#[path = "wai_cli/out.rs"]
|
||||
mod out;
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||
match http_vectors::main(&args) {
|
||||
|
|
|
|||
2178
wai/wai-rs/src/catalogue.rs
Normal file
2178
wai/wai-rs/src/catalogue.rs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -20,6 +20,10 @@
|
|||
//! dispatch table knows them; the actual neural model runs in whatever
|
||||
//! ML runtime the sink has installed (PyTorch / Core ML / ONNX / etc.).
|
||||
|
||||
// The operation catalogue: every operation the toolchain offers, `noun.verb`, as data every
|
||||
// door (the CLI, the C ABI, the SDKs, MCP, the HTTP API) is named from and checked against.
|
||||
// Pure data; no feature.
|
||||
pub mod catalogue;
|
||||
pub mod codecs;
|
||||
pub mod container;
|
||||
// Platform decode capabilities (extensions/platform-decode.md): the query a sink makes of
|
||||
|
|
@ -43,9 +47,6 @@ pub mod pin;
|
|||
// The reader limits the extension formats share with SPEC §2: how many samples, counting
|
||||
// every channel, and how many frames a reader must be willing to render. Always on.
|
||||
pub mod limits;
|
||||
// The rule the toolchain's operation names keep (noun.verb, a closed list of verbs) and the
|
||||
// forms the doors derive from a name: an MCP tool's, an OpenAPI operationId. Always on.
|
||||
pub mod ops;
|
||||
#[cfg(feature = "ffi")]
|
||||
pub mod ffi;
|
||||
#[cfg(feature = "codecs")]
|
||||
|
|
|
|||
|
|
@ -1,66 +0,0 @@
|
|||
//! The rule the toolchain's operation names keep (WAI toolchain v1, naming rule N3), and the
|
||||
//! mechanical forms the doors derive from a name.
|
||||
//!
|
||||
//! An operation is named `noun.verb`: both parts lowercase ASCII letters, the noun singular
|
||||
//! (a compound written as one word, as `keylog` is), the verb from a closed list ([`VERBS`]).
|
||||
//! A near-synonym is not a second verb: `get`, `describe` and `info` are `show`; `keygen`,
|
||||
//! `init` and `new` are `create`. The catalogue names a few operations whose last word is not on
|
||||
//! the list ([`NAMED_AS_IS`]); they keep the names it gives them.
|
||||
//!
|
||||
//! Each door takes its form from the name, never a name of its own: an OpenAPI operation's
|
||||
//! `operationId` is the name itself, and an MCP tool's name is the name with `_` for the dot
|
||||
//! ([`mcp_tool_name`]). `wai mcp`'s tools and the room service's API description are held to
|
||||
//! this rule by their tests.
|
||||
|
||||
/// The verbs an operation's name may end with.
|
||||
pub const VERBS: &[&str] = &[
|
||||
"create", "list", "show", "update", "delete", "check", "verify", "run", "build", "export", "import", "start", "stop", "join",
|
||||
"leave", "grant", "invite", "revoke", "rotate", "tail", "search", "test", "load", "encode", "decode", "inspect", "wrap", "unwrap",
|
||||
"fetch", "generate", "promote", "rollback", "lock", "upgrade", "setup", "guide", "stream",
|
||||
];
|
||||
|
||||
/// Names the catalogue gives as they are, though their last word is not on the list: the
|
||||
/// catalogue's table is the authority on its own names.
|
||||
pub const NAMED_AS_IS: &[&str] = &[
|
||||
"room.session", "agent.mcp", "toolchain.hello", "toolchain.doctor", "ops.schema", "edge.derive", "meter.calibrate", "audit.report",
|
||||
];
|
||||
|
||||
/// `Ok` when `id` is an operation's name by the rule (or one the catalogue gives as it is), or
|
||||
/// why it is not.
|
||||
pub fn check_id(id: &str) -> Result<(), String> {
|
||||
if NAMED_AS_IS.contains(&id) {
|
||||
return Ok(());
|
||||
}
|
||||
let Some((noun, verb)) = id.split_once('.') else {
|
||||
return Err(format!("{id:?}: an operation is named noun.verb"));
|
||||
};
|
||||
let letters = |s: &str| !s.is_empty() && s.bytes().all(|b| b.is_ascii_lowercase());
|
||||
if !letters(noun) || !letters(verb) {
|
||||
return Err(format!("{id:?}: the noun and the verb are lowercase ASCII letters, one dot between them"));
|
||||
}
|
||||
if !VERBS.contains(&verb) {
|
||||
return Err(format!("{id:?}: {verb} is not one of the verbs an operation ends with ({})", VERBS.join(", ")));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The MCP tool an operation is served as: its name with `_` for the dot.
|
||||
pub fn mcp_tool_name(id: &str) -> String {
|
||||
id.replace('.', "_")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn names_keep_the_rule() {
|
||||
for ok in ["room.grant", "envelope.inspect", "keylog.verify", "dev.start", "room.session"] {
|
||||
assert_eq!(check_id(ok), Ok(()), "{ok}");
|
||||
}
|
||||
for no in ["room", "room.get", "Room.grant", "room.grant.now", "room_grant", "room.", ".grant", "key-log.verify", "room.describe", "call.session"] {
|
||||
assert!(check_id(no).is_err(), "{no}");
|
||||
}
|
||||
assert_eq!(mcp_tool_name("receipt.verify"), "receipt_verify");
|
||||
}
|
||||
}
|
||||
|
|
@ -116,8 +116,8 @@ fn an_export_is_an_ordinary_project_and_its_check_holds() {
|
|||
assert!(std::fs::read_to_string(p.join("app.oui")).unwrap().starts_with("root = App("));
|
||||
let (code, v) = app_check(&p);
|
||||
assert_eq!(code, 0, "{v}");
|
||||
assert_eq!(v["result"]["room"]["kind"], "show");
|
||||
assert!(v["result"]["failures"].as_array().unwrap().is_empty(), "{v}");
|
||||
assert_eq!(v["data"]["room"]["kind"], "show");
|
||||
assert!(v["data"]["failures"].as_array().unwrap().is_empty(), "{v}");
|
||||
let _ = std::fs::remove_dir_all(&d);
|
||||
}
|
||||
|
||||
|
|
@ -142,7 +142,7 @@ fn the_check_refuses_a_project_that_is_not_whole() {
|
|||
assert!(o.status.success());
|
||||
mutate(&b);
|
||||
let (code, v) = app_check(&b);
|
||||
let failures: String = v["result"]["failures"].as_array().unwrap().iter().map(|f| f.as_str().unwrap()).collect::<Vec<_>>().join("\n");
|
||||
let failures: String = v["data"]["failures"].as_array().unwrap().iter().map(|f| f.as_str().unwrap()).collect::<Vec<_>>().join("\n");
|
||||
assert!(code == 1 && failures.contains(expect), "a project {what}: {code} {v}");
|
||||
}
|
||||
let _ = std::fs::remove_dir_all(&d);
|
||||
|
|
@ -167,7 +167,7 @@ fn a_watch_room_names_its_file_and_its_check_wants_it() {
|
|||
assert!(std::fs::read_to_string(p.join("wai.json")).unwrap().contains("\"dev\": { \"site\": \".\", \"mode\": \"watch\", \"watch\": \"media/film.wav\" }"));
|
||||
assert!(p.join("media/README.md").is_file());
|
||||
let (code, v) = app_check(&p);
|
||||
assert!(code == 1 && v["result"]["failures"].to_string().contains("media/film.wav is not there"), "{v}");
|
||||
assert!(code == 1 && v["data"]["failures"].to_string().contains("media/film.wav is not there"), "{v}");
|
||||
std::fs::write(p.join("media/film.wav"), b"RIFF").unwrap();
|
||||
let (code, v) = app_check(&p);
|
||||
assert_eq!(code, 0, "{v}");
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ fn breaking(dir: &Path, base: &str, extra: &[&str]) -> (i32, Value) {
|
|||
}
|
||||
|
||||
fn finding<'a>(v: &'a Value, id: &str) -> Option<&'a Value> {
|
||||
v["findings"].as_array().unwrap().iter().find(|f| f["id"] == id)
|
||||
v["data"]["findings"].as_array().unwrap().iter().find(|f| f["id"] == id)
|
||||
}
|
||||
|
||||
const REGISTRY: &str = r#"{"schema": "x", "capabilities": [
|
||||
|
|
@ -135,8 +135,8 @@ fn nothing_changed_nothing_breaks() {
|
|||
let (code, v) = breaking(&d, &base, &["--skip", "rust"]);
|
||||
assert_eq!(code, 0, "{v:#}");
|
||||
assert_eq!(v["op"], "compat.check");
|
||||
assert_eq!(v["breaking"], 0);
|
||||
assert!(v["findings"].as_array().unwrap().is_empty(), "{v:#}");
|
||||
assert_eq!(v["data"]["breaking"], 0);
|
||||
assert!(v["data"]["findings"].as_array().unwrap().is_empty(), "{v:#}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -266,8 +266,8 @@ fn a_listed_measurement_is_not_compared_and_a_vector_beside_it_is() {
|
|||
write(&d, "wai/format-probe-conformance/provenance.json", r#"{"decodes": [{"host": "x86-64", "identical": false}]}"#);
|
||||
let (code, v) = breaking(&d, &base, &["--only", "corpora"]);
|
||||
assert_eq!(code, 0, "{v:#}");
|
||||
assert!(v["findings"].as_array().unwrap().is_empty(), "{v:#}");
|
||||
assert!(v["notes"].as_array().unwrap().iter().any(|n| n.as_str().unwrap().contains("provenance.json changed, a measurement")), "{v:#}");
|
||||
assert!(v["data"]["findings"].as_array().unwrap().is_empty(), "{v:#}");
|
||||
assert!(v["data"]["notes"].as_array().unwrap().iter().any(|n| n.as_str().unwrap().contains("provenance.json changed, a measurement")), "{v:#}");
|
||||
edit(&d, "wai/format-probe-conformance/vectors.json", "\"expect\": \"probe\"", "\"expect\": \"refuse\"");
|
||||
let (code, v) = breaking(&d, &base, &["--only", "corpora"]);
|
||||
assert_eq!(code, 1, "{v:#}");
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
//! - the reference origin (`wai http origin`) serving the HTTP corpus's objects conforms,
|
||||
//! and one serving an object with one byte changed fails that object;
|
||||
//! - a record changed after signing, in a case's outcome or in its summary, is refused by
|
||||
//! `wai verify`; a dry run writes nothing; a record is not replaced without `--confirm`;
|
||||
//! `wai verify`; a dry run writes nothing; a record is not replaced without `--yes`;
|
||||
//! each failure to run has its exit status.
|
||||
#![cfg(all(feature = "quickstart", feature = "score", feature = "key_discovery"))]
|
||||
|
||||
|
|
@ -40,7 +40,7 @@ fn text(o: &Output) -> String {
|
|||
/// `wai conform <kind> --suite <suite> --impl <imp> --json` writing its record to `out`.
|
||||
fn conform(dir: &Path, kind: &str, suite: &str, imp: &str, out: &Path) -> (i32, Value) {
|
||||
let root = wai_dir();
|
||||
let o = wai(dir, &["conform", kind, "--suite", suite, "--impl", imp, "--root", root.to_str().unwrap(), "--out", out.to_str().unwrap(), "--confirm", "--json"]);
|
||||
let o = wai(dir, &["conform", kind, "--suite", suite, "--impl", imp, "--root", root.to_str().unwrap(), "--out", out.to_str().unwrap(), "--yes", "--json"]);
|
||||
let v: Value = serde_json::from_slice(&o.stdout).unwrap_or_else(|_| panic!("JSON: {}", text(&o)));
|
||||
(o.status.code().unwrap_or(-1), v)
|
||||
}
|
||||
|
|
@ -56,8 +56,8 @@ fn the_reference_verifiers_conform_and_one_changed_verdict_does_not() {
|
|||
let out = d.join(format!("{suite}.wai-conform.json"));
|
||||
let (code, v) = conform(&d, "verifier", suite, &format!("{} verdicts {{corpus}}", q(bin)), &out);
|
||||
assert_eq!(code, 0, "{suite}: {v:#}");
|
||||
assert_eq!(v["conformant"], true);
|
||||
assert!(v["cases"].as_u64().unwrap() > 100, "{v}");
|
||||
assert_eq!(v["data"]["conformant"], true);
|
||||
assert!(v["data"]["cases"].as_u64().unwrap() > 100, "{v}");
|
||||
let rec: Value = serde_json::from_slice(&std::fs::read(&out).unwrap()).unwrap();
|
||||
assert_eq!(rec["wai"], "conform-result/1");
|
||||
assert!(rec["corpus"]["digest"].as_str().unwrap().starts_with("sha256:"));
|
||||
|
|
@ -72,8 +72,8 @@ fn the_reference_verifiers_conform_and_one_changed_verdict_does_not() {
|
|||
let bad = format!("{} verdicts {{corpus}} | awk 'NR == 1 {{ sub(/\\t/, \"\\t~\") }} 1'", q(env!("CARGO_BIN_EXE_wai_signer_vectors")));
|
||||
let (code, v) = conform(&d, "verifier", "signer", &bad, &d.join("bad.wai-conform.json"));
|
||||
assert_eq!(code, 1, "{v:#}");
|
||||
assert_eq!(v["conformant"], false);
|
||||
assert_eq!(v["failed"], 1, "{v:#}");
|
||||
assert_eq!(v["data"]["conformant"], false);
|
||||
assert_eq!(v["data"]["failed"], 1, "{v:#}");
|
||||
// A record that says "not conformant" is still a genuine record.
|
||||
let o = wai(&d, &["verify", d.join("bad.wai-conform.json").to_str().unwrap()]);
|
||||
assert!(o.status.success() && text(&o).contains("not conformant"), "{}", text(&o));
|
||||
|
|
@ -128,13 +128,13 @@ fn the_score_renderer_conforms_and_one_flipped_output_byte_fails() {
|
|||
let w = q(env!("CARGO_BIN_EXE_wai"));
|
||||
let (code, v) = conform(&d, "decoder", "score", &format!("{w} score render {{in}} --pcm {{out}}"), &d.join("score.wai-conform.json"));
|
||||
assert_eq!(code, 0, "{v:#}");
|
||||
assert_eq!(v["passed"], 3);
|
||||
assert_eq!(v["data"]["passed"], 3);
|
||||
// One byte of the output changed (byte 100 of the PCM, plus one).
|
||||
let flip = format!("{w} score render {{in}} --pcm {{out}} >/dev/null && dd if={{out}} bs=1 skip=100 count=1 2>/dev/null | LC_ALL=C tr '\\000-\\377' '\\001-\\377\\000' | dd of={{out}} bs=1 seek=100 count=1 conv=notrunc 2>/dev/null");
|
||||
let (code, v) = conform(&d, "decoder", "score", &flip, &d.join("flip.wai-conform.json"));
|
||||
assert_eq!(code, 1, "{v:#}");
|
||||
assert_eq!(v["failed"], 3, "{v:#}");
|
||||
assert_eq!(v["passed"], 0);
|
||||
assert_eq!(v["data"]["failed"], 3, "{v:#}");
|
||||
assert_eq!(v["data"]["passed"], 0);
|
||||
}
|
||||
|
||||
/// `wai http origin --root <root>` on a port the system picks: the child and its URL, from
|
||||
|
|
@ -176,8 +176,8 @@ fn the_reference_origin_conforms_and_one_changed_object_fails() {
|
|||
let (code, v) = conform(&d, "origin", "http", &url, &d.join("bad.wai-conform.json"));
|
||||
let _ = c.kill();
|
||||
assert_eq!(code, 1, "{v:#}");
|
||||
assert_eq!(v["failed"], 1, "{v:#}");
|
||||
assert!(v["failures"][0]["got"].as_str().unwrap().contains("the body is not the object"), "{v:#}");
|
||||
assert_eq!(v["data"]["failed"], 1, "{v:#}");
|
||||
assert!(v["data"]["failures"][0]["got"].as_str().unwrap().contains("the body is not the object"), "{v:#}");
|
||||
}
|
||||
|
||||
/// A verifier behind HTTP: the reference's verdicts, served one case a request.
|
||||
|
|
@ -234,10 +234,14 @@ fn a_dry_run_writes_nothing_and_each_failure_has_its_exit_status() {
|
|||
assert_eq!(v["dry_run"], true);
|
||||
assert!(std::fs::read_dir(&d).unwrap().next().is_none(), "a dry run writes nothing");
|
||||
|
||||
// The record exists: not replaced without --confirm (4).
|
||||
// The record exists: not replaced without --yes (4), the proposal and the confirming command
|
||||
// in the envelope.
|
||||
std::fs::write(d.join("score.wai-conform.json"), "{}").unwrap();
|
||||
let o = wai(&d, &["conform", "decoder", "--suite", "score", "--impl", &imp, "--root", r, "--json"]);
|
||||
assert_eq!(o.status.code(), Some(4), "{}", text(&o));
|
||||
let v: Value = serde_json::from_slice(&o.stdout).unwrap();
|
||||
assert_eq!((v["error"]["code"].as_str(), v["proposed"][0]["replaces"].as_bool()), (Some("confirmation-required"), Some(true)), "{v:#}");
|
||||
assert!(v["confirm"].as_str().unwrap().ends_with(" --yes"), "{v:#}");
|
||||
assert_eq!(std::fs::read(d.join("score.wai-conform.json")).unwrap(), b"{}");
|
||||
|
||||
let code = |args: &[&str]| wai(&d, args).status.code();
|
||||
|
|
@ -249,5 +253,5 @@ fn a_dry_run_writes_nothing_and_each_failure_has_its_exit_status() {
|
|||
assert_eq!(code(&["conform", "origin", "--impl", &format!("http://127.0.0.1:{free}"), "--root", r, "--out", d.join("o.wai-conform.json").to_str().unwrap()]), Some(6), "nothing listening");
|
||||
let o = wai(&d, &["conform", "--list", "--root", r, "--json"]);
|
||||
let v: Value = serde_json::from_slice(&o.stdout).unwrap();
|
||||
assert!(v["suites"].as_array().unwrap().iter().all(|s| s["cases"].as_u64().unwrap() > 0), "{v:#}");
|
||||
assert!(v["data"]["suites"].as_array().unwrap().iter().all(|s| s["cases"].as_u64().unwrap() > 0), "{v:#}");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -105,8 +105,10 @@ fn create_writes_every_example_as_it_is_in_the_repository() {
|
|||
let examples = Path::new(env!("CARGO_MANIFEST_DIR")).join("../examples");
|
||||
let o = wai(&d, &["create", "--list", "--json"]);
|
||||
assert!(o.status.success(), "{}", text(&o));
|
||||
let list: serde_json::Value = serde_json::from_slice(&o.stdout).unwrap();
|
||||
let list = list.as_array().unwrap();
|
||||
// The JSON envelope (envelope/1): the list is its data.
|
||||
let env: serde_json::Value = serde_json::from_slice(&o.stdout).unwrap();
|
||||
assert_eq!((env["ok"].as_bool(), env["op"].as_str()), (Some(true), Some("project.create")), "{env}");
|
||||
let list = env["data"].as_array().unwrap();
|
||||
let names: Vec<&str> = list.iter().map(|t| t["template"].as_str().unwrap()).collect();
|
||||
// Every example directory is a template, and every template an example.
|
||||
let mut dirs: Vec<String> = std::fs::read_dir(&examples).unwrap().flatten().filter(|e| e.path().is_dir()).map(|e| e.file_name().to_string_lossy().into_owned()).collect();
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
//! The fleet's operations through the `wai` binary (extensions/fleet.md §9), as an agent runs
|
||||
//! them, on the toolchain's rules: the JSON envelope when standard output is not a terminal, the
|
||||
//! exit statuses, `--dry-run` that writes and sends nothing, and `--confirm` before a fleet
|
||||
//! service is changed or a file replaced; a refusal of the service named by the fleet's reason;
|
||||
//! a report's bundle verified here before it is written; and `wai verify` checking a bundle
|
||||
//! offline and refusing it altered. The fleet service is played by a server in the test that
|
||||
//! answers as `wai-fleet` does.
|
||||
//! them, on the toolchain's agent rules: the JSON envelope with `--json`, the exit statuses,
|
||||
//! `--dry-run` that writes and sends nothing and names what it would, and `--yes` before a fleet
|
||||
//! service is changed or a file replaced (`--confirm`, its deprecated spelling, does the same);
|
||||
//! a refusal of the service named by the fleet's reason; a report's bundle verified here before
|
||||
//! it is written; and `wai verify` checking a bundle offline and refusing it altered. The fleet
|
||||
//! service is played by a server in the test that answers as `wai-fleet` does.
|
||||
#![cfg(feature = "quickstart")]
|
||||
|
||||
use std::io::{Read, Write};
|
||||
|
|
@ -21,11 +21,17 @@ fn tmp(name: &str) -> PathBuf {
|
|||
d
|
||||
}
|
||||
|
||||
/// `wai` run in `dir` as an agent runs it: no terminal, and `--json`.
|
||||
fn wai(dir: &Path, args: &[&str]) -> Output {
|
||||
Command::new(env!("CARGO_BIN_EXE_wai")).current_dir(dir).args(args).output().expect("wai runs")
|
||||
Command::new(env!("CARGO_BIN_EXE_wai")).current_dir(dir).args(args).arg("--json").stdin(std::process::Stdio::null()).output().expect("wai runs")
|
||||
}
|
||||
|
||||
/// The envelope a run printed (its standard output is not a terminal here).
|
||||
/// `wai` run in `dir` as a person scripts it: no terminal, text.
|
||||
fn wai_text(dir: &Path, args: &[&str]) -> Output {
|
||||
Command::new(env!("CARGO_BIN_EXE_wai")).current_dir(dir).args(args).stdin(std::process::Stdio::null()).output().expect("wai runs")
|
||||
}
|
||||
|
||||
/// The envelope a `--json` run printed.
|
||||
fn envelope(o: &Output) -> Value {
|
||||
serde_json::from_slice(&o.stdout).unwrap_or_else(|e| panic!("{e}: {}{}", String::from_utf8_lossy(&o.stdout), String::from_utf8_lossy(&o.stderr)))
|
||||
}
|
||||
|
|
@ -82,11 +88,12 @@ const NODE: &str = "111111111111111111111111111111111111111111111111111111111111
|
|||
fn enrolment_create_signs_writes_and_waits_for_confirmation() {
|
||||
let d = tmp("enrol");
|
||||
std::fs::write(d.join("org.key"), "07".repeat(32)).unwrap();
|
||||
// A dry run writes nothing.
|
||||
// A dry run writes nothing, and its plan names the file.
|
||||
let o = wai(&d, &["enrolment", "create", NODE, "--role", "relay", "--key", "org.key", "--label", "home", "--out", "n.wfe", "--dry-run"]);
|
||||
assert_eq!(o.status.code(), Some(0));
|
||||
let e = envelope(&o);
|
||||
assert_eq!((e["op"].as_str(), e["ok"].as_bool(), e["data"]["dry_run"].as_bool()), (Some("enrolment.create"), Some(true), Some(true)));
|
||||
assert_eq!((e["op"].as_str(), e["ok"].as_bool(), e["dry_run"].as_bool()), (Some("enrolment.create"), Some(true), Some(true)));
|
||||
assert_eq!((e["plan"][0]["write"].as_str(), e["plan"][0]["replaces"].as_bool()), (Some("n.wfe"), Some(false)), "{e:#}");
|
||||
assert!(!d.join("n.wfe").exists(), "a dry run writes nothing");
|
||||
// The enrolment, written: it reads and its signature verifies.
|
||||
let o = wai(&d, &["enrolment", "create", NODE, "--role", "relay", "--key", "org.key", "--label", "home", "--out", "n.wfe"]);
|
||||
|
|
@ -96,13 +103,26 @@ fn enrolment_create_signs_writes_and_waits_for_confirmation() {
|
|||
let parsed = wai::fleet::Enrolment::parse(&b).unwrap();
|
||||
assert_eq!((wai::fleet::hex(&parsed.node), parsed.role, parsed.label_text()), (NODE.to_string(), 1, "home".to_string()));
|
||||
assert_eq!(e["data"]["digest"], json!(wai::fleet::hex(&parsed.digest())));
|
||||
// Replacing it waits for --confirm: exit 4, with the command that confirms.
|
||||
// Replacing it waits for --yes: exit 4, with the proposal and the command that confirms.
|
||||
let o = wai(&d, &["enrolment", "create", NODE, "--role", "ingest", "--key", "org.key", "--out", "n.wfe"]);
|
||||
assert_eq!(o.status.code(), Some(4));
|
||||
let e = envelope(&o);
|
||||
assert_eq!(e["error"]["code"], "confirmation-required");
|
||||
assert!(e["confirm"].as_str().unwrap().ends_with("--confirm"));
|
||||
assert_eq!((e["proposed"][0]["write"].as_str(), e["proposed"][0]["replaces"].as_bool()), (Some("n.wfe"), Some(true)), "{e:#}");
|
||||
assert_eq!(e["confirm"], format!("wai enrolment create {NODE} --role ingest --key org.key --out n.wfe --yes"), "{e:#}");
|
||||
assert_eq!(std::fs::read(d.join("n.wfe")).unwrap(), b, "nothing changed");
|
||||
// In text, the same: exit 4, nothing changed, and the confirming command on standard error.
|
||||
let o = wai_text(&d, &["enrolment", "create", NODE, "--role", "ingest", "--key", "org.key", "--out", "n.wfe"]);
|
||||
assert_eq!(o.status.code(), Some(4));
|
||||
assert!(String::from_utf8_lossy(&o.stderr).contains("to go ahead: wai enrolment create"), "{}", String::from_utf8_lossy(&o.stderr));
|
||||
assert_eq!(std::fs::read(d.join("n.wfe")).unwrap(), b, "nothing changed");
|
||||
// --yes replaces it; --confirm, the deprecated spelling, does the same.
|
||||
let o = wai(&d, &["enrolment", "create", NODE, "--role", "ingest", "--key", "org.key", "--out", "n.wfe", "--yes"]);
|
||||
assert_eq!(o.status.code(), Some(0), "{}", String::from_utf8_lossy(&o.stderr));
|
||||
assert_eq!(wai::fleet::Enrolment::parse(&std::fs::read(d.join("n.wfe")).unwrap()).unwrap().role, 3);
|
||||
let o = wai(&d, &["enrolment", "create", NODE, "--role", "relay", "--key", "org.key", "--label", "home", "--out", "n.wfe", "--confirm"]);
|
||||
assert_eq!(o.status.code(), Some(0), "{}", String::from_utf8_lossy(&o.stderr));
|
||||
assert_eq!(std::fs::read(d.join("n.wfe")).unwrap(), b, "--confirm went ahead as --yes does");
|
||||
// Its own rules refuse it before anything is written: a node that is the organisation.
|
||||
let org = wai::fleet::hex(&ed25519_dalek::SigningKey::from_bytes(&[7; 32]).verifying_key().to_bytes());
|
||||
let o = wai(&d, &["enrolment", "create", &org, "--role", "relay", "--key", "org.key", "--out", "self.wfe"]);
|
||||
|
|
@ -117,10 +137,13 @@ fn enrolment_create_against_a_service() {
|
|||
let d = tmp("enrol-target");
|
||||
std::fs::write(d.join("org.key"), [7u8; 32]).unwrap();
|
||||
let (url, rx) = service(vec![(201, json!({"node": NODE, "role": "relay", "digest": "00"})), (422, json!({"outcome": "refuse", "reason": "enrolment_duplicate"}))]);
|
||||
// A service is not changed without --confirm.
|
||||
// A service is not changed without --yes; a dry run names the request and sends nothing.
|
||||
let o = wai(&d, &["enrolment", "create", NODE, "--role", "relay", "--key", "org.key", "--target", &url]);
|
||||
assert_eq!(o.status.code(), Some(4));
|
||||
let o = wai(&d, &["enrolment", "create", NODE, "--role", "relay", "--key", "org.key", "--target", &url, "--confirm"]);
|
||||
assert_eq!(envelope(&o)["proposed"][0]["request"], "POST /v1/fleet/enrolments");
|
||||
let o = wai(&d, &["enrolment", "create", NODE, "--role", "relay", "--key", "org.key", "--target", &url, "--dry-run"]);
|
||||
assert_eq!((o.status.code(), envelope(&o)["plan"][0]["request"].as_str()), (Some(0), Some("POST /v1/fleet/enrolments")));
|
||||
let o = wai(&d, &["enrolment", "create", NODE, "--role", "relay", "--key", "org.key", "--target", &url, "--yes"]);
|
||||
assert_eq!(o.status.code(), Some(0), "{}", String::from_utf8_lossy(&o.stdout));
|
||||
let (line, body) = rx.recv().unwrap();
|
||||
assert_eq!(line, "POST /v1/fleet/enrolments HTTP/1.0");
|
||||
|
|
@ -128,7 +151,7 @@ fn enrolment_create_against_a_service() {
|
|||
assert!(wai::fleet::Enrolment::parse(&sent).is_ok(), "the service is given a signed enrolment");
|
||||
assert!(!d.join(format!("{}.wfe", &NODE[..16])).exists(), "with --target, a file only with --out");
|
||||
// The service's refusal is named by the fleet's reason.
|
||||
let o = wai(&d, &["enrolment", "create", NODE, "--role", "relay", "--key", "org.key", "--target", &url, "--confirm"]);
|
||||
let o = wai(&d, &["enrolment", "create", NODE, "--role", "relay", "--key", "org.key", "--target", &url, "--yes"]);
|
||||
assert_eq!((o.status.code(), envelope(&o)["error"]["code"].as_str()), (Some(1), Some("enrolment_duplicate")));
|
||||
// No service there: unavailable, and worth a retry.
|
||||
let o = wai(&d, &["enrolment", "list", "--target", "http://127.0.0.1:9"]);
|
||||
|
|
@ -154,9 +177,10 @@ fn report_create_and_export_and_verify() {
|
|||
]);
|
||||
assert_eq!(wai(&d, &["report", "create", "--target", &url, "--start", &start]).status.code(), Some(2), "no end");
|
||||
let o = wai(&d, &["report", "create", "--target", &url, "--start", &start, "--end", &end, "--dry-run"]);
|
||||
assert_eq!((o.status.code(), envelope(&o)["data"]["dry_run"].as_bool()), (Some(0), Some(true)));
|
||||
assert_eq!((o.status.code(), envelope(&o)["dry_run"].as_bool()), (Some(0), Some(true)));
|
||||
assert_eq!(envelope(&o)["plan"][0]["request"], "POST /v1/fleet/reports");
|
||||
assert_eq!(wai(&d, &["report", "create", "--target", &url, "--start", &start, "--end", &end]).status.code(), Some(4));
|
||||
let o = wai(&d, &["report", "create", "--target", &url, "--start", &start, "--end", &end, "--confirm"]);
|
||||
let o = wai(&d, &["report", "create", "--target", &url, "--start", &start, "--end", &end, "--yes"]);
|
||||
assert_eq!(o.status.code(), Some(0));
|
||||
assert_eq!(envelope(&o)["data"]["outcome"]["totals"]["energy"]["hops"]["coverage"], "partial");
|
||||
assert_eq!(rx.recv().unwrap().0, "POST /v1/fleet/reports HTTP/1.0");
|
||||
|
|
@ -170,13 +194,17 @@ fn report_create_and_export_and_verify() {
|
|||
let o = wai(&d, &["report", "export", &digest, "--target", &url, "--out", "altered.wai-fleet.json"]);
|
||||
assert_eq!((o.status.code(), envelope(&o)["error"]["code"].as_str()), (Some(1), Some("bad_signature")));
|
||||
assert!(!d.join("altered.wai-fleet.json").exists());
|
||||
// Exported again over the file: that waits for --yes, and fetches before it asks.
|
||||
let o = wai(&d, &["report", "export", &digest, "--target", &url, "--out", "week.wai-fleet.json", "--dry-run"]);
|
||||
let e = envelope(&o);
|
||||
assert_eq!((o.status.code(), e["plan"][1]["write"].as_str(), e["plan"][1]["replaces"].as_bool()), (Some(0), Some("week.wai-fleet.json"), Some(true)), "{e:#}");
|
||||
// `wai verify` checks the bundle offline, and refuses it altered.
|
||||
let o = wai(&d, &["verify", "week.wai-fleet.json"]);
|
||||
let o = wai_text(&d, &["verify", "week.wai-fleet.json"]);
|
||||
let t = String::from_utf8_lossy(&o.stdout);
|
||||
assert!(o.status.success(), "{t}");
|
||||
assert!(t.contains("every line recomputed") && t.contains("silent") && t.contains("hops partial"), "{t}");
|
||||
std::fs::write(d.join("bad.wai-fleet.json"), altered.to_string()).unwrap();
|
||||
let o = wai(&d, &["verify", "bad.wai-fleet.json"]);
|
||||
let o = wai_text(&d, &["verify", "bad.wai-fleet.json"]);
|
||||
assert_eq!(o.status.code(), Some(1));
|
||||
assert!(String::from_utf8_lossy(&o.stdout).contains("FAIL fleet report: bad_signature"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,9 +57,16 @@ fn refused(run: &Output, out: &Path, what: &str) -> String {
|
|||
stderr
|
||||
}
|
||||
|
||||
/// The `--json` line `wai generate` prints, checked against the file it wrote.
|
||||
/// The `data` of the JSON envelope (envelope/1) that `wai ... --json` prints.
|
||||
fn data(stdout: &str) -> Value {
|
||||
let env: Value = serde_json::from_str(stdout.trim()).expect("one JSON envelope");
|
||||
assert_eq!((env["wai"].as_str(), env["ok"].as_bool()), (Some("envelope/1"), Some(true)), "{env}");
|
||||
env["data"].clone()
|
||||
}
|
||||
|
||||
/// The `--json` data `wai generate` prints, checked against the file it wrote.
|
||||
fn made(stdout: &str, out: &Path, w: u64, h: u64) -> String {
|
||||
let v: Value = serde_json::from_str(stdout.trim()).unwrap();
|
||||
let v: Value = data(stdout);
|
||||
let rgb = std::fs::read(out).unwrap();
|
||||
assert_eq!((v["w"].as_u64(), v["h"].as_u64()), (Some(w), Some(h)));
|
||||
assert_eq!(v["bytes"].as_u64(), Some(w * h * 3));
|
||||
|
|
@ -366,13 +373,13 @@ fn score_build_and_render_reproduce_the_corpus() {
|
|||
let notes = score_dir().join("notes").join(format!("{name}.json"));
|
||||
let stdout = ok(&wai(&["score", "build", p(¬es), "-o", p(&built), "--json"]));
|
||||
assert_eq!(std::fs::read(&built).unwrap(), committed, "{name}: built byte for byte");
|
||||
let b: Value = serde_json::from_str(stdout.trim()).unwrap();
|
||||
let b: Value = data(&stdout);
|
||||
assert_eq!(b["bytes"].as_u64(), Some(committed.len() as u64));
|
||||
assert_eq!(b["score_hash"], spec["score_hash"], "{name}");
|
||||
|
||||
let pcm = scratch(&format!("{name}.s16"));
|
||||
let stdout = ok(&wai(&["score", "render", p(&built), "--pcm", p(&pcm), "--json"]));
|
||||
let r: Value = serde_json::from_str(stdout.trim()).unwrap();
|
||||
let r: Value = data(&stdout);
|
||||
let duration = spec["duration"].as_u64().unwrap();
|
||||
assert_eq!(r["samples"].as_u64(), Some(duration), "{name}: the contract's duration, whole");
|
||||
assert_eq!(r["sample_rate"].as_u64(), Some(22050));
|
||||
|
|
@ -477,7 +484,7 @@ fn score_render_refuses_a_note_count_its_bytes_cannot_hold() {
|
|||
empty[n - 4..].copy_from_slice(&0u32.to_le_bytes());
|
||||
let input = scratch("empty.wscr");
|
||||
std::fs::write(&input, empty).unwrap();
|
||||
let r: Value = serde_json::from_str(ok(&wai(&["score", "render", p(&input), "--json"])).trim()).unwrap();
|
||||
let r: Value = data(&ok(&wai(&["score", "render", p(&input), "--json"])));
|
||||
assert_eq!((r["samples"].as_u64(), r["notes"].as_u64()), (Some(100), Some(0)), "{r}");
|
||||
let input = scratch("huge.wscr");
|
||||
std::fs::write(&input, b).unwrap();
|
||||
|
|
|
|||
|
|
@ -5,14 +5,14 @@
|
|||
//!
|
||||
//! - Every tool is an operation of the toolchain's catalogue, named by it: the tool list is
|
||||
//! exactly the operations below, each tool's name its operation's with `_` for the dot, each
|
||||
//! operation's name keeping the rule (`wai::ops::check_id`). `core` is offered by default,
|
||||
//! operation of the catalogue (`wai::catalogue`). `core` is offered by default,
|
||||
//! `dev` when it is asked for, and a toolset that does not exist is refused.
|
||||
//! - `receipt_verify` agrees with the committed corpora vector by vector: every group and object
|
||||
//! vector of jwp-conformance (kind delivery), every claim vector whose context is one the tool
|
||||
//! takes (none, the previous claim, the object, an OER receipt, a gap's object; kind claim),
|
||||
//! every room of live-conformance/rooms (kind room_session) and every session of
|
||||
//! live-conformance/audit (kind audit_bundle); a receipt changed by one character is refused.
|
||||
//! - `package_build` writes an envelope inside the project directory only, never over a file
|
||||
//! - `envelope_wrap` writes an envelope inside the project directory only, never over a file
|
||||
//! without `overwrite`; `envelope_inspect` reads it back, and the directory as a package.
|
||||
//! - `conformance_run` runs a corpus's independent verifier and reports it passed.
|
||||
//! - The `dev` tools refuse a service that is not on a loopback address, before any
|
||||
|
|
@ -123,8 +123,8 @@ fn corpus(rel: &str) -> Value {
|
|||
|
||||
/// The operations of each toolset, in the order the server lists them. A tool is its
|
||||
/// operation's name with `_` for the dot.
|
||||
const CORE: &[&str] = &["envelope.inspect", "registry.list", "registry.show", "receipt.verify", "package.build", "conformance.list", "conformance.run", "docs.search"];
|
||||
const DEV: &[&str] = &["dev.start", "dev.stop", "room.grant", "room.join", "room.leave", "room.session"];
|
||||
const CORE: &[&str] = &["envelope.inspect", "registry.list", "registry.show", "receipt.verify", "envelope.wrap", "conformance.list", "conformance.run", "docs.search"];
|
||||
const DEV: &[&str] = &["dev.start", "dev.stop", "room.grant", "room.join", "room.leave", "session.show"];
|
||||
|
||||
fn tool_names(c: &mut Client) -> Vec<String> {
|
||||
let list = c.request("tools/list", json!({}));
|
||||
|
|
@ -132,7 +132,7 @@ fn tool_names(c: &mut Client) -> Vec<String> {
|
|||
}
|
||||
|
||||
fn names_of(ops: &[&[&str]]) -> Vec<String> {
|
||||
ops.iter().flat_map(|s| s.iter()).map(|op| wai::ops::mcp_tool_name(op)).collect()
|
||||
ops.iter().flat_map(|s| s.iter()).map(|op| op.replace('.', "_")).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -149,7 +149,7 @@ fn the_handshake_and_the_tool_list() {
|
|||
assert_eq!(names, names_of(&[CORE, DEV]));
|
||||
for t in tools {
|
||||
let op = t["_meta"]["wai/operation"].as_str().unwrap();
|
||||
assert_eq!(wai::ops::check_id(op), Ok(()), "{t}");
|
||||
assert!(wai::catalogue::op(op).is_some_and(|o| o.mcp), "{t}: an operation of the catalogue that wai mcp serves");
|
||||
assert_eq!(t["name"].as_str().unwrap(), op.replace('.', "_"), "{t}: the tool is its operation, named by it");
|
||||
let set = t["_meta"]["wai/toolset"].as_str().unwrap();
|
||||
assert_eq!(set, if DEV.contains(&op) { "dev" } else { "core" }, "{t}");
|
||||
|
|
@ -184,11 +184,11 @@ fn read_only_offers_no_tool_that_writes_serves_or_mints() {
|
|||
let d = scratch("readonly");
|
||||
let mut c = Client::start(&d, &["--toolset", "core,dev", "--read-only"]);
|
||||
let names = tool_names(&mut c);
|
||||
for gone in ["package_build", "dev_start", "dev_stop", "room_grant", "room_join", "room_leave", "room_session"] {
|
||||
for gone in ["envelope_wrap", "dev_start", "dev_stop", "room_grant", "room_join", "room_leave", "session_show"] {
|
||||
assert!(!names.iter().any(|n| n == gone), "{gone} is offered read-only");
|
||||
}
|
||||
assert!(names.iter().any(|n| n == "receipt_verify"));
|
||||
let r = c.request("tools/call", json!({"name": "package_build", "arguments": {"input": "x", "capability": "wai.text.zstd"}}));
|
||||
let r = c.request("tools/call", json!({"name": "envelope_wrap", "arguments": {"input": "x", "capability": "wai.text.zstd"}}));
|
||||
assert_eq!(r["error"]["code"], -32602, "{r}");
|
||||
}
|
||||
|
||||
|
|
@ -228,21 +228,21 @@ fn packaging_and_inspecting_stay_inside_the_project() {
|
|||
let mut c = Client::start(&d, &[]);
|
||||
let input = d.join("hello.txt");
|
||||
std::fs::write(&input, b"hello, world").unwrap();
|
||||
let p = c.ok("package_build", json!({"input": input, "capability": "wai.text.zstd", "intent": "improve"}));
|
||||
let p = c.ok("envelope_wrap", json!({"input": input, "capability": "wai.text.zstd", "intent": "improve"}));
|
||||
assert_eq!((p["format"].as_str(), p["components"][0]["capability"].as_str()), (Some("WAI1"), Some("wai.text.zstd")), "{p}");
|
||||
let out = d.join("hello.txt.wai");
|
||||
assert!(out.is_file());
|
||||
assert!(c.call("package_build", json!({"input": input, "capability": "wai.text.zstd", "intent": "improve"})).unwrap_err().contains("exists"));
|
||||
c.ok("package_build", json!({"input": input, "capability": "wai.text.zstd", "intent": "improve", "overwrite": true}));
|
||||
assert!(c.call("envelope_wrap", json!({"input": input, "capability": "wai.text.zstd", "intent": "improve"})).unwrap_err().contains("exists"));
|
||||
c.ok("envelope_wrap", json!({"input": input, "capability": "wai.text.zstd", "intent": "improve", "overwrite": true}));
|
||||
let outside = std::env::temp_dir().join(format!("wai-cli-mcp-outside-{}.wai", std::process::id()));
|
||||
let e = c.call("package_build", json!({"input": input, "capability": "wai.text.zstd", "output": outside})).unwrap_err();
|
||||
let e = c.call("envelope_wrap", json!({"input": input, "capability": "wai.text.zstd", "output": outside})).unwrap_err();
|
||||
assert!(e.contains("outside the project directory") && !outside.exists(), "{e}");
|
||||
assert!(c.call("package_build", json!({"input": input, "capability": "wai.text.zstd", "output": "../escape.wai"})).is_err());
|
||||
assert!(c.call("package_build", json!({"input": input, "capability": "not.a.capability"})).is_err());
|
||||
assert!(c.call("envelope_wrap", json!({"input": input, "capability": "wai.text.zstd", "output": "../escape.wai"})).is_err());
|
||||
assert!(c.call("envelope_wrap", json!({"input": input, "capability": "not.a.capability"})).is_err());
|
||||
#[cfg(unix)]
|
||||
{
|
||||
std::os::unix::fs::symlink(&outside, d.join("link.wai")).unwrap();
|
||||
assert!(c.call("package_build", json!({"input": input, "capability": "wai.text.zstd", "output": "link.wai", "overwrite": true})).unwrap_err().contains("a link"));
|
||||
assert!(c.call("envelope_wrap", json!({"input": input, "capability": "wai.text.zstd", "output": "link.wai", "overwrite": true})).unwrap_err().contains("a link"));
|
||||
assert!(!outside.exists(), "nothing written through a link");
|
||||
std::fs::remove_file(d.join("link.wai")).unwrap();
|
||||
}
|
||||
|
|
@ -400,7 +400,7 @@ fn development_tools_refuse_any_service_off_this_machine() {
|
|||
assert!(e.starts_with("DEVELOPMENT ONLY"), "{remote}: {e}");
|
||||
}
|
||||
// A target other than local is refused, whatever the service.
|
||||
for t in ["room_grant", "room_join", "room_leave", "room_session"] {
|
||||
for t in ["room_grant", "room_join", "room_leave", "session_show"] {
|
||||
let e = c.call(t, json!({"target": "cloud", "principal": "alice", "role": "publisher"})).unwrap_err();
|
||||
assert!(e.contains("target local alone"), "{t}: {e}");
|
||||
}
|
||||
|
|
@ -412,7 +412,7 @@ fn development_tools_refuse_any_service_off_this_machine() {
|
|||
for t in ["room_join", "room_leave"] {
|
||||
assert!(c.call(t, json!({"principal": "alice"})).is_err());
|
||||
}
|
||||
assert!(c.call("room_session", json!({})).unwrap_err().contains("dev_start"));
|
||||
assert!(c.call("session_show", json!({})).unwrap_err().contains("dev_start"));
|
||||
assert!(c.call("dev_stop", json!({})).is_err());
|
||||
assert!(c.call("room_grant", json!({"principal": "alice", "role": "publisher"})).unwrap_err().contains("dev_start"));
|
||||
let e = c.call("dev_start", json!({"room_bin": d.join("no-such-program")})).unwrap_err();
|
||||
|
|
|
|||
494
wai/wai-rs/tests/cli_ops.rs
Normal file
494
wai/wai-rs/tests/cli_ops.rs
Normal file
|
|
@ -0,0 +1,494 @@
|
|||
//! The operation catalogue through the `wai` binary, as an agent and a person meet it
|
||||
//! (`--features quickstart --test cli_ops`):
|
||||
//!
|
||||
//! - `wai ops list`, `search`, `schema` and `export` read the catalogue, and the export is
|
||||
//! the committed `wai/catalogue/ops.json` (a drifted copy exits 1);
|
||||
//! - every operation answers `--help` from its catalogue entry, and its mechanical form
|
||||
//! `wai <noun> <verb>` runs it as its CLI spelling does;
|
||||
//! - the exit codes: 1 a check that failed, 2 usage, 3 permission, 4 confirmation required,
|
||||
//! 5 a file, an operation or a build feature not found, 6 a target that did not come up;
|
||||
//! - `--json`: one envelope (`envelope/1`) on stdout, success and failure, with a command's
|
||||
//! own JSON or its text in `data`, and with a confirmation's proposal and command;
|
||||
//! - `--dry-run`: every operation of this build whose effect is not `reads` writes nothing
|
||||
//! with it, and (for those that write) writes without it, so the dry run is of a command
|
||||
//! that would write;
|
||||
//! - `--yes`: replacing a file the command line did not name waits for it; `--confirm` is its
|
||||
//! deprecated spelling, does the same, and help and the export say so.
|
||||
#![cfg(feature = "quickstart")]
|
||||
|
||||
use serde_json::Value;
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Command, Output, Stdio};
|
||||
use wai::catalogue::{self, Confirm, Effect, OPS};
|
||||
|
||||
fn wai_in(dir: &Path, args: &[&str]) -> Output {
|
||||
Command::new(env!("CARGO_BIN_EXE_wai")).current_dir(dir).args(args).stdin(Stdio::null()).output().expect("wai runs")
|
||||
}
|
||||
|
||||
fn text(o: &Output) -> String {
|
||||
format!("{}{}", String::from_utf8_lossy(&o.stdout), String::from_utf8_lossy(&o.stderr))
|
||||
}
|
||||
|
||||
fn scratch(name: &str) -> PathBuf {
|
||||
let d = Path::new(env!("CARGO_TARGET_TMPDIR")).join("cli_ops").join(name);
|
||||
let _ = std::fs::remove_dir_all(&d);
|
||||
std::fs::create_dir_all(&d).unwrap();
|
||||
d
|
||||
}
|
||||
|
||||
fn wai_dir() -> PathBuf {
|
||||
Path::new(env!("CARGO_MANIFEST_DIR")).join("..")
|
||||
}
|
||||
|
||||
/// The one JSON envelope a `--json` run prints.
|
||||
fn envelope(o: &Output) -> Value {
|
||||
let v: Value = serde_json::from_slice(&o.stdout).unwrap_or_else(|e| panic!("not one JSON envelope ({e}): {}", text(o)));
|
||||
assert_eq!(v["wai"], catalogue::ENVELOPE, "{v}");
|
||||
v
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ops_list_names_every_operation_and_export_is_the_committed_file() {
|
||||
let d = scratch("list");
|
||||
let o = wai_in(&d, &["ops", "list", "--json"]);
|
||||
assert!(o.status.success(), "{}", text(&o));
|
||||
let v = envelope(&o);
|
||||
assert_eq!((v["ok"].as_bool(), v["op"].as_str()), (Some(true), Some("ops.list")));
|
||||
let ids: Vec<&str> = v["data"].as_array().unwrap().iter().map(|r| r["id"].as_str().unwrap()).collect();
|
||||
assert_eq!(ids, OPS.iter().map(|o| o.id).collect::<Vec<_>>());
|
||||
let plain = wai_in(&d, &["ops", "list"]);
|
||||
for op in OPS {
|
||||
assert!(String::from_utf8_lossy(&plain.stdout).contains(op.id), "{}", op.id);
|
||||
}
|
||||
|
||||
let committed = wai_dir().join("catalogue/ops.json");
|
||||
let o = wai_in(&d, &["ops", "export", "--check", committed.to_str().unwrap()]);
|
||||
assert!(o.status.success(), "wai/catalogue/ops.json is the catalogue's export: {}", text(&o));
|
||||
let o = wai_in(&d, &["ops", "export"]);
|
||||
assert_eq!(o.stdout, std::fs::read(&committed).unwrap(), "`wai ops export` prints the committed export");
|
||||
// A drifted copy is a check that failed.
|
||||
let drifted = d.join("ops.json");
|
||||
std::fs::write(&drifted, String::from_utf8_lossy(&o.stdout).replacen("envelope.inspect", "envelope.inspekt", 1)).unwrap();
|
||||
let o = wai_in(&d, &["ops", "export", "--check", drifted.to_str().unwrap()]);
|
||||
assert_eq!(o.status.code(), Some(1), "{}", text(&o));
|
||||
assert!(text(&o).contains("regenerate"), "{}", text(&o));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ops_search_and_schema_answer_offline() {
|
||||
let d = scratch("search");
|
||||
let o = wai_in(&d, &["ops", "search", "verify", "the", "receipts", "--json"]);
|
||||
let v = envelope(&o);
|
||||
let hits = v["data"].as_array().unwrap();
|
||||
assert!(!hits.is_empty() && hits.len() <= 5, "{v}");
|
||||
assert_eq!(hits[0]["id"], "receipt.verify", "{v}");
|
||||
let o = wai_in(&d, &["ops", "search", "make a new project"]);
|
||||
assert!(String::from_utf8_lossy(&o.stdout).lines().next().unwrap_or("").starts_with("project.create"), "{}", text(&o));
|
||||
|
||||
let o = wai_in(&d, &["ops", "schema", "envelope.wrap"]);
|
||||
assert!(o.status.success(), "{}", text(&o));
|
||||
let s: Value = serde_json::from_slice(&o.stdout).unwrap();
|
||||
assert_eq!((s["id"].as_str(), s["effect"].as_str(), s["dry_run"].as_bool()), (Some("envelope.wrap"), Some("writes-project"), Some(true)));
|
||||
assert!(s["input"]["required"].as_array().unwrap().iter().any(|r| r == "capability"), "{s}");
|
||||
let o = wai_in(&d, &["ops", "schema", "envelope.frob", "--json"]);
|
||||
assert_eq!(o.status.code(), Some(5), "{}", text(&o));
|
||||
assert_eq!(envelope(&o)["error"]["code"], "operation-not-found");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_operation_answers_help_from_its_entry_and_its_mechanical_form() {
|
||||
let d = scratch("help");
|
||||
for op in OPS.iter().filter(|o| o.cli.is_some()) {
|
||||
let cmd = op.command();
|
||||
let mut words: Vec<&str> = cmd.split_whitespace().skip(1).collect();
|
||||
words.push("--help");
|
||||
let o = wai_in(&d, &words);
|
||||
assert!(o.status.success(), "{cmd} --help: {}", text(&o));
|
||||
let t = String::from_utf8_lossy(&o.stdout);
|
||||
assert!(t.contains(op.id) && t.contains(op.summary), "{cmd} --help: {t}");
|
||||
let (n, v) = op.parts();
|
||||
let o = wai_in(&d, &[n, v, "--help"]);
|
||||
assert!(o.status.success() && String::from_utf8_lossy(&o.stdout).contains(op.id), "wai {n} {v} --help: {}", text(&o));
|
||||
}
|
||||
let o = wai_in(&d, &["--help"]);
|
||||
assert!(o.status.success());
|
||||
assert!(String::from_utf8_lossy(&o.stdout).contains("sends nothing off your machine unless you name a remote service"));
|
||||
// The mechanical form runs the operation.
|
||||
let o = wai_in(&d, &["registry", "list"]);
|
||||
let p = wai_in(&d, &["capabilities"]);
|
||||
assert!(o.status.success() && o.stdout == p.stdout, "{}", text(&o));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_exit_codes_say_what_happened() {
|
||||
let d = scratch("exits");
|
||||
// 2: usage.
|
||||
assert_eq!(wai_in(&d, &[]).status.code(), Some(2));
|
||||
assert_eq!(wai_in(&d, &["inspect"]).status.code(), Some(2));
|
||||
assert_eq!(wai_in(&d, &["create", "x", "--frob"]).status.code(), Some(2));
|
||||
// 5: a file, an operation, a build that does not hold the operation.
|
||||
assert_eq!(wai_in(&d, &["inspect", "no-such.wai"]).status.code(), Some(5));
|
||||
let o = wai_in(&d, &["frobnicate", "--json"]);
|
||||
assert_eq!(o.status.code(), Some(5), "{}", text(&o));
|
||||
let v = envelope(&o);
|
||||
assert_eq!((v["ok"].as_bool(), v["error"]["code"].as_str()), (Some(false), Some("operation-not-found")), "{v}");
|
||||
#[cfg(not(feature = "generate"))]
|
||||
{
|
||||
let o = wai_in(&d, &["generate", "--model", "m.json"]);
|
||||
assert_eq!(o.status.code(), Some(5), "a build without `generate` does not hold media.generate: {}", text(&o));
|
||||
assert!(text(&o).contains("needs a build with the feature generate"), "{}", text(&o));
|
||||
}
|
||||
// 1: a check that failed (a receipt whose object changed), with its report in data.
|
||||
assert!(wai_in(&d, &["hello", "h"]).status.success());
|
||||
let obj = std::fs::read_dir(d.join("h")).unwrap().flatten().map(|e| e.path()).find(|p| p.extension().is_some_and(|x| x == "wai")).unwrap();
|
||||
let mut b = std::fs::read(&obj).unwrap();
|
||||
let n = b.len();
|
||||
b[n - 1] ^= 1;
|
||||
std::fs::write(&obj, &b).unwrap();
|
||||
let o = wai_in(&d, &["verify", "h", "--json"]);
|
||||
assert_eq!(o.status.code(), Some(1), "{}", text(&o));
|
||||
let v = envelope(&o);
|
||||
assert_eq!((v["ok"].as_bool(), v["error"]["code"].as_str()), (Some(false), Some("check-failed")), "{v}");
|
||||
assert!(v["data"]["text"].as_str().unwrap().contains("FAIL"), "{v}");
|
||||
// 3: a file this user may not read (where the user is not root, who reads anything).
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let locked = d.join("locked.wai");
|
||||
std::fs::write(&locked, b"WAI1").unwrap();
|
||||
std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o000)).unwrap();
|
||||
if std::fs::read(&locked).is_err() {
|
||||
assert_eq!(wai_in(&d, &["inspect", "locked.wai"]).status.code(), Some(3));
|
||||
}
|
||||
std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o644)).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn a_local_stack_that_does_not_come_up_is_unavailable_exit_6() {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let d = scratch("unavailable");
|
||||
let fail = d.join("fails.sh");
|
||||
std::fs::write(&fail, "#!/bin/sh\nexit 1\n").unwrap();
|
||||
std::fs::set_permissions(&fail, std::fs::Permissions::from_mode(0o755)).unwrap();
|
||||
let web = d.join("web");
|
||||
std::fs::create_dir_all(web.join("room")).unwrap();
|
||||
std::fs::write(web.join("room/index.html"), "<script src=a.js></script>").unwrap();
|
||||
let f = fail.to_str().unwrap();
|
||||
let o = wai_in(&d, &["dev", "--port", "0", "--relay", f, "--room", f, "--web", web.to_str().unwrap(), "--check", "--json"]);
|
||||
assert_eq!(o.status.code(), Some(6), "{}", text(&o));
|
||||
let v = envelope(&o);
|
||||
assert_eq!((v["error"]["code"].as_str(), v["error"]["retryable"].as_bool()), (Some("target-unavailable"), Some(true)), "{v}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replacing_a_file_the_command_line_did_not_name_waits_for_yes() {
|
||||
let d = scratch("confirm");
|
||||
std::fs::write(d.join("p.bin"), b"hello").unwrap();
|
||||
let o = wai_in(&d, &["wrap", "wai.text.zstd", "p.bin"]);
|
||||
assert!(o.status.success(), "{}", text(&o));
|
||||
let first = std::fs::read(d.join("out.wai")).unwrap();
|
||||
std::fs::write(d.join("p.bin"), b"hello, again").unwrap();
|
||||
// Off a terminal, without --yes: exit 4, nothing changed, the proposal and the command.
|
||||
let o = wai_in(&d, &["wrap", "wai.text.zstd", "p.bin", "--json"]);
|
||||
assert_eq!(o.status.code(), Some(4), "{}", text(&o));
|
||||
let v = envelope(&o);
|
||||
assert_eq!(v["error"]["code"], "confirmation-required", "{v}");
|
||||
assert_eq!(v["proposed"][0]["replace"], "out.wai", "{v}");
|
||||
assert_eq!(v["confirm"], "wai wrap wai.text.zstd p.bin --yes", "{v}");
|
||||
assert_eq!(std::fs::read(d.join("out.wai")).unwrap(), first, "nothing changed");
|
||||
let o = wai_in(&d, &["wrap", "wai.text.zstd", "p.bin"]);
|
||||
assert_eq!(o.status.code(), Some(4), "{}", text(&o));
|
||||
assert!(text(&o).contains("to go ahead: wai wrap wai.text.zstd p.bin --yes"), "{}", text(&o));
|
||||
// With --yes it goes ahead; a named output never waits.
|
||||
assert!(wai_in(&d, &["wrap", "wai.text.zstd", "p.bin", "--yes"]).status.success());
|
||||
assert_ne!(std::fs::read(d.join("out.wai")).unwrap(), first);
|
||||
assert!(wai_in(&d, &["wrap", "wai.text.zstd", "p.bin", "-o", "out.wai"]).status.success());
|
||||
// `wai hello` over an earlier run's files waits too.
|
||||
assert!(wai_in(&d, &["hello", "h"]).status.success());
|
||||
assert_eq!(wai_in(&d, &["hello", "h"]).status.code(), Some(4));
|
||||
assert!(wai_in(&d, &["hello", "h", "--yes"]).status.success());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn confirm_is_the_deprecated_spelling_of_yes() {
|
||||
let d = scratch("confirm-alias");
|
||||
std::fs::write(d.join("p.bin"), b"hello").unwrap();
|
||||
assert!(wai_in(&d, &["wrap", "wai.text.zstd", "p.bin"]).status.success());
|
||||
let first = std::fs::read(d.join("out.wai")).unwrap();
|
||||
std::fs::write(d.join("p.bin"), b"hello, again").unwrap();
|
||||
// Without either: exit 4, and the confirming command names --yes.
|
||||
let o = wai_in(&d, &["wrap", "wai.text.zstd", "p.bin", "--json"]);
|
||||
assert_eq!(o.status.code(), Some(4), "{}", text(&o));
|
||||
assert_eq!(envelope(&o)["confirm"], "wai wrap wai.text.zstd p.bin --yes");
|
||||
assert_eq!(std::fs::read(d.join("out.wai")).unwrap(), first, "nothing changed");
|
||||
// --confirm alone goes ahead as --yes does, wherever it is on the line.
|
||||
let o = wai_in(&d, &["wrap", "--confirm", "wai.text.zstd", "p.bin"]);
|
||||
assert!(o.status.success(), "--confirm is --yes: {}", text(&o));
|
||||
let replaced = std::fs::read(d.join("out.wai")).unwrap();
|
||||
assert_ne!(replaced, first);
|
||||
assert!(wai_in(&d, &["wrap", "wai.text.zstd", "p.bin", "--yes"]).status.success());
|
||||
assert_eq!(std::fs::read(d.join("out.wai")).unwrap(), replaced, "the same change");
|
||||
// The help names it deprecated, on its own line and in every operation that confirms.
|
||||
let h = String::from_utf8_lossy(&wai_in(&d, &["--help"]).stdout).into_owned();
|
||||
assert!(h.lines().any(|l| l.trim_start().starts_with("--confirm") && l.contains("deprecated") && l.contains("--yes")), "{h}");
|
||||
for op in OPS.iter().filter(|o| o.cli.is_some() && o.confirms() != Confirm::Never) {
|
||||
let cmd = op.command();
|
||||
let words: Vec<&str> = cmd.split_whitespace().skip(1).chain(["--help"]).collect();
|
||||
let t = String::from_utf8_lossy(&wai_in(&d, &words).stdout).into_owned();
|
||||
assert!(t.contains("--yes") && t.contains("--confirm, deprecated"), "{} --help: {t}", op.id);
|
||||
}
|
||||
// The export says so, for agents.
|
||||
let e: Value = serde_json::from_slice(&wai_in(&d, &["ops", "export"]).stdout).unwrap();
|
||||
let yes = e["agent_flags"].as_array().unwrap().iter().find(|f| f["flag"] == "--yes").unwrap().clone();
|
||||
assert_eq!((yes["aliases"][0]["flag"].as_str(), yes["aliases"][0]["deprecated"]["use_instead"].as_str()), (Some("--confirm"), Some("--yes")), "{yes}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_text_command_carries_its_output_in_the_envelope() {
|
||||
let d = scratch("envelope");
|
||||
let o = wai_in(&d, &["capabilities", "--json"]);
|
||||
assert!(o.status.success(), "{}", text(&o));
|
||||
let v = envelope(&o);
|
||||
assert_eq!((v["op"].as_str(), v["ok"].as_bool()), (Some("registry.list"), Some(true)));
|
||||
assert!(v["data"]["text"].as_str().unwrap().contains("wai.image.png"), "{v}");
|
||||
let o = wai_in(&d, &["inspect", "no-such.wai", "--json"]);
|
||||
let v = envelope(&o);
|
||||
assert_eq!((v["ok"].as_bool(), v["error"]["code"].as_str()), (Some(false), Some("not-found")), "{v}");
|
||||
assert!(v.get("data").is_none(), "{v}");
|
||||
}
|
||||
|
||||
// ─────────────────────────────── --dry-run ───────────────────────────────
|
||||
|
||||
/// Every file under `d`, with its length and modification time.
|
||||
fn snapshot(d: &Path) -> Vec<(String, u64, std::time::SystemTime)> {
|
||||
fn walk(root: &Path, p: &Path, out: &mut Vec<(String, u64, std::time::SystemTime)>) {
|
||||
for e in std::fs::read_dir(p).unwrap().flatten() {
|
||||
let q = e.path();
|
||||
let m = e.metadata().unwrap();
|
||||
out.push((q.strip_prefix(root).unwrap().display().to_string(), m.len(), m.modified().unwrap()));
|
||||
if m.is_dir() {
|
||||
walk(root, &q, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut v = Vec::new();
|
||||
walk(d, d, &mut v);
|
||||
v.sort();
|
||||
v
|
||||
}
|
||||
|
||||
fn corpus_object(id: &str) -> PathBuf {
|
||||
let c = wai_dir().join("http-conformance");
|
||||
let listing: Value = serde_json::from_slice(&std::fs::read(c.join("objects.json")).unwrap()).unwrap();
|
||||
c.join(listing[id]["path"].as_str().unwrap())
|
||||
}
|
||||
|
||||
/// A running `wai http origin` over `root`, stopped when dropped.
|
||||
struct Origin(std::process::Child, u16);
|
||||
|
||||
impl Origin {
|
||||
fn start(root: &Path) -> Origin {
|
||||
let mut child = Command::new(env!("CARGO_BIN_EXE_wai"))
|
||||
.args(["http", "origin", "--root", root.to_str().unwrap(), "--port", "0"])
|
||||
.stdout(Stdio::piped())
|
||||
.spawn()
|
||||
.unwrap();
|
||||
let mut first = String::new();
|
||||
BufReader::new(child.stdout.take().unwrap()).read_line(&mut first).unwrap();
|
||||
let port = first.trim().rsplit(':').next().unwrap().trim_end_matches('/').parse().unwrap();
|
||||
Origin(child, port)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Origin {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.0.kill();
|
||||
let _ = self.0.wait();
|
||||
}
|
||||
}
|
||||
|
||||
/// A fleet service that answers one request with `body` (status 200), as `wai-fleet` would;
|
||||
/// its `http://` address.
|
||||
fn fleet_service(body: Value) -> String {
|
||||
use std::io::{Read, Write};
|
||||
let l = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
let addr = format!("http://{}", l.local_addr().unwrap());
|
||||
std::thread::spawn(move || {
|
||||
if let Ok((mut s, _)) = l.accept() {
|
||||
let mut buf = [0u8; 4096];
|
||||
let _ = s.read(&mut buf);
|
||||
let b = body.to_string();
|
||||
let _ = s.write_all(format!("HTTP/1.1 200 X\r\ncontent-type: application/json\r\ncontent-length: {}\r\n\r\n{b}", b.len()).as_bytes());
|
||||
}
|
||||
});
|
||||
addr
|
||||
}
|
||||
|
||||
/// A command line that writes, for each operation of this build whose effect is not
|
||||
/// `reads`, with what it needs made in `d`: `(id, args, whether it writes without
|
||||
/// --dry-run)`. An operation that runs something, or changes only a service, writes nothing
|
||||
/// of the project's.
|
||||
fn samples(d: &Path, origin_port: u16) -> Vec<(&'static str, Vec<String>, bool)> {
|
||||
let s = |v: &[&str]| v.iter().map(|x| x.to_string()).collect::<Vec<_>>();
|
||||
std::fs::write(d.join("in.txt"), b"a payload to carry").unwrap();
|
||||
assert!(wai_in(d, &["wrap", "wai.text.zstd", "in.txt", "-o", "w.wai"]).status.success());
|
||||
assert!(wai_in(d, &["encode", "zstd", "in.txt", "-o", "z.wai"]).status.success());
|
||||
let reg = d.join("regroot");
|
||||
std::fs::create_dir_all(®).unwrap();
|
||||
std::fs::copy(wai_dir().join("REGISTRY.md"), reg.join("REGISTRY.md")).unwrap();
|
||||
let objs: Vec<String> = ["O9a", "O9b", "O9c"].iter().map(|id| corpus_object(id).to_str().unwrap().to_string()).collect();
|
||||
std::fs::write(d.join("classical.m3u8"), "#EXTM3U\n#EXT-X-VERSION:7\n#EXT-X-TARGETDURATION:1\n#EXT-X-MEDIA-SEQUENCE:0\n#EXTINF:0.640,\nc0.m4s\n#EXTINF:0.640,\nc1.m4s\n#EXTINF:0.640,\nc2.m4s\n#EXT-X-ENDLIST\n").unwrap();
|
||||
let list = serde_json::json!({
|
||||
"uri_prefix": "objects/",
|
||||
"variants": [{ "uri": "wai-180.m3u8", "classical": "classical.m3u8", "resolution": [320, 180], "frame_rate_milli": 25000,
|
||||
"segments": objs.iter().map(|o| serde_json::json!({ "object": o })).collect::<Vec<_>>() }],
|
||||
});
|
||||
std::fs::write(d.join("list.json"), serde_json::to_vec_pretty(&list).unwrap()).unwrap();
|
||||
let od = corpus_object("OD");
|
||||
let cid = wai::http_binding::hex(&wai::http_binding::content_hash(&std::fs::read(&od).unwrap()));
|
||||
let web = d.join("web");
|
||||
std::fs::create_dir_all(web.join("room")).unwrap();
|
||||
std::fs::write(web.join("room/index.html"), "<script src=a.js></script>").unwrap();
|
||||
let me = env!("CARGO_BIN_EXE_wai");
|
||||
std::fs::create_dir_all(d.join("agentproj")).unwrap();
|
||||
#[allow(unused_mut)]
|
||||
let mut more: Vec<(&'static str, Vec<String>, bool)> = Vec::new();
|
||||
#[cfg(feature = "video_manifest")]
|
||||
{
|
||||
let sd = d.join("sealdir");
|
||||
std::fs::create_dir_all(&sd).unwrap();
|
||||
std::fs::copy(d.join("classical.m3u8"), sd.join("classical.m3u8")).unwrap();
|
||||
std::fs::copy(d.join("list.json"), sd.join("list.json")).unwrap();
|
||||
let o = wai_in(&sd, &["http", "hls", "--objects", "list.json", "--out", "."]);
|
||||
assert!(o.status.success(), "{}", text(&o));
|
||||
more.push(("playlist.lock", s(&["http", "seal-playlist", "sealdir/wai-180.m3u8", "--key", &"07".repeat(32), "--out", "sealed"]), true));
|
||||
}
|
||||
#[cfg(feature = "app")]
|
||||
{
|
||||
// A Create runtime as wai-app's runtime.mjs lays one out, and a program made with Create.
|
||||
use sha2::{Digest, Sha256};
|
||||
let rt = d.join("runtime");
|
||||
let mut listed = Vec::new();
|
||||
for (p, text) in [("create/wai-app-app.js", "export {};\n"), ("create/pkg/wai_app_bg.wasm", "\0asm"), ("room/wai-app-room-client.js", "// the room client\n")] {
|
||||
let f = rt.join(p);
|
||||
std::fs::create_dir_all(f.parent().unwrap()).unwrap();
|
||||
std::fs::write(&f, text).unwrap();
|
||||
let h: String = Sha256::digest(text.as_bytes()).iter().map(|x| format!("{x:02x}")).collect();
|
||||
listed.push(serde_json::json!({"path": p, "sha256": h}));
|
||||
}
|
||||
std::fs::write(rt.join("create/wai-app-runtime.json"), serde_json::json!({"files": listed}).to_string()).unwrap();
|
||||
std::fs::write(d.join("app.oui"), "root = App([show], \"Friday night\")\nshow = Show([player, chat], \"Live\", 2)\nplayer = Player()\nchat = Chat(\"Talk\")\n").unwrap();
|
||||
more.push(("app.export", s(&["app", "export", "app.oui", "appproj", "--runtime", "runtime"]), true));
|
||||
}
|
||||
#[cfg(feature = "fleet")]
|
||||
{
|
||||
// A node's enrolment written here; a report's bundle exported from a fleet service (the
|
||||
// corpus's week, which this build verifies before it writes); and a report the service
|
||||
// would sign, which a dry run does not ask for.
|
||||
std::fs::write(d.join("org.key"), "07".repeat(32)).unwrap();
|
||||
let corpus: Value = serde_json::from_slice(&std::fs::read(wai_dir().join("live-conformance/fleet/vectors.json")).unwrap()).unwrap();
|
||||
let week = corpus["vectors"].as_array().unwrap().iter().find(|v| v["name"] == "a_week").unwrap().clone();
|
||||
let digest = week["outcome"]["digest"].as_str().unwrap().to_string();
|
||||
let url = fleet_service(serde_json::json!({"report": digest, "outcome": week["outcome"], "bundle": week["bundle"]}));
|
||||
more.push(("enrolment.create", s(&["enrolment", "create", &"11".repeat(32), "--role", "relay", "--key", "org.key", "--out", "node.wfe"]), true));
|
||||
more.push(("report.export", s(&["report", "export", &digest, "--target", &url, "--out", "week.wai-fleet.json"]), true));
|
||||
more.push(("report.create", s(&["report", "create", "--target", "http://127.0.0.1:9", "--start", "0", "--end", "1"]), false));
|
||||
}
|
||||
#[cfg(feature = "generate")]
|
||||
{
|
||||
let model = wai_dir().join("wai-rs/tests/vectors/generate/generate_spectral_model.json");
|
||||
more.push(("media.generate", s(&["generate", "--model", model.to_str().unwrap(), "--prompt", "a calm spectral horizon", "--seed", "7", "--steps", "1", "--size", "16x16", "-o", "g.rgb"]), true));
|
||||
}
|
||||
#[cfg(feature = "score")]
|
||||
{
|
||||
let notes = wai_dir().join("score-conformance/notes/scale.json");
|
||||
let notes = notes.to_str().unwrap();
|
||||
assert!(wai_in(d, &["score", "build", notes, "-o", "s0.wscr"]).status.success());
|
||||
more.push(("score.build", s(&["score", "build", notes, "-o", "s.wscr"]), true));
|
||||
more.push(("score.decode", s(&["score", "render", "s0.wscr", "--pcm", "s.s16"]), true));
|
||||
}
|
||||
#[cfg(all(feature = "staged", feature = "video_int", feature = "audio_int", feature = "splat_int"))]
|
||||
{
|
||||
let frame: Vec<u8> = (0..6 * 10 * 3).map(|i| ((i * 13) % 256) as u8).collect();
|
||||
std::fs::write(d.join("f.rgb"), &frame).unwrap();
|
||||
for args in [
|
||||
&["stage", "base", "--frames", "f.rgb", "--dims", "1,6,10", "--q", "40", "-o", "b0.wai"][..],
|
||||
&["stage", "refine", "--base", "b0.wai", "--span", "0:1", "--target", "f.rgb", "--q", "1", "-o", "l0.wai"],
|
||||
&["stage", "pack", "b0.wai", "l0.wai", "-o", "x0.wai"],
|
||||
] {
|
||||
let o = wai_in(d, args);
|
||||
assert!(o.status.success(), "{args:?}: {}", text(&o));
|
||||
}
|
||||
more.push(("stage.build", s(&["stage", "base", "--frames", "f.rgb", "--dims", "1,6,10", "--q", "40", "-o", "b.wai"]), true));
|
||||
more.push(("stage.encode", s(&["stage", "refine", "--base", "b0.wai", "--span", "0:1", "--target", "f.rgb", "--q", "1", "-o", "l.wai"]), true));
|
||||
more.push(("stage.wrap", s(&["stage", "pack", "b0.wai", "l0.wai", "-o", "x.wai"]), true));
|
||||
more.push(("stage.unwrap", s(&["stage", "explode", "x0.wai", "0", "-o", "e.wai"]), true));
|
||||
}
|
||||
let mut v = vec![
|
||||
("toolchain.test", s(&["hello", "hello-out"]), true),
|
||||
("project.create", s(&["create", "proj", "--template", "video-room"]), true),
|
||||
("envelope.wrap", s(&["wrap", "wai.text.zstd", "in.txt", "-o", "w2.wai"]), true),
|
||||
("envelope.unwrap", s(&["unwrap", "w.wai", "-o", "payload.out"]), true),
|
||||
("media.encode", s(&["encode", "zstd", "in.txt", "-o", "e.wai"]), true),
|
||||
("media.decode", s(&["decode", "z.wai", "-o", "decoded.txt"]), true),
|
||||
("registry.export", s(&["registry", "export", "--root", reg.to_str().unwrap()]), true),
|
||||
("ops.export", s(&["ops", "export", "--out", "ops-out.json"]), true),
|
||||
("http.export", s(&["http", "publish", corpus_object("O3").to_str().unwrap(), "--out", "pub"]), true),
|
||||
("hls.build", s(&["http", "hls", "--objects", "list.json", "--out", "."]), true),
|
||||
("dash.build", s(&["http", "mpd", "--objects", "list.json", "--out", "wai.mpd"]), true),
|
||||
("corpus.generate", s(&["http", "vectors", "diff", ".", "--seed", "1", "--count", "1", "--out", "diff.bin"]), true),
|
||||
("http.load", s(&["http", "bench", "--url", &format!("http://127.0.0.1:{origin_port}/{cid}.wai#h=1009&n=53794"), "--runs", "1", "--advertise", "wai.video.av1", "--out", "bench.json"]), true),
|
||||
("dev.start", s(&["dev", "--port", "0", "--relay", me, "--room", me, "--web", web.to_str().unwrap()]), false),
|
||||
("http.start", s(&["http", "origin", "--root", ".", "--port", "0"]), false),
|
||||
("conformance.run", s(&["conform", "verifier", "--suite", "jwp", "--impl", &format!("'{}' verdicts {{corpus}}", env!("CARGO_BIN_EXE_wai_jwp_vectors")), "--root", wai_dir().to_str().unwrap(), "--out", "jwp.wai-conform.json"]), true),
|
||||
("agent.setup", s(&["setup-agent", "--dir", "agentproj"]), true),
|
||||
("mcp.start", s(&["mcp", "--project", "."]), false),
|
||||
];
|
||||
v.extend(more);
|
||||
v
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_operation_that_is_not_read_only_writes_nothing_with_dry_run() {
|
||||
let root = scratch("dry-run");
|
||||
let served = root.join("served");
|
||||
std::fs::create_dir_all(&served).unwrap();
|
||||
assert!(wai_in(&served, &["http", "publish", corpus_object("OD").to_str().unwrap(), "--out", "."]).status.success());
|
||||
let origin = Origin::start(&served);
|
||||
let work = root.join("work");
|
||||
std::fs::create_dir_all(&work).unwrap();
|
||||
let table = samples(&work, origin.1);
|
||||
// Every such operation this build holds has a sample: a new one without --dry-run
|
||||
// support cannot join the catalogue unnoticed.
|
||||
for op in OPS.iter().filter(|o| o.cli.is_some() && o.effect != Effect::Reads && o.available()) {
|
||||
assert!(table.iter().any(|(id, _, _)| *id == op.id), "{} is not read-only and has no --dry-run sample here", op.id);
|
||||
}
|
||||
for (id, args, writes) in &table {
|
||||
let before = snapshot(&work);
|
||||
let mut dry: Vec<&str> = args.iter().map(String::as_str).collect();
|
||||
dry.push("--dry-run");
|
||||
let o = wai_in(&work, &dry);
|
||||
assert!(o.status.success(), "{id} --dry-run: {}", text(&o));
|
||||
assert_eq!(snapshot(&work), before, "{id} --dry-run wrote: {}", text(&o));
|
||||
let t = text(&o);
|
||||
assert!(t.contains("dry run") || t.contains("dry-run"), "{id} --dry-run says so: {t}");
|
||||
// The same with --json: the envelope says it was a dry run, and lists the plan.
|
||||
dry.push("--json");
|
||||
let o = wai_in(&work, &dry);
|
||||
assert_eq!(snapshot(&work), before, "{id} --dry-run --json wrote: {}", text(&o));
|
||||
if *writes {
|
||||
let v = envelope(&o);
|
||||
assert_eq!((v["ok"].as_bool(), v["dry_run"].as_bool()), (Some(true), Some(true)), "{id}: {v}");
|
||||
assert!(!v["plan"].as_array().unwrap().is_empty(), "{id}: the plan names what it would write: {v}");
|
||||
// Without --dry-run the same command writes: the dry run was of a write.
|
||||
let refs: Vec<&str> = args.iter().map(String::as_str).collect();
|
||||
let o = wai_in(&work, &refs);
|
||||
assert!(o.status.success(), "{id}: {}", text(&o));
|
||||
assert_ne!(snapshot(&work), before, "{id} without --dry-run writes");
|
||||
}
|
||||
}
|
||||
drop(origin);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue