@openhands/extensions 0.28.0 → 0.30.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 (89) hide show
  1. package/.agents/skills/custom-codereview-guide.md +44 -1
  2. package/.claude-plugin/marketplace.json +7 -5
  3. package/.codex-plugin/marketplace.json +7 -5
  4. package/.github/workflows/build-vscode-app.yml +3 -3
  5. package/.github/workflows/check-extensions.yml +1 -1
  6. package/.github/workflows/tests.yml +1 -1
  7. package/.plugin/marketplace.json +7 -5
  8. package/.release-please-manifest.json +1 -1
  9. package/AGENTS.md +7 -2
  10. package/README.md +3 -3
  11. package/apps/vscode/README.md +8 -6
  12. package/apps/vscode/canvas-extension.json +11 -3
  13. package/apps/vscode/extension.js +60 -12
  14. package/apps/vscode/scripts/package.mjs +20 -74
  15. package/automations/bundle-index.js +9 -7
  16. package/automations/catalog/github-issue-to-pr/manifest.json +1 -1
  17. package/automations/catalog/github-issue-triage/manifest.json +4 -3
  18. package/automations/catalog/github-pr-reviewer/manifest.json +5 -4
  19. package/automations/catalog/gitlab-issue-to-mr/manifest.json +3 -3
  20. package/automations/catalog/jira-issue-to-bitbucket-pr/manifest.json +4 -4
  21. package/automations/catalog/jira-issue-to-gitlab-mr/manifest.json +5 -5
  22. package/automations/catalog/jira-issue-to-pr/manifest.json +1 -1
  23. package/integrations/catalog/atlassian-rovo.json +4 -3
  24. package/integrations/catalog/bitbucket.json +16 -1
  25. package/integrations/catalog/github.json +1 -0
  26. package/integrations/catalog/gitlab.json +1 -1
  27. package/marketplaces/openhands-extensions.json +7 -5
  28. package/package.json +1 -1
  29. package/plugins/openhands/skills/openhands-automation/references/custom-automation.md +76 -8
  30. package/plugins/pr-review/README.md +1 -0
  31. package/plugins/pr-review/scripts/prompt.py +44 -11
  32. package/plugins/runtime-api-configs/SKILL.md +1 -1
  33. package/plugins/vulnerability-remediation/skills/github/scripts/agent_conversation.py +224 -8
  34. package/pyproject.toml +1 -1
  35. package/python/openhands_extensions/_version.py +1 -1
  36. package/skills/canvas-extension-api/SKILL.md +4 -1
  37. package/skills/canvas-extension-api/references/backend-safety.md +3 -3
  38. package/skills/canvas-extension-api/references/native-design.md +250 -0
  39. package/skills/canvas-extension-api/references/v1-contract.md +7 -4
  40. package/skills/canvas-extension-api/scripts/validate-extension.mjs +17 -1
  41. package/skills/canvas-extension-api/scripts/validate-extension.test.mjs +49 -0
  42. package/skills/github/scripts/agent_conversation.py +224 -8
  43. package/skills/github-issue-to-pr/scripts/agent_conversation.py +224 -8
  44. package/skills/github-issue-triage/README.md +73 -14
  45. package/skills/github-issue-triage/SKILL.md +26 -46
  46. package/skills/github-issue-triage/scripts/agent_conversation.py +224 -8
  47. package/skills/github-issue-triage/scripts/triage_publication.py +293 -0
  48. package/skills/github-issue-triage/scripts/worker.py +153 -103
  49. package/skills/github-pr-reviewer/README.md +103 -1
  50. package/skills/github-pr-reviewer/SKILL.md +35 -8
  51. package/skills/github-pr-reviewer/references/state-schema.md +8 -0
  52. package/skills/github-pr-reviewer/scripts/agent_conversation.py +224 -8
  53. package/skills/github-pr-reviewer/scripts/main.py +163 -39
  54. package/skills/github-pr-reviewer/scripts/publication.py +193 -0
  55. package/skills/github-pr-reviewer/scripts/worker.py +297 -8
  56. package/skills/github-pr-reviewer/tests/test_main.py +73 -5
  57. package/skills/gitlab-issue-to-mr/README.md +10 -3
  58. package/skills/gitlab-issue-to-mr/SKILL.md +43 -9
  59. package/skills/gitlab-issue-to-mr/references/state-schema.md +4 -0
  60. package/skills/gitlab-issue-to-mr/scripts/main.py +359 -25
  61. package/skills/index.js +10 -10
  62. package/skills/jira-issue-to-pr/.claude-plugin/plugin.json +4 -1
  63. package/skills/jira-issue-to-pr/.codex-plugin/plugin.json +4 -1
  64. package/skills/jira-issue-to-pr/.plugin/plugin.json +4 -1
  65. package/skills/jira-issue-to-pr/README.md +18 -11
  66. package/skills/jira-issue-to-pr/SKILL.md +72 -23
  67. package/skills/jira-issue-to-pr/references/setup.md +43 -12
  68. package/skills/jira-issue-to-pr/scripts/main.py +136 -26
  69. package/skills/openhands-automation/references/custom-automation.md +76 -8
  70. package/skills/slack-channel-monitor/README.md +6 -1
  71. package/skills/slack-channel-monitor/SKILL.md +6 -3
  72. package/skills/slack-channel-monitor/scripts/main.py +36 -4
  73. package/skills/ticket-to-code-change/.claude-plugin/plugin.json +1 -2
  74. package/skills/ticket-to-code-change/.codex-plugin/plugin.json +1 -2
  75. package/skills/ticket-to-code-change/.plugin/plugin.json +1 -2
  76. package/skills/ticket-to-code-change/SKILL.md +5 -3
  77. package/skills/ticket-to-code-change/commands/ticket-to-code-change-setup.md +1 -1
  78. package/tests/fixtures/automations/github-pr-reviewer.json +9 -9
  79. package/tests/test_agent_conversation_dispatch.py +173 -6
  80. package/tests/test_automation_llm_provenance.py +1209 -0
  81. package/tests/test_github_review_publication.py +422 -0
  82. package/tests/test_github_reviewer_delivery.py +174 -21
  83. package/tests/test_github_triage_delivery.py +411 -238
  84. package/tests/test_gitlab_issue_to_mr.py +284 -0
  85. package/tests/test_jira_issue_to_pr.py +200 -6
  86. package/tests/test_pr_review_prompt_injection.py +353 -0
  87. package/tests/test_slack_channel_monitor.py +32 -1
  88. package/tests/test_vscode_app.py +8 -2
  89. package/tests/vscode-app.test.mjs +8 -2
@@ -27,7 +27,32 @@ Cross-repository work is acceptable when this PR contains only the
27
27
  extensions-owned portion and relies on public interfaces from the owning
28
28
  repository.
29
29
 
30
- ## Blocking checkpoints
30
+ ## Issue triage
31
+
32
+ Issue readiness means the work is clear enough to start—not that the fix is
33
+ complete. Do not require a PR, passing implementation tests, or before-and-after
34
+ fix evidence. Reconsider readiness when new information leaves scope or expected
35
+ behavior unresolved. Read the issue, linked discussion, applicable `AGENTS.md`,
36
+ and adjacent source before asking questions.
37
+
38
+ For API failures, compare the extension request with the provider's reported or
39
+ supported version, not upstream main. Use dependency pins or deployment evidence.
40
+ Instructions, request arguments, response parsing, and scanner policy belong
41
+ here; use the ownership boundaries above for other failures. State required
42
+ cross-repository changes and their order. An independent upstream defect need
43
+ not block this work. Naming an owner does not authorize edits or duplicate tickets.
44
+
45
+ Define the smallest useful outcome and explicit non-goals. Cover only affected
46
+ entrypoints, environments, and permissions. For scanners, state whether discovery,
47
+ commenting, and dispatch are allowed for ineligible candidates or after a cap.
48
+ Discovery alone does not authorize unsolicited comments.
49
+
50
+ Request visual evidence only when needed; otherwise use logs, minimal
51
+ reproductions, or failing tests. Do not repeat answered questions. Match managed
52
+ criteria to current human decisions and authoritative readiness policy. Report
53
+ conflicts instead of repeatedly adding a label that policy removes.
54
+
55
+ ## Implementation review checkpoints
31
56
 
32
57
  ### Executable instructions and runtime parity
33
58
 
@@ -42,6 +67,16 @@ in a clean environment far enough to exercise the changed behavior. Keep local
42
67
  and cloud behavior identical unless the documentation names and explains a real
43
68
  platform capability difference.
44
69
 
70
+ For provider changes, inspect the supported implementation; mocks and local types
71
+ alone do not establish an API contract. Separate static/helper tests, tests with
72
+ simulated services, and real integration evidence. If the version or runtime is
73
+ unavailable, report that limit, not an assumed defect. For packaging changes,
74
+ check source, shipped bundle, and deployed files separately.
75
+
76
+ For scanners, check eligibility, permissions, caps, retries, unchanged runs, and
77
+ recovery for each action listed under triage. A launch cap does not permit
78
+ unlimited gate comments.
79
+
45
80
  ### Untrusted triggers and credentials
46
81
 
47
82
  Treat repository events, issue and PR comments, chat messages, catalog fields,
@@ -130,3 +165,11 @@ Do not comment on punctuation, preferred prose tone, optional DRY cleanup, or
130
165
  hypothetical non-standard configurations without a supported failure mode. Before
131
166
  raising a finding, verify that the referenced file and behavior are present in
132
167
  the current PR head and have not already been addressed in review history.
168
+
169
+ ## Review verdict
170
+
171
+ Do not approve material code defects or unmet blocking merge/evidence checks.
172
+ Distinguish code defects from missing merge requirements and optional follow-up.
173
+ If GitHub forbids self-approval of a clean PR, use COMMENT and explain why.
174
+ Respect accepted scope and non-goals: block on a pre-existing limitation only if
175
+ this change exposes it.
@@ -752,7 +752,7 @@
752
752
  {
753
753
  "name": "gitlab-issue-to-mr",
754
754
  "source": "./skills/gitlab-issue-to-mr",
755
- "description": "Create an automation that implements GitLab issues when a configurable trigger label is applied. Clones the default branch for the agent, then commits, pushes, and opens the merge request itself - the agent is handed no push credential.",
755
+ "description": "Create an automation that implements GitLab issues when a configurable trigger label is applied. Polls the projects deterministically and starts one conversation per label event; on OpenHands Cloud and Enterprise it reaches GitLab through the connected MCP server or integration, with no token to set up.",
756
756
  "category": "automations",
757
757
  "keywords": [
758
758
  "gitlab",
@@ -851,11 +851,14 @@
851
851
  {
852
852
  "name": "jira-issue-to-pr",
853
853
  "source": "./skills/jira-issue-to-pr",
854
- "description": "Deploy a cron-based OpenHands automation that watches a Jira Cloud project for issues labeled with a configurable label (default: create-pr) and spawns an agent conversation to create a GitHub pull request for each new issue found. The target GitHub repo is read from the ticket body - no repo parameter required at deploy time.",
854
+ "description": "Deploy a cron-based OpenHands automation that opens a GitHub pull request, a GitLab merge request or a Bitbucket pull request for each Jira issue carrying a configurable label (default: create-pr). The target repository is read from the ticket body - no repo parameter required at deploy time.",
855
855
  "category": "automations",
856
856
  "keywords": [
857
857
  "jira",
858
- "atlassian"
858
+ "atlassian",
859
+ "github",
860
+ "gitlab",
861
+ "bitbucket"
859
862
  ]
860
863
  },
861
864
  {
@@ -873,10 +876,9 @@
873
876
  {
874
877
  "name": "ticket-to-code-change",
875
878
  "source": "./skills/ticket-to-code-change",
876
- "description": "Set up Jira or Linear ticket-to-code-change automations for GitHub, GitLab, and Bitbucket.",
879
+ "description": "Set up Linear ticket-to-code-change automations for GitHub, GitLab, and Bitbucket.",
877
880
  "category": "automations",
878
881
  "keywords": [
879
- "jira",
880
882
  "linear",
881
883
  "github",
882
884
  "gitlab",
@@ -752,7 +752,7 @@
752
752
  {
753
753
  "name": "gitlab-issue-to-mr",
754
754
  "source": "./skills/gitlab-issue-to-mr",
755
- "description": "Create an automation that implements GitLab issues when a configurable trigger label is applied. Clones the default branch for the agent, then commits, pushes, and opens the merge request itself - the agent is handed no push credential.",
755
+ "description": "Create an automation that implements GitLab issues when a configurable trigger label is applied. Polls the projects deterministically and starts one conversation per label event; on OpenHands Cloud and Enterprise it reaches GitLab through the connected MCP server or integration, with no token to set up.",
756
756
  "category": "automations",
757
757
  "keywords": [
758
758
  "gitlab",
@@ -851,11 +851,14 @@
851
851
  {
852
852
  "name": "jira-issue-to-pr",
853
853
  "source": "./skills/jira-issue-to-pr",
854
- "description": "Deploy a cron-based OpenHands automation that watches a Jira Cloud project for issues labeled with a configurable label (default: create-pr) and spawns an agent conversation to create a GitHub pull request for each new issue found. The target GitHub repo is read from the ticket body - no repo parameter required at deploy time.",
854
+ "description": "Deploy a cron-based OpenHands automation that opens a GitHub pull request, a GitLab merge request or a Bitbucket pull request for each Jira issue carrying a configurable label (default: create-pr). The target repository is read from the ticket body - no repo parameter required at deploy time.",
855
855
  "category": "automations",
856
856
  "keywords": [
857
857
  "jira",
858
- "atlassian"
858
+ "atlassian",
859
+ "github",
860
+ "gitlab",
861
+ "bitbucket"
859
862
  ]
860
863
  },
861
864
  {
@@ -873,10 +876,9 @@
873
876
  {
874
877
  "name": "ticket-to-code-change",
875
878
  "source": "./skills/ticket-to-code-change",
876
- "description": "Set up Jira or Linear ticket-to-code-change automations for GitHub, GitLab, and Bitbucket.",
879
+ "description": "Set up Linear ticket-to-code-change automations for GitHub, GitLab, and Bitbucket.",
877
880
  "category": "automations",
878
881
  "keywords": [
879
- "jira",
880
882
  "linear",
881
883
  "github",
882
884
  "gitlab",
@@ -14,15 +14,15 @@ jobs:
14
14
  - uses: actions/checkout@v7
15
15
  - uses: actions/setup-node@v7
16
16
  with:
17
- node-version: "20"
17
+ node-version: "24"
18
18
  - name: Build installable App package
19
19
  run: node apps/vscode/scripts/package.mjs
20
20
  - name: Validate package files
21
21
  run: |
22
22
  test -f dist/apps/vscode/canvas-extension.json
23
23
  test -f dist/apps/vscode/extension.js
24
- test -f dist/apps/vscode/backend/openvscode-linux-amd64.tar.gz
25
- test -f dist/apps/vscode/backend/openvscode-linux-arm64.tar.gz
24
+ test -f dist/apps/vscode/README.md
25
+ test -z "$(find dist/apps/vscode -type f -name '*.tar.gz' -print -quit)"
26
26
  - uses: actions/upload-artifact@v7
27
27
  with:
28
28
  name: vscode-app-${{ github.sha }}
@@ -16,7 +16,7 @@ jobs:
16
16
  - name: Setup Node.js
17
17
  uses: actions/setup-node@v7
18
18
  with:
19
- node-version: "20"
19
+ node-version: "24"
20
20
 
21
21
  - name: Install Claude Code CLI
22
22
  run: npm install -g @anthropic-ai/claude-code
@@ -23,7 +23,7 @@ jobs:
23
23
  - name: Setup Node.js
24
24
  uses: actions/setup-node@v7
25
25
  with:
26
- node-version: "20"
26
+ node-version: "24"
27
27
 
28
28
  - name: Run tests
29
29
  run: uv run --group test pytest tests/
@@ -752,7 +752,7 @@
752
752
  {
753
753
  "name": "gitlab-issue-to-mr",
754
754
  "source": "./skills/gitlab-issue-to-mr",
755
- "description": "Create an automation that implements GitLab issues when a configurable trigger label is applied. Clones the default branch for the agent, then commits, pushes, and opens the merge request itself - the agent is handed no push credential.",
755
+ "description": "Create an automation that implements GitLab issues when a configurable trigger label is applied. Polls the projects deterministically and starts one conversation per label event; on OpenHands Cloud and Enterprise it reaches GitLab through the connected MCP server or integration, with no token to set up.",
756
756
  "category": "automations",
757
757
  "keywords": [
758
758
  "gitlab",
@@ -851,11 +851,14 @@
851
851
  {
852
852
  "name": "jira-issue-to-pr",
853
853
  "source": "./skills/jira-issue-to-pr",
854
- "description": "Deploy a cron-based OpenHands automation that watches a Jira Cloud project for issues labeled with a configurable label (default: create-pr) and spawns an agent conversation to create a GitHub pull request for each new issue found. The target GitHub repo is read from the ticket body - no repo parameter required at deploy time.",
854
+ "description": "Deploy a cron-based OpenHands automation that opens a GitHub pull request, a GitLab merge request or a Bitbucket pull request for each Jira issue carrying a configurable label (default: create-pr). The target repository is read from the ticket body - no repo parameter required at deploy time.",
855
855
  "category": "automations",
856
856
  "keywords": [
857
857
  "jira",
858
- "atlassian"
858
+ "atlassian",
859
+ "github",
860
+ "gitlab",
861
+ "bitbucket"
859
862
  ]
860
863
  },
861
864
  {
@@ -873,10 +876,9 @@
873
876
  {
874
877
  "name": "ticket-to-code-change",
875
878
  "source": "./skills/ticket-to-code-change",
876
- "description": "Set up Jira or Linear ticket-to-code-change automations for GitHub, GitLab, and Bitbucket.",
879
+ "description": "Set up Linear ticket-to-code-change automations for GitHub, GitLab, and Bitbucket.",
877
880
  "category": "automations",
878
881
  "keywords": [
879
- "jira",
880
882
  "linear",
881
883
  "github",
882
884
  "gitlab",
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.28.0"
2
+ ".": "0.30.0"
3
3
  }
package/AGENTS.md CHANGED
@@ -6,7 +6,7 @@ It contains **shareable skills and plugins** that can be loaded by OpenHands (CL
6
6
 
7
7
  ## Cross-Repository Boundaries
8
8
 
9
- This repository owns the public registry of reusable OpenHands skills, plugins, automations, and integrations. These extensions are consumed by OpenHands applications and SDK-based clients.
9
+ This repository owns the public registry of reusable OpenHands skills, plugins, Agent Canvas Apps, automations, and integrations. These extensions are consumed by OpenHands applications and SDK-based clients.
10
10
 
11
11
  Related repositories have distinct responsibilities:
12
12
 
@@ -49,7 +49,12 @@ Before opening a PR:
49
49
  - `plugins/<plugin-name>/hooks/` — lifecycle hooks (optional)
50
50
  - `plugins/<plugin-name>/scripts/` — utility scripts (optional)
51
51
 
52
- There is no application code here; the primary artifacts are Markdown skill definitions and plugin configurations, which can contain `scripts/`, `hooks/` sub-directories.
52
+ - `apps/` — a catalog of Agent Canvas Apps (beta), **one directory per app**.
53
+ - `apps/<app-name>/canvas-extension.json` — the App manifest (Canvas Extensions API v1)
54
+ - `apps/<app-name>/extension.js` — the browser ESM entrypoint exporting `activate(host)`
55
+ - `apps/catalog.json` — the registry catalog, exposed as `APPS_CATALOG` from `@openhands/extensions/apps`
56
+
57
+ Most content is Markdown skill definitions and plugin configurations; the executable parts are plugin `scripts/` and `hooks/` plus the browser ESM App packages under `apps/`, each of which may ship its own `scripts/` and build sub-directories.
53
58
 
54
59
  ## How client code uses this repo
55
60
 
package/README.md CHANGED
@@ -154,10 +154,10 @@ Official skills and plugins for OpenHands — the open-source AI software engine
154
154
  | github-repo-monitor | skill | Create a cron automation that polls a GitHub repository for issue and PR comments containing a configurable trigger p... | `/github-monitor:poll` |
155
155
  | github-stale-ci-pr-closer | skill | Warn and close abandoned pull requests whose required CI remains failing. | `/github-stale-ci-pr-closer` |
156
156
  | gitlab | skill | Interact with GitLab repositories, merge requests, and APIs using the GITLAB_TOKEN environment variable. Use when wor... | — |
157
- | gitlab-issue-to-mr | skill | Create an automation that implements GitLab issues when a configurable trigger label is applied. Clones the default b... | `/issue-to-mr:setup` |
157
+ | gitlab-issue-to-mr | skill | Create an automation that implements GitLab issues when a configurable trigger label is applied. Polls the projects d... | `/issue-to-mr:setup` |
158
158
  | incident-retrospective | skill | Create an automation that drafts incident retrospectives by gathering incident-channel messages from Slack, collectin... | `/incident-retro:setup` |
159
159
  | iterate | skill | Iterate on a GitHub pull request — drive it through CI, code review, and QA until merge-ready. Monitors state, fixes ... | `/iterate`, `/verify`, `/babysit` |
160
- | jira-issue-to-pr | skill | Deploy a cron-based OpenHands automation that watches a Jira Cloud project for issues labeled with a configurable lab... | — |
160
+ | jira-issue-to-pr | skill | Deploy a cron-based OpenHands automation that opens a GitHub pull request, a GitLab merge request or a Bitbucket pull... | — |
161
161
  | jupyter | skill | Read, modify, execute, and convert Jupyter notebooks programmatically. Use when working with .ipynb files for data sc... | — |
162
162
  | kubernetes | skill | Set up and manage local Kubernetes clusters using KIND (Kubernetes IN Docker). Use when testing Kubernetes applicatio... | — |
163
163
  | learn-from-code-review | skill | Distill code review feedback from GitHub PRs into reusable skills and guidelines. Use when users ask to learn from co... | `/learn-from-reviews` |
@@ -190,7 +190,7 @@ Official skills and plugins for OpenHands — the open-source AI software engine
190
190
  | swift-linux | skill | Install and configure Swift programming language on Debian Linux for server-side development. Use when building Swift... | — |
191
191
  | technical-writing | skill | Write and revise technical explanations in flowing, direct, conversational prose that stays concise without becoming ... | — |
192
192
  | theme-factory | skill | Toolkit for styling artifacts with a theme. These artifacts can be slides, docs, reportings, HTML landing pages, etc.... | — |
193
- | ticket-to-code-change | skill | Set up Jira or Linear ticket-to-code-change automations for GitHub, GitLab, and Bitbucket. | `/ticket-to-code-change:setup` |
193
+ | ticket-to-code-change | skill | Set up Linear ticket-to-code-change automations for GitHub, GitLab, and Bitbucket. | `/ticket-to-code-change:setup` |
194
194
  | upstream-fork-sync | skill | Keep a long-lived fork in sync with its upstream. Creates a cron automation that fetches upstream changes, rebases lo... | `/upstream-fork-sync:setup` |
195
195
  | use-jev | skill | Use Jev, a general classifier, for typed choices, scores, and yes/no probabilities in software. | — |
196
196
  | uv | skill | Common project, dependency, and environment operations using uv. | — |
@@ -1,13 +1,15 @@
1
1
  # VS Code App
2
2
 
3
- This App provides a full-height OpenVSCode editor. Before the backend is ready, the App shows backend status, an explicit workspace-folder input, and revision-bound prepare/start/stop/refresh actions. Once ready, the page contains only the editor iframe. The folder is persisted in browser storage and sent as an optional `query.folder` mount parameter; it is a user convenience, not OS sandboxing.
3
+ Install this App in Agent Canvas by opening **Apps → Add App** and entering:
4
4
 
5
- ## Artifact delivery
5
+ ```text
6
+ https://github.com/OpenHands/extensions/tree/main/apps/vscode
7
+ ```
6
8
 
7
- The repository intentionally contains no OpenVSCode binaries. `scripts/package.mjs` downloads the pinned upstream OpenVSCode Server `v1.109.5` Linux amd64 and arm64 archives, verifies their upstream checksums, normalizes the archive layout and metadata, and writes the package-contained archives required by `canvas-extension.json`. The GitHub Actions workflow publishes the resulting `vscode-app-<commit>` artifact; that archive is the installable coordinate for this revision.
9
+ The App provides a full-height OpenVSCode editor. Before the backend is ready, it shows backend status, an optional workspace-folder input, and revision-bound prepare/start/stop/refresh actions. Once ready, the page contains only the editor iframe.
8
10
 
9
- The package is not a release until reviewed and merged. The workflow artifact is a development delivery coordinate, not a published release or a version pin for Agent Server. This App depends on the Canvas consumer change that adds optional `mount({ container, query: { folder } })` support; until that consumer PR lands, setup works but the selected folder is not forwarded to OpenVSCode.
11
+ ## Artifact delivery
10
12
 
11
- The backend uses app-owned data directories for user data and extensions and receives only the manifest allowlisted environment. It does not receive Agent Server credentials. OpenVSCode starts at the existing app-owned `{data_dir}` by default; the optional folder query changes the editor folder only when the Canvas consumer supports it. Folder selection is not OS sandboxing.
13
+ The repository contains no OpenVSCode binaries. During the explicit **Prepare and start** step, Agent Server downloads only the current platform's pinned OpenVSCode Server `v1.109.5` archive from its public GitHub Release, verifies the manifest-pinned SHA-256 digest, safely extracts it into an App-owned content-addressed cache, and starts it with App-owned home, user-data, and extensions directories. It does not send Agent Server or repository credentials to the artifact host.
12
14
 
13
- The real ngrok editor/save/terminal click-through GIF is maintained in the Agent Canvas implementation PR as development-stack evidence. It is not bundled here and is not a release or package test.
15
+ The optional folder is persisted in browser storage and forwarded as the editor folder. It is a user convenience, not OS sandboxing.
@@ -9,10 +9,18 @@
9
9
  "backend": {
10
10
  "schema_version": 1,
11
11
  "artifacts": {
12
- "linux-amd64": {"path": "backend/openvscode-linux-amd64.tar.gz", "sha256": "5ad14274cc5e5ed938ffecf1dd53c0e58272828d8b50cb5da93b6d9e5d2cd9d5"},
13
- "linux-arm64": {"path": "backend/openvscode-linux-arm64.tar.gz", "sha256": "91876160f1b549020204e3909ad42a80d22ef0ebf66a62306cdf60ca5d44524b"}
12
+ "linux-amd64": {
13
+ "url": "https://github.com/gitpod-io/openvscode-server/releases/download/openvscode-server-v1.109.5/openvscode-server-v1.109.5-linux-x64.tar.gz",
14
+ "sha256": "b433bf4f0227321a7014d8460d10a8f958adc0f45aa79bd889e84e65e8f88363",
15
+ "strip_components": 1
16
+ },
17
+ "linux-arm64": {
18
+ "url": "https://github.com/gitpod-io/openvscode-server/releases/download/openvscode-server-v1.109.5/openvscode-server-v1.109.5-linux-arm64.tar.gz",
19
+ "sha256": "36d9c14036489b63de84ebace837fcacf7e60e669a0dc715802c5443684ea4dc",
20
+ "strip_components": 1
21
+ }
14
22
  },
15
- "argv": ["{artifact_dir}/bin/openvscode-bootstrap", "--host", "127.0.0.1", "--port", "{port}", "--without-connection-token", "--accept-server-license-terms", "--telemetry-level", "off", "--user-data-dir", "{data_dir}/user", "--extensions-dir", "{data_dir}/extensions", "--server-base-path", "/app-backends/vscode", "{data_dir}"],
23
+ "argv": ["{artifact_dir}/bin/openvscode-server", "--host", "127.0.0.1", "--port", "{port}", "--without-connection-token", "--accept-server-license-terms", "--telemetry-level", "off", "--user-data-dir", "{data_dir}/user", "--extensions-dir", "{data_dir}/extensions", "--server-base-path", "/app-backends/vscode", "{data_dir}"],
16
24
  "health": {"path": "/", "timeout_seconds": 90, "interval_seconds": 0.5},
17
25
  "inherit_environment": ["PATH", "LANG", "LC_ALL", "TMPDIR"]
18
26
  }
@@ -5,14 +5,32 @@ function folderKey(host) {
5
5
  return `${FOLDER_KEY_PREFIX}:${host.backend.id}:${host.backend.orgId || "local"}:${host.extension.name}`;
6
6
  }
7
7
 
8
- function button(label, onClick) {
8
+ function button(label, onClick, variant = "secondary") {
9
9
  const element = document.createElement("button");
10
10
  element.type = "button";
11
11
  element.textContent = label;
12
+ element.style.cssText = [
13
+ "min-height:40px",
14
+ "padding:0 18px",
15
+ "border-radius:8px",
16
+ "border:1px solid transparent",
17
+ "font:inherit",
18
+ "font-size:14px",
19
+ "font-weight:600",
20
+ "cursor:pointer",
21
+ "transition:opacity 120ms ease,background-color 120ms ease",
22
+ variant === "primary"
23
+ ? "background:#f3ce49;color:#171717"
24
+ : "background:#262626;color:#fafafa;border-color:#525252",
25
+ ].join(";");
12
26
  element.addEventListener("click", onClick);
13
27
  return element;
14
28
  }
15
29
 
30
+ function statusLabel(state) {
31
+ return state.charAt(0).toUpperCase() + state.slice(1);
32
+ }
33
+
16
34
  export function activate(host) {
17
35
  return host.registerPage("editor", async ({ container }) => {
18
36
  container.style.minHeight = "100vh";
@@ -54,26 +72,55 @@ export function activate(host) {
54
72
  return;
55
73
  }
56
74
 
75
+ shell.style.justifyContent = "center";
76
+ shell.style.alignItems = "center";
77
+ shell.style.padding = "32px";
78
+ shell.style.boxSizing = "border-box";
79
+
57
80
  const panel = document.createElement("section");
58
81
  panel.setAttribute("aria-live", "polite");
82
+ panel.style.cssText = "width:min(100%,520px);padding:32px;border:1px solid #262626;border-radius:12px;background:#0a0a0a;color:#fafafa;box-sizing:border-box;text-align:center";
59
83
  const heading = document.createElement("h2");
60
- heading.textContent = "VS Code setup";
61
- const detail = document.createElement("p");
62
- detail.textContent = status.detail || `Backend status: ${status.state}.`;
63
- panel.append(heading, detail);
84
+ heading.textContent = "Set up VS Code";
85
+ heading.style.cssText = "margin:0;font-size:24px;line-height:32px;font-weight:600";
86
+ const description = document.createElement("p");
87
+ description.textContent = "Start the secure VS Code backend for this Agent Canvas session. The first launch downloads and verifies the pinned editor package.";
88
+ description.style.cssText = "margin:12px auto 0;max-width:440px;color:#a3a3a3;font-size:14px;line-height:21px";
89
+ const statusRow = document.createElement("div");
90
+ statusRow.style.cssText = "display:flex;align-items:center;justify-content:center;gap:8px;margin:20px 0 24px;font-size:13px;color:#a3a3a3";
91
+ const statusDot = document.createElement("span");
92
+ statusDot.setAttribute("aria-hidden", "true");
93
+ statusDot.style.cssText = `width:8px;height:8px;border-radius:999px;background:${status.state === "unhealthy" ? "#fda4af" : "#737373"}`;
94
+ const detail = document.createElement("span");
95
+ detail.textContent = status.detail || `Backend status: ${statusLabel(status.state)}`;
96
+ statusRow.append(statusDot, detail);
97
+ panel.append(heading, description, statusRow);
98
+
99
+ const field = document.createElement("label");
100
+ field.style.cssText = "display:flex;flex-direction:column;gap:8px;text-align:left";
101
+ const fieldLabel = document.createElement("span");
102
+ fieldLabel.textContent = "Workspace folder";
103
+ fieldLabel.style.cssText = "font-size:13px;font-weight:500;color:#fafafa";
64
104
  const folder = document.createElement("input");
65
105
  folder.type = "text";
66
- folder.placeholder = "Workspace folder path (optional)";
106
+ folder.placeholder = "Optional path to open in VS Code";
67
107
  folder.value = localStorage.getItem(storageKey) || "";
68
108
  folder.setAttribute("aria-label", "Workspace folder path");
69
- panel.append(folder);
70
- const actions = document.createElement("p");
109
+ folder.style.cssText = "width:100%;height:42px;padding:0 12px;border:1px solid #525252;border-radius:8px;background:#1a1a1a;color:#fafafa;font:inherit;font-size:14px;box-sizing:border-box;outline:none";
110
+ const hint = document.createElement("span");
111
+ hint.textContent = "Leave blank to open the default App workspace.";
112
+ hint.style.cssText = "font-size:12px;color:#8c8c8c";
113
+ field.append(fieldLabel, folder, hint);
114
+ panel.append(field);
115
+
116
+ const actions = document.createElement("div");
117
+ actions.style.cssText = "display:flex;justify-content:center;gap:10px;margin-top:24px;flex-wrap:wrap";
71
118
  if (status.state === "missing" || status.state === "stopped" || status.state === "unhealthy") {
72
119
  actions.append(button("Prepare and start", async () => {
73
120
  const selectedFolder = folder.value.trim();
74
121
  if (selectedFolder) localStorage.setItem(storageKey, selectedFolder);
75
122
  else localStorage.removeItem(storageKey);
76
- actions.replaceChildren(document.createTextNode("Preparing..."));
123
+ actions.replaceChildren(document.createTextNode("Preparing and verifying VS Code..."));
77
124
  try {
78
125
  let approval = status;
79
126
  if (!approval.revision) approval = await request("GET");
@@ -90,14 +137,15 @@ export function activate(host) {
90
137
  render(await request("GET"));
91
138
  } catch (error) {
92
139
  detail.textContent = `Setup failed: ${error.message}`;
93
- actions.replaceChildren(button("Prepare and start", () => render(status)));
140
+ statusDot.style.background = "#fda4af";
141
+ actions.replaceChildren(button("Try again", () => render(status), "primary"));
94
142
  }
95
- }));
143
+ }, "primary"));
96
144
  }
97
145
  if (status.state === "ready" || status.state === "starting") {
98
146
  actions.append(button("Stop", async () => render(await request("POST", `${BACKEND}/stop`))));
99
147
  }
100
- actions.append(button("Refresh", async () => render(await request("GET"))));
148
+ actions.append(button("Refresh status", async () => render(await request("GET"))));
101
149
  panel.append(actions);
102
150
  shell.append(panel);
103
151
  };
@@ -1,84 +1,30 @@
1
- import { createHash } from "node:crypto";
2
- import { readdir, readFile, mkdir, rm, writeFile, chmod, stat } from "node:fs/promises";
3
- import { execFile } from "node:child_process";
4
- import { promisify } from "node:util";
1
+ import { cp, mkdir, readFile, rm } from "node:fs/promises";
5
2
  import path from "node:path";
6
3
 
7
- const exec = promisify(execFile);
8
4
  const appRoot = path.resolve(new URL("..", import.meta.url).pathname);
9
5
  const root = path.resolve(appRoot, "../..");
10
- if (process.platform !== "linux") throw new Error("VS Code App packaging currently supports Linux build runners only");
11
-
12
- /**
13
- * Force a umask-independent mode set on the staged tree.
14
- *
15
- * `cp -a` preserves whatever modes tar materialized, and the build host's umask
16
- * narrows them (0664/0775 under umask 0002, 0644/0755 under umask 022). Those
17
- * bits end up in the final archive's tar headers, so the digest — and with it
18
- * the `sha256` declared in canvas-extension.json — used to vary per build host.
19
- */
20
- async function normalizeModes(dir) {
21
- for (const entry of await readdir(dir, { withFileTypes: true })) {
22
- const target = path.join(dir, entry.name);
23
- if (entry.isSymbolicLink()) continue;
24
- if (entry.isDirectory()) {
25
- await chmod(target, 0o755);
26
- await normalizeModes(target);
27
- } else if (entry.isFile()) {
28
- const { mode } = await stat(target);
29
- await chmod(target, mode & 0o111 ? 0o755 : 0o644);
30
- }
31
- }
32
- }
33
6
  const output = path.resolve(process.env.OUTPUT_DIR ?? path.join(root, "dist"));
34
- const version = "1.109.5";
35
- const releases = {
36
- "linux-amd64": ["x64", "b433bf4f0227321a7014d8460d10a8f958adc0f45aa79bd889e84e65e8f88363"],
37
- "linux-arm64": ["arm64", "36d9c14036489b63de84ebace837fcacf7e60e669a0dc715802c5443684ea4dc"],
38
- };
39
- const work = path.join(output, ".download");
40
7
  const packageRoot = path.join(output, "apps", "vscode");
41
- const manifest = JSON.parse(await readFile(path.join(appRoot, "canvas-extension.json"), "utf8"));
42
- await rm(work, { recursive: true, force: true });
43
- await rm(packageRoot, { recursive: true, force: true });
44
- await mkdir(work, { recursive: true });
45
- await mkdir(path.join(packageRoot, "backend"), { recursive: true });
46
- for (const [platform, [upstreamArch, expected]] of Object.entries(releases)) {
47
- const source = path.join(work, `${platform}.tar.gz`);
48
- const archive = `openvscode-server-v${version}-linux-${upstreamArch}.tar.gz`;
49
- const url = `https://github.com/gitpod-io/openvscode-server/releases/download/openvscode-server-v${version}/${archive}`;
50
- await exec("curl", ["--fail", "--location", "--retry", "3", "--output", source, url]);
51
- const actual = createHash("sha256").update(await readFile(source)).digest("hex");
52
- if (actual !== expected) throw new Error(`${platform}: upstream checksum mismatch (${actual})`);
53
- const unpacked = path.join(work, `${platform}-unpacked`);
54
- const normalized = path.join(work, `${platform}-normalized`);
55
- await mkdir(unpacked, { recursive: true });
56
- await exec("tar", ["-xzf", source, "-C", unpacked]);
57
- await mkdir(normalized, { recursive: true });
58
- const [top] = await readdir(unpacked);
59
- await exec("cp", ["-a", `${path.join(unpacked, top)}/.`, normalized]);
60
- await writeFile(path.join(normalized, "bin", "openvscode-bootstrap"), "#!/bin/sh\nset -eu\ndata_dir=\"\"\nfor candidate do data_dir=\"$candidate\"; done\nmkdir -p \"$data_dir\" \"$data_dir/home\" \"$data_dir/workspace\"\nexport HOME=\"$data_dir/home\"\nexec \"$(dirname \"$0\")/openvscode-server\" \"$@\"\n");
61
- await exec("chmod", ["0755", path.join(normalized, "bin", "openvscode-bootstrap")]);
62
- await normalizeModes(normalized);
63
- const destination = path.join(packageRoot, "backend", `openvscode-${platform}.tar.gz`);
64
- await exec("tar", ["--sort=name", "--mtime=UTC 1970-01-01", "--owner=0", "--group=0", "--numeric-owner", "-czf", destination, "-C", normalized, "."]);
65
- const produced = createHash("sha256").update(await readFile(destination)).digest("hex");
66
- const artifact = manifest?.backend?.artifacts?.[platform];
67
- if (!artifact?.sha256) {
68
- throw new Error(`canvas-extension.json declares no backend.artifacts.${platform}.sha256`);
8
+ const manifest = JSON.parse(
9
+ await readFile(path.join(appRoot, "canvas-extension.json"), "utf8"),
10
+ );
11
+
12
+ for (const [platform, artifact] of Object.entries(manifest.backend.artifacts)) {
13
+ const url = new URL(artifact.url);
14
+ if (url.protocol !== "https:" || !url.pathname.endsWith(".tar.gz")) {
15
+ throw new Error(`${platform}: invalid HTTPS artifact URL`);
69
16
  }
70
- if (path.basename(destination) !== path.basename(artifact.path)) {
71
- throw new Error(`${platform}: built ${path.basename(destination)} but canvas-extension.json declares ${artifact.path}`);
17
+ if (!/^[0-9a-f]{64}$/.test(artifact.sha256)) {
18
+ throw new Error(`${platform}: invalid SHA-256`);
72
19
  }
73
- if (produced !== artifact.sha256) {
74
- throw new Error(
75
- `${platform}: built archive digest ${produced} does not match the sha256 declared for backend.artifacts.${platform} in canvas-extension.json (${artifact.sha256}); update the manifest with the reproducible digest`
76
- );
20
+ if (!Number.isInteger(artifact.strip_components) || artifact.strip_components < 0) {
21
+ throw new Error(`${platform}: invalid strip_components`);
77
22
  }
78
- console.log(`${platform}: ${produced}`);
79
23
  }
80
- await exec("cp", [path.join(appRoot, "canvas-extension.json"), path.join(packageRoot, "canvas-extension.json")]);
81
- await exec("cp", [path.join(appRoot, "extension.js"), path.join(packageRoot, "extension.js")]);
82
- await exec("cp", [path.join(appRoot, "README.md"), path.join(packageRoot, "README.md")]);
83
- await rm(work, { recursive: true, force: true });
84
- console.log(`Wrote ${packageRoot}`);
24
+
25
+ await rm(packageRoot, { recursive: true, force: true });
26
+ await mkdir(packageRoot, { recursive: true });
27
+ for (const filename of ["canvas-extension.json", "extension.js", "README.md"]) {
28
+ await cp(path.join(appRoot, filename), path.join(packageRoot, filename));
29
+ }
30
+ console.log(`Wrote source-installable App package to ${packageRoot}`);