2026-04-11 10:18:28 +00:00
import fs from "node:fs" ;
import path from "node:path" ;
2026-05-09 12:35:22 +02:00
import type { ExtensionAPI , ExtensionContext } from "@earendil-works/pi-coding-agent" ;
2026-04-11 10:18:28 +00:00
2026-04-13 23:17:58 +00:00
import { Type } from "@sinclair/typebox" ;
2026-04-21 12:34:54 +02:00
import { jailStatusTool , systemHealthTool , zfsSnapshotsTool } from "./system-tools.js" ;
2026-04-13 23:17:58 +00:00
import { skillsSearchTool , skillsContentTool } from "./skill-tools.js" ;
2026-04-14 04:56:22 +00:00
import { taskCreateTool , taskStatusTool , taskDecomposeTool } from "./controlplane-tools.js" ;
2026-04-13 23:17:58 +00:00
import { callHostd } from "./hostd-bridge.js" ;
2026-04-11 10:18:28 +00:00
type SafetyAction = "allow" | "ask" | "deny" ;
interface SafetyRule {
id : string ;
tool? : string ;
action : SafetyAction ;
reason? : string ;
patterns? : string [ ] ;
path_globs? : string [ ] ;
}
2026-04-11 14:47:18 +00:00
interface BashPatternRule {
id : string ;
pattern : string ;
action? : Exclude < SafetyAction , "allow" > ;
reason? : string ;
}
2026-04-11 10:18:28 +00:00
interface SafetyConfig {
version? : number ;
default_action? : SafetyAction ;
rules? : SafetyRule [ ] ;
2026-04-11 14:47:18 +00:00
bash_patterns? : BashPatternRule [ ] ;
bashToolPatterns? : BashPatternRule [ ] ;
zero_access_paths? : string [ ] ;
zeroAccessPaths? : string [ ] ;
read_only_paths? : string [ ] ;
readOnlyPaths? : string [ ] ;
no_delete_paths? : string [ ] ;
noDeletePaths? : string [ ] ;
2026-04-11 10:18:28 +00:00
}
const PURPOSE_ENTRY = "clawdie-purpose" ;
const DEFAULT_RULES : SafetyRule [ ] = [
{
id : "block-rm-rf" ,
tool : "bash" ,
action : "deny" ,
reason : "Blocked dangerous rm -rf usage" ,
patterns : [ "rm -rf" , "rm -fr" ] ,
} ,
{
id : "confirm-sudo" ,
tool : "bash" ,
action : "ask" ,
reason : "Confirm sudo commands" ,
patterns : [ "sudo " ] ,
} ,
{
id : "block-env-write" ,
tool : "write" ,
action : "deny" ,
reason : "Blocked writing .env files" ,
path_globs : [ "**/.env" , "**/.env.*" ] ,
} ,
{
id : "block-ssh-read" ,
tool : "read" ,
action : "deny" ,
reason : "Blocked reading SSH keys" ,
path_globs : [ "**/.ssh/**" ] ,
} ,
{
id : "block-ssh-write" ,
tool : "write" ,
action : "deny" ,
reason : "Blocked writing SSH keys" ,
path_globs : [ "**/.ssh/**" ] ,
} ,
] ;
interface HarnessState {
purpose : string | null ;
toolCounts : Map < string , number > ;
2026-04-11 14:47:18 +00:00
toolTotal : number ;
2026-04-11 10:18:28 +00:00
safetyRules : SafetyRule [ ] ;
2026-04-11 14:47:18 +00:00
bashPatterns : BashPatternRule [ ] ;
zeroAccessPaths : string [ ] ;
readOnlyPaths : string [ ] ;
noDeletePaths : string [ ] ;
2026-04-11 10:18:28 +00:00
defaultAction : SafetyAction ;
}
const state : HarnessState = {
purpose : null ,
toolCounts : new Map ( ) ,
2026-04-11 14:47:18 +00:00
toolTotal : 0 ,
2026-04-11 10:18:28 +00:00
safetyRules : DEFAULT_RULES ,
2026-04-11 14:47:18 +00:00
bashPatterns : [ ] ,
zeroAccessPaths : [ ] ,
readOnlyPaths : [ ] ,
noDeletePaths : [ ] ,
2026-04-11 10:18:28 +00:00
defaultAction : "allow" ,
} ;
function globToRegExp ( glob : string ) : RegExp {
const escaped = glob
. replace ( /[.+^${}()|[\]\\]/g , "\\$&" )
. replace ( /\*\*/g , "§§" ) ;
const withSingle = escaped . replace ( /\*/g , "[^/]*" ) . replace ( /\?/g , "." ) ;
const withDouble = withSingle . replace ( /§§/g , ".*" ) ;
return new RegExp ( ` ^ ${ withDouble } $ ` ) ;
}
function matchesGlobList ( target : string , globs : string [ ] ) : boolean {
return globs . some ( ( pattern ) = > globToRegExp ( pattern ) . test ( target ) ) ;
}
function formatCounts ( counts : Map < string , number > ) : string {
const entries = Array . from ( counts . entries ( ) ) . sort ( ( [ a ] , [ b ] ) = >
a . localeCompare ( b ) ,
) ;
if ( entries . length === 0 ) return "none" ;
return entries . map ( ( [ tool , count ] ) = > ` ${ tool } = ${ count } ` ) . join ( " " ) ;
}
2026-04-21 23:03:38 +02:00
function updateHarnessUI ( ctx : ExtensionContext ) : void {
2026-04-11 10:18:28 +00:00
if ( ! ctx . hasUI ) return ;
const purpose = state . purpose ? state . purpose : "not set" ;
const toolCounts = formatCounts ( state . toolCounts ) ;
2026-04-11 14:47:18 +00:00
const safetyCount =
state . safetyRules . length +
state . bashPatterns . length +
state . zeroAccessPaths . length +
state . readOnlyPaths . length +
state . noDeletePaths . length ;
ctx . ui . setStatus (
"clawdie-harness" ,
` Purpose: ${ purpose } | Tools: ${ toolCounts } | Safety: ${ safetyCount } ` ,
) ;
2026-04-11 10:18:28 +00:00
ctx . ui . setWidget ( "clawdie-harness" , [
` Purpose: ${ purpose } ` ,
2026-04-11 14:47:18 +00:00
` Tools: ${ toolCounts } (total= ${ state . toolTotal } ) ` ,
` Safety rules: ${ safetyCount } ` ,
2026-04-11 10:18:28 +00:00
] ) ;
}
2026-04-11 14:47:18 +00:00
function normalizeList ( value? : string [ ] | null ) : string [ ] {
if ( ! value ) return [ ] ;
return value . map ( ( entry ) = > entry . trim ( ) ) . filter ( Boolean ) ;
}
function normalizeSafetyConfig ( parsed : SafetyConfig ) : void {
state . safetyRules = parsed . rules ? . length ? parsed.rules : DEFAULT_RULES ;
state . defaultAction = parsed . default_action ? ? "allow" ;
state . bashPatterns = parsed . bash_patterns ? . length
? parsed . bash_patterns
: parsed . bashToolPatterns ? . length
? parsed . bashToolPatterns
: [ ] ;
state . zeroAccessPaths = normalizeList ( parsed . zero_access_paths ? ? parsed . zeroAccessPaths ) ;
state . readOnlyPaths = normalizeList ( parsed . read_only_paths ? ? parsed . readOnlyPaths ) ;
state . noDeletePaths = normalizeList ( parsed . no_delete_paths ? ? parsed . noDeletePaths ) ;
}
2026-04-11 10:18:28 +00:00
async function loadSafetyConfig ( cwd : string ) : Promise < void > {
const filePath = path . join ( cwd , ".agent" , "harness" , "safety.yaml" ) ;
if ( ! fs . existsSync ( filePath ) ) {
state . safetyRules = DEFAULT_RULES ;
2026-04-11 14:47:18 +00:00
state . bashPatterns = [ ] ;
state . zeroAccessPaths = [ ] ;
state . readOnlyPaths = [ ] ;
state . noDeletePaths = [ ] ;
2026-04-11 10:18:28 +00:00
state . defaultAction = "allow" ;
return ;
}
try {
const yamlModule = await import ( "yaml" ) ;
const parsed = yamlModule . parse (
fs . readFileSync ( filePath , "utf8" ) ,
) as SafetyConfig ;
2026-04-11 14:47:18 +00:00
normalizeSafetyConfig ( parsed ) ;
2026-04-11 10:18:28 +00:00
} catch {
state . safetyRules = DEFAULT_RULES ;
2026-04-11 14:47:18 +00:00
state . bashPatterns = [ ] ;
state . zeroAccessPaths = [ ] ;
state . readOnlyPaths = [ ] ;
state . noDeletePaths = [ ] ;
2026-04-11 10:18:28 +00:00
state . defaultAction = "allow" ;
}
}
function extractPath ( input : Record < string , unknown > ) : string | null {
const candidates = [ "path" , "filePath" , "filepath" , "target" ] ;
for ( const key of candidates ) {
const value = input [ key ] ;
if ( typeof value === "string" && value . length > 0 ) return value ;
}
return null ;
}
2026-04-11 14:47:18 +00:00
function resolvePath ( cwd : string , target : string ) : string {
if ( target . startsWith ( "~" ) ) {
return path . join ( process . env . HOME || "" , target . slice ( 1 ) ) ;
}
return path . isAbsolute ( target ) ? target : path.resolve ( cwd , target ) ;
}
function matchesPathGlobs ( cwd : string , targetPath : string , globs : string [ ] ) : boolean {
const resolved = resolvePath ( cwd , targetPath ) ;
const relative = path . relative ( cwd , resolved ) || targetPath ;
return (
matchesGlobList ( targetPath , globs ) ||
matchesGlobList ( resolved , globs ) ||
matchesGlobList ( relative , globs )
) ;
}
function isGlobPattern ( value : string ) : boolean {
return /[*?]/ . test ( value ) ;
}
2026-04-11 10:18:28 +00:00
function shouldMatchRule (
rule : SafetyRule ,
toolName : string ,
input : Record < string , unknown > ,
2026-04-11 14:47:18 +00:00
cwd : string ,
2026-04-11 10:18:28 +00:00
) : boolean {
if ( rule . tool && rule . tool !== toolName ) return false ;
if ( rule . patterns && rule . patterns . length > 0 ) {
const command = typeof input . command === "string" ? input . command : "" ;
if ( ! rule . patterns . some ( ( pattern ) = > command . includes ( pattern ) ) ) return false ;
}
if ( rule . path_globs && rule . path_globs . length > 0 ) {
const targetPath = extractPath ( input ) ;
if ( ! targetPath ) return false ;
2026-04-11 14:47:18 +00:00
if ( ! matchesPathGlobs ( cwd , targetPath , rule . path_globs ) ) return false ;
2026-04-11 10:18:28 +00:00
}
return true ;
}
2026-04-21 23:03:38 +02:00
function restorePurpose ( ctx : ExtensionContext ) : string | null {
2026-04-11 10:18:28 +00:00
const entries = ctx . sessionManager . getEntries ( ) ;
for ( let i = entries . length - 1 ; i >= 0 ; i -= 1 ) {
const entry = entries [ i ] ;
if ( entry . type === "custom" && entry . customType === PURPOSE_ENTRY ) {
2026-04-21 23:03:38 +02:00
const data = entry . data as Record < string , unknown > | undefined ;
const value = data ? . value ;
2026-04-11 10:18:28 +00:00
if ( typeof value === "string" && value . trim ( ) . length > 0 ) return value . trim ( ) ;
}
}
return null ;
}
2026-04-21 23:03:38 +02:00
function setPurpose ( pi : ExtensionAPI , ctx : ExtensionContext , purpose : string ) : void {
2026-04-11 10:18:28 +00:00
const trimmed = purpose . trim ( ) ;
if ( ! trimmed ) return ;
state . purpose = trimmed ;
pi . appendEntry ( PURPOSE_ENTRY , { value : trimmed } ) ;
if ( ctx . hasUI ) ctx . ui . notify ( ` Purpose set: ${ trimmed } ` , "info" ) ;
updateHarnessUI ( ctx ) ;
}
export default function registerHarness ( pi : ExtensionAPI ) : void {
pi . on ( "session_start" , async ( _event , ctx ) = > {
await loadSafetyConfig ( ctx . cwd ) ;
const restored = restorePurpose ( ctx ) ;
if ( restored ) state . purpose = restored ;
if ( ! state . purpose && ctx . hasUI ) {
const purpose = await ctx . ui . input (
"Session purpose" ,
"Describe the goal for this session" ,
) ;
if ( purpose ) setPurpose ( pi , ctx , purpose ) ;
}
updateHarnessUI ( ctx ) ;
} ) ;
2026-04-11 14:47:18 +00:00
pi . on ( "tool_execution_end" , ( event , ctx ) = > {
2026-04-11 10:18:28 +00:00
const count = state . toolCounts . get ( event . toolName ) ? ? 0 ;
state . toolCounts . set ( event . toolName , count + 1 ) ;
2026-04-11 14:47:18 +00:00
state . toolTotal += 1 ;
2026-04-11 10:18:28 +00:00
updateHarnessUI ( ctx ) ;
} ) ;
2026-04-11 14:47:18 +00:00
pi . on ( "before_agent_start" , ( event ) = > {
if ( ! state . purpose ) return undefined ;
return {
systemPrompt :
` ${ event . systemPrompt } \ n \ n<purpose> \ nYour singular purpose this session: ${ state . purpose } \ nStay focused on this goal. If a request drifts from this purpose, gently remind the user. \ n</purpose> ` ,
} ;
} ) ;
pi . on ( "input" , async ( _event , ctx ) = > {
if ( ! ctx . hasUI ) return { action : "continue" as const } ;
if ( ! state . purpose ) {
ctx . ui . notify ( "Set a purpose first." , "warning" ) ;
return { action : "handled" as const } ;
}
return { action : "continue" as const } ;
} ) ;
2026-04-11 10:18:28 +00:00
pi . on ( "tool_call" , async ( event , ctx ) = > {
const input = ( event . input ? ? { } ) as Record < string , unknown > ;
for ( const rule of state . safetyRules ) {
2026-04-11 14:47:18 +00:00
if ( ! shouldMatchRule ( rule , event . toolName , input , ctx . cwd ) ) continue ;
2026-04-11 10:18:28 +00:00
const action = rule . action ? ? state . defaultAction ;
const reason = rule . reason ? ? ` Blocked by safety rule ${ rule . id } ` ;
if ( action === "deny" ) {
return { block : true , reason } ;
}
if ( action === "ask" ) {
2026-04-11 14:47:18 +00:00
if ( ! ctx . hasUI ) return { block : true , reason } ;
2026-04-11 10:18:28 +00:00
const ok = await ctx . ui . confirm ( "Safety check" , reason ) ;
if ( ! ok ) return { block : true , reason } ;
}
}
2026-04-11 14:47:18 +00:00
const targetPath = extractPath ( input ) ;
const resolvedTarget = targetPath ? resolvePath ( ctx . cwd , targetPath ) : null ;
const command = typeof input . command === "string" ? input . command : "" ;
const isWriteTool = event . toolName === "write" || event . toolName === "edit" ;
if ( resolvedTarget && state . zeroAccessPaths . length > 0 ) {
if ( matchesPathGlobs ( ctx . cwd , resolvedTarget , state . zeroAccessPaths ) ) {
return { block : true , reason : "Blocked access to protected path" } ;
}
}
if ( resolvedTarget && isWriteTool && state . readOnlyPaths . length > 0 ) {
if ( matchesPathGlobs ( ctx . cwd , resolvedTarget , state . readOnlyPaths ) ) {
return { block : true , reason : "Blocked write to read-only path" } ;
}
}
if ( event . toolName === "bash" ) {
for ( const rule of state . bashPatterns ) {
const regex = new RegExp ( rule . pattern ) ;
if ( ! regex . test ( command ) ) continue ;
const action = rule . action ? ? "deny" ;
const reason = rule . reason ? ? "Blocked by bash safety rule" ;
if ( action === "ask" ) {
if ( ! ctx . hasUI ) return { block : true , reason } ;
const ok = await ctx . ui . confirm ( "Safety check" , reason ) ;
if ( ! ok ) return { block : true , reason } ;
} else {
return { block : true , reason } ;
}
}
for ( const pathPattern of state . zeroAccessPaths ) {
if ( ! isGlobPattern ( pathPattern ) && command . includes ( pathPattern ) ) {
return { block : true , reason : ` Blocked bash access to ${ pathPattern } ` } ;
}
}
for ( const pathPattern of state . readOnlyPaths ) {
if (
! isGlobPattern ( pathPattern ) &&
command . includes ( pathPattern ) &&
/(^|\\s)(rm|mv|sed|tee|>)/ . test ( command )
) {
return { block : true , reason : ` Blocked bash write to ${ pathPattern } ` } ;
}
}
for ( const pathPattern of state . noDeletePaths ) {
if ( ! isGlobPattern ( pathPattern ) && command . includes ( pathPattern ) && /(^|\\s)(rm|mv)/ . test ( command ) ) {
return { block : true , reason : ` Blocked bash delete of ${ pathPattern } ` } ;
}
}
}
2026-04-11 10:18:28 +00:00
return undefined ;
} ) ;
pi . registerCommand ( "purpose" , {
description : "Set or update the session purpose" ,
handler : async ( args , ctx ) = > {
const raw = args ? . trim ( ) ;
const purpose = raw
? raw
: await ctx . ui . input ( "Session purpose" , "Describe the goal for this session" ) ;
if ( purpose ) setPurpose ( pi , ctx , purpose ) ;
} ,
} ) ;
pi . registerCommand ( "harness-reload" , {
description : "Reload harness safety rules from .agent/harness/safety.yaml" ,
handler : async ( _args , ctx ) = > {
await loadSafetyConfig ( ctx . cwd ) ;
updateHarnessUI ( ctx ) ;
if ( ctx . hasUI ) ctx . ui . notify ( "Harness rules reloaded" , "info" ) ;
} ,
} ) ;
2026-04-13 23:17:58 +00:00
// ── System tools ────────────────────────────────────────────────────────
pi . registerTool ( jailStatusTool ) ;
pi . registerTool ( systemHealthTool ) ;
2026-04-21 12:34:54 +02:00
pi . registerTool ( zfsSnapshotsTool ) ;
2026-04-13 23:17:58 +00:00
// ── Skill library tools ─────────────────────────────────────────────────
pi . registerTool ( skillsSearchTool ) ;
pi . registerTool ( skillsContentTool ) ;
// ── Controlplane task tools ─────────────────────────────────────────────
2026-04-14 04:56:22 +00:00
pi . registerTool ( taskDecomposeTool ) ;
2026-04-13 23:17:58 +00:00
pi . registerTool ( taskCreateTool ) ;
pi . registerTool ( taskStatusTool ) ;
// ── Hostd passthrough ───────────────────────────────────────────────────
pi . registerTool ( {
name : "hostd" ,
label : "Hostd" ,
description :
"Execute a privileged hostd operation on the FreeBSD host. " +
"Available ops: bastille-start, bastille-stop, bastille-restart, bastille-list, " +
"zfs-snapshot, zfs-list, zfs-create, zfs-rollback, pf-reload, pf-enable, " +
2026-05-11 18:18:44 +02:00
"browser-clone-create, browser-clone-destroy, browser-clone-reap, browser-clone-force-unmount, " +
2026-04-13 23:17:58 +00:00
"service-start, service-stop, service-restart, service-status, " +
2026-05-10 09:50:20 +02:00
"pkg-version, pkg-audit, freebsd-update-status, freebsd-version, " +
2026-04-13 23:17:58 +00:00
"bastille-pkg-install, bastille-mount-pkg-cache." ,
parameters : Type.Object ( {
op : Type.String ( { description : "The hostd operation name" } ) ,
params : Type.Optional (
Type . Record ( Type . String ( ) , Type . String ( ) , {
description : "Operation parameters as key-value pairs" ,
} ) ,
) ,
} ) ,
async execute ( _toolCallId : string , params : { op : string ; params? : Record < string , string > } ) {
const result = await callHostd ( params . op , params . params ? ? { } ) ;
return {
content : [ { type : "text" as const , text : result.output } ] ,
details : result ,
} ;
} ,
} ) ;
2026-04-11 10:18:28 +00:00
}