Revert "feat(skills): add private bundle sharing (#14401)" (#14913)

This reverts commit 757fae28d7.
This commit is contained in:
Jinjing
2026-08-16 10:39:57 -07:00
committed by GitHub
parent 1da1bdc01c
commit 763b1febeb
356 changed files with 329 additions and 40894 deletions
+1 -8
View File
@@ -17,7 +17,6 @@
Fixes #
## Visual Proof
<!-- REQUIRED for UI / behavior changes. Please attach a BEFORE and AFTER that can easily tabbed/switched. Use videos for when appropriate over screenshots -->
<!-- If there is truly no visual or interaction change, write exactly: `N/A` and briefly say why. -->
<!-- For attachments NEVER add directly to the PR files (do not commit to files), use `gh image` extension or drag + drop (works for any attachment) -->
@@ -29,19 +28,13 @@ Fixes #
- [ ] I manually tested these changes locally
- [ ] Automated tests added/updated, or explained why not below
## AI Disclosure
## AI Disclosure
<!-- DO NOT FILL IN IF YOU ARE STABLYAI TEAM MEMBER (INTERNAL CONTRIBUTOR), IGNORE SECTION: -->
<!-- Which AI model if anyone was used, please state the details -->
## Review
## Agent skill upstream boundary
- [ ] Not applicable, or this change follows `docs/reference/agent-skill-sharing-upstream-boundary.md` and copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.
## Notes
Ensure no issues in: Security, Cross-platoform support (Linux, Windows, Mac), Remote SSH, Mobile, general backwards compatibility, performance
## Checklist
-96
View File
@@ -923,87 +923,6 @@ jobs:
retention-days: 7
if-no-files-found: ignore
skill-sharing-release-gate:
needs: cut
if: needs.cut.outputs.should_release == 'true'
name: skill sharing release gate ${{ matrix.platform }}
runs-on: ${{ matrix.os }}
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
include:
- os: macos-15
platform: mac
- os: windows-2022
platform: windows
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: refs/tags/${{ needs.cut.outputs.tag }}
- uses: ./.github/actions/install-node-dependencies
with:
native-runtime: node
- name: Install Electron package binary for tests
run: node config/scripts/install-electron-package-binary.mjs
- name: Run skill package, transaction, and compatibility suites
env:
ORCA_REAL_PROCESS_SKILL_TEST: '1'
ORCA_REAL_WINDOWS_SKILL_TEST: ${{ runner.os == 'Windows' && '1' || '0' }}
run: pnpm test:skill-sharing:release --reporter=json --outputFile=skill-sharing-release-results.json
- name: Archive bounded skill-sharing results
if: always()
uses: actions/upload-artifact@v7
with:
name: skill-sharing-release-${{ matrix.platform }}
path: skill-sharing-release-results.json
retention-days: 14
if-no-files-found: error
skill-sharing-linux-floor-release-gate:
needs: cut
if: needs.cut.outputs.should_release == 'true'
name: skill sharing release gate linux-glibc-2.31
runs-on: ubuntu-latest
timeout-minutes: 20
container: ubuntu:20.04
steps:
- name: Install Ubuntu 20.04 prerequisites
run: apt-get update && apt-get install -y build-essential ca-certificates git python3
- name: Checkout
uses: actions/checkout@v6
with:
ref: refs/tags/${{ needs.cut.outputs.tag }}
- uses: ./.github/actions/install-node-dependencies
with:
native-runtime: node
- name: Install Electron package binary for tests
run: node config/scripts/install-electron-package-binary.mjs
- name: Run skill package, transaction, and compatibility suites
env:
ORCA_REAL_PROCESS_SKILL_TEST: '1'
run: pnpm test:skill-sharing:release --reporter=json --outputFile=skill-sharing-release-results.json
- name: Archive bounded skill-sharing results
if: always()
uses: actions/upload-artifact@v7
with:
name: skill-sharing-release-linux-glibc-2.31
path: skill-sharing-release-results.json
retention-days: 14
if-no-files-found: error
# Why: these broader terminal rendering repros are useful release evidence,
# but they include heavier app-like flows and must not block publishing.
terminal-rendering-release-evidence:
@@ -1107,14 +1026,12 @@ jobs:
- os: ubuntu-latest
platform: linux-x64
release_command: node config/scripts/ensure-native-runtime.mjs --runtime=electron && pnpm exec electron-builder --config config/electron-builder.config.cjs --linux AppImage deb rpm --x64 --publish always
unpacked_dir: dist/linux-unpacked
eb_cache_path: |
~/.cache/electron
~/.cache/electron-builder
- os: ubuntu-24.04-arm
platform: linux-arm64
release_command: node config/scripts/ensure-native-runtime.mjs --runtime=electron && ORCA_LINUX_ARM64_RELEASE=1 pnpm exec electron-builder --config config/electron-builder.config.cjs --linux AppImage deb rpm --arm64 --publish always
unpacked_dir: dist/linux-arm64-unpacked
eb_cache_path: |
~/.cache/electron
~/.cache/electron-builder
@@ -1292,17 +1209,6 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Load packaged node-pty on the Linux floor
if: matrix.platform == 'linux-x64' || matrix.platform == 'linux-arm64'
uses: nick-fields/retry@v4
with:
timeout_minutes: 10
max_attempts: 3
retry_wait_seconds: 30
command: >-
node config/scripts/run-linux-packaged-node-pty-floor-smoke.mjs
--app-dir ${{ matrix.unpacked_dir }}
# Why: SignPath signs GitHub workflow artifacts, so Windows builds must
# upload only after the production-signed installer has been returned.
- name: Build Windows release artifacts
@@ -1990,8 +1896,6 @@ jobs:
- cut
- build
- build-mac
- skill-sharing-linux-floor-release-gate
- skill-sharing-release-gate
- terminal-rendering-golden
runs-on: ubuntu-latest
permissions:
-2
View File
@@ -94,8 +94,6 @@ docs/**
!docs/readme/
!docs/readme/**
!docs/STYLEGUIDE.md
!docs/agent-skill-sharing-implementation-checklist.md
!docs/agent-skill-sharing-installation-plan.md
!docs/ai-vault-process-isolation-plan.md
!docs/mobile-terminal-shortcut-bar.md
!docs/reference/
-1
View File
@@ -92,7 +92,6 @@ const winSpeechNativeResource = {
module.exports = {
appId,
productName: 'Orca',
protocols: [{ name: 'Orca', schemes: ['orca'] }],
...(devChannelBuildVersion
? { extraMetadata: { version: devChannelBuildVersion } }
: localBuildVersion
@@ -1,47 +0,0 @@
const { join } = require('node:path')
const resourcesDirectory = process.argv[2]
if (!resourcesDirectory) {
throw new Error('linux-packaged-node-pty-floor-resources-required')
}
const pty = require(join(resourcesDirectory, 'node_modules', 'node-pty'))
const expected = 'orca-node-pty-floor-ok'
let output = ''
let settled = false
const child = pty.spawn('/bin/sh', ['-c', `printf ${expected}; sleep 0.1`], {
name: 'xterm-color',
cols: 80,
rows: 24,
cwd: '/tmp',
env: { PATH: process.env.PATH ?? '/usr/bin:/bin' }
})
const timeout = setTimeout(() => {
if (settled) {
return
}
settled = true
child.kill()
process.stderr.write('packaged node-pty floor smoke timed out\n')
process.exitCode = 1
}, 5_000)
child.onData((data) => {
output = `${output}${data}`.slice(-256)
})
child.onExit(({ exitCode }) => {
if (settled) {
return
}
settled = true
clearTimeout(timeout)
if (exitCode !== 0 || output !== expected) {
process.stderr.write(
`packaged node-pty floor smoke failed: exit=${exitCode} bytes=${Buffer.byteLength(output)}\n`
)
process.exitCode = 1
return
}
process.stdout.write(`${expected}\n`)
})
@@ -1,94 +0,0 @@
import { existsSync } from 'node:fs'
import { spawnSync } from 'node:child_process'
import { isAbsolute, relative, resolve, sep } from 'node:path'
import { pathToFileURL } from 'node:url'
const FLOOR_IMAGE = 'ubuntu:20.04'
const LINUX_EXECUTABLE = 'orca-ide'
const FLOOR_PACKAGES = [
'ca-certificates',
'libasound2',
'libatspi2.0-0',
'libdrm2',
'libgbm1',
'libgtk-3-0',
'libnss3',
'libx11-xcb1',
'libxkbcommon0',
'libxss1'
]
function workspaceRelativePath(workspaceDirectory, inputPath) {
const absolutePath = resolve(workspaceDirectory, inputPath)
const relativePath = relative(workspaceDirectory, absolutePath)
if (!relativePath || relativePath === '..' || relativePath.startsWith(`..${sep}`)) {
throw new Error('linux-packaged-node-pty-floor-app-directory-invalid')
}
return relativePath.split(sep).join('/')
}
export function packagedNodePtyFloorDockerArgs({ workspaceDirectory, appDirectory }) {
const relativeAppDirectory = workspaceRelativePath(workspaceDirectory, appDirectory)
const containerAppDirectory = `/workspace/${relativeAppDirectory}`
const command = [
'export DEBIAN_FRONTEND=noninteractive',
'apt-get update -qq',
`apt-get install -y -qq ${FLOOR_PACKAGES.join(' ')} >/dev/null`,
`ELECTRON_RUN_AS_NODE=1 ${containerAppDirectory}/${LINUX_EXECUTABLE} ` +
'/workspace/config/scripts/linux-packaged-node-pty-floor-child.cjs ' +
`${containerAppDirectory}/resources`
].join(' && ')
return [
'run',
'--rm',
'--mount',
`type=bind,src=${workspaceDirectory},dst=/workspace,readonly`,
'--workdir',
'/workspace',
FLOOR_IMAGE,
'/bin/bash',
'-lc',
command
]
}
function parseAppDirectory(argv) {
const index = argv.indexOf('--app-dir')
const value = index !== -1 ? argv[index + 1] : undefined
if (!value || isAbsolute(value) || value.startsWith('-')) {
throw new Error('Usage: run-linux-packaged-node-pty-floor-smoke.mjs --app-dir <relative-path>')
}
return value
}
export function runPackagedNodePtyFloorSmoke({
workspaceDirectory = process.cwd(),
appDirectory,
spawn = spawnSync
}) {
if (process.platform !== 'linux') {
throw new Error('linux-packaged-node-pty-floor-smoke-requires-linux')
}
const absoluteAppDirectory = resolve(workspaceDirectory, appDirectory)
if (!existsSync(resolve(absoluteAppDirectory, LINUX_EXECUTABLE))) {
throw new Error(`linux-packaged-node-pty-floor-executable-missing: ${absoluteAppDirectory}`)
}
const result = spawn(
'docker',
packagedNodePtyFloorDockerArgs({ workspaceDirectory, appDirectory }),
{ encoding: 'utf8', maxBuffer: 8 * 1024 * 1024 }
)
if (result.error || result.signal || result.status !== 0) {
const detail =
result.error?.message ||
result.stderr?.trim() ||
`status-${result.status}-signal-${result.signal ?? 'none'}`
throw new Error(`linux-packaged-node-pty-floor-smoke-failed: ${detail}`)
}
process.stdout.write(result.stdout)
}
const isMain = process.argv[1] && pathToFileURL(resolve(process.argv[1])).href === import.meta.url
if (isMain) {
runPackagedNodePtyFloorSmoke({ appDirectory: parseAppDirectory(process.argv.slice(2)) })
}
@@ -1,36 +0,0 @@
import { readFileSync } from 'node:fs'
import { describe, expect, it } from 'vitest'
import { packagedNodePtyFloorDockerArgs } from './run-linux-packaged-node-pty-floor-smoke.mjs'
describe('packaged node-pty Linux floor smoke', () => {
it('mounts the exact package read-only in an Ubuntu 20.04 container', () => {
const args = packagedNodePtyFloorDockerArgs({
workspaceDirectory: '/repo',
appDirectory: 'dist/linux-arm64-unpacked'
})
expect(args).toContain('type=bind,src=/repo,dst=/workspace,readonly')
expect(args).toContain('ubuntu:20.04')
expect(args.at(-1)).toContain('/workspace/dist/linux-arm64-unpacked/orca-ide')
expect(args.at(-1)).toContain('ELECTRON_RUN_AS_NODE=1')
})
it('rejects an app directory outside the workspace', () => {
expect(() =>
packagedNodePtyFloorDockerArgs({
workspaceDirectory: '/repo',
appDirectory: '../outside'
})
).toThrow('linux-packaged-node-pty-floor-app-directory-invalid')
})
it('loads node-pty from packaged runtime resources', () => {
const childSource = readFileSync(
new URL('./linux-packaged-node-pty-floor-child.cjs', import.meta.url),
'utf8'
)
expect(childSource).toContain("join(resourcesDirectory, 'node_modules', 'node-pty')")
expect(childSource).not.toContain("join(resourcesDirectory, 'app.asar'")
})
})
@@ -1,82 +0,0 @@
import { readFileSync } from 'node:fs'
import { parse } from 'yaml'
import { describe, expect, it } from 'vitest'
const workflow = parse(readFileSync('.github/workflows/release-cut.yml', 'utf8'))
const packageJson = JSON.parse(readFileSync('package.json', 'utf8'))
function stepNamed(job, name) {
return job.steps.find((step) => step.name === name)
}
describe('skill-sharing release workflow', () => {
it('blocks publication on native Windows, macOS, and the Linux floor', () => {
const platform = workflow.jobs['skill-sharing-release-gate']
const linux = workflow.jobs['skill-sharing-linux-floor-release-gate']
const publishNeeds = workflow.jobs['publish-release'].needs
expect(platform.strategy.matrix.include).toEqual([
{ os: 'macos-15', platform: 'mac' },
{ os: 'windows-2022', platform: 'windows' }
])
expect(linux.container).toBe('ubuntu:20.04')
expect(publishNeeds).toContain('skill-sharing-release-gate')
expect(publishNeeds).toContain('skill-sharing-linux-floor-release-gate')
})
it('runs the focused contract and transaction suite with real Windows coverage', () => {
const platform = workflow.jobs['skill-sharing-release-gate']
const linux = workflow.jobs['skill-sharing-linux-floor-release-gate']
const platformTest = stepNamed(
platform,
'Run skill package, transaction, and compatibility suites'
)
const linuxTest = stepNamed(linux, 'Run skill package, transaction, and compatibility suites')
const command = packageJson.scripts['test:skill-sharing:release']
expect(platformTest.env.ORCA_REAL_WINDOWS_SKILL_TEST).toContain("runner.os == 'Windows'")
expect(platformTest.env.ORCA_REAL_PROCESS_SKILL_TEST).toBe('1')
expect(linuxTest.env.ORCA_REAL_PROCESS_SKILL_TEST).toBe('1')
expect(platformTest.run).toContain('pnpm test:skill-sharing:release')
expect(linuxTest.run).toContain('pnpm test:skill-sharing:release')
expect(command).toContain('src/main/skills')
expect(command).toContain('src/relay/skill-install-handler.test.ts')
expect(command).toContain('src/shared/skill-bundle-install-contract.test.ts')
})
it('archives bounded machine-readable evidence from every platform', () => {
for (const jobName of [
'skill-sharing-release-gate',
'skill-sharing-linux-floor-release-gate'
]) {
const job = workflow.jobs[jobName]
const test = stepNamed(job, 'Run skill package, transaction, and compatibility suites')
const archive = stepNamed(job, 'Archive bounded skill-sharing results')
expect(test.run).toContain('--reporter=json')
expect(test.run).toContain('--outputFile=skill-sharing-release-results.json')
expect(archive.if).toBe('always()')
expect(archive.with['retention-days']).toBe(14)
expect(archive.with['if-no-files-found']).toBe('error')
}
})
it('loads each exact Linux package on the glibc 2.31 floor', () => {
const build = workflow.jobs.build
const smoke = stepNamed(build, 'Load packaged node-pty on the Linux floor')
const linuxEntries = build.strategy.matrix.include.filter(({ platform }) =>
platform.startsWith('linux-')
)
expect(linuxEntries).toEqual([
expect.objectContaining({ platform: 'linux-x64', unpacked_dir: 'dist/linux-unpacked' }),
expect.objectContaining({
platform: 'linux-arm64',
unpacked_dir: 'dist/linux-arm64-unpacked'
})
])
expect(smoke.if).toContain("matrix.platform == 'linux-x64'")
expect(smoke.with.command).toContain('run-linux-packaged-node-pty-floor-smoke.mjs')
expect(smoke.with.command).toContain('${{ matrix.unpacked_dir }}')
})
})
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,97 +0,0 @@
# Administer agent skill sharing
This guide describes the first-release access, lifecycle, retention, and recovery contract for
Orca skill sharing. The operator runbook remains the source of truth for incident commands and
environment-specific procedures.
## Access model
- Shared bundles are unlisted bearer resources. Orca provides no public browse, search, recipient
inventory, or package index.
- Anyone with an active, unexpired link can inspect the package and request a short-lived download
grant without signing in.
- Publishing, package/version management, owned-link inventory, revocation, and deletion require
an authenticated package owner with current organization access.
- Missing, expired, revoked, deleted, and unauthorized resources return the same non-disclosing
response.
- Desktop and remote runtimes receive no GCP identity or long-lived storage credential.
The durable share ID is a credential. Do not put it in tickets, logs, analytics, or support
bundles. Use revocation if a link may have reached an unintended recipient.
## Revocation and deletion
Revoking a share immediately blocks new resolution and download grants. A generation-bound grant
issued before revocation can work until its five-minute expiry. Already installed skills remain on
recipient machines.
Package deletion follows this order:
1. Mark the package deleted and revoke its active shares.
2. Dereference retained versions transactionally.
3. Delete only an object generation that no retained version references.
4. Reconcile bounded pending deletions after partial database or GCS failures.
A version cannot be deleted while an active pinned share references it. Deletion uses the exact
recorded GCS generation and never overwrites an immutable published key.
## User and organization departure
Packages belong to an owner tenant and record the publishing user. In an organization tenant,
another current member can manage the package after its publisher leaves; Orca does not rewrite
the recorded creator. Removing a user does not automatically revoke the organization's links,
delete packages, or remove installed copies.
Before deleting an organization tenant:
1. Disable new grants for the tenant.
2. Have an authorized operator inventory and revoke active shares.
3. Decide whether packages transfer to another authorized owner, remain retained, or are deleted.
4. Resolve legal hold, erasure, and audit-retention requirements.
5. Apply the coordinated metadata and object lifecycle; do not bypass reference checks.
The product does not yet encode a universal ownership-transfer or legal-retention policy. Privacy,
security, and the organization owner must approve the applicable policy before external rollout.
Until that decision is recorded, preserve metadata and soft-deleted generations rather than
guessing.
## Retention contract
| Data | Default behavior |
| -------------------------------------- | --------------------------------------------------------- |
| Upload policy and pending upload row | Expires after 15 minutes |
| Abandoned `uploads/` quarantine object | Deleted by GCS after one day |
| Published immutable package object | No age-based deletion; retained while referenced |
| Issued download grant | Expires after five minutes |
| Deleted package object | Recoverable through GCS soft delete for seven days |
| PostgreSQL metadata | Covered by backups and seven-day point-in-time recovery |
| Installed recipient copy | Independent local data; Cloud deletion does not remove it |
| Audit event | Follows the approved audit-retention policy |
Organization retention, legal hold, and erasure requirements take precedence over product rollback
retention. Product deletion is not a legal-hold mechanism.
## Audit and privacy
Audit records may include package/version IDs, actor IDs, event category, outcome, and timestamp.
They must not include skill contents, filenames, manifests, organization membership lists, local
paths, durable share URLs, upload policies, download grants, or credentials. Anonymous abuse
controls must not persist raw requester IP addresses.
Normal Cloud logs are limited to route, method, status, duration, and bounded failure categories.
Use seeded privacy canaries when validating staging logs and diagnostic exports.
## Recovery and incident controls
Upload, download, and remote-install operations have independent kill switches. Disable the
narrowest affected operation; existing local discovery and installs continue to work.
Coordinate PostgreSQL point-in-time recovery with GCS generation recovery. Restore metadata into
an isolated database, identify exact referenced generations, restore only matching soft-deleted
objects, verify archive and package identities, then transactionally repoint metadata. Keep grants
disabled until bearer preview and a generation-bound download pass.
See the Orca Cloud `docs/skill-sharing-runbook.md` for deployment controls, reconciliation,
saturation, signing failures, database outages, and the guarded restore workflow. Security
invariants and unresolved release gates are recorded in
[Agent skill sharing threat model](./agent-skill-sharing-threat-model.md).
@@ -1,38 +0,0 @@
# Agent skill provider paths
Last verified: 2026-08-11.
V1 supports only providers whose paths are independently established by official documentation.
The registry is deliberately small; it is not copied or synchronized from a community path table.
| Provider | Detection | Global canonical support | Workspace support | Orca placement |
| --- | --- | --- | --- | --- |
| Codex | `codex` CLI found through Orca's host-owned PATH detection | Reads `$HOME/.agents/skills` directly | Reads `.agents/skills` from the current directory through the repository root | Canonical copy only |
| Claude Code | `claude` CLI found through Orca's host-owned PATH detection | Reads `$HOME/.claude/skills` | Reads `.claude/skills` from the launch directory through the repository root, plus nested directories as files are accessed | Relative directory symlink on POSIX, directory junction on Windows, or verified independent-copy fallback |
Codex locations and symlink behavior are documented in the official OpenAI documentation:
[Build skills](https://learn.chatgpt.com/docs/build-skills#where-codex-loads-local-skills).
Claude Code locations, precedence, parent traversal, live detection, and symlink behavior are
documented in the official Anthropic documentation:
[Extend Claude with skills](https://code.claude.com/docs/en/skills#where-skills-live).
Codex therefore needs no provider-specific placement. Claude Code does not document
`.agents/skills` as a discovery root, so Orca reconciles its documented `.claude/skills` path back
to the canonical copy. Orca never replaces a path it does not own. If alias creation is unavailable,
the verified copy fallback is tracked in the install receipt so update and removal can detect drift.
## Registry change process
Every registry change requires normal code review and all of the following evidence:
1. Link current official provider documentation for global and workspace paths.
2. Record whether the provider reads `.agents/skills` directly and its documented link behavior.
3. Verify global and folder-workspace discovery on macOS, Linux, native Windows, and WSL where the
provider supports those platforms.
4. Exercise local, paired-runtime, and SSH host-owned path resolution.
5. Test alias denial, broken owned aliases, independent-copy drift, update, rollback, and removal.
6. Update mixed-version capability evidence if the placement contract changes.
Do not add automated upstream path-table synchronization. A provider release that changes discovery
semantics must enter through this review process.
@@ -1,101 +0,0 @@
# Agent skill sharing threat model
Status: implementation baseline for security and privacy review. This document does not constitute
security approval.
## Scope and trust model
This model covers private skill packaging, Orca Cloud publication and authorization, durable share
resolution, local and remote installation, provider placement, update, rollback, removal, and
operator recovery. It applies to macOS, Linux, native Windows, WSL, paired Orca runtimes, and SSH
targets.
Private means unlisted: an unpredictable active share ID is a bearer credential, while publication
and management remain access-controlled to authenticated Orca users and organizations. V1 is not
end-to-end encrypted from Orca Cloud operators. A skill is code from its author: `SKILL.md` can
change agent behavior and packaged scripts may be executed later by a user or agent, although the
installer itself never executes package content.
## Protected assets
- Skill contents, filenames, manifests, package metadata, release notes, and author identity.
- Organization membership, selected-user ACLs, durable share identifiers, and package existence.
- Signed upload policies, download grants, authentication tokens, database credentials, and GCP
service identities.
- Existing local skills, provider configuration, user modifications, provenance, and transaction
recovery state.
- Cloud package metadata, immutable object generations, audit records, and deletion state.
- Availability and cost of Cloud Run, Cloud SQL, GCS, connected runtimes, and local filesystems.
## Actors and boundaries
Actors include an authorized publisher, an authorized recipient, an authenticated but unauthorized
Orca user, a malicious skill author, a compromised renderer, an untrusted remote RPC caller, an
attacker controlling a network endpoint, and an operator with GCP or database access.
Trust boundaries are:
1. Source skill directory to the owner-private package staging directory.
2. Desktop renderer to the main process and its authenticated Cloud client.
3. Orca client to a paired runtime or SSH host across independently versioned protocols.
4. Orca Cloud API authorization to short-lived GCS access.
5. GCS quarantine to validated immutable publication and PostgreSQL metadata.
6. Extracted staging to canonical destination and provider placements.
7. Local diagnostic records to a user-reviewed support-bundle upload.
8. Terraform and deployment identities to staging and production resources.
## Security invariants
- Package identity is deterministic and binds normalized paths, exact bytes, executable state, and
immutable package/version IDs.
- Publication and installation accept only the `manifest.json` plus `skill/` envelope and never
execute archive content.
- Archive validation completes before destination mutation.
- Destination paths are resolved by the runtime that owns the host or workspace.
- Unowned or modified local content is never silently replaced or deleted.
- Final GCS objects are immutable, generation-fenced, and reachable only through fresh ACL checks
followed by short-lived grants.
- No remote caller can turn a desktop-local path into remote filesystem authority.
- Interrupted transactions converge to a verified old or requested version.
- Logs and support bundles exclude package contents and private authorization or filesystem data.
- Cloud sharing, downloading, and remote installation have independent kill switches.
## Threat register
| ID | Threat | Required controls and current evidence | Residual release gate |
| ----- | ------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| TM-01 | Source changes after preview or a link swaps bytes during packaging. | Observe the specific source directory, copy without following links, re-observe staged bytes, compare every identity, and bind preview to the final digest. Source-drift and link/special-file tests cover rejection and cleanup. | Repeat race and permission tests on every supported filesystem. |
| TM-02 | Archive traversal, drive paths, links, devices, duplicate paths, Unicode/case collisions, or decompression/resource exhaustion escape staging. | Streaming parser rejects unsafe entry classes and normalized collisions; compressed, extracted, entry, file, depth, and per-file limits are enforced during parsing and extraction. Boundary, malformed, checksum, and fuzz tests run before destination mutation. | Keep package safety suites required in release CI. |
| TM-03 | A forged manifest lies about names, bytes, executable state, or package identity. | Parse the manifest before trusting entries, require `skill/SKILL.md`, hash extracted bytes independently, recompute package identity, and compare package/version IDs and digest. | Cross-platform identical-byte digest evidence remains required. |
| TM-04 | A client chooses another home, workspace, WSL distro, SSH path, or escapes a destination root. | The executing runtime resolves home and workspace identities, realpath-checks directories, uses platform-native joins, and rejects client-supplied remote paths. `local-file` ingress is trusted in-process only. | Real SSH and mixed-version topology tests remain required. |
| TM-05 | Installation overwrites or deletion removes unowned or locally modified content. | Planner distinguishes missing, unchanged, clean, modified, unowned, external-link, broken-link, and collision states. Provenance lives outside installed content. Replacement requires an explicit conflict decision; removal revalidates ownership and digest. | Complete the remaining real junction, external-link, copy-drift, and permission matrix. |
| TM-06 | A crash between renames or receipt publication loses both old and new versions. | Same-filesystem staging, durable journals, backups, flushed receipt replacement, bounded recovery, and ownership tokens protect every commit boundary. Failure injection covers every journal transition. | Real process termination and disk/antivirus contention tests remain required. |
| TM-07 | A grant leaks through redirects, userinfo, an insecure scheme, DNS/host confusion, or an oversized stream. | Cloud requests reject redirects. Package downloads require configured origins and HTTPS, reject URL credentials and cross-origin redirects, cap redirect count, recheck expiry, stream exact expected bytes, and verify archive/package digests. | Validate approved production origins and exercise malicious network cases in staging. |
| TM-08 | Reuse of an upload ID, wrong tenant, wrong key, stale generation, or quarantine object publishes attacker-selected bytes. | Random tenant-bound upload rows, signed POST conditions, expiry, exact metadata/key/type/size validation, generation-fenced reads, streamed validation, and idempotent finalization fail closed. | Staging GCS integration must cover stale generations and lifecycle deletion. |
| TM-09 | IDOR, stale organization membership, or a guessed identifier exposes package management data or bearer-protected content. | Management requests authenticate and re-evaluate current ownership. Recipient preview and grants require the exact unpredictable, active bearer share ID and ignore legacy ACL rows. Not-found responses hide unauthorized existence. | Human authorization review and organization-departure policy approval remain required. |
| TM-10 | Content-addressed deduplication discloses another tenant's package through response shape or timing. | Tenant-scoped APIs never expose GCS keys, generations, or whether an object already existed; existing-object reuse verifies both archive and logical package identity. Cross-tenant tests prove identical archives use separate tenant-hashed objects and response shapes disclose no reuse. | Keep tenant-isolation and response-disclosure coverage required in release CI. |
| TM-11 | A compromised renderer, old client, or arbitrary RPC caller sends credentials, local paths, unknown opcodes, or unsupported operations to a host. | Main owns auth tokens and grants; remote requests use strict schemas and capabilities; package transfer is separate from install; no stream opcode was added; mixed versions fail with update-required results. | Real client-newer/server-newer and SSH parity gates remain required. |
| TM-12 | Transfer replay, overlap, disconnect, or abandoned staging consumes disk or commits different bytes. | Session count, idle time, total bytes, and chunk size are bounded. Offsets are monotonic, identical retry is idempotent, changed replay fails, commit hashes the exact staged file, and cancellation/disconnect cleanup is bounded. | Exercise offline-GCS and real disconnects at every transfer boundary. |
| TM-13 | Provider aliases or junctions escape canonical storage or point at external content. | POSIX aliases are relative from real parents, Windows junctions use absolute canonical targets, targets are revalidated, and unowned/broken/external links are conflicts. Copy fallback is independently hashed and receipt-owned. Real Windows and WSL coverage includes existing, broken, external, denied, and drifted placement behavior. | Preserve the physical placement matrix in release coverage. |
| TM-14 | Skill instructions or scripts are mistaken for trusted Orca code or executed during install. | Share/install previews identify author, organization, scripts, executable files, digest, and version. Installation never runs scripts. Trust copy says the package is code from its author. | Security and design must approve the final trust wording and accessibility behavior. |
| TM-15 | Telemetry, logs, or support bundles leak instructions, filenames, paths, ACLs, grants, policies, or credentials. | Desktop install diagnostics map values to bounded categories before tracing. Deployed staging logs contain route templates and bounded request metadata only, and the logging exclusion removes bearer URLs. Support-bundle tests inject private canaries and prove they are absent from collected output. | Preserve deployed-log and support-bundle privacy checks for future changes. |
| TM-16 | Permissive staging permissions expose package bytes to another local user. | Package archives, downloads, extraction, relayed uploads, locks, journals, and receipts use owner-private modes on POSIX; Windows uses owner-profile paths and inherited ACLs. Existing POSIX download roots are tightened before use. Real Windows, WSL, Ubuntu-floor, and SSH validation passed. | Preserve owner-private staging checks across supported hosts. |
| TM-17 | Deletion, revocation, retention, or user departure leaves unauthorized grants or unrecoverable metadata/blob divergence. | Revocation blocks new grants immediately; existing grants expire within five minutes. Database references govern final deletion, quarantine lifecycle cleans abandonment, and GCS soft delete supplies recovery. Local installs remain independent. | Approve departure/legal-retention policy and exercise coordinated database/GCS recovery. |
| TM-18 | Broad IAM, public bucket access, service-account keys, or a compromised deployment identity bypasses application authorization. | Uniform bucket access, public-access prevention, bucket-scoped object access, service-account-scoped signing, skill-secret-only access, Cloud SQL client role, no desktop IAM, and no long-lived keys are Terraform-defined. | Review the staging and production plans and verify live IAM before rollout. |
| TM-19 | Unbounded validation or request concurrency causes memory, CPU, database, storage, or egress denial of service. | Fixed streaming buffers, package limits, per-instance finalization semaphore, rate/quota limits, bounded transfer sessions, Cloud Run instance limits, lifecycle cleanup, and independent kill switches constrain work. | Complete load testing, dashboards, alerts, and budget thresholds. |
| TM-20 | Operator recovery, diagnostics, or legal workflows bypass tenant isolation or leak content. | Runbooks require generation-specific recovery, coordinated PostgreSQL/GCS restoration, audited lifecycle actions, and no package contents in normal logs. | Security/privacy approval and restricted break-glass procedure remain required. |
## Required review evidence
Security and privacy approval must not rely on this document alone. Reviewers need:
- Package/admission schemas and stable failure categories.
- Archive parser, extraction containment, transaction, provenance, and removal tests.
- Cloud authorization, object-generation, tenant-isolation, deletion, and recovery tests.
- Terraform plans plus live staging IAM, bucket, Cloud Run, Secret Manager, and Cloud SQL evidence.
- Real macOS, Linux-floor, Windows, WSL, paired-runtime, mixed-version, and SSH results.
- Captured staging logs, metrics, traces, and support bundles with seeded private canaries absent.
- Load-test results and independently tested upload, download, and remote-install kill switches.
Approval owners record findings and accepted residual risks outside this implementation document.
The external rollout gate remains closed until those findings are resolved or explicitly accepted.
@@ -1,43 +0,0 @@
# Agent skill sharing upstream boundary
Status: proposed for formal engineering and legal review.
Date: 2026-08-11.
## Context
Orca needs private, durable, cross-machine skill sharing with bounded archive ingestion,
host-owned destination resolution, crash-safe transactions, provenance, and mixed-version remote
support. `vercel-labs/skills` exposes a CLI and does not provide the Cloud authorization or local
transaction contract Orca requires.
The behavioral assessment used upstream commit
`c6f69c631292444cc541ac6d91e2226b0ff247da`.
## Decision
Orca implements its package, Cloud, installation, provider-placement, and recovery behavior
independently. The upstream project is a behavioral reference only.
Do not copy or mechanically translate upstream source, tests, fixtures, registry entries, provider
path tables, comments, or documentation. Derive provider paths from official provider
documentation and verify them with real installations. Orca does not depend on the upstream CLI,
npm package, or an unsupported programmatic API.
If a future change proposes incorporating upstream material, stop and review the exact material,
license, attribution, notices, and maintenance implications before implementation or merge.
## Consequences
- Orca owns stability, security, compatibility, and maintenance of this narrower installer.
- There is no automatic upstream synchronization job.
- Similar behavior is acceptable when independently derived from requirements and official
provider contracts; textual or structural copying is not.
- Provider registry changes require normal code review plus official-documentation and real-host
evidence.
- The pull request template requires reviewers to confirm this boundary for relevant changes.
## Review record
Product chose the reference-only approach during planning. Formal engineering and legal reviewers
remain to be named before external rollout.
-88
View File
@@ -1,88 +0,0 @@
# Share and install agent skills
Orca can put one skill or a bundle of skills behind one unlisted, revocable link. Shared bundles
do not appear in search, a catalog, or a public index. Anyone who has an active link can inspect
and install its contents without signing in, so treat the link like a credential.
## Share skills
Publishing and link management require an Orca account in the desktop app.
1. Open **Skills** and choose **Share skills**.
2. Select one or more skills. One link can contain a large collection, such as 30 skills.
3. Review the bundle name, included skills and files, scripts, executable files, digest, account,
and optional release notes.
4. Choose **Publish skill**, **Publish bundle**, or **Publish new version**, then copy the link.
Orca publishes an immutable version. Later changes do not silently alter a link's current bytes;
publish a new version to update the Cloud package.
Use **Settings → Share Skills** to copy or revoke active links. Revocation blocks new previews and
download grants. A grant issued immediately before revocation can remain usable for up to five
minutes, and revocation does not remove copies that recipients already installed.
## Install from a link
Opening an Orca skill link shows a preview before changing any files. You can also open **Skills**,
choose **Install from link**, and paste the URL.
1. Verify the author and organization.
2. Review the version, release notes, included skills, scripts, executable files, and digest.
3. Select all skills or only the ones you want.
4. Choose the destination machine and either global or workspace scope.
5. Review new, unchanged, updated, and conflicting skills, then choose **Install N skills**.
Supported destinations include the local machine, paired Orca runtimes, WSL, and SSH hosts. The
destination runtime resolves its own home and workspace paths, so folder workspaces and remote
filesystems do not borrow paths from the client machine.
Orca keeps one canonical installed copy and places it where supported agents can discover it.
Current provider coverage is documented in
[Agent skill provider paths](./agent-skill-provider-paths.md).
## Conflicts, updates, and rollback
**Keep local** is the default when an existing skill differs. Orca replaces modified content only
after you explicitly choose to discard it.
Open **Skills → Manage installs** to inspect managed skills and their immutable version history.
Installing the latest version performs an update; selecting an older retained version performs a
rollback. Both use the same protected install transaction. If a bundle changes between versions,
Orca updates only the selected skills that still exist in that version.
An interrupted install is recovered on restart. If Orca reports a conflict or partial result,
review the named skill and retry; completed skills do not need to be installed again.
## Remove an installed skill
Use **Skills → Manage installs → Remove**. Orca removes only copies and provider placements that it
owns and can verify. Modified or unowned files are preserved and reported. Discarding modified
content requires a separate explicit confirmation.
Removing a local install does not revoke its share or delete its Cloud package. Likewise,
revoking or deleting Cloud data does not reach into recipients' machines.
## Retention and deletion
- Upload grants expire after 15 minutes.
- Abandoned upload bytes are removed from quarantine after one day.
- Published versions have no automatic age-based deletion.
- Deleting a package revokes its links before unreferenced objects are deleted.
- Deleted GCS objects remain operator-recoverable through a seven-day soft-delete window.
- Installed copies remain until someone removes them on each destination machine.
Organization legal or retention requirements can override normal rollback and deletion timing.
## Trust and privacy
A skill is code from its author. `SKILL.md` can change agent behavior, and included scripts or
executables may run later when a person or agent uses the skill. Orca validates the package and
never executes its contents during installation, but you should install only from people you trust
and review unexpected scripts or executable files.
Orca records bounded operational identifiers and outcomes. Normal logs, telemetry, and support
bundles exclude skill contents, filenames, manifests, local paths, share URLs, upload policies,
download grants, credentials, and access lists.
If a link no longer works, ask its owner for an active link. Missing, expired, revoked, and deleted
links intentionally show the same response so Orca does not disclose private package existence.
-1
View File
@@ -25,7 +25,6 @@
"lint:react-doctor:changed": "node config/scripts/lint-react-doctor-changed.mjs",
"prepare": "husky",
"test": "node config/scripts/ensure-native-runtime.mjs --runtime=node && vitest run --config config/vitest.config.ts",
"test:skill-sharing:release": "vitest run --config config/vitest.config.ts src/main/skills src/main/runtime/rpc/methods/skills.test.ts src/relay/skill-install-handler.test.ts src/shared/skill-bundle-install-contract.test.ts src/shared/skill-install-contract.test.ts src/shared/skill-install-failure.test.ts src/shared/skill-package-manifest.test.ts",
"test:repro:remote-agent-session": "pnpm run build:cli && pnpm run build:electron-vite && node config/scripts/remote-agent-session-authority-repro.mjs",
"check:reliability-gates": "node config/scripts/check-reliability-gates.mjs",
"check:max-lines-ratchet": "node config/scripts/check-max-lines-ratchet.mjs",
+7 -7
View File
@@ -40,18 +40,18 @@
{
"name": "orca-cli",
"sourcePath": "skills/orca-cli",
"releaseRevision": 37,
"packageDigest": "d5648df9c29b479bbbe0dea68f0506b2b0bad0827ce551451fb8f300c113b305",
"gitTreeSha": "ee3a35c76f875ea6ebaf68947af13ba6346b13f2",
"releaseRevision": 36,
"packageDigest": "6eaa0624f45402646d87d9a559f2b3dbbc0efd1d5e56cdfde219671ad62d320e",
"gitTreeSha": "a5a90c9731fbee834eca6d655b39eaa795dd2323",
"files": [
{
"path": "SKILL.md",
"size": 3944,
"size": 3913,
"executable": false,
"classification": "text",
"exactSha256": "cdd5d9c8a95837a6cc24d68b150a117684afe9bbb2d763de2cc7fba1da1ed163",
"textNormalizedSha256": "cdd5d9c8a95837a6cc24d68b150a117684afe9bbb2d763de2cc7fba1da1ed163",
"identitySha256": "cdd5d9c8a95837a6cc24d68b150a117684afe9bbb2d763de2cc7fba1da1ed163"
"exactSha256": "ed609615d7fcefc30509b54a55a5513991f0dc45d817a1e12918f05eb4846012",
"textNormalizedSha256": "ed609615d7fcefc30509b54a55a5513991f0dc45d817a1e12918f05eb4846012",
"identitySha256": "ed609615d7fcefc30509b54a55a5513991f0dc45d817a1e12918f05eb4846012"
}
]
},
-16
View File
@@ -577,22 +577,6 @@
"identitySha256": "ed609615d7fcefc30509b54a55a5513991f0dc45d817a1e12918f05eb4846012"
}
]
},
{
"releaseRevision": 37,
"packageDigest": "d5648df9c29b479bbbe0dea68f0506b2b0bad0827ce551451fb8f300c113b305",
"gitTreeSha": "ee3a35c76f875ea6ebaf68947af13ba6346b13f2",
"files": [
{
"path": "SKILL.md",
"size": 3944,
"executable": false,
"classification": "text",
"exactSha256": "cdd5d9c8a95837a6cc24d68b150a117684afe9bbb2d763de2cc7fba1da1ed163",
"textNormalizedSha256": "cdd5d9c8a95837a6cc24d68b150a117684afe9bbb2d763de2cc7fba1da1ed163",
"identitySha256": "cdd5d9c8a95837a6cc24d68b150a117684afe9bbb2d763de2cc7fba1da1ed163"
}
]
}
],
"orchestration": [
+3 -36
View File
@@ -2,12 +2,12 @@
name: orca-cli
description: >-
Use the public `orca` CLI to operate Orca-managed worktrees, folder contexts,
terminals, repos, automations, artifacts, skill sharing, worktree comments, and the browser
terminals, repos, automations, artifacts, worktree comments, and the browser
embedded inside the Orca app. Use when the user says "$orca-cli", "use orca cli",
"Orca worktree", "child worktree", "cardStatus", "spawn codex/claude in a worktree",
"read/wait/send Orca terminal", "terminal send", "full handoff", "handover",
"give this to another agent", "another worktree", "Orca browser", "orca artifacts",
"share HTML/Markdown", "public artifact link", "share skills", or "control the browser inside
"share HTML/Markdown", "public artifact link", or "control the browser inside
Orca". Prefer this over raw `git worktree`, ad hoc
PTYs, Playwright, or Computer Use when the task touches Orca-managed state.
Use Computer Use for browser windows, webviews, or desktop UI outside Orca's
@@ -272,39 +272,6 @@ ORCA artifacts delete <id> --json
- `ORCA_CLOUD_AUTH_TOKEN` is a development-only authentication override. Prefer the active
Orca profile's normal PropelAuth session and never expose the token in logs or agent output.
## Skill Sharing
Agents can publish one or more installed skills behind one unlisted link through the
signed-in Orca account. The user must first grant the separate, default-off permission in
Settings → Share Skills ("Allow agents and the Orca CLI to publish skill links"). There is
no CLI or RPC way to grant it. Manual publishing from the reviewed desktop flow remains
available without this agent permission.
```text
ORCA skills installed --json
ORCA skills share --skill <selector> [--skill <selector> ...] --bundle-name <name> --json
```
- `skills installed` returns safe discovery IDs and names. It does not expose local skill
paths in CLI output. Sharing then verifies that each `SKILL.md` declares a portable
lowercase name containing only letters, numbers, and hyphens.
- Each `--skill` must be an exact discovery ID or an unambiguous installed-skill name.
Use IDs when names collide.
- Multiple `--skill` flags create one bundle and one link. `--all` and arbitrary paths are
intentionally unsupported; name every skill the user asked to publish.
- Skill folders can contain scripts, configuration, credentials, or other private files.
Treat the permission as authority, not blanket intent: publish only the explicitly
requested skills and never widen the selection.
- A denied command fails with `agent_skill_sharing_disabled`. Do not retry; ask the user to
enable the switch in the desktop app if they want this action.
- Orca stages one agent-published bundle at a time per host. If another publish is active,
wait for it to finish before retrying `agent_skill_sharing_busy`.
- Run the command in an Orca terminal on the machine that stores the skills. Forwarded WSL,
SSH, and paired-runtime invocations fail before discovery so Orca cannot read from the
wrong filesystem.
- The JSON result contains the unlisted URL and public share/package/version IDs. It never
includes cloud authentication tokens.
## Built-In Browser
The built-in browser is Orca's embedded browser tab surface, scoped to Orca worktrees; it is not Chrome/Safari or desktop app UI.
@@ -376,7 +343,7 @@ Common recoveries:
## Next Action
Confirm `orca status --json` unless already checked this turn, then choose the narrowest command for the job: `worktree ps/current/create`, `terminal list/read/wait/send`, `automations list`, `artifacts list/share`, `skills installed/share`, or built-in browser `snapshot`.
Confirm `orca status --json` unless already checked this turn, then choose the narrowest command for the job: `worktree ps/current/create`, `terminal list/read/wait/send`, `automations list`, `artifacts list/share`, or built-in browser `snapshot`.
## Mobile Emulator (iOS Simulator via serve-sim)
+2 -2
View File
@@ -2,12 +2,12 @@
name: orca-cli
description: >-
Use the public `orca` CLI to operate Orca-managed worktrees, folder contexts,
terminals, repos, automations, artifacts, skill sharing, worktree comments, and the browser
terminals, repos, automations, artifacts, worktree comments, and the browser
embedded inside the Orca app. Use when the user says "$orca-cli", "use orca cli",
"Orca worktree", "child worktree", "cardStatus", "spawn codex/claude in a worktree",
"read/wait/send Orca terminal", "terminal send", "full handoff", "handover",
"give this to another agent", "another worktree", "Orca browser", "orca artifacts",
"share HTML/Markdown", "public artifact link", "share skills", or "control the browser inside
"share HTML/Markdown", "public artifact link", or "control the browser inside
Orca". Prefer this over raw `git worktree`, ad hoc
PTYs, Playwright, or Computer Use when the task touches Orca-managed state.
Use Computer Use for browser windows, webviews, or desktop UI outside Orca's
File diff suppressed because one or more lines are too long
-5
View File
@@ -235,11 +235,6 @@ export const HANDLER_GROUPS: readonly HandlerGroup[] = [
keys: ['vm recipe doctor'],
load: async () => (await import('./handlers/vm.js')).VM_HANDLERS
},
{
name: 'skill-sharing',
keys: ['skills installed', 'skills share'],
load: async () => (await import('./handlers/skill-sharing.js')).SKILL_SHARING_HANDLERS
},
{
name: 'skills',
keys: ['skills list', 'skills get', 'skills install', 'skills update'],
-267
View File
@@ -1,267 +0,0 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { REPEATED_FLAG_SEPARATOR } from '../args'
import { RuntimeRpcFailureError } from '../runtime-client'
import { SKILL_SHARING_HANDLERS } from './skill-sharing'
const successMeta = { runtimeId: 'runtime-1' }
function context(
call: ReturnType<typeof vi.fn>,
flags = new Map<string, string | boolean>(),
options: { isRemote?: boolean; json?: boolean } = {}
) {
return {
client: { call, isRemote: options.isRemote ?? false },
cwd: '/repo',
flags,
json: options.json ?? false
} as never
}
afterEach(() => {
vi.restoreAllMocks()
vi.unstubAllEnvs()
})
describe('skill sharing CLI handlers', () => {
it('lists safe installed-skill selectors without local paths', async () => {
const call = vi.fn().mockResolvedValue({
id: 'request-1',
ok: true,
result: {
skills: [
{
id: 'skill-id',
name: 'alpha',
description: 'Alpha skill',
providers: ['codex'],
sourceKind: 'home',
sourceLabel: 'Codex',
rootPath: '/secret/root',
directoryPath: '/secret/root/alpha',
skillFilePath: '/secret/root/alpha/SKILL.md',
installed: true,
updatedAt: null
}
],
sources: [],
scannedAt: 1
},
_meta: successMeta
})
const log = vi.spyOn(console, 'log').mockImplementation(() => undefined)
await SKILL_SHARING_HANDLERS['skills installed']!(context(call, new Map(), { json: true }))
expect(call).toHaveBeenCalledWith('skills.discover', { cwd: '/repo' })
const output = String(log.mock.calls[0][0])
expect(output).toContain('skill-id')
expect(output).not.toContain('/secret/root')
})
it('denies publishing at preflight before invoking skills.share', async () => {
const call = vi.fn().mockResolvedValue({
id: 'request-1',
ok: true,
result: { settings: { agentSkillSharingEnabled: false } },
_meta: successMeta
})
await expect(
SKILL_SHARING_HANDLERS['skills share']!(
context(
call,
new Map([
['skill', 'alpha'],
['bundle-name', 'team-skills']
])
)
)
).rejects.toMatchObject({ code: 'agent_skill_sharing_disabled' })
expect(call).toHaveBeenCalledExactlyOnceWith('settings.get')
})
it('publishes multiple explicit skills and prints only public output', async () => {
const call = vi
.fn()
.mockResolvedValueOnce({
id: 'settings',
ok: true,
result: { settings: { agentSkillSharingEnabled: true } },
_meta: successMeta
})
.mockResolvedValueOnce({
id: 'share',
ok: true,
result: {
status: 'ok',
value: {
share: { id: 'shr_public', url: 'https://share.onorca.dev/skills/share/shr_public' },
version: {
packageId: 'pkg_public',
versionId: 'ver_public',
name: 'team-skills'
},
selectedSkills: [
{ id: 'alpha-id', name: 'alpha', description: null },
{ id: 'beta-id', name: 'beta', description: null }
]
}
},
_meta: successMeta
})
const log = vi.spyOn(console, 'log').mockImplementation(() => undefined)
await SKILL_SHARING_HANDLERS['skills share']!(
context(
call,
new Map([
['skill', `alpha${REPEATED_FLAG_SEPARATOR}beta`],
['bundle-name', 'team-skills'],
['release-notes', 'Initial bundle']
]),
{ json: true }
)
)
expect(call).toHaveBeenLastCalledWith(
'skills.share',
{
skillSelectors: ['alpha', 'beta'],
bundleName: 'team-skills',
releaseNotes: 'Initial bundle',
target: { cwd: '/repo' }
},
{ timeoutMs: 600_000 }
)
const output = String(log.mock.calls[0][0])
expect(output).toContain('shr_public')
expect(output).toContain('pkg_public')
expect(output).not.toMatch(/token|secret/i)
})
it('normalizes a human-readable bundle name before publishing', async () => {
const call = vi
.fn()
.mockResolvedValueOnce({
id: 'settings',
ok: true,
result: { settings: { agentSkillSharingEnabled: true } },
_meta: successMeta
})
.mockResolvedValueOnce({
id: 'share',
ok: true,
result: {
status: 'ok',
value: {
share: { id: 'shr_public', url: 'https://share.onorca.dev/skills/share/shr_public' },
version: {
packageId: 'pkg_public',
versionId: 'ver_public',
name: 'team-skills-v2.0'
},
selectedSkills: [{ id: 'alpha-id', name: 'alpha', description: null }]
}
},
_meta: successMeta
})
vi.spyOn(console, 'log').mockImplementation(() => undefined)
await SKILL_SHARING_HANDLERS['skills share']!(
context(
call,
new Map([
['skill', 'alpha'],
['bundle-name', 'Téam Skills -- v2.0']
])
)
)
expect(call).toHaveBeenLastCalledWith(
'skills.share',
{
skillSelectors: ['alpha'],
bundleName: 'team-skills-v2.0',
releaseNotes: '',
target: { cwd: '/repo' }
},
{ timeoutMs: 600_000 }
)
})
it('rejects a bundle name that cannot produce a valid package name', async () => {
const call = vi.fn()
await expect(
SKILL_SHARING_HANDLERS['skills share']!(
context(
call,
new Map([
['skill', 'alpha'],
['bundle-name', '🔥']
])
)
)
).rejects.toMatchObject({
code: 'invalid_argument',
message: '--bundle-name must contain at least one English letter or number.'
})
expect(call).not.toHaveBeenCalled()
})
it.each([
['a forwarded shell', false, '/remote/repo'],
['a paired runtime', true, undefined]
])('rejects %s before any discovery or publish call', async (_label, isRemote, forwardedCwd) => {
if (forwardedCwd) {
vi.stubEnv('ORCA_CLI_CWD', forwardedCwd)
}
const call = vi.fn()
await expect(
SKILL_SHARING_HANDLERS['skills share']!(
context(
call,
new Map([
['skill', 'alpha'],
['bundle-name', 'alpha']
]),
{ isRemote }
)
)
).rejects.toMatchObject({ code: 'invalid_environment' })
expect(call).not.toHaveBeenCalled()
})
it('gives an actionable upgrade error for an older runtime', async () => {
const call = vi
.fn()
.mockResolvedValueOnce({
id: 'settings',
ok: true,
result: { settings: {} },
_meta: successMeta
})
.mockRejectedValueOnce(
new RuntimeRpcFailureError({
id: 'share',
ok: false,
error: { code: 'method_not_found', message: 'Unknown method' },
_meta: successMeta
})
)
await expect(
SKILL_SHARING_HANDLERS['skills share']!(
context(
call,
new Map([
['skill', 'alpha'],
['bundle-name', 'alpha']
])
)
)
).rejects.toMatchObject({ code: 'update_required' })
})
})
-185
View File
@@ -1,185 +0,0 @@
import type {
AgentSkillShareOperation,
AgentSkillShareResult
} from '../../shared/agent-skill-sharing-contract'
import {
AGENT_SKILL_SHARING_DISABLED_CODE,
AGENT_SKILL_SHARING_DISABLED_MESSAGE,
AGENT_SKILL_SHARING_DISABLED_NEXT_STEPS
} from '../../shared/agent-skill-sharing-gate'
import { normalizeSkillBundleName } from '../../shared/skill-bundle-name'
import type { SkillCloudOperation } from '../../shared/skill-cloud-contract'
import type { SkillDiscoveryResult } from '../../shared/skills'
import type { CommandHandler, HandlerContext } from '../dispatch'
import { getRepeatedStringFlag } from '../flags'
import { printResult } from '../format'
import {
RuntimeClientError,
RuntimeRpcFailureError,
type RuntimeRpcSuccess
} from '../runtime-client'
const SHARE_TIMEOUT_MS = 10 * 60_000
type InstalledSkillSummary = Pick<
SkillDiscoveryResult['skills'][number],
'id' | 'name' | 'description' | 'providers' | 'sourceKind' | 'sourceLabel'
>
type SharedSkillSummary = {
url: string
shareId: string
packageId: string
versionId: string
bundleName: string
skills: AgentSkillShareResult['selectedSkills']
}
function stringFlag(ctx: HandlerContext, name: string): string | undefined {
const value = ctx.flags.get(name)
return typeof value === 'string' && value.trim() ? value.trim() : undefined
}
function rejectForwardedSkillFilesystem(ctx: HandlerContext, command: string): void {
if (!process.env.ORCA_CLI_CWD && !ctx.client.isRemote) {
return
}
throw new RuntimeClientError(
'invalid_environment',
`orca skills ${command} must run on the machine whose installed skills you want to use. Run the command from an Orca terminal on that machine.`
)
}
async function preflightPublishCapability(ctx: HandlerContext): Promise<void> {
let enabled: unknown
try {
const response = await ctx.client.call<{
settings?: { agentSkillSharingEnabled?: boolean }
}>('settings.get')
enabled = response.result?.settings?.agentSkillSharingEnabled
} catch {
return
}
if (enabled === false) {
throw new RuntimeClientError(
AGENT_SKILL_SHARING_DISABLED_CODE,
AGENT_SKILL_SHARING_DISABLED_MESSAGE,
{ nextSteps: [...AGENT_SKILL_SHARING_DISABLED_NEXT_STEPS] }
)
}
}
function requireCloudOperation<T>(operation: SkillCloudOperation<T>): T {
if (operation.status === 'ok') {
return operation.value
}
if (operation.status === 'reconnect-required') {
throw new RuntimeClientError('authentication_required', 'Sign in to Orca and try again.')
}
throw new RuntimeClientError('authentication_unconfigured', operation.message)
}
function installedSummary(result: SkillDiscoveryResult): InstalledSkillSummary[] {
return result.skills.map(({ id, name, description, providers, sourceKind, sourceLabel }) => ({
id,
name,
description,
providers,
sourceKind,
sourceLabel
}))
}
function formatInstalledSkills(skills: InstalledSkillSummary[]): string {
if (skills.length === 0) {
return 'No installed skills found.'
}
return skills
.map(
(skill) =>
`${skill.name} (${skill.id})\n ${skill.description ?? 'No description'}\n ${skill.sourceLabel}`
)
.join('\n')
}
function sharedSummary(result: AgentSkillShareResult): SharedSkillSummary {
return {
url: result.share.url,
shareId: result.share.id,
packageId: result.version.packageId,
versionId: result.version.versionId,
bundleName: result.version.name,
skills: result.selectedSkills
}
}
function formatSharedSkill(result: SharedSkillSummary): string {
return `Shared ${result.skills.length} skill${result.skills.length === 1 ? '' : 's'}: ${result.url}`
}
async function callShare(
ctx: HandlerContext,
params: {
skillSelectors: string[]
bundleName: string
releaseNotes: string
target: { cwd: string }
}
): Promise<RuntimeRpcSuccess<AgentSkillShareOperation>> {
try {
return await ctx.client.call<AgentSkillShareOperation>('skills.share', params, {
timeoutMs: SHARE_TIMEOUT_MS
})
} catch (error) {
if (error instanceof RuntimeRpcFailureError && error.code === 'method_not_found') {
throw new RuntimeClientError(
'update_required',
'The connected Orca runtime does not support agent skill sharing yet. Update Orca on that machine and try again.'
)
}
throw error
}
}
export const SKILL_SHARING_HANDLERS: Record<string, CommandHandler> = {
'skills installed': async (ctx) => {
rejectForwardedSkillFilesystem(ctx, 'installed')
const response = await ctx.client.call<SkillDiscoveryResult>('skills.discover', {
cwd: ctx.cwd
})
const skills = installedSummary(response.result)
printResult({ ...response, result: { skills } }, ctx.json, (value) =>
formatInstalledSkills(value.skills)
)
},
'skills share': async (ctx) => {
rejectForwardedSkillFilesystem(ctx, 'share')
const skillSelectors = getRepeatedStringFlag(ctx.flags, 'skill')
if (skillSelectors.length === 0) {
throw new RuntimeClientError(
'invalid_argument',
'Select at least one installed skill with --skill. Run `orca skills installed` to list them.'
)
}
const bundleLabel = stringFlag(ctx, 'bundle-name')
if (!bundleLabel) {
throw new RuntimeClientError('invalid_argument', 'Missing required --bundle-name.')
}
const bundleName = normalizeSkillBundleName(bundleLabel)
if (!bundleName) {
throw new RuntimeClientError(
'invalid_argument',
'--bundle-name must contain at least one English letter or number.'
)
}
await preflightPublishCapability(ctx)
const response = await callShare(ctx, {
skillSelectors,
bundleName,
releaseNotes: stringFlag(ctx, 'release-notes') ?? '',
target: { cwd: ctx.cwd }
})
const value = sharedSummary(requireCloudOperation(response.result))
printResult({ ...response, result: value }, ctx.json, formatSharedSkill)
}
}
-2
View File
@@ -23,8 +23,6 @@ Accounts:
account list List managed Claude and Codex accounts on this Orca host
Skills:
skills installed List installed skill selectors
skills share Publish selected skills behind one unlisted link
skills list List version-matched skill guides bundled with this Orca CLI
skills get Print a version-matched skill guide as Markdown
skills install Install bundled Orca skills globally via the community skills CLI
@@ -21,7 +21,6 @@ import {
MIN_COMPATIBLE_RUNTIME_CLIENT_VERSION,
RUNTIME_PROTOCOL_VERSION,
SESSION_TAB_CLOSE_INTENT_RUNTIME_CAPABILITY,
SKILL_INSTALL_RESULT_V2_CAPABILITY,
WORKTREE_VISIBILITY_DEFAULTS_RUNTIME_CAPABILITY,
WORKTREE_VISIBILITY_SOURCE_DEFAULTS_RUNTIME_CAPABILITY
} from '../../shared/protocol-version'
@@ -68,7 +67,6 @@ describe('CLI remote WebSocket transport', () => {
clientCapabilities: [
SESSION_TAB_CLOSE_INTENT_RUNTIME_CAPABILITY,
AGENT_SESSION_BOUNDARY_RUNTIME_CAPABILITY,
SKILL_INSTALL_RESULT_V2_CAPABILITY,
WORKTREE_VISIBILITY_DEFAULTS_RUNTIME_CAPABILITY,
WORKTREE_VISIBILITY_SOURCE_DEFAULTS_RUNTIME_CAPABILITY
]
+2 -2
View File
@@ -214,7 +214,7 @@ describe('orca skills CLI', () => {
'Usage: orca skills get <topic> [--full] [--json]'
)
expect(String(logSpy.mock.calls[1]?.[0])).toContain(
'Commands:\n installed List installed skill selectors'
'Commands:\n list List version-matched skill guides'
)
expect(String(logSpy.mock.calls[1]?.[0])).toContain(
'get Print a version-matched skill guide'
@@ -225,7 +225,7 @@ describe('orca skills CLI', () => {
expect(String(logSpy.mock.calls[1]?.[0])).toContain(
'update Update already-installed Orca skills'
)
expect(String(logSpy.mock.calls[2]?.[0])).toContain('Skills:\n skills installed')
expect(String(logSpy.mock.calls[2]?.[0])).toContain('Skills:\n skills list')
expect(String(logSpy.mock.calls[2]?.[0])).toContain('skills update')
expect(runtimeClientConstructorMock).not.toHaveBeenCalled()
})
-26
View File
@@ -1,26 +0,0 @@
import { describe, expect, it } from 'vitest'
import { effectiveAllowedFlags } from '../args'
import { formatCommandHelp } from '../help'
import { SKILL_COMMAND_SPECS } from './skills'
function spec(path: string): (typeof SKILL_COMMAND_SPECS)[number] {
const found = SKILL_COMMAND_SPECS.find((entry) => entry.path.join(' ') === path)
if (!found) {
throw new Error(`Missing skill spec: ${path}`)
}
return found
}
describe('skill command specs', () => {
it('requires explicit selectors for sharing and exposes no bulk or path flag', () => {
const flags = effectiveAllowedFlags(spec('skills share'))
expect(flags).toContain('skill')
expect(flags).toContain('bundle-name')
expect(flags).not.toContain('all')
expect(flags).not.toContain('path')
expect(formatCommandHelp(spec('skills share'))).toContain(
'Only discovered skill directories can be selected'
)
})
})
-30
View File
@@ -2,36 +2,6 @@ import type { CommandSpec } from '../args'
import { GLOBAL_FLAGS } from '../args'
export const SKILL_COMMAND_SPECS: CommandSpec[] = [
{
path: ['skills', 'installed'],
summary: 'List installed skill selectors',
usage: 'orca skills installed [--json]',
allowedFlags: [...GLOBAL_FLAGS],
notes: [
'Lists discovery IDs and names without reading skill contents into the CLI.',
'Package metadata is validated when the selected skills are shared.',
'Use an exact ID or an unambiguous name with `orca skills share --skill <selector>`.'
]
},
{
path: ['skills', 'share'],
summary: 'Publish explicitly selected installed skills behind one unlisted link',
usage:
'orca skills share --skill <selector> [--skill <selector> ...] --bundle-name <name> ' +
'[--release-notes <text>] [--json]',
allowedFlags: [...GLOBAL_FLAGS, 'skill', 'bundle-name', 'release-notes'],
notes: [
'Requires the default-off permission in Settings → Share Skills.',
'The bundle name may be human-readable; Orca converts it to a portable lowercase package name.',
'Selectors are exact discovery IDs or unambiguous names from `orca skills installed`.',
'Only discovered skill directories can be selected; arbitrary paths and --all are not supported.',
'The resulting link is unlisted. Anyone with it can inspect and install the bundle.'
],
examples: [
'orca skills share --skill frontend --bundle-name "Frontend Skills"',
'orca skills share --skill frontend --skill testing --bundle-name "Team Toolkit" --json'
]
},
{
path: ['skills', 'list'],
summary: 'List version-matched skill guides bundled with this Orca CLI',
@@ -73,9 +73,6 @@ describe('ClaudeRuntimeAuthService', () => {
const { ClaudeRuntimeAuthService } = await import('./runtime-auth-service')
const service = new ClaudeRuntimeAuthService(store as never)
expect(service.getRuntimeConfigDir({ runtime: 'wsl', wslDistro: 'Ubuntu' })).toBe(
ubuntuAuthPath
)
const preparation = await service.prepareForClaudeLaunch()
expect(preparation).toMatchObject({
@@ -151,8 +151,8 @@ export class ClaudeRuntimeAuthService {
})
}
getRuntimeConfigDir(target?: ClaudeAccountSelectionTarget): string {
return this.getPreparation(target).configDir
getRuntimeConfigDir(): string {
return this.pathResolver.getRuntimePaths().configDir
}
private initializeLastSyncedState(): void {
-4
View File
@@ -148,10 +148,6 @@ export class ClaudeAccountService {
return this.cancelPendingClaudeLogin?.() ?? false
}
getRuntimeConfigDir(target?: ClaudeAccountSelectionTarget): string {
return this.runtimeAuth.getRuntimeConfigDir(target)
}
private serializeMutation<T>(fn: () => Promise<T>): Promise<T> {
const next = this.mutationQueue.then(fn, fn)
this.mutationQueue = next.catch(() => {})
@@ -125,11 +125,7 @@ function readAnnotationObjects(
const raw = view.byteArray(valueRva)
if (raw) {
// Annotation strings are not NUL-terminated; trim a trailing one anyway.
let value = raw.toString('utf8')
while (value.endsWith('\0')) {
value = value.slice(0, -1)
}
into[name] = value
into[name] = raw.toString('utf8').replace(/\0+$/, '')
}
}
}
+1 -40
View File
@@ -71,8 +71,6 @@ import { resolveConsent } from './telemetry/consent'
import { triggerStartupNotificationRegistration } from './ipc/startup-notification-registration'
import { OrcaRuntimeService, type RuntimeWorktreeLifecycleEvent } from './runtime/orca-runtime'
import { ArtifactCloudService } from './artifacts/artifact-cloud-service'
import { SkillCloudService } from './skills/skill-cloud-service'
import { recoverPendingSkillTransactions } from './skills/skill-transaction-startup-recovery'
import { isArtifactSharingEnabled } from '../shared/artifact-sharing-gate'
import { loadAgentSessionClaimSigner } from './runtime/agent-session-claim-identity'
import {
@@ -182,8 +180,6 @@ import {
} from './startup/single-instance-lock'
import { startEventLoopStallProbe } from './startup/event-loop-stall-probe'
import { startMainThreadChurnProbe } from './diagnostics/main-thread-churn-probe'
import { parseSkillShareId } from '../shared/skill-share-link'
import { SkillShareDeepLinkState } from './startup/skill-share-deep-link-state'
import {
isStartupDiagnosticsEnabled,
logStartupDiagnostic,
@@ -400,7 +396,6 @@ const expectedRendererReload = createWebContentsTimedFlag()
const recoveryReloadInFlight = createWebContentsTimedFlag()
// Why: a tray "Settings…" click can precede the renderer's ui:openSettings listener; it pulls this one-shot on mount.
const pendingOpenSettings = createWebContentsTimedFlag()
const skillShareDeepLinks = new SkillShareDeepLinkState()
let firstWindowStartupServicesReady: Promise<void> = Promise.resolve()
let managedWslCliReconciliationReady: Promise<void> = Promise.resolve()
let managedWslCliStartupBarrierReady: Promise<void> = Promise.resolve()
@@ -701,9 +696,6 @@ function focusExistingWindow(): void {
}
function requestDesktopActivation(argv: readonly string[] = []): void {
skillShareDeepLinks.capture(argv, (shareId) => {
mainWindow?.webContents.send('ui:openSkillShare', shareId)
})
// Why: a duplicate `orca serve` must not drag a headless server into opening a desktop window (#11935).
if (!shouldActivateDesktopForSecondInstance(argv)) {
return
@@ -711,16 +703,6 @@ function requestDesktopActivation(argv: readonly string[] = []): void {
desktopActivationGate.requestActivation()
}
app.on('open-url', (event, url) => {
if (!parseSkillShareId(url)) {
return
}
event.preventDefault()
requestDesktopActivation([url])
})
skillShareDeepLinks.capture(process.argv)
const handleMacAppActivation = createMacAppActivationHandler({
getWindow: () => mainWindow,
requestActivation: requestDesktopActivation
@@ -906,10 +888,6 @@ ipcMain.handle('ui:consumePendingOpenSettings', (event) =>
pendingOpenSettings.matches(event.sender.id, { consume: true })
)
ipcMain.handle('ui:consumePendingSkillShare', () => {
return skillShareDeepLinks.consume()
})
ipcMain.handle(
'app:startupDiagnostic',
(_event, event: string, details?: Record<string, unknown>) => {
@@ -2390,21 +2368,6 @@ void app.whenReady().then(async () => {
packaged: app.isPackaged,
platform: process.platform
})
const skillTransactionRecovery = recoverPendingSkillTransactions(
join(app.getPath('userData'), 'skill-installs')
)
void skillTransactionRecovery
.then((report) => {
if (report.scanned || report.failures.length || report.truncated) {
console.info('[skills] startup transaction recovery:', {
scanned: report.scanned,
recovered: report.recovered,
failures: report.failures.map((failure) => failure.code),
truncated: report.truncated
})
}
})
.catch((error) => console.warn('[skills] startup transaction recovery failed:', error))
// Why: cohort-classifier reads repo count synchronously at every emit, so hydrate it here — before any IPC handler or window can trigger track().
initCohortClassifier(store)
initOnboardingCohortClassifier(store)
@@ -2596,8 +2559,7 @@ void app.whenReady().then(async () => {
}),
buildAgentHookPtyEnv: () =>
isAgentStatusHooksEnabled(store?.getSettings()) ? agentHookServer.buildPtyEnv() : {},
orchestrationEnvironmentTransport,
skillTransactionRecovery
orchestrationEnvironmentTransport
})
runtime = runtimeService
runtimeService.prepareLegacyWorkerTerminalRecovery()
@@ -2703,7 +2665,6 @@ void app.whenReady().then(async () => {
isArtifactSharingEnabled(store?.getSettings())
)
)
runtimeService.setSkillCloudService(new SkillCloudService(app.getPath('userData')))
runtimeService.setAccountServices({ claudeAccounts, codexAccounts, rateLimits })
runtimeService.setCommitMessageAgentEnvironmentResolvers({
// Why: Codex hooks/auth live in Orca's managed runtime home even for the default path, so every launch must resolve CODEX_HOME via runtime-home.
+1 -1
View File
@@ -527,7 +527,7 @@ describe('registerCoreHandlers', () => {
expect(registerDashboardPopoutHandlersMock).toHaveBeenCalledWith(store, undefined)
expect(registerTerminalPreviewHandlersMock).toHaveBeenCalledWith(runtime)
expect(registerSettingsHandlersMock).toHaveBeenCalledWith(store, agentAwakeService)
expect(registerSkillsHandlersMock).toHaveBeenCalledWith(store, runtime)
expect(registerSkillsHandlersMock).toHaveBeenCalledWith(store)
expect(registerWorkspaceSpaceHandlersMock).toHaveBeenCalledWith(store)
expect(registerWorkspacePortHandlersMock).toHaveBeenCalledWith(store)
expect(registerLocalhostWorktreeLabelHandlersMock).toHaveBeenCalledWith(store)
+1 -1
View File
@@ -175,7 +175,7 @@ export function registerCoreHandlers(
registerTerminalRenderDesyncEvidenceHandler()
registerComputerUsePermissionHandlers()
registerSettingsHandlers(store, agentAwakeService)
registerSkillsHandlers(store, runtime)
registerSkillsHandlers(store)
if (automations) {
registerAutomationHandlers(store, automations)
}
@@ -1,71 +0,0 @@
import type { PairingOffer } from '../../shared/pairing'
import { sendRemoteRuntimeRequest } from '../../shared/remote-runtime-client'
import type {
RuntimeOrchestrationEnvelope,
RuntimeRpcResponse
} from '../../shared/runtime-rpc-envelope'
import {
sendRemoteRuntimeConnectionRequest,
sendRemoteRuntimeSharedControlRequest
} from './runtime-environment-request-connections'
export function sendRemoteRuntimeRequestAbortable(
pairing: PairingOffer,
method: string,
params: unknown,
timeoutMs: number,
envelope?: RuntimeOrchestrationEnvelope,
signal?: AbortSignal
): Promise<RuntimeRpcResponse<unknown>> {
if (signal) {
return sendRemoteRuntimeRequest(pairing, method, params, timeoutMs, envelope, signal)
}
return envelope
? sendRemoteRuntimeRequest(pairing, method, params, timeoutMs, envelope)
: sendRemoteRuntimeRequest(pairing, method, params, timeoutMs)
}
export function sendRemoteRuntimeConnectionRequestAbortable(
environmentId: string,
pairing: PairingOffer,
method: string,
params: unknown,
timeoutMs: number,
signal?: AbortSignal
): Promise<RuntimeRpcResponse<unknown>> {
return signal
? sendRemoteRuntimeConnectionRequest(environmentId, pairing, method, params, timeoutMs, signal)
: sendRemoteRuntimeConnectionRequest(environmentId, pairing, method, params, timeoutMs)
}
export function sendRemoteRuntimeSharedControlRequestAbortable(
environmentId: string,
pairing: PairingOffer,
method: string,
params: unknown,
timeoutMs: number,
envelope?: RuntimeOrchestrationEnvelope,
signal?: AbortSignal
): Promise<RuntimeRpcResponse<unknown>> {
if (signal) {
return sendRemoteRuntimeSharedControlRequest(
environmentId,
pairing,
method,
params,
timeoutMs,
envelope,
signal
)
}
return envelope
? sendRemoteRuntimeSharedControlRequest(
environmentId,
pairing,
method,
params,
timeoutMs,
envelope
)
: sendRemoteRuntimeSharedControlRequest(environmentId, pairing, method, params, timeoutMs)
}
@@ -5,8 +5,7 @@ const runtimeCallQueuePool = new RuntimeRpcCallQueuePool()
export function enqueueRuntimeCall<T>(
selector: string,
method: string,
run: () => Promise<T>,
signal?: AbortSignal
run: () => Promise<T>
): Promise<T> {
return runtimeCallQueuePool.enqueue(selector, method, run, 0, signal)
return runtimeCallQueuePool.enqueue(selector, method, run)
}
@@ -28,8 +28,7 @@ export function sendRemoteRuntimeConnectionRequest<TResult>(
pairing: PairingOffer,
method: string,
params: unknown,
timeoutMs: number,
signal?: AbortSignal
timeoutMs: number
): Promise<RuntimeRpcResponse<TResult>> {
const pairingKey = getPairingKey(pairing)
let cached = requestConnections.get(environmentId)
@@ -41,7 +40,7 @@ export function sendRemoteRuntimeConnectionRequest<TResult>(
}
requestConnections.set(environmentId, cached)
}
return cached.connection.request(method, params, timeoutMs, signal)
return cached.connection.request(method, params, timeoutMs)
}
export function closeRemoteRuntimeRequestConnection(environmentId: string): void {
@@ -57,15 +56,13 @@ export function sendRemoteRuntimeSharedControlRequest<TResult>(
method: string,
params: unknown,
timeoutMs: number,
envelope?: RuntimeOrchestrationEnvelope,
signal?: AbortSignal
envelope?: RuntimeOrchestrationEnvelope
): Promise<RuntimeRpcResponse<TResult>> {
return getSharedControlConnection(environmentId, pairing).request(
method,
params,
timeoutMs,
envelope,
signal
envelope
)
}
@@ -1,4 +1,3 @@
import { waitForPromiseWithSignal } from '../../shared/abort-signal-reason'
import { getPreferredPairingOffer } from '../../shared/runtime-environments'
import { resolveEnvironment, markEnvironmentUsed } from '../../shared/runtime-environment-store'
import { isOrchestrationMutation } from '../../shared/orchestration-rpc-contract'
@@ -16,13 +15,10 @@ import { withRemoteRuntimeTailscaleHint } from '../../shared/remote-runtime-tail
import { enqueueRuntimeCall } from './runtime-environment-call-queue'
import {
reconnectRemoteRuntimeSharedControlConnection,
sendRemoteRuntimeConnectionRequest,
sendRemoteRuntimeSharedControlRequest,
subscribeRemoteRuntimeSharedControlRequest
} from './runtime-environment-request-connections'
import {
sendRemoteRuntimeConnectionRequestAbortable,
sendRemoteRuntimeRequestAbortable,
sendRemoteRuntimeSharedControlRequestAbortable
} from './runtime-environment-abortable-requests'
import { attachRemoteControlDiagnostics } from './runtime-environment-status-diagnostics'
import { runtimeEnvironmentRevisionFailure } from './runtime-environment-revision-guard'
import { withTailscaleHintForResponse } from './runtime-environment-tailscale-response'
@@ -90,8 +86,7 @@ export async function callRuntimeEnvironment(
params: unknown,
timeoutMs?: number,
expectedEnvironmentPairingRevision?: number,
envelope?: RuntimeOrchestrationEnvelope,
options?: { signal?: AbortSignal }
envelope?: RuntimeOrchestrationEnvelope
): Promise<RuntimeRpcResponse<unknown>> {
const environment = resolveEnvironment(userDataPath, selector)
// Why: connection failures reject (they don't resolve as ok:false), so the
@@ -101,84 +96,82 @@ export async function callRuntimeEnvironment(
// environment, so a re-pair between enqueue and dispatch can change it.
let endpoint = getPreferredPairingOffer(environment).endpoint
try {
return await enqueueRuntimeCall(
environment.id,
method,
async () => {
const currentEnvironment = resolveEnvironment(userDataPath, environment.id)
const revisionFailure = runtimeEnvironmentRevisionFailure(
currentEnvironment,
expectedEnvironmentPairingRevision,
method
)
if (revisionFailure) {
return revisionFailure
}
const pairing = getPreferredPairingOffer(currentEnvironment)
endpoint = pairing.endpoint
const effectiveTimeoutMs = timeoutMs ?? DEFAULT_REMOTE_RUNTIME_TIMEOUT_MS
const sharedControlEnvelope = shouldUseSharedControlEnvelope(method, params, envelope)
if (envelope && !sharedControlEnvelope) {
const response = await sendRemoteRuntimeRequestAbortable(
pairing,
method,
params,
effectiveTimeoutMs,
envelope,
options?.signal
)
markEnvironmentUsedFromResponse(userDataPath, currentEnvironment.id, response)
return response
}
if (shouldUseCachedRequestConnection(method)) {
const response = await sendRemoteRuntimeConnectionRequestAbortable(
currentEnvironment.id,
pairing,
method,
params,
effectiveTimeoutMs,
options?.signal
)
markEnvironmentUsedFromResponse(userDataPath, currentEnvironment.id, response)
return response
}
if (
method !== 'status.get' &&
!shouldUseOneShotRequest(method) &&
(await waitForPromiseWithSignal(
supportsSharedControl(userDataPath, currentEnvironment, pairing, effectiveTimeoutMs),
options?.signal
))
) {
const response = await sendRemoteRuntimeSharedControlRequestAbortable(
currentEnvironment.id,
pairing,
method,
params,
effectiveTimeoutMs,
sharedControlEnvelope,
options?.signal
)
markEnvironmentUsedFromResponse(userDataPath, currentEnvironment.id, response)
return response
}
// Why: startup/control-plane RPCs use the proven one-shot path so repo
// hydration cannot be coupled to a stale terminal-control connection.
const response = await sendRemoteRuntimeRequestAbortable(
return await enqueueRuntimeCall(environment.id, method, async () => {
const currentEnvironment = resolveEnvironment(userDataPath, environment.id)
const revisionFailure = runtimeEnvironmentRevisionFailure(
currentEnvironment,
expectedEnvironmentPairingRevision,
method
)
if (revisionFailure) {
return revisionFailure
}
const pairing = getPreferredPairingOffer(currentEnvironment)
endpoint = pairing.endpoint
const effectiveTimeoutMs = timeoutMs ?? DEFAULT_REMOTE_RUNTIME_TIMEOUT_MS
const sharedControlEnvelope = shouldUseSharedControlEnvelope(method, params, envelope)
if (envelope && !sharedControlEnvelope) {
const response = await sendRemoteRuntimeRequest(
pairing,
method,
params,
effectiveTimeoutMs,
sharedControlEnvelope,
options?.signal
envelope
)
markEnvironmentUsedFromResponse(userDataPath, currentEnvironment.id, response)
return response
},
options?.signal
)
}
if (shouldUseCachedRequestConnection(method)) {
const response = await sendRemoteRuntimeConnectionRequest(
currentEnvironment.id,
pairing,
method,
params,
effectiveTimeoutMs
)
markEnvironmentUsedFromResponse(userDataPath, currentEnvironment.id, response)
return response
}
if (
method !== 'status.get' &&
!shouldUseOneShotRequest(method) &&
(await supportsSharedControl(userDataPath, currentEnvironment, pairing, effectiveTimeoutMs))
) {
const response = sharedControlEnvelope
? await sendRemoteRuntimeSharedControlRequest(
currentEnvironment.id,
pairing,
method,
params,
effectiveTimeoutMs,
sharedControlEnvelope
)
: await sendRemoteRuntimeSharedControlRequest(
currentEnvironment.id,
pairing,
method,
params,
effectiveTimeoutMs
)
markEnvironmentUsedFromResponse(userDataPath, currentEnvironment.id, response)
return response
}
// Why: startup/control-plane RPCs use the proven one-shot path so repo
// hydration cannot be coupled to a stale terminal-control connection.
const response = sharedControlEnvelope
? await sendRemoteRuntimeRequest(
pairing,
method,
params,
effectiveTimeoutMs,
sharedControlEnvelope
)
: await sendRemoteRuntimeRequest(pairing, method, params, effectiveTimeoutMs)
markEnvironmentUsedFromResponse(userDataPath, currentEnvironment.id, response)
return response
})
} catch (error) {
if (error instanceof Error && error.name !== 'AbortError') {
if (error instanceof Error) {
error.message = withRemoteRuntimeTailscaleHint(error.message, endpoint)
}
throw error
@@ -1,54 +0,0 @@
import { describe, expect, it } from 'vitest'
import {
skillCloudBundlePackageVersionInstallSchema,
skillCloudBundleShareInstallSchema,
skillCloudPackageVersionInstallSchema,
skillCloudShareInstallSchema
} from './skill-cloud-install-ipc-schemas'
const destination = { scope: 'global' as const }
const providers = ['codex', 'claude']
describe('skill cloud install IPC schemas', () => {
it('accepts provider selection for every install entry point', () => {
expect(
skillCloudShareInstallSchema.parse({ shareId: 'share-1', destination, providers })
).toMatchObject({ providers })
expect(
skillCloudBundleShareInstallSchema.parse({
shareId: 'share-1',
selectedSkillIds: ['skill-1'],
destination,
providers
})
).toMatchObject({ providers })
expect(
skillCloudPackageVersionInstallSchema.parse({
packageId: 'package-1',
versionId: 'version-1',
destination,
providers
})
).toMatchObject({ providers })
expect(
skillCloudBundlePackageVersionInstallSchema.parse({
packageId: 'package-1',
versionId: 'version-1',
selectedSkillIds: ['skill-1'],
destination,
providers
})
).toMatchObject({ providers })
})
it('bounds provider selection', () => {
expect(() =>
skillCloudBundleShareInstallSchema.parse({
shareId: 'share-1',
selectedSkillIds: ['skill-1'],
destination,
providers: Array.from({ length: 65 }, (_, index) => `provider-${index}`)
})
).toThrow()
})
})
@@ -1,59 +0,0 @@
import { z } from 'zod'
import { SkillBundleInstallRequestSchema } from '../../shared/skill-bundle-install-contract'
import { SkillInstallDestinationSchema } from '../../shared/skill-install-contract'
export const skillCloudInstallEnvironmentIdSchema = z.string().min(1).max(128)
const skillCloudInstallProvidersSchema = z.array(z.string().min(1).max(64)).max(64)
const installDestinationFields = {
operationId: z.string().min(1).max(128).optional(),
environmentId: skillCloudInstallEnvironmentIdSchema.optional(),
destination: SkillInstallDestinationSchema,
providers: skillCloudInstallProvidersSchema.optional(),
conflictResolution: z
.enum(['replace-unmodified', 'replace-and-discard-local', 'cancel'])
.optional()
} as const
export const skillCloudShareInstallSchema = z
.object({
shareId: z.string().min(1).max(128),
versionId: z.string().min(1).max(128).optional(),
...installDestinationFields
})
.strict()
export const skillCloudBundleShareInstallSchema = z
.object({
shareId: z.string().min(1).max(128),
versionId: z.string().min(1).max(128).optional(),
operationId: z.string().min(1).max(128).optional(),
environmentId: skillCloudInstallEnvironmentIdSchema.optional(),
selectedSkillIds: SkillBundleInstallRequestSchema.shape.selectedSkillIds,
destination: SkillInstallDestinationSchema,
providers: skillCloudInstallProvidersSchema.optional(),
conflictDecisions: SkillBundleInstallRequestSchema.shape.conflictDecisions.optional()
})
.strict()
export const skillCloudPackageVersionInstallSchema = z
.object({
packageId: z.string().min(1).max(128),
versionId: z.string().min(1).max(128),
...installDestinationFields
})
.strict()
export const skillCloudBundlePackageVersionInstallSchema = z
.object({
packageId: z.string().min(1).max(128),
versionId: z.string().min(1).max(128),
operationId: z.string().min(1).max(128).optional(),
environmentId: skillCloudInstallEnvironmentIdSchema.optional(),
selectedSkillIds: SkillBundleInstallRequestSchema.shape.selectedSkillIds,
destination: SkillInstallDestinationSchema,
providers: skillCloudInstallProvidersSchema.optional(),
conflictDecisions: SkillBundleInstallRequestSchema.shape.conflictDecisions.optional()
})
.strict()
-305
View File
@@ -1,305 +0,0 @@
import { randomUUID } from 'node:crypto'
import { join } from 'node:path'
import { app, BrowserWindow, ipcMain } from 'electron'
import { z } from 'zod'
import { SKILL_INSTALL_UPDATE_REQUIRED_MESSAGE } from '../../shared/skill-install-capability'
import { SkillDiscoveryTargetSchema, type SkillDiscoveryResult } from '../../shared/skills'
import type { SkillCloudDownloadGrant } from '../../shared/skill-cloud-contract'
import type { SkillBundleInstallProgress } from '../../shared/skill-bundle-install-contract'
import type { OrcaRuntimeService } from '../runtime/orca-runtime'
import {
installSkillBundleCloudGrant,
installSkillCloudGrant
} from '../skills/skill-cloud-grant-installation'
import { SkillRemoteInstallCancellation } from '../skills/skill-remote-install-cancellation'
import { classifySkillCloudInstallTarget } from '../skills/skill-cloud-install-target'
import { SkillSharePreparationService } from '../skills/skill-share-preparation-service'
import {
supportsSkillRuntimeBundleInstall,
supportsSkillRuntimeCancellation,
supportsSkillRuntimeInstall
} from '../skills/skill-runtime-capability'
import { callRuntimeEnvironment } from './runtime-environment-transport-routing'
import { registerSkillInstallManagementIpcHandlers } from './skill-install-management-ipc-handlers'
import { sendBundleInstallProgress, sendSkillInstallProgress } from './skill-install-progress-ipc'
import {
skillCloudBundlePackageVersionInstallSchema,
skillCloudBundleShareInstallSchema,
skillCloudInstallEnvironmentIdSchema,
skillCloudPackageVersionInstallSchema,
skillCloudShareInstallSchema
} from './skill-cloud-install-ipc-schemas'
const sharePrepareSchema = z
.object({
skillIds: z.array(z.string().min(1).max(4096)).min(1).max(512),
bundleName: z.string().regex(/^(?!.*(?:--|\.\.))[a-z0-9](?:[a-z0-9.-]{0,62}[a-z0-9])?$/),
target: SkillDiscoveryTargetSchema.optional(),
packageId: z.string().min(1).max(128).optional()
})
.strict()
const sharePublishSchema = z
.object({
preparationId: z.string().uuid(),
releaseNotes: z.string().max(10_000)
})
.strict()
const packageVersionSchema = z
.object({
packageId: z.string().min(1).max(128),
versionId: z.string().min(1).max(128)
})
.strict()
function registerSharingHandlers(
runtime: OrcaRuntimeService,
discover: (target?: z.infer<typeof SkillDiscoveryTargetSchema>) => Promise<SkillDiscoveryResult>
): void {
const preparations = new SkillSharePreparationService(
join(app.getPath('userData'), 'skill-share-preparations'),
{
publishVersion: (request) => runtime.publishSkillPackageVersion(request),
createShare: (packageId, request) => runtime.createSkillPackageShare(packageId, request)
}
)
ipcMain.handle('skills:prepareShare', async (_event, value: unknown) => {
const input = sharePrepareSchema.parse(value)
const result = await discover(input.target)
const requested = new Set(input.skillIds)
const skills = result.skills.filter((candidate) => requested.has(candidate.id))
if (skills.length !== requested.size) {
throw new Error('skill-share-source-not-found')
}
return preparations.prepare({
sources: skills.map((skill) => ({ id: skill.name, sourceDirectory: skill.directoryPath })),
bundleName: input.bundleName,
description:
skills.length === 1 ? (skills[0].description ?? '') : `${skills.length} shared skills`,
packageId: input.packageId
})
})
ipcMain.handle('skills:publishShare', async (_event, value: unknown) => {
const input = sharePublishSchema.parse(value)
return preparations.publish(input, (progress) => {
for (const window of BrowserWindow.getAllWindows()) {
if (!window.isDestroyed()) {
window.webContents.send('skills:shareProgress', progress)
}
}
})
})
ipcMain.handle('skills:cancelShare', (_event, id: unknown) => {
preparations.cancel(z.string().uuid().parse(id))
})
ipcMain.handle('skills:releaseShare', async (_event, id: unknown) => {
await preparations.release(z.string().uuid().parse(id))
})
}
function registerCloudInstallHandlers(runtime: OrcaRuntimeService): void {
const remoteInstallCancellation = new SkillRemoteInstallCancellation()
const installAuthorizedGrant = async (
grant: SkillCloudDownloadGrant,
input:
| z.infer<typeof skillCloudShareInstallSchema>
| z.infer<typeof skillCloudPackageVersionInstallSchema>
) => {
if (!input.environmentId || input.environmentId.startsWith('ssh:')) {
return installSkillCloudGrant(runtime, grant, {
...input,
operationId: input.operationId ?? randomUUID()
})
}
const operationId = input.operationId ?? randomUUID()
const signal = remoteInstallCancellation.begin(operationId)
try {
return await installSkillCloudGrant(runtime, grant, { ...input, operationId }, signal)
} finally {
remoteInstallCancellation.finish(operationId, signal)
}
}
const installAuthorizedBundleGrant = async (
grant: SkillCloudDownloadGrant,
input:
| z.infer<typeof skillCloudBundleShareInstallSchema>
| z.infer<typeof skillCloudBundlePackageVersionInstallSchema>,
onProgress: (progress: SkillBundleInstallProgress) => void
) => {
if (!input.environmentId || input.environmentId.startsWith('ssh:')) {
return installSkillBundleCloudGrant(
runtime,
grant,
{
...input,
operationId: input.operationId ?? randomUUID()
},
undefined,
onProgress
)
}
const operationId = input.operationId ?? randomUUID()
const signal = remoteInstallCancellation.begin(operationId)
try {
return await installSkillBundleCloudGrant(
runtime,
grant,
{ ...input, operationId },
signal,
onProgress
)
} finally {
remoteInstallCancellation.finish(operationId, signal)
}
}
ipcMain.handle('skills:resolveShare', (_event, shareId: unknown) =>
runtime.resolveSkillShare(z.string().min(1).max(128).parse(shareId), {})
)
ipcMain.handle('skills:createDownloadGrant', (_event, shareId: unknown) =>
runtime.createSkillDownloadGrant(z.string().min(1).max(128).parse(shareId), {})
)
ipcMain.handle('skills:installShare', async (event, value: unknown) => {
const parsed = skillCloudShareInstallSchema.parse(value)
const input = { ...parsed, operationId: parsed.operationId ?? randomUUID() }
sendSkillInstallProgress(event, { operationId: input.operationId, phase: 'authorizing' })
if (
input.environmentId &&
!(await supportsSkillRuntimeInstall(app.getPath('userData'), input.environmentId))
) {
return { status: 'unsupported' as const, message: SKILL_INSTALL_UPDATE_REQUIRED_MESSAGE }
}
const installTarget = await classifySkillCloudInstallTarget(runtime, input)
const grant = await runtime.createSkillDownloadGrant(input.shareId, {
versionId: input.versionId,
installTarget
})
if (grant.status === 'ok') {
sendSkillInstallProgress(event, { operationId: input.operationId, phase: 'installing' })
}
return grant.status === 'ok' ? installAuthorizedGrant(grant.value, input) : grant
})
ipcMain.handle('skills:installBundleShare', async (event, value: unknown) => {
const parsed = skillCloudBundleShareInstallSchema.parse(value)
const input = { ...parsed, operationId: parsed.operationId ?? randomUUID() }
sendSkillInstallProgress(event, { operationId: input.operationId, phase: 'authorizing' })
if (
input.environmentId &&
!input.environmentId.startsWith('ssh:') &&
!(await supportsSkillRuntimeBundleInstall(app.getPath('userData'), input.environmentId))
) {
return { status: 'unsupported' as const, message: SKILL_INSTALL_UPDATE_REQUIRED_MESSAGE }
}
const installTarget = await classifySkillCloudInstallTarget(runtime, input)
const grant = await runtime.createSkillDownloadGrant(input.shareId, {
versionId: input.versionId,
installTarget
})
if (grant.status === 'ok') {
sendSkillInstallProgress(event, { operationId: input.operationId, phase: 'installing' })
}
return grant.status === 'ok'
? installAuthorizedBundleGrant(grant.value, input, (progress) =>
sendBundleInstallProgress(event, progress)
)
: grant
})
ipcMain.handle('skills:installPackageVersion', async (event, value: unknown) => {
const parsed = skillCloudPackageVersionInstallSchema.parse(value)
const input = { ...parsed, operationId: parsed.operationId ?? randomUUID() }
sendSkillInstallProgress(event, { operationId: input.operationId, phase: 'authorizing' })
if (
input.environmentId &&
!(await supportsSkillRuntimeInstall(app.getPath('userData'), input.environmentId))
) {
return { status: 'unsupported' as const, message: SKILL_INSTALL_UPDATE_REQUIRED_MESSAGE }
}
const installTarget = await classifySkillCloudInstallTarget(runtime, input)
const grant = await runtime.createSkillPackageVersionDownloadGrant(
input.packageId,
input.versionId,
{ installTarget }
)
if (grant.status === 'ok') {
sendSkillInstallProgress(event, { operationId: input.operationId, phase: 'installing' })
}
return grant.status === 'ok' ? installAuthorizedGrant(grant.value, input) : grant
})
ipcMain.handle('skills:installBundlePackageVersion', async (event, value: unknown) => {
const parsed = skillCloudBundlePackageVersionInstallSchema.parse(value)
const input = { ...parsed, operationId: parsed.operationId ?? randomUUID() }
sendSkillInstallProgress(event, { operationId: input.operationId, phase: 'authorizing' })
if (
input.environmentId &&
!input.environmentId.startsWith('ssh:') &&
!(await supportsSkillRuntimeBundleInstall(app.getPath('userData'), input.environmentId))
) {
return { status: 'unsupported' as const, message: SKILL_INSTALL_UPDATE_REQUIRED_MESSAGE }
}
const installTarget = await classifySkillCloudInstallTarget(runtime, input)
const grant = await runtime.createSkillPackageVersionDownloadGrant(
input.packageId,
input.versionId,
{ installTarget }
)
if (grant.status === 'ok') {
sendSkillInstallProgress(event, { operationId: input.operationId, phase: 'installing' })
}
return grant.status === 'ok'
? installAuthorizedBundleGrant(grant.value, input, (progress) =>
sendBundleInstallProgress(event, progress)
)
: grant
})
ipcMain.handle('skills:cancelInstall', async (_event, value: unknown) => {
const input = z
.object({
operationId: z.string().min(1).max(128),
environmentId: skillCloudInstallEnvironmentIdSchema.optional()
})
.strict()
.parse(value)
if (!input.environmentId || input.environmentId.startsWith('ssh:')) {
return { cancelled: runtime.cancelSharedSkillInstall(input.operationId) }
}
const transferCancelled = remoteInstallCancellation.cancel(input.operationId)
if (!(await supportsSkillRuntimeCancellation(app.getPath('userData'), input.environmentId))) {
return { cancelled: transferCancelled }
}
const response = await callRuntimeEnvironment(
app.getPath('userData'),
input.environmentId,
'skills.cancelInstall',
{ operationId: input.operationId },
15_000
).catch(() => null)
const installCancelled =
response?.ok === true && response.result && typeof response.result === 'object'
? (response.result as { cancelled?: unknown }).cancelled === true
: false
return { cancelled: transferCancelled || installCancelled }
})
ipcMain.handle('skills:getPackage', (_event, packageId: unknown) =>
runtime.getSkillPackage(z.string().min(1).max(128).parse(packageId), {})
)
ipcMain.handle('skills:listOwnedShares', () => runtime.listOwnedSkillShares({}))
ipcMain.handle('skills:revokeShare', (_event, shareId: unknown) =>
runtime.revokeSkillShare(z.string().min(1).max(128).parse(shareId), {})
)
ipcMain.handle('skills:deletePackageVersion', (_event, value: unknown) => {
const input = packageVersionSchema.parse(value)
return runtime.deleteSkillPackageVersion(input.packageId, input.versionId, {})
})
ipcMain.handle('skills:deletePackage', (_event, packageId: unknown) =>
runtime.deleteSkillPackage(z.string().min(1).max(128).parse(packageId), {})
)
}
export function registerSkillCloudIpcHandlers(
runtime: OrcaRuntimeService,
discover: (target?: z.infer<typeof SkillDiscoveryTargetSchema>) => Promise<SkillDiscoveryResult>
): void {
registerSharingHandlers(runtime, discover)
registerCloudInstallHandlers(runtime)
registerSkillInstallManagementIpcHandlers(runtime)
}
@@ -1,124 +0,0 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
const {
callRuntimeEnvironmentMock,
handleMock,
supportsBundleInstallMock,
supportsManagementMock
} = vi.hoisted(() => ({
callRuntimeEnvironmentMock: vi.fn(),
handleMock: vi.fn(),
supportsBundleInstallMock: vi.fn(),
supportsManagementMock: vi.fn()
}))
vi.mock('electron', () => ({
app: { getPath: () => '/test/user-data' },
ipcMain: { handle: handleMock }
}))
vi.mock('./runtime-environment-transport-routing', () => ({
callRuntimeEnvironment: callRuntimeEnvironmentMock
}))
vi.mock('../skills/skill-runtime-capability', () => ({
supportsSkillRuntimeBundleInstall: supportsBundleInstallMock,
supportsSkillRuntimeManagement: supportsManagementMock
}))
vi.mock('../wsl', () => ({ listWslDistrosAsync: vi.fn(async () => []) }))
import { registerSkillInstallManagementIpcHandlers } from './skill-install-management-ipc-handlers'
type IpcHandler = (_event: unknown, value: unknown) => Promise<unknown>
async function waitForCalls(count: number): Promise<void> {
for (
let attempt = 0;
attempt < 100 && callRuntimeEnvironmentMock.mock.calls.length < count;
attempt += 1
) {
await Promise.resolve()
}
expect(callRuntimeEnvironmentMock).toHaveBeenCalledTimes(count)
}
describe('skill install management IPC', () => {
const handlers = new Map<string, IpcHandler>()
beforeEach(() => {
handlers.clear()
callRuntimeEnvironmentMock.mockReset()
handleMock.mockReset()
handleMock.mockImplementation((channel: string, handler: IpcHandler) => {
handlers.set(channel, handler)
})
supportsBundleInstallMock.mockReset().mockResolvedValue(true)
supportsManagementMock.mockReset().mockResolvedValue(true)
})
it('bounds remote bundle preview requests', async () => {
const pending: (() => void)[] = []
let active = 0
let peakActive = 0
callRuntimeEnvironmentMock.mockImplementation(
async (
_root,
_environment,
_method,
request: { name: string; package: { packageDigest: string } }
) => {
active += 1
peakActive = Math.max(peakActive, active)
await new Promise<void>((resolve) => pending.push(resolve))
active -= 1
return {
ok: true,
result: {
name: request.name,
packageDigest: request.package.packageDigest,
destinationIdentity: 'global:remote-1',
currentState: 'missing',
providers: []
}
}
}
)
registerSkillInstallManagementIpcHandlers({} as never)
const handler = handlers.get('skills:previewBundleInstall')
expect(handler).toBeDefined()
const selectedSkills = Array.from({ length: 17 }, (_, index) => ({
id: `skill-${index}`,
name: `skill-${index}`,
digest: 'a'.repeat(64)
}))
const preview = handler!(null, {
environmentId: 'remote-1',
package: {
packageId: 'package-1',
versionId: 'version-1',
bundleDigest: 'c'.repeat(64),
archiveSha256: 'b'.repeat(64),
compressedBytes: 100
},
selectedSkills,
destination: { scope: 'global' }
})
await waitForCalls(8)
expect(peakActive).toBe(8)
pending.splice(0).forEach((resolve) => resolve())
await waitForCalls(16)
expect(peakActive).toBe(8)
pending.splice(0).forEach((resolve) => resolve())
await waitForCalls(17)
expect(peakActive).toBe(8)
pending.splice(0).forEach((resolve) => resolve())
await expect(preview).resolves.toMatchObject({
status: 'ok',
value: { skills: expect.arrayContaining([expect.objectContaining({ name: 'skill-16' })]) }
})
})
})
@@ -1,218 +0,0 @@
import { randomUUID } from 'node:crypto'
import { app, ipcMain } from 'electron'
import { z } from 'zod'
import { SKILL_INSTALL_UPDATE_REQUIRED_MESSAGE } from '../../shared/skill-install-capability'
import {
SkillBundleInstallPreviewRequestSchema,
SkillBundleInstallPreviewSchema
} from '../../shared/skill-bundle-install-contract'
import {
ManagedSkillInstallListSchema,
SkillInstallDestinationSchema,
SkillInstallPreviewSchema,
SkillInstallResultSchema,
SkillPackageIdentitySchema
} from '../../shared/skill-install-contract'
import type { OrcaRuntimeService } from '../runtime/orca-runtime'
import {
supportsSkillRuntimeBundleInstall,
supportsSkillRuntimeManagement
} from '../skills/skill-runtime-capability'
import { listWslDistrosAsync } from '../wsl'
import { callRuntimeEnvironment } from './runtime-environment-transport-routing'
const environmentIdSchema = z.string().min(1).max(128)
const skillNameSchema = z.string().regex(/^[a-z0-9][a-z0-9-]{0,63}$/)
const installPreviewSchema = z
.object({
environmentId: environmentIdSchema.optional(),
package: SkillPackageIdentitySchema,
name: skillNameSchema,
destination: SkillInstallDestinationSchema
})
.strict()
const removeSchema = z
.object({
environmentId: environmentIdSchema.optional(),
name: skillNameSchema,
destination: SkillInstallDestinationSchema,
conflictResolution: z.enum(['replace-and-discard-local', 'cancel']).optional()
})
.strict()
type BundlePreviewInput = z.infer<typeof SkillBundleInstallPreviewRequestSchema> & {
environmentId?: string
}
const REMOTE_BUNDLE_PREVIEW_CONCURRENCY = 8
async function previewBundleInstall(runtime: OrcaRuntimeService, input: BundlePreviewInput) {
if (!input.environmentId) {
return runtime.previewSharedSkillBundleInstallRequest(input)
}
const environmentId = input.environmentId
const previews: z.infer<typeof SkillInstallPreviewSchema>[] = []
for (
let offset = 0;
offset < input.selectedSkills.length;
offset += REMOTE_BUNDLE_PREVIEW_CONCURRENCY
) {
const batch = input.selectedSkills.slice(offset, offset + REMOTE_BUNDLE_PREVIEW_CONCURRENCY)
previews.push(
...(await Promise.all(
batch.map(async (skill) => {
const request = {
package: {
packageId: input.package.packageId,
versionId: input.package.versionId,
packageDigest: skill.digest,
archiveSha256: input.package.archiveSha256,
compressedBytes: input.package.compressedBytes
},
name: skill.name,
destination: input.destination
}
const response = await callRuntimeEnvironment(
app.getPath('userData'),
environmentId,
'skills.previewInstall',
request,
30_000
)
if (response.ok !== true) {
throw new Error(`skill-bundle-preview-remote-${response.error.code}`)
}
return SkillInstallPreviewSchema.parse(response.result)
})
))
)
}
return SkillBundleInstallPreviewSchema.parse({
packageId: input.package.packageId,
versionId: input.package.versionId,
bundleDigest: input.package.bundleDigest,
destinationIdentity: previews[0]?.destinationIdentity ?? '',
skills: input.selectedSkills.map((skill, index) => ({
...skill,
currentState: previews[index].currentState
}))
})
}
export function registerSkillInstallManagementIpcHandlers(runtime: OrcaRuntimeService): void {
ipcMain.handle('skills:listWslDistros', async (_event, environmentIdValue: unknown) => {
const environmentId = environmentIdSchema.optional().parse(environmentIdValue)
if (!environmentId) {
return listWslDistrosAsync()
}
const response = await callRuntimeEnvironment(
app.getPath('userData'),
environmentId,
'host.wsl.listDistros',
{},
15_000
)
return response.ok === true && Array.isArray(response.result)
? response.result.filter((distro): distro is string => typeof distro === 'string')
: []
})
ipcMain.handle('skills:previewInstall', async (_event, value: unknown) => {
const input = installPreviewSchema.parse(value)
const request = { package: input.package, name: input.name, destination: input.destination }
if (!input.environmentId) {
return {
status: 'ok' as const,
value: await runtime.previewSharedSkillInstallRequest(request)
}
}
const userDataPath = app.getPath('userData')
if (!(await supportsSkillRuntimeManagement(userDataPath, input.environmentId))) {
return { status: 'unsupported' as const, message: SKILL_INSTALL_UPDATE_REQUIRED_MESSAGE }
}
const response = await callRuntimeEnvironment(
userDataPath,
input.environmentId,
'skills.previewInstall',
request,
30_000
)
if (response.ok !== true) {
throw new Error(`skill-preview-remote-${response.error.code}`)
}
return { status: 'ok' as const, value: SkillInstallPreviewSchema.parse(response.result) }
})
ipcMain.handle('skills:previewBundleInstall', async (_event, value: unknown) => {
const parsed = z
.object({
environmentId: environmentIdSchema.optional(),
package: SkillBundleInstallPreviewRequestSchema.shape.package,
selectedSkills: SkillBundleInstallPreviewRequestSchema.shape.selectedSkills,
destination: SkillInstallDestinationSchema
})
.strict()
.parse(value)
if (
parsed.environmentId &&
(!(await supportsSkillRuntimeManagement(app.getPath('userData'), parsed.environmentId)) ||
!(await supportsSkillRuntimeBundleInstall(app.getPath('userData'), parsed.environmentId)))
) {
return { status: 'unsupported' as const, message: SKILL_INSTALL_UPDATE_REQUIRED_MESSAGE }
}
return { status: 'ok' as const, value: await previewBundleInstall(runtime, parsed) }
})
ipcMain.handle('skills:removeInstall', async (_event, value: unknown) => {
const input = removeSchema.parse(value)
const request = {
operationId: randomUUID(),
name: input.name,
destination: input.destination,
conflictResolution: input.conflictResolution
}
if (!input.environmentId) {
return {
status: 'ok' as const,
value: await runtime.removeSharedSkillInstallRequest(request)
}
}
const userDataPath = app.getPath('userData')
if (!(await supportsSkillRuntimeManagement(userDataPath, input.environmentId))) {
return { status: 'unsupported' as const, message: SKILL_INSTALL_UPDATE_REQUIRED_MESSAGE }
}
const response = await callRuntimeEnvironment(
userDataPath,
input.environmentId,
'skills.removeInstall',
request,
5 * 60_000
)
if (response.ok !== true) {
throw new Error(`skill-remove-remote-${response.error.code}`)
}
return { status: 'ok' as const, value: SkillInstallResultSchema.parse(response.result) }
})
ipcMain.handle('skills:listManagedInstalls', async (_event, environmentIdValue: unknown) => {
const environmentId = environmentIdSchema.optional().parse(environmentIdValue)
if (!environmentId) {
return { status: 'ok' as const, value: await runtime.listManagedSkillInstalls() }
}
if (environmentId.startsWith('ssh:')) {
const value = await runtime.listManagedSkillInstalls(environmentId.slice('ssh:'.length))
return { status: 'ok' as const, value }
}
const userDataPath = app.getPath('userData')
if (!(await supportsSkillRuntimeManagement(userDataPath, environmentId))) {
return { status: 'unsupported' as const, message: SKILL_INSTALL_UPDATE_REQUIRED_MESSAGE }
}
const response = await callRuntimeEnvironment(
userDataPath,
environmentId,
'skills.listManagedInstalls',
{},
30_000
)
if (response.ok !== true) {
throw new Error(`skill-list-managed-remote-${response.error.code}`)
}
return { status: 'ok' as const, value: ManagedSkillInstallListSchema.parse(response.result) }
})
}
@@ -1,34 +0,0 @@
import { describe, expect, it, vi } from 'vitest'
import type { IpcMainInvokeEvent } from 'electron'
import { sendBundleInstallProgress, sendSkillInstallProgress } from './skill-install-progress-ipc'
function event(destroyed = false): IpcMainInvokeEvent {
return {
sender: { isDestroyed: () => destroyed, send: vi.fn() }
} as unknown as IpcMainInvokeEvent
}
describe('skill install progress IPC', () => {
it('projects destination-owned bundle progress without paths or grants', () => {
const target = event()
sendBundleInstallProgress(target, {
operationId: 'operation_1',
skillId: 'alpha',
skillName: 'alpha',
skillIndex: 2,
skillCount: 30
})
expect(target.sender.send).toHaveBeenCalledWith('skills:installProgress', {
operationId: 'operation_1',
phase: 'installing',
currentSkill: { id: 'alpha', name: 'alpha', index: 2, total: 30 }
})
})
it('does not publish after the invoking renderer is destroyed', () => {
const target = event(true)
sendSkillInstallProgress(target, { operationId: 'operation_1', phase: 'authorizing' })
expect(target.sender.send).not.toHaveBeenCalled()
})
})
@@ -1,28 +0,0 @@
import type { IpcMainInvokeEvent } from 'electron'
import type { SkillBundleInstallProgress } from '../../shared/skill-bundle-install-contract'
import type { SkillInstallProgress } from '../../shared/skill-sharing-contract'
export function sendSkillInstallProgress(
event: IpcMainInvokeEvent,
progress: SkillInstallProgress
): void {
if (!event.sender.isDestroyed()) {
event.sender.send('skills:installProgress', progress)
}
}
export function sendBundleInstallProgress(
event: IpcMainInvokeEvent,
progress: SkillBundleInstallProgress
): void {
sendSkillInstallProgress(event, {
operationId: progress.operationId,
phase: 'installing',
currentSkill: {
id: progress.skillId,
name: progress.skillName,
index: progress.skillIndex,
total: progress.skillCount
}
})
}
+10 -16
View File
@@ -1,6 +1,5 @@
import { app, BrowserWindow, ipcMain } from 'electron'
import type { Store } from '../persistence'
import type { OrcaRuntimeService } from '../runtime/orca-runtime'
import {
SkillDiscoveryTargetSchema,
type SkillDiscoveryResult,
@@ -20,17 +19,8 @@ import {
discoverSkillsOnTarget,
resolveSkillDiscoveryTarget
} from '../skills/skill-discovery-target'
import { registerSkillCloudIpcHandlers } from './skill-cloud-ipc-handlers'
export function registerSkillsHandlers(store: Store, runtime?: OrcaRuntimeService): void {
const discover = async (target?: SkillDiscoveryTarget): Promise<SkillDiscoveryResult> => {
const parsedTarget = target ? SkillDiscoveryTargetSchema.parse(target) : undefined
const resolvedTarget = resolveSkillDiscoveryTarget(parsedTarget)
return discoverSkillsOnTarget(resolvedTarget, store.getRepos(), {
providerRootOverrides: await runtime?.resolveSkillDiscoveryProviderRoots(resolvedTarget),
refresh: parsedTarget?.refresh === true
})
}
export function registerSkillsHandlers(store: Store): void {
const scanInventory = (): Promise<SkillFreshnessInventory> =>
// Why: the update command targets this machine's global homes. WSL and SSH
// inventories stay out until their installer rail has an equivalent proof.
@@ -65,13 +55,17 @@ export function registerSkillsHandlers(store: Store, runtime?: OrcaRuntimeServic
ipcMain.handle(
'skills:discover',
async (_event, target?: SkillDiscoveryTarget): Promise<SkillDiscoveryResult> => discover(target)
async (_event, target?: SkillDiscoveryTarget): Promise<SkillDiscoveryResult> => {
const parsedTarget = target ? SkillDiscoveryTargetSchema.parse(target) : undefined
return discoverSkillsOnTarget(
resolveSkillDiscoveryTarget(parsedTarget),
store.getRepos(),
// Why: only a caller that knows disk changed may bypass the shared scans.
{ refresh: parsedTarget?.refresh === true }
)
}
)
if (runtime) {
registerSkillCloudIpcHandlers(runtime, discover)
}
ipcMain.handle('skills:freshnessInventory', async (): Promise<SkillFreshnessInventory> => {
return scanInventory()
})
@@ -93,20 +93,6 @@ describe('Store', () => {
expect(updated.branchPrefix).toBe('git-username')
})
it('persists the agent skill sharing capability as an exact boolean', async () => {
const store = await createStore()
expect(store.updateSettings({ agentSkillSharingEnabled: true }).agentSkillSharingEnabled).toBe(
true
)
expect(
store.updateSettings({ agentSkillSharingEnabled: 'yes' as never }).agentSkillSharingEnabled
).toBe(false)
expect(
store.updateSettings({ agentSkillSharingEnabled: 1 as never }).agentSkillSharingEnabled
).toBe(false)
})
it('normalizes bot-author overrides on load and every settings write', async () => {
writeDataFile({
settings: {
@@ -70,9 +70,6 @@ export function updateSettings(
if ('artifactSharingEnabled' in updates) {
sanitizedUpdates.artifactSharingEnabled = updates.artifactSharingEnabled === true
}
if ('agentSkillSharingEnabled' in updates) {
sanitizedUpdates.agentSkillSharingEnabled = updates.agentSkillSharingEnabled === true
}
if ('disabledTuiAgents' in updates) {
sanitizedUpdates.disabledTuiAgents = normalizeDisabledTuiAgents(updates.disabledTuiAgents)
}
@@ -109,11 +109,6 @@ export type { PtyProcessInfo, PtySpawnResult }
type PtyProbeOptions = { signal?: AbortSignal }
export type IPtyProvider = {
requestHostRpc?: (
method: string,
params: unknown,
options?: { signal?: AbortSignal; timeoutMs?: number }
) => Promise<unknown>
/** Fresh local spawns currently route to an in-process, non-persistent fallback. */
readonly routesFreshSpawnsToLocalProvider?: true
/** Re-probes a degraded durable host before main commits to fallback spawn semantics. */
+3 -4
View File
@@ -40,9 +40,6 @@ export class SshPtyProvider implements IPtyProvider {
private spawnExitRaces = new SshPtySpawnExitRaceTracker()
private readonly outputState: SshPtyProviderOutputState
requestHostRpc: NonNullable<IPtyProvider['requestHostRpc']> = (method, params, options) =>
this.mux.request(method, params as Record<string, unknown>, options)
constructor(
connectionId: string,
mux: SshChannelMultiplexer,
@@ -307,7 +304,9 @@ export class SshPtyProvider implements IPtyProvider {
return processes
}
hasPty = (id: string): boolean => this.livePtyIds.has(id)
hasPty(id: string): boolean {
return this.livePtyIds.has(id)
}
async getDefaultShell(): Promise<string> {
const result = await this.mux.request('pty.getDefaultShell')
@@ -1,225 +0,0 @@
import { randomUUID } from 'node:crypto'
import { mkdir, mkdtemp, readdir, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { AgentSkillShareRequest } from '../../shared/agent-skill-sharing-contract'
import { getDefaultSettings } from '../../shared/constants'
import type { DiscoveredSkill } from '../../shared/skills'
import { extractSkillBundleArchive } from '../skills/skill-bundle-extraction'
import { OrcaRuntimeService } from './orca-runtime'
const mocks = vi.hoisted(() => ({ userDataPath: '/tmp' }))
vi.mock('electron', () => ({
BrowserWindow: { fromId: vi.fn(() => null) },
webContents: { fromId: vi.fn(() => null) },
ipcMain: { on: vi.fn(), removeListener: vi.fn() },
app: { getPath: vi.fn(() => mocks.userDataPath), isPackaged: false }
}))
let testRoot = ''
async function createSkill(id: string, name: string): Promise<DiscoveredSkill> {
const directoryPath = join(testRoot, 'skills', id)
await mkdir(directoryPath, { recursive: true })
await writeFile(
join(directoryPath, 'SKILL.md'),
`---\nname: ${name}\ndescription: ${name} description\n---\n\n# ${name}\n`,
'utf8'
)
await writeFile(join(directoryPath, 'notes.txt'), name, 'utf8')
return {
id,
name,
description: `${name} description`,
providers: ['agent-skills'],
sourceKind: 'home',
sourceLabel: 'Shared',
rootPath: join(testRoot, 'skills'),
directoryPath,
skillFilePath: join(directoryPath, 'SKILL.md'),
installed: true,
updatedAt: null
}
}
function request(skillSelectors: string[]): AgentSkillShareRequest {
return {
skillSelectors,
bundleName: 'team-skills',
releaseNotes: 'Release notes'
}
}
async function operationDirectories(): Promise<string[]> {
return readdir(join(testRoot, 'agent-skill-share-operations')).catch(() => [])
}
function runtimeWithCloud(options: {
isEnabled: () => boolean
failPublish?: boolean
waitForAbort?: boolean
}) {
const manifests: string[][] = []
let notifyStarted: (() => void) | null = null
const started = new Promise<void>((resolve) => {
notifyStarted = resolve
})
const publishVersion = vi.fn(async (input) => {
const extractionRoot = join(testRoot, 'extracted')
await mkdir(extractionRoot, { recursive: true })
const extracted = await extractSkillBundleArchive({
archivePath: input.archivePath,
destinationDirectory: join(extractionRoot, randomUUID()),
expectedArchiveSha256: input.archiveSha256,
expectedPackageId: input.packageId
})
manifests.push(extracted.manifest.skills.map((skill) => skill.name))
if (options.failPublish) {
throw new Error('publish-failed')
}
if (options.waitForAbort) {
notifyStarted?.()
await new Promise<never>((_resolve, reject) => {
const rejectAbort = (): void => reject(new Error('upload-aborted'))
input.signal?.addEventListener('abort', rejectAbort, { once: true })
if (input.signal?.aborted) {
rejectAbort()
}
})
}
return {
status: 'ok' as const,
value: {
packageId: extracted.manifest.packageId,
versionId: extracted.manifest.versionId,
name: extracted.manifest.bundleName,
description: extracted.manifest.description,
packageDigest: extracted.manifest.bundleDigest,
archiveSha256: input.archiveSha256,
compressedBytes: input.compressedBytes,
createdAt: extracted.manifest.createdAt,
releaseNotes: input.releaseNotes,
manifest: extracted.manifest
}
}
})
const createShare = vi.fn(async () => ({
status: 'ok' as const,
value: { id: 'share-id', url: 'https://share.onorca.dev/skills/share/share-id' }
}))
const runtime = new OrcaRuntimeService({
getSettings: () => ({
...getDefaultSettings(testRoot),
agentSkillSharingEnabled: options.isEnabled()
})
} as never)
runtime.setSkillCloudService({ publishVersion, createShare } as never)
return { runtime, publishVersion, createShare, manifests, started }
}
beforeEach(async () => {
testRoot = await mkdtemp(join(tmpdir(), 'orca-agent-skill-share-'))
mocks.userDataPath = testRoot
})
afterEach(async () => {
await rm(testRoot, { recursive: true, force: true })
})
describe('agent skill sharing runtime', () => {
it('publishes only the selected discovered skills and cleans preparation files', async () => {
const alpha = await createSkill('alpha-id', 'alpha')
const beta = await createSkill('beta-id', 'beta')
const ignored = await createSkill('ignored-id', 'ignored')
const staleOperation = join(testRoot, 'agent-skill-share-operations', 'stale-operation')
await mkdir(staleOperation, { recursive: true })
await writeFile(join(staleOperation, 'package.tar.gz'), 'crash residue')
const { runtime, manifests } = runtimeWithCloud({ isEnabled: () => true })
const result = await runtime.publishDiscoveredSkillsFromAgent(request(['alpha-id', 'beta']), [
alpha,
beta,
ignored
])
expect(result).toMatchObject({
status: 'ok',
value: {
selectedSkills: [
{ id: 'alpha-id', name: 'alpha' },
{ id: 'beta-id', name: 'beta' }
]
}
})
expect(manifests).toEqual([['alpha', 'beta']])
expect(await operationDirectories()).toEqual([])
})
it('denies before reading a discovered directory and blocks later publishes when disabled', async () => {
let enabled = false
const { runtime, publishVersion } = runtimeWithCloud({ isEnabled: () => enabled })
const missing = {
...(await createSkill('alpha-id', 'alpha')),
directoryPath: join(testRoot, 'does-not-exist')
}
await expect(
runtime.publishDiscoveredSkillsFromAgent(request(['alpha-id']), [missing])
).rejects.toMatchObject({ code: 'agent_skill_sharing_disabled' })
expect(publishVersion).not.toHaveBeenCalled()
enabled = true
const alpha = await createSkill('alpha-two', 'alpha-two')
await expect(
runtime.publishDiscoveredSkillsFromAgent(request(['alpha-two']), [alpha])
).resolves.toMatchObject({ status: 'ok' })
enabled = false
await expect(
runtime.publishDiscoveredSkillsFromAgent(request(['alpha-two']), [alpha])
).rejects.toMatchObject({ code: 'agent_skill_sharing_disabled' })
})
it('cleans preparation files after cloud failure', async () => {
const alpha = await createSkill('alpha-id', 'alpha')
const { runtime } = runtimeWithCloud({ isEnabled: () => true, failPublish: true })
await expect(
runtime.publishDiscoveredSkillsFromAgent(request(['alpha-id']), [alpha])
).rejects.toThrow('publish-failed')
expect(await operationDirectories()).toEqual([])
})
it('returns an actionable error for package-incompatible skill metadata', async () => {
const invalid = await createSkill('invalid-id', 'invalid_name')
const { runtime, publishVersion } = runtimeWithCloud({ isEnabled: () => true })
await expect(
runtime.publishDiscoveredSkillsFromAgent(request(['invalid-id']), [invalid])
).rejects.toMatchObject({ code: 'agent_skill_not_shareable' })
expect(publishVersion).not.toHaveBeenCalled()
expect(await operationDirectories()).toEqual([])
})
it('cancels an upload and cleans preparation files', async () => {
const alpha = await createSkill('alpha-id', 'alpha')
const controller = new AbortController()
const { runtime, started } = runtimeWithCloud({
isEnabled: () => true,
waitForAbort: true
})
const publishing = runtime.publishDiscoveredSkillsFromAgent(
request(['alpha-id']),
[alpha],
controller.signal
)
await started
await expect(
runtime.publishDiscoveredSkillsFromAgent(request(['alpha-id']), [alpha])
).rejects.toMatchObject({ code: 'agent_skill_sharing_busy' })
controller.abort()
await expect(publishing).rejects.toThrow('upload-aborted')
expect(await operationDirectories()).toEqual([])
})
})
@@ -1,32 +0,0 @@
import { describe, expect, it, vi } from 'vitest'
import { OrcaRuntimeService } from './orca-runtime'
vi.mock('electron', () => ({
BrowserWindow: { fromId: vi.fn(() => null) },
webContents: { fromId: vi.fn(() => null) },
ipcMain: { on: vi.fn(), removeListener: vi.fn() },
app: { getPath: vi.fn(() => '/tmp'), isPackaged: false }
}))
describe('managed skill startup recovery', () => {
it('waits for startup recovery before reading local managed installs', async () => {
let finishRecovery!: () => void
const recovery = new Promise<void>((resolve) => {
finishRecovery = resolve
})
const runtime = new OrcaRuntimeService(null, undefined, {
skillTransactionRecovery: recovery
})
const listing = runtime.listManagedSkillInstalls()
let settled = false
void listing.finally(() => {
settled = true
})
await Promise.resolve()
expect(settled).toBe(false)
finishRecovery()
await expect(listing).resolves.toEqual([])
})
})
+2 -796
View File
@@ -13,10 +13,6 @@ import {
import { extractOscTitleScanTail } from '../../shared/osc-title-scan-tail'
import { planWorktreeSortOrderUpdates } from '../../shared/worktree/sort-order-update'
import { isArtifactSharingEnabled } from '../../shared/artifact-sharing-gate'
import {
assertAgentSkillSharingAllowed,
isAgentSkillSharingEnabled
} from '../../shared/agent-skill-sharing-gate'
import { sortDirEntries } from '../../shared/file-name-sort'
import { isServerDriveListRequest, listWindowsDrives } from './windows-drive-listing'
import { extractLastOsc7Uri, extractOscScanTail } from '../daemon/osc7-uri-extraction'
@@ -118,7 +114,7 @@ import { getGitCloneFailureMessage } from '../../shared/git-clone-failure-messag
import { GIT_FETCH_SKIP_AUTO_MAINTENANCE_CONFIG_ARGS } from '../../shared/git-fetch-auto-maintenance'
import { createHash, randomUUID } from 'node:crypto'
import { homedir } from 'node:os'
import { dirname, isAbsolute, join, resolve } from 'node:path'
import { isAbsolute, join, resolve } from 'node:path'
import { mkdir, readFile, readdir, rm, stat } from 'node:fs/promises'
import { resolveWorktreeCreateBase } from '../worktree-create-base'
import { resolveWorktreeAddBaseRef } from '../../shared/worktree/base-ref'
@@ -151,73 +147,6 @@ import type {
ArtifactWriteRequest
} from '../../shared/artifacts'
import type { ArtifactCloudService } from '../artifacts/artifact-cloud-service'
import type {
SkillCloudDownloadGrant,
SkillCloudOperation,
SkillCloudOptions,
SkillCloudPackageDetails,
SkillCloudPublishRequest,
SkillCloudPublishResult,
SkillCloudVersion
} from '../../shared/skill-cloud-contract'
import type { SkillCloudService } from '../skills/skill-cloud-service'
import {
AGENT_SKILL_NOT_SHAREABLE_CODE,
AGENT_SKILL_SHARING_BUSY_CODE,
AgentSkillSharingError,
type AgentSkillShareOperation,
type AgentSkillShareRequest
} from '../../shared/agent-skill-sharing-contract'
import type { DiscoveredSkill } from '../../shared/skills'
import { selectDiscoveredSkills } from '../skills/agent-skill-selection'
import { SkillSharePreparationService } from '../skills/skill-share-preparation-service'
import type {
SkillInstallPreview,
SkillInstallPreviewRequest,
SkillInstallRequest,
SkillInstallResult,
ManagedSkillInstall,
SkillRemoveRequest
} from '../../shared/skill-install-contract'
import type {
SkillBundleInstallPreview,
SkillBundleInstallPreviewRequest,
SkillBundleInstallProgress,
SkillBundleInstallRequest,
SkillBundleInstallResult
} from '../../shared/skill-bundle-install-contract'
import { SkillBundleInstallPreviewSchema } from '../../shared/skill-bundle-install-contract'
import { executeSkillInstallRequest } from '../skills/skill-install-request-service'
import { executeSkillBundleInstallRequest } from '../skills/skill-bundle-install-request-service'
import type { SkillInstallDestinationAuthority } from '../skills/skill-install-destinations'
import {
previewSharedSkillBundleInstall,
previewSharedSkillInstall,
removeSharedSkillInstall
} from '../skills/skill-install-management-service'
import { listManagedSkillInstalls } from '../skills/skill-install-provenance'
import { getWslHome, toLinuxPath } from '../wsl'
import { WslSkillInstallFilesystem } from '../skills/skill-wsl-install-filesystem'
import { nativeSkillInstallFilesystem } from '../skills/skill-install-filesystem'
import type { SkillProviderRootOverrides } from '../skills/skill-provider-destinations'
import {
resolveEnvironmentSkillProviderRoots,
resolveWslGrokSkillProviderRoot,
withClaudeSkillProviderRoot
} from '../skills/skill-provider-runtime-roots'
import type {
SkillUploadBeginRequest,
SkillUploadChunkRequest
} from '../../shared/skill-upload-session-contract'
import { SkillUploadSessionService } from '../skills/skill-upload-session-service'
import type { SkillSshWorkspaceAuthority } from '../../shared/skill-ssh-relay-contract'
import { installSkillBundleOnSshHost } from '../skills/skill-bundle-ssh-relay-service'
import {
installSkillOnSshHost,
listSkillInstallsOnSshHost,
previewSkillInstallOnSshHost,
removeSkillInstallOnSshHost
} from '../skills/skill-ssh-relay-service'
import { ORCHESTRATION_MESSAGE_WAIT_DEFAULT_TIMEOUT_MS } from '../../shared/orchestration-message-wait-timeout'
import { shouldForwardHeadlessTerminalQueryReply } from './headless-terminal-query-reply-policy'
import type { TerminalRevealIdentity } from '../../shared/terminal-reveal-identity'
@@ -1313,7 +1242,6 @@ type RuntimeStore = {
minimaxUsageModels?: GlobalSettings['minimaxUsageModels']
prBotAuthorOverrides?: GlobalSettings['prBotAuthorOverrides']
artifactSharingEnabled?: GlobalSettings['artifactSharingEnabled']
agentSkillSharingEnabled?: GlobalSettings['agentSkillSharingEnabled']
terminalQuickCommands?: GlobalSettings['terminalQuickCommands']
gitlabProjects?: GlobalSettings['gitlabProjects']
mobileAutoRestoreFitMs?: number | null
@@ -3497,12 +3425,6 @@ export class OrcaRuntimeService {
private commitMessageAgentEnv: CommitMessageAgentEnvironmentResolvers | null = null
private automationService: AutomationService | null = null
private artifactService: ArtifactCloudService | null = null
private skillCloudService: SkillCloudService | null = null
private agentSkillShareInProgress = false
private skillUploadSessions: SkillUploadSessionService | null = null
private readonly skillTransactionRecovery: Promise<unknown>
private readonly skillInstallOperations = new Map<string, AbortController>()
private readonly skillInstallProgress = new Map<string, SkillBundleInstallProgress>()
private readonly claudeAgentTeams = new ClaudeAgentTeamsService()
private mobileDictation: {
id: string
@@ -3554,7 +3476,6 @@ export class OrcaRuntimeService {
getDesktopWindowStatus?: () => RuntimeDesktopWindowStatus
agentSessionClaimSigner?: AgentSessionClaimSigner
orchestrationEnvironmentTransport?: OrchestrationEnvironmentTransport
skillTransactionRecovery?: Promise<unknown>
}
) {
this.store = store
@@ -3567,7 +3488,6 @@ export class OrcaRuntimeService {
this.store?.setMobileClientTabSelections?.(state)
})
this.orchestrationEnvironmentTransport = deps?.orchestrationEnvironmentTransport ?? null
this.skillTransactionRecovery = deps?.skillTransactionRecovery ?? Promise.resolve()
if (stats) {
this.stats = stats
this.agentDetector = new AgentDetector(stats)
@@ -3714,7 +3634,6 @@ export class OrcaRuntimeService {
// Read-only on purpose: clients preflight the publish capability here, but SettingsUpdate
// still omits the key so no RPC caller can grant it to itself.
| 'artifactSharingEnabled'
| 'agentSkillSharingEnabled'
> {
if (!this.store?.getSettings) {
throw new Error('runtime_unavailable')
@@ -3739,8 +3658,7 @@ export class OrcaRuntimeService {
minimaxGroupId: settings.minimaxGroupId ?? '',
minimaxUsageModels: settings.minimaxUsageModels ?? 'general',
prBotAuthorOverrides: settings.prBotAuthorOverrides ?? [],
artifactSharingEnabled: isArtifactSharingEnabled(settings),
agentSkillSharingEnabled: isAgentSkillSharingEnabled(settings)
artifactSharingEnabled: isArtifactSharingEnabled(settings)
}
}
@@ -4796,704 +4714,6 @@ export class OrcaRuntimeService {
this.artifactService = service
}
setSkillCloudService(service: SkillCloudService): void {
this.skillCloudService = service
}
assertAgentSkillSharingAllowed(): void {
assertAgentSkillSharingAllowed(() => isAgentSkillSharingEnabled(this.store?.getSettings()))
}
async publishDiscoveredSkillsFromAgent(
request: AgentSkillShareRequest,
discoveredSkills: readonly DiscoveredSkill[],
signal?: AbortSignal
): Promise<AgentSkillShareOperation> {
this.assertAgentSkillSharingAllowed()
if (this.agentSkillShareInProgress) {
throw new AgentSkillSharingError(
AGENT_SKILL_SHARING_BUSY_CODE,
'Another agent skill bundle is being published. Wait for it to finish and try again.'
)
}
this.agentSkillShareInProgress = true
try {
return await this.executeAgentSkillShare(request, discoveredSkills, signal)
} finally {
this.agentSkillShareInProgress = false
}
}
private async executeAgentSkillShare(
request: AgentSkillShareRequest,
discoveredSkills: readonly DiscoveredSkill[],
signal?: AbortSignal
): Promise<AgentSkillShareOperation> {
const selectedSkills = selectDiscoveredSkills(discoveredSkills, request.skillSelectors)
const operationRoot = join(app.getPath('userData'), 'agent-skill-share-operations')
const cloud = this.requireSkillCloudService()
const preparations = new SkillSharePreparationService(operationRoot, {
publishVersion: (input) => cloud.publishVersion(input),
createShare: (packageId, input) => cloud.createShare(packageId, input)
})
let preparationId: string | null = null
const cancel = (): void => {
if (preparationId) {
preparations.cancel(preparationId)
}
}
signal?.addEventListener('abort', cancel, { once: true })
try {
if (signal?.aborted) {
throw signal.reason ?? new Error('skill-share-cancelled')
}
const preview = await preparations
.prepare({
sources: selectedSkills.map((skill) => ({
id: skill.name,
sourceDirectory: skill.directoryPath
})),
bundleName: request.bundleName,
description:
selectedSkills.length === 1
? (selectedSkills[0].description ?? '')
: `${selectedSkills.length} shared skills`
})
.catch((error: unknown) => {
if (
error instanceof Error &&
['skill-package-skill-name-required', 'skill-package-skill-name-invalid'].includes(
error.message
)
) {
throw new AgentSkillSharingError(
AGENT_SKILL_NOT_SHAREABLE_CODE,
'A selected skill cannot be shared. Its SKILL.md must declare a lowercase name containing only letters, numbers, and hyphens.'
)
}
throw error
})
preparationId = preview.preparationId
if (signal?.aborted) {
throw signal.reason ?? new Error('skill-share-cancelled')
}
this.assertAgentSkillSharingAllowed()
const published = await preparations.publish({
preparationId,
releaseNotes: request.releaseNotes
})
return published.status === 'ok'
? {
status: 'ok',
value: {
...published.value,
selectedSkills: selectedSkills.map(({ id, name, description }) => ({
id,
name,
description
}))
}
}
: published
} finally {
signal?.removeEventListener('abort', cancel)
await preparations.dispose()
}
}
publishSkillPackage(
request: SkillCloudPublishRequest
): Promise<SkillCloudOperation<SkillCloudPublishResult>> {
return this.requireSkillCloudService().publish(request)
}
publishSkillPackageVersion(
request: SkillCloudPublishRequest
): Promise<SkillCloudOperation<SkillCloudVersion>> {
return this.requireSkillCloudService().publishVersion(request)
}
createSkillPackageShare(
packageId: string,
request: SkillCloudOptions & {
pinnedVersionId?: string
idempotencyKey?: string
}
) {
return this.requireSkillCloudService().createShare(packageId, request)
}
resolveSkillShare(
shareId: string,
options: SkillCloudOptions
): Promise<SkillCloudOperation<{ id: string; version: SkillCloudVersion }>> {
return this.requireSkillCloudService().resolveShare(shareId, options)
}
createSkillDownloadGrant(
shareId: string,
options: SkillCloudOptions & {
versionId?: string
installTarget?: 'local' | 'remote'
}
): Promise<SkillCloudOperation<SkillCloudDownloadGrant>> {
return this.requireSkillCloudService().createDownloadGrant(shareId, options)
}
createSkillPackageVersionDownloadGrant(
packageId: string,
versionId: string,
options: SkillCloudOptions & { installTarget?: 'local' | 'remote' }
): Promise<SkillCloudOperation<SkillCloudDownloadGrant>> {
return this.requireSkillCloudService().createPackageVersionDownloadGrant(
packageId,
versionId,
options
)
}
getSkillPackage(
packageId: string,
options: SkillCloudOptions
): Promise<SkillCloudOperation<SkillCloudPackageDetails>> {
return this.requireSkillCloudService().getPackage(packageId, options)
}
listOwnedSkillShares(options: SkillCloudOptions) {
return this.requireSkillCloudService().listOwnedShares(options)
}
revokeSkillShare(
shareId: string,
options: SkillCloudOptions
): Promise<SkillCloudOperation<void>> {
return this.requireSkillCloudService().revokeShare(shareId, options)
}
deleteSkillPackageVersion(
packageId: string,
versionId: string,
options: SkillCloudOptions
): Promise<SkillCloudOperation<void>> {
return this.requireSkillCloudService().deleteVersion(packageId, versionId, options)
}
deleteSkillPackage(
packageId: string,
options: SkillCloudOptions
): Promise<SkillCloudOperation<void>> {
return this.requireSkillCloudService().deletePackage(packageId, options)
}
async installSharedSkillRequest(
request: SkillInstallRequest,
signal?: AbortSignal
): Promise<SkillInstallResult> {
if (this.skillInstallOperations.has(request.operationId)) {
throw new Error('skill-install-operation-in-progress')
}
const controller = new AbortController()
const abort = (): void => controller.abort()
if (signal?.aborted) {
abort()
} else {
signal?.addEventListener('abort', abort, { once: true })
}
this.skillInstallOperations.set(request.operationId, controller)
try {
return await this.executeSharedSkillInstall(request, controller.signal)
} finally {
signal?.removeEventListener('abort', abort)
if (this.skillInstallOperations.get(request.operationId) === controller) {
this.skillInstallOperations.delete(request.operationId)
}
}
}
async installSharedSkillBundleRequest(
request: SkillBundleInstallRequest,
signal?: AbortSignal,
onProgress?: (progress: SkillBundleInstallProgress) => void
): Promise<SkillBundleInstallResult> {
if (this.skillInstallOperations.has(request.operationId)) {
throw new Error('skill-install-operation-in-progress')
}
const controller = new AbortController()
const abort = (): void => controller.abort()
if (signal?.aborted) {
abort()
} else {
signal?.addEventListener('abort', abort, { once: true })
}
this.skillInstallOperations.set(request.operationId, controller)
const reportProgress = (progress: SkillBundleInstallProgress): void => {
this.skillInstallProgress.set(request.operationId, progress)
try {
onProgress?.(progress)
} catch {
// Why: renderer teardown must not change the host-owned install outcome.
}
}
try {
const runtimeId = this.getStatus().runtimeId
const sshTarget = await this.resolveSkillSshTarget(request.destination)
if (sshTarget) {
return installSkillBundleOnSshHost({
provider: sshTarget.provider,
userDataPath: app.getPath('userData'),
request: {
...request,
destination:
request.destination.scope === 'global'
? { scope: 'global', executionTarget: { kind: 'host' } }
: request.destination
},
workspace: sshTarget.workspace,
requireHttps: app.isPackaged,
signal: controller.signal,
onProgress: reportProgress
})
}
await this.skillTransactionRecovery
const allowedDownloadOrigins = ['https://storage.googleapis.com']
if (!app.isPackaged && process.env.ORCA_SKILL_PACKAGE_DOWNLOAD_ORIGINS) {
allowedDownloadOrigins.push(
...process.env.ORCA_SKILL_PACKAGE_DOWNLOAD_ORIGINS.split(',')
.map((origin) => origin.trim())
.filter(Boolean)
)
}
return await executeSkillBundleInstallRequest(request, {
authority: this.skillInstallDestinationAuthority(runtimeId),
stateDirectory: app.getPath('userData'),
allowedDownloadOrigins: [...new Set(allowedDownloadOrigins)],
requireHttps: app.isPackaged,
resolveStagedUpload: (uploadId, identity) =>
this.requireSkillUploadSessions().take(uploadId, identity),
detectProviders: detectInstalledAgentsWithShellPathHydration,
resolveProviderRootOverrides: (destination) =>
this.resolveSkillProviderRootOverrides(destination),
signal: controller.signal,
onProgress: reportProgress
})
} finally {
signal?.removeEventListener('abort', abort)
if (this.skillInstallOperations.get(request.operationId) === controller) {
this.skillInstallOperations.delete(request.operationId)
}
this.skillInstallProgress.delete(request.operationId)
}
}
getSharedSkillInstallProgress(operationId: string): SkillBundleInstallProgress | null {
return this.skillInstallProgress.get(operationId) ?? null
}
cancelSharedSkillInstall(operationId: string): boolean {
const operation = this.skillInstallOperations.get(operationId)
operation?.abort()
return Boolean(operation)
}
private async executeSharedSkillInstall(
request: SkillInstallRequest,
signal: AbortSignal
): Promise<SkillInstallResult> {
const runtimeId = this.getStatus().runtimeId
const sshTarget = await this.resolveSkillSshTarget(request.destination)
if (sshTarget) {
return installSkillOnSshHost({
provider: sshTarget.provider,
userDataPath: app.getPath('userData'),
request: {
...request,
destination:
request.destination.scope === 'global'
? { scope: 'global', executionTarget: { kind: 'host' } }
: request.destination
},
workspace: sshTarget.workspace,
requireHttps: app.isPackaged,
signal
})
}
await this.skillTransactionRecovery
const allowedDownloadOrigins = ['https://storage.googleapis.com']
if (!app.isPackaged && process.env.ORCA_SKILL_PACKAGE_DOWNLOAD_ORIGINS) {
allowedDownloadOrigins.push(
...process.env.ORCA_SKILL_PACKAGE_DOWNLOAD_ORIGINS.split(',')
.map((origin) => origin.trim())
.filter(Boolean)
)
}
return executeSkillInstallRequest(request, {
authority: this.skillInstallDestinationAuthority(runtimeId),
stateDirectory: app.getPath('userData'),
allowedDownloadOrigins: [...new Set(allowedDownloadOrigins)],
requireHttps: app.isPackaged,
resolveStagedUpload: (uploadId, identity) =>
this.requireSkillUploadSessions().take(uploadId, identity),
detectProviders: detectInstalledAgentsWithShellPathHydration,
resolveProviderRootOverrides: (destination) =>
this.resolveSkillProviderRootOverrides(destination),
signal
})
}
async previewSharedSkillInstallRequest(
request: SkillInstallPreviewRequest
): Promise<SkillInstallPreview> {
const runtimeId = this.getStatus().runtimeId
const sshTarget = await this.resolveSkillSshTarget(request.destination)
if (sshTarget) {
return previewSkillInstallOnSshHost({
provider: sshTarget.provider,
request: {
...request,
destination:
request.destination.scope === 'global'
? { scope: 'global', executionTarget: { kind: 'host' } }
: request.destination
},
workspace: sshTarget.workspace
})
}
await this.skillTransactionRecovery
return previewSharedSkillInstall(request, {
authority: this.skillInstallDestinationAuthority(runtimeId),
stateDirectory: app.getPath('userData'),
detectProviders: detectInstalledAgentsWithShellPathHydration,
resolveProviderRootOverrides: (destination) =>
this.resolveSkillProviderRootOverrides(destination)
})
}
async previewSharedSkillBundleInstallRequest(
request: SkillBundleInstallPreviewRequest
): Promise<SkillBundleInstallPreview> {
if (await this.resolveSkillSshTarget(request.destination)) {
const previews: SkillInstallPreview[] = []
for (let offset = 0; offset < request.selectedSkills.length; offset += 8) {
const batch = request.selectedSkills.slice(offset, offset + 8)
previews.push(
...(await Promise.all(
batch.map((skill) =>
this.previewSharedSkillInstallRequest({
package: {
packageId: request.package.packageId,
versionId: request.package.versionId,
packageDigest: skill.digest,
archiveSha256: request.package.archiveSha256,
compressedBytes: request.package.compressedBytes
},
name: skill.name,
destination: request.destination
})
)
))
)
}
return SkillBundleInstallPreviewSchema.parse({
packageId: request.package.packageId,
versionId: request.package.versionId,
bundleDigest: request.package.bundleDigest,
destinationIdentity: previews[0]?.destinationIdentity ?? '',
skills: request.selectedSkills.map((skill, index) => ({
...skill,
currentState: previews[index].currentState
}))
})
}
await this.skillTransactionRecovery
const runtimeId = this.getStatus().runtimeId
return previewSharedSkillBundleInstall(request, {
authority: this.skillInstallDestinationAuthority(runtimeId),
stateDirectory: app.getPath('userData'),
detectProviders: detectInstalledAgentsWithShellPathHydration,
resolveProviderRootOverrides: (destination) =>
this.resolveSkillProviderRootOverrides(destination)
})
}
async removeSharedSkillInstallRequest(request: SkillRemoveRequest): Promise<SkillInstallResult> {
const runtimeId = this.getStatus().runtimeId
const sshTarget = await this.resolveSkillSshTarget(request.destination)
if (sshTarget) {
return removeSkillInstallOnSshHost({
provider: sshTarget.provider,
request: {
...request,
destination:
request.destination.scope === 'global'
? { scope: 'global', executionTarget: { kind: 'host' } }
: request.destination
},
workspace: sshTarget.workspace
})
}
await this.skillTransactionRecovery
return removeSharedSkillInstall(request, {
authority: this.skillInstallDestinationAuthority(runtimeId),
stateDirectory: app.getPath('userData'),
detectProviders: detectInstalledAgentsWithShellPathHydration,
resolveProviderRootOverrides: (destination) =>
this.resolveSkillProviderRootOverrides(destination)
})
}
async listManagedSkillInstalls(connectionId?: string): Promise<ManagedSkillInstall[]> {
if (connectionId) {
const provider = this.requireSkillSshProvider(connectionId)
return listSkillInstallsOnSshHost({
provider,
connectionId,
workspaces: await this.listSkillSshWorkspaces(connectionId)
})
}
await this.skillTransactionRecovery
const runtimeId = this.getStatus().runtimeId
const [installs, worktrees] = await Promise.all([
listManagedSkillInstalls(join(app.getPath('userData'), 'skill-installs'), {
observeReceipt: async (receipt) => {
if (!receipt.wslDistro) {
return nativeSkillInstallFilesystem.observeSkill(
receipt.canonicalPath,
receipt.fileModes
)
}
const filesystem = new WslSkillInstallFilesystem(receipt.wslDistro, [
dirname(receipt.canonicalPath)
])
return filesystem.observeSkill(receipt.canonicalPath, receipt.fileModes)
}
}),
this.listResolvedWorktrees()
])
const folderWorkspaces = this.listFolderWorkspaces()
return installs.flatMap((install): ManagedSkillInstall[] => {
if (install.scope === 'global') {
const wslPrefix = `global:${runtimeId}:wsl:`
return [
{
...install,
destination: install.destinationIdentity.startsWith(wslPrefix)
? {
scope: 'global',
executionTarget: {
kind: 'wsl',
distro: install.destinationIdentity.slice(wslPrefix.length)
}
}
: { scope: 'global' }
}
]
}
const worktree = worktrees.find(
(candidate) => install.destinationIdentity === `workspace:${runtimeId}:${candidate.id}`
)
if (worktree) {
return [{ ...install, destination: { scope: 'workspace', worktreeId: worktree.id } }]
}
const folder = folderWorkspaces.find(
(candidate) => install.destinationIdentity === `workspace:${runtimeId}:${candidate.id}`
)
return folder
? [{ ...install, destination: { scope: 'workspace', folderWorkspaceId: folder.id } }]
: []
})
}
async skillInstallDestinationUsesSsh(
destination: SkillInstallRequest['destination']
): Promise<boolean> {
return Boolean(await this.resolveSkillSshTarget(destination))
}
async resolveSkillDiscoveryProviderRoots(target: {
kind: 'native-host' | 'wsl'
distro?: string
}): Promise<SkillProviderRootOverrides> {
const roots = await this.resolveSkillProviderRootOverrides({
scope: 'global',
homeDirectory: homedir(),
...(target.kind === 'wsl' && target.distro ? { wslDistro: target.distro } : {})
})
if (target.kind !== 'wsl') {
return roots
}
return Object.fromEntries(
Object.entries(roots).map(([provider, root]) => [provider, toLinuxPath(root)])
)
}
private async resolveSkillProviderRootOverrides(destination: {
scope: 'global' | 'workspace'
homeDirectory: string
workspaceDirectory?: string
wslDistro?: string
}): Promise<SkillProviderRootOverrides> {
if (destination.scope !== 'global') {
return {}
}
const wslGrokRoot = destination.wslDistro
? await resolveWslGrokSkillProviderRoot(destination.wslDistro)
: null
const roots: SkillProviderRootOverrides = destination.wslDistro
? wslGrokRoot
? { grok: wslGrokRoot }
: {}
: resolveEnvironmentSkillProviderRoots()
const claudeConfigDirectory = this.accountServices?.claudeAccounts.getRuntimeConfigDir(
destination.wslDistro
? { runtime: 'wsl', wslDistro: destination.wslDistro }
: { runtime: 'host' }
)
return withClaudeSkillProviderRoot(roots, claudeConfigDirectory)
}
private skillInstallDestinationAuthority(runtimeId: string): SkillInstallDestinationAuthority {
return {
environmentId: runtimeId,
homeDirectory: homedir(),
resolveWorktree: async (id) => {
const repo = this.listRepos().find(
(candidate) => candidate.id === getRepoIdFromWorktreeId(id)
)
if (repo?.connectionId) {
throw new Error('skill-install-ssh-dispatch-required')
}
const projectRuntime = this.resolveProjectRuntimeForWorktree(id)
const worktree = await this.showManagedWorktree(`id:${id}`)
if (worktree.id !== id) {
return null
}
return {
id,
path: worktree.path,
...(projectRuntime?.status === 'resolved' && projectRuntime.runtime.kind === 'wsl'
? { wslDistro: projectRuntime.runtime.distro }
: {})
}
},
resolveFolderWorkspace: async (id) => {
const workspace = this.listFolderWorkspaces().find((candidate) => candidate.id === id)
if (!workspace || workspace.connectionId) {
return null
}
return {
id,
path: workspace.folderPath,
...(parseWslUncPath(workspace.folderPath)?.distro
? { wslDistro: parseWslUncPath(workspace.folderPath)!.distro }
: {})
}
},
resolveWsl: async (distro) => {
if (process.platform !== 'win32') {
return null
}
const homeDirectory = getWslHome(distro)
return homeDirectory ? { homeDirectory } : null
}
}
}
private async resolveSkillSshTarget(destination: SkillInstallRequest['destination']): Promise<{
provider: IPtyProvider
workspace?: SkillSshWorkspaceAuthority
} | null> {
if (destination.scope === 'global') {
return destination.executionTarget?.kind === 'ssh'
? { provider: this.requireSkillSshProvider(destination.executionTarget.connectionId) }
: null
}
if (destination.worktreeId) {
const repo = this.listRepos().find(
(candidate) => candidate.id === getRepoIdFromWorktreeId(destination.worktreeId!)
)
if (!repo?.connectionId) {
return null
}
const worktree = await this.showManagedWorktree(`id:${destination.worktreeId}`)
if (worktree.id !== destination.worktreeId) {
throw new Error('skill-install-workspace-not-found')
}
return {
provider: this.requireSkillSshProvider(repo.connectionId),
workspace: { kind: 'worktree', id: worktree.id, path: worktree.path }
}
}
const folder = this.listFolderWorkspaces().find(
(candidate) => candidate.id === destination.folderWorkspaceId
)
if (!folder?.connectionId) {
return null
}
return {
provider: this.requireSkillSshProvider(folder.connectionId),
workspace: { kind: 'folder', id: folder.id, path: folder.folderPath }
}
}
private requireSkillSshProvider(connectionId: string): IPtyProvider {
const provider = this.getSshProviderFn?.(connectionId)
if (!provider?.requestHostRpc) {
throw new Error('skill-install-ssh-relay-unavailable')
}
return provider
}
private async listSkillSshWorkspaces(
connectionId: string
): Promise<SkillSshWorkspaceAuthority[]> {
const repos = new Map(
this.listRepos()
.filter((repo) => repo.connectionId === connectionId)
.map((repo) => [repo.id, repo])
)
const worktrees = (await this.listResolvedWorktrees())
.filter((worktree) => repos.has(getRepoIdFromWorktreeId(worktree.id)))
.map(
(worktree): SkillSshWorkspaceAuthority => ({
kind: 'worktree',
id: worktree.id,
path: worktree.path
})
)
const folders = this.listFolderWorkspaces()
.filter((folder) => folder.connectionId === connectionId)
.map(
(folder): SkillSshWorkspaceAuthority => ({
kind: 'folder',
id: folder.id,
path: folder.folderPath
})
)
return [...worktrees, ...folders]
}
beginSkillUpload(request: SkillUploadBeginRequest): Promise<{
uploadId: string
chunkBytes: number
acknowledgedOffset: number
}> {
return this.requireSkillUploadSessions().begin(request)
}
appendSkillUploadChunk(
request: SkillUploadChunkRequest
): Promise<{ acknowledgedOffset: number }> {
return this.requireSkillUploadSessions().append(request)
}
commitSkillUpload(uploadId: string): Promise<{ uploadId: string }> {
return this.requireSkillUploadSessions().commit(uploadId)
}
cancelSkillUpload(uploadId: string): Promise<void> {
return this.requireSkillUploadSessions().cancel(uploadId)
}
listArtifacts(options: ArtifactListOptions): Promise<ArtifactCloudOperation<ArtifactListPage>> {
return this.requireArtifactService().list(options)
}
@@ -5535,20 +4755,6 @@ export class OrcaRuntimeService {
return this.artifactService
}
private requireSkillCloudService(): SkillCloudService {
if (!this.skillCloudService) {
throw new Error('Skill Cloud service is unavailable.')
}
return this.skillCloudService
}
private requireSkillUploadSessions(): SkillUploadSessionService {
this.skillUploadSessions ??= new SkillUploadSessionService(
join(app.getPath('userData'), 'skill-installs', 'remote-uploads')
)
return this.skillUploadSessions
}
getRuntimeId(): string {
return this.runtimeId
}
-21
View File
@@ -14,27 +14,6 @@ class LineageError extends Error {
}
describe('mapRuntimeError', () => {
it('preserves the stable skill failure category and retryability across RPC', () => {
expect(
mapRuntimeError(
'req_1',
{ runtimeId: 'runtime-1' },
new Error('skill-download-transport-failed')
)
).toMatchObject({
ok: false,
error: {
code: 'skill_install_failure',
message: 'skill-download-transport-failed',
data: {
category: 'transport',
code: 'skill-download-transport-failed',
retryable: true
}
}
})
})
it.each(['terminal_tab_close_timeout', 'terminal_tab_not_found', 'terminal_tab_pinned'])(
'preserves the durable terminal tab close failure %s',
(code) => {
+1 -30
View File
@@ -8,18 +8,6 @@ import { COMPUTER_ERROR_CODES } from '../../../shared/runtime-types'
import { LINEAR_ERROR_CODES } from '../../../shared/linear/agent-access'
import { AGENT_SESSION_RPC_ERROR_CODES } from '../../../shared/agent-session-host-authority'
import { ARTIFACT_SHARING_DISABLED_CODE } from '../../../shared/artifact-sharing-gate'
import { AGENT_SKILL_SHARING_DISABLED_CODE } from '../../../shared/agent-skill-sharing-gate'
import {
AGENT_SKILL_NOT_SHAREABLE_CODE,
AGENT_SKILL_SELECTOR_AMBIGUOUS_CODE,
AGENT_SKILL_SELECTOR_NOT_FOUND_CODE,
AGENT_SKILL_SHARING_BUSY_CODE,
AGENT_SKILL_SHARING_UNSUPPORTED_ENVIRONMENT_CODE
} from '../../../shared/agent-skill-sharing-contract'
import {
SKILL_INSTALL_RPC_ERROR_CODE,
classifySkillInstallFailureCode
} from '../../../shared/skill-install-failure'
import { GIT_DIFF_TOO_LARGE_CODE } from '../../../shared/git-diff-transport-budget'
export function successResponse(id: string, meta: RpcEnvelopeMeta, result: unknown): RpcSuccess {
@@ -112,14 +100,7 @@ const STRUCTURED_RUNTIME_PASSTHROUGH_CODES: ReadonlySet<string> = new Set([
'waiter_exists',
'invalid_argument',
GIT_DIFF_TOO_LARGE_CODE,
ARTIFACT_SHARING_DISABLED_CODE,
AGENT_SKILL_SHARING_DISABLED_CODE,
AGENT_SKILL_NOT_SHAREABLE_CODE,
AGENT_SKILL_SELECTOR_AMBIGUOUS_CODE,
AGENT_SKILL_SELECTOR_NOT_FOUND_CODE,
AGENT_SKILL_SHARING_BUSY_CODE,
AGENT_SKILL_SHARING_UNSUPPORTED_ENVIRONMENT_CODE,
SKILL_INSTALL_RPC_ERROR_CODE
ARTIFACT_SHARING_DISABLED_CODE
])
export function mapRuntimeError(id: string, meta: RpcEnvelopeMeta, error: unknown): RpcFailure {
@@ -178,16 +159,6 @@ export function mapRuntimeError(id: string, meta: RpcEnvelopeMeta, error: unknow
if (RUNTIME_PASSTHROUGH_CODES.has(message)) {
return errorResponse(id, meta, message, message)
}
const skillInstallFailure = classifySkillInstallFailureCode(message)
if (skillInstallFailure) {
return errorResponse(
id,
meta,
SKILL_INSTALL_RPC_ERROR_CODE,
skillInstallFailure.code,
skillInstallFailure
)
}
if (message === 'invalid_terminal_send') {
return errorResponse(id, meta, 'invalid_argument', 'Missing terminal send payload')
}
@@ -1,30 +0,0 @@
import { describe, expect, it, vi } from 'vitest'
import { getDefaultSettings } from '../../../../shared/constants'
import { OrcaRuntimeService } from '../../orca-runtime'
import { SettingsUpdate } from './client-ui-schemas'
vi.mock('electron', () => ({
app: { getPath: () => '/orca-state', isPackaged: true }
}))
function runtimeWithSharing(agentSkillSharingEnabled: unknown): OrcaRuntimeService {
return new OrcaRuntimeService({
getSettings: () => ({ ...getDefaultSettings('/tmp'), agentSkillSharingEnabled })
} as never)
}
describe('agent skill publish capability cannot be granted over RPC', () => {
it('rejects settings.update attempts to enable it', () => {
expect(SettingsUpdate.safeParse({ agentSkillSharingEnabled: true }).success).toBe(false)
})
it('publishes the capability read-only through settings.get', () => {
expect(runtimeWithSharing(true).getClientSettings().agentSkillSharingEnabled).toBe(true)
expect(runtimeWithSharing(false).getClientSettings().agentSkillSharingEnabled).toBe(false)
expect(runtimeWithSharing(undefined).getClientSettings().agentSkillSharingEnabled).toBe(false)
})
it('fails closed for truthy non-booleans', () => {
expect(runtimeWithSharing('yes').getClientSettings().agentSkillSharingEnabled).toBe(false)
})
})
@@ -174,7 +174,6 @@ const TopLevelViewSchema = z.enum([
'activity',
'automations',
'space',
'skills',
'artifacts',
'mobile'
])
+2 -264
View File
@@ -1,14 +1,11 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { describe, expect, it, vi } from 'vitest'
import type { RpcContext } from '../core'
vi.mock('electron', () => ({
app: { getPath: () => '/orca-state', isPackaged: true }
}))
vi.mock('../../../skills/skill-discovery-target', () => ({
resolveSkillDiscoveryTarget: vi.fn((target) => ({ kind: 'native-host', cwd: target?.cwd })),
discoverSkillsOnTarget: vi.fn(async () => ({ skills: [], sources: [], scannedAt: 1 }))
}))
import { SKILL_METHODS } from './skills'
import {
discoverSkillsOnTarget,
@@ -33,7 +30,6 @@ function makeContext(overrides: {
return {
runtime: {
listRepos: () => [],
resolveSkillDiscoveryProviderRoots: async () => ({}),
resolveProjectRuntimeForWorktree:
overrides.resolveProjectRuntimeForWorktree ?? (() => undefined)
}
@@ -48,22 +44,6 @@ function discoverMethod() {
return method
}
function installMethod() {
const method = SKILL_METHODS.find((entry) => entry.name === 'skills.install')
if (!method) {
throw new Error('skills.install method not registered')
}
return method
}
function method(name: string) {
const value = SKILL_METHODS.find((entry) => entry.name === name)
if (!value) {
throw new Error(`${name} method not registered`)
}
return value
}
describe('skills.discover RPC', () => {
it('resolves the project runtime from the owning runtime store when the caller omits it', async () => {
const resolveProjectRuntimeForWorktree = vi.fn(() => WSL_RUNTIME)
@@ -92,13 +72,11 @@ describe('skills.discover RPC', () => {
it('only bypasses the host scan cache when the caller asks for a refresh', async () => {
await discoverMethod().handler({ cwd: '/repo' }, makeContext({}))
expect(vi.mocked(discoverSkillsOnTarget)).toHaveBeenLastCalledWith(expect.anything(), [], {
providerRootOverrides: {},
refresh: false
})
await discoverMethod().handler({ cwd: '/repo', refresh: true }, makeContext({}))
expect(vi.mocked(discoverSkillsOnTarget)).toHaveBeenLastCalledWith(expect.anything(), [], {
providerRootOverrides: {},
refresh: true
})
})
@@ -107,243 +85,3 @@ describe('skills.discover RPC', () => {
expect(discoverMethod().params?.parse({ cwd: '/repo' })).toEqual({ cwd: '/repo' })
})
})
describe('skills.install RPC', () => {
it('routes one bundle request without changing the single-skill method', async () => {
const installSharedSkillBundleRequest = vi.fn(async () => ({ status: 'complete' }))
const request = {
operationId: 'operation_1',
package: {
packageId: 'package_1',
versionId: 'version_1',
bundleDigest: 'a'.repeat(64),
archiveSha256: 'b'.repeat(64),
compressedBytes: 100
},
selectedSkillIds: ['alpha'],
ingress: {
kind: 'download-grant' as const,
url: 'https://storage.googleapis.com/package',
expiresAt: '2026-08-11T12:00:00.000Z'
},
destination: { scope: 'global' as const },
conflictDecisions: []
}
await method('skills.installBundle').handler(request, {
runtime: { installSharedSkillBundleRequest }
} as unknown as RpcContext)
expect(installSharedSkillBundleRequest).toHaveBeenCalledWith(request, undefined)
})
it('delegates installation to the executing runtime service', async () => {
const installSharedSkillRequest = vi.fn(async () => ({ status: 'installed' }))
const runtime = {
installSharedSkillRequest
}
const request = {
operationId: 'operation_1',
package: {
packageId: 'package_1',
versionId: 'version_1',
packageDigest: 'a'.repeat(64),
archiveSha256: 'b'.repeat(64),
compressedBytes: 100
},
ingress: {
kind: 'download-grant' as const,
url: 'https://storage.googleapis.com/package',
expiresAt: '2026-08-11T12:00:00.000Z'
},
destination: { scope: 'global' as const }
}
await installMethod().handler(request, { runtime } as unknown as RpcContext)
expect(installSharedSkillRequest).toHaveBeenCalledWith(request, undefined)
})
it('downgrades cancelled results for clients without the additive result capability', async () => {
const cancelled = {
operationId: 'operation_1',
status: 'cancelled' as const,
name: 'example',
packageDigest: 'a'.repeat(64),
placements: [],
failure: {
category: 'cancelled' as const,
code: 'skill-install-cancelled',
retryable: true
}
}
const runtime = { installSharedSkillRequest: vi.fn(async () => cancelled) }
const request = {
operationId: 'operation_1',
package: {
packageId: 'package_1',
versionId: 'version_1',
packageDigest: 'a'.repeat(64),
archiveSha256: 'b'.repeat(64),
compressedBytes: 100
},
ingress: {
kind: 'download-grant' as const,
url: 'https://storage.googleapis.com/package',
expiresAt: '2026-08-11T12:00:00.000Z'
},
destination: { scope: 'global' as const }
}
await expect(
installMethod().handler(request, { runtime, clientCapabilities: [] } as unknown as RpcContext)
).resolves.toMatchObject({ status: 'failed', errorCategory: 'skill-install-cancelled' })
await expect(
installMethod().handler(request, {
runtime,
clientCapabilities: ['skills.install-result.v2']
} as unknown as RpcContext)
).resolves.toMatchObject({ status: 'cancelled', failure: { category: 'cancelled' } })
})
it('routes cancellation to the destination runtime by operation ID', async () => {
const cancelSharedSkillInstall = vi.fn(() => true)
const result = await method('skills.cancelInstall').handler({ operationId: 'operation_1' }, {
runtime: { cancelSharedSkillInstall }
} as unknown as RpcContext)
expect(result).toEqual({ cancelled: true })
expect(cancelSharedSkillInstall).toHaveBeenCalledWith('operation_1')
})
it('returns active bundle progress without requiring it from older clients', async () => {
const progress = {
operationId: 'operation_1',
skillId: 'alpha',
skillName: 'alpha',
skillIndex: 1,
skillCount: 2
}
const getSharedSkillInstallProgress = vi.fn(() => progress)
expect(
method('skills.getInstallProgress').handler({ operationId: 'operation_1' }, {
runtime: { getSharedSkillInstallProgress }
} as unknown as RpcContext)
).toEqual(progress)
})
})
describe('skills.share RPC', () => {
const request = {
skillSelectors: ['alpha', 'beta'],
bundleName: 'team-skills',
releaseNotes: '',
target: { cwd: '/repo' }
}
beforeEach(() => {
vi.mocked(discoverSkillsOnTarget).mockClear()
vi.mocked(resolveSkillDiscoveryTarget).mockImplementation((target) => ({
kind: 'native-host',
cwd: target?.cwd ?? undefined
}))
})
it('checks permission before discovery', async () => {
const denial = new Error('denied')
const assertAgentSkillSharingAllowed = vi.fn(() => {
throw denial
})
await expect(
method('skills.share').handler(request, {
runtime: { assertAgentSkillSharingAllowed }
} as unknown as RpcContext)
).rejects.toBe(denial)
expect(discoverSkillsOnTarget).not.toHaveBeenCalled()
})
it('discovers on the executing host and delegates the selected bundle', async () => {
const discoveredSkills = [{ id: 'alpha' }, { id: 'beta' }]
vi.mocked(discoverSkillsOnTarget).mockResolvedValueOnce({
skills: discoveredSkills,
sources: [],
scannedAt: 1
} as never)
const publishDiscoveredSkillsFromAgent = vi.fn(async () => ({ status: 'ok' }))
const runtime = {
assertAgentSkillSharingAllowed: vi.fn(),
resolveProjectRuntimeForWorktree: vi.fn(),
resolveSkillDiscoveryProviderRoots: vi.fn(async () => ({})),
listRepos: vi.fn(() => []),
publishDiscoveredSkillsFromAgent
}
const signal = new AbortController().signal
await method('skills.share').handler(request, { runtime, signal } as unknown as RpcContext)
expect(discoverSkillsOnTarget).toHaveBeenCalledWith(
expect.objectContaining({ kind: 'native-host', cwd: '/repo' }),
[],
{ providerRootOverrides: {}, refresh: true }
)
expect(publishDiscoveredSkillsFromAgent).toHaveBeenCalledWith(request, discoveredSkills, signal)
})
it('rejects WSL before attempting to read Linux paths with native fs', async () => {
vi.mocked(resolveSkillDiscoveryTarget).mockReturnValueOnce({
kind: 'wsl',
distro: 'Ubuntu',
homeDir: '/home/alice',
cwd: '/repo'
})
const runtime = {
assertAgentSkillSharingAllowed: vi.fn(),
resolveProjectRuntimeForWorktree: vi.fn()
}
await expect(
method('skills.share').handler(request, { runtime } as unknown as RpcContext)
).rejects.toMatchObject({ code: 'agent_skill_sharing_unsupported_environment' })
expect(discoverSkillsOnTarget).not.toHaveBeenCalled()
})
it('rejects paired runtime callers before discovery', async () => {
const runtime = {
assertAgentSkillSharingAllowed: vi.fn()
}
await expect(
method('skills.share').handler(request, {
runtime,
clientKind: 'runtime'
} as unknown as RpcContext)
).rejects.toMatchObject({ code: 'agent_skill_sharing_unsupported_environment' })
expect(discoverSkillsOnTarget).not.toHaveBeenCalled()
})
})
describe('skill management RPC', () => {
it('delegates preview and removal to the executing runtime', async () => {
const previewSharedSkillInstallRequest = vi.fn(async () => ({ currentState: 'missing' }))
const removeSharedSkillInstallRequest = vi.fn(async () => ({ status: 'removed' }))
const runtime = { previewSharedSkillInstallRequest, removeSharedSkillInstallRequest }
const destination = { scope: 'global' as const }
const packageIdentity = {
packageId: 'package_1',
versionId: 'version_1',
packageDigest: 'a'.repeat(64),
archiveSha256: 'b'.repeat(64),
compressedBytes: 100
}
await method('skills.previewInstall').handler(
{ name: 'example', package: packageIdentity, destination },
{ runtime } as unknown as RpcContext
)
await method('skills.removeInstall').handler(
{ operationId: 'operation_1', name: 'example', destination },
{ runtime } as unknown as RpcContext
)
expect(previewSharedSkillInstallRequest).toHaveBeenCalledOnce()
expect(removeSharedSkillInstallRequest).toHaveBeenCalledOnce()
})
})
+7 -138
View File
@@ -1,44 +1,9 @@
import { defineMethod, type RpcMethod } from '../core'
import { z } from 'zod'
import { SkillDiscoveryTargetSchema } from '../../../../shared/skills'
import {
SkillInstallPreviewRequestSchema,
SkillInstallRequestSchema,
SkillRemoveRequestSchema
} from '../../../../shared/skill-install-contract'
import {
SkillBundleInstallProgressSchema,
SkillBundleInstallRequestSchema
} from '../../../../shared/skill-bundle-install-contract'
import {
SkillUploadBeginRequestSchema,
SkillUploadChunkRequestSchema,
SkillUploadCommitRequestSchema
} from '../../../../shared/skill-upload-session-contract'
import {
discoverSkillsOnTarget,
resolveSkillDiscoveryTarget
} from '../../../skills/skill-discovery-target'
import { SKILL_INSTALL_RESULT_V2_CAPABILITY } from '../../../../shared/skill-install-capability'
import type { OrcaRuntimeService } from '../../orca-runtime'
import {
AGENT_SKILL_SHARING_UNSUPPORTED_ENVIRONMENT_CODE,
AgentSkillShareRequestSchema,
AgentSkillSharingError
} from '../../../../shared/agent-skill-sharing-contract'
function resolveDiscoveryTarget(
params: z.infer<typeof SkillDiscoveryTargetSchema>,
runtime: Pick<OrcaRuntimeService, 'resolveProjectRuntimeForWorktree'>
) {
const target = params.projectRuntime
? params
: {
...params,
projectRuntime: runtime.resolveProjectRuntimeForWorktree(params.worktreeId)
}
return resolveSkillDiscoveryTarget(target)
}
export const SKILL_METHODS: RpcMethod[] = [
defineMethod({
@@ -48,111 +13,15 @@ export const SKILL_METHODS: RpcMethod[] = [
// Why: the executing runtime owns WSL project preferences. Remote callers
// send worktree identity only; trusting their projectRuntime absence
// would scan this host's native filesystem for a WSL-configured project.
const resolvedTarget = resolveDiscoveryTarget(params, runtime)
return discoverSkillsOnTarget(resolvedTarget, runtime.listRepos(), {
providerRootOverrides: await runtime.resolveSkillDiscoveryProviderRoots(resolvedTarget),
const target = params.projectRuntime
? params
: {
...params,
projectRuntime: runtime.resolveProjectRuntimeForWorktree(params.worktreeId)
}
return discoverSkillsOnTarget(resolveSkillDiscoveryTarget(target), runtime.listRepos(), {
refresh: params.refresh === true
})
}
}),
defineMethod({
name: 'skills.share',
params: AgentSkillShareRequestSchema,
handler: async (params, { runtime, signal, clientKind }) => {
runtime.assertAgentSkillSharingAllowed()
if (clientKind !== undefined) {
throw new AgentSkillSharingError(
AGENT_SKILL_SHARING_UNSUPPORTED_ENVIRONMENT_CODE,
'Publishing skills through a paired client is not supported. Run the command from Orca on the machine that stores the skills.'
)
}
const resolvedTarget = resolveDiscoveryTarget(params.target ?? {}, runtime)
if (resolvedTarget.kind !== 'native-host') {
throw new AgentSkillSharingError(
AGENT_SKILL_SHARING_UNSUPPORTED_ENVIRONMENT_CODE,
'Publishing skills from a forwarded WSL session is not supported yet. Run the command from Orca on the machine that stores the skills.'
)
}
const discovered = await discoverSkillsOnTarget(resolvedTarget, runtime.listRepos(), {
providerRootOverrides: await runtime.resolveSkillDiscoveryProviderRoots(resolvedTarget),
refresh: true
})
return runtime.publishDiscoveredSkillsFromAgent(params, discovered.skills, signal)
}
}),
defineMethod({
name: 'skills.install',
params: SkillInstallRequestSchema,
handler: async (params, { runtime, signal, clientCapabilities }) => {
const result = await runtime.installSharedSkillRequest(params, signal)
if (
result.status === 'cancelled' &&
!clientCapabilities?.includes(SKILL_INSTALL_RESULT_V2_CAPABILITY)
) {
return {
...result,
status: 'failed' as const,
errorCategory: result.failure?.code ?? 'skill-install-cancelled',
failure: undefined
}
}
return result
}
}),
defineMethod({
name: 'skills.installBundle',
params: SkillBundleInstallRequestSchema,
handler: (params, { runtime, signal }) =>
runtime.installSharedSkillBundleRequest(params, signal)
}),
defineMethod({
name: 'skills.cancelInstall',
params: z.object({ operationId: z.string().min(1).max(128) }).strict(),
handler: (params, { runtime }) => ({
cancelled: runtime.cancelSharedSkillInstall(params.operationId)
})
}),
defineMethod({
name: 'skills.getInstallProgress',
params: z.object({ operationId: z.string().min(1).max(128) }).strict(),
handler: (params, { runtime }) => {
const progress = runtime.getSharedSkillInstallProgress(params.operationId)
return progress ? SkillBundleInstallProgressSchema.parse(progress) : null
}
}),
defineMethod({
name: 'skills.previewInstall',
params: SkillInstallPreviewRequestSchema,
handler: (params, { runtime }) => runtime.previewSharedSkillInstallRequest(params)
}),
defineMethod({
name: 'skills.removeInstall',
params: SkillRemoveRequestSchema,
handler: (params, { runtime }) => runtime.removeSharedSkillInstallRequest(params)
}),
defineMethod({
name: 'skills.listManagedInstalls',
params: null,
handler: (_params, { runtime }) => runtime.listManagedSkillInstalls()
}),
defineMethod({
name: 'skills.beginUpload',
params: SkillUploadBeginRequestSchema,
handler: (params, { runtime }) => runtime.beginSkillUpload(params)
}),
defineMethod({
name: 'skills.uploadChunk',
params: SkillUploadChunkRequestSchema,
handler: (params, { runtime }) => runtime.appendSkillUploadChunk(params)
}),
defineMethod({
name: 'skills.commitUpload',
params: SkillUploadCommitRequestSchema,
handler: (params, { runtime }) => runtime.commitSkillUpload(params.uploadId)
}),
defineMethod({
name: 'skills.cancelUpload',
params: SkillUploadCommitRequestSchema,
handler: (params, { runtime }) => runtime.cancelSkillUpload(params.uploadId)
})
]
@@ -1,52 +0,0 @@
import { describe, expect, it } from 'vitest'
import type { DiscoveredSkill } from '../../shared/skills'
import {
AGENT_SKILL_SELECTOR_AMBIGUOUS_CODE,
AGENT_SKILL_SELECTOR_NOT_FOUND_CODE
} from '../../shared/agent-skill-sharing-contract'
import { selectDiscoveredSkills } from './agent-skill-selection'
function skill(id: string, name: string): DiscoveredSkill {
return {
id,
name,
description: null,
providers: ['agent-skills'],
sourceKind: 'home',
sourceLabel: 'Shared',
rootPath: '/skills',
directoryPath: `/skills/${id}`,
skillFilePath: `/skills/${id}/SKILL.md`,
installed: true,
updatedAt: null
}
}
describe('agent skill selection', () => {
it('accepts exact IDs and unambiguous names while deduplicating repeats', () => {
expect(
selectDiscoveredSkills(
[skill('id-alpha', 'alpha'), skill('id-beta', 'beta')],
['alpha', 'id-beta', 'alpha']
).map((entry) => entry.id)
).toEqual(['id-alpha', 'id-beta'])
})
it('fails missing selectors with installed-list recovery', () => {
expect(() => selectDiscoveredSkills([], ['missing'])).toThrow(
expect.objectContaining({ code: AGENT_SKILL_SELECTOR_NOT_FOUND_CODE })
)
})
it('requires an ID when names are ambiguous', () => {
expect(() =>
selectDiscoveredSkills([skill('one', 'same'), skill('two', 'same')], ['same'])
).toThrow(expect.objectContaining({ code: AGENT_SKILL_SELECTOR_AMBIGUOUS_CODE }))
})
it('rejects two exact IDs whose bundle folder names would collide', () => {
expect(() =>
selectDiscoveredSkills([skill('one', 'same'), skill('two', 'same')], ['one', 'two'])
).toThrow(expect.objectContaining({ code: AGENT_SKILL_SELECTOR_AMBIGUOUS_CODE }))
})
})
-50
View File
@@ -1,50 +0,0 @@
import {
AGENT_SKILL_SELECTOR_AMBIGUOUS_CODE,
AGENT_SKILL_SELECTOR_NOT_FOUND_CODE,
AgentSkillSharingError
} from '../../shared/agent-skill-sharing-contract'
import type { DiscoveredSkill } from '../../shared/skills'
export function selectDiscoveredSkills(
skills: readonly DiscoveredSkill[],
selectors: readonly string[]
): DiscoveredSkill[] {
const selected = new Map<string, DiscoveredSkill>()
for (const selector of selectors) {
const exactId = skills.find((skill) => skill.id === selector)
if (exactId) {
selected.set(exactId.id, exactId)
continue
}
const named = skills.filter((skill) => skill.name === selector)
if (named.length === 0) {
throw new AgentSkillSharingError(
AGENT_SKILL_SELECTOR_NOT_FOUND_CODE,
`Installed skill "${selector}" was not found. Run \`orca skills installed\` to list valid selectors.`,
{ selector }
)
}
if (named.length > 1) {
throw new AgentSkillSharingError(
AGENT_SKILL_SELECTOR_AMBIGUOUS_CODE,
`More than one installed skill is named "${selector}". Use its discovery ID from \`orca skills installed\`.`,
{ selector, matchingIds: named.map((skill) => skill.id) }
)
}
selected.set(named[0].id, named[0])
}
const values = [...selected.values()]
const byName = new Map<string, DiscoveredSkill[]>()
for (const skill of values) {
byName.set(skill.name, [...(byName.get(skill.name) ?? []), skill])
}
const collision = [...byName.entries()].find(([, named]) => named.length > 1)
if (collision) {
throw new AgentSkillSharingError(
AGENT_SKILL_SELECTOR_AMBIGUOUS_CODE,
`The selected skills include more than one installed skill named "${collision[0]}". Publish them in separate bundles.`,
{ selector: collision[0], matchingIds: collision[1].map((skill) => skill.id) }
)
}
return values
}
+3 -20
View File
@@ -204,11 +204,7 @@ describe('skill discovery', () => {
await writeFile(join(codexSkills, 'review', 'SKILL.md'), '# review')
await mkdir(join(home, '.agents'), { recursive: true })
// Shared root is a symlink onto the Codex root: one canonical file, two roots.
await symlink(
codexSkills,
join(home, '.agents', 'skills'),
process.platform === 'win32' ? 'junction' : 'dir'
)
await symlink(codexSkills, join(home, '.agents', 'skills'), 'dir')
const result = await discoverSkills({ homeDir: home, repos: [], includeCwd: false })
@@ -227,11 +223,7 @@ describe('skill discovery', () => {
await writeFile(join(claudeSkills, 'orchestration', 'SKILL.md'), '# orchestration')
// `npx skills add --global` links a provider home onto an existing install.
await mkdir(join(home, '.grok'), { recursive: true })
await symlink(
claudeSkills,
join(home, '.grok', 'skills'),
process.platform === 'win32' ? 'junction' : 'dir'
)
await symlink(claudeSkills, join(home, '.grok', 'skills'), 'dir')
const result = await discoverSkills({ homeDir: home, repos: [], includeCwd: false })
@@ -293,16 +285,7 @@ describe('skill discovery', () => {
'/home/test/.omp/agent/skills',
'/home/test/.gemini/skills',
'/home/test/.gemini/antigravity/skills',
'/home/test/.cursor/skills',
'/home/test/.factory/skills',
'/home/test/.continue/skills',
'/home/test/.trae-cn/skills',
'/home/test/.augment/skills',
'/workspace/current/.factory/skills',
'/workspace/current/.continue/skills',
'/workspace/current/.trae/skills',
'/workspace/current/.grok/skills',
'/workspace/current/.augment/skills'
'/home/test/.cursor/skills'
])
)
// Why: these live outside ~/.agents/skills, so they must carry the shared
-2
View File
@@ -24,7 +24,6 @@ import {
SkillScanCoalescer,
type SkillScanOutcome
} from './skill-scan-coalescer'
import type { SkillProviderRootOverrides } from './skill-provider-destinations'
export { buildSkillDiscoverySources } from './skill-discovery-sources'
@@ -208,7 +207,6 @@ export async function discoverSkills(args: {
homeDir?: string
cwd?: string
includeCwd?: boolean
providerRootOverrides?: SkillProviderRootOverrides
refresh?: boolean
}): Promise<SkillDiscoveryResult> {
const startedAt = Date.now()
@@ -1,202 +0,0 @@
import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import {
AGENT_PLUGIN_MANIFEST_PATH,
AGENT_PLUGIN_SCHEMA_V1
} from '../../shared/skill-bundle-manifest'
import { createSkillBundleArchive } from './skill-bundle-creation'
import { extractSkillBundleArchive } from './skill-bundle-extraction'
import { writeSkillTarGzip } from './skill-package-tar'
const temporaryDirectories: string[] = []
async function temporaryDirectory(): Promise<string> {
const directory = await mkdtemp(join(tmpdir(), 'orca-skill-bundle-test-'))
temporaryDirectories.push(directory)
return directory
}
async function createSkill(root: string, name: string, description: string): Promise<string> {
const directory = join(root, name)
await mkdir(directory, { recursive: true })
await writeFile(
join(directory, 'SKILL.md'),
`---\nname: ${name}\ndescription: ${description}\n---\n\n# ${name}\n`
)
await writeFile(join(directory, 'notes.txt'), `${name} notes\n`)
return directory
}
afterEach(async () => {
await Promise.all(
temporaryDirectories.splice(0).map((directory) => rm(directory, { recursive: true }))
)
})
describe('skill bundle creation and extraction', () => {
it('round trips multiple skills through an Agent Plugins compatible root', async () => {
const root = await temporaryDirectory()
const alpha = await createSkill(root, 'alpha-skill', 'Alpha')
const beta = await createSkill(root, 'beta-skill', 'Beta')
const created = await createSkillBundleArchive({
sources: [{ sourceDirectory: beta }, { sourceDirectory: alpha }],
archivePath: join(root, 'bundle.tar.gz'),
packageId: 'package_1',
versionId: 'version_1',
bundleName: 'team-skills',
description: 'Team skills',
createdAt: '2026-08-11T12:00:00.000Z'
})
const extracted = await extractSkillBundleArchive({
archivePath: created.archivePath,
destinationDirectory: join(root, 'extracted'),
expectedBundleDigest: created.manifest.bundleDigest
})
expect(extracted.pluginManifest).toEqual({
$schema: AGENT_PLUGIN_SCHEMA_V1,
name: 'team-skills',
version: 'version_1',
description: 'Team skills'
})
expect(extracted.manifest.skills.map((skill) => skill.name)).toEqual([
'alpha-skill',
'beta-skill'
])
expect(await readFile(join(extracted.skillsDirectory, 'beta-skill', 'notes.txt'), 'utf8')).toBe(
'beta-skill notes\n'
)
})
it('creates deterministic archives regardless of source selection order', async () => {
const root = await temporaryDirectory()
const alpha = await createSkill(root, 'alpha-skill', 'Alpha')
const beta = await createSkill(root, 'beta-skill', 'Beta')
const publication = {
packageId: 'package_1',
versionId: 'version_1',
bundleName: 'team-skills',
createdAt: '2026-08-11T12:00:00.000Z'
}
const first = await createSkillBundleArchive({
...publication,
sources: [{ sourceDirectory: alpha }, { sourceDirectory: beta }],
archivePath: join(root, 'first.tar.gz')
})
const second = await createSkillBundleArchive({
...publication,
sources: [{ sourceDirectory: beta }, { sourceDirectory: alpha }],
archivePath: join(root, 'second.tar.gz')
})
expect(first.archiveSha256).toBe(second.archiveSha256)
expect(await readFile(first.archivePath)).toEqual(await readFile(second.archivePath))
})
it('packages and extracts thirty selected skills within the shared limits', async () => {
const root = await temporaryDirectory()
const names = Array.from(
{ length: 30 },
(_, index) => `skill-${String(index).padStart(2, '0')}`
)
const sources = await Promise.all(
names.map(async (name) => ({
sourceDirectory: await createSkill(root, name, `Description for ${name}`)
}))
)
const created = await createSkillBundleArchive({
sources: sources.toReversed(),
archivePath: join(root, 'thirty-skills.tar.gz'),
packageId: 'package_30',
versionId: 'version_30',
bundleName: 'thirty-skills'
})
const extracted = await extractSkillBundleArchive({
archivePath: created.archivePath,
destinationDirectory: join(root, 'thirty-skills'),
expectedBundleDigest: created.manifest.bundleDigest
})
expect(extracted.manifest.skills.map((skill) => skill.name)).toEqual(names)
expect(extracted.manifest.skills).toHaveLength(30)
await expect(
readFile(join(extracted.skillsDirectory, 'skill-29', 'notes.txt'), 'utf8')
).resolves.toBe('skill-29 notes\n')
})
it('rejects conflicting staging roots without changing their imported namespace', async () => {
const root = await temporaryDirectory()
const source = await createSkill(root, 'alpha-skill', 'Alpha')
const created = await createSkillBundleArchive({
sources: [{ sourceDirectory: source }],
archivePath: join(root, 'bundle.tar.gz'),
packageId: 'package_1',
versionId: 'version_1',
bundleName: 'team-skills'
})
const destination = join(root, 'conflicting-staging')
const importedManifest = join(destination, 'dev.orca.skill-sharing', 'manifest.json')
await mkdir(join(destination, 'dev.orca.skill-sharing'), { recursive: true })
await writeFile(importedManifest, 'unowned\n')
await expect(
extractSkillBundleArchive({
archivePath: created.archivePath,
destinationDirectory: destination
})
).rejects.toMatchObject({ code: 'EEXIST' })
await expect(readFile(importedManifest, 'utf8')).resolves.toBe('unowned\n')
})
it('rejects unknown top-level extension namespaces and removes fresh staging', async () => {
const root = await temporaryDirectory()
const plugin = Buffer.from(
JSON.stringify({ $schema: AGENT_PLUGIN_SCHEMA_V1, name: 'team-skills' })
)
const unknown = Buffer.from('{}')
const archivePath = join(root, 'unknown-extension.tar.gz')
await writeSkillTarGzip(archivePath, [
{
path: AGENT_PLUGIN_MANIFEST_PATH,
size: plugin.length,
executable: false,
bytes: plugin
},
{
path: 'dev.orca.unexpected/manifest.json',
size: unknown.length,
executable: false,
bytes: unknown
}
])
const destination = join(root, 'unknown-extension-staging')
await expect(
extractSkillBundleArchive({ archivePath, destinationDirectory: destination })
).rejects.toThrow('skill-bundle-manifest-envelope-invalid')
await expect(stat(destination)).rejects.toMatchObject({ code: 'ENOENT' })
})
it('rejects duplicate names and source drift without publishing an archive', async () => {
const root = await temporaryDirectory()
const first = await createSkill(root, 'same-skill', 'First')
const secondRoot = join(root, 'other')
const second = await createSkill(secondRoot, 'same-skill', 'Second')
const archivePath = join(root, 'duplicate.tar.gz')
await expect(
createSkillBundleArchive({
sources: [{ sourceDirectory: first }, { sourceDirectory: second }],
archivePath,
packageId: 'package_1',
versionId: 'version_1',
bundleName: 'team-skills'
})
).rejects.toThrow('skill-bundle-skill-collision')
await expect(readFile(archivePath)).rejects.toMatchObject({ code: 'ENOENT' })
})
})
-270
View File
@@ -1,270 +0,0 @@
import { randomUUID } from 'node:crypto'
import { cp, mkdir, mkdtemp, readFile, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import {
AGENT_PLUGIN_MANIFEST_PATH,
AGENT_PLUGIN_SCHEMA_V1,
ORCA_SKILL_BUNDLE_MANIFEST_PATH,
computeSkillBundleDigest,
parseAgentPluginManifest,
parseSkillBundleManifest,
type AgentPluginManifestV1,
type SkillBundleEntry,
type SkillBundleManifestV1
} from '../../shared/skill-bundle-manifest'
import {
computeSkillPackageDigest,
validateSkillPackageName
} from '../../shared/skill-package-manifest'
import { summarizeSkillMarkdown } from '../../shared/skill-metadata'
import { renameSkillPathWithWindowsRetry } from './skill-filesystem-retry'
import { extractSkillBundleArchive } from './skill-bundle-extraction'
import { observeSkillPackage, type ObservedSkillPackage } from './skill-package-identity'
import { writeSkillTarGzip, type SkillTarWriteEntry } from './skill-package-tar'
import { startSkillPhaseOperation } from './skill-operation-observability'
export type SkillBundleSource = { id?: string; sourceDirectory: string }
export type CreatedSkillBundle = {
pluginManifest: AgentPluginManifestV1
manifest: SkillBundleManifestV1
archivePath: string
archiveSha256: string
compressedBytes: number
}
export type SkillBundleCreationDependencies = { afterSourcesObserved?: () => Promise<void> }
const SOURCE_OBSERVATION_CONCURRENCY = 4
function observationsMatch(left: ObservedSkillPackage, right: ObservedSkillPackage): boolean {
return (
left.files.length === right.files.length &&
left.files.every((file, index) => {
const other = right.files[index]
return (
file.path === other.path &&
file.size === other.size &&
file.executable === other.executable &&
file.classification === other.classification &&
file.exactSha256 === other.exactSha256 &&
file.identitySha256 === other.identitySha256
)
})
)
}
function bundleEntry(input: {
id: string
name: string
description: string
observed: ObservedSkillPackage
}): SkillBundleEntry {
const files = input.observed.files.map((file) => ({
path: file.path,
size: file.size,
executable: file.executable,
classification: file.classification,
sha256: file.exactSha256,
identitySha256: file.identitySha256
}))
return {
id: input.id,
name: input.name,
description: input.description,
digest: computeSkillPackageDigest(files),
files
}
}
async function stageSkill(input: {
source: SkillBundleSource
sourceObservation: ObservedSkillPackage
skillsRoot: string
name: string
description: string
}): Promise<SkillBundleEntry> {
const stagedDirectory = join(input.skillsRoot, input.name)
await cp(input.source.sourceDirectory, stagedDirectory, {
recursive: true,
verbatimSymlinks: true,
force: false,
errorOnExist: true
})
const stagedObservation = await observeSkillPackage(stagedDirectory)
if (!observationsMatch(input.sourceObservation, stagedObservation)) {
throw new Error('skill-package-source-changed-during-staging')
}
const stagedSummary = summarizeSkillMarkdown(
await readFile(join(stagedDirectory, 'SKILL.md'), 'utf8')
)
if (
stagedSummary.name !== input.name ||
(stagedSummary.description ?? '') !== input.description
) {
throw new Error('skill-package-source-changed-during-staging')
}
return bundleEntry({
id: input.source.id ?? input.name,
name: input.name,
description: input.description,
observed: stagedObservation
})
}
async function createSkillBundleArchiveUnobserved(
input: {
sources: readonly SkillBundleSource[]
archivePath: string
packageId: string
versionId: string
bundleName: string
description?: string
createdAt?: string
},
dependencies: SkillBundleCreationDependencies = {}
): Promise<CreatedSkillBundle> {
if (input.sources.length === 0) {
throw new Error('skill-bundle-empty')
}
const sourceObservations: ObservedSkillPackage[] = []
const summaries: { name: string; description: string }[] = []
for (let offset = 0; offset < input.sources.length; offset += SOURCE_OBSERVATION_CONCURRENCY) {
const batch = input.sources.slice(offset, offset + SOURCE_OBSERVATION_CONCURRENCY)
const observed = await Promise.all(
batch.map(async (source) => {
const [observation, markdown] = await Promise.all([
observeSkillPackage(source.sourceDirectory),
readFile(join(source.sourceDirectory, 'SKILL.md'), 'utf8')
])
const summary = summarizeSkillMarkdown(markdown)
if (!summary.name) {
throw new Error('skill-package-skill-name-required')
}
validateSkillPackageName(summary.name)
return {
observation,
summary: { name: summary.name, description: summary.description ?? '' }
}
})
)
sourceObservations.push(...observed.map((value) => value.observation))
summaries.push(...observed.map((value) => value.summary))
}
const foldedNames = new Set<string>()
for (const summary of summaries) {
const foldedName = summary.name.toLocaleLowerCase('en-US')
if (foldedNames.has(foldedName)) {
throw new Error('skill-bundle-skill-collision')
}
foldedNames.add(foldedName)
}
await dependencies.afterSourcesObserved?.()
const workDirectory = await mkdtemp(join(tmpdir(), 'orca-skill-bundle-'))
const skillsRoot = join(workDirectory, 'skills')
const verificationDirectory = join(workDirectory, 'verification')
const temporaryArchive = `${input.archivePath}.${process.pid}.${randomUUID()}.tmp`
try {
await mkdir(skillsRoot, { recursive: true })
const skills: SkillBundleEntry[] = []
for (const [index, source] of input.sources.entries()) {
const skill = await stageSkill({
source,
sourceObservation: sourceObservations[index],
skillsRoot,
...summaries[index]
})
skills.push(skill)
}
skills.sort((left, right) => left.name.localeCompare(right.name, 'en-US'))
const manifest = parseSkillBundleManifest({
schemaVersion: 1,
packageId: input.packageId,
versionId: input.versionId,
bundleName: input.bundleName,
description: input.description ?? '',
createdAt: input.createdAt ?? new Date().toISOString(),
skills,
bundleDigest: computeSkillBundleDigest(skills)
})
const pluginManifest = parseAgentPluginManifest({
$schema: AGENT_PLUGIN_SCHEMA_V1,
name: manifest.bundleName,
version: manifest.versionId,
description: manifest.description
})
const pluginBytes = Buffer.from(JSON.stringify(pluginManifest), 'utf8')
const manifestBytes = Buffer.from(JSON.stringify(manifest), 'utf8')
const entries: SkillTarWriteEntry[] = [
{
path: AGENT_PLUGIN_MANIFEST_PATH,
size: pluginBytes.length,
executable: false,
bytes: pluginBytes
},
{
path: ORCA_SKILL_BUNDLE_MANIFEST_PATH,
size: manifestBytes.length,
executable: false,
bytes: manifestBytes
},
...manifest.skills.flatMap((skill) =>
skill.files.map((file) => ({
path: `skills/${skill.name}/${file.path}`,
size: file.size,
executable: file.executable,
sourcePath: join(skillsRoot, skill.name, ...file.path.split('/'))
}))
)
]
await mkdir(dirname(input.archivePath), { recursive: true })
const archiveIdentity = await writeSkillTarGzip(temporaryArchive, entries)
await extractSkillBundleArchive({
archivePath: temporaryArchive,
destinationDirectory: verificationDirectory,
expectedArchiveSha256: archiveIdentity.archiveSha256,
expectedBundleDigest: manifest.bundleDigest,
expectedPackageId: manifest.packageId,
expectedVersionId: manifest.versionId
})
await renameSkillPathWithWindowsRetry(temporaryArchive, input.archivePath)
return { pluginManifest, manifest, archivePath: input.archivePath, ...archiveIdentity }
} finally {
await rm(workDirectory, { recursive: true, force: true })
await rm(temporaryArchive, { force: true }).catch(() => undefined)
}
}
export async function createSkillBundleArchive(
input: {
sources: readonly SkillBundleSource[]
archivePath: string
packageId: string
versionId: string
bundleName: string
description?: string
createdAt?: string
},
dependencies: SkillBundleCreationDependencies = {}
): Promise<CreatedSkillBundle> {
const operation = startSkillPhaseOperation({
phase: 'package',
packageKind: 'bundle',
skillCount: input.sources.length
})
try {
const created = await createSkillBundleArchiveUnobserved(input, dependencies)
const files = created.manifest.skills.flatMap((skill) => skill.files)
operation.complete({
status: 'complete',
fileCount: files.length,
totalBytes: files.reduce((total, file) => total + file.size, 0),
compressedBytes: created.compressedBytes
})
return created
} catch (error) {
operation.fail(error)
throw error
}
}
-250
View File
@@ -1,250 +0,0 @@
import { createHash } from 'node:crypto'
import { mkdir, open, readFile, rm } from 'node:fs/promises'
import { dirname, join } from 'node:path'
import {
AGENT_PLUGIN_MANIFEST_PATH,
ORCA_SKILL_BUNDLE_MANIFEST_PATH,
parseAgentPluginManifest,
parseSkillBundleManifest,
type AgentPluginManifestV1,
type SkillBundleManifestV1
} from '../../shared/skill-bundle-manifest'
import {
SKILL_PACKAGE_MAX_MANIFEST_BYTES,
type SkillPackageFile
} from '../../shared/skill-package-manifest'
import { SKILL_INSTALL_CANCELLED_FAILURE } from '../../shared/skill-install-failure'
import { summarizeSkillMarkdown } from '../../shared/skill-metadata'
import { observeSkillPackage } from './skill-package-identity'
import {
openSkillTarGzip,
parseSkillTarHeader,
SKILL_TAR_BLOCK_BYTES,
type TarByteReader
} from './skill-package-tar'
import { SkillInstallOperationError } from './skill-install-operation-error'
export type SkillBundleExtractionResult = {
pluginManifest: AgentPluginManifestV1
manifest: SkillBundleManifestV1
skillsDirectory: string
archiveSha256: string
compressedBytes: number
}
const SKILL_VERIFICATION_CONCURRENCY = 4
async function consumePadding(reader: TarByteReader, size: number): Promise<void> {
const padding = (SKILL_TAR_BLOCK_BYTES - (size % SKILL_TAR_BLOCK_BYTES)) % SKILL_TAR_BLOCK_BYTES
if (padding > 0 && !(await reader.readExact(padding)).every((byte) => byte === 0)) {
throw new Error('skill-package-tar-padding-invalid')
}
}
function throwIfCancelled(signal?: AbortSignal): void {
if (signal?.aborted) {
throw new SkillInstallOperationError(SKILL_INSTALL_CANCELLED_FAILURE)
}
}
async function readJsonEntry(
reader: TarByteReader,
expectedPath: string,
signal?: AbortSignal
): Promise<unknown> {
throwIfCancelled(signal)
const header = parseSkillTarHeader(await reader.readExact(SKILL_TAR_BLOCK_BYTES))
if (
!header ||
header.path !== expectedPath ||
header.executable ||
header.size > SKILL_PACKAGE_MAX_MANIFEST_BYTES
) {
throw new Error('skill-bundle-manifest-envelope-invalid')
}
const bytes = await reader.readExact(header.size)
throwIfCancelled(signal)
await consumePadding(reader, header.size)
try {
return JSON.parse(bytes.toString('utf8'))
} catch {
throw new Error('skill-bundle-manifest-invalid')
}
}
async function extractFile(
reader: TarByteReader,
destination: string,
expected: SkillPackageFile,
signal?: AbortSignal
): Promise<void> {
throwIfCancelled(signal)
await mkdir(dirname(destination), { recursive: true, mode: 0o700 })
const handle = await open(destination, 'wx', expected.executable ? 0o700 : 0o600)
const hash = createHash('sha256')
let offset = 0
try {
while (offset < expected.size) {
throwIfCancelled(signal)
const bytes = await reader.readExact(Math.min(64 * 1024, expected.size - offset))
hash.update(bytes)
let written = 0
while (written < bytes.length) {
const result = await handle.write(bytes, written, bytes.length - written, offset + written)
if (result.bytesWritten === 0) {
throw new Error('skill-package-extraction-write-failed')
}
written += result.bytesWritten
}
offset += bytes.length
}
} finally {
await handle.close()
}
if (hash.digest('hex') !== expected.sha256) {
throw new Error('skill-package-file-digest-mismatch')
}
await consumePadding(reader, expected.size)
}
async function requireArchiveEnd(reader: TarByteReader, signal?: AbortSignal): Promise<void> {
for (let index = 0; index < 2; index += 1) {
throwIfCancelled(signal)
if (!(await reader.readExact(SKILL_TAR_BLOCK_BYTES)).every((byte) => byte === 0)) {
throw new Error('skill-package-tar-trailing-entry')
}
}
for (;;) {
throwIfCancelled(signal)
const block = await reader.readExactOrNull(SKILL_TAR_BLOCK_BYTES)
if (!block) {
return
}
if (!block.every((byte) => byte === 0)) {
throw new Error('skill-package-tar-trailing-data')
}
}
}
async function verifySkill(input: {
directory: string
manifest: SkillBundleManifestV1['skills'][number]
signal?: AbortSignal
}): Promise<void> {
throwIfCancelled(input.signal)
const observed = await observeSkillPackage(input.directory, undefined, undefined, input.signal)
throwIfCancelled(input.signal)
if (
observed.observedDigest !== input.manifest.digest ||
observed.files.length !== input.manifest.files.length ||
observed.files.some((file, index) => {
const expected = input.manifest.files[index]
return (
file.path !== expected.path ||
file.exactSha256 !== expected.sha256 ||
file.identitySha256 !== expected.identitySha256 ||
file.executable !== expected.executable
)
})
) {
throw new Error('skill-bundle-extracted-identity-mismatch')
}
const summary = summarizeSkillMarkdown(await readFile(join(input.directory, 'SKILL.md'), 'utf8'))
if (summary.name !== input.manifest.name) {
throw new Error('skill-package-skill-name-mismatch')
}
}
export async function extractSkillBundleArchive(input: {
archivePath: string
destinationDirectory: string
expectedArchiveSha256?: string
expectedBundleDigest?: string
expectedPackageId?: string
expectedVersionId?: string
signal?: AbortSignal
}): Promise<SkillBundleExtractionResult> {
const archive = await openSkillTarGzip(input.archivePath)
let destinationCreated = false
try {
throwIfCancelled(input.signal)
await mkdir(input.destinationDirectory, { mode: 0o700 })
destinationCreated = true
const pluginManifest = parseAgentPluginManifest(
await readJsonEntry(archive.reader, AGENT_PLUGIN_MANIFEST_PATH, input.signal)
)
const manifest = parseSkillBundleManifest(
await readJsonEntry(archive.reader, ORCA_SKILL_BUNDLE_MANIFEST_PATH, input.signal)
)
if (
pluginManifest.name !== manifest.bundleName ||
pluginManifest.version !== manifest.versionId ||
(input.expectedBundleDigest && input.expectedBundleDigest !== manifest.bundleDigest) ||
(input.expectedPackageId && input.expectedPackageId !== manifest.packageId) ||
(input.expectedVersionId && input.expectedVersionId !== manifest.versionId)
) {
throw new Error('skill-bundle-identity-mismatch')
}
const skillsDirectory = join(input.destinationDirectory, 'skills')
await mkdir(skillsDirectory, { mode: 0o700 })
for (const skill of manifest.skills) {
for (const file of skill.files) {
throwIfCancelled(input.signal)
const archivePath = `skills/${skill.name}/${file.path}`
const header = parseSkillTarHeader(await archive.reader.readExact(SKILL_TAR_BLOCK_BYTES))
if (
!header ||
header.path !== archivePath ||
header.size !== file.size ||
header.executable !== file.executable
) {
throw new Error('skill-bundle-file-envelope-mismatch')
}
await extractFile(
archive.reader,
join(skillsDirectory, skill.name, ...file.path.split('/')),
file,
input.signal
)
}
}
await requireArchiveEnd(archive.reader, input.signal)
const archiveIdentity = await archive.archiveIdentity
if (
input.expectedArchiveSha256 &&
archiveIdentity.archiveSha256 !== input.expectedArchiveSha256
) {
throw new Error('skill-package-archive-digest-mismatch')
}
for (
let offset = 0;
offset < manifest.skills.length;
offset += SKILL_VERIFICATION_CONCURRENCY
) {
const batch = manifest.skills.slice(offset, offset + SKILL_VERIFICATION_CONCURRENCY)
await Promise.all(
batch.map((skill) =>
verifySkill({
directory: join(skillsDirectory, skill.name),
manifest: skill,
signal: input.signal
})
)
)
}
throwIfCancelled(input.signal)
return { pluginManifest, manifest, skillsDirectory, ...archiveIdentity }
} catch (error) {
const failure = input.signal?.aborted
? new SkillInstallOperationError(SKILL_INSTALL_CANCELLED_FAILURE, { cause: error })
: error instanceof Error
? error
: new Error(String(error))
archive.abort(failure)
await archive.archiveIdentity.catch(() => undefined)
if (destinationCreated) {
await rm(input.destinationDirectory, { recursive: true, force: true })
}
throw failure
}
}
@@ -1,141 +0,0 @@
import { isAbsolute, join } from 'node:path'
import {
type SkillBundleInstallProgress,
SkillBundleInstallRequestSchema,
type SkillBundleInstallRequest,
type SkillBundleInstallResult,
type SkillBundlePackageIdentity
} from '../../shared/skill-bundle-install-contract'
import { resolveSkillInstallDestination } from './skill-install-destinations'
import type { SkillInstallDestinationAuthority } from './skill-install-destinations'
import { installSkillBundle } from './skill-bundle-install-service'
import { downloadSkillPackageGrant } from './skill-package-download'
import { detectSkillProvidersInWsl } from './skill-wsl-provider-detection'
import { selectedOrDetectedSkillProviders } from '../../shared/skill-install-providers'
import { createWslSkillInstallFilesystem } from './skill-wsl-install-filesystem'
import { startSkillBundleInstallOperation } from './skill-operation-observability'
import type { SkillProviderRootOverrides } from './skill-provider-destinations'
type StagedSkillBundle = { archivePath: string; cleanup(): Promise<void> }
export type SkillBundleInstallRequestDependencies = {
authority: SkillInstallDestinationAuthority
stateDirectory: string
allowedDownloadOrigins: readonly string[]
requireHttps: boolean
allowTrustedLocalFile?: boolean
signal?: AbortSignal
onProgress?: (progress: SkillBundleInstallProgress) => void
fetcher?: typeof fetch
detectProviders: () => Promise<readonly string[]>
resolveProviderRootOverrides?: (destination: {
scope: 'global' | 'workspace'
homeDirectory: string
workspaceDirectory?: string
wslDistro?: string
}) => Promise<SkillProviderRootOverrides> | SkillProviderRootOverrides
resolveStagedUpload?: (
uploadId: string,
identity: SkillBundlePackageIdentity
) => Promise<StagedSkillBundle>
}
async function resolveIngress(
request: SkillBundleInstallRequest,
dependencies: SkillBundleInstallRequestDependencies
): Promise<StagedSkillBundle> {
if (request.ingress.kind === 'download-grant') {
return downloadSkillPackageGrant({
url: request.ingress.url,
expiresAt: request.ingress.expiresAt,
expectedArchiveSha256: request.package.archiveSha256,
expectedCompressedBytes: request.package.compressedBytes,
temporaryRoot: join(dependencies.stateDirectory, 'skill-installs', 'downloads'),
allowedOrigins: dependencies.allowedDownloadOrigins,
requireHttps: dependencies.requireHttps,
signal: dependencies.signal,
fetcher: dependencies.fetcher
})
}
if (request.ingress.kind === 'staged-upload') {
if (!dependencies.resolveStagedUpload) {
throw new Error('skill-bundle-staged-upload-unsupported')
}
return dependencies.resolveStagedUpload(request.ingress.uploadId, request.package)
}
if (!dependencies.allowTrustedLocalFile || !isAbsolute(request.ingress.path)) {
throw new Error('skill-install-local-ingress-rejected')
}
return { archivePath: request.ingress.path, cleanup: async () => undefined }
}
async function executeParsedSkillBundleInstallRequest(
request: SkillBundleInstallRequest,
dependencies: SkillBundleInstallRequestDependencies
): Promise<SkillBundleInstallResult> {
let ingress: StagedSkillBundle | null = null
try {
const destination = await resolveSkillInstallDestination(
request.destination,
dependencies.authority
)
ingress = await resolveIngress(request, dependencies)
const detectedProviders = selectedOrDetectedSkillProviders(
destination.wslDistro
? await detectSkillProvidersInWsl(destination.wslDistro)
: await dependencies.detectProviders(),
request.providers
)
const providerRootOverrides = await dependencies.resolveProviderRootOverrides?.(destination)
const filesystem = destination.wslDistro
? createWslSkillInstallFilesystem({
distro: destination.wslDistro,
homeDirectory: destination.homeDirectory,
workspaceDirectory: destination.workspaceDirectory,
providerRootOverrides
})
: undefined
return await installSkillBundle({
operationId: request.operationId,
archivePath: ingress.archivePath,
packageId: request.package.packageId,
versionId: request.package.versionId,
bundleDigest: request.package.bundleDigest,
selectedSkillIds: request.selectedSkillIds,
conflictDecisions: new Map(
request.conflictDecisions.map((decision) => [decision.skillId, decision.resolution])
),
scope: destination.scope,
homeDirectory: destination.homeDirectory,
workspaceDirectory: destination.workspaceDirectory,
orcaStateDirectory: dependencies.stateDirectory,
detectedProviders,
providerRootOverrides,
destinationIdentity: destination.destinationIdentity,
hostIdentity: dependencies.authority.environmentId,
expectedArchiveSha256: request.package.archiveSha256,
filesystem,
wslDistro: destination.wslDistro,
signal: dependencies.signal,
onProgress: dependencies.onProgress
})
} finally {
await ingress?.cleanup()
}
}
export async function executeSkillBundleInstallRequest(
input: unknown,
dependencies: SkillBundleInstallRequestDependencies
): Promise<SkillBundleInstallResult> {
const request = SkillBundleInstallRequestSchema.parse(input)
const operation = startSkillBundleInstallOperation(request)
try {
const result = await executeParsedSkillBundleInstallRequest(request, dependencies)
operation.complete(result)
return result
} catch (error) {
operation.fail(error)
throw error
}
}
@@ -1,262 +0,0 @@
import { readdirSync, statSync } from 'node:fs'
import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { SKILL_INSTALL_CANCELLED_FAILURE } from '../../shared/skill-install-failure'
import { createSkillBundleArchive } from './skill-bundle-creation'
import { installSkillBundle } from './skill-bundle-install-service'
import { listManagedSkillInstalls } from './skill-install-provenance'
const temporaryDirectories: string[] = []
async function temporaryDirectory(): Promise<string> {
const directory = await mkdtemp(join(tmpdir(), 'orca-skill-bundle-install-test-'))
temporaryDirectories.push(directory)
return directory
}
async function createSkill(root: string, name: string): Promise<string> {
const directory = join(root, name)
await mkdir(directory, { recursive: true })
await writeFile(
join(directory, 'SKILL.md'),
`---\nname: ${name}\ndescription: ${name}\n---\n\n# ${name}\n`
)
return directory
}
afterEach(async () => {
await Promise.all(
temporaryDirectories.splice(0).map((directory) => rm(directory, { recursive: true }))
)
})
describe('skill bundle installation', () => {
it('cancels before extracting bundle bytes and removes staging', async () => {
const root = await temporaryDirectory()
const source = await createSkill(join(root, 'sources'), 'alpha-skill')
const bundle = await createSkillBundleArchive({
sources: [{ sourceDirectory: source }],
archivePath: join(root, 'bundle.tar.gz'),
packageId: 'package_cancel',
versionId: 'version_cancel',
bundleName: 'team-skills'
})
const controller = new AbortController()
controller.abort()
await expect(
installSkillBundle({
operationId: 'operation_cancel',
archivePath: bundle.archivePath,
packageId: bundle.manifest.packageId,
versionId: bundle.manifest.versionId,
bundleDigest: bundle.manifest.bundleDigest,
selectedSkillIds: ['alpha-skill'],
expectedArchiveSha256: bundle.archiveSha256,
scope: 'global',
homeDirectory: join(root, 'home'),
orcaStateDirectory: join(root, 'state'),
detectedProviders: [],
destinationIdentity: 'local-global',
hostIdentity: 'host_1',
signal: controller.signal
})
).rejects.toMatchObject({ data: SKILL_INSTALL_CANCELLED_FAILURE })
await expect(readdir(join(root, 'home', '.agents', 'skills'))).resolves.toEqual([])
})
it('cancels during bundle extraction and removes its journal and partial bytes', async () => {
const root = await temporaryDirectory()
const source = await createSkill(join(root, 'sources'), 'alpha-skill')
const payloadBytes = 256 * 1024
await writeFile(join(source, 'payload.bin'), Buffer.alloc(payloadBytes, 0x61))
const bundle = await createSkillBundleArchive({
sources: [{ sourceDirectory: source }],
archivePath: join(root, 'bundle.tar.gz'),
packageId: 'package_mid_extract_cancel',
versionId: 'version_mid_extract_cancel',
bundleName: 'team-skills'
})
const destinationRoot = join(root, 'home', '.agents', 'skills')
const controller = new AbortController()
let observedPartialBytes = false
const signal = new Proxy(controller.signal, {
get(target, property) {
if (property === 'aborted' && !target.aborted) {
const extraction = readdirSync(destinationRoot, { withFileTypes: true }).find(
(entry) => entry.isDirectory() && entry.name.startsWith('.orca-skill-extract-')
)
if (extraction) {
const size = (() => {
try {
return statSync(
join(destinationRoot, extraction.name, 'skills', 'alpha-skill', 'payload.bin')
).size
} catch {
return 0
}
})()
if (size > 0 && size < payloadBytes) {
observedPartialBytes = true
controller.abort()
}
}
}
const value = Reflect.get(target, property, target) as unknown
return typeof value === 'function' ? value.bind(target) : value
}
})
await expect(
installSkillBundle({
operationId: 'operation_mid_extract_cancel',
archivePath: bundle.archivePath,
packageId: bundle.manifest.packageId,
versionId: bundle.manifest.versionId,
bundleDigest: bundle.manifest.bundleDigest,
selectedSkillIds: ['alpha-skill'],
expectedArchiveSha256: bundle.archiveSha256,
scope: 'global',
homeDirectory: join(root, 'home'),
orcaStateDirectory: join(root, 'state'),
detectedProviders: [],
destinationIdentity: 'local-global',
hostIdentity: 'host_1',
signal
})
).rejects.toMatchObject({ data: SKILL_INSTALL_CANCELLED_FAILURE })
expect(observedPartialBytes).toBe(true)
await expect(readdir(destinationRoot)).resolves.toEqual([])
await expect(
readdir(join(root, 'state', 'skill-installs', 'extraction-journals'))
).resolves.toEqual([])
await expect(listManagedSkillInstalls(join(root, 'state', 'skill-installs'))).resolves.toEqual(
[]
)
})
it('installs a selected subset and keeps an unowned conflict local', async () => {
const root = await temporaryDirectory()
const alpha = await createSkill(join(root, 'sources'), 'alpha-skill')
const beta = await createSkill(join(root, 'sources'), 'beta-skill')
const homeDirectory = join(root, 'home')
const existingBeta = join(homeDirectory, '.agents', 'skills', 'beta-skill')
await mkdir(existingBeta, { recursive: true })
await writeFile(
join(existingBeta, 'SKILL.md'),
'---\nname: beta-skill\ndescription: Local\n---\n\n# Keep me\n'
)
const bundle = await createSkillBundleArchive({
sources: [{ sourceDirectory: alpha }, { sourceDirectory: beta }],
archivePath: join(root, 'bundle.tar.gz'),
packageId: 'package_1',
versionId: 'version_1',
bundleName: 'team-skills',
createdAt: '2026-08-11T12:00:00.000Z'
})
const onProgress = vi.fn((progress: { skillIndex: number }) => {
if (progress.skillIndex === 1) {
throw new Error('renderer closed')
}
})
const result = await installSkillBundle({
operationId: 'operation_1',
archivePath: bundle.archivePath,
packageId: bundle.manifest.packageId,
versionId: bundle.manifest.versionId,
bundleDigest: bundle.manifest.bundleDigest,
selectedSkillIds: ['alpha-skill', 'beta-skill'],
expectedArchiveSha256: bundle.archiveSha256,
scope: 'global',
homeDirectory,
orcaStateDirectory: join(root, 'state'),
detectedProviders: [],
destinationIdentity: 'local-global',
hostIdentity: 'host_1',
onProgress
})
expect(result.status).toBe('partial')
expect(result.skills.map((skill) => [skill.name, skill.status])).toEqual([
['alpha-skill', 'installed'],
['beta-skill', 'kept-local']
])
expect(onProgress.mock.calls.map(([progress]) => progress)).toEqual([
{
operationId: 'operation_1',
skillId: 'alpha-skill',
skillName: 'alpha-skill',
skillIndex: 1,
skillCount: 2
},
{
operationId: 'operation_1',
skillId: 'beta-skill',
skillName: 'beta-skill',
skillIndex: 2,
skillCount: 2
}
])
expect(
await readFile(join(homeDirectory, '.agents', 'skills', 'alpha-skill', 'SKILL.md'), 'utf8')
).toContain('# alpha-skill')
expect(await readFile(join(existingBeta, 'SKILL.md'), 'utf8')).toContain('# Keep me')
await expect(listManagedSkillInstalls(join(root, 'state', 'skill-installs'))).resolves.toEqual([
expect.objectContaining({
name: 'alpha-skill',
packageId: 'package_1',
versionId: 'version_1',
bundleDigest: bundle.manifest.bundleDigest
})
])
})
it('reports partial when a selected provider placement is unowned', async () => {
const root = await temporaryDirectory()
const alpha = await createSkill(join(root, 'sources'), 'alpha-skill')
const homeDirectory = join(root, 'home')
const providerPath = join(homeDirectory, '.claude', 'skills', 'alpha-skill')
await mkdir(providerPath, { recursive: true })
await writeFile(join(providerPath, 'SKILL.md'), '# Unowned')
const bundle = await createSkillBundleArchive({
sources: [{ sourceDirectory: alpha }],
archivePath: join(root, 'bundle.tar.gz'),
packageId: 'package_provider_partial',
versionId: 'version_1',
bundleName: 'team-skills'
})
const result = await installSkillBundle({
operationId: 'operation_provider_partial',
archivePath: bundle.archivePath,
packageId: bundle.manifest.packageId,
versionId: bundle.manifest.versionId,
bundleDigest: bundle.manifest.bundleDigest,
selectedSkillIds: ['alpha-skill'],
expectedArchiveSha256: bundle.archiveSha256,
scope: 'global',
homeDirectory,
orcaStateDirectory: join(root, 'state'),
detectedProviders: ['claude'],
destinationIdentity: 'local-global',
hostIdentity: 'host_1'
})
expect(result).toMatchObject({
status: 'partial',
skills: [
{
name: 'alpha-skill',
status: 'installed',
placements: expect.arrayContaining([
expect.objectContaining({ provider: 'claude', status: 'skipped' })
])
}
]
})
})
})
@@ -1,268 +0,0 @@
import { mkdir } from 'node:fs/promises'
import { join } from 'node:path'
import type {
SkillBundleInstallProgress,
SkillBundleInstallResult,
SkillBundleSkillResult
} from '../../shared/skill-bundle-install-contract'
import { SkillBundleInstallResultSchema } from '../../shared/skill-bundle-install-contract'
import type { SkillBundleEntry } from '../../shared/skill-bundle-manifest'
import { parseSkillPackageManifest } from '../../shared/skill-package-manifest'
import { extractSkillBundleArchive } from './skill-bundle-extraction'
import {
installSharedExtractedSkill,
skillInstallLocalInput,
type SkillInstallServiceInput
} from './skill-install-service'
import {
previewLocalExtractedSkillPackage,
type LocalExtractedSkillPackage
} from './skill-install-transaction'
import { skillInstallFailureFromError } from './skill-install-operation-error'
import { renameSkillPathWithWindowsRetry } from './skill-filesystem-retry'
import {
beginSkillExtractionRecovery,
finishSkillExtractionRecovery
} from './skill-extraction-recovery'
import { nativeSkillInstallFilesystem } from './skill-install-filesystem'
export type SkillBundleInstallServiceInput = Omit<
SkillInstallServiceInput,
'expectedPackageDigest' | 'expectedPackageId' | 'expectedVersionId' | 'conflictResolution'
> & {
packageId: string
versionId: string
bundleDigest: string
selectedSkillIds: readonly string[]
conflictDecisions?: ReadonlyMap<
string,
'keep-local' | 'replace-unmodified' | 'replace-and-discard-local'
>
onProgress?: (progress: SkillBundleInstallProgress) => void
}
function skillManifest(input: {
packageId: string
versionId: string
createdAt: string
skill: SkillBundleEntry
}) {
return parseSkillPackageManifest({
schemaVersion: 1,
packageId: input.packageId,
versionId: input.versionId,
name: input.skill.name,
description: input.skill.description,
createdAt: input.createdAt,
files: input.skill.files,
packageDigest: input.skill.digest
})
}
function conflictResult(
skill: SkillBundleEntry,
preview: Awaited<ReturnType<typeof previewLocalExtractedSkillPackage>>
): SkillBundleSkillResult {
const state = preview.currentState
const kind =
state.kind === 'modified' ||
state.kind === 'unowned' ||
state.kind === 'external-link' ||
state.kind === 'name-collision'
? state.kind
: 'modified'
return {
skillId: skill.id,
name: skill.name,
digest: skill.digest,
status: 'kept-local',
placements: [],
conflict: {
kind,
...('digest' in state && state.digest ? { existingDigest: state.digest } : {})
}
}
}
function completedSkillResult(input: {
skill: SkillBundleEntry
preview: Awaited<ReturnType<typeof previewLocalExtractedSkillPackage>>
result: Awaited<ReturnType<typeof installSharedExtractedSkill>>
}): SkillBundleSkillResult {
const status =
input.result.status === 'partial'
? input.preview.currentState.kind === 'missing'
? 'installed'
: input.preview.currentState.kind === 'unchanged'
? 'unchanged'
: 'updated'
: input.result.status === 'conflict'
? 'kept-local'
: input.result.status === 'cancelled'
? 'cancelled'
: input.result.status === 'failed'
? 'failed'
: input.result.status === 'removed'
? 'failed'
: input.result.status
return {
skillId: input.skill.id,
name: input.skill.name,
digest: input.skill.digest,
status,
...(input.result.canonicalPath ? { canonicalPath: input.result.canonicalPath } : {}),
placements: input.result.placements
.filter((placement) => placement.status !== 'removed')
.map((placement) => ({
...placement,
status: placement.status === 'removed' ? 'skipped' : placement.status
})),
...(input.result.conflict ? { conflict: input.result.conflict } : {}),
...(input.result.errorCategory ? { errorCategory: input.result.errorCategory } : {}),
...(input.result.failure ? { failure: input.result.failure } : {})
}
}
function bundleStatus(
skills: readonly SkillBundleSkillResult[]
): SkillBundleInstallResult['status'] {
if (skills.every((skill) => skill.status === 'cancelled')) {
return 'cancelled'
}
if (skills.every((skill) => skill.status === 'failed')) {
return 'failed'
}
return skills.some(
(skill) =>
['kept-local', 'failed', 'cancelled'].includes(skill.status) ||
skill.placements.some(
(placement) => placement.status === 'failed' || placement.status === 'skipped'
)
)
? 'partial'
: 'complete'
}
export async function installSkillBundle(
input: SkillBundleInstallServiceInput
): Promise<SkillBundleInstallResult> {
const destinationRoot = skillInstallLocalInput({
...input,
expectedPackageDigest: input.bundleDigest,
expectedPackageId: input.packageId,
expectedVersionId: input.versionId
}).destinationRoot
await mkdir(destinationRoot, { recursive: true })
const stateDirectory = join(input.orcaStateDirectory, 'skill-installs')
const recovery = await beginSkillExtractionRecovery(
stateDirectory,
destinationRoot,
input.wslDistro
)
const extractionPath = recovery.extractionPath
try {
const extracted = await extractSkillBundleArchive({
archivePath: input.archivePath,
destinationDirectory: extractionPath,
expectedArchiveSha256: input.expectedArchiveSha256,
expectedBundleDigest: input.bundleDigest,
expectedPackageId: input.packageId,
expectedVersionId: input.versionId,
signal: input.signal
})
const selected = new Set(input.selectedSkillIds)
const skills = extracted.manifest.skills.filter((skill) => selected.has(skill.id))
if (skills.length !== selected.size) {
throw new Error('skill-bundle-selection-invalid')
}
const results: SkillBundleSkillResult[] = []
for (const [index, skill] of skills.entries()) {
try {
input.onProgress?.({
operationId: input.operationId,
skillId: skill.id,
skillName: skill.name,
skillIndex: index + 1,
skillCount: skills.length
})
} catch {
// Why: progress observers cannot participate in the install transaction.
}
if (input.signal?.aborted) {
results.push({
skillId: skill.id,
name: skill.name,
digest: skill.digest,
status: 'cancelled',
placements: []
})
continue
}
const manifest = skillManifest({
packageId: extracted.manifest.packageId,
versionId: extracted.manifest.versionId,
createdAt: extracted.manifest.createdAt,
skill
})
const wrapper = join(extractionPath, 'selected', skill.id)
const extractedSkill: LocalExtractedSkillPackage = {
extractionPath: wrapper,
manifest,
archiveSha256: extracted.archiveSha256
}
const decision = input.conflictDecisions?.get(skill.id)
const serviceInput: SkillInstallServiceInput = {
...input,
expectedPackageDigest: skill.digest,
expectedPackageId: extracted.manifest.packageId,
expectedVersionId: extracted.manifest.versionId,
sourceBundleDigest: extracted.manifest.bundleDigest,
conflictResolution: decision === 'keep-local' ? undefined : decision
}
const localInput = skillInstallLocalInput(serviceInput)
const preview = await previewLocalExtractedSkillPackage(localInput, extractedSkill)
const isConflict = !['missing', 'unchanged', 'clean-update'].includes(
preview.currentState.kind
)
if (isConflict && decision !== 'replace-and-discard-local') {
results.push(conflictResult(skill, preview))
continue
}
await mkdir(wrapper, { recursive: true, mode: 0o700 })
const source = join(extracted.skillsDirectory, skill.name)
const target = join(wrapper, 'skill')
await (input.filesystem
? input.filesystem.rename(source, target)
: renameSkillPathWithWindowsRetry(source, target))
try {
await input.filesystem?.prepareExtractedSkill(target, manifest)
const result = await installSharedExtractedSkill(serviceInput, extractedSkill)
results.push(completedSkillResult({ skill, preview, result }))
} catch (error) {
const failure = skillInstallFailureFromError(error)
results.push({
skillId: skill.id,
name: skill.name,
digest: skill.digest,
status: failure?.category === 'cancelled' ? 'cancelled' : 'failed',
placements: [],
...(failure ? { errorCategory: failure.code, failure } : {})
})
}
}
return SkillBundleInstallResultSchema.parse({
operationId: input.operationId,
packageId: extracted.manifest.packageId,
versionId: extracted.manifest.versionId,
bundleDigest: extracted.manifest.bundleDigest,
status: bundleStatus(results),
skills: results
})
} finally {
await finishSkillExtractionRecovery(
stateDirectory,
recovery,
input.filesystem ?? nativeSkillInstallFilesystem
)
}
}
@@ -1,30 +0,0 @@
import { describe, expect, it } from 'vitest'
import type { SkillBundleInstallResult } from '../../shared/skill-bundle-install-contract'
import { summarizeSkillBundleObservation } from './skill-bundle-observability-summary'
describe('skill bundle observability summary', () => {
it('caps distinct error categories while preserving the aggregate count', () => {
const result: SkillBundleInstallResult = {
operationId: 'operation',
packageId: 'package',
versionId: 'version',
bundleDigest: 'a'.repeat(64),
status: 'failed',
skills: Array.from({ length: 40 }, (_, index) => ({
skillId: `skill-${index}`,
name: `skill-${index}`,
digest: 'b'.repeat(64),
status: 'failed' as const,
placements: [],
errorCategory: `skill-error-${index}`
}))
}
const summary = summarizeSkillBundleObservation(result)
expect(summary.errorCategories.size).toBe(32)
expect([...summary.errorCategories.values()].reduce((total, count) => total + count, 0)).toBe(
40
)
})
})
@@ -1,52 +0,0 @@
import type { SkillBundleInstallResult } from '../../shared/skill-bundle-install-contract'
const MAX_ERROR_CATEGORIES = 32
const SAFE_ERROR_CATEGORY = /^[a-z0-9][a-z0-9._-]{0,63}$/i
export type SkillBundleObservationSummary = {
attributes: Record<string, number>
errorCategories: Map<string, number>
}
function increment(attributes: Record<string, number>, key: string): void {
attributes[key] = (attributes[key] ?? 0) + 1
}
function recordErrorCategory(categories: Map<string, number>, value: string): void {
const category = SAFE_ERROR_CATEGORY.test(value) ? value : 'other'
const key =
categories.has(category) || (category !== 'other' && categories.size < MAX_ERROR_CATEGORIES - 1)
? category
: 'other'
categories.set(key, (categories.get(key) ?? 0) + 1)
}
export function summarizeSkillBundleObservation(
result: SkillBundleInstallResult
): SkillBundleObservationSummary {
const attributes: Record<string, number> = {
skillCount: result.skills.length,
placementCount: 0,
conflictCount: 0
}
const errorCategories = new Map<string, number>()
for (const skill of result.skills) {
increment(attributes, `${skill.status}SkillCount`)
if (skill.conflict) {
increment(attributes, 'conflictCount')
increment(attributes, `${skill.conflict.kind}ConflictCount`)
}
if (skill.errorCategory) {
recordErrorCategory(errorCategories, skill.errorCategory)
}
for (const placement of skill.placements) {
increment(attributes, 'placementCount')
increment(attributes, `${placement.topology}PlacementCount`)
increment(attributes, `${placement.status}PlacementCount`)
if (placement.errorCategory) {
recordErrorCategory(errorCategories, placement.errorCategory)
}
}
}
return { attributes, errorCategories }
}
@@ -1,65 +0,0 @@
import { open, mkdir, writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import { describe, it } from 'vitest'
import { createSkillBundleArchive } from './skill-bundle-creation'
import { installSkillBundle } from './skill-bundle-install-service'
const CHILD = process.env.ORCA_SKILL_BUNDLE_PROCESS_CHILD === '1'
async function createLargeSkill(root: string): Promise<string> {
const source = join(root, 'source')
await mkdir(source, { recursive: true })
await writeFile(
join(source, 'SKILL.md'),
'---\nname: crash-bundle-skill\ndescription: Process termination\n---\n\n# Bundle\n'
)
for (let index = 0; index < 7; index += 1) {
await writeFile(join(source, `payload-${index}.bin`), Buffer.alloc(4 * 1024 * 1024, index))
}
await writeFile(join(source, 'payload-final.bin'), Buffer.alloc(3 * 1024 * 1024, 0xff))
return source
}
async function markReady(path: string): Promise<void> {
const handle = await open(path, 'w', 0o600)
try {
await handle.writeFile(`${JSON.stringify({ pid: process.pid })}\n`)
await handle.sync()
} finally {
await handle.close()
}
}
describe.runIf(CHILD)('skill bundle process termination child', () => {
it('extracts a bundle until the parent terminates this process', async () => {
const root = process.env.ORCA_SKILL_BUNDLE_CRASH_ROOT
const marker = process.env.ORCA_SKILL_BUNDLE_CRASH_MARKER
if (!root || !marker) {
throw new Error('missing-bundle-crash-environment')
}
const source = await createLargeSkill(root)
const bundle = await createSkillBundleArchive({
sources: [{ sourceDirectory: source }],
archivePath: join(root, 'bundle.tar.gz'),
packageId: 'package_bundle_crash',
versionId: 'version_bundle_crash',
bundleName: 'crash-bundle'
})
await markReady(marker)
await installSkillBundle({
operationId: 'operation_bundle_crash',
archivePath: bundle.archivePath,
packageId: bundle.manifest.packageId,
versionId: bundle.manifest.versionId,
bundleDigest: bundle.manifest.bundleDigest,
selectedSkillIds: ['crash-bundle-skill'],
expectedArchiveSha256: bundle.archiveSha256,
scope: 'global',
homeDirectory: join(root, 'home'),
orcaStateDirectory: join(root, 'state'),
detectedProviders: [],
destinationIdentity: 'global:bundle-process-test',
hostIdentity: 'bundle-process-test'
})
})
})
@@ -1,175 +0,0 @@
import { spawn, type ChildProcess } from 'node:child_process'
import { createRequire } from 'node:module'
import { mkdtemp, readFile, readdir, rm, stat } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { dirname, join, resolve } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { recoverPendingSkillTransactions } from './skill-transaction-startup-recovery'
const RUN_REAL_PROCESS = process.env.ORCA_REAL_PROCESS_SKILL_TEST === '1'
const require = createRequire(import.meta.url)
const vitestBin = join(dirname(require.resolve('vitest/package.json')), 'vitest.mjs')
const childTest = resolve('src/main/skills/skill-bundle-process-termination-child.test.ts')
const roots: string[] = []
function boundedOutput(child: ChildProcess): { value: () => string } {
let output = ''
const append = (chunk: Buffer): void => {
output = `${output}${chunk.toString('utf8')}`.slice(-8_192)
}
child.stdout?.on('data', append)
child.stderr?.on('data', append)
return { value: () => output }
}
async function waitForPid(
path: string,
child: ChildProcess,
output: () => string
): Promise<number> {
const deadline = Date.now() + 30_000
while (Date.now() < deadline) {
const marker = await readFile(path, 'utf8').catch(() => '')
try {
const parsed: unknown = JSON.parse(marker)
const pid =
parsed && typeof parsed === 'object' && 'pid' in parsed
? (parsed as { pid?: unknown }).pid
: null
if (typeof pid === 'number' && Number.isInteger(pid) && pid > 0) {
return pid
}
} catch {
// The synced marker may not be fully visible on its first read.
}
if (child.exitCode !== null) {
throw new Error(`bundle-crash-child-exited-${child.exitCode}: ${output()}`)
}
await new Promise<void>((resolveWait) => setTimeout(resolveWait, 10))
}
throw new Error(`bundle-crash-marker-timeout: ${output()}`)
}
async function waitForActiveExtraction(stateDirectory: string): Promise<string> {
const directory = join(stateDirectory, 'extraction-journals')
const deadline = Date.now() + 10_000
while (Date.now() < deadline) {
const names = await readdir(directory).catch(() => [])
const journalName = names.find((name) => name.endsWith('.json'))
if (journalName) {
const journal = JSON.parse(await readFile(join(directory, journalName), 'utf8')) as {
extractionPath?: unknown
}
if (typeof journal.extractionPath === 'string') {
const entries = await readdir(journal.extractionPath).catch(() => [])
if (entries.length > 0) {
return journal.extractionPath
}
}
}
await new Promise<void>((resolveWait) => setTimeout(resolveWait, 5))
}
throw new Error('bundle-extraction-journal-timeout')
}
function processIsAlive(pid: number): boolean {
try {
process.kill(pid, 0)
return true
} catch (error) {
return (error as NodeJS.ErrnoException).code === 'EPERM'
}
}
async function waitForProcessExit(pid: number): Promise<void> {
const deadline = Date.now() + 5_000
while (Date.now() < deadline) {
if (!processIsAlive(pid)) {
return
}
await new Promise<void>((resolveWait) => setTimeout(resolveWait, 20))
}
throw new Error(`bundle-crash-process-still-running-${pid}`)
}
async function waitForChildExit(child: ChildProcess): Promise<void> {
if (child.exitCode !== null || child.signalCode !== null) {
return
}
await new Promise<void>((resolveExit, reject) => {
const timeout = setTimeout(
() => reject(new Error('bundle-crash-coordinator-kill-failed')),
5_000
)
child.once('exit', () => {
clearTimeout(timeout)
resolveExit()
})
})
}
async function startChild(root: string): Promise<{ child: ChildProcess; marker: string }> {
const marker = join(root, 'crash-ready')
const child = spawn(
process.execPath,
[
vitestBin,
'run',
'--config',
resolve('config/vitest.config.ts'),
'--pool=forks',
'--maxWorkers=1',
'--no-file-parallelism',
childTest
],
{
cwd: process.cwd(),
env: {
...process.env,
ORCA_REAL_PROCESS_SKILL_TEST: '0',
ORCA_SKILL_BUNDLE_PROCESS_CHILD: '1',
ORCA_SKILL_BUNDLE_CRASH_ROOT: root,
ORCA_SKILL_BUNDLE_CRASH_MARKER: marker
},
stdio: ['ignore', 'pipe', 'pipe'],
windowsHide: true
}
)
return { child, marker }
}
afterEach(async () => {
await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })))
})
describe.runIf(RUN_REAL_PROCESS)('skill bundle process termination recovery', () => {
it('removes a partially extracted bundle and its durable journal after process death', async () => {
const root = await mkdtemp(join(tmpdir(), 'orca-skill-bundle-process-crash-'))
roots.push(root)
const stateDirectory = join(root, 'state', 'skill-installs')
const { child, marker } = await startChild(root)
const output = boundedOutput(child)
const transactionPid = await waitForPid(marker, child, output.value)
const extractionPath = await waitForActiveExtraction(stateDirectory)
process.kill(transactionPid, 'SIGKILL')
await waitForProcessExit(transactionPid)
if (child.exitCode === null && child.signalCode === null) {
child.kill('SIGKILL')
}
await waitForChildExit(child)
expect(await stat(extractionPath)).toBeTruthy()
const report = await recoverPendingSkillTransactions(stateDirectory)
expect(report.failures).toEqual([])
expect(report.orphanedExtractionsRecovered).toBe(1)
await expect(stat(extractionPath)).rejects.toMatchObject({ code: 'ENOENT' })
await expect(readdir(join(stateDirectory, 'extraction-journals'))).resolves.toEqual([])
expect(
(await readdir(join(root, 'home', '.agents', 'skills')).catch(() => [])).filter((name) =>
name.includes('.orca-')
)
).toEqual([])
}, 45_000)
})
@@ -1,181 +0,0 @@
import { createHash } from 'node:crypto'
import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { SkillBundleInstallRequest } from '../../shared/skill-bundle-install-contract'
import { SKILL_PACKAGE_CONTENT_TYPE } from '../../shared/skill-package-manifest'
import type { IPtyProvider } from '../providers/pty-provider-contract'
import { installSkillBundleOnSshHost } from './skill-bundle-ssh-relay-service'
const roots: string[] = []
afterEach(async () => {
await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })))
})
async function userDataPath(): Promise<string> {
const root = await mkdtemp(join(tmpdir(), 'orca-bundle-ssh-client-test-'))
roots.push(root)
return root
}
function request(bytes: Buffer): SkillBundleInstallRequest {
return {
operationId: 'bundle-operation',
package: {
packageId: 'package_1',
versionId: 'version_1',
bundleDigest: 'a'.repeat(64),
archiveSha256: createHash('sha256').update(bytes).digest('hex'),
compressedBytes: bytes.length
},
selectedSkillIds: ['skill-1'],
ingress: {
kind: 'download-grant',
url: 'https://storage.googleapis.com/test/bundle.tar.gz',
expiresAt: new Date(Date.now() + 60_000).toISOString()
},
destination: { scope: 'global', executionTarget: { kind: 'host' } },
conflictDecisions: []
}
}
function result() {
return {
operationId: 'bundle-operation',
packageId: 'package_1',
versionId: 'version_1',
bundleDigest: 'a'.repeat(64),
status: 'complete' as const,
skills: []
}
}
describe('installSkillBundleOnSshHost', () => {
it('uses the additive method only when advertised by the SSH host', async () => {
const bytes = Buffer.from('private bundle archive')
const requestHostRpc = vi.fn(async (method: string) => {
if (method === 'relay.status') {
return { capabilities: ['skills.install.bundle.v1'] }
}
if (method === 'skills.installBundle') {
return result()
}
throw new Error(`unexpected method ${method}`)
})
await expect(
installSkillBundleOnSshHost({
provider: { requestHostRpc } as unknown as IPtyProvider,
userDataPath: await userDataPath(),
request: request(bytes),
requireHttps: true
})
).resolves.toEqual(result())
})
it('does not send an unknown method to an older SSH host', async () => {
const requestHostRpc = vi.fn(async () => ({ capabilities: ['skills.install.v1'] }))
await expect(
installSkillBundleOnSshHost({
provider: { requestHostRpc } as unknown as IPtyProvider,
userDataPath: await userDataPath(),
request: request(Buffer.from('archive')),
requireHttps: true
})
).rejects.toThrow('skill-bundle-ssh-update-required')
expect(requestHostRpc).toHaveBeenCalledOnce()
})
it('polls current-skill progress only when the SSH host advertises it', async () => {
const bytes = Buffer.from('private bundle archive')
const onProgress = vi.fn()
const progress = {
operationId: 'bundle-operation',
skillId: 'skill-1',
skillName: 'alpha',
skillIndex: 1,
skillCount: 30
}
const requestHostRpc = vi.fn(async (method: string) => {
if (method === 'relay.status') {
return {
capabilities: ['skills.install.bundle.v1', 'skills.install-progress.v1']
}
}
if (method === 'skills.getInstallProgress') {
return progress
}
if (method === 'skills.installBundle') {
await new Promise((resolve) => setTimeout(resolve, 0))
return result()
}
throw new Error(`unexpected method ${method}`)
})
await installSkillBundleOnSshHost({
provider: { requestHostRpc } as unknown as IPtyProvider,
userDataPath: await userDataPath(),
request: request(bytes),
requireHttps: true,
onProgress
})
expect(onProgress).toHaveBeenCalledWith(progress)
expect(requestHostRpc.mock.calls.map(([method]) => method)).toEqual([
'relay.status',
'skills.getInstallProgress',
'skills.installBundle'
])
})
it('falls back to client-mediated transfer after direct download fails', async () => {
const bytes = Buffer.from('private bundle archive')
const requestHostRpc = vi.fn(async (method: string, params: unknown) => {
if (method === 'relay.status') {
return { capabilities: ['skills.install.bundle.v1', 'skills.upload.v1'] }
}
if (method === 'skills.installBundle') {
const ingress = (params as { request: SkillBundleInstallRequest }).request.ingress
if (ingress.kind === 'download-grant') {
throw Object.assign(new Error('skill-download-transport-failed'), { code: -32000 })
}
return result()
}
if (method === 'skills.beginUpload') {
return { uploadId: 'upload_1', chunkBytes: 256 * 1024 }
}
if (method === 'skills.uploadChunk') {
const chunk = params as { offset: number; bytesBase64: string }
return {
acknowledgedOffset: chunk.offset + Buffer.from(chunk.bytesBase64, 'base64').length
}
}
return { ok: true }
})
await expect(
installSkillBundleOnSshHost({
provider: { requestHostRpc } as unknown as IPtyProvider,
userDataPath: await userDataPath(),
request: request(bytes),
requireHttps: true,
fetcher: vi.fn(
async () =>
new Response(bytes, { headers: { 'content-type': SKILL_PACKAGE_CONTENT_TYPE } })
) as typeof fetch
})
).resolves.toEqual(result())
expect(requestHostRpc.mock.calls.map(([method]) => method)).toEqual([
'relay.status',
'skills.installBundle',
'skills.beginUpload',
'skills.uploadChunk',
'skills.commitUpload',
'skills.installBundle',
'skills.cancelUpload'
])
})
})
@@ -1,130 +0,0 @@
import {
SkillBundleInstallProgressSchema,
SkillBundleInstallResultSchema,
type SkillBundleInstallProgress,
type SkillBundleInstallRequest,
type SkillBundleInstallResult
} from '../../shared/skill-bundle-install-contract'
import {
SKILL_BUNDLE_INSTALL_CAPABILITY,
SKILL_INSTALL_PROVIDERS_CAPABILITY,
SKILL_INSTALL_PROGRESS_CAPABILITY,
SKILL_UPLOAD_CAPABILITY
} from '../../shared/skill-install-capability'
import {
SKILL_SSH_RELAY_CANCEL_UPLOAD_METHOD,
SKILL_SSH_RELAY_GET_INSTALL_PROGRESS_METHOD,
SKILL_SSH_RELAY_INSTALL_BUNDLE_METHOD,
type SkillSshWorkspaceAuthority
} from '../../shared/skill-ssh-relay-contract'
import type { IPtyProvider } from '../providers/pty-provider-contract'
import {
SKILL_SSH_REQUEST_TIMEOUT_MS,
requireSkillSshRelayClient,
retryableSkillSshTransportError,
shouldUseSkillSshClientTransfer,
skillSshRelayCapabilities
} from './skill-ssh-relay-client'
import { transferSkillPackageToSshHost } from './skill-ssh-package-transfer'
import { retrySkillTransferRpc } from './skill-transfer-rpc-retry'
import { startSkillInstallProgressPolling } from './skill-install-progress-polling'
import { recordSkillCapabilityAbsence } from './skill-operation-observability'
export async function installSkillBundleOnSshHost(input: {
provider: IPtyProvider
userDataPath: string
request: SkillBundleInstallRequest
workspace?: SkillSshWorkspaceAuthority
requireHttps: boolean
signal?: AbortSignal
onProgress?: (progress: SkillBundleInstallProgress) => void
fetcher?: typeof fetch
}): Promise<SkillBundleInstallResult> {
const client = requireSkillSshRelayClient(input.provider)
const supported = await skillSshRelayCapabilities(client)
const request = input.request
if (request.providers !== undefined && !supported.includes(SKILL_INSTALL_PROVIDERS_CAPABILITY)) {
throw new Error('skill-bundle-ssh-update-required')
}
if (!supported.includes(SKILL_BUNDLE_INSTALL_CAPABILITY)) {
recordSkillCapabilityAbsence({
capability: SKILL_BUNDLE_INSTALL_CAPABILITY,
destination: 'global-ssh'
})
throw new Error('skill-bundle-ssh-update-required')
}
const stopProgress =
input.onProgress && supported.includes(SKILL_INSTALL_PROGRESS_CAPABILITY)
? startSkillInstallProgressPolling({
read: async () => {
const value = await client(
SKILL_SSH_RELAY_GET_INSTALL_PROGRESS_METHOD,
{ operationId: request.operationId },
{ timeoutMs: 2_000, signal: input.signal }
)
if (value === null) {
return null
}
const parsed = SkillBundleInstallProgressSchema.safeParse(value)
return parsed.success ? parsed.data : null
},
onProgress: input.onProgress
})
: null
try {
try {
return SkillBundleInstallResultSchema.parse(
await retrySkillTransferRpc({
signal: input.signal,
retryable: retryableSkillSshTransportError,
call: () =>
client(
SKILL_SSH_RELAY_INSTALL_BUNDLE_METHOD,
{ request: request, workspace: input.workspace },
{ timeoutMs: SKILL_SSH_REQUEST_TIMEOUT_MS, signal: input.signal }
)
})
)
} catch (error) {
if (
request.ingress.kind !== 'download-grant' ||
!shouldUseSkillSshClientTransfer(error, input.requireHttps)
) {
throw error
}
}
if (!supported.includes(SKILL_UPLOAD_CAPABILITY)) {
recordSkillCapabilityAbsence({
capability: SKILL_UPLOAD_CAPABILITY,
destination: 'global-ssh'
})
throw new Error('skill-bundle-ssh-download-unavailable')
}
return await retrySkillTransferRpc({
signal: input.signal,
retryable: retryableSkillSshTransportError,
call: async () => {
const uploadId = await transferSkillPackageToSshHost(client, input)
try {
return SkillBundleInstallResultSchema.parse(
await client(
SKILL_SSH_RELAY_INSTALL_BUNDLE_METHOD,
{
request: {
...request,
ingress: { kind: 'staged-upload', uploadId }
},
workspace: input.workspace
},
{ timeoutMs: SKILL_SSH_REQUEST_TIMEOUT_MS, signal: input.signal }
)
)
} finally {
await client(SKILL_SSH_RELAY_CANCEL_UPLOAD_METHOD, { uploadId }).catch(() => undefined)
}
}
})
} finally {
stopProgress?.()
}
}
@@ -1,21 +0,0 @@
import { describe, expect, it } from 'vitest'
import { runSkillCandidateTasks } from './skill-candidate-concurrency'
describe('runSkillCandidateTasks', () => {
it('bounds concurrency and preserves task order', async () => {
let active = 0
let maxActive = 0
const results = await runSkillCandidateTasks(
Array.from({ length: 12 }, (_, index) => async () => {
active += 1
maxActive = Math.max(maxActive, active)
await new Promise((resolve) => setTimeout(resolve, 12 - index))
active -= 1
return index
})
)
expect(maxActive).toBe(4)
expect(results).toEqual(Array.from({ length: 12 }, (_, index) => index))
})
})
@@ -1,113 +0,0 @@
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const mocks = vi.hoisted(() => ({
callRuntimeEnvironment: vi.fn(),
downloadSkillPackageGrant: vi.fn()
}))
vi.mock('../ipc/runtime-environment-transport-routing', () => ({
callRuntimeEnvironment: mocks.callRuntimeEnvironment
}))
vi.mock('./skill-package-download', () => ({
downloadSkillPackageGrant: mocks.downloadSkillPackageGrant
}))
import { transferSkillPackageToRuntime } from './skill-client-mediated-transfer'
describe('transferSkillPackageToRuntime cancellation', () => {
let root = ''
beforeEach(async () => {
root = await mkdtemp(join(tmpdir(), 'orca-skill-transfer-cancel-test-'))
mocks.callRuntimeEnvironment.mockReset()
mocks.downloadSkillPackageGrant.mockReset()
})
afterEach(async () => {
await rm(root, { recursive: true, force: true })
})
it('cancels a committed chunk session when the commit request is interrupted', async () => {
const bytes = Buffer.from('package-data')
const archivePath = join(root, 'package.tar.gz')
await writeFile(archivePath, bytes)
const downloadCleanup = vi.fn(async () => undefined)
const controller = new AbortController()
let commitStarted: () => void = () => {}
const commitPending = new Promise<void>((resolve) => {
commitStarted = resolve
})
mocks.downloadSkillPackageGrant.mockResolvedValue({ archivePath, cleanup: downloadCleanup })
mocks.callRuntimeEnvironment.mockImplementation((...args: unknown[]) => {
const method = args[2]
if (method === 'skills.beginUpload') {
return Promise.resolve({
id: 'rpc-begin',
ok: true,
result: { uploadId: 'upload-1', chunkBytes: 256 * 1024 },
_meta: { runtimeId: 'runtime-1' }
})
}
if (method === 'skills.uploadChunk') {
return Promise.resolve({
id: 'rpc-chunk',
ok: true,
result: { acknowledgedOffset: bytes.length },
_meta: { runtimeId: 'runtime-1' }
})
}
if (method === 'skills.commitUpload') {
const options = args[7] as { signal?: AbortSignal }
return new Promise((_resolve, reject) => {
options.signal?.addEventListener('abort', () => reject(options.signal?.reason), {
once: true
})
commitStarted()
})
}
return Promise.resolve({
id: 'rpc-cancel',
ok: true,
result: undefined,
_meta: { runtimeId: 'runtime-1' }
})
})
const pending = transferSkillPackageToRuntime({
userDataPath: root,
environmentId: 'environment-1',
transferId: 'operation-1',
package: {
packageId: 'package-1',
versionId: 'version-1',
packageDigest: 'a'.repeat(64),
archiveSha256: 'b'.repeat(64),
compressedBytes: bytes.length
},
grant: {
url: 'https://storage.googleapis.com/bucket/package.tar.gz',
expiresAt: '2099-01-01T00:00:00.000Z'
},
requireHttps: true,
signal: controller.signal
})
await commitPending
controller.abort()
await expect(pending).rejects.toThrow('skill-install-cancelled')
expect(downloadCleanup).toHaveBeenCalledOnce()
expect(
mocks.callRuntimeEnvironment.mock.calls.filter((call) => call[2] === 'skills.commitUpload')
).toHaveLength(1)
expect(mocks.callRuntimeEnvironment).toHaveBeenCalledWith(
root,
'environment-1',
'skills.cancelUpload',
{ uploadId: 'upload-1' },
15_000
)
})
})
@@ -1,455 +0,0 @@
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import { tmpdir } from 'node:os'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const mocks = vi.hoisted(() => ({
callRuntimeEnvironment: vi.fn(),
downloadSkillPackageGrant: vi.fn()
}))
vi.mock('../ipc/runtime-environment-transport-routing', () => ({
callRuntimeEnvironment: mocks.callRuntimeEnvironment
}))
vi.mock('./skill-package-download', () => ({
downloadSkillPackageGrant: mocks.downloadSkillPackageGrant
}))
import { transferSkillPackageToRuntime } from './skill-client-mediated-transfer'
describe('transferSkillPackageToRuntime', () => {
let root = ''
beforeEach(async () => {
root = await mkdtemp(join(tmpdir(), 'orca-skill-transfer-test-'))
mocks.callRuntimeEnvironment.mockReset()
mocks.downloadSkillPackageGrant.mockReset()
})
afterEach(async () => {
await rm(root, { recursive: true, force: true })
})
it('cleans the local package after bounded begin retries', async () => {
const archivePath = join(root, 'package.tar.gz')
await writeFile(archivePath, Buffer.from('package-data'))
const cleanup = vi.fn(async () => undefined)
mocks.downloadSkillPackageGrant.mockResolvedValue({ archivePath, cleanup })
mocks.callRuntimeEnvironment.mockRejectedValue(new Error('connection dropped'))
await expect(
transferSkillPackageToRuntime({
userDataPath: root,
environmentId: 'environment-1',
transferId: 'operation-1',
package: {
packageId: 'package-1',
versionId: 'version-1',
packageDigest: 'a'.repeat(64),
archiveSha256: 'b'.repeat(64),
compressedBytes: 12
},
grant: {
url: 'https://storage.googleapis.com/bucket/package.tar.gz',
expiresAt: '2099-01-01T00:00:00.000Z'
},
requireHttps: true
})
).rejects.toThrow('connection dropped')
expect(cleanup).toHaveBeenCalledOnce()
expect(mocks.callRuntimeEnvironment).toHaveBeenCalledTimes(3)
})
it('reuses the operation identity and resumes an acknowledged upload', async () => {
const archivePath = join(root, 'package.tar.gz')
const bytes = Buffer.from('package-data')
await writeFile(archivePath, bytes)
const cleanup = vi.fn(async () => undefined)
mocks.downloadSkillPackageGrant.mockResolvedValue({ archivePath, cleanup })
const beginRequests: unknown[] = []
let beginAttempts = 0
mocks.callRuntimeEnvironment.mockImplementation(
async (_userData: string, _environment: string, method: string, params: unknown) => {
if (method === 'skills.beginUpload') {
beginRequests.push(params)
beginAttempts += 1
if (beginAttempts === 1) {
throw new Error('connection dropped after receiver resumed upload')
}
return {
id: 'rpc-begin',
ok: true,
result: { uploadId: 'upload-1', chunkBytes: 256 * 1024, acknowledgedOffset: 4 },
_meta: { runtimeId: 'runtime-1' }
}
}
if (method === 'skills.uploadChunk') {
const chunk = params as { offset: number; bytesBase64: string }
expect(chunk.offset).toBe(4)
expect(Buffer.from(chunk.bytesBase64, 'base64')).toEqual(bytes.subarray(4))
return {
id: 'rpc-chunk',
ok: true,
result: { acknowledgedOffset: bytes.length },
_meta: { runtimeId: 'runtime-1' }
}
}
return {
id: 'rpc-success',
ok: true,
result: { uploadId: 'upload-1' },
_meta: { runtimeId: 'runtime-1' }
}
}
)
const transferred = await transferSkillPackageToRuntime({
userDataPath: root,
environmentId: 'environment-1',
transferId: 'operation-1',
package: {
packageId: 'package-1',
versionId: 'version-1',
packageDigest: 'a'.repeat(64),
archiveSha256: 'b'.repeat(64),
compressedBytes: bytes.length
},
grant: {
url: 'https://storage.googleapis.com/bucket/package.tar.gz',
expiresAt: '2099-01-01T00:00:00.000Z'
},
requireHttps: true
})
await transferred.cleanup()
expect(beginRequests).toEqual([
expect.objectContaining({ transferId: 'operation-1' }),
expect.objectContaining({ transferId: 'operation-1' })
])
expect(cleanup).toHaveBeenCalledOnce()
})
it('rejects an invalid acknowledgement and cancels the remote session', async () => {
const archivePath = join(root, 'package.tar.gz')
await writeFile(archivePath, Buffer.from('package-data'))
const downloadCleanup = vi.fn(async () => undefined)
mocks.downloadSkillPackageGrant.mockResolvedValue({ archivePath, cleanup: downloadCleanup })
mocks.callRuntimeEnvironment.mockImplementation(
async (_userData: string, _environment: string, method: string) => {
if (method === 'skills.beginUpload') {
return {
id: 'rpc-1',
ok: true,
result: { uploadId: 'upload-1', chunkBytes: 256 * 1024 },
_meta: { runtimeId: 'runtime-1' }
}
}
if (method === 'skills.uploadChunk') {
return {
id: 'rpc-2',
ok: true,
result: { acknowledgedOffset: 1 },
_meta: { runtimeId: 'runtime-1' }
}
}
return {
id: 'rpc-3',
ok: true,
result: undefined,
_meta: { runtimeId: 'runtime-1' }
}
}
)
await expect(
transferSkillPackageToRuntime({
userDataPath: root,
environmentId: 'environment-1',
transferId: 'operation-1',
package: {
packageId: 'package-1',
versionId: 'version-1',
packageDigest: 'a'.repeat(64),
archiveSha256: 'b'.repeat(64),
compressedBytes: 12
},
grant: {
url: 'https://storage.googleapis.com/bucket/package.tar.gz',
expiresAt: '2099-01-01T00:00:00.000Z'
},
requireHttps: true
})
).rejects.toThrow('skill-transfer-ack-invalid')
expect(downloadCleanup).toHaveBeenCalledOnce()
expect(mocks.callRuntimeEnvironment).toHaveBeenCalledWith(
root,
'environment-1',
'skills.cancelUpload',
{ uploadId: 'upload-1' },
15_000
)
})
it('cancels the remote upload when commit fails', async () => {
const archivePath = join(root, 'package.tar.gz')
await writeFile(archivePath, Buffer.from('package-data'))
const downloadCleanup = vi.fn(async () => undefined)
mocks.downloadSkillPackageGrant.mockResolvedValue({ archivePath, cleanup: downloadCleanup })
mocks.callRuntimeEnvironment.mockImplementation(
async (_userData: string, _environment: string, method: string) => {
if (method === 'skills.beginUpload') {
return {
id: 'rpc-1',
ok: true,
result: { uploadId: 'upload-1', chunkBytes: 256 * 1024 },
_meta: { runtimeId: 'runtime-1' }
}
}
if (method === 'skills.uploadChunk') {
return {
id: 'rpc-2',
ok: true,
result: { acknowledgedOffset: 12 },
_meta: { runtimeId: 'runtime-1' }
}
}
if (method === 'skills.commitUpload') {
return {
id: 'rpc-3',
ok: false,
error: { code: 'skill-upload-commit-failed', message: 'commit failed' },
_meta: { runtimeId: 'runtime-1' }
}
}
return {
id: 'rpc-4',
ok: true,
result: undefined,
_meta: { runtimeId: 'runtime-1' }
}
}
)
await expect(
transferSkillPackageToRuntime({
userDataPath: root,
environmentId: 'environment-1',
transferId: 'operation-1',
package: {
packageId: 'package-1',
versionId: 'version-1',
packageDigest: 'a'.repeat(64),
archiveSha256: 'b'.repeat(64),
compressedBytes: 12
},
grant: {
url: 'https://storage.googleapis.com/bucket/package.tar.gz',
expiresAt: '2099-01-01T00:00:00.000Z'
},
requireHttps: true
})
).rejects.toThrow('skill-transfer-remote-skill-upload-commit-failed')
expect(downloadCleanup).toHaveBeenCalledOnce()
expect(
mocks.callRuntimeEnvironment.mock.calls.filter((call) => call[2] === 'skills.commitUpload')
).toHaveLength(1)
expect(mocks.callRuntimeEnvironment).toHaveBeenCalledWith(
root,
'environment-1',
'skills.cancelUpload',
{ uploadId: 'upload-1' },
15_000
)
})
it('cancels the session after bounded chunk retry exhaustion', async () => {
const archivePath = join(root, 'package.tar.gz')
await writeFile(archivePath, Buffer.from('package-data'))
const cleanup = vi.fn(async () => undefined)
mocks.downloadSkillPackageGrant.mockResolvedValue({ archivePath, cleanup })
mocks.callRuntimeEnvironment.mockImplementation(
async (_userData: string, _environment: string, method: string) => {
if (method === 'skills.beginUpload') {
return {
id: 'rpc-begin',
ok: true,
result: { uploadId: 'upload-1', chunkBytes: 256 * 1024 },
_meta: { runtimeId: 'runtime-1' }
}
}
if (method === 'skills.uploadChunk') {
throw new Error('connection dropped')
}
return {
id: 'rpc-cancel',
ok: true,
result: undefined,
_meta: { runtimeId: 'runtime-1' }
}
}
)
await expect(
transferSkillPackageToRuntime({
userDataPath: root,
environmentId: 'environment-1',
transferId: 'operation-1',
package: {
packageId: 'package-1',
versionId: 'version-1',
packageDigest: 'a'.repeat(64),
archiveSha256: 'b'.repeat(64),
compressedBytes: 12
},
grant: {
url: 'https://storage.googleapis.com/bucket/package.tar.gz',
expiresAt: '2099-01-01T00:00:00.000Z'
},
requireHttps: true
})
).rejects.toThrow('connection dropped')
expect(
mocks.callRuntimeEnvironment.mock.calls.filter((call) => call[2] === 'skills.uploadChunk')
).toHaveLength(3)
expect(
mocks.callRuntimeEnvironment.mock.calls.filter((call) => call[2] === 'skills.cancelUpload')
).toHaveLength(1)
expect(cleanup).toHaveBeenCalledOnce()
})
it('retries the identical chunk and idempotent commit after lost responses', async () => {
const archivePath = join(root, 'package.tar.gz')
await writeFile(archivePath, Buffer.from('package-data'))
const cleanup = vi.fn(async () => undefined)
mocks.downloadSkillPackageGrant.mockResolvedValue({ archivePath, cleanup })
let chunkAttempts = 0
let commitAttempts = 0
mocks.callRuntimeEnvironment.mockImplementation(
async (_userData: string, _environment: string, method: string, params: unknown) => {
if (method === 'skills.beginUpload') {
return {
id: 'rpc-begin',
ok: true,
result: { uploadId: 'upload-1', chunkBytes: 256 * 1024 },
_meta: { runtimeId: 'runtime-1' }
}
}
if (method === 'skills.uploadChunk') {
chunkAttempts += 1
if (chunkAttempts === 1) {
throw new Error('connection dropped after receiver write')
}
return {
id: 'rpc-chunk',
ok: true,
result: { acknowledgedOffset: 12 },
_meta: { runtimeId: 'runtime-1' }
}
}
if (method === 'skills.commitUpload') {
commitAttempts += 1
if (commitAttempts === 1) {
throw new Error('connection dropped after receiver commit')
}
return {
id: 'rpc-commit',
ok: true,
result: { uploadId: 'upload-1' },
_meta: { runtimeId: 'runtime-1' }
}
}
expect(method).toBe('skills.cancelUpload')
expect(params).toEqual({ uploadId: 'upload-1' })
return {
id: 'rpc-cancel',
ok: true,
result: undefined,
_meta: { runtimeId: 'runtime-1' }
}
}
)
const transferred = await transferSkillPackageToRuntime({
userDataPath: root,
environmentId: 'environment-1',
transferId: 'operation-1',
package: {
packageId: 'package-1',
versionId: 'version-1',
packageDigest: 'a'.repeat(64),
archiveSha256: 'b'.repeat(64),
compressedBytes: 12
},
grant: {
url: 'https://storage.googleapis.com/bucket/package.tar.gz',
expiresAt: '2099-01-01T00:00:00.000Z'
},
requireHttps: true
})
await transferred.cleanup()
expect(chunkAttempts).toBe(2)
expect(commitAttempts).toBe(2)
expect(cleanup).toHaveBeenCalledOnce()
})
it('cancels the begun session when cancellation arrives before chunking', async () => {
const archivePath = join(root, 'package.tar.gz')
await writeFile(archivePath, Buffer.from('package-data'))
const cleanup = vi.fn(async () => undefined)
const controller = new AbortController()
mocks.downloadSkillPackageGrant.mockResolvedValue({ archivePath, cleanup })
mocks.callRuntimeEnvironment.mockImplementation(
async (_userData: string, _environment: string, method: string) => {
if (method === 'skills.beginUpload') {
controller.abort()
return {
id: 'rpc-begin',
ok: true,
result: { uploadId: 'upload-1', chunkBytes: 256 * 1024 },
_meta: { runtimeId: 'runtime-1' }
}
}
return {
id: 'rpc-cancel',
ok: true,
result: undefined,
_meta: { runtimeId: 'runtime-1' }
}
}
)
await expect(
transferSkillPackageToRuntime({
userDataPath: root,
environmentId: 'environment-1',
transferId: 'operation-1',
package: {
packageId: 'package-1',
versionId: 'version-1',
packageDigest: 'a'.repeat(64),
archiveSha256: 'b'.repeat(64),
compressedBytes: 12
},
grant: {
url: 'https://storage.googleapis.com/bucket/package.tar.gz',
expiresAt: '2099-01-01T00:00:00.000Z'
},
requireHttps: true,
signal: controller.signal
})
).rejects.toThrow('skill-install-cancelled')
expect(cleanup).toHaveBeenCalledOnce()
expect(mocks.callRuntimeEnvironment).toHaveBeenCalledWith(
root,
'environment-1',
'skills.cancelUpload',
{ uploadId: 'upload-1' },
15_000
)
})
})
@@ -1,191 +0,0 @@
import { open } from 'node:fs/promises'
import { join } from 'node:path'
import type { RuntimeRpcResponse } from '../../shared/runtime-rpc-envelope'
import type { SkillPackageIdentity } from '../../shared/skill-install-contract'
import type { SkillBundlePackageIdentity } from '../../shared/skill-bundle-install-contract'
import {
SKILL_UPLOAD_CHUNK_MAX_BYTES,
SkillUploadBeginResultSchema
} from '../../shared/skill-upload-session-contract'
import { callRuntimeEnvironment } from '../ipc/runtime-environment-transport-routing'
import { downloadSkillPackageGrant } from './skill-package-download'
import { startSkillPhaseOperation } from './skill-operation-observability'
import { retrySkillTransferRpc, throwIfSkillTransferCancelled } from './skill-transfer-rpc-retry'
const REMOTE_TRANSFER_TIMEOUT_MS = 5 * 60_000
const REMOTE_TRANSFER_CLEANUP_TIMEOUT_MS = 15_000
function allowedOrigins(allowConfiguredOrigins: boolean): string[] {
const origins = ['https://storage.googleapis.com']
if (allowConfiguredOrigins && process.env.ORCA_SKILL_PACKAGE_DOWNLOAD_ORIGINS) {
origins.push(
...process.env.ORCA_SKILL_PACKAGE_DOWNLOAD_ORIGINS.split(',')
.map((origin) => origin.trim())
.filter(Boolean)
)
}
return [...new Set(origins)]
}
function retryableRemoteTransferError(error: unknown): boolean {
return !(error instanceof Error && error.message.startsWith('skill-transfer-remote-'))
}
async function remoteCall<T>(
userDataPath: string,
environmentId: string,
method: string,
params: unknown,
signal?: AbortSignal,
timeoutMs = REMOTE_TRANSFER_TIMEOUT_MS
): Promise<T> {
const args = [userDataPath, environmentId, method, params, timeoutMs] as const
const response = (await (signal
? callRuntimeEnvironment(...args, undefined, undefined, { signal })
: callRuntimeEnvironment(...args))) as RuntimeRpcResponse<T>
if (response.ok !== true) {
throw new Error(`skill-transfer-remote-${response.error.code}`)
}
return response.result
}
type SkillClientTransferInput = {
userDataPath: string
environmentId: string
transferId: string
package: SkillPackageIdentity | SkillBundlePackageIdentity
grant: { url: string; expiresAt: string }
requireHttps: boolean
signal?: AbortSignal
}
async function transferSkillPackageToRuntimeUnobserved(
input: SkillClientTransferInput
): Promise<{ uploadId: string; cleanup(): Promise<void> }> {
const downloaded = await downloadSkillPackageGrant({
url: input.grant.url,
expiresAt: input.grant.expiresAt,
expectedArchiveSha256: input.package.archiveSha256,
expectedCompressedBytes: input.package.compressedBytes,
temporaryRoot: join(input.userDataPath, 'skill-installs', 'client-transfers'),
allowedOrigins: allowedOrigins(!input.requireHttps),
requireHttps: input.requireHttps,
signal: input.signal
})
let uploadId: string | null = null
try {
const begun = SkillUploadBeginResultSchema.parse(
await retrySkillTransferRpc({
signal: input.signal,
retryable: retryableRemoteTransferError,
checkCancellationAfterSuccess: false,
call: () =>
remoteCall(
input.userDataPath,
input.environmentId,
'skills.beginUpload',
{
package: input.package,
transferId: input.transferId
},
input.signal
)
})
)
uploadId = begun.uploadId
throwIfSkillTransferCancelled(input.signal)
const chunkBytes = Math.min(begun.chunkBytes, SKILL_UPLOAD_CHUNK_MAX_BYTES)
if (begun.acknowledgedOffset > input.package.compressedBytes) {
throw new Error('skill-transfer-offset-invalid')
}
if (!Number.isInteger(chunkBytes) || chunkBytes < 1) {
throw new Error('skill-transfer-chunk-size-invalid')
}
const handle = await open(downloaded.archivePath, 'r')
try {
let offset = begun.acknowledgedOffset
while (offset < input.package.compressedBytes) {
const bytes = Buffer.alloc(Math.min(chunkBytes, input.package.compressedBytes - offset))
const read = await handle.read(bytes, 0, bytes.length, offset)
if (read.bytesRead !== bytes.length) {
throw new Error('skill-transfer-source-changed')
}
const acknowledged = await retrySkillTransferRpc({
signal: input.signal,
retryable: retryableRemoteTransferError,
call: () =>
remoteCall<{ acknowledgedOffset: number }>(
input.userDataPath,
input.environmentId,
'skills.uploadChunk',
{ uploadId, offset, bytesBase64: bytes.toString('base64') },
input.signal
)
})
if (acknowledged.acknowledgedOffset !== offset + bytes.length) {
throw new Error('skill-transfer-ack-invalid')
}
offset = acknowledged.acknowledgedOffset
}
} finally {
await handle.close()
}
await retrySkillTransferRpc({
signal: input.signal,
retryable: retryableRemoteTransferError,
call: () =>
remoteCall(
input.userDataPath,
input.environmentId,
'skills.commitUpload',
{ uploadId },
input.signal
)
})
const committedId = uploadId
uploadId = null
return {
uploadId: committedId,
cleanup: () =>
remoteCall(
input.userDataPath,
input.environmentId,
'skills.cancelUpload',
{ uploadId: committedId },
undefined,
REMOTE_TRANSFER_CLEANUP_TIMEOUT_MS
)
}
} finally {
await downloaded.cleanup()
if (uploadId) {
await remoteCall(
input.userDataPath,
input.environmentId,
'skills.cancelUpload',
{ uploadId },
undefined,
REMOTE_TRANSFER_CLEANUP_TIMEOUT_MS
).catch(() => undefined)
}
}
}
export async function transferSkillPackageToRuntime(
input: SkillClientTransferInput
): Promise<{ uploadId: string; cleanup(): Promise<void> }> {
const operation = startSkillPhaseOperation({
phase: 'transfer',
transport: 'runtime-rpc',
destination: 'remote-runtime',
compressedBytes: input.package.compressedBytes
})
try {
const transfer = await transferSkillPackageToRuntimeUnobserved(input)
operation.complete({ status: 'complete', compressedBytes: input.package.compressedBytes })
return transfer
} catch (error) {
operation.fail(error)
throw error
}
}
-60
View File
@@ -1,60 +0,0 @@
import type { SkillCloudOperation, SkillCloudOptions } from '../../shared/skill-cloud-contract'
import { ensureActiveOrcaProfile } from '../orca-profiles/profile-index-store'
import { getOrcaCloudAuthConfig } from '../orca-profiles/profile-cloud-auth-config'
import { runWithFreshOrcaCloudSession } from '../orca-profiles/profile-cloud-session-refresh'
import {
allowsArtifactCloudAuthOverride,
resolveArtifactCloudApiUrl
} from '../artifacts/artifact-cloud-config'
export async function runSkillCloudOperation<T>(input: {
userDataPath: string
options: SkillCloudOptions
operation(token: string, apiUrl: string): Promise<T>
}): Promise<SkillCloudOperation<T>> {
const apiUrl = resolveArtifactCloudApiUrl(input.options.apiUrl)
const active = ensureActiveOrcaProfile(input.userDataPath)
const stamp = {
profileId: active.profile.id,
userId: active.profile.cloud?.userId,
cloudProfileId: active.profile.cloud?.cloudProfileId,
organizationId: active.profile.cloud?.activeOrgId ?? ''
}
const assertCurrent = () => {
const current = ensureActiveOrcaProfile(input.userDataPath)
if (
current.profile.id !== stamp.profileId ||
current.profile.cloud?.userId !== stamp.userId ||
current.profile.cloud?.cloudProfileId !== stamp.cloudProfileId ||
(current.profile.cloud?.activeOrgId ?? '') !== stamp.organizationId
) {
throw new Error('The signed-in Orca account changed during the skill request.')
}
}
const override = input.options.authToken?.trim() || process.env.ORCA_CLOUD_AUTH_TOKEN?.trim()
if (override) {
if (!allowsArtifactCloudAuthOverride()) {
throw new Error('Skill authentication overrides are available only in development builds.')
}
const value = await input.operation(override, apiUrl)
assertCurrent()
return { status: 'ok', value }
}
const config = getOrcaCloudAuthConfig()
if (!config.configured) {
return { status: 'unconfigured', message: config.setupMessage }
}
const result = await runWithFreshOrcaCloudSession(
config.config,
active,
input.userDataPath,
async (session) => {
const value = await input.operation(session.accessToken, apiUrl)
assertCurrent()
return value
}
)
return result.status === 'ok'
? { status: 'ok', value: result.value }
: { status: 'reconnect-required' }
}
-31
View File
@@ -1,31 +0,0 @@
const DEFAULT_SKILL_CLOUD_TIMEOUT_MS = 60_000
export function createSkillCloudDeadline(input: {
signal?: AbortSignal
timeoutMs?: number
timeoutMessage: string
}): {
signal: AbortSignal
cleanup(): void
} {
const timeoutMs = input.timeoutMs ?? DEFAULT_SKILL_CLOUD_TIMEOUT_MS
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1) {
throw new Error(`${input.timeoutMessage}-invalid`)
}
const controller = new AbortController()
const forwardAbort = (): void => controller.abort(input.signal?.reason)
if (input.signal?.aborted) {
forwardAbort()
} else {
input.signal?.addEventListener('abort', forwardAbort, { once: true })
}
const timeout = setTimeout(() => controller.abort(new Error(input.timeoutMessage)), timeoutMs)
timeout.unref()
return {
signal: controller.signal,
cleanup: () => {
clearTimeout(timeout)
input.signal?.removeEventListener('abort', forwardAbort)
}
}
}
@@ -1,176 +0,0 @@
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { uploadSkillPackageToSignedPolicy } from './skill-cloud-direct-upload'
const roots: string[] = []
afterEach(async () => {
vi.useRealTimers()
await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })))
})
describe('uploadSkillPackageToSignedPolicy', () => {
it('allows a slow progressing upload to continue beyond one minute', async () => {
vi.useFakeTimers()
const root = await mkdtemp(join(tmpdir(), 'orca-skill-cloud-upload-'))
roots.push(root)
const archivePath = join(root, 'package.tar.gz')
const archive = Buffer.alloc(3 * 64 * 1024, 0x61)
await writeFile(archivePath, archive)
let markStarted!: () => void
const started = new Promise<void>((resolve) => {
markStarted = resolve
})
let chunksSeen = 0
const chunkWaiters = new Set<() => void>()
const waitForChunk = async (count: number): Promise<void> => {
if (chunksSeen >= count) {
return
}
await new Promise<void>((resolve) => {
const check = (): void => {
if (chunksSeen >= count) {
chunkWaiters.delete(check)
resolve()
}
}
chunkWaiters.add(check)
})
}
const fetcher = vi.fn(async (_url: URL | RequestInfo, init?: RequestInit) => {
markStarted()
const body = init?.body as unknown as AsyncIterable<Buffer>
for await (const _chunk of body) {
chunksSeen += 1
chunkWaiters.forEach((notify) => notify())
await new Promise<void>((resolve) => setTimeout(resolve, 30_000))
}
return new Response(null, { status: 204 })
})
const progress = vi.fn()
const upload = uploadSkillPackageToSignedPolicy({
policy: {
url: 'https://storage.googleapis.com/upload',
fields: {},
expiresAt: new Date(Date.now() + 10 * 60_000).toISOString()
},
archivePath,
expectedBytes: archive.length,
fetcher: fetcher as typeof fetch,
onProgress: progress
})
await started
await waitForChunk(1)
await vi.advanceTimersByTimeAsync(30_000)
await waitForChunk(2)
await vi.advanceTimersByTimeAsync(30_000)
await waitForChunk(3)
await vi.advanceTimersByTimeAsync(1_000)
expect(progress).toHaveBeenCalled()
expect(fetcher.mock.calls[0]?.[1]?.signal?.aborted).toBe(false)
await vi.advanceTimersByTimeAsync(29_000)
await waitForChunk(4)
await vi.advanceTimersByTimeAsync(30_000)
await waitForChunk(5)
await vi.advanceTimersByTimeAsync(30_000)
await expect(upload).resolves.toBeUndefined()
})
it('streams exact bytes with policy fields and bounded progress', async () => {
const root = await mkdtemp(join(tmpdir(), 'orca-skill-cloud-upload-'))
roots.push(root)
const archivePath = join(root, 'package.tar.gz')
const archive = Buffer.from('private-package-bytes')
await writeFile(archivePath, archive)
let uploaded = Buffer.alloc(0)
const fetcher = vi.fn(async (_url: URL | RequestInfo, init?: RequestInit) => {
const chunks: Buffer[] = []
const body = init?.body as unknown as AsyncIterable<Buffer>
for await (const chunk of body) {
chunks.push(Buffer.from(chunk))
}
uploaded = Buffer.concat(chunks)
expect(Number(init?.headers && new Headers(init.headers).get('content-length'))).toBe(
uploaded.length
)
return new Response(null, { status: 204 })
}) as typeof fetch
const progress = vi.fn()
await uploadSkillPackageToSignedPolicy({
policy: {
url: 'https://storage.googleapis.com/upload',
fields: { key: 'uploads/private/package.tar.gz', policy: 'opaque-policy' }
},
archivePath,
expectedBytes: archive.length,
fetcher,
onProgress: progress
})
expect(uploaded.includes(archive)).toBe(true)
expect(uploaded.toString('utf8')).toContain('opaque-policy')
expect(progress).toHaveBeenLastCalledWith(archive.length)
})
it('rejects insecure destinations and source drift before upload', async () => {
const root = await mkdtemp(join(tmpdir(), 'orca-skill-cloud-upload-'))
roots.push(root)
const archivePath = join(root, 'package.tar.gz')
await writeFile(archivePath, 'bytes')
await expect(
uploadSkillPackageToSignedPolicy({
policy: { url: 'http://storage.test/upload', fields: {} },
archivePath,
expectedBytes: 5
})
).rejects.toThrow('skill-cloud-upload-url-invalid')
await expect(
uploadSkillPackageToSignedPolicy({
policy: { url: 'https://storage.test/upload', fields: {} },
archivePath,
expectedBytes: 4
})
).rejects.toThrow('skill-cloud-upload-source-changed')
await expect(
uploadSkillPackageToSignedPolicy({
policy: {
url: 'https://storage.test/upload',
fields: {},
expiresAt: 'not-a-date'
},
archivePath,
expectedBytes: 5
})
).rejects.toThrow('skill-cloud-upload-policy-expiry-invalid')
})
it('bounds a stalled signed upload even without a caller-owned signal', async () => {
const root = await mkdtemp(join(tmpdir(), 'orca-skill-cloud-upload-'))
roots.push(root)
const archivePath = join(root, 'package.tar.gz')
await writeFile(archivePath, 'bytes')
const fetcher = vi.fn(
async (_url: URL | RequestInfo, init?: RequestInit) =>
await new Promise<Response>((_resolve, reject) => {
init?.signal?.addEventListener('abort', () => reject(init.signal?.reason), { once: true })
})
) as typeof fetch
const upload = uploadSkillPackageToSignedPolicy({
policy: { url: 'https://storage.googleapis.com/upload', fields: {} },
archivePath,
expectedBytes: 5,
fetcher,
timeoutMs: 25
})
const startedAt = Date.now()
await expect(upload).rejects.toThrow('skill-cloud-upload-timeout')
expect(Date.now() - startedAt).toBeLessThan(1_000)
})
})
@@ -1,118 +0,0 @@
import { randomBytes } from 'node:crypto'
import { createReadStream } from 'node:fs'
import { stat } from 'node:fs/promises'
import { Readable } from 'node:stream'
import { createSkillCloudDeadline } from './skill-cloud-deadline'
type SignedPostPolicy = {
url: string
fields: Record<string, string>
expiresAt?: string
}
const DEFAULT_UPLOAD_TIMEOUT_MS = 15 * 60_000
function resolveUploadTimeoutMs(policy: SignedPostPolicy, timeoutMs?: number): number {
if (timeoutMs !== undefined) {
return timeoutMs
}
if (!policy.expiresAt) {
return DEFAULT_UPLOAD_TIMEOUT_MS
}
const expiresAt = Date.parse(policy.expiresAt)
if (!Number.isFinite(expiresAt)) {
throw new Error('skill-cloud-upload-policy-expiry-invalid')
}
return Math.max(1, Math.min(DEFAULT_UPLOAD_TIMEOUT_MS, expiresAt - Date.now()))
}
function quoted(value: string): string {
return value
.replaceAll('\\', '\\\\')
.replaceAll('"', '\\"')
.replaceAll('\r', '')
.replaceAll('\n', '')
}
function fieldBytes(boundary: string, name: string, value: string): Buffer {
return Buffer.from(
`--${boundary}\r\nContent-Disposition: form-data; name="${quoted(name)}"\r\n\r\n${value}\r\n`,
'utf8'
)
}
export async function uploadSkillPackageToSignedPolicy(input: {
policy: SignedPostPolicy
archivePath: string
expectedBytes: number
signal?: AbortSignal
onProgress?: (bytesSent: number) => void
fetcher?: typeof fetch
timeoutMs?: number
}): Promise<void> {
const policyUrl = new URL(input.policy.url)
if (policyUrl.protocol !== 'https:' || policyUrl.username || policyUrl.password) {
throw new Error('skill-cloud-upload-url-invalid')
}
const archive = await stat(input.archivePath)
if (!archive.isFile() || archive.size !== input.expectedBytes) {
throw new Error('skill-cloud-upload-source-changed')
}
const boundary = `orca-skill-${randomBytes(18).toString('hex')}`
const fields = Object.entries(input.policy.fields).map(([name, value]) =>
fieldBytes(boundary, name, value)
)
const fileHeader = Buffer.from(
`--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="package.tar.gz"\r\nContent-Type: application/vnd.orca.skill+tar+gzip\r\n\r\n`,
'utf8'
)
const ending = Buffer.from(`\r\n--${boundary}--\r\n`, 'utf8')
const contentLength =
fields.reduce((total, field) => total + field.length, 0) +
fileHeader.length +
archive.size +
ending.length
const deadline = createSkillCloudDeadline({
signal: input.signal,
timeoutMs: resolveUploadTimeoutMs(input.policy, input.timeoutMs),
timeoutMessage: 'skill-cloud-upload-timeout'
})
async function* body() {
for (const field of fields) {
yield field
}
yield fileHeader
let bytesSent = 0
for await (const chunk of createReadStream(input.archivePath, { signal: deadline.signal })) {
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
bytesSent += bytes.length
if (bytesSent > input.expectedBytes) {
throw new Error('skill-cloud-upload-source-changed')
}
input.onProgress?.(bytesSent)
yield bytes
}
if (bytesSent !== input.expectedBytes) {
throw new Error('skill-cloud-upload-source-changed')
}
yield ending
}
try {
const response = await (input.fetcher ?? fetch)(policyUrl, {
method: 'POST',
headers: {
'content-type': `multipart/form-data; boundary=${boundary}`,
'content-length': String(contentLength)
},
body: Readable.from(body()) as unknown as BodyInit,
duplex: 'half',
redirect: 'error',
signal: deadline.signal
} as RequestInit & { duplex: 'half' })
if (!response.ok && response.status !== 201 && response.status !== 204) {
throw new Error('skill-cloud-upload-failed')
}
} finally {
deadline.cleanup()
}
}
@@ -1,135 +0,0 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { SkillCloudDownloadGrant } from '../../shared/skill-cloud-contract'
import type { OrcaRuntimeService } from '../runtime/orca-runtime'
const mocks = vi.hoisted(() => ({
getRuntimeEnvironmentStatus: vi.fn(),
installSkillBundleOnRemoteRuntime: vi.fn(),
installSkillOnRemoteRuntime: vi.fn()
}))
vi.mock('electron', () => ({
app: { getPath: () => '/state', isPackaged: true }
}))
vi.mock('../ipc/runtime-environment-transport-routing', () => ({
getRuntimeEnvironmentStatus: mocks.getRuntimeEnvironmentStatus
}))
vi.mock('./skill-remote-install-service', () => ({
installSkillBundleOnRemoteRuntime: mocks.installSkillBundleOnRemoteRuntime,
installSkillOnRemoteRuntime: mocks.installSkillOnRemoteRuntime
}))
import {
installSkillBundleCloudGrant,
installSkillCloudGrant
} from './skill-cloud-grant-installation'
const grant = {
grant: {
url: 'https://storage.googleapis.com/bucket/package.tar.gz',
expiresAt: '2099-01-01T00:00:00.000Z'
},
version: {
packageId: 'package-1',
versionId: 'version-1',
name: 'private-skill',
packageDigest: 'a'.repeat(64),
archiveSha256: 'b'.repeat(64),
compressedBytes: 12
}
} as unknown as SkillCloudDownloadGrant
describe('installSkillCloudGrant', () => {
beforeEach(() => {
mocks.getRuntimeEnvironmentStatus.mockReset()
mocks.installSkillBundleOnRemoteRuntime.mockReset()
mocks.installSkillOnRemoteRuntime.mockReset()
})
it('carries cancellation into client-mediated remote installation', async () => {
const signal = new AbortController().signal
const result = {
operationId: 'operation-1',
status: 'installed',
name: 'private-skill',
packageDigest: 'a'.repeat(64),
placements: []
}
mocks.getRuntimeEnvironmentStatus.mockResolvedValue({
ok: true,
result: { capabilities: ['skills.install.v1', 'skills.upload.v1'] }
})
mocks.installSkillOnRemoteRuntime.mockResolvedValue(result)
await expect(
installSkillCloudGrant(
{} as OrcaRuntimeService,
grant,
{
operationId: 'operation-1',
environmentId: 'environment-1',
destination: { scope: 'global' }
},
signal
)
).resolves.toEqual({ status: 'ok', value: result })
expect(mocks.installSkillOnRemoteRuntime).toHaveBeenCalledWith(
expect.objectContaining({ signal })
)
})
it('installs selected bundle skills through a capable remote runtime', async () => {
const bundleGrant = {
...grant,
version: {
...grant.version,
name: 'private-bundle',
manifest: {
schemaVersion: 1,
packageId: 'package-1',
versionId: 'version-1',
bundleName: 'private-bundle',
description: '',
createdAt: '2026-08-11T00:00:00.000Z',
bundleDigest: 'c'.repeat(64),
skills: [
{
id: 'skill-1',
name: 'private-skill',
description: '',
digest: 'a'.repeat(64),
files: []
}
]
}
}
} as unknown as SkillCloudDownloadGrant
const result = {
operationId: 'operation-1',
packageId: 'package-1',
versionId: 'version-1',
bundleDigest: 'c'.repeat(64),
status: 'complete',
skills: []
}
mocks.getRuntimeEnvironmentStatus.mockResolvedValue({
ok: true,
result: { capabilities: ['skills.install.bundle.v1', 'skills.upload.v1'] }
})
mocks.installSkillBundleOnRemoteRuntime.mockResolvedValue(result)
await expect(
installSkillBundleCloudGrant({} as OrcaRuntimeService, bundleGrant, {
operationId: 'operation-1',
environmentId: 'environment-1',
selectedSkillIds: ['skill-1'],
destination: { scope: 'global' }
})
).resolves.toEqual({ status: 'ok', value: result })
expect(mocks.installSkillBundleOnRemoteRuntime).toHaveBeenCalledWith(
expect.objectContaining({
request: expect.objectContaining({ selectedSkillIds: ['skill-1'] })
})
)
})
})
@@ -1,217 +0,0 @@
import { app } from 'electron'
import {
SKILL_BUNDLE_INSTALL_CAPABILITY,
SKILL_INSTALL_CAPABILITY,
SKILL_INSTALL_UPDATE_REQUIRED_MESSAGE
} from '../../shared/skill-install-capability'
import type {
SkillBundleInstallProgress,
SkillBundleInstallRequest,
SkillBundleInstallResult
} from '../../shared/skill-bundle-install-contract'
import type {
SkillInstallDestination,
SkillInstallRequest
} from '../../shared/skill-install-contract'
import type { SkillCloudDownloadGrant } from '../../shared/skill-cloud-contract'
import { getRuntimeEnvironmentStatus } from '../ipc/runtime-environment-transport-routing'
import type { OrcaRuntimeService } from '../runtime/orca-runtime'
import { skillInstallFailureFromError } from './skill-install-operation-error'
import { recordSkillCapabilityAbsence } from './skill-operation-observability'
import {
installSkillBundleOnRemoteRuntime,
installSkillOnRemoteRuntime
} from './skill-remote-install-service'
export type SkillCloudGrantInstallInput = {
operationId: string
environmentId?: string
destination: SkillInstallDestination
providers?: string[]
conflictResolution?: 'replace-unmodified' | 'replace-and-discard-local' | 'cancel'
}
export type SkillBundleCloudGrantInstallInput = {
operationId: string
environmentId?: string
selectedSkillIds: string[]
destination: SkillInstallDestination
providers?: string[]
conflictDecisions?: SkillBundleInstallRequest['conflictDecisions']
}
function bundleFailureResult(
grant: SkillCloudDownloadGrant,
request: SkillBundleInstallRequest,
failure: NonNullable<ReturnType<typeof skillInstallFailureFromError>>
): SkillBundleInstallResult {
const manifest = grant.version.manifest
if (!('skills' in manifest)) {
throw new Error('skill-bundle-cloud-manifest-required')
}
return {
operationId: request.operationId,
packageId: request.package.packageId,
versionId: request.package.versionId,
bundleDigest: request.package.bundleDigest,
status: failure.category === 'cancelled' ? 'cancelled' : 'failed',
skills: manifest.skills
.filter((skill) => request.selectedSkillIds.includes(skill.id))
.map((skill) => ({
skillId: skill.id,
name: skill.name,
digest: skill.digest,
status: failure.category === 'cancelled' ? ('cancelled' as const) : ('failed' as const),
placements: [],
errorCategory: failure.code,
failure
}))
}
}
export async function installSkillBundleCloudGrant(
runtime: OrcaRuntimeService,
grant: SkillCloudDownloadGrant,
input: SkillBundleCloudGrantInstallInput,
signal?: AbortSignal,
onProgress?: (progress: SkillBundleInstallProgress) => void
) {
const manifest = grant.version.manifest
if (!('skills' in manifest)) {
throw new Error('skill-bundle-cloud-manifest-required')
}
const request: SkillBundleInstallRequest = {
operationId: input.operationId,
package: {
packageId: grant.version.packageId,
versionId: grant.version.versionId,
bundleDigest: manifest.bundleDigest,
archiveSha256: grant.version.archiveSha256,
compressedBytes: grant.version.compressedBytes
},
selectedSkillIds: input.selectedSkillIds,
ingress: { kind: 'download-grant', ...grant.grant },
destination: input.destination,
...(input.providers ? { providers: input.providers } : {}),
conflictDecisions: input.conflictDecisions ?? []
}
try {
if (!input.environmentId) {
return {
status: 'ok' as const,
value: await runtime.installSharedSkillBundleRequest(request, undefined, onProgress)
}
}
const userDataPath = app.getPath('userData')
const status = await getRuntimeEnvironmentStatus(userDataPath, input.environmentId, 15_000)
if (
status.ok !== true ||
status.result.capabilities?.includes(SKILL_BUNDLE_INSTALL_CAPABILITY) !== true
) {
if (status.ok === true) {
recordSkillCapabilityAbsence({
capability: SKILL_BUNDLE_INSTALL_CAPABILITY,
destination: 'remote-runtime'
})
}
return { status: 'unsupported' as const, message: SKILL_INSTALL_UPDATE_REQUIRED_MESSAGE }
}
return {
status: 'ok' as const,
value: await installSkillBundleOnRemoteRuntime({
userDataPath,
environmentId: input.environmentId,
request,
capabilities: status.result.capabilities ?? [],
requireHttps: app.isPackaged,
signal,
onProgress
})
}
} catch (error) {
const failure = skillInstallFailureFromError(error)
if (failure?.category === 'compatibility') {
return { status: 'unsupported' as const, message: SKILL_INSTALL_UPDATE_REQUIRED_MESSAGE }
}
if (!failure) {
throw error
}
return { status: 'ok' as const, value: bundleFailureResult(grant, request, failure) }
}
}
export async function installSkillCloudGrant(
runtime: OrcaRuntimeService,
grant: SkillCloudDownloadGrant,
input: SkillCloudGrantInstallInput,
signal?: AbortSignal
) {
const request: SkillInstallRequest = {
operationId: input.operationId,
package: {
packageId: grant.version.packageId,
versionId: grant.version.versionId,
packageDigest: grant.version.packageDigest,
archiveSha256: grant.version.archiveSha256,
compressedBytes: grant.version.compressedBytes
},
ingress: {
kind: 'download-grant',
url: grant.grant.url,
expiresAt: grant.grant.expiresAt
},
destination: input.destination,
...(input.providers ? { providers: input.providers } : {}),
conflictResolution: input.conflictResolution
}
try {
if (!input.environmentId) {
return { status: 'ok' as const, value: await runtime.installSharedSkillRequest(request) }
}
const userDataPath = app.getPath('userData')
const status = await getRuntimeEnvironmentStatus(userDataPath, input.environmentId, 15_000)
if (
status.ok !== true ||
status.result.capabilities?.includes(SKILL_INSTALL_CAPABILITY) !== true
) {
if (status.ok === true) {
recordSkillCapabilityAbsence({
capability: SKILL_INSTALL_CAPABILITY,
destination: 'remote-runtime'
})
}
return { status: 'unsupported' as const, message: SKILL_INSTALL_UPDATE_REQUIRED_MESSAGE }
}
return {
status: 'ok' as const,
value: await installSkillOnRemoteRuntime({
userDataPath,
environmentId: input.environmentId,
request,
capabilities: status.result.capabilities ?? [],
requireHttps: app.isPackaged,
signal
})
}
} catch (error) {
const failure = skillInstallFailureFromError(error)
if (failure?.category === 'compatibility') {
return { status: 'unsupported' as const, message: SKILL_INSTALL_UPDATE_REQUIRED_MESSAGE }
}
if (!failure) {
throw error
}
return {
status: 'ok' as const,
value: {
operationId: request.operationId,
status: failure.category === 'cancelled' ? ('cancelled' as const) : ('failed' as const),
name: grant.version.name,
packageDigest: request.package.packageDigest,
placements: [],
errorCategory: failure.code,
failure
}
}
}
}
@@ -1,33 +0,0 @@
import { describe, expect, it, vi } from 'vitest'
import type { OrcaRuntimeService } from '../runtime/orca-runtime'
import { classifySkillCloudInstallTarget } from './skill-cloud-install-target'
function runtime(usesSsh: boolean): OrcaRuntimeService {
return {
skillInstallDestinationUsesSsh: vi.fn().mockResolvedValue(usesSsh)
} as unknown as OrcaRuntimeService
}
describe('classifySkillCloudInstallTarget', () => {
it('treats paired runtimes and SSH-owned workspaces as remote', async () => {
await expect(
classifySkillCloudInstallTarget(runtime(false), {
environmentId: 'environment_1',
destination: { scope: 'global' }
})
).resolves.toBe('remote')
await expect(
classifySkillCloudInstallTarget(runtime(true), {
destination: { scope: 'workspace', worktreeId: 'worktree_1' }
})
).resolves.toBe('remote')
})
it('keeps native and WSL installs in the local grant lane', async () => {
await expect(
classifySkillCloudInstallTarget(runtime(false), {
destination: { scope: 'global', executionTarget: { kind: 'wsl', distro: 'Ubuntu' } }
})
).resolves.toBe('local')
})
})
@@ -1,11 +0,0 @@
import type { SkillInstallDestination } from '../../shared/skill-install-contract'
import type { OrcaRuntimeService } from '../runtime/orca-runtime'
export async function classifySkillCloudInstallTarget(
runtime: OrcaRuntimeService,
input: { environmentId?: string; destination: SkillInstallDestination }
): Promise<'local' | 'remote'> {
return input.environmentId || (await runtime.skillInstallDestinationUsesSsh(input.destination))
? 'remote'
: 'local'
}
@@ -1,80 +0,0 @@
import { describe, expect, it, vi } from 'vitest'
import { skillCloudRequest, type SkillCloudRequestError } from './skill-cloud-request'
vi.mock('electron', () => ({ app: { isPackaged: false } }))
describe('skillCloudRequest', () => {
it('bounds requests that do not have a caller-owned cancellation signal', async () => {
vi.useFakeTimers()
const fetcher = vi.fn(
async (_input: URL | RequestInfo, init?: RequestInit) =>
await new Promise<Response>((_resolve, reject) => {
init?.signal?.addEventListener('abort', () => reject(init.signal?.reason), { once: true })
})
) as typeof fetch
const request = skillCloudRequest({
apiUrl: 'http://127.0.0.1:8787',
path: '/v1/skill-shares/share_1',
fetcher,
timeoutMs: 100
})
const expectation = expect(request).rejects.toThrow('skill-cloud-request-timeout')
await vi.advanceTimersByTimeAsync(100)
await expectation
vi.useRealTimers()
})
it('sends credentials only to the validated Orca origin', async () => {
const fetcher = vi.fn(async (input: URL | RequestInfo, init?: RequestInit) => {
expect(String(input)).toBe('http://127.0.0.1:8787/v1/skill-shares/share_1')
expect(new Headers(init?.headers).get('authorization')).toBe('Bearer private-token')
expect(init?.redirect).toBe('error')
return Response.json({ share: { id: 'share_1' } })
}) as typeof fetch
await expect(
skillCloudRequest({
apiUrl: 'http://127.0.0.1:8787',
authToken: 'private-token',
path: '/v1/skill-shares/share_1',
fetcher
})
).resolves.toEqual({ share: { id: 'share_1' } })
})
it('omits authorization for bearer-link requests', async () => {
const fetcher = vi.fn(async (_input: URL | RequestInfo, init?: RequestInit) => {
expect(new Headers(init?.headers).has('authorization')).toBe(false)
return Response.json({ share: { id: 'share_1' } })
}) as typeof fetch
await skillCloudRequest({
apiUrl: 'http://127.0.0.1:8787',
path: '/v1/skill-shares/share_1',
fetcher
})
})
it('rejects non-API paths and returns structured errors without response contents', async () => {
await expect(
skillCloudRequest({
apiUrl: 'http://127.0.0.1:8787',
authToken: 'token',
path: 'https://attacker.test/v1/skills'
})
).rejects.toThrow('skill-cloud-request-path-invalid')
const fetcher = vi.fn(async () =>
Response.json({ code: 'skill_share_not_found', private: 'do-not-reflect' }, { status: 404 })
) as typeof fetch
const request = skillCloudRequest({
apiUrl: 'http://127.0.0.1:8787',
authToken: 'token',
path: '/v1/skill-shares/missing',
fetcher
})
await expect(request).rejects.toMatchObject({
statusCode: 404,
code: 'skill_share_not_found',
message: 'The skill Cloud request failed.'
} satisfies Partial<SkillCloudRequestError>)
})
})
-64
View File
@@ -1,64 +0,0 @@
import { resolveArtifactCloudApiUrl } from '../artifacts/artifact-cloud-config'
import { createSkillCloudDeadline } from './skill-cloud-deadline'
export class SkillCloudRequestError extends Error {
constructor(
readonly statusCode: number,
readonly code: string,
message: string
) {
super(message)
}
}
export async function skillCloudRequest<T>(input: {
apiUrl?: string
authToken?: string
path: string
method?: 'DELETE' | 'GET' | 'POST' | 'PUT'
body?: unknown
signal?: AbortSignal
idempotencyKey?: string
fetcher?: typeof fetch
timeoutMs?: number
}): Promise<T> {
const apiUrl = resolveArtifactCloudApiUrl(input.apiUrl)
const url = new URL(input.path, `${apiUrl}/`)
if (url.origin !== apiUrl || !url.pathname.startsWith('/v1/')) {
throw new Error('skill-cloud-request-path-invalid')
}
const deadline = createSkillCloudDeadline({
signal: input.signal,
timeoutMs: input.timeoutMs,
timeoutMessage: 'skill-cloud-request-timeout'
})
try {
const response = await (input.fetcher ?? fetch)(url, {
method: input.method ?? 'GET',
headers: {
...(input.authToken ? { authorization: `Bearer ${input.authToken}` } : {}),
accept: 'application/json',
...(input.body === undefined ? {} : { 'content-type': 'application/json' }),
...(input.idempotencyKey ? { 'idempotency-key': input.idempotencyKey } : {})
},
...(input.body === undefined ? {} : { body: JSON.stringify(input.body) }),
signal: deadline.signal,
redirect: 'error'
})
if (response.status === 204) {
return undefined as T
}
const value: unknown = await response.json().catch(() => null)
if (!response.ok) {
const error = value as { code?: unknown; message?: unknown } | null
throw new SkillCloudRequestError(
response.status,
typeof error?.code === 'string' ? error.code : 'skill_cloud_request_failed',
typeof error?.message === 'string' ? error.message : 'The skill Cloud request failed.'
)
}
return value as T
} finally {
deadline.cleanup()
}
}
-247
View File
@@ -1,247 +0,0 @@
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { SkillCloudVersion } from '../../shared/skill-cloud-contract'
import { SkillCloudService } from './skill-cloud-service'
const { packaged } = vi.hoisted(() => ({ packaged: { value: false } }))
const createdPaths: string[] = []
vi.mock('electron', () => ({
app: {
get isPackaged() {
return packaged.value
}
}
}))
afterEach(() => {
for (const path of createdPaths.splice(0)) {
rmSync(path, { recursive: true, force: true })
}
packaged.value = false
vi.unstubAllEnvs()
vi.unstubAllGlobals()
})
function userDataPath(): string {
const path = mkdtempSync(join(tmpdir(), 'orca-skill-cloud-service-'))
createdPaths.push(path)
return path
}
function publishedVersion(archiveSha256: string, compressedBytes: number): SkillCloudVersion {
return {
packageId: 'pkg_retry',
versionId: 'ver_retry',
name: 'retry-skill',
description: 'Retry publication',
packageDigest: 'a'.repeat(64),
archiveSha256,
compressedBytes,
createdAt: '2026-08-12T12:00:00.000Z',
releaseNotes: 'retry',
manifest: {
schemaVersion: 1,
packageId: 'pkg_retry',
versionId: 'ver_retry',
name: 'retry-skill',
description: 'Retry publication',
createdAt: '2026-08-12T12:00:00.000Z',
packageDigest: 'a'.repeat(64),
files: []
}
}
}
function publishRequest(archivePath: string, archiveSha256: string, compressedBytes: number) {
return {
apiUrl: 'http://127.0.0.1:8787',
archivePath,
archiveSha256,
compressedBytes,
packageId: 'pkg_retry',
releaseNotes: 'retry'
}
}
describe('SkillCloudService bearer links', () => {
it('resolves and grants downloads without an Orca session', async () => {
const requests: RequestInit[] = []
vi.stubGlobal(
'fetch',
vi.fn(async (input: URL | RequestInfo, init?: RequestInit) => {
requests.push(init ?? {})
return String(input).endsWith('/download-grants')
? Response.json({
grant: { url: 'https://storage.test/package', expiresAt: '2026-08-11T00:05:00Z' },
version: { versionId: 'ver_1' }
})
: Response.json({ share: { id: 'share_1', version: { versionId: 'ver_1' } } })
})
)
const service = new SkillCloudService('/unused')
const options = { apiUrl: 'http://127.0.0.1:8787' }
await expect(service.resolveShare('share_1', options)).resolves.toMatchObject({ status: 'ok' })
await expect(service.createDownloadGrant('share_1', options)).resolves.toMatchObject({
status: 'ok'
})
expect(requests).toHaveLength(2)
for (const request of requests) {
expect(new Headers(request.headers).has('authorization')).toBe(false)
}
})
it('uses the development auth token without opening a profile session', async () => {
vi.stubEnv('ORCA_CLOUD_AUTH_TOKEN', 'desktop-e2e-token')
const requests: RequestInit[] = []
vi.stubGlobal(
'fetch',
vi.fn(async (_input: URL | RequestInfo, init?: RequestInit) => {
requests.push(init ?? {})
return Response.json({ shares: [] })
})
)
await expect(
new SkillCloudService(userDataPath()).listOwnedShares({ apiUrl: 'http://127.0.0.1:8787' })
).resolves.toEqual({ status: 'ok', value: [] })
expect(new Headers(requests[0]?.headers).get('authorization')).toBe('Bearer desktop-e2e-token')
})
it('rejects the development auth token in packaged builds', async () => {
packaged.value = true
vi.stubEnv('ORCA_CLOUD_AUTH_TOKEN', 'desktop-e2e-token')
await expect(
new SkillCloudService(userDataPath()).listOwnedShares({ apiUrl: 'https://share.onorca.dev' })
).rejects.toThrow('available only in development builds')
})
})
describe('SkillCloudService publication retries', () => {
it('reuses a reserved upload after its create response is lost', async () => {
vi.stubEnv('ORCA_CLOUD_AUTH_TOKEN', 'desktop-e2e-token')
const root = userDataPath()
const archivePath = join(root, 'package.tar.gz')
const archive = Buffer.from('skill archive')
const archiveSha256 = 'b'.repeat(64)
const version = publishedVersion(archiveSha256, archive.byteLength)
writeFileSync(archivePath, archive)
let createAttempts = 0
let uploads = 0
const createKeys: string[] = []
vi.stubGlobal(
'fetch',
vi.fn(async (input: URL | RequestInfo, init?: RequestInit) => {
const url = String(input)
if (url.endsWith('/v1/skill-packages/uploads')) {
createAttempts += 1
createKeys.push(new Headers(init?.headers).get('idempotency-key') ?? '')
if (createAttempts === 1) {
throw new Error('create response lost')
}
return Response.json({
upload: {
id: 'upl_retry',
policy: { url: 'https://storage.test/upload', fields: {}, expiresAt: '2099-01-01' }
}
})
}
if (url === 'https://storage.test/upload') {
uploads += 1
return new Response(null, { status: 204 })
}
if (url.endsWith('/v1/skill-packages/pkg_retry')) {
return new Response(null, { status: 404 })
}
if (url.endsWith('/v1/skill-packages/uploads/upl_retry/finalize')) {
return Response.json({ version })
}
throw new Error(`Unexpected request: ${url}`)
})
)
const service = new SkillCloudService(root)
const request = publishRequest(archivePath, archiveSha256, archive.byteLength)
await expect(service.publishVersion(request)).rejects.toThrow('create response lost')
await expect(service.publishVersion(request)).resolves.toEqual({ status: 'ok', value: version })
expect(createKeys).toHaveLength(2)
expect(new Set(createKeys).size).toBe(1)
expect(createKeys[0]).not.toBe('')
expect(uploads).toBe(1)
})
it('finds the finalized version when retrying after its response is lost', async () => {
vi.stubEnv('ORCA_CLOUD_AUTH_TOKEN', 'desktop-e2e-token')
const root = userDataPath()
const archivePath = join(root, 'package.tar.gz')
const archive = Buffer.from('skill archive')
const archiveSha256 = 'c'.repeat(64)
const version = publishedVersion(archiveSha256, archive.byteLength)
writeFileSync(archivePath, archive)
let createAttempts = 0
let uploads = 0
let finalizations = 0
const createKeys: string[] = []
vi.stubGlobal(
'fetch',
vi.fn(async (input: URL | RequestInfo, init?: RequestInit) => {
const url = String(input)
if (url.endsWith('/v1/skill-packages/uploads')) {
createAttempts += 1
createKeys.push(new Headers(init?.headers).get('idempotency-key') ?? '')
if (createAttempts === 2) {
return Response.json(
{ code: 'skill_upload_not_pending', message: 'Upload is already finalized.' },
{ status: 409 }
)
}
return Response.json({
upload: {
id: 'upl_retry',
policy: { url: 'https://storage.test/upload', fields: {}, expiresAt: '2099-01-01' }
}
})
}
if (url === 'https://storage.test/upload') {
uploads += 1
return new Response(null, { status: 204 })
}
if (url.endsWith('/v1/skill-packages/uploads/upl_retry/finalize')) {
finalizations += 1
throw new Error('finalize response lost')
}
if (url.endsWith('/v1/skill-packages/pkg_retry')) {
return Response.json({
package: {
id: 'pkg_retry',
name: 'retry-skill',
description: 'Retry publication',
createdAt: version.createdAt,
canManage: true,
versions: [version]
}
})
}
throw new Error(`Unexpected request: ${url}`)
})
)
const service = new SkillCloudService(root)
const request = publishRequest(archivePath, archiveSha256, archive.byteLength)
await expect(service.publishVersion(request)).rejects.toThrow('finalize response lost')
await expect(service.publishVersion(request)).resolves.toEqual({ status: 'ok', value: version })
expect(createKeys).toHaveLength(2)
expect(new Set(createKeys).size).toBe(1)
expect(createKeys[0]).not.toBe('')
expect(uploads).toBe(1)
expect(finalizations).toBe(1)
})
})
-300
View File
@@ -1,300 +0,0 @@
import { createHash, randomUUID } from 'node:crypto'
import type {
SkillCloudDownloadGrant,
SkillCloudInstallTarget,
SkillCloudOwnedShare,
SkillCloudOperation,
SkillCloudOptions,
SkillCloudPackageDetails,
SkillCloudPublishRequest,
SkillCloudPublishResult,
SkillCloudShare,
SkillCloudVersion
} from '../../shared/skill-cloud-contract'
import { resolveArtifactCloudApiUrl } from '../artifacts/artifact-cloud-config'
import { runSkillCloudOperation } from './skill-cloud-auth'
import { uploadSkillPackageToSignedPolicy } from './skill-cloud-direct-upload'
import { skillCloudRequest } from './skill-cloud-request'
import { startSkillPhaseOperation } from './skill-operation-observability'
const ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/
function publishUploadIdempotencyKey(request: SkillCloudPublishRequest): string {
return (
request.idempotencyKey ??
createHash('sha256').update(`${request.packageId}\0${request.archiveSha256}`).digest('hex')
)
}
function id(value: string): string {
if (!ID_PATTERN.test(value)) {
throw new Error('skill-cloud-id-invalid')
}
return encodeURIComponent(value)
}
export class SkillCloudService {
constructor(private readonly userDataPath: string) {}
async publish(
request: SkillCloudPublishRequest
): Promise<SkillCloudOperation<SkillCloudPublishResult>> {
const version = await this.publishVersion(request)
if (version.status !== 'ok') {
return version
}
const share = await this.createShare(version.value.packageId, request)
return share.status === 'ok'
? { status: 'ok', value: { version: version.value, share: share.value } }
: share
}
publishVersion(
request: SkillCloudPublishRequest
): Promise<SkillCloudOperation<SkillCloudVersion>> {
return this.withAuth(request, async (token, apiUrl) => {
let upload: {
upload: {
id: string
policy: { url: string; fields: Record<string, string>; expiresAt: string }
}
}
try {
upload = await skillCloudRequest<typeof upload>({
apiUrl,
authToken: token,
path: '/v1/skill-packages/uploads',
method: 'POST',
body: {
expectedArchiveSha256: request.archiveSha256,
expectedCompressedBytes: request.compressedBytes
},
idempotencyKey: publishUploadIdempotencyKey(request),
signal: request.signal
})
} catch (error) {
const existing = await skillCloudRequest<{ package: SkillCloudPackageDetails }>({
apiUrl,
authToken: token,
path: `/v1/skill-packages/${id(request.packageId)}`,
signal: request.signal
}).catch(() => null)
const published = existing?.package.versions.find(
(version) =>
version.archiveSha256 === request.archiveSha256 &&
version.compressedBytes === request.compressedBytes
)
if (published) {
return published
}
throw error
}
const uploadOperation = startSkillPhaseOperation({
phase: 'upload',
compressedBytes: request.compressedBytes
})
try {
await uploadSkillPackageToSignedPolicy({
policy: upload.upload.policy,
archivePath: request.archivePath,
expectedBytes: request.compressedBytes,
signal: request.signal,
onProgress: (bytesSent) =>
request.onProgress?.({
phase: 'uploading',
bytesSent,
totalBytes: request.compressedBytes
})
})
uploadOperation.complete({
status: 'complete',
compressedBytes: request.compressedBytes
})
} catch (error) {
uploadOperation.fail(error)
throw error
}
request.onProgress?.({
phase: 'finalizing',
bytesSent: request.compressedBytes,
totalBytes: request.compressedBytes
})
const finalizationOperation = startSkillPhaseOperation({
phase: 'finalization',
compressedBytes: request.compressedBytes
})
let finalized: { version: SkillCloudVersion }
try {
finalized = await skillCloudRequest<{ version: SkillCloudVersion }>({
apiUrl,
authToken: token,
path: `/v1/skill-packages/uploads/${id(upload.upload.id)}/finalize`,
method: 'POST',
body: { releaseNotes: request.releaseNotes },
idempotencyKey: upload.upload.id,
signal: request.signal
})
if (finalized.version.packageId !== request.packageId) {
throw new Error('skill-cloud-published-package-mismatch')
}
finalizationOperation.complete({ status: 'complete' })
} catch (error) {
finalizationOperation.fail(error)
throw error
}
return finalized.version
})
}
createShare(
packageId: string,
request: SkillCloudOptions & {
pinnedVersionId?: string
idempotencyKey?: string
signal?: AbortSignal
}
): Promise<SkillCloudOperation<SkillCloudShare>> {
return this.withAuth(request, async (token, apiUrl) => {
const shared = await skillCloudRequest<{ share: SkillCloudShare }>({
apiUrl,
authToken: token,
path: `/v1/skill-packages/${id(packageId)}/shares`,
method: 'POST',
body: {
pinnedVersionId: request.pinnedVersionId
},
idempotencyKey: request.idempotencyKey ?? randomUUID(),
signal: request.signal
})
return shared.share
})
}
resolveShare(
shareId: string,
options: SkillCloudOptions
): Promise<SkillCloudOperation<{ id: string; version: SkillCloudVersion }>> {
return this.withoutAuth(options, async (apiUrl) => {
const result = await skillCloudRequest<{
share: { id: string; version: SkillCloudVersion }
}>({ apiUrl, path: `/v1/skill-shares/${id(shareId)}` })
return result.share
})
}
createDownloadGrant(
shareId: string,
options: SkillCloudOptions & { versionId?: string; installTarget?: SkillCloudInstallTarget }
): Promise<SkillCloudOperation<SkillCloudDownloadGrant>> {
return this.withoutAuth(options, (apiUrl) =>
skillCloudRequest<SkillCloudDownloadGrant>({
apiUrl,
path: `/v1/skill-shares/${id(shareId)}/download-grants`,
method: 'POST',
body: {
...(options.versionId ? { versionId: options.versionId } : {}),
...(options.installTarget ? { installTarget: options.installTarget } : {})
}
})
)
}
createPackageVersionDownloadGrant(
packageId: string,
versionId: string,
options: SkillCloudOptions & { installTarget?: SkillCloudInstallTarget }
): Promise<SkillCloudOperation<SkillCloudDownloadGrant>> {
return this.withAuth(options, (token, apiUrl) =>
skillCloudRequest<SkillCloudDownloadGrant>({
apiUrl,
authToken: token,
path: `/v1/skill-packages/${id(packageId)}/versions/${id(versionId)}/download-grants`,
method: 'POST',
body: options.installTarget ? { installTarget: options.installTarget } : {}
})
)
}
getPackage(
packageId: string,
options: SkillCloudOptions
): Promise<SkillCloudOperation<SkillCloudPackageDetails>> {
return this.withAuth(options, async (token, apiUrl) => {
const result = await skillCloudRequest<{ package: SkillCloudPackageDetails }>({
apiUrl,
authToken: token,
path: `/v1/skill-packages/${id(packageId)}`
})
return result.package
})
}
listOwnedShares(
options: SkillCloudOptions
): Promise<SkillCloudOperation<SkillCloudOwnedShare[]>> {
return this.withAuth(options, async (token, apiUrl) => {
const result = await skillCloudRequest<{ shares: SkillCloudOwnedShare[] }>({
apiUrl,
authToken: token,
path: '/v1/skill-shares'
})
return result.shares
})
}
revokeShare(shareId: string, options: SkillCloudOptions): Promise<SkillCloudOperation<void>> {
return this.withAuth(options, (token, apiUrl) =>
skillCloudRequest<void>({
apiUrl,
authToken: token,
path: `/v1/skill-shares/${id(shareId)}`,
method: 'DELETE'
})
)
}
deleteVersion(
packageId: string,
versionId: string,
options: SkillCloudOptions
): Promise<SkillCloudOperation<void>> {
return this.withAuth(options, (token, apiUrl) =>
skillCloudRequest<void>({
apiUrl,
authToken: token,
path: `/v1/skill-packages/${id(packageId)}/versions/${id(versionId)}`,
method: 'DELETE'
})
)
}
deletePackage(packageId: string, options: SkillCloudOptions): Promise<SkillCloudOperation<void>> {
return this.withAuth(options, (token, apiUrl) =>
skillCloudRequest<void>({
apiUrl,
authToken: token,
path: `/v1/skill-packages/${id(packageId)}`,
method: 'DELETE'
})
)
}
private withAuth<T>(
options: SkillCloudOptions,
operation: (token: string, apiUrl: string) => Promise<T>
): Promise<SkillCloudOperation<T>> {
return runSkillCloudOperation({
userDataPath: this.userDataPath,
options,
operation
})
}
private async withoutAuth<T>(
options: SkillCloudOptions,
operation: (apiUrl: string) => Promise<T>
): Promise<SkillCloudOperation<T>> {
const value = await operation(resolveArtifactCloudApiUrl(options.apiUrl))
return { status: 'ok', value }
}
}
@@ -180,7 +180,7 @@ describe('bounded concurrent skill discovery', () => {
const line = String(info.mock.calls.at(0)?.at(0))
// `present` is the signal that separates "big tree" from "big root set", and
// is not derivable from the other counts.
expect(line).toContain('[skills] scan roots=23 present=3 walked=23 skills=3')
expect(line).toContain('[skills] scan roots=14 present=3 walked=14 skills=3')
expect(line).toContain('home-claude')
expect(line).not.toContain(home)
expect(line).not.toContain(tmpdir())
+2 -79
View File
@@ -10,8 +10,6 @@ import type {
import type { AgentType } from '../../shared/agent-status-types'
import type { Repo } from '../../shared/repo-types'
import { getRepoExecutionHostId, LOCAL_EXECUTION_HOST_ID } from '../../shared/execution-host'
import type { SkillProviderRootOverrides } from './skill-provider-destinations'
import { resolveEnvironmentSkillProviderRoots } from './skill-provider-runtime-roots'
export type SkillScanRoot = Omit<SkillDiscoverySource, 'exists' | 'skippedReason'>
type SkillDiscoveryPathApi = Pick<typeof posix, 'basename' | 'join'>
@@ -69,14 +67,11 @@ export function buildSkillDiscoverySources(
repos?: Repo[]
includeCwd?: boolean
pathApi?: SkillDiscoveryPathApi
providerRootOverrides?: SkillProviderRootOverrides
} = {}
): SkillScanRoot[] {
const pathApi = args.pathApi ?? { basename, join }
const home = args.homeDir ?? homedir()
const cwd = args.cwd ?? process.cwd()
const providerRootOverrides =
args.providerRootOverrides ?? (args.pathApi ? {} : resolveEnvironmentSkillProviderRoots())
const roots: SkillScanRoot[] = [
source(
'home-codex',
@@ -97,7 +92,7 @@ export function buildSkillDiscoverySources(
source(
'home-claude',
'Claude home',
providerRootOverrides.claude ?? pathApi.join(home, '.claude', 'skills'),
pathApi.join(home, '.claude', 'skills'),
'home',
['claude'],
'claude'
@@ -115,7 +110,7 @@ export function buildSkillDiscoverySources(
source(
'home-grok',
'Grok home',
providerRootOverrides.grok ?? pathApi.join(home, '.grok', 'skills'),
pathApi.join(home, '.grok', 'skills'),
'home',
['agent-skills'],
'grok'
@@ -175,38 +170,6 @@ export function buildSkillDiscoverySources(
'home',
['agent-skills'],
'cursor'
),
source(
'home-droid',
'Droid home',
pathApi.join(home, '.factory', 'skills'),
'home',
['agent-skills'],
'droid'
),
source(
'home-continue',
'Continue home',
pathApi.join(home, '.continue', 'skills'),
'home',
['agent-skills'],
'continue'
),
source(
'home-trae',
'Trae home',
pathApi.join(home, '.trae-cn', 'skills'),
'home',
['agent-skills'],
'trae'
),
source(
'home-aug',
'Augment home',
pathApi.join(home, '.augment', 'skills'),
'home',
['agent-skills'],
'aug'
)
]
@@ -241,46 +204,6 @@ export function buildSkillDiscoverySources(
'repo',
['claude'],
'claude'
),
source(
`repo-droid-${stablePathId(repoPath)}`,
`${label} .factory`,
pathApi.join(repoPath, '.factory', 'skills'),
'repo',
['agent-skills'],
'droid'
),
source(
`repo-continue-${stablePathId(repoPath)}`,
`${label} .continue`,
pathApi.join(repoPath, '.continue', 'skills'),
'repo',
['agent-skills'],
'continue'
),
source(
`repo-trae-${stablePathId(repoPath)}`,
`${label} .trae`,
pathApi.join(repoPath, '.trae', 'skills'),
'repo',
['agent-skills'],
'trae'
),
source(
`repo-grok-${stablePathId(repoPath)}`,
`${label} .grok`,
pathApi.join(repoPath, '.grok', 'skills'),
'repo',
['agent-skills'],
'grok'
),
source(
`repo-aug-${stablePathId(repoPath)}`,
`${label} .augment`,
pathApi.join(repoPath, '.augment', 'skills'),
'repo',
['agent-skills'],
'aug'
)
)
}
+9 -32
View File
@@ -3,7 +3,6 @@ import type { SkillDiscoveryResult, SkillDiscoveryTarget } from '../../shared/sk
import { getDefaultWslDistro, getWslHome, parseWslPath, toLinuxPath } from '../wsl'
import { clearSkillRootScanCache, discoverSkills } from './discovery'
import { discoverSkillsInWsl } from './skill-discovery-wsl'
import type { SkillProviderRootOverrides } from './skill-provider-destinations'
import { stablePathId } from './skill-discovery-sources'
import { getRepoExecutionHostId } from '../../shared/execution-host'
import { isSkillRootUnavailableError, SkillScanCoalescer } from './skill-scan-coalescer'
@@ -88,55 +87,33 @@ function repoDigest(repos: readonly Repo[]): string {
}
// Keys use exact paths — lowercasing would alias two roots that are distinct on Linux.
function scanKey(
target: ResolvedSkillDiscoveryTarget,
repos: readonly Repo[],
providerRootOverrides: SkillProviderRootOverrides | undefined
): string {
const providerRoots = stablePathId(
Object.entries(providerRootOverrides ?? {})
.sort(([left], [right]) => left.localeCompare(right))
.map(([provider, root]) => `${provider}\0${root}`)
.join('\0')
)
const targetKey =
target.kind === 'wsl'
? `wsl\0${target.distro}\0${target.homeDir}\0${target.cwd}`
: `native\0${target.cwd ?? ''}\0${target.cwd ? '' : repoDigest(repos)}`
return `${targetKey}\0${providerRoots}`
function scanKey(target: ResolvedSkillDiscoveryTarget, repos: readonly Repo[]): string {
return target.kind === 'wsl'
? `wsl\0${target.distro}\0${target.homeDir}\0${target.cwd}`
: `native\0${target.cwd ?? ''}\0${target.cwd ? '' : repoDigest(repos)}`
}
export async function discoverSkillsOnTarget(
target: ResolvedSkillDiscoveryTarget,
repos: readonly Repo[],
options: { refresh?: boolean; providerRootOverrides?: SkillProviderRootOverrides } = {}
options: { refresh?: boolean } = {}
): Promise<SkillDiscoveryResult> {
const refresh = options.refresh === true
try {
const outcome = await targetScans.run(
scanKey(target, repos, options.providerRootOverrides),
scanKey(target, repos),
{ ttlMs: target.kind === 'wsl' ? WSL_RESULT_TTL_MS : 0, refresh },
async () => {
if (target.kind === 'wsl') {
return discoverSkillsInWsl({
distro: target.distro,
homeDir: target.homeDir,
cwd: target.cwd,
providerRootOverrides: options.providerRootOverrides
cwd: target.cwd
})
}
return target.cwd
? discoverSkills({
repos: [],
cwd: target.cwd,
refresh,
providerRootOverrides: options.providerRootOverrides
})
: discoverSkills({
repos: [...repos],
refresh,
providerRootOverrides: options.providerRootOverrides
})
? discoverSkills({ repos: [], cwd: target.cwd, refresh })
: discoverSkills({ repos: [...repos], refresh })
}
)
return outcome.value
+1 -4
View File
@@ -16,7 +16,6 @@ import {
type SkillScanRoot
} from './skill-discovery-sources'
import { discoverClaudePluginSkillSourcesInWsl } from './claude-plugin-skill-sources-wsl'
import type { SkillProviderRootOverrides } from './skill-provider-destinations'
const MAX_MARKDOWN_BYTES = 256 * 1024
const WSL_SCAN_TIMEOUT_MS = 10_000
@@ -172,7 +171,6 @@ export async function discoverSkillsInWsl(args: {
distro: string
homeDir: string
cwd: string
providerRootOverrides?: SkillProviderRootOverrides
}): Promise<SkillDiscoveryResult> {
// Plugin roots are resolved (in JS) from metadata this first wsl.exe call
// reads, then fed to the scan's own wsl.exe call below — two sequential
@@ -194,8 +192,7 @@ export async function discoverSkillsInWsl(args: {
homeDir: args.homeDir,
cwd: args.cwd,
repos: [],
pathApi: pathPosix,
providerRootOverrides: args.providerRootOverrides
pathApi: pathPosix
}),
...pluginRoots
]
@@ -1,145 +0,0 @@
import { randomUUID } from 'node:crypto'
import { readdir, rm } from 'node:fs/promises'
import { basename, dirname, join, resolve } from 'node:path'
import { readNodeFileWithinLimit } from '../../shared/node-bounded-file-reader'
import { writeSkillStateFile } from './skill-install-provenance'
import {
nativeSkillInstallFilesystem,
type SkillInstallFilesystem
} from './skill-install-filesystem'
import { WslSkillInstallFilesystem } from './skill-wsl-install-filesystem'
const MAX_PENDING_EXTRACTIONS = 64
const MAX_EXTRACTION_JOURNAL_BYTES = 64 * 1024
const OWNER_TOKEN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/
export type SkillExtractionJournalV1 = {
schemaVersion: 1
operation: 'extract'
ownerToken: string
destinationRoot: string
extractionPath: string
wslDistro?: string
}
export type SkillExtractionRecoveryReport = {
scanned: number
recovered: number
failures: { journalKey: string; code: string }[]
truncated: boolean
}
function journalPath(stateDirectory: string, ownerToken: string): string {
return join(stateDirectory, 'extraction-journals', `${ownerToken}.json`)
}
function normalized(path: string): string {
const value = resolve(path)
return process.platform === 'win32' ? value.toLocaleLowerCase('en-US') : value
}
function isJournal(value: unknown, ownerToken: string): value is SkillExtractionJournalV1 {
if (!value || typeof value !== 'object') {
return false
}
const journal = value as Partial<SkillExtractionJournalV1>
return (
journal.schemaVersion === 1 &&
journal.operation === 'extract' &&
journal.ownerToken === ownerToken &&
OWNER_TOKEN.test(ownerToken) &&
typeof journal.destinationRoot === 'string' &&
typeof journal.extractionPath === 'string' &&
journal.destinationRoot.length <= 32_768 &&
journal.extractionPath.length <= 32_768 &&
normalized(dirname(journal.extractionPath)) === normalized(journal.destinationRoot) &&
basename(journal.extractionPath) === `.orca-skill-extract-${ownerToken}` &&
(journal.wslDistro === undefined ||
(typeof journal.wslDistro === 'string' &&
journal.wslDistro.length > 0 &&
journal.wslDistro.length <= 256))
)
}
function filesystemFor(journal: SkillExtractionJournalV1): SkillInstallFilesystem {
if (!journal.wslDistro) {
return nativeSkillInstallFilesystem
}
if (process.platform !== 'win32') {
throw new Error('skill-transaction-wsl-recovery-unavailable')
}
return new WslSkillInstallFilesystem(journal.wslDistro, [journal.destinationRoot])
}
export async function beginSkillExtractionRecovery(
stateDirectory: string,
destinationRoot: string,
wslDistro?: string
): Promise<SkillExtractionJournalV1> {
const ownerToken = randomUUID()
const journal: SkillExtractionJournalV1 = {
schemaVersion: 1,
operation: 'extract',
ownerToken,
destinationRoot,
extractionPath: join(destinationRoot, `.orca-skill-extract-${ownerToken}`),
...(wslDistro ? { wslDistro } : {})
}
await writeSkillStateFile(journalPath(stateDirectory, ownerToken), journal)
return journal
}
export async function finishSkillExtractionRecovery(
stateDirectory: string,
journal: SkillExtractionJournalV1,
filesystem: SkillInstallFilesystem
): Promise<void> {
await filesystem.remove(journal.extractionPath)
await rm(journalPath(stateDirectory, journal.ownerToken), { force: true })
}
export async function recoverPendingSkillExtractions(
stateDirectory: string
): Promise<SkillExtractionRecoveryReport> {
const directory = join(stateDirectory, 'extraction-journals')
const entries = await readdir(directory, { withFileTypes: true }).catch((error) => {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
return []
}
throw error
})
const files = entries
.filter((entry) => entry.isFile() && entry.name.endsWith('.json'))
.sort((left, right) => left.name.localeCompare(right.name))
const report: SkillExtractionRecoveryReport = {
scanned: 0,
recovered: 0,
failures: [],
truncated: files.length > MAX_PENDING_EXTRACTIONS
}
for (const entry of files.slice(0, MAX_PENDING_EXTRACTIONS)) {
const journalKey = entry.name.slice(0, -'.json'.length)
report.scanned += 1
try {
const parsed: unknown = JSON.parse(
(
await readNodeFileWithinLimit(join(directory, entry.name), MAX_EXTRACTION_JOURNAL_BYTES)
).buffer.toString('utf8')
)
if (!isJournal(parsed, journalKey)) {
throw new Error('skill-extraction-journal-invalid')
}
await finishSkillExtractionRecovery(stateDirectory, parsed, filesystemFor(parsed))
report.recovered += 1
} catch (error) {
report.failures.push({
journalKey,
code:
error instanceof Error && /^skill-[a-z0-9-]+$/.test(error.message)
? error.message
: 'skill-extraction-recovery-failed'
})
}
}
return report
}
-28
View File
@@ -1,28 +0,0 @@
import { rename } from 'node:fs/promises'
const WINDOWS_RENAME_RETRY_DELAYS_MS = [50, 100, 150, 200, 250]
export async function renameSkillPathWithWindowsRetry(
source: string,
target: string
): Promise<void> {
for (let attempt = 0; ; attempt += 1) {
try {
await rename(source, target)
return
} catch (error) {
const code = (error as NodeJS.ErrnoException).code
const retryable = code === 'EPERM' || code === 'EACCES' || code === 'EBUSY'
if (
process.platform !== 'win32' ||
!retryable ||
attempt >= WINDOWS_RENAME_RETRY_DELAYS_MS.length
) {
throw error
}
await new Promise<void>((resolve) =>
setTimeout(resolve, WINDOWS_RENAME_RETRY_DELAYS_MS[attempt])
)
}
}
}
@@ -1,6 +1,6 @@
import { execFileSync } from 'node:child_process'
import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { devNull, tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import type { SkillBundleManifest } from '../../shared/skill-freshness'
@@ -50,14 +50,7 @@ describe('observed skill git tree identity', () => {
await chmod(join(work, 'nested', 'deep', 'tool.sh'), 0o755)
await writeFile(join(work, 'blob.bin'), Buffer.from([0, 1, 2, 253, 254, 255]))
const globalConfig = join(base, 'global.gitconfig')
const systemConfig = join(base, 'system.gitconfig')
await Promise.all([writeFile(globalConfig, ''), writeFile(systemConfig, '')])
const env = {
...process.env,
GIT_CONFIG_GLOBAL: globalConfig,
GIT_CONFIG_SYSTEM: systemConfig
}
const env = { ...process.env, GIT_CONFIG_GLOBAL: devNull, GIT_CONFIG_SYSTEM: devNull }
execFileSync('git', ['init', '--quiet', repoShell], { env })
const gitDirArgs = ['--git-dir', join(repoShell, '.git'), '--work-tree', work]
execFileSync('git', [...gitDirArgs, '-c', 'core.autocrlf=false', 'add', '-A'], {

Some files were not shown because too many files have changed in this diff Show More