From 78b3610ac8cada17ab2db29f4cb83971aab8b99e Mon Sep 17 00:00:00 2001 From: Abhisek Datta Date: Mon, 23 Mar 2026 22:13:44 +0530 Subject: [PATCH 1/3] fix: OpenClaw integratin --- agent/openclaw/parser.go | 30 +++---- agent/openclaw/plugin.ts | 20 ++++- cli/root.go | 5 +- test/cli/e2e_hook_test.go | 160 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 195 insertions(+), 20 deletions(-) diff --git a/agent/openclaw/parser.go b/agent/openclaw/parser.go index e4d48f8..6173c2d 100644 --- a/agent/openclaw/parser.go +++ b/agent/openclaw/parser.go @@ -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{ @@ -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 { @@ -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 { diff --git a/agent/openclaw/plugin.ts b/agent/openclaw/plugin.ts index 8bd8f43..1ff050d 100644 --- a/agent/openclaw/plugin.ts +++ b/agent/openclaw/plugin.ts @@ -1,6 +1,14 @@ import { execFileSync } from "child_process"; +import { join } from "path"; +import { homedir } from "os"; export default function gryphPlugin(api) { + const transcriptPath = join(homedir(), ".openclaw", "logs", "commands.log"); + + function resolveSessionId(event, ctx) { + return event.sessionId || ctx.sessionKey || ""; + } + function invokeGryph(hookType, payload) { try { execFileSync("__GRYPH_COMMAND__", ["_hook", "openclaw", hookType], { @@ -20,8 +28,9 @@ export default function gryphPlugin(api) { hook_type: "before_tool_call", tool: event.toolName, args: event.params, - session_id: ctx.sessionKey, + session_id: resolveSessionId(event, ctx), agent_id: ctx.agentId, + transcript_path: transcriptPath, }); }); @@ -34,8 +43,9 @@ export default function gryphPlugin(api) { 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 (_) {} }); @@ -44,8 +54,9 @@ export default function gryphPlugin(api) { try { invokeGryph("session_start", { hook_type: "session_start", - session_id: event.sessionId, + session_id: resolveSessionId(event, ctx), agent_id: ctx.agentId, + transcript_path: transcriptPath, }); } catch (_) {} }); @@ -54,10 +65,11 @@ export default function gryphPlugin(api) { try { invokeGryph("session_end", { hook_type: "session_end", - session_id: event.sessionId, + session_id: resolveSessionId(event, ctx), message_count: event.messageCount, duration_ms: event.durationMs, agent_id: ctx.agentId, + transcript_path: transcriptPath, }); } catch (_) {} }); diff --git a/cli/root.go b/cli/root.go index 351e57c..36ebc4f 100644 --- a/cli/root.go +++ b/cli/root.go @@ -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" @@ -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{ diff --git a/test/cli/e2e_hook_test.go b/test/cli/e2e_hook_test.go index 18f1cfa..5542919 100644 --- a/test/cli/e2e_hook_test.go +++ b/test/cli/e2e_hook_test.go @@ -797,3 +797,163 @@ func TestHook_PiAgent_DeterministicSessionID(t *testing.T) { assert.Equal(t, evts[0].SessionID, evts[1].SessionID, "events with same session_id should have same UUID") } + +func TestHook_OpenClaw(t *testing.T) { + tests := []struct { + name string + hookType string + fixture string + assert func(t *testing.T, env *testEnv, stdout, stderr string, err error) + }{ + { + name: "before_tool_call_read", + hookType: "before_tool_call", + fixture: "../../agent/openclaw/testdata/before_tool_call_read.json", + assert: func(t *testing.T, env *testEnv, stdout, stderr string, err error) { + assert.NoError(t, err) + store, cleanup := env.openStore() + defer cleanup() + ctx := context.Background() + evts, qErr := store.QueryEvents(ctx, events.NewEventFilter()) + require.NoError(t, qErr) + assert.Len(t, evts, 1) + assert.Equal(t, events.ActionFileRead, evts[0].ActionType) + p, pErr := evts[0].GetFileReadPayload() + require.NoError(t, pErr) + assert.Equal(t, "/home/user/project/README.md", p.Path) + }, + }, + { + name: "before_tool_call_write", + hookType: "before_tool_call", + fixture: "../../agent/openclaw/testdata/before_tool_call_write.json", + assert: func(t *testing.T, env *testEnv, stdout, stderr string, err error) { + assert.NoError(t, err) + store, cleanup := env.openStore() + defer cleanup() + ctx := context.Background() + evts, qErr := store.QueryEvents(ctx, events.NewEventFilter()) + require.NoError(t, qErr) + assert.Len(t, evts, 1) + assert.Equal(t, events.ActionFileWrite, evts[0].ActionType) + }, + }, + { + name: "before_tool_call_exec", + hookType: "before_tool_call", + fixture: "../../agent/openclaw/testdata/before_tool_call_exec.json", + assert: func(t *testing.T, env *testEnv, stdout, stderr string, err error) { + assert.NoError(t, err) + store, cleanup := env.openStore() + defer cleanup() + ctx := context.Background() + evts, qErr := store.QueryEvents(ctx, events.NewEventFilter()) + require.NoError(t, qErr) + assert.Len(t, evts, 1) + assert.Equal(t, events.ActionCommandExec, evts[0].ActionType) + p, pErr := evts[0].GetCommandExecPayload() + require.NoError(t, pErr) + assert.Equal(t, "npm install", p.Command) + }, + }, + { + name: "after_tool_call_success", + hookType: "after_tool_call", + fixture: "../../agent/openclaw/testdata/after_tool_call_read.json", + assert: func(t *testing.T, env *testEnv, stdout, stderr string, err error) { + assert.NoError(t, err) + store, cleanup := env.openStore() + defer cleanup() + ctx := context.Background() + evts, qErr := store.QueryEvents(ctx, events.NewEventFilter()) + require.NoError(t, qErr) + assert.Len(t, evts, 1) + assert.Equal(t, events.ActionFileRead, evts[0].ActionType) + assert.Equal(t, events.ResultSuccess, evts[0].ResultStatus) + }, + }, + { + name: "after_tool_call_error", + hookType: "after_tool_call", + fixture: "../../agent/openclaw/testdata/after_tool_call_error.json", + assert: func(t *testing.T, env *testEnv, stdout, stderr string, err error) { + assert.NoError(t, err) + store, cleanup := env.openStore() + defer cleanup() + ctx := context.Background() + evts, qErr := store.QueryEvents(ctx, events.NewEventFilter()) + require.NoError(t, qErr) + assert.Len(t, evts, 1) + assert.Equal(t, events.ResultError, evts[0].ResultStatus) + }, + }, + { + name: "session_start", + hookType: "session_start", + fixture: "../../agent/openclaw/testdata/session_start.json", + assert: func(t *testing.T, env *testEnv, stdout, stderr string, err error) { + assert.NoError(t, err) + store, cleanup := env.openStore() + defer cleanup() + ctx := context.Background() + evts, qErr := store.QueryEvents(ctx, events.NewEventFilter()) + require.NoError(t, qErr) + assert.Len(t, evts, 1) + assert.Equal(t, events.ActionSessionStart, evts[0].ActionType) + }, + }, + { + name: "session_end", + hookType: "session_end", + fixture: "../../agent/openclaw/testdata/session_end.json", + assert: func(t *testing.T, env *testEnv, stdout, stderr string, err error) { + assert.NoError(t, err) + store, cleanup := env.openStore() + defer cleanup() + ctx := context.Background() + sessions, sErr := store.QuerySessions(ctx, session.NewSessionFilter()) + require.NoError(t, sErr) + require.Len(t, sessions, 1) + assert.False(t, sessions[0].EndedAt.IsZero(), "EndedAt should be set") + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + env := newTestEnv(t) + payload, err := os.ReadFile(tt.fixture) + require.NoError(t, err) + stdout, stderr, runErr := env.runHook("openclaw", tt.hookType, payload) + tt.assert(t, env, stdout, stderr, runErr) + }) + } +} + +func TestHook_OpenClaw_DeterministicSessionID(t *testing.T) { + env := newTestEnv(t) + + payload1, err := os.ReadFile("../../agent/openclaw/testdata/before_tool_call_read.json") + require.NoError(t, err) + _, _, err = env.runHook("openclaw", "before_tool_call", payload1) + require.NoError(t, err) + + payload2, err := os.ReadFile("../../agent/openclaw/testdata/before_tool_call_exec.json") + require.NoError(t, err) + _, _, err = env.runHook("openclaw", "before_tool_call", payload2) + require.NoError(t, err) + + store, cleanup := env.openStore() + defer cleanup() + ctx := context.Background() + + evts, err := store.QueryEvents(ctx, events.NewEventFilter()) + require.NoError(t, err) + require.Len(t, evts, 2) + + assert.Equal(t, evts[0].SessionID, evts[1].SessionID, + "events with same session_id should have same UUID") + + expected := uuid.NewSHA1(uuid.NameSpaceOID, []byte("openclaw-session-abc123")) + assert.Equal(t, expected, evts[0].SessionID) +} From 13b95c42d17623a08366228de18a735f5f2949f2 Mon Sep 17 00:00:00 2001 From: Abhisek Datta Date: Mon, 23 Mar 2026 22:25:00 +0530 Subject: [PATCH 2/3] fix: Add openclaw plugin manifest --- agent/openclaw/hooks.go | 24 +++++++++++++++++++++++- agent/openclaw/openclaw.plugin.json | 12 ++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 agent/openclaw/openclaw.plugin.json diff --git a/agent/openclaw/hooks.go b/agent/openclaw/hooks.go index 71aff8e..9641be0 100644 --- a/agent/openclaw/hooks.go +++ b/agent/openclaw/hooks.go @@ -15,6 +15,9 @@ import ( //go:embed plugin.ts var pluginTS []byte +//go:embed openclaw.plugin.json +var pluginManifest []byte + func processedPlugin() []byte { return bytes.ReplaceAll(pluginTS, []byte(utils.GryphCommandPlaceholder), []byte(utils.GryphCommand())) } @@ -26,7 +29,10 @@ var HookTypes = []string{ "session_end", } -const pluginFileName = "index.ts" +const ( + pluginFileName = "index.ts" + manifestFileName = "openclaw.plugin.json" +) func pluginDir(extensionsPath string) string { return filepath.Join(extensionsPath, "gryph") @@ -36,6 +42,10 @@ func pluginPath(extensionsPath string) string { return filepath.Join(pluginDir(extensionsPath), pluginFileName) } +func manifestPath(extensionsPath string) string { + return filepath.Join(pluginDir(extensionsPath), manifestFileName) +} + func InstallHooks(ctx context.Context, opts agent.InstallOptions) (*agent.InstallResult, error) { result := &agent.InstallResult{ BackupPaths: make(map[string]string), @@ -106,6 +116,12 @@ 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 + } + result.HooksInstalled = HookTypes result.Success = true return result, nil @@ -201,5 +217,11 @@ 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") + } + return status, nil } diff --git a/agent/openclaw/openclaw.plugin.json b/agent/openclaw/openclaw.plugin.json new file mode 100644 index 0000000..cbb61f5 --- /dev/null +++ b/agent/openclaw/openclaw.plugin.json @@ -0,0 +1,12 @@ +{ + "name": "gryph", + "version": "1.0.0", + "description": "Gryph audit trail plugin for OpenClaw", + "main": "index.ts", + "hooks": [ + "before_tool_call", + "after_tool_call", + "session_start", + "session_end" + ] +} From 38286f2fb5697c15ac4ca159ab153c66a7eab90b Mon Sep 17 00:00:00 2001 From: Abhisek Datta Date: Mon, 23 Mar 2026 22:31:59 +0530 Subject: [PATCH 3/3] fix: Update OpenClaw adapter to use plugin format --- agent/openclaw/hooks.go | 20 +++++ agent/openclaw/openclaw.plugin.json | 16 ++-- agent/openclaw/package.json | 12 +++ agent/openclaw/plugin.ts | 125 +++++++++++++++------------- 4 files changed, 104 insertions(+), 69 deletions(-) create mode 100644 agent/openclaw/package.json diff --git a/agent/openclaw/hooks.go b/agent/openclaw/hooks.go index 9641be0..895a431 100644 --- a/agent/openclaw/hooks.go +++ b/agent/openclaw/hooks.go @@ -18,6 +18,9 @@ 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())) } @@ -32,6 +35,7 @@ var HookTypes = []string{ const ( pluginFileName = "index.ts" manifestFileName = "openclaw.plugin.json" + packageFileName = "package.json" ) func pluginDir(extensionsPath string) string { @@ -46,6 +50,10 @@ 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), @@ -122,6 +130,12 @@ func InstallHooks(ctx context.Context, opts agent.InstallOptions) (*agent.Instal 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 @@ -223,5 +237,11 @@ func GetHookStatus(ctx context.Context) (*agent.HookStatus, error) { 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 } diff --git a/agent/openclaw/openclaw.plugin.json b/agent/openclaw/openclaw.plugin.json index cbb61f5..79e842a 100644 --- a/agent/openclaw/openclaw.plugin.json +++ b/agent/openclaw/openclaw.plugin.json @@ -1,12 +1,8 @@ { - "name": "gryph", - "version": "1.0.0", - "description": "Gryph audit trail plugin for OpenClaw", - "main": "index.ts", - "hooks": [ - "before_tool_call", - "after_tool_call", - "session_start", - "session_end" - ] + "id": "gryph", + "configSchema": { + "type": "object", + "additionalProperties": false, + "properties": {} + } } diff --git a/agent/openclaw/package.json b/agent/openclaw/package.json new file mode 100644 index 0000000..53a811c --- /dev/null +++ b/agent/openclaw/package.json @@ -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" + ] + } +} diff --git a/agent/openclaw/plugin.ts b/agent/openclaw/plugin.ts index 1ff050d..1aea7f5 100644 --- a/agent/openclaw/plugin.ts +++ b/agent/openclaw/plugin.ts @@ -1,76 +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) { - const transcriptPath = join(homedir(), ".openclaw", "logs", "commands.log"); +const PLUGIN_ID = "gryph"; +const transcriptPath = join(homedir(), ".openclaw", "logs", "commands.log"); - function resolveSessionId(event, ctx) { - return event.sessionId || ctx.sessionKey || ""; - } +function resolveSessionId(event, ctx) { + return event.sessionId || ctx.sessionKey || ""; +} - 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"); - } +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("before_tool_call", (event, ctx) => { - invokeGryph("before_tool_call", { - hook_type: "before_tool_call", - tool: event.toolName, - args: event.params, - session_id: resolveSessionId(event, ctx), - agent_id: ctx.agentId, - transcript_path: transcriptPath, - }); - }); - - 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: 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: resolveSessionId(event, ctx), - agent_id: ctx.agentId, - transcript_path: transcriptPath, - }); - } 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: resolveSessionId(event, ctx), - message_count: event.messageCount, - duration_ms: event.durationMs, - agent_id: ctx.agentId, - transcript_path: transcriptPath, - }); - } 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 (_) {} + }); + }, +});