fix(cli): remove side-effect from agent:end state updater (#133) (#147)
Some checks failed
ci/woodpecker/push/ci Pipeline failed
Some checks failed
ci/woodpecker/push/ci Pipeline failed
Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #147.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { Inject, Injectable, Logger, type OnModuleDestroy } from '@nestjs/common';
|
||||
import {
|
||||
createAgentSession,
|
||||
DefaultResourceLoader,
|
||||
SessionManager,
|
||||
type AgentSession as PiAgentSession,
|
||||
type AgentSessionEvent,
|
||||
@@ -27,6 +28,27 @@ import type { SessionInfoDto } from './session.dto.js';
|
||||
export interface AgentSessionOptions {
|
||||
provider?: string;
|
||||
modelId?: string;
|
||||
/**
|
||||
* Sandbox working directory for the session.
|
||||
* File, git, and shell tools will be restricted to this directory.
|
||||
* Falls back to AGENT_FILE_SANDBOX_DIR env var or process.cwd().
|
||||
*/
|
||||
sandboxDir?: string;
|
||||
/**
|
||||
* Platform-level system prompt for this session.
|
||||
* Merged with skill prompt additions (platform prompt first, then skills).
|
||||
* Falls back to AGENT_SYSTEM_PROMPT env var when omitted.
|
||||
*/
|
||||
systemPrompt?: string;
|
||||
/**
|
||||
* Explicit allowlist of tool names available in this session.
|
||||
* When set, only listed tools are registered with the agent.
|
||||
* When omitted for non-admin users, falls back to AGENT_USER_TOOLS env var.
|
||||
* Admins (isAdmin=true) always receive the full tool set unless explicitly restricted.
|
||||
*/
|
||||
allowedTools?: string[];
|
||||
/** Whether the requesting user has admin privileges. Controls default tool access. */
|
||||
isAdmin?: boolean;
|
||||
}
|
||||
|
||||
export interface AgentSession {
|
||||
@@ -41,6 +63,10 @@ export interface AgentSession {
|
||||
channels: Set<string>;
|
||||
/** System prompt additions injected from enabled prompt-type skills. */
|
||||
skillPromptAdditions: string[];
|
||||
/** Resolved sandbox directory for this session. */
|
||||
sandboxDir: string;
|
||||
/** Tool names available in this session, or null when all tools are available. */
|
||||
allowedTools: string[] | null;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
@@ -49,8 +75,6 @@ export class AgentService implements OnModuleDestroy {
|
||||
private readonly sessions = new Map<string, AgentSession>();
|
||||
private readonly creating = new Map<string, Promise<AgentSession>>();
|
||||
|
||||
private readonly customTools: ToolDefinition[];
|
||||
|
||||
constructor(
|
||||
@Inject(ProviderService) private readonly providerService: ProviderService,
|
||||
@Inject(BRAIN) private readonly brain: Brain,
|
||||
@@ -59,21 +83,49 @@ export class AgentService implements OnModuleDestroy {
|
||||
@Inject(CoordService) private readonly coordService: CoordService,
|
||||
@Inject(McpClientService) private readonly mcpClientService: McpClientService,
|
||||
@Inject(SkillLoaderService) private readonly skillLoaderService: SkillLoaderService,
|
||||
) {
|
||||
const fileBaseDir = process.env['AGENT_FILE_SANDBOX_DIR'] ?? process.cwd();
|
||||
const gitDefaultCwd = process.env['AGENT_GIT_CWD'] ?? process.cwd();
|
||||
const shellDefaultCwd = process.env['AGENT_SHELL_CWD'] ?? process.cwd();
|
||||
) {}
|
||||
|
||||
this.customTools = [
|
||||
...createBrainTools(brain),
|
||||
...createCoordTools(coordService),
|
||||
...createMemoryTools(memory, embeddingService.available ? embeddingService : null),
|
||||
...createFileTools(fileBaseDir),
|
||||
...createGitTools(gitDefaultCwd),
|
||||
...createShellTools(shellDefaultCwd),
|
||||
/**
|
||||
* Build the full set of custom tools scoped to the given sandbox directory.
|
||||
* Brain/coord/memory/web tools are stateless with respect to cwd; file/git/shell
|
||||
* tools receive the resolved sandboxDir so they operate within the sandbox.
|
||||
*/
|
||||
private buildToolsForSandbox(sandboxDir: string): ToolDefinition[] {
|
||||
return [
|
||||
...createBrainTools(this.brain),
|
||||
...createCoordTools(this.coordService),
|
||||
...createMemoryTools(
|
||||
this.memory,
|
||||
this.embeddingService.available ? this.embeddingService : null,
|
||||
),
|
||||
...createFileTools(sandboxDir),
|
||||
...createGitTools(sandboxDir),
|
||||
...createShellTools(sandboxDir),
|
||||
...createWebTools(),
|
||||
];
|
||||
this.logger.log(`Registered ${this.customTools.length} custom tools`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the tool allowlist for a session.
|
||||
* - Admin users: all tools unless an explicit allowedTools list is passed.
|
||||
* - Regular users: use allowedTools if provided, otherwise parse AGENT_USER_TOOLS env var.
|
||||
* Returns null when all tools should be available.
|
||||
*/
|
||||
private resolveAllowedTools(isAdmin: boolean, allowedTools?: string[]): string[] | null {
|
||||
if (allowedTools !== undefined) {
|
||||
return allowedTools.length === 0 ? [] : allowedTools;
|
||||
}
|
||||
if (isAdmin) {
|
||||
return null; // admins get everything
|
||||
}
|
||||
const envTools = process.env['AGENT_USER_TOOLS'];
|
||||
if (!envTools) {
|
||||
return null; // no restriction configured
|
||||
}
|
||||
return envTools
|
||||
.split(',')
|
||||
.map((t) => t.trim())
|
||||
.filter((t) => t.length > 0);
|
||||
}
|
||||
|
||||
async createSession(sessionId: string, options?: AgentSessionOptions): Promise<AgentSession> {
|
||||
@@ -98,8 +150,15 @@ export class AgentService implements OnModuleDestroy {
|
||||
const providerName = model?.provider ?? 'default';
|
||||
const modelId = model?.id ?? 'default';
|
||||
|
||||
// Resolve sandbox directory: option > env var > process.cwd()
|
||||
const sandboxDir =
|
||||
options?.sandboxDir ?? process.env['AGENT_FILE_SANDBOX_DIR'] ?? process.cwd();
|
||||
|
||||
// Resolve allowed tool set
|
||||
const allowedTools = this.resolveAllowedTools(options?.isAdmin ?? false, options?.allowedTools);
|
||||
|
||||
this.logger.log(
|
||||
`Creating agent session: ${sessionId} (provider=${providerName}, model=${modelId})`,
|
||||
`Creating agent session: ${sessionId} (provider=${providerName}, model=${modelId}, sandbox=${sandboxDir}, tools=${allowedTools === null ? 'all' : allowedTools.join(',') || 'none'})`,
|
||||
);
|
||||
|
||||
// Load skill tools from the catalog
|
||||
@@ -114,21 +173,53 @@ export class AgentService implements OnModuleDestroy {
|
||||
);
|
||||
}
|
||||
|
||||
// Combine static tools with dynamically discovered MCP client tools
|
||||
// Build per-session tools scoped to the sandbox directory
|
||||
const sandboxTools = this.buildToolsForSandbox(sandboxDir);
|
||||
|
||||
// Combine static tools with dynamically discovered MCP client tools and skill tools
|
||||
const mcpTools = this.mcpClientService.getToolDefinitions();
|
||||
const allCustomTools = [...this.customTools, ...skillMetaTools, ...mcpTools];
|
||||
let allCustomTools = [...sandboxTools, ...skillMetaTools, ...mcpTools];
|
||||
if (mcpTools.length > 0) {
|
||||
this.logger.log(`Attaching ${mcpTools.length} MCP client tool(s) to session ${sessionId}`);
|
||||
}
|
||||
|
||||
// Filter tools by allowlist when a restriction is in effect
|
||||
if (allowedTools !== null) {
|
||||
const allowedSet = new Set(allowedTools);
|
||||
const before = allCustomTools.length;
|
||||
allCustomTools = allCustomTools.filter((t) => allowedSet.has(t.name));
|
||||
this.logger.log(
|
||||
`Tool restriction applied: ${allCustomTools.length}/${before} tools allowed for session ${sessionId}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Build system prompt: platform prompt + skill additions appended
|
||||
const platformPrompt = options?.systemPrompt ?? process.env['AGENT_SYSTEM_PROMPT'] ?? undefined;
|
||||
const appendSystemPrompt =
|
||||
promptAdditions.length > 0 ? promptAdditions.join('\n\n') : undefined;
|
||||
|
||||
// Construct a resource loader that injects the configured system prompt
|
||||
const resourceLoader = new DefaultResourceLoader({
|
||||
cwd: sandboxDir,
|
||||
noExtensions: true,
|
||||
noSkills: true,
|
||||
noPromptTemplates: true,
|
||||
noThemes: true,
|
||||
systemPrompt: platformPrompt,
|
||||
appendSystemPrompt: appendSystemPrompt,
|
||||
});
|
||||
await resourceLoader.reload();
|
||||
|
||||
let piSession: PiAgentSession;
|
||||
try {
|
||||
const result = await createAgentSession({
|
||||
sessionManager: SessionManager.inMemory(),
|
||||
modelRegistry: this.providerService.getRegistry(),
|
||||
model: model ?? undefined,
|
||||
cwd: sandboxDir,
|
||||
tools: [],
|
||||
customTools: allCustomTools,
|
||||
resourceLoader,
|
||||
});
|
||||
piSession = result.session;
|
||||
} catch (err) {
|
||||
@@ -162,6 +253,8 @@ export class AgentService implements OnModuleDestroy {
|
||||
promptCount: 0,
|
||||
channels: new Set(),
|
||||
skillPromptAdditions: promptAdditions,
|
||||
sandboxDir,
|
||||
allowedTools,
|
||||
};
|
||||
|
||||
this.sessions.set(sessionId, session);
|
||||
|
||||
Reference in New Issue
Block a user