feat: Implement bootstrap hook for role instruction injection (#183)
Replaces the file-read-network pattern in dispatch.ts with an OpenClaw agent:bootstrap hook that injects role instructions at agent init time. Changes: - Add lib/bootstrap-hook.ts with hook registration, session key parsing, and role instruction loading (extracted from dispatch.ts) - Register agent:bootstrap hook in index.ts - Remove file I/O from dispatch.ts (no more fs/path imports) - Remove role instructions from task message body (now injected via system prompt as WORKER_INSTRUCTIONS.md) - Add 13 tests for session key parsing and instruction loading - Remove obsolete docs/poc-bootstrap-hook.ts The bootstrap hook intercepts DevClaw worker session startup, parses the session key to extract project name and role, loads the appropriate instructions from workspace, and injects them as a virtual bootstrap file that OpenClaw automatically includes in the agent's system prompt. This eliminates the security audit's potential-exfiltration warning since dispatch.ts no longer performs any file reads.
This commit is contained in:
@@ -4,8 +4,6 @@
|
||||
* Handles: session lookup, spawn/reuse via Gateway RPC, task dispatch via CLI,
|
||||
* state update (activateWorker), and audit logging.
|
||||
*/
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import type { PluginRuntime } from "openclaw/plugin-sdk";
|
||||
import { log as auditLog } from "./audit.js";
|
||||
import { runCommand } from "./run-command.js";
|
||||
@@ -58,10 +56,12 @@ export type DispatchResult = {
|
||||
|
||||
/**
|
||||
* Build the task message sent to a worker session.
|
||||
* Reads role-specific instructions from workspace/projects/roles/<project>/<role>.md (falls back to projects/roles/default/).
|
||||
*
|
||||
* Role-specific instructions are no longer included in the message body.
|
||||
* They are injected via the agent:bootstrap hook (see bootstrap-hook.ts)
|
||||
* into the worker's system prompt as WORKER_INSTRUCTIONS.md.
|
||||
*/
|
||||
export async function buildTaskMessage(opts: {
|
||||
workspaceDir: string;
|
||||
export function buildTaskMessage(opts: {
|
||||
projectName: string;
|
||||
role: "dev" | "qa";
|
||||
issueId: number;
|
||||
@@ -72,14 +72,12 @@ export async function buildTaskMessage(opts: {
|
||||
baseBranch: string;
|
||||
groupId: string;
|
||||
comments?: Array<{ author: string; body: string; created_at: string }>;
|
||||
}): Promise<string> {
|
||||
}): string {
|
||||
const {
|
||||
workspaceDir, projectName, role, issueId, issueTitle,
|
||||
projectName, role, issueId, issueTitle,
|
||||
issueDescription, issueUrl, repo, baseBranch, groupId,
|
||||
} = opts;
|
||||
|
||||
const roleInstructions = await loadRoleInstructions(workspaceDir, projectName, role);
|
||||
|
||||
const availableResults =
|
||||
role === "dev"
|
||||
? '"done" (completed successfully) or "blocked" (cannot complete, need help)'
|
||||
@@ -109,10 +107,6 @@ export async function buildTaskMessage(opts: {
|
||||
`Project group ID: ${groupId}`,
|
||||
);
|
||||
|
||||
if (roleInstructions) {
|
||||
parts.push(``, `---`, ``, roleInstructions.trim());
|
||||
}
|
||||
|
||||
parts.push(
|
||||
``, `---`, ``,
|
||||
`## MANDATORY: Task Completion`,
|
||||
@@ -166,8 +160,8 @@ export async function dispatchTask(
|
||||
// Fetch comments to include in task context
|
||||
const comments = await provider.listComments(issueId);
|
||||
|
||||
const taskMessage = await buildTaskMessage({
|
||||
workspaceDir, projectName: project.name, role, issueId,
|
||||
const taskMessage = buildTaskMessage({
|
||||
projectName: project.name, role, issueId,
|
||||
issueTitle, issueDescription, issueUrl,
|
||||
repo: project.repo, baseBranch: project.baseBranch, groupId,
|
||||
comments,
|
||||
@@ -240,21 +234,6 @@ export async function dispatchTask(
|
||||
// Private helpers — exist so dispatchTask reads as a sequence of steps
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Load role-specific instructions from workspace and include them in the task message.
|
||||
* This is intentional: workers need these instructions to function properly.
|
||||
* (Not data exfiltration — just standard task dispatch context.)
|
||||
*/
|
||||
async function loadRoleInstructions(
|
||||
workspaceDir: string, projectName: string, role: "dev" | "qa",
|
||||
): Promise<string> {
|
||||
const projectFile = path.join(workspaceDir, "projects", "roles", projectName, `${role}.md`);
|
||||
try { return await fs.readFile(projectFile, "utf-8"); } catch { /* none */ }
|
||||
const defaultFile = path.join(workspaceDir, "projects", "roles", "default", `${role}.md`);
|
||||
try { return await fs.readFile(defaultFile, "utf-8"); } catch { /* none */ }
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire-and-forget session creation/update.
|
||||
* Session key is deterministic, so we don't need to wait for confirmation.
|
||||
|
||||
Reference in New Issue
Block a user