Enforce run mode on connector write permission events

Reject workspace-write permission events in read-only runs so a connector cannot escalate the active permission mode.

Checks: connector runtime tests; desktop pack.
This commit is contained in:
Ryder Sun committed 2026-11-07 00:32:30 +08:00
1 parent 3b1a618b37
commit ee457b563e
2 files changed
+23 -2

No files matched your search

@@ -87,7 +87,7 @@ function assertOutboundDestination(connector, value, outbound = {}) {
return origin
}
function assertEventCapabilities(connector, input) {
function assertEventCapabilities(connector, input, permissionMode) {
const type = String(input?.type || '')
if (!connector.capabilities.eventTypes.includes(type)) {
fail('AGENT_CONNECTOR_EVENT_UNDECLARED')
@@ -95,6 +95,10 @@ function assertEventCapabilities(connector, input) {
if (type === 'Cancelled' && !connector.capabilities.session.cancel) {
fail('AGENT_CONNECTOR_CANCEL_UNSUPPORTED')
}
if (type === 'Permission' && input.permission === 'workspace-write'
&& permissionMode !== 'workspace-write') {
fail('AGENT_CONNECTOR_PERMISSION_MODE_UNSUPPORTED')
}
if (type === 'Usage') {
for (const field of Object.keys(input?.usage || {})) {
if (connector.capabilities.usage[field] !== true) {
@@ -415,7 +419,7 @@ class AgentConnectorRuntime {
}
const attachments = validatedAttachments(options.attachments, connector.capabilities)
const emit = (input) => {
assertEventCapabilities(connector, input)
assertEventCapabilities(connector, input, permissionMode)
const event = parseConnectorRunEvent(input, provenance)
const duplicate = state.events.find(existing => existing.eventId === event.eventId)
if (duplicate && canonicalJson(duplicate) === canonicalJson(event)) return event
@@ -369,6 +369,23 @@ test('a recipe declaring Permission retains the permission request callback', as
assert.equal(permissions, 1)
})
test('workspace write permission events respect the active run mode', async () => {
const { runtime } = runtimeFixture(async input => {
input.emit({ eventId: 'write-request', cursor: 'write-request', sequence: 1,
type: 'Permission', requestId: 'write-request', permission: 'workspace-write', decision: 'requested' })
return {}
}, { permissionModes: ['read-only', 'workspace-write'] })
const agent = runtime.detectAgents()[0]
await assert.rejects(runtime.run(agent, 'Review', '/tmp/workspace', {
runId: 'run-read-only-write', agentRunId: 'agent-run-read-only-write', sandbox: 'read-only',
}), { message: 'AGENT_CONNECTOR_PERMISSION_MODE_UNSUPPORTED' })
const allowed = await runtime.run(agent, 'Review', '/tmp/workspace', {
runId: 'run-write', agentRunId: 'agent-run-write', sandbox: 'workspace-write',
})
assert.equal(allowed.outcome, 'waiting_permission')
assert.equal(allowed.waitingRequest.permission, 'workspace-write')
})
test('Connector outcome references must come from declared, validated events', async () => {
const artifactId = `artifact-${'a'.repeat(64)}`
const evidenceId = `evidence-${'b'.repeat(64)}`