Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 43 additions & 1 deletion agent/openclaw/hooks.go

@devin-ai-integration devin-ai-integration Bot Mar 23, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 "Already installed" check returns early without writing new required manifest and package.json files

When InstallHooks detects that the plugin file (index.ts) already exists and contains "invokeGryph", it returns early at line 80 with a success warning, skipping the manifest and package.json writes at lines 127-137. This means that users upgrading from a previous gryph version (which didn't deploy these files) will get a misleading "already installed" success, while GetHookStatus (agent/openclaw/hooks.go:234-244) will report the installation as invalid due to the missing openclaw.plugin.json and package.json. The only workaround is gryph install --force, but users won't know this unless they run gryph status.

Suggested fix approach

The "already installed" check should also verify that the manifest and package.json exist. If they are missing, the install should proceed to write them (or at least the missing files) rather than returning early.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@devin-ai-integration devin-ai-integration Bot Mar 23, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 UninstallHooks with RestoreBackup leaves manifest file behind

When UninstallHooks is called with RestoreBackup, it only restores the plugin file (index.ts) and returns early at line 160 without removing the gryph-specific manifest file (openclaw.plugin.json). This leaves a gryph artifact in the plugin directory even though the intent of restore-uninstall is to return to the pre-gryph state. The manifest was never backed up during install (only the plugin file was), so there's nothing to restore for it — it should simply be removed.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ import (
//go:embed plugin.ts
var pluginTS []byte

//go:embed openclaw.plugin.json
var pluginManifest []byte

//go:embed package.json
var packageJSON []byte

func processedPlugin() []byte {
return bytes.ReplaceAll(pluginTS, []byte(utils.GryphCommandPlaceholder), []byte(utils.GryphCommand()))
}
Expand All @@ -26,7 +32,11 @@ var HookTypes = []string{
"session_end",
}

const pluginFileName = "index.ts"
const (
pluginFileName = "index.ts"
manifestFileName = "openclaw.plugin.json"
packageFileName = "package.json"
)

func pluginDir(extensionsPath string) string {
return filepath.Join(extensionsPath, "gryph")
Expand All @@ -36,6 +46,14 @@ func pluginPath(extensionsPath string) string {
return filepath.Join(pluginDir(extensionsPath), pluginFileName)
}

func manifestPath(extensionsPath string) string {
return filepath.Join(pluginDir(extensionsPath), manifestFileName)
}

func packagePath(extensionsPath string) string {
return filepath.Join(pluginDir(extensionsPath), packageFileName)
}

func InstallHooks(ctx context.Context, opts agent.InstallOptions) (*agent.InstallResult, error) {
result := &agent.InstallResult{
BackupPaths: make(map[string]string),
Expand Down Expand Up @@ -106,6 +124,18 @@ func InstallHooks(ctx context.Context, opts agent.InstallOptions) (*agent.Instal
return result, result.Error
}

manifest := manifestPath(detection.HooksPath)
if err := os.WriteFile(manifest, pluginManifest, 0644); err != nil {
result.Error = fmt.Errorf("failed to write plugin manifest: %w", err)
return result, result.Error
}

pkgFile := packagePath(detection.HooksPath)
if err := os.WriteFile(pkgFile, packageJSON, 0644); err != nil {
result.Error = fmt.Errorf("failed to write package.json: %w", err)
return result, result.Error
}

result.HooksInstalled = HookTypes
result.Success = true
return result, nil
Expand Down Expand Up @@ -201,5 +231,17 @@ func GetHookStatus(ctx context.Context) (*agent.HookStatus, error) {
status.Issues = append(status.Issues, "plugin file differs from expected content (may need update)")
}

manifest := manifestPath(detection.HooksPath)
if _, err := os.Stat(manifest); os.IsNotExist(err) {
status.Valid = false
status.Issues = append(status.Issues, "plugin manifest (openclaw.plugin.json) is missing")
}

pkgFile := packagePath(detection.HooksPath)
if _, err := os.Stat(pkgFile); os.IsNotExist(err) {
status.Valid = false
status.Issues = append(status.Issues, "package.json is missing")
}

return status, nil
}
8 changes: 8 additions & 0 deletions agent/openclaw/openclaw.plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"id": "gryph",
"configSchema": {
"type": "object",
"additionalProperties": false,
"properties": {}
}
}
12 changes: 12 additions & 0 deletions agent/openclaw/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "@gryph/openclaw-plugin",
"version": "1.0.0",
"private": true,
"description": "Gryph audit trail plugin for OpenClaw",
"type": "module",
"openclaw": {
"extensions": [
"./index.ts"
]
}
}
30 changes: 17 additions & 13 deletions agent/openclaw/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,24 @@ import (
)

type ToolEventInput struct {
HookType string `json:"hook_type"`
Tool string `json:"tool"`
Args map[string]interface{} `json:"args"`
Result interface{} `json:"result,omitempty"`
Error string `json:"error,omitempty"`
DurationMs int64 `json:"duration_ms,omitempty"`
SessionID string `json:"session_id"`
AgentID string `json:"agent_id,omitempty"`
HookType string `json:"hook_type"`
Tool string `json:"tool"`
Args map[string]interface{} `json:"args"`
Result interface{} `json:"result,omitempty"`
Error string `json:"error,omitempty"`
DurationMs int64 `json:"duration_ms,omitempty"`
SessionID string `json:"session_id"`
AgentID string `json:"agent_id,omitempty"`
TranscriptPath string `json:"transcript_path,omitempty"`
}

type SessionEventInput struct {
HookType string `json:"hook_type"`
SessionID string `json:"session_id"`
AgentID string `json:"agent_id,omitempty"`
MessageCount int `json:"message_count,omitempty"`
DurationMs int64 `json:"duration_ms,omitempty"`
HookType string `json:"hook_type"`
SessionID string `json:"session_id"`
AgentID string `json:"agent_id,omitempty"`
MessageCount int `json:"message_count,omitempty"`
DurationMs int64 `json:"duration_ms,omitempty"`
TranscriptPath string `json:"transcript_path,omitempty"`
}

var ToolNameMapping = map[string]events.ActionType{
Expand Down Expand Up @@ -88,6 +90,7 @@ func (a *Adapter) parseToolEvent(hookType string, rawData []byte, isAfter bool)
event := events.NewEvent(sessionID, AgentName, actionType)
event.ToolName = toolName
event.AgentSessionID = input.SessionID
event.TranscriptPath = input.TranscriptPath
event.RawEvent = rawData

if err := a.buildPayload(event, actionType, toolName, input.Args, input.Result); err != nil {
Expand Down Expand Up @@ -117,6 +120,7 @@ func (a *Adapter) parseSessionEvent(rawData []byte, actionType events.ActionType

event := events.NewEvent(sessionID, AgentName, actionType)
event.AgentSessionID = input.SessionID
event.TranscriptPath = input.TranscriptPath
event.RawEvent = rawData

switch actionType {
Expand Down
125 changes: 72 additions & 53 deletions agent/openclaw/plugin.ts
Original file line number Diff line number Diff line change
@@ -1,64 +1,83 @@
import { execFileSync } from "child_process";
import { join } from "path";
import { homedir } from "os";
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";

export default function gryphPlugin(api) {
function invokeGryph(hookType, payload) {
try {
execFileSync("__GRYPH_COMMAND__", ["_hook", "openclaw", hookType], {
input: JSON.stringify(payload),
stdio: ["pipe", "pipe", "pipe"],
timeout: 5000,
});
} catch (e) {
if (e.status === 2) {
throw new Error(e.stderr?.toString()?.trim() || "Blocked by gryph");
}
}
}
const PLUGIN_ID = "gryph";
const transcriptPath = join(homedir(), ".openclaw", "logs", "commands.log");

function resolveSessionId(event, ctx) {
return event.sessionId || ctx.sessionKey || "";
}

api.on("before_tool_call", (event, ctx) => {
invokeGryph("before_tool_call", {
hook_type: "before_tool_call",
tool: event.toolName,
args: event.params,
session_id: ctx.sessionKey,
agent_id: ctx.agentId,
function invokeGryph(hookType, payload) {
try {
execFileSync("__GRYPH_COMMAND__", ["_hook", "openclaw", hookType], {
input: JSON.stringify(payload),
stdio: ["pipe", "pipe", "pipe"],
timeout: 5000,
});
});
} catch (e) {
if (e.status === 2) {
throw new Error(e.stderr?.toString()?.trim() || "Blocked by gryph");
}
}
}

api.on("after_tool_call", (event, ctx) => {
try {
invokeGryph("after_tool_call", {
hook_type: "after_tool_call",
export default definePluginEntry({
id: PLUGIN_ID,
name: "Gryph",
description: "Audit trail plugin for OpenClaw agent actions",
register(api) {
api.on("before_tool_call", (event, ctx) => {
invokeGryph("before_tool_call", {
hook_type: "before_tool_call",
tool: event.toolName,
args: event.params,
result: event.result,
error: event.error,
duration_ms: event.durationMs,
session_id: ctx.sessionKey,
session_id: resolveSessionId(event, ctx),
agent_id: ctx.agentId,
transcript_path: transcriptPath,
});
} catch (_) {}
});
});

api.on("session_start", (event, ctx) => {
try {
invokeGryph("session_start", {
hook_type: "session_start",
session_id: event.sessionId,
agent_id: ctx.agentId,
});
} catch (_) {}
});
api.on("after_tool_call", (event, ctx) => {
try {
invokeGryph("after_tool_call", {
hook_type: "after_tool_call",
tool: event.toolName,
args: event.params,
result: event.result,
error: event.error,
duration_ms: event.durationMs,
session_id: resolveSessionId(event, ctx),
agent_id: ctx.agentId,
transcript_path: transcriptPath,
});
} catch (_) {}
});

api.on("session_end", (event, ctx) => {
try {
invokeGryph("session_end", {
hook_type: "session_end",
session_id: event.sessionId,
message_count: event.messageCount,
duration_ms: event.durationMs,
agent_id: ctx.agentId,
});
} catch (_) {}
});
}
api.on("session_start", (event, ctx) => {
try {
invokeGryph("session_start", {
hook_type: "session_start",
session_id: resolveSessionId(event, ctx),
agent_id: ctx.agentId,
transcript_path: transcriptPath,
});
} catch (_) {}
});

api.on("session_end", (event, ctx) => {
try {
invokeGryph("session_end", {
hook_type: "session_end",
session_id: resolveSessionId(event, ctx),
message_count: event.messageCount,
duration_ms: event.durationMs,
agent_id: ctx.agentId,
transcript_path: transcriptPath,
});
} catch (_) {}
});
},
});
5 changes: 2 additions & 3 deletions cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/safedep/gryph/agent/claudecode"
"github.com/safedep/gryph/agent/cursor"
"github.com/safedep/gryph/agent/gemini"
"github.com/safedep/gryph/agent/openclaw"
"github.com/safedep/gryph/agent/opencode"
"github.com/safedep/gryph/agent/piagent"
"github.com/safedep/gryph/agent/windsurf"
Expand Down Expand Up @@ -56,9 +57,7 @@ func NewApp(cfg *config.Config) (*App, error) {
opencode.Register(registry, privacyChecker, cfg.GetAgentLoggingLevel(agent.AgentOpenCode), cfg.Logging.ContentHash)
windsurf.Register(registry, privacyChecker, cfg.GetAgentLoggingLevel(agent.AgentWindsurf), cfg.Logging.ContentHash)
piagent.Register(registry, privacyChecker, cfg.GetAgentLoggingLevel(agent.AgentPiAgent), cfg.Logging.ContentHash)

// For now, let us keep openclaw agent disabled because it is non-functional
// openclaw.Register(registry, privacyChecker, cfg.GetAgentLoggingLevel(agent.AgentOpenClaw), cfg.Logging.ContentHash)
openclaw.Register(registry, privacyChecker, cfg.GetAgentLoggingLevel(agent.AgentOpenClaw), cfg.Logging.ContentHash)

// Create presenter based on config
presenter := tui.NewPresenter(tui.FormatTable, tui.PresenterOptions{
Expand Down
Loading
Loading