feat: run agent tools with host permissions

This commit is contained in:
mesalogo
2026-08-15 18:06:03 +08:00
parent 79e2511f6f
commit 34bbfab2af
38 changed files with 224 additions and 1395 deletions
+35 -267
View File
@@ -1,4 +1,4 @@
import { createHash, randomUUID } from 'node:crypto'
import { randomUUID } from 'node:crypto'
import { isAbsolute } from 'node:path'
import {
AgentSideConnection,
@@ -25,13 +25,7 @@ import {
SessionId,
type SessionEvent
} from '@deepseek-ai/dsh-session'
import { setSandboxMode } from '@deepseek-ai/dsh-sandbox-policy'
import { setApprovalPolicy } from '@deepseek-ai/dsh-user-approval'
import type {
ToolDefinition,
ToolExecution,
ToolExecutionResult
} from '@deepseek-ai/dsh-tools'
import type { ToolDefinition } from '@deepseek-ai/dsh-tools'
import * as ToolSkill from '@deepseek-ai/dsh-tool-skill'
export const GOODBUDDY_CONTROL_PROTOCOL_VERSION = 1
@@ -50,8 +44,13 @@ export const GOODBUDDY_HARNESS_MAX_STEP_TOKENS = 16 * 1024
const DELTA_BATCH_CHARACTERS = 4 * 1024
const DELTA_BATCH_INTERVAL_MS = 100
const MAX_SUMMARY_CHARACTERS = 4_000
const MAX_FINGERPRINT_BYTES = 4 * 1024 * 1024
const MAX_MCP_PROXY_RESULT_BYTES = 256 * 1024
const ASK_BLOCKED_TOOL_NAMES = new Set([
'bash',
'pwsh',
'write',
'edit'
])
const GOODBUDDY_EXECUTION_GUIDANCE = [
'GoodBuddy controlled execution rules:',
'- In Execute mode, act through the available tools instead of writing a long implementation plan.',
@@ -70,15 +69,13 @@ export type GoodBuddyHarnessCapabilities = {
supports: {
cancellation: true
sessionRelease: true
oneShotApproval: true
reasoningEvents: boolean
toolEvents: boolean
usageEvents: boolean
credentialResolution: true
}
sandbox: {
provider: string
enforcement: 'full' | 'partial'
execution: {
mode: 'host'
}
}
@@ -87,7 +84,7 @@ export type GoodBuddyHarnessControlConfig = {
model: string
workspace: string
harnessVersion: string
sandbox: GoodBuddyHarnessCapabilities['sandbox']
execution: GoodBuddyHarnessCapabilities['execution']
credentialRefs: readonly string[]
skills: readonly {
name: string
@@ -109,10 +106,10 @@ type OwnedSession = {
handle: AgentHandle
preparation?: Preparation
proxyToolDisposers: Map<string, () => void>
sandboxRetries: GoodBuddySandboxRetryLedger
inflight?: {
requestId: string
messageId: string
mode: GoodBuddyWorkMode
turn?: number
endReason?: string
turnError?: unknown
@@ -209,156 +206,10 @@ function parseProxyToolCatalog(
})
}
type DeniedToolCall = {
toolName: string
operationFingerprint: string
}
type CredentialResolver = (
ref: string
) => Promise<string | undefined>
function argumentsFingerprint(value: unknown): string | undefined {
try {
const serialized = JSON.stringify(value, (_key, nested) => {
if (
nested &&
typeof nested === 'object' &&
!Array.isArray(nested)
) {
return Object.fromEntries(
Object.entries(nested as Record<string, unknown>).sort(
([left], [right]) => left.localeCompare(right)
)
)
}
return nested
})
if (
serialized === undefined ||
Buffer.byteLength(serialized, 'utf8') >
MAX_FINGERPRINT_BYTES
) {
return undefined
}
return createHash('sha256').update(serialized).digest('hex')
} catch {
return undefined
}
}
function isSandboxDenial(
result: Readonly<ToolExecutionResult>
): boolean {
const sandboxValue =
!result.isError &&
result.value &&
typeof result.value === 'object' &&
!Array.isArray(result.value)
? (result.value as Record<string, unknown>).sandbox
: undefined
return (
(result.isError &&
result.error.info?.code === 'FS_SANDBOX_DENIED') ||
result.content.some(
(content) =>
content.type === 'text' &&
content.text.includes('[sandbox: file access denied under ')
) ||
(!!sandboxValue &&
typeof sandboxValue === 'object' &&
!Array.isArray(sandboxValue) &&
(sandboxValue as Record<string, unknown>).denied === true)
)
}
function requestedEscalation(
value: unknown
): {
mode: 'workspace-write' | 'danger-full-access'
operationFingerprint: string
} | undefined {
if (
!value ||
typeof value !== 'object' ||
Array.isArray(value)
) {
return undefined
}
const argumentsRecord = value as Record<string, unknown>
const mode = argumentsRecord.sandbox_permissions
if (
(mode !== 'workspace-write' &&
mode !== 'danger-full-access') ||
typeof argumentsRecord.justification !== 'string' ||
!argumentsRecord.justification.trim()
) {
return undefined
}
const operationArguments = { ...argumentsRecord }
delete operationArguments.sandbox_permissions
delete operationArguments.justification
const operationFingerprint = argumentsFingerprint(
operationArguments
)
return operationFingerprint
? {
mode,
operationFingerprint
}
: undefined
}
export class GoodBuddySandboxRetryLedger {
private readonly deniedToolCalls = new Map<
string,
DeniedToolCall
>()
clear(): void {
this.deniedToolCalls.clear()
}
record(
execution: Readonly<ToolExecution>,
result: Readonly<ToolExecutionResult>
): void {
if (!isSandboxDenial(result)) {
return
}
const operationFingerprint = argumentsFingerprint(
execution.arguments
)
if (!operationFingerprint) {
return
}
this.deniedToolCalls.set(execution.callId, {
toolName: execution.name,
operationFingerprint
})
}
consumeRetry(toolName: string, argumentsValue: unknown): boolean {
const escalation = requestedEscalation(argumentsValue)
if (escalation?.mode !== 'danger-full-access') {
return false
}
const denied = [...this.deniedToolCalls.entries()]
.reverse()
.find(
([, candidate]) =>
candidate.toolName === toolName &&
candidate.operationFingerprint ===
escalation.operationFingerprint
)
if (!denied) {
return false
}
this.deniedToolCalls.delete(denied[0])
return true
}
}
/**
* Memory-only credential provider. It deliberately has no writable operation
* and can resolve only references registered by the trusted host.
@@ -615,13 +466,12 @@ export class GoodBuddyHarnessControlPlane {
supports: {
cancellation: true,
sessionRelease: true,
oneShotApproval: true,
reasoningEvents: true,
toolEvents: true,
usageEvents: true,
credentialResolution: true
},
sandbox: this.config.sandbox
execution: this.config.execution
}
}
@@ -682,9 +532,9 @@ export class GoodBuddyHarnessControlPlane {
this.sendEvent(sessionId, event)
)
inflight.eventTail = queued.catch((error: unknown) => {
inflight.eventError ??= error
record.handle.agent.cancel({ kind: 'user' })
})
inflight.eventError ??= error
record.handle.agent.cancel({ kind: 'user' })
})
}
private flushPendingDelta(sessionId: string): void {
@@ -797,6 +647,23 @@ export class GoodBuddyHarnessControlPlane {
return
}
this.observing = true
this.ctx.on('tools/execute', async (exec, next) => {
const sessionId = exec.agent?.session.id
const record = sessionId
? this.sessions.get(sessionId)
: undefined
if (
record &&
record.handle.agent === exec.agent &&
record.inflight?.mode === 'ask' &&
ASK_BLOCKED_TOOL_NAMES.has(exec.name)
) {
throw new Error(
`Ask 模式不允许执行修改或命令工具:${exec.name}`
)
}
return next()
})
this.ctx.on(
'session/event',
(session, event: SessionEvent) => {
@@ -878,92 +745,6 @@ export class GoodBuddyHarnessControlPlane {
}
}
)
this.ctx.on(
'tools/result',
(
exec: Readonly<ToolExecution>,
result: Readonly<ToolExecutionResult>
) => {
const sessionId = exec.agent?.session.id
if (!sessionId) {
return
}
const record = this.sessions.get(sessionId)
if (
record?.handle.agent !== exec.agent ||
!record.inflight
) {
return
}
record.sandboxRetries.record(exec, result)
}
)
this.ctx.on('approval/request', async (request, next) => {
const record = this.sessions.get(request.agent.session.id)
if (
!record ||
record.handle.agent !== request.agent ||
!record.inflight ||
!this.connection
) {
return next()
}
const matchingRetry = request.callId
? record.handle.agent.session.events
.filter(
(
event
): event is Extract<
SessionEvent,
{ type: 'tool/call' }
> =>
event.type === 'tool/call' &&
event.data.callId === request.callId
)
.at(-1)
: undefined
let retryArguments: unknown
if (matchingRetry) {
try {
retryArguments = JSON.parse(matchingRetry.data.arguments)
} catch {
return 'rejected'
}
}
if (
!matchingRetry ||
!record.sandboxRetries.consumeRetry(
request.toolName,
retryArguments
)
) {
return 'rejected'
}
const response = await this.connection.requestPermission({
sessionId: request.agent.session.id,
toolCall: {
toolCallId:
request.callId ?? `approval-${randomUUID()}`,
title: request.reason ?? request.toolName
},
options: [
{
optionId: 'allow-once',
name: 'Allow once',
kind: 'allow_once'
},
{
optionId: 'reject-once',
name: 'Reject',
kind: 'reject_once'
}
]
})
return response.outcome.outcome === 'selected' &&
response.outcome.optionId === 'allow-once'
? 'allowed-once'
: 'rejected'
})
}
private queueUsage(sessionId: string, usage: TokenUsage): void {
@@ -1150,12 +931,9 @@ export class GoodBuddyHarnessControlPlane {
await Promise.all([skillTool, skillRegistrations])
}
})
setSandboxMode(handle.agent.session, 'read-only')
setApprovalPolicy(handle.agent.session, 'never')
this.sessions.set(sessionId, {
handle,
proxyToolDisposers: new Map(),
sandboxRetries: new GoodBuddySandboxRetryLedger()
proxyToolDisposers: new Map()
})
return {
sessionId,
@@ -1187,16 +965,6 @@ export class GoodBuddyHarnessControlPlane {
'a single-use goodbuddy/session/prepare is required'
)
}
setSandboxMode(
record.handle.agent.session,
preparation.mode === 'ask'
? 'read-only'
: 'workspace-write'
)
setApprovalPolicy(
record.handle.agent.session,
preparation.mode === 'ask' ? 'never' : 'ask'
)
if (preparation.mode === 'execute') {
await this.refreshProxyTools(params.sessionId, record)
} else {
@@ -1205,7 +973,6 @@ export class GoodBuddyHarnessControlPlane {
}
record.proxyToolDisposers.clear()
}
record.sandboxRetries.clear()
const text = promptText(params.prompt)
if (!text.trim()) {
throw RequestError.invalidParams(
@@ -1222,6 +989,7 @@ export class GoodBuddyHarnessControlPlane {
record.inflight = {
requestId: preparation.requestId,
messageId: message.id,
mode: preparation.mode,
resolve,
reject,
emittedCharacters: 0,