mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 00:57:45 +00:00
* plugins: bind worker and panel approval to exact package bytes Consent for any plugin that runs code (a v2 sandboxed-ui or trusted-worker profile, or a v1 manifest with a worker or panel) now names its installed content hash, so an update cannot swap code under an old approval. Shipped v1 installs re-prompt once. Data packs keep their fingerprints, and dev trees stay unbound so a save doesn't re-prompt. The re-review notice is one short line. * plugins: integrity-check reviewed bytes before every worker spawn The content verifier cached its first check, so bytes swapped between a crash and its restart still ran. The controller now hands the manager one uncached verifyBeforeSpawn hook that runs before every spawn, including crash restarts, and replaces the cached first-start check, so nothing is hashed twice and a running worker is reused without hashing. It is an integrity check, not a sandbox: Node still reads files after it. Approval is re-asserted at the same points, so a revoked plugin stops restarting instead of counting as a crash, and cancellation does not wait on a hash. * plugins: restart a running worker on its updated code A refresh that changes a running worker's spec used to stop it and wait for the next command or event. Reconcile now restarts it once on the new spec after every stop, through the same approval and integrity path, and skips the restart if a newer reconcile or dispose has taken over. Idle workers stay lazy. * test(plugins): re-review a dev copy that takes over an approved install Approval of worker and panel plugins now names their exact bytes, so switching the demo plugin from its installed copy to the dev folder asks for review again; the journey approves the dev copy before using it. * fix(plugins): admit worker restarts against current reviewed spec * test(plugins): say why the worker refresh tests read the clock and wait out a backoff * fix(plugins): refuse custom Git remote helpers in plugin downloads and test smudge filters for real * fix(plugins): keep crash restarts through refresh pauses, stop workers before awaiting refresh on shutdown, and narrow inherited Git protocol allowlists * test(plugins): shrink the flaky-test baseline for the worker controller test
193 lines
7.1 KiB
TypeScript
193 lines
7.1 KiB
TypeScript
// Require visible consent before the hello-orca panel, worker command or event subscription runs.
|
|
|
|
import { cp, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import type { Page } from '@stablyai/playwright-test'
|
|
import { expect, test } from './helpers/orca-app'
|
|
import { removeWorktreeViaStore } from './helpers/dead-terminal'
|
|
|
|
async function openPluginSettings(page: Page): Promise<void> {
|
|
await page.evaluate(() => {
|
|
const state = window.__store?.getState()
|
|
if (!state) {
|
|
throw new Error('store unavailable')
|
|
}
|
|
state.openSettingsTarget({ pane: 'plugins', repoId: null })
|
|
state.openSettingsPage()
|
|
})
|
|
await expect(page.locator('[data-settings-section="plugins"]')).toBeVisible()
|
|
}
|
|
|
|
async function openDemoPanel(page: Page): Promise<void> {
|
|
await page.evaluate(() => {
|
|
const state = window.__store?.getState()
|
|
if (!state) {
|
|
throw new Error('store unavailable')
|
|
}
|
|
state.closeSettingsPage()
|
|
if (!state.rightSidebarOpen) {
|
|
state.toggleRightSidebar()
|
|
}
|
|
})
|
|
const panelButton = page.getByRole('button', { name: 'Hello Orca', exact: true })
|
|
await expect(panelButton).toBeVisible({ timeout: 15_000 })
|
|
await panelButton.click()
|
|
const frame = page.frameLocator('iframe[title="Hello Orca"]')
|
|
await expect(frame.getByRole('heading', { name: 'Hello Orca 👋' })).toBeVisible()
|
|
await expect(frame.locator('meta[http-equiv="Content-Security-Policy"]')).toHaveAttribute(
|
|
'content',
|
|
/default-src 'none'/
|
|
)
|
|
}
|
|
|
|
async function createWorktree(page: Page, name: string): Promise<string> {
|
|
await page.waitForFunction(
|
|
() => {
|
|
const state = window.__store?.getState()
|
|
return Boolean(state && Object.values(state.worktreesByRepo).flat().length > 0)
|
|
},
|
|
undefined,
|
|
{ timeout: 15_000 }
|
|
)
|
|
return page.evaluate(async (worktreeName) => {
|
|
const state = window.__store?.getState()
|
|
if (!state) {
|
|
throw new Error('store unavailable')
|
|
}
|
|
const worktrees = Object.values(state.worktreesByRepo).flat()
|
|
const active =
|
|
worktrees.find((worktree) => worktree.id === state.activeWorktreeId) ?? worktrees[0]
|
|
if (!active) {
|
|
throw new Error('active worktree was not found')
|
|
}
|
|
const result = await state.createWorktree(active.repoId, worktreeName)
|
|
return result.worktree.id
|
|
}, name)
|
|
}
|
|
|
|
test('runs hello-orca panel, command, and event behind visible consent', async ({ orcaPage }) => {
|
|
const tempRoot = await mkdtemp(join(tmpdir(), 'orca-hello-plugin-e2e-'))
|
|
const pluginRoot = join(tempRoot, 'hello-orca')
|
|
let createdWorktreeId: string | null = null
|
|
await cp(join(process.cwd(), 'examples', 'plugins', 'hello-orca'), pluginRoot, {
|
|
recursive: true
|
|
})
|
|
|
|
try {
|
|
const installed = await orcaPage.evaluate(async (sourcePath) => {
|
|
const settings = await window.api.settings.set({ pluginSystemEnabled: true })
|
|
window.__store?.setState({ settings })
|
|
const result = await window.api.plugins.install({ kind: 'local-path', path: sourcePath })
|
|
if (!result.ok) {
|
|
throw new Error(result.error)
|
|
}
|
|
const pending = (await window.api.plugins.refresh()).find(
|
|
(entry) => entry.pluginKey === result.pluginKey
|
|
)
|
|
if (!pending) {
|
|
throw new Error('installed plugin was not listed')
|
|
}
|
|
let blocked = false
|
|
try {
|
|
await window.api.plugins.invokeCommand({
|
|
pluginKey: result.pluginKey,
|
|
commandId: 'hello-ping',
|
|
args: { source: 'before-consent' }
|
|
})
|
|
} catch {
|
|
blocked = true
|
|
}
|
|
return { pluginKey: result.pluginKey, status: pending.status, blocked }
|
|
}, pluginRoot)
|
|
|
|
expect(installed.status).toBe('pending')
|
|
expect(installed.blocked).toBe(true)
|
|
|
|
await openPluginSettings(orcaPage)
|
|
await orcaPage.getByRole('tab', { name: /^Installed/ }).click()
|
|
const row = orcaPage.locator(`[data-plugin-key="${installed.pluginKey}"]`)
|
|
await expect(row).toContainText('Needs review')
|
|
await row.getByRole('button', { name: 'Review & enable' }).click()
|
|
const consent = orcaPage.getByRole('dialog', { name: 'Review permissions' })
|
|
await expect(consent).toBeVisible()
|
|
await expect(consent).toContainText('Local folder')
|
|
await expect(consent).toContainText('full access to your files, network, and other processes')
|
|
await expect(consent.getByRole('button', { name: 'Keep Disabled' })).toBeFocused()
|
|
await consent.getByRole('button', { name: 'Enable plugin' }).click()
|
|
await expect(consent).toBeHidden()
|
|
await expect(row).toContainText('Enabled')
|
|
|
|
const commandResults = await orcaPage.evaluate(async (pluginKey) => {
|
|
const first = await window.api.plugins.invokeCommand({
|
|
pluginKey,
|
|
commandId: 'hello-ping',
|
|
args: { source: 'e2e' }
|
|
})
|
|
const second = await window.api.plugins.invokeCommand({
|
|
pluginKey,
|
|
commandId: 'hello-ping',
|
|
args: { source: 'e2e' }
|
|
})
|
|
return { first, second }
|
|
}, installed.pluginKey)
|
|
expect(commandResults.first).toEqual({ pong: true, count: 1, args: { source: 'e2e' } })
|
|
expect(commandResults.second).toEqual({ pong: true, count: 2, args: { source: 'e2e' } })
|
|
|
|
await orcaPage.evaluate(async (sourcePath) => {
|
|
const settings = await window.api.settings.set({ devPluginPaths: [sourcePath] })
|
|
window.__store?.setState({ settings })
|
|
await window.api.plugins.refresh()
|
|
}, pluginRoot)
|
|
// A dev folder taking over the approved install is different code, so it needs its own review.
|
|
await orcaPage.evaluate(async (pluginKey) => {
|
|
const plugin = (await window.api.plugins.list()).find(
|
|
(entry) => entry.pluginKey === pluginKey
|
|
)
|
|
if (!plugin?.consentFingerprint || plugin.status !== 'pending') {
|
|
throw new Error('the dev copy should wait for review')
|
|
}
|
|
await window.api.plugins.consent({
|
|
pluginKey,
|
|
reviewedFingerprint: plugin.consentFingerprint,
|
|
decision: 'approve'
|
|
})
|
|
}, installed.pluginKey)
|
|
|
|
await openDemoPanel(orcaPage)
|
|
|
|
const panelPath = join(pluginRoot, 'panel.html')
|
|
const panelHtml = await readFile(panelPath, 'utf8')
|
|
await writeFile(panelPath, panelHtml.replace('Hello Orca 👋', 'Hello Orca reloaded'))
|
|
await expect(
|
|
orcaPage.frameLocator('iframe[title="Hello Orca"]').getByRole('heading', {
|
|
name: 'Hello Orca reloaded'
|
|
})
|
|
).toBeVisible({ timeout: 15_000 })
|
|
|
|
createdWorktreeId = await createWorktree(orcaPage, `plugin-event-${Date.now()}`)
|
|
await expect
|
|
.poll(
|
|
() =>
|
|
orcaPage.evaluate(
|
|
async ({ pluginKey, worktreeId }) =>
|
|
(await window.api.plugins.getLogs({ pluginKey })).some(
|
|
(entry) =>
|
|
entry.line.includes('worktree created:') && entry.line.includes(worktreeId)
|
|
),
|
|
{ pluginKey: installed.pluginKey, worktreeId: createdWorktreeId! }
|
|
),
|
|
{ timeout: 15_000 }
|
|
)
|
|
.toBe(true)
|
|
} finally {
|
|
try {
|
|
if (createdWorktreeId) {
|
|
await removeWorktreeViaStore(orcaPage, createdWorktreeId)
|
|
}
|
|
} finally {
|
|
await rm(tempRoot, { recursive: true, force: true })
|
|
}
|
|
}
|
|
})
|