@openhands/extensions 0.25.0 → 0.26.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 (118) hide show
  1. package/.claude-plugin/marketplace.json +24 -0
  2. package/.codex-plugin/marketplace.json +24 -0
  3. package/.github/pull_request_template.md +17 -2
  4. package/.github/scripts/check_pr_description.py +64 -5
  5. package/.github/scripts/markdown_sections.py +95 -0
  6. package/.github/workflows/build-vscode-app.yml +30 -0
  7. package/.github/workflows/check-extensions.yml +3 -0
  8. package/.github/workflows/tests.yml +26 -0
  9. package/.plugin/marketplace.json +24 -0
  10. package/.release-please-manifest.json +1 -1
  11. package/AGENTS.md +3 -2
  12. package/README.md +9 -3
  13. package/apps/README.md +20 -0
  14. package/apps/catalog.json +10 -0
  15. package/apps/index.d.ts +11 -0
  16. package/apps/index.js +5 -0
  17. package/apps/vscode/README.md +13 -0
  18. package/apps/vscode/canvas-extension.json +19 -0
  19. package/apps/vscode/extension.js +114 -0
  20. package/apps/vscode/scripts/package.mjs +84 -0
  21. package/automations/bundle-index.js +15 -15
  22. package/automations/catalog/github-agents-md-maintainer/manifest.json +1 -1
  23. package/automations/catalog/github-pr-reviewer/manifest.json +5 -5
  24. package/automations/catalog/github-stale-ci-pr-closer/manifest.json +3 -3
  25. package/automations/catalog/gitlab-issue-to-mr/manifest.json +1 -1
  26. package/automations/catalog/news-digest/manifest.json +2 -2
  27. package/index.d.ts +2 -0
  28. package/index.js +1 -0
  29. package/marketplaces/openhands-extensions.json +24 -0
  30. package/package.json +9 -1
  31. package/plugins/openhands/skills/openhands-api/README.md +1 -1
  32. package/plugins/openhands/skills/openhands-api/SKILL.md +3 -3
  33. package/plugins/openhands/skills/openhands-api/scripts/openhands_api.py +13 -0
  34. package/plugins/openhands/skills/openhands-automation/SKILL.md +66 -4
  35. package/plugins/openhands/skills/openhands-automation/references/custom-automation.md +21 -7
  36. package/plugins/runtime-api-configs/.claude-plugin/plugin.json +20 -0
  37. package/plugins/runtime-api-configs/.codex-plugin/plugin.json +20 -0
  38. package/plugins/runtime-api-configs/.plugin/plugin.json +20 -0
  39. package/plugins/runtime-api-configs/README.md +59 -0
  40. package/plugins/runtime-api-configs/SKILL.md +240 -0
  41. package/plugins/runtime-api-configs/scripts/warm_runtime_configs.py +331 -0
  42. package/plugins/runtime-api-configs/tests/test_warm_runtime_configs.py +338 -0
  43. package/plugins/vulnerability-remediation/skills/github/scripts/agent_conversation.py +223 -43
  44. package/plugins/vulnerability-remediation/skills/github/scripts/github_client.py +67 -15
  45. package/pyproject.toml +1 -1
  46. package/python/openhands_extensions/_version.py +1 -1
  47. package/scripts/check_plugin_resources.py +116 -0
  48. package/scripts/sync_extensions.py +32 -5
  49. package/skills/canvas-extension-api/SKILL.md +34 -3
  50. package/skills/canvas-extension-api/references/backend-safety.md +14 -0
  51. package/skills/canvas-extension-api/references/connections.md +24 -0
  52. package/skills/canvas-extension-api/references/sidecar-pattern.md +49 -10
  53. package/skills/canvas-extension-api/references/v1-contract.md +53 -3
  54. package/skills/canvas-extension-api/scripts/validate-extension.mjs +52 -0
  55. package/skills/canvas-extension-api/scripts/validate-extension.test.mjs +192 -1
  56. package/skills/github/scripts/agent_conversation.py +223 -43
  57. package/skills/github/scripts/github_client.py +67 -15
  58. package/skills/github-agents-md-maintainer/README.md +7 -0
  59. package/skills/github-agents-md-maintainer/SKILL.md +13 -6
  60. package/skills/github-agents-md-maintainer/scripts/github_client.py +67 -15
  61. package/skills/github-agents-md-maintainer/scripts/main.py +199 -29
  62. package/skills/github-delivery-watchdog/scripts/github_client.py +67 -15
  63. package/skills/github-issue-to-pr/scripts/agent_conversation.py +223 -43
  64. package/skills/github-issue-to-pr/scripts/github_client.py +67 -15
  65. package/skills/github-issue-to-pr/scripts/worker.py +10 -1
  66. package/skills/github-issue-triage/scripts/agent_conversation.py +223 -43
  67. package/skills/github-issue-triage/scripts/github_client.py +67 -15
  68. package/skills/github-pr-reviewer/README.md +5 -4
  69. package/skills/github-pr-reviewer/SKILL.md +8 -17
  70. package/skills/github-pr-reviewer/references/state-schema.md +0 -15
  71. package/skills/github-pr-reviewer/scripts/agent_conversation.py +223 -43
  72. package/skills/github-pr-reviewer/scripts/github_client.py +67 -15
  73. package/skills/github-pr-reviewer/scripts/worker.py +131 -108
  74. package/skills/github-repo-monitor/scripts/github_client.py +67 -15
  75. package/skills/github-stale-ci-pr-closer/scripts/worker.py +223 -50
  76. package/skills/github-stale-ci-pr-closer/tests/test_worker.py +155 -15
  77. package/skills/gitlab-issue-to-mr/README.md +12 -0
  78. package/skills/gitlab-issue-to-mr/SKILL.md +13 -6
  79. package/skills/gitlab-issue-to-mr/scripts/main.py +217 -42
  80. package/skills/incident-retrospective/SKILL.md +5 -5
  81. package/skills/index.js +23 -14
  82. package/skills/jira-issue-to-pr/README.md +12 -4
  83. package/skills/jira-issue-to-pr/SKILL.md +129 -18
  84. package/skills/jira-issue-to-pr/references/setup.md +8 -0
  85. package/skills/jira-issue-to-pr/scripts/main.py +181 -54
  86. package/skills/jira-issue-to-pr/scripts/setup_webhook.py +290 -0
  87. package/skills/linear-triage/SKILL.md +5 -5
  88. package/skills/news-digest/README.md +6 -0
  89. package/skills/news-digest/SKILL.md +19 -9
  90. package/skills/news-digest/commands/news-digest-setup.md +1 -1
  91. package/skills/news-digest/scripts/main.py +133 -15
  92. package/skills/openhands-api/README.md +1 -1
  93. package/skills/openhands-api/SKILL.md +3 -3
  94. package/skills/openhands-api/scripts/openhands_api.py +13 -0
  95. package/skills/openhands-automation/SKILL.md +66 -4
  96. package/skills/openhands-automation/references/custom-automation.md +21 -7
  97. package/skills/research-brief/SKILL.md +5 -5
  98. package/skills/slack-standup-digest/SKILL.md +5 -5
  99. package/skills/ticket-to-code-change/SKILL.md +6 -3
  100. package/skills/use-jev/.claude-plugin/plugin.json +8 -0
  101. package/skills/use-jev/.codex-plugin/plugin.json +8 -0
  102. package/skills/use-jev/.plugin/plugin.json +8 -0
  103. package/skills/use-jev/README.md +66 -0
  104. package/skills/use-jev/SKILL.md +66 -0
  105. package/tests/fixtures/automations/github-pr-reviewer.json +9 -9
  106. package/tests/test_agent_conversation_dispatch.py +212 -0
  107. package/tests/test_apps_catalog.py +26 -0
  108. package/tests/test_automation_setup.py +44 -0
  109. package/tests/test_check_pr_description.py +89 -6
  110. package/tests/test_github_reviewer_delivery.py +254 -118
  111. package/tests/test_github_stale_ci_pr_closer.py +56 -0
  112. package/tests/test_jira_issue_to_pr.py +312 -0
  113. package/tests/test_jira_setup_webhook.py +309 -0
  114. package/tests/test_openhands_api_python.py +37 -0
  115. package/tests/test_skill_plugin_loading.py +25 -15
  116. package/tests/test_sync_extensions.py +102 -0
  117. package/tests/test_vscode_app.py +33 -0
  118. package/tests/vscode-app.test.mjs +43 -0
@@ -12,24 +12,62 @@ from urllib.parse import parse_qsl, urlencode, urlsplit
12
12
  from urllib.request import Request, urlopen
13
13
 
14
14
 
15
+ # The secret name of the GitHub token an OpenHands Cloud or Enterprise user's
16
+ # connected GitHub integration provides: how this run's sandbox serves it, and
17
+ # how a conversation sees it in its environment.
18
+ CLOUD_GITHUB_TOKEN_SECRET = "github_token"
19
+ CLOUD_GITHUB_TOKEN_ENV = "GITHUB_TOKEN"
20
+
21
+
22
+ def is_cloud_run() -> bool:
23
+ """Whether this run is on OpenHands Cloud or Enterprise.
24
+
25
+ The automation service hands a run the Agent Server URL only on a local
26
+ Agent Canvas; elsewhere the run talks to the OpenHands API instead.
27
+ """
28
+ return bool(os.environ.get("OPENHANDS_CLOUD_API_URL")) and not os.environ.get(
29
+ "AGENT_SERVER_URL"
30
+ )
31
+
32
+
33
+ def _load_cloud_secret(name: str) -> str | None:
34
+ """Read one named secret of this run's sandbox from the OpenHands API."""
35
+ api = os.environ["OPENHANDS_CLOUD_API_URL"].rstrip("/")
36
+ request = Request(
37
+ f"{api}/api/v1/sandboxes/{os.environ['SANDBOX_ID']}/settings/secrets/{name}",
38
+ headers={"X-Session-API-Key": os.environ["SESSION_API_KEY"]},
39
+ )
40
+ try:
41
+ with urlopen(request, timeout=90) as response:
42
+ return response.read().decode().strip()
43
+ except HTTPError as exc:
44
+ if exc.code == 404:
45
+ return None
46
+ raise
47
+
48
+
15
49
  def _load_secret(name: str) -> str:
16
- """Read one named secret from the environment or configured Agent Server."""
50
+ """Read one named secret from the environment, or from the configured Agent
51
+ Server on a local run and the OpenHands API on a cloud one."""
17
52
  value = os.environ.get(name)
18
53
  if value:
19
54
  return value
20
55
 
21
- from openhands.sdk.workspace import RemoteWorkspace
56
+ if is_cloud_run():
57
+ value = _load_cloud_secret(name)
58
+ else:
59
+ from openhands.sdk.workspace import RemoteWorkspace
22
60
 
23
- workspace = RemoteWorkspace(
24
- host=os.environ["AGENT_SERVER_URL"],
25
- api_key=os.environ["SESSION_API_KEY"],
26
- working_dir=os.environ.get("WORKSPACE_BASE", "/workspace"),
27
- )
28
- try:
29
- secret = workspace.get_secrets([name]).get(name)
30
- value = secret.get_value() if secret else None
31
- finally:
32
- workspace.reset_client()
61
+ workspace = RemoteWorkspace(
62
+ host=os.environ["AGENT_SERVER_URL"],
63
+ api_key=os.environ["SESSION_API_KEY"],
64
+ working_dir=os.environ.get("WORKSPACE_BASE", "/workspace"),
65
+ )
66
+ try:
67
+ secret = workspace.get_secrets([name]).get(name)
68
+ value = secret.get_value() if secret else None
69
+ finally:
70
+ workspace.reset_client()
33
71
  if not value:
34
72
  raise ValueError(f"The GitHub credential {name} is unavailable")
35
73
  return value
@@ -95,11 +133,22 @@ class GitHubRepository:
95
133
  "Expected the environment variable containing the GitHub token"
96
134
  )
97
135
  self.token_name = github_token_secret
98
- self.token = _load_secret(github_token_secret)
136
+ try:
137
+ self.token = _load_secret(github_token_secret)
138
+ except ValueError:
139
+ if not is_cloud_run():
140
+ raise
141
+ # No secret is saved under that name, so use the user's connected
142
+ # GitHub integration, under the name a conversation sees it by.
143
+ self.token = _load_cloud_secret(CLOUD_GITHUB_TOKEN_SECRET)
144
+ if not self.token:
145
+ raise
146
+ self.token_name = CLOUD_GITHUB_TOKEN_ENV
99
147
  self.conversation = conversation
100
148
  self.conversation_id = str(conversation.id) if conversation else None
101
149
  self.dispatcher = dispatcher
102
- self.workspace = Path(os.environ["WORKSPACE_BASE"])
150
+ # A cloud run is not given a workspace base; its sandbox has /workspace.
151
+ self.workspace = Path(os.environ.get("WORKSPACE_BASE", "/workspace"))
103
152
  self.project = self.workspace
104
153
  self.evidence = self.workspace / "evidence"
105
154
  self.evidence.mkdir(exist_ok=True)
@@ -111,10 +160,13 @@ class GitHubRepository:
111
160
 
112
161
  @property
113
162
  def github_instructions(self):
163
+ # A cloud conversation runs in a sandbox of its own, so this run's
164
+ # workspace path means nothing to it.
165
+ where = "your working directory" if is_cloud_run() else self.project
114
166
  return (
115
167
  f"Use `GH_TOKEN=${self.token_name} gh api` for GitHub requests. "
116
168
  "Never print the credential value. "
117
- f"Only {self.repository} is in scope. Work in {self.project}. "
169
+ f"Only {self.repository} is in scope. Work in {where}. "
118
170
  "Do not modify the automation bundle or its configuration."
119
171
  )
120
172
 
package/pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "openhands-extensions"
3
- version = "0.25.0"
3
+ version = "0.26.0"
4
4
  description = "OpenHands extensions, plugins, and skills (Python bindings for the integration catalog)"
5
5
  requires-python = ">=3.12"
6
6
  dependencies = [
@@ -18,7 +18,7 @@ from importlib.metadata import PackageNotFoundError, version
18
18
  #: Kept in lock-step with pyproject.toml/package.json by the version test.
19
19
  #: release-please only rewrites a semver found on the annotated line itself, so
20
20
  #: the annotation must stay on the assignment; on its own line it is a no-op.
21
- _FALLBACK_VERSION = "0.25.0" # x-release-please-version
21
+ _FALLBACK_VERSION = "0.26.0" # x-release-please-version
22
22
 
23
23
  try:
24
24
  __version__: str = version("openhands-extensions")
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env python3
2
+ """Fail if any plugin ships non-source noise in its SDK-visible resource dirs.
3
+
4
+ The OpenHands SDK's skill loader (`openhands.sdk.skills`) walks a fixed set of
5
+ directory names under each plugin and lists everything it finds in that plugin's
6
+ `SkillResources` manifest:
7
+
8
+ >>> from openhands.sdk.skills import RESOURCE_DIRECTORIES
9
+ >>> RESOURCE_DIRECTORIES
10
+ ('scripts', 'references', 'assets')
11
+
12
+ Anything that lands in one of those directories becomes visible to the agent at
13
+ skill-load time. A stale ``__pycache__/warm_runtime_configs.cpython-313.pyc``
14
+ left behind by a developer running tests in-tree therefore gets surfaced to
15
+ every downstream consumer that loads the plugin as a skill.
16
+
17
+ This check scans ``plugins/<name>/{scripts,references,assets}`` for files that
18
+ clearly aren't skill content - Python bytecode caches, pytest caches, editor
19
+ backups, OS litter - and exits 1 if any are found. Keep this list narrow: it
20
+ should flag obvious noise, not police plugin contents.
21
+
22
+ Usage:
23
+ python scripts/check_plugin_resources.py # scan and report
24
+ python scripts/check_plugin_resources.py --fix # also delete offenders
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import argparse
30
+ import fnmatch
31
+ import shutil
32
+ import sys
33
+ from pathlib import Path
34
+
35
+ REPO_ROOT = Path(__file__).resolve().parent.parent
36
+ PLUGINS_DIR = REPO_ROOT / "plugins"
37
+
38
+ # Mirrors openhands.sdk.skills.RESOURCE_DIRECTORIES. Pinned here rather than
39
+ # imported so the check runs without the SDK installed in CI.
40
+ RESOURCE_DIRECTORIES = ("scripts", "references", "assets")
41
+
42
+ # Names (dirs or files) that should never appear under a resource directory.
43
+ # Dir patterns match against directory basenames; file patterns are fnmatch'd
44
+ # against file basenames.
45
+ _FORBIDDEN_DIR_NAMES = frozenset({"__pycache__", ".pytest_cache", ".mypy_cache", ".ruff_cache"})
46
+ _FORBIDDEN_FILE_GLOBS = ("*.pyc", "*.pyo", "*.pyd", "*.swp", "*.swo", ".DS_Store", "Thumbs.db")
47
+
48
+
49
+ def _is_forbidden_file(name: str) -> bool:
50
+ return any(fnmatch.fnmatchcase(name, pat) for pat in _FORBIDDEN_FILE_GLOBS)
51
+
52
+
53
+ def scan() -> list[Path]:
54
+ offenders: list[Path] = []
55
+ if not PLUGINS_DIR.is_dir():
56
+ return offenders
57
+ for plugin_dir in sorted(PLUGINS_DIR.iterdir()):
58
+ if not plugin_dir.is_dir():
59
+ continue
60
+ for resource_name in RESOURCE_DIRECTORIES:
61
+ resource_dir = plugin_dir / resource_name
62
+ if not resource_dir.is_dir():
63
+ continue
64
+ for path in sorted(resource_dir.rglob("*")):
65
+ if path.is_dir() and path.name in _FORBIDDEN_DIR_NAMES:
66
+ offenders.append(path)
67
+ elif path.is_file() and _is_forbidden_file(path.name):
68
+ offenders.append(path)
69
+ return offenders
70
+
71
+
72
+ def main() -> int:
73
+ parser = argparse.ArgumentParser(description=__doc__)
74
+ parser.add_argument(
75
+ "--fix",
76
+ action="store_true",
77
+ help="Delete offending files/dirs instead of just failing.",
78
+ )
79
+ args = parser.parse_args()
80
+
81
+ offenders = scan()
82
+ if not offenders:
83
+ print(f"OK: no non-source files found under plugins/*/{{{','.join(RESOURCE_DIRECTORIES)}}}/")
84
+ return 0
85
+
86
+ print(
87
+ f"ERROR: {len(offenders)} non-source entries found under plugin resource "
88
+ f"directories. These would be surfaced to agents in SkillResources:",
89
+ file=sys.stderr,
90
+ )
91
+ for path in offenders:
92
+ kind = "dir " if path.is_dir() else "file"
93
+ print(f" {kind} {path.relative_to(REPO_ROOT)}", file=sys.stderr)
94
+
95
+ if args.fix:
96
+ # Delete deepest paths first so a child file inside a to-be-removed
97
+ # __pycache__ directory doesn't vanish out from under its own unlink.
98
+ for path in sorted(offenders, key=lambda p: len(p.parts), reverse=True):
99
+ if path.is_dir():
100
+ shutil.rmtree(path, ignore_errors=True)
101
+ else:
102
+ path.unlink(missing_ok=True)
103
+ print(f"\nRemoved {len(offenders)} entries.", file=sys.stderr)
104
+ return 0
105
+
106
+ print(
107
+ "\nRe-run with --fix to delete, or add the pattern to .gitignore and "
108
+ "clean your working tree. These are typically produced by running tests "
109
+ "in-tree without PYTHONDONTWRITEBYTECODE=1.",
110
+ file=sys.stderr,
111
+ )
112
+ return 1
113
+
114
+
115
+ if __name__ == "__main__":
116
+ sys.exit(main())
@@ -9,7 +9,8 @@ Four sync tasks, runnable individually or all at once:
9
9
  3. **coverage** — warn when a skill/plugin directory is not listed in any
10
10
  marketplace, or a marketplace entry points to a missing directory.
11
11
  4. **symlinks** — enforce ``.plugin/`` as the canonical manifest directory
12
- with vendor symlinks (``.claude-plugin``, ``.codex-plugin``).
12
+ with vendor manifests (``.claude-plugin``, ``.codex-plugin``), each
13
+ either a symlink or a real directory mirroring ``plugin.json``.
13
14
 
14
15
  Usage:
15
16
  python scripts/sync_extensions.py # run all, write changes
@@ -375,13 +376,30 @@ def sync_coverage(*, check: bool) -> list[str]:
375
376
  return problems
376
377
 
377
378
 
378
- # ── 4. Vendor symlinks ────────────────────────────────────────────────
379
+ # ── 4. Vendor manifests ─────────────────────────────────────────────────
379
380
 
380
381
  VENDOR_SYMLINKS = [".claude-plugin", ".codex-plugin"] # add new vendors here
381
382
 
382
383
 
384
+ def _vendor_manifest_matches(directory: Path, vendor: str) -> bool:
385
+ """True when a real vendor directory mirrors the canonical manifest."""
386
+ src = directory / ".plugin" / "plugin.json"
387
+ dst = directory / vendor / "plugin.json"
388
+ return (
389
+ src.is_file()
390
+ and dst.is_file()
391
+ and src.read_bytes() == dst.read_bytes()
392
+ )
393
+
394
+
383
395
  def _check_vendor_symlinks(directory: Path, check: bool) -> list[str]:
384
- """Check/fix vendor symlinks for a single directory with .plugin/."""
396
+ """Check/fix vendor manifests for a single directory with .plugin/.
397
+
398
+ A vendor path is either a symlink to ``.plugin/`` or a real directory
399
+ whose ``plugin.json`` mirrors the canonical one. Real directories exist
400
+ because some installers (e.g. Codex ``plugin add``) drop symlinked
401
+ directories instead of copying them; see skills/iterate.
402
+ """
385
403
  problems: list[str] = []
386
404
  canon = directory / ".plugin"
387
405
  if not canon.is_dir():
@@ -393,6 +411,15 @@ def _check_vendor_symlinks(directory: Path, check: bool) -> list[str]:
393
411
  if target == canon.resolve():
394
412
  continue
395
413
  problems.append(f"wrong target: {link.relative_to(REPO_ROOT)} → {link.readlink()}")
414
+ elif link.is_dir():
415
+ if _vendor_manifest_matches(directory, vendor):
416
+ continue
417
+ problems.append(f"stale manifest copy: {link.relative_to(REPO_ROOT)}")
418
+ if not check:
419
+ (link / "plugin.json").write_bytes(
420
+ (canon / "plugin.json").read_bytes()
421
+ )
422
+ continue
396
423
  elif link.exists():
397
424
  problems.append(f"not a symlink: {link.relative_to(REPO_ROOT)}")
398
425
  continue
@@ -405,11 +432,11 @@ def _check_vendor_symlinks(directory: Path, check: bool) -> list[str]:
405
432
 
406
433
 
407
434
  def sync_symlinks(*, check: bool) -> list[str]:
408
- """Ensure every directory with .plugin/ also has vendor symlinks.
435
+ """Ensure every directory with .plugin/ has discoverable vendor manifests.
409
436
 
410
437
  Scans both plugins/ and skills/ directories. Skills that ship a
411
438
  ``.plugin/`` manifest (e.g. those with ``commands/``) need vendor
412
- symlinks so that Codex and Claude Code can discover them.
439
+ manifests so that Codex and Claude Code can discover them.
413
440
  """
414
441
  problems: list[str] = []
415
442
  for base in SKILL_DIRS:
@@ -22,7 +22,7 @@ Treat apps as trusted, same-realm browser code owned by the active Agent Server.
22
22
 
23
23
  ## Prefer the proven App authoring loop
24
24
 
25
- Treat an App as one authenticated browser dependency graph that Canvas imports from a Blob URL. Do not build a hosted SPA: the production result must be exactly one self-contained browser ESM entrypoint that exports `activate(host)`. Always ship this UI package even when it uses a separately installed Sidecar; make the App page the guided UI for Sidecar setup after the App is opened. Never treat a Sidecar as another browser chunk or an automatic extension install hook.
25
+ Treat an App as one authenticated browser dependency graph that Canvas imports from a Blob URL. Do not build a hosted SPA: the production result must be exactly one self-contained browser ESM entrypoint that exports `activate(host)`. Always ship this UI package even when it uses an optional package-owned managed backend or a manual/deployment-owned Sidecar. Make the App page the guided UI for service setup after the App is opened. Never treat a service as another browser chunk or an automatic extension install hook.
26
26
 
27
27
  For a new App, create an independent package in the target repository and implement its own UI, tests, and build tooling. Follow the Vite library-build reference in `references/packaging-recipes.md`; do not copy a shared starter or introduce a repository-wide runtime/workspace unless the target repository explicitly requires it.
28
28
 
@@ -41,7 +41,9 @@ Treat the static validator as a gate, not a replacement for the browser Blob smo
41
41
 
42
42
  Start by locating the target directory instead of assuming the current workspace. Inspect repository instructions, existing package management, build tooling, tests, and git status before editing.
43
43
 
44
- Make an early architecture decision: browser-only App, Agent Server-integrated App, Sidecar-backed App, or explicitly deployment-specific App. Keep the portable host API 1 surface separate from deployment-owned capabilities.
44
+ Make an early architecture decision: browser-only App, Agent Server-integrated App, package-owned managed-backend App, manual/deployment-owned Sidecar App, or explicitly deployment-specific App. Keep the portable host API 1 surface separate from optional Agent Server/host capabilities and deployment-owned behavior.
45
+
46
+ Use the package-owned managed-backend path only when the target stack includes the lifecycle contract from [software-agent-sdk PR #5270](https://github.com/OpenHands/software-agent-sdk/pull/5270) at `ac7b322ddf7d2e0e90a3643c5e26d848236af9a3`, the bridge contract from [PR #5272](https://github.com/OpenHands/software-agent-sdk/pull/5272) at `6d9c82973e765cce4843d6a2fb52f634efa64293`, and the host version named in `references/v1-contract.md`. Until a release containing both SDK PRs lands, those exact revisions are the minimum Agent Server implementation. Feature-detect `canvas_app_backend_bridge_v1` and `app_backend_ingress_url` through `GET /server_info`; absence is an actionable unsupported state. Do not convert manual Sidecars into managed backends unless the App owns checksum-pinned artifacts for every supported platform.
45
47
 
46
48
  Clarify only choices that materially affect implementation:
47
49
 
@@ -99,6 +101,33 @@ Use this minimal manifest shape:
99
101
  }
100
102
  ```
101
103
 
104
+ For an optional package-owned managed backend, add this top-level sibling of `contributes`:
105
+
106
+ ```json
107
+ "backend": {
108
+ "schema_version": 1,
109
+ "artifacts": {
110
+ "linux-amd64": {
111
+ "path": "backend/linux-amd64.tar.gz",
112
+ "sha256": "<64 lowercase hex characters>"
113
+ },
114
+ "linux-arm64": {
115
+ "path": "backend/linux-arm64.tar.gz",
116
+ "sha256": "<64 lowercase hex characters>"
117
+ }
118
+ },
119
+ "argv": [
120
+ "{artifact_dir}/bin/server",
121
+ "--host=127.0.0.1",
122
+ "--port={port}",
123
+ "--data-dir={data_dir}"
124
+ ],
125
+ "health": { "path": "/health" }
126
+ }
127
+ ```
128
+
129
+ This declaration is optional. Omit it for browser-only Apps and manual/deployment-owned Sidecars. It never authorizes automatic preparation or startup. The server supports Linux amd64 and arm64 initially, verifies package-contained `.tar.gz` artifacts by SHA-256, and executes the structured argv without a shell. `argv[0]` must remain inside `{artifact_dir}`; allowed placeholders are `{artifact_dir}`, `{data_dir}`, and `{port}`.
130
+
102
131
  Follow these invariants:
103
132
 
104
133
  - Use lowercase letters, digits, and hyphens for app names and page IDs.
@@ -146,6 +175,8 @@ Prefer [`@openhands/typescript-client`](https://github.com/OpenHands/software-ag
146
175
  Respect the host API 1 connection boundary:
147
176
 
148
177
  - Treat `host.agentServer.request` as the portable Agent Server HTTP path; Canvas selects the active backend and supplies authentication.
178
+ - For a managed-backend view, require the feature-detected host helper documented in `references/v1-contract.md`. The helper owns session creation, validated iframe isolation, retry/new-tab behavior, and revocation; the App never reads the app-scoped cookie or constructs the ingress URL. The ingress must be a separate browser origin from Canvas, and the host must preserve the server-returned sandbox including `allow-same-origin`.
179
+ - Installing, enabling, activating, or mounting an App never prepares or starts a managed backend. Show the resolved revision and declaration, obtain explicit lifecycle consent, and call prepare then start with that exact revision. An update invalidates approval.
149
180
  - Do not derive an Agent Server URL from `window.location` or extract session keys from Canvas internals.
150
181
  - Do not send Automation service requests through `host.agentServer.request` unless the backend explicitly documents an Agent Server proxy for them. Automation is a separate service with deployment-provided base URL and authentication.
151
182
  - Do not open a direct Agent Server WebSocket from a portable v1 app. The host currently exposes neither the owning server origin nor a WebSocket/auth capability. Poll through `host.agentServer.request`, add a backend-owned bridge, or feature-detect a future host subscription API.
@@ -211,7 +242,7 @@ Treat validator warnings as prompts for inspection, not proof of invalidity. The
211
242
 
212
243
  ## Install and verify safely
213
244
 
214
- Install only when requested. Installation and enablement are separate product actions: installation must leave the app disabled, and enabling executes trusted same-realm code.
245
+ Install only when requested. Installation and enablement are separate product actions: installation must leave the app disabled, and enabling executes trusted same-realm code. Managed-backend preparation and startup are additional explicit actions tied to the resolved revision; installation, enablement, activation, and ordinary page mounts never execute backend code.
215
246
 
216
247
  For a backend-local app, install the path as interpreted on the Agent Server machine. For a Git-hosted app, provide `source`, optional `ref`, and optional `repo_path`. Never assume a frontend-local path exists inside a remote or containerized backend.
217
248
 
@@ -7,3 +7,17 @@ For command-backed Apps, expose fixed command templates only. Validate paths, pa
7
7
  Keep browser-local data namespaced by App name and backend ID in IndexedDB/OPFS/localStorage. Put backend-shared mutable state below `<agent-server-home>/.openhands/apps/<app-name>/`, after discovering the home through `GET /api/file/home` and validating the absolute path. Never write mutable state beside the installed manifest or bundle.
8
8
 
9
9
  For a CLI, binary, compiler, database, or Sidecar, read [sidecar-pattern.md](sidecar-pattern.md). Probe without mutation; disclose exact downloads, builds, data paths, checksums, and process behavior; and gate installation behind acknowledgement. Do not expose general command execution, shell interpolation, silent or unpinned downloads, unauthenticated non-loopback binding, mutable state beside the installed bundle, or automatic data deletion when an App is removed.
10
+
11
+ ## Package-owned managed backends
12
+
13
+ The managed-backend contract requires the stacked, unreleased Agent Server changes in [software-agent-sdk PR #5270](https://github.com/OpenHands/software-agent-sdk/pull/5270) at `ac7b322ddf7d2e0e90a3643c5e26d848236af9a3` and [PR #5272](https://github.com/OpenHands/software-agent-sdk/pull/5272) at `6d9c82973e765cce4843d6a2fb52f634efa64293`. Until a release containing both lands, those exact revisions are the minimum Agent Server implementation. Feature-detect `canvas_app_backend_bridge_v1` through `GET /server_info`; never infer support from an endpoint failure, Canvas internals, or a guessed port. Use the host helper only at the minimum host version documented in [v1-contract.md](v1-contract.md).
14
+
15
+ A managed backend is declarative, not an install hook. Installation, enablement, activation, and ordinary page mounts never prepare or start it. Show the resolved revision and backend declaration, then require explicit user consent before calling prepare and start with that exact revision. Updating or replacing the package invalidates approval. Never put a credential in the manifest, argv, browser bundle, URL, or log.
16
+
17
+ For embedded backend views, require the configured ingress to be a separate browser origin from Canvas. That origin is the isolation boundary. Apply the server-returned sandbox exactly, including `allow-same-origin`; removing it breaks Origin validation, partitioned-cookie delivery, and Worker creation in Chromium. Do not replace origin isolation with an opaque sandbox origin.
18
+
19
+ The Agent Server selects the current platform artifact, verifies its lowercase SHA-256 checksum, extracts it safely to immutable content-addressed storage, and runs only the declared structured argv without a shell. The executable must remain inside `{artifact_dir}`. Supported placeholders are `{artifact_dir}`, `{data_dir}`, and `{port}`. Inherited environment names are restricted to the server's non-credential allowlist; ambient Agent Server credentials do not flow to the child.
20
+
21
+ Only local Linux amd64 and arm64 artifacts are supported initially. Treat `unsupported` as an explicit UI state. The owned process is constrained by its launch contract and process group but is not OS-isolated; run untrusted backend code only inside a suitably isolated Agent Server runtime.
22
+
23
+ Backend data belongs to the service independently of the installed bundle. Disable and uninstall stop the owned process and revoke browser sessions but preserve mutable data. Data deletion is a separate explicit action. Installed Apps and their backends belong to one Agent Server; do not assume a service or its data is shared across isolated conversation runtimes.
@@ -48,6 +48,30 @@ Supply the actual Agent Server origin, including any reverse-proxy path prefix.
48
48
 
49
49
  If typed high-level clients cover the operation, prefer those over the generic `HttpClient`. Use the generic client for uncovered Agent Server endpoints, not as a reason to duplicate authentication and error handling with `fetch`.
50
50
 
51
+ ## Managed App backend views
52
+
53
+ The managed-backend bridge requires the stacked, unreleased Agent Server changes in [software-agent-sdk PR #5270](https://github.com/OpenHands/software-agent-sdk/pull/5270) at `ac7b322ddf7d2e0e90a3643c5e26d848236af9a3` and [PR #5272](https://github.com/OpenHands/software-agent-sdk/pull/5272) at `6d9c82973e765cce4843d6a2fb52f634efa64293`. Until a release containing both lands, those exact revisions are the minimum Agent Server implementation. Read `GET /server_info`: support is present only when `capabilities` contains `canvas_app_backend_bridge_v1` and `app_backend_ingress_url` supplies the separately configured browser origin. If either is absent, render an actionable unsupported state. Never guess localhost, derive an origin from `window.location`, or read Canvas credentials.
54
+
55
+ The typed `CanvasExtensionsClient`, constructed with the normal Agent Server host and API key plus `appBackendIngressUrl`, exposes `createAppBackendSession(name)` and `revokeAppBackendSession(name)`. Session creation returns `ingress_url`, `expires_at`, and `iframe_sandbox`. The app-scoped credential is an HttpOnly, Secure, SameSite=None, Partitioned cookie; it is not returned to browser JavaScript or placed in a URL.
56
+
57
+ Control calls go to the configured ingress origin:
58
+
59
+ - `POST /app-backends/{name}/session` creates a short-lived session for a ready owned backend.
60
+ - `DELETE /app-backends/{name}/session` revokes it and active sockets.
61
+
62
+ Both calls require normal `X-Session-API-Key` control authentication and an allowed Canvas `Origin`. Use the credential-free returned `ingress_url`, rooted at `/app-backends/{name}/`, for iframe assets, streaming, ranges, and relative WebSocket URLs. Apply the returned sandbox exactly; the v1 bridge returns `allow-forms allow-modals allow-popups allow-same-origin allow-scripts`. The configured ingress must be a separate browser origin from Canvas and is the isolation boundary. Do not remove `allow-same-origin`: real Chromium sends `Origin: null` for mutating fetches and WebSockets, drops the partitioned session cookie, and rejects same-site Worker creation with `SecurityError` when the frame has an opaque sandbox origin. HTTPS, or a loopback secure context, and browser partitioned-cookie support are required for third-party iframe operation.
63
+
64
+ Apps consume this flow through optional `host.appBackendView`, not by calling the control API or constructing an iframe URL. Feature-detect it before rendering a managed view:
65
+
66
+ ```js
67
+ const disposeView = host.appBackendView?.mount({ container });
68
+ if (!disposeView) {
69
+ container.textContent = "This Canvas host cannot open the App backend view.";
70
+ }
71
+ ```
72
+
73
+ `mount({ container })` returns a disposer. Call it on page unmount; Canvas also disposes views on backend switch and extension deactivation. The helper owns session creation, URL and sandbox validation, loading, retry, safe new-tab fallback, and revocation. Mounting or remounting never starts or stops the service. Use the helper only at the minimum host version documented in [v1-contract.md](v1-contract.md).
74
+
51
75
  ## Automation backend
52
76
 
53
77
  Treat the Automation service as a separate backend. Its API is normally mounted under `/api/automation/v1`, but the origin, path prefix, and authentication mode are deployment-provided values rather than Agent Server defaults.
@@ -1,29 +1,68 @@
1
1
  # Sidecar-backed Apps
2
2
 
3
- Use this reference when an App needs a separately installed service. Host API 1 supports a browser App package and authenticated Agent Server HTTP requests; it does not provide Sidecar installation, process management, connection brokering, Automation adaptation, arbitrary install hooks, or an Agent Server WebSocket capability.
3
+ Use this reference when an App needs a service in addition to its browser bundle. Choose either an optional package-owned managed backend, when the target Agent Server and Canvas host expose that capability, or a manual/deployment-owned Sidecar. Browser-only Apps remain valid.
4
+
5
+ The managed-backend contract is available on the stacked, unreleased Agent Server changes in [software-agent-sdk PR #5270](https://github.com/OpenHands/software-agent-sdk/pull/5270) at `ac7b322ddf7d2e0e90a3643c5e26d848236af9a3` and [PR #5272](https://github.com/OpenHands/software-agent-sdk/pull/5272) at `6d9c82973e765cce4843d6a2fb52f634efa64293`. Until a release containing both lands, those exact revisions are the minimum Agent Server implementation. Feature-detect `canvas_app_backend_bridge_v1` in `GET /server_info`; absence means the managed view path is unsupported, not that the App should guess a port or fall back to hidden Canvas state. Use the host helper only at the minimum host version documented in [v1-contract.md](v1-contract.md).
4
6
 
5
7
  ## Keep three boundaries
6
8
 
7
- 1. **Canvas App package**: ship `canvas-extension.json` and exactly one self-contained browser ESM entrypoint exporting `activate(host)`. Bundle UI dependencies, CSS, and required assets into that entrypoint.
8
- 2. **Sidecar distribution**: distribute the service, its versioned artifacts, and its service-specific installation and operation mechanism separately.
9
- 3. **Onboarding and operator layer**: implement this as a guided UI in the App view. Present status, disclosures, explicit consent, and lifecycle actions there. Do not infer that installing or enabling the Canvas App authorizes any Sidecar mutation.
9
+ 1. **Canvas App UI**: always ship `canvas-extension.json` and exactly one self-contained browser ESM entrypoint exporting `activate(host)`. Bundle UI dependencies, CSS, and required assets into that entrypoint.
10
+ 2. **Service distribution and ownership**: for a managed backend, declare checksum-pinned platform artifacts in the same package and let the owning Agent Server prepare and run them. For a manual Sidecar, distribute and operate the service separately. Do not combine the models or add a general install hook.
11
+ 3. **Onboarding and operator layer**: implement this as guided UI in the App view. Present status, disclosures, explicit revision-bound consent, and lifecycle actions there. Installing or enabling the App never authorizes backend preparation or startup.
12
+
13
+ ## Declare a package-owned managed backend
14
+
15
+ Add the optional `backend` block only when the App owns its service artifacts:
16
+
17
+ ```json
18
+ {
19
+ "backend": {
20
+ "schema_version": 1,
21
+ "artifacts": {
22
+ "linux-amd64": {
23
+ "path": "backend/linux-amd64.tar.gz",
24
+ "sha256": "<64 lowercase hex characters>"
25
+ },
26
+ "linux-arm64": {
27
+ "path": "backend/linux-arm64.tar.gz",
28
+ "sha256": "<64 lowercase hex characters>"
29
+ }
30
+ },
31
+ "argv": [
32
+ "{artifact_dir}/bin/server",
33
+ "--host=127.0.0.1",
34
+ "--port={port}",
35
+ "--data-dir={data_dir}"
36
+ ],
37
+ "health": {
38
+ "path": "/health",
39
+ "timeout_seconds": 30,
40
+ "interval_seconds": 0.1
41
+ },
42
+ "inherit_environment": ["LANG", "PATH", "TZ"]
43
+ }
44
+ }
45
+ ```
46
+
47
+ Artifact paths are package-relative `.tar.gz` files. The executable in `argv[0]` must resolve inside `{artifact_dir}`. The argv is a structured string list, never shell source; supported placeholders are `{artifact_dir}`, `{data_dir}`, and `{port}`. The current server supports Linux amd64 and arm64 and reports other platforms as `unsupported`.
10
48
 
11
49
  ## Choose a connection boundary
12
50
 
13
51
  Prefer connection patterns in this order:
14
52
 
15
- 1. **Narrow Agent Server bridge**: add a backend-owned, authenticated endpoint for the required Sidecar operations and call it through `host.agentServer.request` with root-relative paths. Keep Sidecar credentials and routing on the backend.
16
- 2. **Documented future or deployment capability**: require a host or deployment adapter that deliberately exposes Sidecar access, feature-detect it, and document the required version and deployment contract.
17
- 3. **Direct browser connection only when explicitly non-portable**: document its origin, authentication, exposure, and deployment ownership. Do not represent this design as a portable host API 1 App.
18
- 4. **Bounded polling through the Agent Server**: use a narrow bridge for status where live events are unavailable in v1. Bound intervals, respect visibility, abort on disposal, and show stale state.
53
+ 1. **Managed backend view**: for a package-owned backend, feature-detect `host.appBackendView` and call `host.appBackendView.mount({ container })`. Canvas owns session creation, iframe isolation, ingress validation, and revocation.
54
+ 2. **Narrow Agent Server bridge**: for a manual Sidecar, add a backend-owned, authenticated endpoint for only the required operations and call it through `host.agentServer.request` with root-relative paths. Keep Sidecar credentials and routing on the backend.
55
+ 3. **Documented deployment capability**: require a deployment adapter that deliberately exposes Sidecar access, feature-detect it, and document its version and contract.
56
+ 4. **Direct browser connection only when explicitly non-portable**: document its origin, authentication, exposure, and deployment ownership. Do not represent this design as a portable host API 1 App.
57
+ 5. **Bounded polling through the Agent Server**: use a narrow bridge for status where live events are unavailable in v1. Bound intervals, respect visibility, abort on disposal, and show stale state.
19
58
 
20
59
  Reject guessed localhost names or ports, `window.location` derivation, Canvas internals, undocumented globals, and embedded credentials. Do not attempt to route a Sidecar or Automation call through `host.agentServer.request` without a documented Agent Server bridge.
21
60
 
22
61
  ## Model onboarding and operations
23
62
 
24
- Represent service state explicitly: `unknown`, `probing`, `missing`, `stopped`, `incompatible`, `ready`, and `unhealthy`. Preserve actionable failures and transition deliberately for consent, install, start, repair, upgrade, and stop failures. Treat unavailable Sidecar status as a supported UI state.
63
+ For a managed backend, use the server states exactly: `missing`, `stopped`, `starting`, `ready`, `unhealthy`, and `unsupported`. For a manual Sidecar, model additional UI-only states such as `unknown`, `probing`, and `incompatible` when needed. Preserve actionable failures and transition deliberately for consent, prepare/install, start, repair, upgrade, and stop failures.
25
64
 
26
- Before any mutating action, disclose the source, version, checksum, install and data paths, ports and network exposure, process behavior, credentials, and rollback or uninstall behavior. Obtain explicit consent for each applicable action.
65
+ Before any mutating action, disclose the source, resolved revision, version, checksums, install and data paths, ports and network exposure, process behavior, inherited environment names, and rollback or uninstall behavior. Obtain explicit consent for each applicable action. Managed-backend prepare and start calls carry the exact installed revision; updating the package invalidates prior approval.
27
66
 
28
67
  Keep these actions separate and idempotent where meaningful:
29
68