From 4c19ab03085232bf6993e4e9172ef9e542042a9f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 7 May 2026 06:27:17 +0000 Subject: [PATCH 1/3] Initial plan From 429e95f64beea78f9e802bc0cda8aeadf520f113 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 7 May 2026 06:32:30 +0000 Subject: [PATCH 2/3] Add operation and diagnostics CLI tooling slice Agent-Logs-Url: https://github.com/SourceOS-Linux/sourceos-devtools/sessions/70962b03-33ca-44b5-9d46-fa01a151b017 Co-authored-by: mdheller <21163552+mdheller@users.noreply.github.com> --- README.md | 10 + bin/sourceos | 10 + bin/sourceosctl | 10 + docs/operation-conformance-runner.md | 32 ++- sourceosctl/commands/diagnostics.py | 62 +++++ sourceosctl/commands/operation.py | 241 ++++++++++++++++++ .../browser-capture-sample.json | 41 +++ .../local-agent-execution-sample.json | 64 +++++ .../terminal-command-sample.json | 41 +++ tests/test_diagnostics_cli.py | 45 ++++ tests/test_operation_cli.py | 58 +++++ 11 files changed, 608 insertions(+), 6 deletions(-) create mode 100755 bin/sourceos create mode 100644 sourceosctl/commands/diagnostics.py create mode 100644 sourceosctl/commands/operation.py create mode 100644 tests/fixtures/workspace-operation/browser-capture-sample.json create mode 100644 tests/fixtures/workspace-operation/local-agent-execution-sample.json create mode 100644 tests/fixtures/workspace-operation/terminal-command-sample.json create mode 100644 tests/test_diagnostics_cli.py create mode 100644 tests/test_operation_cli.py diff --git a/README.md b/README.md index 101258c..a561951 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,11 @@ sourceosctl [--version] [] [options] | `sourceosctl office convert --to --execute --policy-ok` | Run guarded local LibreOffice conversion and emit OfficeArtifactEvidence | | `sourceosctl office inspect ` | Inspect a local office artifact file and hash it | | `sourceosctl office evidence inspect ` | Inspect Office Plane evidence JSON (read-only) | +| `sourceosctl operation conformance --contracts-dir ../prophet-core-contracts` | Run Workspace Operation fixture bundle conformance checks | +| `sourceosctl operation validate-fixture ` | Validate one Workspace Operation fixture | +| `sourceosctl operation replay-fixture --surface terminal-command\|browser-capture\|local-agent-execution` | Generate a local replay fixture starter | +| `sourceosctl operation scaffold-adapter ` | Scaffold starter adapter skeletons | +| `sourceosctl diagnostics redact --output ` | Export redacted diagnostics (cookies/tokens/keys/IDs/prompts/policy snippets) | ### Running from the repo @@ -127,6 +132,11 @@ python3 bin/sourceosctl office generate --execute --policy-ok --artifact-type sp python3 bin/sourceosctl office generate --execute --policy-ok --artifact-type slide-deck --format pptx --title "Demo Deck" --evidence-out ./office-pptx-evidence.json python3 bin/sourceosctl office convert ./example.docx --to pdf --dry-run python3 bin/sourceosctl office convert ./example.docx --to pdf --execute --policy-ok --evidence-out ./office-convert-evidence.json +python3 bin/sourceos operation conformance --contracts-dir ../prophet-core-contracts +python3 bin/sourceos operation validate-fixture tests/fixtures/workspace-operation/minimal-operation.json --structural-only +python3 bin/sourceos operation replay-fixture ./tmp-operation-replay.json --surface terminal-command +python3 bin/sourceos operation scaffold-adapter ./tmp-adapter +python3 bin/sourceos diagnostics redact ./diagnostic.log --output ./diagnostic.redacted.log ``` ### Local Model Door defaults diff --git a/bin/sourceos b/bin/sourceos new file mode 100755 index 0000000..753197f --- /dev/null +++ b/bin/sourceos @@ -0,0 +1,10 @@ +#!/usr/bin/env python3 +"""sourceos alias entry-point script.""" + +import os +import runpy +import sys + +repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.argv[0] = "sourceos" +runpy.run_path(os.path.join(repo_root, "bin", "sourceosctl"), run_name="__main__") diff --git a/bin/sourceosctl b/bin/sourceosctl index 6eb0994..46cfeec 100755 --- a/bin/sourceosctl +++ b/bin/sourceosctl @@ -44,6 +44,16 @@ if len(sys.argv) > 1 and sys.argv[1] == "reasoning": sys.exit(reasoning_main(sys.argv[2:])) +if len(sys.argv) > 1 and sys.argv[1] == "operation": + from sourceosctl.commands.operation import operation_main + + sys.exit(operation_main(sys.argv[2:])) + +if len(sys.argv) > 1 and sys.argv[1] == "diagnostics": + from sourceosctl.commands.diagnostics import diagnostics_main + + sys.exit(diagnostics_main(sys.argv[2:])) + if len(sys.argv) > 1 and sys.argv[1] == "network": from sourceosctl.commands.network import network_main diff --git a/docs/operation-conformance-runner.md b/docs/operation-conformance-runner.md index ebdde44..f896e23 100644 --- a/docs/operation-conformance-runner.md +++ b/docs/operation-conformance-runner.md @@ -42,6 +42,28 @@ python3 tools/sourceos_operation_conformance.py \ --schemas-dir ../prophet-core-contracts/schemas ``` +From `~/dev/sourceos-devtools`, the first-class CLI wrappers are: + +```bash +python3 bin/sourceos operation conformance \ + --contracts-dir ../prophet-core-contracts + +python3 bin/sourceos operation validate-fixture \ + tests/fixtures/workspace-operation/minimal-operation.json \ + --structural-only + +python3 bin/sourceos operation replay-fixture \ + ./tmp-operation-replay.json \ + --surface terminal-command + +python3 bin/sourceos operation scaffold-adapter \ + ./tmp-adapter + +python3 bin/sourceos diagnostics redact \ + ./diagnostic.log \ + --output ./diagnostic.redacted.log +``` + ## What structural validation checks - Every fixture is operation-centered. @@ -69,10 +91,8 @@ If `jsonschema` is installed, the runner also validates objects against: This optional path is the bridge between the lightweight contract validation currently in `prophet-core-contracts` and a reusable SourceOS conformance runner that downstream repos can invoke. -## Next work +## Included sample fixtures -- Add a CLI wrapper once the repo's command layout is standardized. -- Add adapter scaffolding commands. -- Add redaction CLI for diagnostic bundles. -- Emit machine-readable validation reports. -- Add fixture-generation helpers for terminal, browser, local agent-machine, sync, and release operations. +- `tests/fixtures/workspace-operation/terminal-command-sample.json` +- `tests/fixtures/workspace-operation/browser-capture-sample.json` +- `tests/fixtures/workspace-operation/local-agent-execution-sample.json` diff --git a/sourceosctl/commands/diagnostics.py b/sourceosctl/commands/diagnostics.py new file mode 100644 index 0000000..7e3df3b --- /dev/null +++ b/sourceosctl/commands/diagnostics.py @@ -0,0 +1,62 @@ +"""Diagnostic redaction CLI helpers.""" + +from __future__ import annotations + +import argparse +import json +import re +from pathlib import Path + + +def _redact(raw: str) -> tuple[str, dict[str, int]]: + patterns: list[tuple[str, re.Pattern[str], str]] = [ + ("cookies", re.compile(r"(?i)(\bcookie\s*[:=]\s*)([^\n]+)"), r"\1"), + ("bearer", re.compile(r"(?i)\bbearer\s+[a-z0-9._~+/=-]+"), "Bearer "), + ("oauth", re.compile(r'(?i)("?(?:access_token|refresh_token|oauth_token|id_token)"?\s*[:=]\s*)(".*?"|[^,\s]+)'), r'\1""'), + ("api_keys", re.compile(r'(?i)("?(?:api[_-]?key|x-api-key|apikey|client_secret|authorization)"?\s*[:=]\s*)(".*?"|[^,\s]+)'), r'\1""'), + ("secrets", re.compile(r'(?i)("?(?:secret|password|token)"?\s*[:=]\s*)(".*?"|[^,\s]+)'), r'\1""'), + ("sensitive_ids", re.compile(r'(?i)("?(?:user|account|session|device|customer|tenant|workspace|organization|org|principal|subject)_id"?\s*[:=]\s*)(".*?"|[^,\s]+)'), r'\1""'), + ("model_prompts", re.compile(r'(?i)("?(?:prompt|model_prompt|system_prompt|user_prompt)"?\s*[:=]\s*)(".*?"|[^,\n]+)'), r'\1""'), + ("policy_marked", re.compile(r"(?is).*?"), ""), + ] + counts: dict[str, int] = {} + redacted = raw + for name, pattern, replacement in patterns: + redacted, count = pattern.subn(replacement, redacted) + if count: + counts[name] = count + return redacted, counts + + +def redact_cmd(args: argparse.Namespace) -> int: + input_path = Path(args.input).expanduser().resolve() + if not input_path.exists(): + print(json.dumps({"type": "DiagnosticRedaction", "result": "fail", "errors": [f"missing input file: {input_path}"]}, indent=2, sort_keys=True)) + return 1 + raw = input_path.read_text(encoding="utf-8") + redacted, counts = _redact(raw) + + if args.output: + output_path = Path(args.output).expanduser().resolve() + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text(redacted, encoding="utf-8") + print(json.dumps({"type": "DiagnosticRedaction", "result": "pass", "input": str(input_path), "output": str(output_path), "redactionCounts": counts}, indent=2, sort_keys=True)) + else: + print(redacted) + return 0 + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(prog="sourceosctl diagnostics", description="Diagnostic helpers") + sub = parser.add_subparsers(dest="diagnostics_command", required=True) + redact_p = sub.add_parser("redact", help="Redact sensitive tokens and snippets from diagnostic exports") + redact_p.add_argument("input") + redact_p.add_argument("--output", default=None) + redact_p.set_defaults(func=redact_cmd) + return parser + + +def diagnostics_main(argv: list[str] | None = None) -> int: + parser = build_parser() + args = parser.parse_args(argv) + return args.func(args) or 0 diff --git a/sourceosctl/commands/operation.py b/sourceosctl/commands/operation.py new file mode 100644 index 0000000..b218068 --- /dev/null +++ b/sourceosctl/commands/operation.py @@ -0,0 +1,241 @@ +"""Workspace Operation Plane developer tooling commands.""" + +from __future__ import annotations + +import argparse +import importlib.util +import json +import sys +from pathlib import Path +from typing import Any, Dict, List + + +def _repo_root() -> Path: + return Path(__file__).resolve().parents[2] + + +def _load_conformance_module(): + module_path = _repo_root() / "tools" / "sourceos_operation_conformance.py" + spec = importlib.util.spec_from_file_location("sourceos_operation_conformance", module_path) + if spec is None or spec.loader is None: + raise RuntimeError(f"unable to load conformance module from {module_path}") + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + return module + + +def _as_list(value: Any) -> list[Any]: + if value is None: + return [] + if isinstance(value, list): + return value + return [value] + + +def _validate_state_machine(path: Path, data: Dict[str, Any], errors: List[str]) -> None: + operation = data.get("operation") or {} + operation_status = operation.get("status") + allowed = { + "pending", + "in_progress", + "awaiting_decision", + "blocked", + "retrying", + "completed", + "failed", + "cancelled", + } + if operation_status and operation_status not in allowed: + errors.append(f"{path}: operation.status is unknown: {operation_status}") + + task_ids = set(operation.get("task_ids") or []) + events = _as_list(data.get("event")) + list(data.get("events") or []) + for index, event in enumerate(events, start=1): + if not isinstance(event, dict): + errors.append(f"{path}: event[{index}] must be an object") + continue + if event.get("operation_id") and event.get("operation_id") != operation.get("operation_id"): + errors.append(f"{path}: event[{index}] operation_id mismatch") + event_task_id = event.get("task_id") + if event_task_id and task_ids and event_task_id not in task_ids: + errors.append(f"{path}: event[{index}] task_id not listed in operation.task_ids: {event_task_id}") + if not event.get("event_type"): + errors.append(f"{path}: event[{index}] event_type is required") + + +def validate_fixture(args: argparse.Namespace) -> int: + module = _load_conformance_module() + fixture = Path(args.fixture).expanduser().resolve() + if not fixture.exists(): + print(json.dumps({"type": "OperationFixtureValidation", "result": "fail", "errors": [f"missing fixture: {fixture}"]}, indent=2, sort_keys=True)) + return 1 + + results = module.ValidationErrorSet() + try: + data = module.load_json(fixture) + except Exception as exc: # noqa: BLE001 + print(json.dumps({"type": "OperationFixtureValidation", "result": "fail", "errors": [f"{fixture}: failed to parse JSON: {exc}"]}, indent=2, sort_keys=True)) + return 1 + + module.validate_structural(fixture, data, results) + _validate_state_machine(fixture, data, results.errors) + + if not args.structural_only: + schemas_dir = Path(args.schemas_dir).expanduser().resolve() if args.schemas_dir else None + module.optional_schema_validation(module.Paths(examples_dir=fixture.parent, schemas_dir=schemas_dir), [fixture], results) + + report = { + "type": "OperationFixtureValidation", + "fixture": str(fixture), + "result": "pass" if results.ok() else "fail", + "errors": results.errors, + "warnings": results.warnings, + } + print(json.dumps(report, indent=2, sort_keys=True)) + return 0 if results.ok() else 1 + + +def replay_fixture(args: argparse.Namespace) -> int: + output = Path(args.output).expanduser().resolve() + if output.exists() and not args.force: + print(json.dumps({"type": "OperationReplayFixture", "result": "fail", "errors": [f"output file exists: {output}; use --force to overwrite"]}, indent=2, sort_keys=True)) + return 1 + + surface_prefix = args.surface.replace("-", "_") + operation_id = f"op_{surface_prefix}_fixture_001" + task_id = f"task_{surface_prefix}_fixture_001" + artifact_id = f"artifact_{surface_prefix}_fixture_001" + event_id = f"event_{surface_prefix}_fixture_001" + payload = { + "operation": { + "schema_version": "0.1.0", + "operation_id": operation_id, + "operation_type": f"sourceos.{args.surface}.replay", + "status": "completed", + "idempotency_key": f"idem_{operation_id}", + "task_ids": [task_id], + "artifact_ids": [artifact_id], + "decision_ids": [], + "policy_gate_ids": [], + }, + "task": { + "schema_version": "0.1.0", + "task_id": task_id, + "operation_id": operation_id, + "task_type": f"sourceos.{args.surface}.task", + "status": "completed", + "retryable": True, + "idempotency_key": f"idem_{task_id}", + }, + "event": { + "schema_version": "0.1.0", + "event_id": event_id, + "event_type": f"{args.surface}.completed", + "operation_id": operation_id, + "task_id": task_id, + }, + "artifact": { + "schema_version": "0.1.0", + "artifact_id": artifact_id, + "artifact_type": "diagnostic_bundle", + "created_by_operation_id": operation_id, + "admission_state": "admitted", + "activation_state": "active", + }, + } + + output.parent.mkdir(parents=True, exist_ok=True) + output.write_text(json.dumps(payload, indent=2, sort_keys=True), encoding="utf-8") + print(json.dumps({"type": "OperationReplayFixture", "result": "pass", "surface": args.surface, "output": str(output)}, indent=2, sort_keys=True)) + return 0 + + +def scaffold_adapter(args: argparse.Namespace) -> int: + out_dir = Path(args.output_dir).expanduser().resolve() + out_dir.mkdir(parents=True, exist_ok=True) + templates = { + "terminal-command": { + "adapter_type": "terminal-command", + "command": ["bash", "-lc", "echo SourceOS operation adapter terminal skeleton"], + "capture": {"stdout": True, "stderr": True, "exit_code": True}, + }, + "browser-capture": { + "adapter_type": "browser-capture", + "capture": {"network": True, "console": True, "dom_snapshot": True}, + "entrypoint": "https://example.local/workroom", + }, + "local-agent-execution": { + "adapter_type": "local-agent-execution", + "agent_runtime": "sourceos-local-agent", + "plan_ref": "urn:srcos:agent-plan:local-default", + "collect": ["events", "artifacts", "policy-gates"], + }, + } + + written: list[str] = [] + for name, payload in templates.items(): + path = out_dir / f"{name}.adapter.json" + if path.exists() and not args.force: + print(json.dumps({"type": "OperationAdapterScaffold", "result": "fail", "errors": [f"output file exists: {path}; use --force to overwrite"]}, indent=2, sort_keys=True)) + return 1 + path.write_text(json.dumps(payload, indent=2, sort_keys=True), encoding="utf-8") + written.append(str(path)) + + print(json.dumps({"type": "OperationAdapterScaffold", "result": "pass", "outputDir": str(out_dir), "files": written}, indent=2, sort_keys=True)) + return 0 + + +def conformance(args: argparse.Namespace) -> int: + module = _load_conformance_module() + argv: list[str] = [] + if args.contracts_dir: + argv.extend(["--contracts-dir", args.contracts_dir]) + if args.examples_dir: + argv.extend(["--examples-dir", args.examples_dir]) + if args.schemas_dir: + argv.extend(["--schemas-dir", args.schemas_dir]) + if args.structural_only: + argv.append("--structural-only") + return int(module.main(argv)) + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(prog="sourceosctl operation", description="Workspace Operation Plane tooling") + sub = parser.add_subparsers(dest="operation_command", required=True) + + validate_p = sub.add_parser("validate-fixture", help="Validate a Workspace Operation fixture JSON") + validate_p.add_argument("fixture") + validate_p.add_argument("--schemas-dir", default="../prophet-core-contracts/schemas") + validate_p.add_argument("--structural-only", action="store_true", default=False) + validate_p.set_defaults(func=validate_fixture) + + replay_p = sub.add_parser("replay-fixture", help="Generate a local replay fixture skeleton") + replay_p.add_argument("output") + replay_p.add_argument( + "--surface", + choices=["terminal-command", "browser-capture", "local-agent-execution"], + default="terminal-command", + ) + replay_p.add_argument("--force", action="store_true", default=False) + replay_p.set_defaults(func=replay_fixture) + + scaffold_p = sub.add_parser("scaffold-adapter", help="Scaffold starter adapter skeleton files") + scaffold_p.add_argument("output_dir") + scaffold_p.add_argument("--force", action="store_true", default=False) + scaffold_p.set_defaults(func=scaffold_adapter) + + conformance_p = sub.add_parser("conformance", help="Run fixture bundle conformance checks") + conformance_p.add_argument("--contracts-dir", default=None) + conformance_p.add_argument("--examples-dir", default="../prophet-core-contracts/examples/workspace-operation") + conformance_p.add_argument("--schemas-dir", default="../prophet-core-contracts/schemas") + conformance_p.add_argument("--structural-only", action="store_true", default=False) + conformance_p.set_defaults(func=conformance) + + return parser + + +def operation_main(argv: list[str] | None = None) -> int: + parser = build_parser() + args = parser.parse_args(argv) + return args.func(args) or 0 diff --git a/tests/fixtures/workspace-operation/browser-capture-sample.json b/tests/fixtures/workspace-operation/browser-capture-sample.json new file mode 100644 index 0000000..be5dca2 --- /dev/null +++ b/tests/fixtures/workspace-operation/browser-capture-sample.json @@ -0,0 +1,41 @@ +{ + "operation": { + "schema_version": "0.1.0", + "operation_id": "op_browser_capture_sample_001", + "operation_type": "sourceos.browser-capture.replay", + "status": "completed", + "idempotency_key": "idem_op_browser_capture_sample_001", + "task_ids": [ + "task_browser_capture_sample_001" + ], + "artifact_ids": [ + "artifact_browser_capture_sample_001" + ], + "decision_ids": [], + "policy_gate_ids": [] + }, + "task": { + "schema_version": "0.1.0", + "task_id": "task_browser_capture_sample_001", + "operation_id": "op_browser_capture_sample_001", + "task_type": "sourceos.browser-capture.task", + "status": "completed", + "retryable": true, + "idempotency_key": "idem_task_browser_capture_sample_001" + }, + "event": { + "schema_version": "0.1.0", + "event_id": "event_browser_capture_sample_001", + "event_type": "browser-capture.completed", + "operation_id": "op_browser_capture_sample_001", + "task_id": "task_browser_capture_sample_001" + }, + "artifact": { + "schema_version": "0.1.0", + "artifact_id": "artifact_browser_capture_sample_001", + "artifact_type": "diagnostic_bundle", + "created_by_operation_id": "op_browser_capture_sample_001", + "admission_state": "admitted", + "activation_state": "active" + } +} diff --git a/tests/fixtures/workspace-operation/local-agent-execution-sample.json b/tests/fixtures/workspace-operation/local-agent-execution-sample.json new file mode 100644 index 0000000..7516c44 --- /dev/null +++ b/tests/fixtures/workspace-operation/local-agent-execution-sample.json @@ -0,0 +1,64 @@ +{ + "operation": { + "schema_version": "0.1.0", + "operation_id": "op_local_agent_execution_sample_001", + "operation_type": "sourceos.local-agent-execution.replay", + "status": "completed", + "idempotency_key": "idem_op_local_agent_execution_sample_001", + "task_ids": [ + "task_local_agent_execution_sample_001" + ], + "artifact_ids": [ + "artifact_local_agent_execution_sample_001" + ], + "decision_ids": [ + "decision_local_agent_execution_sample_001" + ], + "policy_gate_ids": [ + "gate_local_agent_execution_sample_001" + ] + }, + "task": { + "schema_version": "0.1.0", + "task_id": "task_local_agent_execution_sample_001", + "operation_id": "op_local_agent_execution_sample_001", + "task_type": "sourceos.local-agent-execution.task", + "status": "completed", + "retryable": true, + "idempotency_key": "idem_task_local_agent_execution_sample_001" + }, + "event": { + "schema_version": "0.1.0", + "event_id": "event_local_agent_execution_sample_001", + "event_type": "local-agent-execution.completed", + "operation_id": "op_local_agent_execution_sample_001", + "task_id": "task_local_agent_execution_sample_001" + }, + "artifact": { + "schema_version": "0.1.0", + "artifact_id": "artifact_local_agent_execution_sample_001", + "artifact_type": "diagnostic_bundle", + "created_by_operation_id": "op_local_agent_execution_sample_001", + "admission_state": "admitted", + "activation_state": "active" + }, + "decision": { + "schema_version": "0.1.0", + "decision_id": "decision_local_agent_execution_sample_001", + "status": "approved", + "options": [ + "approve", + "reject" + ] + }, + "policy_gate": { + "schema_version": "0.1.0", + "gate_id": "gate_local_agent_execution_sample_001", + "status": "cleared", + "responsible_actor": "sourceos-operator", + "remediation_options": [ + "rerun-local-agent", + "inspect-evidence" + ] + } +} diff --git a/tests/fixtures/workspace-operation/terminal-command-sample.json b/tests/fixtures/workspace-operation/terminal-command-sample.json new file mode 100644 index 0000000..63b3b5a --- /dev/null +++ b/tests/fixtures/workspace-operation/terminal-command-sample.json @@ -0,0 +1,41 @@ +{ + "operation": { + "schema_version": "0.1.0", + "operation_id": "op_terminal_command_sample_001", + "operation_type": "sourceos.terminal-command.replay", + "status": "completed", + "idempotency_key": "idem_op_terminal_command_sample_001", + "task_ids": [ + "task_terminal_command_sample_001" + ], + "artifact_ids": [ + "artifact_terminal_command_sample_001" + ], + "decision_ids": [], + "policy_gate_ids": [] + }, + "task": { + "schema_version": "0.1.0", + "task_id": "task_terminal_command_sample_001", + "operation_id": "op_terminal_command_sample_001", + "task_type": "sourceos.terminal-command.task", + "status": "completed", + "retryable": true, + "idempotency_key": "idem_task_terminal_command_sample_001" + }, + "event": { + "schema_version": "0.1.0", + "event_id": "event_terminal_command_sample_001", + "event_type": "terminal-command.completed", + "operation_id": "op_terminal_command_sample_001", + "task_id": "task_terminal_command_sample_001" + }, + "artifact": { + "schema_version": "0.1.0", + "artifact_id": "artifact_terminal_command_sample_001", + "artifact_type": "diagnostic_bundle", + "created_by_operation_id": "op_terminal_command_sample_001", + "admission_state": "admitted", + "activation_state": "active" + } +} diff --git a/tests/test_diagnostics_cli.py b/tests/test_diagnostics_cli.py new file mode 100644 index 0000000..108aac2 --- /dev/null +++ b/tests/test_diagnostics_cli.py @@ -0,0 +1,45 @@ +"""Unit tests for sourceosctl diagnostics commands.""" + +import pathlib +import sys +import tempfile +import unittest + +_REPO_ROOT = pathlib.Path(__file__).parent.parent +sys.path.insert(0, str(_REPO_ROOT)) + +from sourceosctl.commands import diagnostics + + +class TestDiagnosticsCommands(unittest.TestCase): + def test_redact_masks_sensitive_fields(self): + with tempfile.TemporaryDirectory() as tmp: + in_path = pathlib.Path(tmp) / "diagnostic.log" + out_path = pathlib.Path(tmp) / "diagnostic.redacted.log" + in_path.write_text( + "\n".join( + [ + "cookie: session=abc123", + "Authorization: Bearer super-secret-bearer-token", + 'api_key: "abc123"', + 'user_id: "user-42"', + 'prompt: "Summarize private model prompt"', + "never expose this snippet", + ] + ), + encoding="utf-8", + ) + rc = diagnostics.diagnostics_main(["redact", str(in_path), "--output", str(out_path)]) + self.assertEqual(rc, 0) + redacted = out_path.read_text(encoding="utf-8") + self.assertNotIn("abc123", redacted) + self.assertNotIn("super-secret-bearer-token", redacted) + self.assertNotIn("Summarize private model prompt", redacted) + self.assertIn("", redacted) + self.assertIn("", redacted) + self.assertIn("", redacted) + self.assertIn("", redacted) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_operation_cli.py b/tests/test_operation_cli.py new file mode 100644 index 0000000..87f832c --- /dev/null +++ b/tests/test_operation_cli.py @@ -0,0 +1,58 @@ +"""Unit tests for sourceosctl operation commands.""" + +import json +import pathlib +import sys +import tempfile +import unittest + +_REPO_ROOT = pathlib.Path(__file__).parent.parent +sys.path.insert(0, str(_REPO_ROOT)) + +from sourceosctl.commands import operation + + +FIXTURE = _REPO_ROOT / "tests" / "fixtures" / "workspace-operation" / "minimal-operation.json" + + +class TestOperationCommands(unittest.TestCase): + def test_validate_fixture_passes_for_smoke_fixture(self): + self.assertEqual(operation.operation_main(["validate-fixture", str(FIXTURE), "--structural-only"]), 0) + + def test_conformance_passes_for_local_fixture_dir(self): + fixture_dir = FIXTURE.parent + self.assertEqual( + operation.operation_main( + [ + "conformance", + "--examples-dir", + str(fixture_dir), + "--schemas-dir", + str(_REPO_ROOT / "fixtures" / "schemas"), + "--structural-only", + ] + ), + 0, + ) + + def test_replay_fixture_generates_browser_fixture(self): + with tempfile.TemporaryDirectory() as tmp: + out = pathlib.Path(tmp) / "browser-replay.json" + rc = operation.operation_main(["replay-fixture", str(out), "--surface", "browser-capture"]) + self.assertEqual(rc, 0) + payload = json.loads(out.read_text(encoding="utf-8")) + self.assertEqual(payload["operation"]["operation_type"], "sourceos.browser-capture.replay") + self.assertEqual(payload["event"]["event_type"], "browser-capture.completed") + + def test_scaffold_adapter_writes_three_skeletons(self): + with tempfile.TemporaryDirectory() as tmp: + out_dir = pathlib.Path(tmp) / "adapter" + rc = operation.operation_main(["scaffold-adapter", str(out_dir)]) + self.assertEqual(rc, 0) + self.assertTrue((out_dir / "terminal-command.adapter.json").exists()) + self.assertTrue((out_dir / "browser-capture.adapter.json").exists()) + self.assertTrue((out_dir / "local-agent-execution.adapter.json").exists()) + + +if __name__ == "__main__": + unittest.main() From 6f460b10e6e45c68d0a998e9de4b19c61fc66674 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 7 May 2026 06:39:24 +0000 Subject: [PATCH 3/3] Address validation feedback on operation and diagnostics commands Agent-Logs-Url: https://github.com/SourceOS-Linux/sourceos-devtools/sessions/70962b03-33ca-44b5-9d46-fa01a151b017 Co-authored-by: mdheller <21163552+mdheller@users.noreply.github.com> --- sourceosctl/commands/diagnostics.py | 30 ++++++++++++++++++----------- sourceosctl/commands/operation.py | 29 +++++++++++++++++----------- tests/test_diagnostics_cli.py | 22 +++++++++++++++++++++ tests/test_operation_cli.py | 2 +- 4 files changed, 60 insertions(+), 23 deletions(-) diff --git a/sourceosctl/commands/diagnostics.py b/sourceosctl/commands/diagnostics.py index 7e3df3b..d92b436 100644 --- a/sourceosctl/commands/diagnostics.py +++ b/sourceosctl/commands/diagnostics.py @@ -8,20 +8,28 @@ from pathlib import Path +REDACTION_PATTERNS: list[tuple[str, re.Pattern[str], str]] = [ + ("cookies", re.compile(r"(?i)(\bcookie\s*[:=]\s*)([^\n]+)"), r"\1"), + ("bearer", re.compile(r"(?i)\bbearer\s+[a-zA-Z0-9._~+/=-]+"), "Bearer "), + # Token-bearing identity fields (quoted JSON value or plain token blob). + ("oauth", re.compile(r'(?is)("?(?:access_token|refresh_token|oauth_token|id_token)"?\s*[:=]\s*)("(?:[^"\\]|\\.)*"|[^,\s]+)'), r'\1""'), + ("api_keys", re.compile(r'(?is)("?(?:api[_-]?key|x-api-key|apikey|client_secret|authorization)"?\s*[:=]\s*)("(?:[^"\\]|\\.)*"|[^,\s]+)'), r'\1""'), + ("secrets", re.compile(r'(?is)("?(?:secret|password|token)"?\s*[:=]\s*)("(?:[^"\\]|\\.)*"|[^,\s]+)'), r'\1""'), + ("sensitive_ids", re.compile(r'(?is)("?(?:user|account|session|device|customer|tenant|workspace|organization|org|principal|subject)_id"?\s*[:=]\s*)("(?:[^"\\]|\\.)*"|[^,\s]+)'), r'\1""'), + # Prompt-bearing fields, including escaped multi-line quoted strings. + ( + "model_prompts", + re.compile(r'(?is)("?(?:prompt|model_prompt|system_prompt|user_prompt)"?\s*[:=]\s*)("(?:[^"\\]|\\.)*"|[^,\n]+)'), + r'\1""', + ), + ("policy_marked", re.compile(r"(?is).*?"), ""), +] + + def _redact(raw: str) -> tuple[str, dict[str, int]]: - patterns: list[tuple[str, re.Pattern[str], str]] = [ - ("cookies", re.compile(r"(?i)(\bcookie\s*[:=]\s*)([^\n]+)"), r"\1"), - ("bearer", re.compile(r"(?i)\bbearer\s+[a-z0-9._~+/=-]+"), "Bearer "), - ("oauth", re.compile(r'(?i)("?(?:access_token|refresh_token|oauth_token|id_token)"?\s*[:=]\s*)(".*?"|[^,\s]+)'), r'\1""'), - ("api_keys", re.compile(r'(?i)("?(?:api[_-]?key|x-api-key|apikey|client_secret|authorization)"?\s*[:=]\s*)(".*?"|[^,\s]+)'), r'\1""'), - ("secrets", re.compile(r'(?i)("?(?:secret|password|token)"?\s*[:=]\s*)(".*?"|[^,\s]+)'), r'\1""'), - ("sensitive_ids", re.compile(r'(?i)("?(?:user|account|session|device|customer|tenant|workspace|organization|org|principal|subject)_id"?\s*[:=]\s*)(".*?"|[^,\s]+)'), r'\1""'), - ("model_prompts", re.compile(r'(?i)("?(?:prompt|model_prompt|system_prompt|user_prompt)"?\s*[:=]\s*)(".*?"|[^,\n]+)'), r'\1""'), - ("policy_marked", re.compile(r"(?is).*?"), ""), - ] counts: dict[str, int] = {} redacted = raw - for name, pattern, replacement in patterns: + for name, pattern, replacement in REDACTION_PATTERNS: redacted, count = pattern.subn(replacement, redacted) if count: counts[name] = count diff --git a/sourceosctl/commands/operation.py b/sourceosctl/commands/operation.py index b218068..ee420a2 100644 --- a/sourceosctl/commands/operation.py +++ b/sourceosctl/commands/operation.py @@ -17,8 +17,10 @@ def _repo_root() -> Path: def _load_conformance_module(): module_path = _repo_root() / "tools" / "sourceos_operation_conformance.py" spec = importlib.util.spec_from_file_location("sourceos_operation_conformance", module_path) - if spec is None or spec.loader is None: - raise RuntimeError(f"unable to load conformance module from {module_path}") + if spec is None: + raise RuntimeError(f"unable to create conformance module spec from {module_path}") + if spec.loader is None: + raise RuntimeError(f"conformance module spec has no loader: {module_path}") module = importlib.util.module_from_spec(spec) sys.modules[spec.name] = module spec.loader.exec_module(module) @@ -34,6 +36,7 @@ def _as_list(value: Any) -> list[Any]: def _validate_state_machine(path: Path, data: Dict[str, Any], errors: List[str]) -> None: + """Validate lightweight status/event consistency beyond structural shape checks.""" operation = data.get("operation") or {} operation_status = operation.get("status") allowed = { @@ -74,7 +77,7 @@ def validate_fixture(args: argparse.Namespace) -> int: results = module.ValidationErrorSet() try: data = module.load_json(fixture) - except Exception as exc: # noqa: BLE001 + except (json.JSONDecodeError, OSError, ValueError) as exc: print(json.dumps({"type": "OperationFixtureValidation", "result": "fail", "errors": [f"{fixture}: failed to parse JSON: {exc}"]}, indent=2, sort_keys=True)) return 1 @@ -102,11 +105,11 @@ def replay_fixture(args: argparse.Namespace) -> int: print(json.dumps({"type": "OperationReplayFixture", "result": "fail", "errors": [f"output file exists: {output}; use --force to overwrite"]}, indent=2, sort_keys=True)) return 1 - surface_prefix = args.surface.replace("-", "_") - operation_id = f"op_{surface_prefix}_fixture_001" - task_id = f"task_{surface_prefix}_fixture_001" - artifact_id = f"artifact_{surface_prefix}_fixture_001" - event_id = f"event_{surface_prefix}_fixture_001" + surface_normalized = args.surface.replace("-", "_") + operation_id = f"op_{surface_normalized}_fixture_001" + task_id = f"task_{surface_normalized}_fixture_001" + artifact_id = f"artifact_{surface_normalized}_fixture_001" + event_id = f"event_{surface_normalized}_fixture_001" payload = { "operation": { "schema_version": "0.1.0", @@ -201,12 +204,13 @@ def conformance(args: argparse.Namespace) -> int: def build_parser() -> argparse.ArgumentParser: + default_examples_dir = _repo_root() / "tests" / "fixtures" / "workspace-operation" parser = argparse.ArgumentParser(prog="sourceosctl operation", description="Workspace Operation Plane tooling") sub = parser.add_subparsers(dest="operation_command", required=True) validate_p = sub.add_parser("validate-fixture", help="Validate a Workspace Operation fixture JSON") validate_p.add_argument("fixture") - validate_p.add_argument("--schemas-dir", default="../prophet-core-contracts/schemas") + validate_p.add_argument("--schemas-dir", default=None) validate_p.add_argument("--structural-only", action="store_true", default=False) validate_p.set_defaults(func=validate_fixture) @@ -227,8 +231,11 @@ def build_parser() -> argparse.ArgumentParser: conformance_p = sub.add_parser("conformance", help="Run fixture bundle conformance checks") conformance_p.add_argument("--contracts-dir", default=None) - conformance_p.add_argument("--examples-dir", default="../prophet-core-contracts/examples/workspace-operation") - conformance_p.add_argument("--schemas-dir", default="../prophet-core-contracts/schemas") + conformance_p.add_argument( + "--examples-dir", + default=str(default_examples_dir), + ) + conformance_p.add_argument("--schemas-dir", default=None) conformance_p.add_argument("--structural-only", action="store_true", default=False) conformance_p.set_defaults(func=conformance) diff --git a/tests/test_diagnostics_cli.py b/tests/test_diagnostics_cli.py index 108aac2..37025d9 100644 --- a/tests/test_diagnostics_cli.py +++ b/tests/test_diagnostics_cli.py @@ -1,5 +1,6 @@ """Unit tests for sourceosctl diagnostics commands.""" +import json import pathlib import sys import tempfile @@ -40,6 +41,27 @@ def test_redact_masks_sensitive_fields(self): self.assertIn("", redacted) self.assertIn("", redacted) + def test_redact_json_preserves_json_shape(self): + with tempfile.TemporaryDirectory() as tmp: + in_path = pathlib.Path(tmp) / "diagnostic.json" + out_path = pathlib.Path(tmp) / "diagnostic.redacted.json" + in_path.write_text( + json.dumps( + { + "user_id": "user-123", + "prompt": "Line one\nLine two", + "authorization": "Bearer abcDEF123", + } + ), + encoding="utf-8", + ) + rc = diagnostics.diagnostics_main(["redact", str(in_path), "--output", str(out_path)]) + self.assertEqual(rc, 0) + payload = json.loads(out_path.read_text(encoding="utf-8")) + self.assertEqual(payload["user_id"], "") + self.assertEqual(payload["prompt"], "") + self.assertEqual(payload["authorization"], "") + if __name__ == "__main__": unittest.main() diff --git a/tests/test_operation_cli.py b/tests/test_operation_cli.py index 87f832c..ef36e87 100644 --- a/tests/test_operation_cli.py +++ b/tests/test_operation_cli.py @@ -16,7 +16,7 @@ class TestOperationCommands(unittest.TestCase): - def test_validate_fixture_passes_for_smoke_fixture(self): + def test_validate_fixture_passes_for_minimal_operation(self): self.assertEqual(operation.operation_main(["validate-fixture", str(FIXTURE), "--structural-only"]), 0) def test_conformance_passes_for_local_fixture_dir(self):