diff --git a/assets/sourceos/bin/turtle-agent-machine b/assets/sourceos/bin/turtle-agent-machine new file mode 100644 index 00000000000..b0589cb0759 --- /dev/null +++ b/assets/sourceos/bin/turtle-agent-machine @@ -0,0 +1,28 @@ +#!/usr/bin/env sh +# TurtleTerm Agent Machine bridge. +set -eu + +bin_dir="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" +agentctl="$bin_dir/turtle-agentctl" + +cmd="${1:-surfaces}" +case "$cmd" in + surfaces) + shift || true + exec "$agentctl" --stdio agent-machine-surfaces "$@" + ;; + probe) + shift || true + exec "$agentctl" --stdio agent-machine-probe "$@" + ;; + request-execution) + shift || true + surface="${1:-agent-machine/local-agentpod}" + shift || true + exec "$agentctl" --stdio request-surface-execution "$surface" "$@" + ;; + *) + echo "usage: turtle-agent-machine [surfaces|probe|request-execution -- ]" >&2 + exit 2 + ;; +esac diff --git a/assets/sourceos/bin/turtle-cloudfog b/assets/sourceos/bin/turtle-cloudfog new file mode 100644 index 00000000000..45eb5e9de42 --- /dev/null +++ b/assets/sourceos/bin/turtle-cloudfog @@ -0,0 +1,28 @@ +#!/usr/bin/env sh +# TurtleTerm CloudFog surface bridge. +set -eu + +bin_dir="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" +agentctl="$bin_dir/turtle-agentctl" + +cmd="${1:-surfaces}" +case "$cmd" in + surfaces) + shift || true + exec "$agentctl" --stdio cloudfog-surfaces "$@" + ;; + inspect) + shift || true + exec "$agentctl" --stdio cloudfog-inspect "$@" + ;; + request-execution) + shift || true + surface="${1:-cloudfog/local-devshell}" + shift || true + exec "$agentctl" --stdio request-surface-execution "$surface" "$@" + ;; + *) + echo "usage: turtle-cloudfog [surfaces|inspect |request-execution -- ]" >&2 + exit 2 + ;; +esac diff --git a/assets/sourceos/bin/turtle-superconscious b/assets/sourceos/bin/turtle-superconscious new file mode 100644 index 00000000000..91471a1d77c --- /dev/null +++ b/assets/sourceos/bin/turtle-superconscious @@ -0,0 +1,22 @@ +#!/usr/bin/env sh +# TurtleTerm Superconscious bridge. +set -eu + +bin_dir="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" +agentctl="$bin_dir/turtle-agentctl" + +cmd="${1:-observe}" +case "$cmd" in + observe) + shift || true + exec "$agentctl" --stdio superconscious-observe "$@" + ;; + propose) + shift || true + exec "$agentctl" --stdio superconscious-propose "$@" + ;; + *) + echo "usage: turtle-superconscious [observe |propose ]" >&2 + exit 2 + ;; +esac diff --git a/assets/sourceos/tests/test_cloudshell_fog_receipt_context.py b/assets/sourceos/tests/test_cloudshell_fog_receipt_context.py new file mode 100644 index 00000000000..b4116ae0adf --- /dev/null +++ b/assets/sourceos/tests/test_cloudshell_fog_receipt_context.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +"""Validate TurtleTerm receipt context propagation for CloudShell FOG sessions.""" + +from __future__ import annotations + +import json +import os +import subprocess +import sys +import tempfile +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[3] +TURTLE_WRAPPER = REPO_ROOT / "assets" / "sourceos" / "bin" / "turtle-term" + + +def read_ndjson(path: Path) -> list[dict]: + return [json.loads(line) for line in path.read_text(encoding="utf-8").splitlines() if line.strip()] + + +def main() -> int: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + events = tmp_path / "events.ndjson" + receipts = tmp_path / "receipts" + + env = dict(os.environ) + env.update( + { + "SOURCEOS_TERMINAL_SESSION_ID": "csf-session-0001", + "SOURCEOS_WORKSPACE": "workspace:lattice-demo", + "SOURCEOS_TERMINAL_EVENTS": str(events), + "SOURCEOS_TERMINAL_RECEIPTS": str(receipts), + "SOURCEOS_ACTOR_ID": "human:operator@example.com", + "SOURCEOS_POLICY_BUNDLE_ID": "policy:cloudshell-default", + "SOURCEOS_EXECUTION_DOMAIN": "cloudshell-fog/k8s", + } + ) + + result = subprocess.run( + [sys.executable, str(TURTLE_WRAPPER), "run", "--", sys.executable, "-c", "print('cloudshell-fog-ok')"], + cwd=str(REPO_ROOT), + env=env, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + ) + + assert result.returncode == 0, result.stderr + assert "cloudshell-fog-ok" in result.stdout + assert events.exists(), "event stream missing" + + rows = read_ndjson(events) + completed = [row for row in rows if row.get("event_type") == "command.completed"][-1] + receipt_path = Path(completed["receipt_path"]) + assert receipt_path.exists(), f"receipt missing: {receipt_path}" + + receipt = json.loads(receipt_path.read_text(encoding="utf-8")) + assert receipt["schema"] == "sourceos.terminal.receipt.v0" + assert receipt["session_id"] == "csf-session-0001" + assert receipt["workspace_id"] == "workspace:lattice-demo" + assert receipt["actor_id"] == "human:operator@example.com" + assert receipt["policy_bundle_id"] == "policy:cloudshell-default" + assert receipt["execution_domain"] == "cloudshell-fog/k8s" + assert receipt["stdout_digest"].startswith("sha256:") + assert receipt["stderr_digest"].startswith("sha256:") + + print("validated CloudShell FOG receipt context propagation") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/docs/integration/cloudshell-fog.md b/docs/integration/cloudshell-fog.md new file mode 100644 index 00000000000..edd4ebe305c --- /dev/null +++ b/docs/integration/cloudshell-fog.md @@ -0,0 +1,86 @@ +# CloudShell FOG Integration Profile v0 — TurtleTerm + +## Purpose + +TurtleTerm is the SourceOS policy-aware, agent-addressable terminal workbench for trusted command execution, terminal receipts, agent delegation, and reproducible operator workflows. + +CloudShell FOG is the browser/fog/cloud shell execution plane for session lifecycle, placement, PTY attach, runtime allocation, and audit. + +This profile defines how TurtleTerm should integrate with CloudShell FOG while preserving TurtleTerm as the owner of terminal command receipt semantics. + +## Ownership boundary + +### TurtleTerm owns + +- local/operator command lifecycle receipts +- SourceOS terminal session/event/receipt schemas +- stdout/stderr digest capture for command execution +- operator terminal event stream and receipt paths +- local agent gateway and terminal CLI behavior + +### CloudShell FOG owns + +- browser/fog/cloud shell session lifecycle +- WSS PTY attach contract +- fog/cloud placement metadata +- Kubernetes/fog runtime connector behavior +- CloudShell audit events +- runtime allocation and teardown semantics + +## Integration principle + +TurtleTerm should expose or preserve receipt metadata that allows CloudShell FOG sessions and audit events to correlate with local/operator command receipts. + +TurtleTerm should not absorb CloudShell FOG's placement engine or runtime connector responsibilities. + +## Environment propagation + +When a TurtleTerm workflow is launched in the context of a CloudShell FOG session, the launcher MAY set: + +- `SOURCEOS_TERMINAL_SESSION_ID` = CloudShell session ID or derived stable terminal session ID +- `SOURCEOS_WORKSPACE` = CloudShell workspace or project identifier, if known +- `SOURCEOS_ACTOR_ID` = CloudShell authenticated subject or mapped SourceOS actor identity +- `SOURCEOS_POLICY_BUNDLE_ID` = CloudShell policy/profile identifier +- `SOURCEOS_EXECUTION_DOMAIN` = `cloudshell-fog`, `k8s`, `fog`, or a more specific domain + +TurtleTerm should preserve these values in session/event/receipt outputs rather than rewriting them with local-only defaults. + +## Receipt correlation fields + +Where available, CloudShell FOG metadata SHOULD be attached to TurtleTerm receipt context: + +- CloudShell session ID +- CloudShell placement region +- CloudShell placement node ID +- CloudShell trust tier +- CloudShell placement reasons +- runtime image or runtime profile +- runtime namespace/pod identity when applicable + +## Event mapping + +| TurtleTerm / SourceOS terminal concept | CloudShell FOG concept | +|---|---| +| `sourceos.terminal.session.v0` | `session.created` / shell session context | +| `command.started` | command execution within attached shell context | +| `command.completed` | completed command plus receipt pointer | +| command stdout/stderr digests | command-level evidence, not PTY stream replacement | +| `execution_domain` | CloudShell runtime / placement execution domain | +| policy bundle ID | CloudShell policy/profile context | + +## Non-goals + +- TurtleTerm does not replace CloudShell FOG's browser shell or WSS PTY contract. +- TurtleTerm does not own CloudShell FOG's Kubernetes/fog placement engine. +- CloudShell FOG does not need to capture every PTY byte as a TurtleTerm command receipt by default. + +## Open questions + +1. Should SourceOS terminal schemas remain in TurtleTerm or move to a shared terminal-contracts repository? +2. Should TurtleTerm support a `cloudshell-fog` receipt enrichment mode with explicit placement fields? +3. Should AgentPlane become the canonical bridge for launching TurtleTerm-backed workflows from CloudShell FOG? + +## Tracking + +- TurtleTerm: issue #1 +- CloudShell FOG: SocioProphet/cloudshell-fog#35 diff --git a/docs/sourceos/AGENTIC_INTEGRATION_PLAN.md b/docs/sourceos/AGENTIC_INTEGRATION_PLAN.md index eccb0a34108..3956de146ac 100644 --- a/docs/sourceos/AGENTIC_INTEGRATION_PLAN.md +++ b/docs/sourceos/AGENTIC_INTEGRATION_PLAN.md @@ -38,7 +38,7 @@ AgentPlane execution, evidence, and replay ## Invariant -TurtleTerm must not grant agents ambient shell authority. +TurtleTerm must never give agents unreviewed shell capability. Every risky action becomes an ExecutionDecision: allow, deny, ask, defer, or rewrite. diff --git a/docs/sourceos/INSTALL.md b/docs/sourceos/INSTALL.md index 0d6c33094ec..396d65b511a 100644 --- a/docs/sourceos/INSTALL.md +++ b/docs/sourceos/INSTALL.md @@ -70,13 +70,13 @@ turtle-agentctl --stdio ping Homebrew profile path: ```bash -ln -sf "$(brew --prefix)/etc/turtle-term/wezterm.lua" ~/.wezterm.lua +ln -sf "$(brew --prefix)/etc/turtle-term/turtleterm.lua" ~/.wezterm.lua ``` Direct install profile path: ```bash -ln -sf "$HOME/.local/etc/turtle-term/wezterm.lua" ~/.wezterm.lua +ln -sf "$HOME/.local/etc/turtle-term/turtleterm.lua" ~/.wezterm.lua ``` Then launch TurtleTerm: diff --git a/packaging/linux/rpm/turtle-term.spec b/packaging/linux/rpm/turtle-term.spec index 106de4fd85b..3b4a5ade758 100644 --- a/packaging/linux/rpm/turtle-term.spec +++ b/packaging/linux/rpm/turtle-term.spec @@ -43,7 +43,7 @@ cargo build --release --locked -p wezterm-gui cargo build --release --locked -p wezterm-mux-server %install -TURTLE_TERM_STAGE_PREFIX=%{buildroot}%{_prefix} packaging/scripts/stage-linux-package.sh +TURTLE_TERM_STAGE_PREFIX=%{buildroot}%{_prefix} bash packaging/scripts/stage-linux-package.sh %check desktop-file-validate %{buildroot}%{_datadir}/applications/ai.sourceos.TurtleTerm.desktop diff --git a/packaging/scripts/build-arch-package.sh b/packaging/scripts/build-arch-package.sh index f529fbfd754..8db69a13b28 100644 --- a/packaging/scripts/build-arch-package.sh +++ b/packaging/scripts/build-arch-package.sh @@ -23,7 +23,7 @@ TURTLE_TERM_STAGE_PREFIX="$pkgroot/usr" \ TURTLE_TERM_ETC_DIR="$pkgroot/etc" \ TURTLE_TERM_RUNTIME_PREFIX="/usr" \ TURTLE_TERM_RUNTIME_ETC_DIR="/etc" \ - "$repo_root/packaging/scripts/stage-linux-package.sh" >/dev/null + bash "$repo_root/packaging/scripts/stage-linux-package.sh" >/dev/null cat > "$pkgroot/.PKGINFO" </dev/null + bash "$repo_root/packaging/scripts/stage-linux-package.sh" >/dev/null cat > "$debian_dir/control" </dev/null +TURTLE_TERM_STAGE_PREFIX=%{buildroot}/usr TURTLE_TERM_ETC_DIR=%{buildroot}/etc TURTLE_TERM_RUNTIME_PREFIX=/usr TURTLE_TERM_RUNTIME_ETC_DIR=/etc bash $repo_root/packaging/scripts/stage-linux-package.sh >/dev/null cp $repo_root/LICENSE.md %{buildroot}/LICENSE.md if [ -f $repo_root/THIRD_PARTY_NOTICES.md ]; then cp $repo_root/THIRD_PARTY_NOTICES.md %{buildroot}/THIRD_PARTY_NOTICES.md; fi @@ -62,7 +62,8 @@ if [ -f $repo_root/THIRD_PARTY_NOTICES.md ]; then cp $repo_root/THIRD_PARTY_NOTI /usr/share/turtle-term/ EOF -rpmbuild --define "_topdir $rpmbuild_root" -bb "$spec" >/dev/null +echo "building RPM with generated spec: $spec" >&2 +rpmbuild --define "_topdir $rpmbuild_root" -bb "$spec" rpm="$(find "$rpmbuild_root/RPMS" -name 'turtle-term-*.rpm' -print -quit)" test -n "$rpm" sha256sum "$rpm" > "$rpm.sha256" diff --git a/packaging/scripts/verify-arch-package.sh b/packaging/scripts/verify-arch-package.sh index 1b536022f90..461da04acdf 100644 --- a/packaging/scripts/verify-arch-package.sh +++ b/packaging/scripts/verify-arch-package.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -set -euo pipefail +set -eu repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" tmp="$(mktemp -d)" @@ -15,7 +15,7 @@ EOF done pkg="$(TURTLE_TERM_OUT_DIR="$tmp" TURTLE_TERM_VERSION="0.1.0" TURTLE_TERM_ARCH_ARCH="$(uname -m)" \ - "$repo_root/packaging/scripts/build-arch-package.sh")" + bash "$repo_root/packaging/scripts/build-arch-package.sh")" extract="$tmp/extract" test -f "$pkg" diff --git a/packaging/scripts/verify-deb-package.sh b/packaging/scripts/verify-deb-package.sh index 035107f1c19..d88f7357cdc 100644 --- a/packaging/scripts/verify-deb-package.sh +++ b/packaging/scripts/verify-deb-package.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -set -euo pipefail +set -eu repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" tmp="$(mktemp -d)" @@ -15,7 +15,7 @@ EOF done TURTLE_TERM_OUT_DIR="$tmp" TURTLE_TERM_VERSION="0.1.0" TURTLE_TERM_DEB_ARCH="amd64" \ - "$repo_root/packaging/scripts/build-deb-package.sh" >/dev/null + bash "$repo_root/packaging/scripts/build-deb-package.sh" >/dev/null deb="$tmp/turtle-term_0.1.0_amd64.deb" extract="$tmp/extract" diff --git a/packaging/scripts/verify-linux-package-layout.sh b/packaging/scripts/verify-linux-package-layout.sh index 42f39c25844..014d03789ca 100644 --- a/packaging/scripts/verify-linux-package-layout.sh +++ b/packaging/scripts/verify-linux-package-layout.sh @@ -15,7 +15,7 @@ EOF done prefix="$tmp/prefix" -TURTLE_TERM_STAGE_PREFIX="$prefix" "$repo_root/packaging/scripts/stage-linux-package.sh" >/dev/null +TURTLE_TERM_STAGE_PREFIX="$prefix" bash "$repo_root/packaging/scripts/stage-linux-package.sh" >/dev/null required_paths=( "$prefix/bin/turtleterm" diff --git a/packaging/scripts/verify-rpm-package.sh b/packaging/scripts/verify-rpm-package.sh index b9e14040b7c..254947a759e 100644 --- a/packaging/scripts/verify-rpm-package.sh +++ b/packaging/scripts/verify-rpm-package.sh @@ -1,10 +1,32 @@ #!/usr/bin/env bash -set -euo pipefail +set -eu repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" tmp="$(mktemp -d)" trap 'rm -rf "$tmp"' EXIT +require_rpm_path() { + local pattern="$1" + if ! rpm -qpl "$rpm" | grep -q "$pattern"; then + echo "missing expected RPM path matching: $pattern" >&2 + echo "RPM contents:" >&2 + rpm -qpl "$rpm" >&2 + exit 1 + fi +} + +require_file_text() { + local path="$1" + local pattern="$2" + if ! grep -q "$pattern" "$path"; then + echo "missing expected text in $path: $pattern" >&2 + echo "--- $path ---" >&2 + cat "$path" >&2 + echo "--- end $path ---" >&2 + exit 1 + fi +} + mkdir -p "$repo_root/target/release" for binary in wezterm wezterm-gui wezterm-mux-server; do cat > "$repo_root/target/release/$binary" <<'EOF' @@ -15,7 +37,7 @@ EOF done rpm="$(TURTLE_TERM_OUT_DIR="$tmp" TURTLE_TERM_VERSION="0.1.0" TURTLE_TERM_RPM_ARCH="$(uname -m)" \ - "$repo_root/packaging/scripts/build-rpm-package.sh")" + bash "$repo_root/packaging/scripts/build-rpm-package.sh")" extract="$tmp/extract" test -f "$rpm" @@ -37,13 +59,13 @@ PY rpm -qp --queryformat '%{NAME}\n' "$rpm" | grep -qx 'turtle-term' rpm -qp --queryformat '%{VERSION}\n' "$rpm" | grep -qx '0.1.0' -rpm -qpl "$rpm" | grep -q '^/usr/bin/turtleterm$' -rpm -qpl "$rpm" | grep -q '^/usr/bin/turtle-agentctl$' -rpm -qpl "$rpm" | grep -q '^/etc/turtle-term/turtleterm.lua$' -rpm -qpl "$rpm" | grep -q '^/usr/share/applications/ai.sourceos.TurtleTerm.desktop$' -rpm -qpl "$rpm" | grep -q '^/usr/share/metainfo/ai.sourceos.TurtleTerm.metainfo.xml$' -rpm -qpl "$rpm" | grep -q '^/usr/share/icons/hicolor/scalable/apps/ai.sourceos.TurtleTerm.svg$' -rpm -qpl "$rpm" | grep -q '^/usr/libexec/turtle-term/wezterm-gui$' +require_rpm_path '^/usr/bin/turtleterm$' +require_rpm_path '^/usr/bin/turtle-agentctl$' +require_rpm_path '^/etc/turtle-term/turtleterm.lua$' +require_rpm_path '^/usr/share/applications/ai.sourceos.TurtleTerm.desktop$' +require_rpm_path '^/usr/share/metainfo/ai.sourceos.TurtleTerm.metainfo.xml$' +require_rpm_path '^/usr/share/icons/hicolor/scalable/apps/ai.sourceos.TurtleTerm.svg$' +require_rpm_path '^/usr/libexec/turtle-term/wezterm-gui$' if rpm -qpl "$rpm" | grep -q '^/usr/bin/wezterm-gui$'; then echo 'private runtime leaked onto product PATH in rpm' >&2 @@ -52,11 +74,11 @@ fi mkdir -p "$extract" (cd "$extract" && rpm2cpio "$rpm" | cpio -idmu >/dev/null 2>&1) -grep -q 'TURTLE_TERM_RUNTIME_DIR="/usr/libexec/turtle-term"' "$extract/usr/bin/turtleterm" -grep -q 'TURTLETERM_CONFIG="/etc/turtle-term/turtleterm.lua"' "$extract/usr/bin/turtleterm" -grep -q 'exec "/usr/libexec/turtle-term/turtleterm"' "$extract/usr/bin/turtleterm" -grep -q 'TURTLE_TERM_RUNTIME_DIR="/usr/libexec/turtle-term"' "$extract/usr/bin/turtleterm-mux-server" -grep -q 'exec "/usr/libexec/turtle-term/turtleterm-mux-server"' "$extract/usr/bin/turtleterm-mux-server" +require_file_text "$extract/usr/bin/turtleterm" 'TURTLE_TERM_RUNTIME_DIR="/usr/libexec/turtle-term"' +require_file_text "$extract/usr/bin/turtleterm" 'TURTLETERM_CONFIG="/etc/turtle-term/turtleterm.lua"' +require_file_text "$extract/usr/bin/turtleterm" 'exec "/usr/libexec/turtle-term/turtleterm"' +require_file_text "$extract/usr/bin/turtleterm-mux-server" 'TURTLE_TERM_RUNTIME_DIR="/usr/libexec/turtle-term"' +require_file_text "$extract/usr/bin/turtleterm-mux-server" 'exec "/usr/libexec/turtle-term/turtleterm-mux-server"' if grep -R "$tmp\|BUILDROOT\|rpm-root\|arch-root\|deb-root" "$extract/usr/bin/turtleterm" "$extract/usr/bin/turtleterm-mux-server"; then echo 'buildroot path leaked into RPM launch wrappers' >&2 exit 1