agent-portal-2 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/.continue/agents/new-config.yaml +22 -0
  2. package/AGENT_STEERING.md +36 -0
  3. package/ARCHITECTURE.md +13 -0
  4. package/CHANGELOG.md +97 -0
  5. package/CLI.md +38 -0
  6. package/CONTRIBUTING.md +55 -0
  7. package/INSTALLATION.md +58 -0
  8. package/LICENSE +60 -0
  9. package/PLUGIN_SYSTEM.md +33 -0
  10. package/PYTHON_SDK.md +22 -0
  11. package/QUICKSTART.md +19 -0
  12. package/README.md +385 -0
  13. package/RELEASE_NOTES_v0.1.0.md +281 -0
  14. package/ROADMAP.md +3 -0
  15. package/RUNTIME.md +44 -0
  16. package/SAFETY_MODEL.md +24 -0
  17. package/TESTING.md +35 -0
  18. package/TROUBLESHOOTING.md +30 -0
  19. package/UPGRADE_GUIDE.md +288 -0
  20. package/VS_CODE_EXTENSION.md +47 -0
  21. package/agent-portal.config.json +20 -0
  22. package/apps/desktop/agent-portal-desktop.zip +0 -0
  23. package/apps/desktop/fixtures/local-workflow.html +151 -0
  24. package/apps/desktop/package.json +18 -0
  25. package/apps/desktop/src/main.ts +117 -0
  26. package/apps/desktop/tsconfig.json +8 -0
  27. package/apps/vscode-extension/LICENSE +60 -0
  28. package/apps/vscode-extension/README.md +20 -0
  29. package/apps/vscode-extension/media/agent-portal-logo.png +0 -0
  30. package/apps/vscode-extension/package.json +149 -0
  31. package/apps/vscode-extension/src/extension.ts +614 -0
  32. package/apps/vscode-extension/tsconfig.json +12 -0
  33. package/assets/branding/agent-portal-logo.png +0 -0
  34. package/connectors/chatgpt-tools/README.md +9 -0
  35. package/connectors/claude-mcp-server/README.md +9 -0
  36. package/connectors/gemini-connector/README.md +9 -0
  37. package/connectors/rest-websocket-api/README.md +9 -0
  38. package/docs/MCP_SERVER.md +68 -0
  39. package/docs/architecture.md +214 -0
  40. package/docs/roadmap.md +125 -0
  41. package/package.json +21 -0
  42. package/packages/agent-portal-mcp/README.md +12 -0
  43. package/packages/agent-portal-mcp/agent_portal_mcp/__init__.py +3 -0
  44. package/packages/agent-portal-mcp/agent_portal_mcp/bridge/__init__.py +1 -0
  45. package/packages/agent-portal-mcp/agent_portal_mcp/bridge/runtime_client.py +180 -0
  46. package/packages/agent-portal-mcp/agent_portal_mcp/cli.py +32 -0
  47. package/packages/agent-portal-mcp/agent_portal_mcp/doctor.py +71 -0
  48. package/packages/agent-portal-mcp/agent_portal_mcp/schemas/__init__.py +1 -0
  49. package/packages/agent-portal-mcp/agent_portal_mcp/schemas/actions.py +17 -0
  50. package/packages/agent-portal-mcp/agent_portal_mcp/schemas/results.py +24 -0
  51. package/packages/agent-portal-mcp/agent_portal_mcp/schemas/risk.py +20 -0
  52. package/packages/agent-portal-mcp/agent_portal_mcp/security/__init__.py +1 -0
  53. package/packages/agent-portal-mcp/agent_portal_mcp/security/policy.py +27 -0
  54. package/packages/agent-portal-mcp/agent_portal_mcp/server.py +148 -0
  55. package/packages/agent-portal-mcp/agent_portal_mcp/tool_registry.py +58 -0
  56. package/packages/agent-portal-mcp/agent_portal_mcp/tools/__init__.py +1 -0
  57. package/packages/agent-portal-mcp/agent_portal_mcp/tools/browser.py +89 -0
  58. package/packages/agent-portal-mcp/agent_portal_mcp/tools/common.py +98 -0
  59. package/packages/agent-portal-mcp/agent_portal_mcp/tools/inspection.py +93 -0
  60. package/packages/agent-portal-mcp/agent_portal_mcp/tools/navigation.py +93 -0
  61. package/packages/agent-portal-mcp/agent_portal_mcp/tools/reports.py +34 -0
  62. package/packages/agent-portal-mcp/agent_portal_mcp/tools/steering.py +93 -0
  63. package/packages/agent-portal-mcp/pyproject.toml +20 -0
  64. package/packages/agent-portal-mcp/tests/test_doctor.py +20 -0
  65. package/packages/agent-portal-mcp/tests/test_mcp_server.py +161 -0
  66. package/packages/core/package.json +15 -0
  67. package/packages/core/src/index.ts +1842 -0
  68. package/packages/core/tsconfig.json +8 -0
  69. package/packages/mcp-server/package.json +15 -0
  70. package/packages/mcp-server/src/index.ts +73 -0
  71. package/packages/mcp-server/tsconfig.json +8 -0
  72. package/packages/sdk/package.json +15 -0
  73. package/packages/sdk/src/index.ts +544 -0
  74. package/packages/sdk/tsconfig.json +8 -0
  75. package/plugins/README.md +16 -0
  76. package/plugins/agent-portal-browser/plugin.json +19 -0
  77. package/plugins/agent-portal-python/plugin.json +16 -0
  78. package/plugins/agent-portal-skills/plugin.json +19 -0
  79. package/plugins/agent-portal-vscode/plugin.json +27 -0
  80. package/plugins/example-runtime-plugin/README.md +3 -0
  81. package/plugins/example-runtime-plugin/plugin.json +20 -0
  82. package/plugins/plugin.schema.json +53 -0
  83. package/python/README.md +18 -0
  84. package/python/agent_portal/__init__.py +5 -0
  85. package/python/agent_portal/__main__.py +5 -0
  86. package/python/agent_portal/browser.py +393 -0
  87. package/python/agent_portal/cli.py +164 -0
  88. package/python/agent_portal/config.py +31 -0
  89. package/python/agent_portal/doctor.py +165 -0
  90. package/python/agent_portal/exceptions.py +39 -0
  91. package/python/agent_portal/logging_utils.py +33 -0
  92. package/python/agent_portal/metrics.py +309 -0
  93. package/python/agent_portal/models.py +160 -0
  94. package/python/agent_portal/plugin_system.py +42 -0
  95. package/python/agent_portal/rate_limit.py +253 -0
  96. package/python/agent_portal/runtime.py +739 -0
  97. package/python/agent_portal/server.py +351 -0
  98. package/python/agent_portal/validation.py +299 -0
  99. package/python/pyproject.toml +29 -0
  100. package/python/tests/test_config.py +24 -0
  101. package/python/tests/test_doctor.py +19 -0
  102. package/python/tests/test_metrics.py +180 -0
  103. package/python/tests/test_rate_limit.py +237 -0
  104. package/python/tests/test_runtime.py +122 -0
  105. package/python/tests/test_server.py +53 -0
  106. package/python/tests/test_validation.py +170 -0
  107. package/releases/desktop/agent-portal-desktop/README.md +378 -0
  108. package/releases/desktop/agent-portal-desktop/RELEASE_NOTES.md +14 -0
  109. package/releases/desktop/agent-portal-desktop/assets/branding/agent-portal-logo.png +0 -0
  110. package/releases/desktop/agent-portal-desktop/fixtures/local-workflow.html +151 -0
  111. package/releases/desktop/agent-portal-desktop/launch-agent-portal.bat +4 -0
  112. package/releases/desktop/agent-portal-desktop.zip +0 -0
  113. package/releases/python/agent_portal-0.0.2-py3-none-any.whl +0 -0
  114. package/releases/python/agent_portal-0.0.2.tar.gz +0 -0
  115. package/scripts/package_desktop.mjs +117 -0
  116. package/scripts/release_python.py +46 -0
  117. package/tests/plugin-manifest.test.mjs +26 -0
  118. package/tests/runtime.test.mjs +41 -0
  119. package/tests/vscode-extension.test.mjs +22 -0
  120. package/tsconfig.base.json +16 -0
@@ -0,0 +1,164 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ from dataclasses import asdict
6
+ from pathlib import Path
7
+ import sys
8
+ from urllib.error import HTTPError, URLError
9
+ from urllib.request import Request, urlopen
10
+
11
+ from .config import load_config, save_default_config
12
+ from .doctor import run_doctor
13
+ from .plugin_system import discover_plugins, validate_plugin_manifest
14
+ from .runtime import PortalRuntime
15
+ from .server import serve
16
+
17
+
18
+ def main() -> None:
19
+ parser = build_parser()
20
+ args = parser.parse_args()
21
+ workspace = Path.cwd()
22
+ save_default_config(workspace)
23
+ config = load_config(workspace)
24
+ host = args.host or config.runtime_host
25
+ port = args.port or config.runtime_port
26
+ runtime_url = f"http://{host}:{port}"
27
+
28
+ config.runtime_host = host
29
+ config.runtime_port = port
30
+
31
+ try:
32
+ if args.command == "start":
33
+ serve(PortalRuntime(workspace, config))
34
+ return
35
+ if args.command == "stop":
36
+ print_json_or_text(post_json(f"{runtime_url}/control/stop"), args.json)
37
+ return
38
+ if args.command == "status":
39
+ print_json_or_text(get_json(f"{runtime_url}/status"), args.json)
40
+ return
41
+ if args.command == "doctor":
42
+ report = run_doctor(workspace)
43
+ payload = {"checks": [asdict(check) for check in report.checks]}
44
+ print_json_or_text(payload, args.json)
45
+ return
46
+ if args.command == "open":
47
+ payload = {"url": args.url}
48
+ print_json_or_text(post_json(f"{runtime_url}/browser/open", payload), args.json)
49
+ return
50
+ if args.command == "screenshot":
51
+ payload = {"label": args.label}
52
+ print_json_or_text(post_json(f"{runtime_url}/browser/screenshot", payload), args.json)
53
+ return
54
+ if args.command == "report":
55
+ print_json_or_text(post_json(f"{runtime_url}/report/generate"), args.json)
56
+ return
57
+ if args.command == "plugins":
58
+ if args.plugins_command == "list":
59
+ payload = [str(path) for path in discover_plugins(workspace)]
60
+ print_json_or_text(payload, args.json)
61
+ return
62
+ if args.plugins_command == "validate":
63
+ results = {
64
+ str(path): validate_plugin_manifest(path)
65
+ for path in discover_plugins(workspace)
66
+ }
67
+ print_json_or_text(results, args.json)
68
+ return
69
+ if args.command == "mcp":
70
+ mcp_cli = load_mcp_cli_module()
71
+ argv = [args.mcp_command]
72
+ if args.host or args.port:
73
+ argv.extend(["--runtime-url", runtime_url])
74
+ if args.json:
75
+ argv.append("--json")
76
+ old_argv = sys.argv[:]
77
+ try:
78
+ sys.argv = ["agent-portal-mcp", *argv]
79
+ mcp_cli.main()
80
+ finally:
81
+ sys.argv = old_argv
82
+ return
83
+ except (HTTPError, URLError) as exc:
84
+ print_json_or_text(
85
+ {
86
+ "error": "Runtime request failed",
87
+ "details": str(exc),
88
+ "suggestedFix": f"Start the runtime with `agent-portal --host {host} --port {port} start`.",
89
+ },
90
+ True if args.json else False,
91
+ )
92
+ raise SystemExit(1) from exc
93
+
94
+
95
+ def build_parser() -> argparse.ArgumentParser:
96
+ parser = argparse.ArgumentParser(prog="agent-portal")
97
+ parser.add_argument("--json", action="store_true")
98
+ parser.add_argument("--verbose", action="store_true")
99
+ parser.add_argument("--debug", action="store_true")
100
+ parser.add_argument("--host")
101
+ parser.add_argument("--port", type=int)
102
+ parser.add_argument("--profile")
103
+
104
+ subparsers = parser.add_subparsers(dest="command", required=True)
105
+ subparsers.add_parser("start")
106
+ subparsers.add_parser("stop")
107
+ subparsers.add_parser("status")
108
+ subparsers.add_parser("doctor")
109
+
110
+ open_parser = subparsers.add_parser("open")
111
+ open_parser.add_argument("url")
112
+
113
+ screenshot_parser = subparsers.add_parser("screenshot")
114
+ screenshot_parser.add_argument("--label", default="manual")
115
+
116
+ subparsers.add_parser("report")
117
+
118
+ plugins_parser = subparsers.add_parser("plugins")
119
+ plugins_subparsers = plugins_parser.add_subparsers(dest="plugins_command", required=True)
120
+ plugins_subparsers.add_parser("list")
121
+ plugins_subparsers.add_parser("validate")
122
+
123
+ mcp_parser = subparsers.add_parser("mcp")
124
+ mcp_subparsers = mcp_parser.add_subparsers(dest="mcp_command", required=True)
125
+ mcp_subparsers.add_parser("start")
126
+ mcp_subparsers.add_parser("doctor")
127
+ return parser
128
+
129
+
130
+ def get_json(url: str) -> object:
131
+ with urlopen(url) as response:
132
+ return json.loads(response.read().decode("utf8"))
133
+
134
+
135
+ def post_json(url: str, payload: dict[str, object] | None = None) -> object:
136
+ data = json.dumps(payload or {}).encode("utf8")
137
+ request = Request(url, data=data, headers={"Content-Type": "application/json"})
138
+ with urlopen(request) as response:
139
+ return json.loads(response.read().decode("utf8"))
140
+
141
+
142
+ def print_json_or_text(payload: object, json_output: bool) -> None:
143
+ if json_output:
144
+ print(json.dumps(payload, indent=2))
145
+ return
146
+ if isinstance(payload, dict):
147
+ for key, value in payload.items():
148
+ print(f"{key}: {value}")
149
+ return
150
+ if isinstance(payload, list):
151
+ for entry in payload:
152
+ print(f"- {entry}")
153
+ return
154
+ print(payload)
155
+
156
+
157
+ def load_mcp_cli_module():
158
+ repo_root = Path(__file__).resolve().parents[2]
159
+ mcp_src = repo_root / "packages" / "agent-portal-mcp"
160
+ if str(mcp_src) not in sys.path:
161
+ sys.path.insert(0, str(mcp_src))
162
+ from agent_portal_mcp import cli as mcp_cli # type: ignore
163
+
164
+ return mcp_cli
@@ -0,0 +1,31 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from dataclasses import asdict
5
+ from pathlib import Path
6
+
7
+ from .models import RuntimeConfigModel
8
+
9
+
10
+ DEFAULT_CONFIG_PATH = "agent-portal.config.json"
11
+
12
+
13
+ def load_config(base_path: Path | None = None) -> RuntimeConfigModel:
14
+ root = base_path or Path.cwd()
15
+ config_path = root / DEFAULT_CONFIG_PATH
16
+ if not config_path.exists():
17
+ return RuntimeConfigModel()
18
+
19
+ raw = json.loads(config_path.read_text(encoding="utf8"))
20
+ return RuntimeConfigModel(**raw)
21
+
22
+
23
+ def save_default_config(base_path: Path | None = None) -> Path:
24
+ root = base_path or Path.cwd()
25
+ config_path = root / DEFAULT_CONFIG_PATH
26
+ if not config_path.exists():
27
+ config_path.write_text(
28
+ json.dumps(asdict(RuntimeConfigModel()), indent=2),
29
+ encoding="utf8",
30
+ )
31
+ return config_path
@@ -0,0 +1,165 @@
1
+ from __future__ import annotations
2
+
3
+ import importlib.util
4
+ import json
5
+ import os
6
+ import socket
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ from .config import DEFAULT_CONFIG_PATH, load_config
11
+ from .models import DoctorReport, HealthCheckResult
12
+ from .plugin_system import discover_plugins, validate_plugin_manifest
13
+
14
+
15
+ def run_doctor(base_path: Path | None = None) -> DoctorReport:
16
+ root = base_path or Path.cwd()
17
+ config = load_config(root)
18
+ report = DoctorReport()
19
+
20
+ report.checks.append(check_python_version())
21
+ report.checks.append(check_dependency("playwright", "Install with `pip install -e ./python`."))
22
+ report.checks.append(check_config_file(root))
23
+ report.checks.append(check_runtime_port(config.runtime_host, config.runtime_port))
24
+ report.checks.append(check_directory_writable(root / config.screenshot_directory, "Screenshot directory"))
25
+ report.checks.append(check_directory_writable(root / config.report_directory, "Report directory"))
26
+ report.checks.extend(check_plugin_manifests(root))
27
+ report.checks.append(check_vscode_extension(root))
28
+ report.checks.append(check_os_support())
29
+ report.checks.append(check_chromium_presence())
30
+ return report
31
+
32
+
33
+ def check_python_version() -> HealthCheckResult:
34
+ if sys.version_info >= (3, 10):
35
+ return HealthCheckResult("python-version", "passed", sys.version.split()[0])
36
+ return HealthCheckResult(
37
+ "python-version",
38
+ "failed",
39
+ sys.version.split()[0],
40
+ "Install Python 3.10 or newer.",
41
+ )
42
+
43
+
44
+ def check_dependency(module_name: str, suggested_fix: str) -> HealthCheckResult:
45
+ found = importlib.util.find_spec(module_name) is not None
46
+ if found:
47
+ return HealthCheckResult(module_name, "passed", "Installed")
48
+ return HealthCheckResult(module_name, "failed", "Missing", suggested_fix)
49
+
50
+
51
+ def check_config_file(root: Path) -> HealthCheckResult:
52
+ config_path = root / DEFAULT_CONFIG_PATH
53
+ if not config_path.exists():
54
+ return HealthCheckResult(
55
+ "config-file",
56
+ "warning",
57
+ f"{config_path.name} not found",
58
+ "Start the runtime once or create the default config with `agent-portal start`.",
59
+ )
60
+ try:
61
+ json.loads(config_path.read_text(encoding="utf8"))
62
+ return HealthCheckResult("config-file", "passed", str(config_path))
63
+ except json.JSONDecodeError as exc:
64
+ return HealthCheckResult(
65
+ "config-file",
66
+ "failed",
67
+ str(exc),
68
+ "Fix the JSON syntax in agent-portal.config.json.",
69
+ )
70
+
71
+
72
+ def check_runtime_port(host: str, port: int) -> HealthCheckResult:
73
+ sock = socket.socket()
74
+ try:
75
+ sock.bind((host, port))
76
+ return HealthCheckResult("runtime-port", "passed", f"{host}:{port} is available")
77
+ except OSError:
78
+ return HealthCheckResult(
79
+ "runtime-port",
80
+ "warning",
81
+ f"{host}:{port} is already in use",
82
+ "Stop the other runtime or choose a different port.",
83
+ )
84
+ finally:
85
+ sock.close()
86
+
87
+
88
+ def check_directory_writable(directory: Path, label: str) -> HealthCheckResult:
89
+ try:
90
+ directory.mkdir(parents=True, exist_ok=True)
91
+ test_file = directory / ".write-test"
92
+ test_file.write_text("ok", encoding="utf8")
93
+ test_file.unlink()
94
+ return HealthCheckResult(label, "passed", str(directory))
95
+ except OSError as exc:
96
+ return HealthCheckResult(
97
+ label,
98
+ "failed",
99
+ str(exc),
100
+ f"Fix filesystem permissions for {directory}.",
101
+ )
102
+
103
+
104
+ def check_plugin_manifests(root: Path) -> list[HealthCheckResult]:
105
+ results: list[HealthCheckResult] = []
106
+ for plugin_manifest in discover_plugins(root):
107
+ errors = validate_plugin_manifest(plugin_manifest)
108
+ if errors:
109
+ results.append(
110
+ HealthCheckResult(
111
+ f"plugin:{plugin_manifest.parent.name}",
112
+ "failed",
113
+ "; ".join(errors),
114
+ "Fix the manifest fields and retry.",
115
+ )
116
+ )
117
+ else:
118
+ results.append(
119
+ HealthCheckResult(
120
+ f"plugin:{plugin_manifest.parent.name}",
121
+ "passed",
122
+ "Valid"
123
+ )
124
+ )
125
+ return results
126
+
127
+
128
+ def check_vscode_extension(root: Path) -> HealthCheckResult:
129
+ extension_dir = root / "apps" / "vscode-extension"
130
+ package_json = extension_dir / "package.json"
131
+ if package_json.exists():
132
+ return HealthCheckResult("vscode-extension", "passed", str(package_json))
133
+ return HealthCheckResult(
134
+ "vscode-extension",
135
+ "warning",
136
+ str(package_json),
137
+ "Restore the VS Code extension package if editor integration is required.",
138
+ )
139
+
140
+
141
+ def check_os_support() -> HealthCheckResult:
142
+ if os.name == "nt":
143
+ return HealthCheckResult("os-support", "passed", "Windows supported")
144
+ return HealthCheckResult(
145
+ "os-support",
146
+ "warning",
147
+ os.name,
148
+ "Validate browser paths and runtime behavior on this operating system.",
149
+ )
150
+
151
+
152
+ def check_chromium_presence() -> HealthCheckResult:
153
+ locations = [
154
+ Path.home() / "AppData" / "Local" / "ms-playwright",
155
+ Path.home() / ".cache" / "ms-playwright",
156
+ ]
157
+ for playwright_cache in locations:
158
+ if playwright_cache.exists():
159
+ return HealthCheckResult("chromium-installed", "passed", str(playwright_cache))
160
+ return HealthCheckResult(
161
+ "chromium-installed",
162
+ "warning",
163
+ "Chromium cache not found",
164
+ "Run `npx playwright install chromium` and `python -m playwright install chromium` if needed.",
165
+ )
@@ -0,0 +1,39 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ class AgentPortalError(Exception):
5
+ def __init__(
6
+ self,
7
+ message: str,
8
+ *,
9
+ module: str,
10
+ likely_cause: str,
11
+ suggested_fix: str,
12
+ can_continue: bool,
13
+ ) -> None:
14
+ super().__init__(message)
15
+ self.module = module
16
+ self.likely_cause = likely_cause
17
+ self.suggested_fix = suggested_fix
18
+ self.can_continue = can_continue
19
+
20
+ def to_dict(self) -> dict[str, object]:
21
+ return {
22
+ "message": str(self),
23
+ "module": self.module,
24
+ "likelyCause": self.likely_cause,
25
+ "suggestedFix": self.suggested_fix,
26
+ "canContinue": self.can_continue,
27
+ }
28
+
29
+
30
+ class RuntimeStartupError(AgentPortalError):
31
+ pass
32
+
33
+
34
+ class BrowserOperationError(AgentPortalError):
35
+ pass
36
+
37
+
38
+ class PolicyBlockedError(AgentPortalError):
39
+ pass
@@ -0,0 +1,33 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import logging
5
+ import sys
6
+ from datetime import datetime, timezone
7
+
8
+
9
+ class JsonFormatter(logging.Formatter):
10
+ def format(self, record: logging.LogRecord) -> str:
11
+ payload = {
12
+ "timestamp": datetime.now(timezone.utc).isoformat(),
13
+ "level": record.levelname,
14
+ "logger": record.name,
15
+ "message": record.getMessage(),
16
+ }
17
+ if hasattr(record, "context"):
18
+ payload["context"] = getattr(record, "context")
19
+ return json.dumps(payload)
20
+
21
+
22
+ def build_logger(name: str, level: str = "INFO") -> logging.Logger:
23
+ logger = logging.getLogger(name)
24
+ if logger.handlers:
25
+ logger.setLevel(level.upper())
26
+ return logger
27
+
28
+ logger.setLevel(level.upper())
29
+ handler = logging.StreamHandler(sys.stdout)
30
+ handler.setFormatter(JsonFormatter())
31
+ logger.addHandler(handler)
32
+ logger.propagate = False
33
+ return logger